Kritik af protokollen og organisatoriske tilgange til Telegram. Del 1, teknisk: erfaring med at skrive en klient fra bunden - TL, MT

På det seneste er indlæg om, hvor godt Telegram er, hvor geniale og erfarne Durov-brødrene er i at bygge netværkssystemer osv. begyndt at dukke op oftere på Habré. Samtidig er det de færreste, der rigtig har fordybet sig i den tekniske enhed – højst bruger de en ret simpel (og ret anderledes end MTProto) Bot API baseret på JSON, og accepterer som regel bare på tro al den ros og PR, der kredser om budbringeren. For næsten halvandet år siden begyndte min kollega hos Eshelon NGO Vasily (desværre blev hans konto på Habré slettet sammen med udkastet) at skrive sin egen Telegram-klient fra bunden i Perl, og senere sluttede forfatteren til disse linjer sig. Hvorfor Perl, vil nogle straks spørge? Fordi sådanne projekter allerede findes på andre sprog. Faktisk er det ikke meningen, der kunne være et hvilket som helst andet sprog, hvor der ikke er færdiglavet bibliotek, og derfor skal forfatteren gå hele vejen fra bunden. Desuden er kryptografi et spørgsmål om tillid, men bekræft. Med et produkt rettet mod sikkerhed kan du ikke bare stole på et færdiglavet bibliotek fra producenten og blindt stole på det (dette er dog et emne for anden del). I øjeblikket fungerer biblioteket ganske godt på det "gennemsnitlige" niveau (giver dig mulighed for at lave enhver API-anmodning).

Der vil dog ikke være meget kryptografi eller matematik i denne serie af indlæg. Men der vil være mange andre tekniske detaljer og arkitektoniske krykker (også nyttige for dem, der ikke vil skrive fra bunden, men vil bruge biblioteket på ethvert sprog). Så hovedmålet var at forsøge at implementere klienten fra bunden ifølge officiel dokumentation. Det vil sige, lad os antage, at kildekoden for officielle klienter er lukket (igen, i anden del vil vi dække mere detaljeret emnet om, at dette er sandt det sker så), men som i gamle dage, for eksempel, er der en standard som RFC - er det muligt at skrive en klient i henhold til specifikationen alene, "uden at se" på kildekoden, det være sig officiel (Telegram Desktop, mobil), eller uofficiel Telethon?

Indholdsfortegnelse:

Dokumentation... den findes, ikke? Er det sandt?..

Fragmenter af noter til denne artikel begyndte at blive indsamlet sidste sommer. Hele denne tid på den officielle hjemmeside https://core.telegram.org Dokumentationen var fra lag 23, dvs. fast et sted i 2014 (husk, at der ikke engang var kanaler dengang?). Selvfølgelig burde dette i teorien have givet os mulighed for at implementere en klient med funktionalitet på det tidspunkt i 2014. Men selv i denne tilstand var dokumentationen for det første ufuldstændig, og for det andet modsagde den sig selv nogle steder. For godt en måned siden, i september 2019, var det tilfældigt Det blev opdaget, at der var en stor opdatering af dokumentationen på siden, for det helt nye Layer 105, med en bemærkning om, at nu skal alt læses igen. Faktisk blev mange artikler revideret, men mange forblev uændrede. Når du læser nedenstående kritik om dokumentationen, skal du derfor huske på, at nogle af disse ting ikke længere er relevante, men nogle er stadig ret. Efter alt er 5 år i den moderne verden ikke bare lang tid, men meget en masse. Siden dengang (især hvis du ikke tager højde for de kasserede og genoplivede geochat-steder siden da), er antallet af API-metoder i ordningen vokset fra hundrede til mere end to hundrede og halvtreds!

Hvor skal man starte som ung forfatter?

Det er lige meget om du skriver fra bunden eller bruger for eksempel færdige biblioteker som Telethon til Python eller Madeline til PHP, under alle omstændigheder skal du først registrere din ansøgningfå parametre api_id и api_hash (de, der har arbejdet med VKontakte API'en, forstår straks), hvorved serveren identificerer applikationen. Det her skal gør det af juridiske årsager, men vi vil tale mere om, hvorfor biblioteksforfattere ikke kan udgive det i anden del. Du kan være tilfreds med testværdierne, selvom de er meget begrænsede - faktum er, at nu kan du registrere dig kun en app, så skynd dig ikke hovedkulds ud i den.

Nu skulle vi fra et teknisk synspunkt være interesserede i, at vi efter registreringen skulle modtage meddelelser fra Telegram om opdateringer til dokumentation, protokol mv. Det vil sige, man kunne antage, at stedet med havnen simpelthen blev forladt og fortsatte med at arbejde specifikt med dem, der begyndte at lave kunder, fordi det er nemmere. Men nej, intet lignende blev observeret, ingen information kom.

Og hvis du skriver fra bunden, så er det faktisk stadig langt væk at bruge de opnåede parametre. Selvom https://core.telegram.org/ og taler om dem i Kom godt i gang først og fremmest, faktisk skal du først implementere MTProto protokol - men hvis du troede layout efter OSI-modellen sidst på siden for en generel beskrivelse af protokollen, så er det helt forgæves.

Faktisk, både før og efter MTProto, på flere niveauer på én gang (som udenlandske netværkere, der arbejder i OS-kernen siger, lagovertrædelse), vil et stort, smertefuldt og forfærdeligt emne komme i vejen...

Binær serialisering: TL (Type Language) og dets skema, og lag og mange andre skræmmende ord

Dette emne er faktisk nøglen til Telegrams problemer. Og der vil være mange forfærdelige ord, hvis du prøver at dykke ned i det.

Så her er diagrammet. Hvis dette ord dukker op, så sig, JSON-skema, du tænkte rigtigt. Målet er det samme: et sprog til at beskrive et muligt sæt af overførte data. Det er her lighederne slutter. Hvis fra siden MTProto protokol, eller fra kildetræet for den officielle klient, vil vi prøve at åbne et eller andet skema, vi vil se noget som:

int ? = Int;
long ? = Long;
double ? = Double;
string ? = String;

vector#1cb5c415 {t:Type} # [ t ] = Vector t;

rpc_error#2144ca19 error_code:int error_message:string = RpcError;

rpc_answer_unknown#5e2ad36e = RpcDropAnswer;
rpc_answer_dropped_running#cd78e586 = RpcDropAnswer;
rpc_answer_dropped#a43ad8b7 msg_id:long seq_no:int bytes:int = RpcDropAnswer;

msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;

---functions---

set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:bytes = Set_client_DH_params_answer;

ping#7abe77ec ping_id:long = Pong;
ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong;

invokeAfterMsg#cb9f372d msg_id:long query:!X = X;
invokeAfterMsgs#3dc4b4f0 msg_ids:Vector<long> query:!X = X;

account.updateProfile#78515775 flags:# first_name:flags.0?string last_name:flags.1?string about:flags.2?string = User;
account.sendChangePhoneCode#8e57deb flags:# allow_flashcall:flags.0?true phone_number:string current_number:flags.0?Bool = auth.SentCode;

En person, der ser dette for første gang, vil intuitivt kun kunne genkende en del af det, der er skrevet - ja, det er tilsyneladende strukturer (selvom hvor er navnet, til venstre eller til højre?), der er felter i dem, hvorefter en type følger efter et kolon... sandsynligvis. Her i vinkelparentes er der sandsynligvis skabeloner som i C++ (faktisk, ikke rigtig). Og hvad betyder alle de andre symboler, spørgsmålstegn, udråbstegn, procenter, hash-tegn (og tydeligvis betyder de forskellige ting forskellige steder), nogle gange tilstedeværende og nogle gange ikke, hexadecimale tal - og vigtigst af alt, hvordan man kommer fra dette højre (som ikke vil blive afvist af serveren) byte stream? Du skal læse dokumentationen (ja, der er links til skemaet i JSON-versionen i nærheden - men det gør det ikke klarere).

Åbn siden Binær data serialisering og dyk ned i den magiske verden af ​​svampe og diskret matematik, noget der ligner matan på 4. år. Alfabet, type, værdi, kombinator, funktionel kombinator, normal form, sammensat type, polymorf type... og det er alt sammen kun den første side! Næste venter på dig TL sprog, som, selvom den allerede indeholder et eksempel på en triviel anmodning og svar, slet ikke giver et svar på mere typiske tilfælde, hvilket betyder, at du bliver nødt til at vade gennem en genfortælling af matematik oversat fra russisk til engelsk på yderligere otte indlejrede sider!

Læsere, der er fortrolige med funktionelle sprog og automatisk typeslutning, vil selvfølgelig se beskrivelsessproget på dette sprog, selv fra eksemplet, som meget mere fortroligt, og kan sige, at det faktisk ikke er dårligt i princippet. Indsigelserne hertil er:

  • Ja, mål lyder godt, men ak, hun ikke opnået
  • Uddannelsen på russiske universiteter varierer selv blandt it-specialer - ikke alle har taget det tilsvarende kursus
  • Endelig, som vi vil se, er det i praksis ikke påkrævet, da kun en begrænset delmængde af selv den TL, der blev beskrevet, anvendes

Som sagt Leonørd på kanalen #perl i FreeNode IRC-netværket, som forsøgte at implementere en gate fra Telegram til Matrix (oversættelsen af ​​citatet er unøjagtig fra hukommelsen):

Det føles som om nogen blev introduceret til typeteori for første gang, blev begejstrede og begyndte at prøve at lege med det, uden at være ligeglade med, om det var nødvendigt i praksis.

Se selv, om behovet for bare-typer (int, long osv.) som noget elementært ikke rejser spørgsmål - i sidste ende skal de implementeres manuelt - lad os f.eks. tage et forsøg på at udlede fra dem vektor. Det er faktisk, en matrix, hvis du kalder de resulterende ting ved deres rigtige navne.

Men før

En kort beskrivelse af et undersæt af TL-syntaks for dem, der ikke læser den officielle dokumentation

constructor = Type;
myVec ids:Vector<long> = Type;

fixed#abcdef34 id:int = Type2;

fixedVec set:Vector<Type2> = FixedVec;

constructorOne#crc32 field1:int = PolymorType;
constructorTwo#2crc32 field_a:long field_b:Type3 field_c:int = PolymorType;
constructorThree#deadcrc bit_flags_of_what_really_present:# optional_field4:bit_flags_of_what_really_present.1?Type = PolymorType;

an_id#12abcd34 id:int = Type3;
a_null#6789cdef = Type3;

Definition starter altid designer, hvorefter eventuelt (i praksis - altid) gennem symbolet # skal være CRC32 fra den normaliserede beskrivelsesstreng af denne type. Dernæst kommer en beskrivelse af felterne; hvis de findes, kan typen være tom. Alt dette ender med et lighedstegn, navnet på den type, som denne konstruktør - altså i virkeligheden undertypen - tilhører. Fyren til højre for lighedstegnet er polymorf - det vil sige, at flere specifikke typer kan svare til det.

Hvis definitionen kommer efter linjen ---functions---, så vil syntaksen forblive den samme, men betydningen vil være anderledes: konstruktøren bliver navnet på RPC-funktionen, felterne bliver til parametre (det vil sige, den forbliver nøjagtig den samme givne struktur, som beskrevet nedenfor , vil dette blot være den tildelte betydning), og den "polymorfe type" - typen af ​​det returnerede resultat. Sandt nok vil det stadig forblive polymorf - bare defineret i afsnittet ---types---, men denne konstruktør vil "ikke blive taget i betragtning". Overbelastning af typerne af kaldte funktioner ved deres argumenter, dvs. Af en eller anden grund er flere funktioner med samme navn, men forskellige signaturer, som i C++, ikke tilvejebragt i TL.

Hvorfor "konstruktør" og "polymorf", hvis det ikke er OOP? Tja, faktisk vil det være lettere for nogen at tænke over dette i OOP-termer - en polymorf type som en abstrakt klasse, og konstruktører er dens direkte efterkommerklasser, og final i en række sprogs terminologi. Faktisk selvfølgelig kun her lighed med reelle overbelastede konstruktørmetoder i OO-programmeringssprog. Da her kun er datastrukturer, er der ingen metoder (selvom beskrivelsen af ​​funktioner og metoder yderligere er ret i stand til at skabe forvirring i hovedet om, at de findes, men det er en anden sag) - man kan tænke på en konstruktør som en værdi fra hvilken er ved at blive bygget type, når du læser en byte-stream.

Hvordan sker dette? Deserializeren, som altid læser 4 bytes, ser værdien 0xcrc32 - og forstår, hvad der nu vil ske field1 med type int, dvs. læser præcis 4 bytes, herpå det overliggende felt med typen PolymorType Læs. Ser 0x2crc32 og forstår, at der er to felter længere, først long, hvilket betyder, at vi læser 8 bytes. Og så igen en kompleks type, som er deserialiseret på samme måde. For eksempel, Type3 kunne deklareres i kredsløbet, så snart to konstruktører, henholdsvis, så skal de mødes enten 0x12abcd34, hvorefter du skal læse 4 bytes mere intEller 0x6789cdef, hvorefter der ikke kommer noget. Alt andet - du skal kaste en undtagelse. Anyway, efter dette går vi tilbage til at læse 4 bytes int felter field_c в constructorTwo og med det læser vi vores PolymorType.

Endelig, hvis du bliver fanget 0xdeadcrc for constructorThree, så bliver alt mere kompliceret. Vores første felt er bit_flags_of_what_really_present med type # - faktisk er dette kun et alias for typen nat, hvilket betyder "naturligt tal". Det vil sige, at usigneret int i øvrigt er det eneste tilfælde, hvor usignerede tal forekommer i rigtige kredsløb. Så næste er en konstruktion med et spørgsmålstegn, hvilket betyder, at dette felt - det vil kun være til stede på ledningen, hvis den tilsvarende bit er indstillet i det omtalte felt (omtrent som en ternær operator). Så lad os antage, at denne bit blev sat, hvilket betyder, at vi yderligere skal læse et felt som Type, som i vores eksempel har 2 konstruktører. Den ene er tom (består kun af identifikatoren), den anden har et felt ids med type ids:Vector<long>.

Du tror måske, at både skabeloner og generiske produkter er i de professionelle eller Java. Men nej. Næsten. Det her kun tilfælde af brug af vinkelbeslag i rigtige kredsløb, og bruges KUN til Vector. I en byte-stream vil disse være 4 CRC32 bytes for selve Vector-typen, altid de samme, derefter 4 bytes - antallet af array-elementer, og så selve disse elementer.

Læg dertil det faktum, at serialisering altid sker i ord på 4 bytes, alle typer er multipla af det - de indbyggede typer er også beskrevet bytes и string med manuel serialisering af længden og denne justering med 4 - ja, det ser ud til at lyde normalt og endda relativt effektivt? Selvom TL hævdes at være en effektiv binær serialisering, for helvede med dem, med udvidelsen af ​​næsten alt, selv boolske værdier og enkelttegns strenge til 4 bytes, vil JSON stadig være meget tykkere? Se, selv unødvendige felter kan springes over af bit-flag, alt er ganske godt, og endda kan udvides til fremtiden, så hvorfor ikke tilføje nye valgfrie felter til konstruktøren senere?..

Men nej, hvis du ikke læser min korte beskrivelse, men den fulde dokumentation, og tænker over implementeringen. For det første beregnes konstruktorens CRC32 i henhold til den normaliserede linje i tekstbeskrivelsen af ​​skemaet (fjern ekstra mellemrum osv.) - så hvis et nyt felt tilføjes, vil typebeskrivelseslinjen ændre sig, og dermed dens CRC32 og , følgelig serialisering. Og hvad ville den gamle klient gøre, hvis han modtog et felt med nye flag sat, og han ikke ved, hvad han skal gøre med dem næste gang?

For det andet, lad os huske CRC32, som her bruges i det væsentlige som hash -funktioner for entydigt at bestemme, hvilken type der (af)serialiseres. Her står vi med problemet med kollisioner – og nej, sandsynligheden er ikke én ud af 232, men meget større. Hvem huskede, at CRC32 er designet til at opdage (og rette) fejl i kommunikationskanalen og dermed forbedre disse egenskaber til skade for andre? For eksempel er den ligeglad med at omarrangere bytes: Hvis du beregner CRC32 ud fra to linjer, bytter du i den anden de første 4 bytes med de næste 4 bytes - det vil være det samme. Når vores input er tekststrenge fra det latinske alfabet (og lidt tegnsætning), og disse navne ikke er specielt tilfældige, øges sandsynligheden for en sådan omarrangering meget.

Hvem tjekkede forresten, hvad der var der? virkelig CRC32? En af de tidlige kildekoder (selv før Waltman) havde en hash-funktion, der gangede hvert tegn med tallet 239, så elsket af disse mennesker, ha ha!

Til sidst, okay, indså vi, at konstruktører med en felttype Vector<int> и Vector<PolymorType> vil have forskellige CRC32. Hvad med on-line ydeevne? Og fra et teoretisk synspunkt, bliver dette en del af typen? Lad os sige, at vi passerer en række af ti tusinde tal, godt med Vector<int> alt er klart, længden og yderligere 40000 bytes. Hvad hvis dette Vector<Type2>, som kun består af ét felt int og det er alene i typen - skal vi gentage 10000xabcdef0 34 gange og derefter 4 bytes int, eller sproget er i stand til at UAFHÆNGIGT det for os fra konstruktøren fixedVec og i stedet for 80000 bytes, overføre igen kun 40000?

Dette er slet ikke et tomt teoretisk spørgsmål - forestil dig, at du modtager en liste over gruppebrugere, som hver har et id, fornavn, efternavn - forskellen i mængden af ​​data, der overføres via en mobilforbindelse, kan være betydelig. Det er netop effektiviteten af ​​Telegram-serialisering, der annonceres for os.

Så…

Vector, som aldrig blev frigivet

Hvis du forsøger at vade gennem siderne med beskrivelse af kombinatorer og så videre, vil du se, at en vektor (og endda en matrix) formelt forsøger at blive udsendt gennem tuples af flere ark. Men til sidst glemmer de, det sidste trin springes over, og der gives simpelthen en definition af en vektor, som endnu ikke er bundet til en type. Hvad er der galt? På sprog programmering, især funktionelle, er det ret typisk at beskrive strukturen rekursivt - compileren med sin dovne evaluering vil forstå og gøre alt selv. I sproget dataserialisering det, der er brug for, er EFFEKTIVITET: det er nok blot at beskrive Listen, dvs. struktur af to elementer - det første er et dataelement, det andet er selve strukturen eller et tomt rum til halen (pakke (cons) i Lisp). Men dette vil naturligvis kræve hver element bruger yderligere 4 bytes (CRC32 i tilfældet i TL) for at beskrive sin type. Et array kan også nemt beskrives fast størrelse, men i tilfælde af en række af ukendt længde på forhånd, bryder vi af.

Derfor, da TL ikke tillader udlæsning af en vektor, skulle den tilføjes på siden. I sidste ende siger dokumentationen:

Serialisering bruger altid den samme konstruktør "vektor" (const 0x1cb5c415 = crc32("vektor t:Type # [t] = vektor t"), der ikke er afhængig af den specifikke værdi af variablen af ​​typen t.

Værdien af ​​den valgfri parameter t er ikke involveret i serialiseringen, da den er afledt af resultattypen (altid kendt før deserialisering).

Se nærmere: vector {t:Type} # [ t ] = Vector t - men ingen steder Denne definition i sig selv siger ikke, at det første tal skal være lig med vektorens længde! Og det kommer ingen steder fra. Dette er en given ting, der skal huskes og implementeres med dine hænder. Andetsteds nævner dokumentationen endda ærligt, at typen ikke er ægte:

Vector t polymorfe pseudotype er en "type", hvis værdi er en sekvens af værdier af enhver type t, enten indrammet eller blottet.

... men fokuserer ikke på det. Når du, træt af at vade matematikkens stræk (måske endda kendt af dig fra et universitetskursus), beslutter dig for at give op og rent faktisk ser på, hvordan du arbejder med det i praksis, er indtrykket i dit hoved, at dette er seriøst. Matematik i kernen, det blev tydeligt opfundet af Cool People (to matematikere - ACM-vinder), og ikke hvem som helst. Målet - at vise sig frem - er nået.

I øvrigt om antallet. Lad os minde dig om det # det er et synonym nat, naturligt tal:

Der er typeudtryk (type-udtr) og numeriske udtryk (nat-udtr). De er dog defineret på samme måde.

type-expr ::= expr
nat-expr ::= expr

men i grammatikken beskrives de på samme måde, dvs. Denne forskel skal igen huskes og implementeres manuelt.

Nå, ja, skabelontyper (vector<int>, vector<User>) har en fælles identifikator (#1cb5c415), dvs. hvis du ved, at opkaldet annonceres som

users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;

så venter du ikke længere bare på en vektor, men en vektor af brugere. Mere præcist, skal vent - i ægte kode vil hvert element, hvis ikke en blottet type, have en konstruktør, og på en god måde i implementeringen ville det være nødvendigt at kontrollere - men vi blev sendt nøjagtigt i hvert element i denne vektor den type? Hvad hvis det var en slags PHP, hvor et array kan indeholde forskellige typer i forskellige elementer?

På dette tidspunkt begynder du at tænke - er sådan en TL nødvendig? Måske til vognen ville det være muligt at bruge en menneskelig serializer, den samme protobuf som allerede eksisterede dengang? Det var teorien, lad os se på praksis.

Eksisterende TL-implementeringer i kode

TL blev født i dybden af ​​VKontakte selv før de berømte begivenheder med salget af Durovs andel og (sikkert), selv før udviklingen af ​​Telegram begyndte. Og i open source kildekoden til den første implementering du kan finde en masse sjove krykker. Og selve sproget blev implementeret der mere fuldt ud, end det er nu i Telegram. For eksempel bruges hashes slet ikke i skemaet (hvilket betyder en indbygget pseudotype (som en vektor) med afvigende adfærd). Eller

Templates are not used now. Instead, the same universal constructors (for example, vector {t:Type} [t] = Vector t) are used w

men lad os overveje for fuldstændighedens skyld at spore, så at sige, udviklingen af ​​Tankens Kæmpe.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Eller denne smukke:

    static const char *reserved_words_polymorhic[] = {

      "alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", NULL

      };

Dette fragment handler om skabeloner som:

intHash {alpha:Type} vector<coupleInt<alpha>> = IntHash<alpha>;

Dette er definitionen af ​​en hashmap-skabelontype som en vektor af int - Type-par. I C++ ville det se sådan ud:

    template <T> class IntHash {
      vector<pair<int,T>> _map;
    }

så, alpha - nøgleord! Men kun i C++ kan du skrive T, men du skal skrive alfa, beta... Men ikke mere end 8 parametre, det er der fantasien slutter. Det lader til, at der engang i Skt. Petersborg fandt nogle dialoger som denne sted:

-- Надо сделать в TL шаблоны
-- Бл... Ну пусть параметры зовут альфа, бета,... Какие там ещё буквы есть... О, тэта!
-- Грамматика? Ну потом напишем

-- Смотрите, какой я синтаксис придумал для шаблонов и вектора!
-- Ты долбанулся, как мы это парсить будем?
-- Да не ссыте, он там один в схеме, захаркодить -- и ок

Men det drejede sig om den første publicerede implementering af TL "generelt". Lad os gå videre til at overveje implementeringer i Telegram-klienterne selv.

Ord til Vasily:

Vasily, [09.10.18 17:07] Mest af alt er røven varm, fordi de skabte en masse abstraktioner, og derefter hamrede en bolt på dem og dækkede kodegeneratoren med krykker
Som et resultat, først fra dock pilot.jpg
Derefter fra koden dzhekichan.webp

Selvfølgelig, fra folk, der er fortrolige med algoritmer og matematik, kan vi forvente, at de har læst Aho, Ullmann, og er fortrolige med de værktøjer, der er blevet de facto standard i branchen gennem årtier til at skrive deres DSL-compilere, ikke?

Forfatter telegram-cli er Vitaly Valtman, som det kan forstås ud fra forekomsten af ​​TLO-formatet uden for dets (cli) grænser, medlem af teamet - nu er et bibliotek til TL-parsing blevet tildelt særskilt, hvad er indtrykket af hende TL-parser? ..

16.12 04:18 Vasily: Jeg tror, ​​at nogen ikke mestrede lex+yacc
16.12 04:18 Vasily: Jeg kan ikke forklare det anderledes
16.12 04:18 Vasily: Nå, ellers blev de betalt for antallet af linjer i VK
16.12 04:19 Vasily: 3k+ linjer mm<censored> i stedet for en parser

Måske en undtagelse? Lad os se hvordan делает Dette er den OFFICIELLE klient - Telegram Desktop:

    nametype = re.match(r'([a-zA-Z.0-9_]+)(#[0-9a-f]+)?([^=]*)=s*([a-zA-Z.<>0-9_]+);', line);
    if (not nametype):
      if (not re.match(r'vector#1cb5c415 {t:Type} # [ t ] = Vector t;', line)):
         print('Bad line found: ' + line);

1100+ linjer i Python, et par regulære udtryk + specielle tilfælde som en vektor, som selvfølgelig er deklareret i skemaet, som det skal være ifølge TL-syntaksen, men de stolede på denne syntaks til at parse det... Spørgsmålet opstår, hvorfor var det hele et mirakel?иDet er mere lagdelt, hvis ingen alligevel vil analysere det i henhold til dokumentationen?!

Forresten... Kan du huske, at vi talte om CRC32-tjek? Så i Telegram Desktop-kodegeneratoren er der en liste over undtagelser for de typer, hvor den beregnede CRC32 passer ikke sammen med den, der er angivet i diagrammet!

Vasily, [18.12/22 49:XNUMX] og her vil jeg tænke over om sådan en TL er nødvendig
hvis jeg ville rode med alternative implementeringer, ville jeg begynde at indsætte linjeskift, halvdelen af ​​parserne vil bryde på multi-line definitioner
tdesktop dog også

Husk pointen om one-liner, det vender vi tilbage til lidt senere.

Okay, telegram-cli er uofficiel, Telegram Desktop er officiel, men hvad med de andre? Hvem ved?.. I Android-klientkoden var der slet ingen skemaparser (hvilket rejser spørgsmål om open source, men dette er til anden del), men der var flere andre sjove stykker kode, men mere om dem i underafsnit nedenfor.

Hvilke andre spørgsmål rejser serialisering i praksis? For eksempel gjorde de en masse ting, selvfølgelig, med bitfelter og betingede felter:

Vasily: flags.0? true
betyder, at feltet er til stede og er lig med sand, hvis flaget er sat

Vasily: flags.1? int
betyder, at feltet er til stede og skal deserialiseres

Vasily: Røv, du skal ikke bekymre dig om, hvad du laver!
Vasily: Der er en omtale et sted i dokumentet, at sandt er en nul-længde type, men det er umuligt at samle noget fra deres dokument
Vasily: I open source-implementeringerne er dette heller ikke tilfældet, men der er en masse krykker og understøtter

Hvad med Telethon? Ser vi frem til emnet MTProto, et eksempel - i dokumentationen er der sådanne stykker, men tegnet % den beskrives kun som "svarende til en given bar-type", dvs. i eksemplerne nedenfor er der enten en fejl eller noget udokumenteret:

Vasily, [22.06.18 18:38] På ét sted:

msg_container#73f1f8dc messages:vector message = MessageContainer;

I en anden:

msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;

Og det er to store forskelle, i det virkelige liv kommer der en slags nøgen vektor

Jeg har ikke set en blottet vektordefinition og er ikke stødt på en

Analyse er skrevet i hånden i telethon

I hans diagram er definitionen kommenteret ud msg_container

Igen forbliver spørgsmålet om %. Det er ikke beskrevet.

Vadim Goncharov, [22.06.18 19:22] og i tdesktop?

Vasily, [22.06.18 19:23] Men deres TL-parser på almindelige motorer vil højst sandsynligt heller ikke spise dette

// parsed manually

TL er en smuk abstraktion, ingen implementerer den fuldstændigt

Og % er ikke i deres version af ordningen

Men her modsiger dokumentationen sig selv, så idk

Det fandtes i grammatikken, de kunne simpelthen have glemt at beskrive semantikken

Du så dokumentet på TL, du kan ikke finde ud af det uden en halv liter

"Nå, lad os sige," vil en anden læser sige, "du kritiserer noget, så vis mig, hvordan det skal gøres."

Vasily svarer: "Med hensyn til parseren kan jeg godt lide ting som

    args: /* empty */ { $$ = NULL; }
        | args arg { $$ = g_list_append( $1, $2 ); }
        ;

    arg: LC_ID ':' type-term { $$ = tl_arg_new( $1, $3 ); }
            | LC_ID ':' condition '?' type-term { $$ = tl_arg_new_cond( $1, $5, $3 ); free($3); }
            | UC_ID ':' type-term { $$ = tl_arg_new( $1, $3 ); }
            | type-term { $$ = tl_arg_new( "", $1 ); }
            | '[' LC_ID ']' { $$ = tl_arg_new_mult( "", tl_type_new( $2, TYPE_MOD_NONE ) ); }
            ;

på en eller anden måde kan lide det bedre end

struct tree *parse_args4 (void) {
  PARSE_INIT (type_args4);
  struct parse so = save_parse ();
  PARSE_TRY (parse_optional_arg_def);
  if (S) {
    tree_add_child (T, S);
  } else {
    load_parse (so);
  }
  if (LEX_CHAR ('!')) {
    PARSE_ADD (type_exclam);
    EXPECT ("!");
  }
  PARSE_TRY_PES (parse_type_term);
  PARSE_OK;
}

eller

        # Regex to match the whole line
        match = re.match(r'''
            ^                  # We want to match from the beginning to the end
            ([w.]+)           # The .tl object can contain alpha_name or namespace.alpha_name
            (?:
                #             # After the name, comes the ID of the object
                ([0-9a-f]+)    # The constructor ID is in hexadecimal form
            )?                 # If no constructor ID was given, CRC32 the 'tl' to determine it

            (?:s              # After that, we want to match its arguments (name:type)
                {?             # For handling the start of the '{X:Type}' case
                w+            # The argument name will always be an alpha-only name
                :              # Then comes the separator between name:type
                [wd<>#.?!]+  # The type is slightly more complex, since it's alphanumeric and it can
                               # also have Vector<type>, flags:# and flags.0?default, plus :!X as type
                }?             # For handling the end of the '{X:Type}' case
            )*                 # Match 0 or more arguments
            s                 # Leave a space between the arguments and the equal
            =
            s                 # Leave another space between the equal and the result
            ([wd<>#.?]+)     # The result can again be as complex as any argument type
            ;$                 # Finally, the line should always end with ;
            ''', tl, re.IGNORECASE | re.VERBOSE)

dette er HELE lexeren:

    ---functions---         return FUNCTIONS;
    ---types---             return TYPES;
    [a-z][a-zA-Z0-9_]*      yylval.string = strdup(yytext); return LC_ID;
    [A-Z][a-zA-Z0-9_]*      yylval.string = strdup(yytext); return UC_ID;
    [0-9]+                  yylval.number = atoi(yytext); return NUM;
    #[0-9a-fA-F]{1,8}       yylval.number = strtol(yytext+1, NULL, 16); return ID_HASH;

    n                      /* skip new line */
    [ t]+                  /* skip spaces */
    //.*$                 /* skip comments */
    /*.**/              /* skip comments */
    .                       return (int)yytext[0];

de der. enklere er at sige det mildt."

Som et resultat heraf passer parseren og kodegeneratoren for den faktisk anvendte delmængde af TL generelt ind i ca. 100 linjers grammatik og ~300 linjer i generatoren (tæller alle print's genererede kode), inklusive typeinformationsboller til introspektion i hver klasse. Hver polymorf type bliver til en tom abstrakt basisklasse, og konstruktører arver fra den og har metoder til serialisering og deserialisering.

Mangel på typer i typesproget

Stærk skrivning er en god ting, ikke? Nej, dette er ikke en holivar (selvom jeg foretrækker dynamiske sprog), men et postulat inden for rammerne af TL. Baseret på det skal sproget give os alle mulige former for checks. Nå, okay, måske ikke han selv, men implementeringen, men han burde i det mindste beskrive dem. Og hvilke muligheder ønsker vi?

Først og fremmest begrænsninger. Her ser vi i dokumentationen til upload af filer:

Filens binære indhold opdeles derefter i dele. Alle dele skal have samme størrelse ( del_størrelse ) og følgende betingelser skal være opfyldt:

  • part_size % 1024 = 0 (deles med 1KB)
  • 524288 % part_size = 0 (512KB skal være ligeligt deleligt med part_size)

Den sidste del behøver ikke at opfylde disse betingelser, forudsat at dens størrelse er mindre end part_size.

Hver del skal have et sekvensnummer, fil_del, med en værdi fra 0 til 2,999.

Efter at filen er blevet partitioneret, skal du vælge en metode til at gemme den på serveren. Brug upload.saveBigFilePart i tilfælde af at den fulde størrelse af filen er mere end 10 MB og upload.saveFilePart til mindre filer.
[…] en af ​​følgende dataindtastningsfejl kan returneres:

  • FILE_PARTS_INVALID — Ugyldigt antal dele. Værdien er ikke mellem 1..3000

Er noget af dette i diagrammet? Kan dette på en eller anden måde udtrykkes ved hjælp af TL? Ingen. Men undskyld mig, selv bedstefars Turbo Pascal var i stand til at beskrive de specificerede typer intervaller. Og han vidste en ting mere, nu bedre kendt som enum - en type, der består af en opregning af et fast (lille) antal værdier. I sprog som C - numerisk, bemærk, at vi hidtil kun har talt om typer numre. Men der er også arrays, strenge... for eksempel ville det være rart at beskrive, at denne streng kun kan indeholde et telefonnummer, ikke?

Intet af dette er i TL. Men der er for eksempel i JSON Schema. Og hvis en anden kan skændes om deleligheden af ​​512 KB, at dette stadig skal tjekkes i kode, så sørg for at klienten simpelthen kunne ikke sende et nummer uden for rækkevidde 1..3000 (og den tilsvarende fejl kunne ikke være opstået) det ville have været muligt, ikke?..

I øvrigt om fejl og returværdier. Selv dem, der har arbejdet med TL, slører øjnene - det gik ikke umiddelbart op for os hver en funktion i TL kan faktisk ikke kun returnere den beskrevne returtype, men også en fejl. Men dette kan ikke udledes på nogen måde ved hjælp af selve TL. Det er selvfølgelig allerede klart, og der er ikke behov for noget i praksis (selvom RPC faktisk kan udføres på forskellige måder, vi vender tilbage til dette senere) - men hvad med renheden af ​​begreberne Mathematics of Abstract Types fra den himmelske verden?.. Jeg tog slæbebåden op - så match den.

Og endelig, hvad med læsbarheden? Nå, der vil jeg generelt gerne beskrivelse har det rigtigt i skemaet (i JSON-skemaet er det igen), men hvis du allerede er anstrengt med det, hvad så med den praktiske side - i det mindste trivielt at se på forskelle under opdateringer? Se selv kl rigtige eksempler:

channelFull#76af5481 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int = ChatFull;
+channelFull#1c87a71a flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_view_stats:flags.12?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int = ChatFull;

eller

message#44f9b43d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long = Message;
+message#44f9b43d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long = Message;

Det afhænger af alle, men GitHub, for eksempel, nægter at fremhæve ændringer inde i så lange linjer. Spillet “find 10 forskelle”, og hvad hjernen umiddelbart ser er, at begyndelsen og slutningen i begge eksempler er de samme, du skal kedelig læse et sted i midten... Efter min mening er dette ikke kun i teorien, men rent visuelt beskidt og sjusket.

I øvrigt om teoriens renhed. Hvorfor har vi brug for bitfelter? Ser det ikke ud til, at de lugt dårligt set fra typeteoriens synspunkt? Forklaringen kan ses i tidligere versioner af diagrammet. I starten, ja, sådan var det, for hvert nys blev der skabt en ny type. Disse rudimenter eksisterer stadig i denne form, for eksempel:

storage.fileUnknown#aa963b05 = storage.FileType;
storage.filePartial#40bc6f52 = storage.FileType;
storage.fileJpeg#7efe0e = storage.FileType;
storage.fileGif#cae1aadf = storage.FileType;
storage.filePng#a4f63c0 = storage.FileType;
storage.filePdf#ae1e508d = storage.FileType;
storage.fileMp3#528a0677 = storage.FileType;
storage.fileMov#4b09ebbc = storage.FileType;
storage.fileMp4#b3cea0e4 = storage.FileType;
storage.fileWebp#1081464c = storage.FileType;

Men forestil dig nu, hvis du har 5 valgfrie felter i din struktur, så skal du bruge 32 typer for alle mulige muligheder. Kombinatorisk eksplosion. Således knuste TL-teoriens krystalrenhed endnu en gang mod den støbejernsrøv af serialiseringens barske virkelighed.

Derudover overtræder disse fyre nogle steder selv deres egen typologi. For eksempel, i MTProto (næste kapitel) kan responsen komprimeres af Gzip, alt er fint - bortset fra at lagene og kredsløbet er overtrådt. Endnu en gang var det ikke selve RpcResult, der blev høstet, men dets indhold. Tja, hvorfor gøre det her?.. Jeg var nødt til at skære i en krykke, så kompressionen ville virke hvor som helst.

Eller et andet eksempel, vi opdagede engang en fejl - den blev sendt InputPeerUser i stedet for InputUser. Eller omvendt. Men det virkede! Det vil sige, at serveren var ligeglad med typen. Hvordan kan det være? Svaret kan gives til os af kodefragmenter fra telegram-cli:

  if (tgl_get_peer_type (E->id) != TGL_PEER_CHANNEL || (C && (C->flags & TGLCHF_MEGAGROUP))) {
    out_int (CODE_messages_get_history);
    out_peer_id (TLS, E->id);
  } else {    
    out_int (CODE_channels_get_important_history);

    out_int (CODE_input_channel);
    out_int (tgl_get_peer_id (E->id));
    out_long (E->id.access_hash);
  }
  out_int (E->max_id);
  out_int (E->offset);
  out_int (E->limit);
  out_int (0);
  out_int (0);

Med andre ord, det er her serialisering udføres MANUELT, ikke genereret kode! Måske er serveren implementeret på lignende måde?.. I princippet vil dette fungere, hvis det gøres én gang, men hvordan kan det understøttes senere under opdateringer? Er det derfor, ordningen blev opfundet? Og her går vi videre til næste spørgsmål.

Versionering. Lag

Hvorfor de skematiske versioner kaldes lag, kan kun spekuleres ud fra historien om offentliggjorte skemaer. Tilsyneladende troede forfatterne først, at grundlæggende ting kunne gøres ved hjælp af den uændrede ordning, og kun hvor det var nødvendigt, for specifikke anmodninger, angiver de, at de blev gjort ved hjælp af en anden version. I princippet endda en god idé - og det nye bliver så at sige "blandet", lagt oven på det gamle. Men lad os se, hvordan det blev gjort. Sandt nok var jeg ikke i stand til at se på det fra begyndelsen - det er sjovt, men diagrammet over basislaget eksisterer simpelthen ikke. Lag startede med 2. Dokumentationen fortæller os om en speciel TL-funktion:

Hvis en klient understøtter Layer 2, skal følgende konstruktør bruges:

invokeWithLayer2#289dd1f6 {X:Type} query:!X = X;

I praksis betyder det, at før hvert API-kald, en int med værdien 0x289dd1f6 skal tilføjes før metodenummeret.

Lyder normalt. Men hvad skete der så? Så dukkede op

invokeWithLayer3#b7475268 query:!X = X;

Så hvad er det næste? Som du måske kan gætte,

invokeWithLayer4#dea0d430 query:!X = X;

Sjov? Nej, det er for tidligt at grine, tænk på det hver en anmodning fra et andet lag skal pakkes ind i sådan en speciel type - hvis de alle er forskellige for dig, hvordan kan du ellers skelne dem? Og at tilføje kun 4 bytes foran er en ret effektiv metode. Så,

invokeWithLayer5#417a57ae query:!X = X;

Men det er indlysende, at efter et stykke tid vil dette blive en slags bacchanalia. Og løsningen kom:

Opdatering: Starter med Layer 9, hjælpemetoder invokeWithLayerN kan kun bruges sammen med initConnection

Hurra! Efter 9 versioner kom vi endelig til, hvad der blev gjort i internetprotokoller tilbage i 80'erne - blev enige om versionen én gang i begyndelsen af ​​forbindelsen!

Så hvad er det næste?..

invokeWithLayer10#39620c41 query:!X = X;
...
invokeWithLayer18#1c900537 query:!X = X;

Men nu kan du stadig grine. Først efter yderligere 9 lag blev der endelig tilføjet en universel konstruktør med et versionsnummer, som kun skal kaldes én gang i begyndelsen af ​​forbindelsen, og meningen med lagene syntes at være forsvundet, nu er det bare en betinget version, som f.eks. alle andre steder. Problem løst.

Nemlig?..

Vasily, [16.07.18 14:01] Selv i fredags tænkte jeg:
Teleserveren sender begivenheder uden anmodning. Anmodninger skal pakkes ind i InvokeWithLayer. Serveren ombryder ikke opdateringer; der er ingen struktur for ombrydning af svar og opdateringer.

De der. klienten kan ikke angive det lag, hvori han ønsker opdateringer

Vadim Goncharov, [16.07.18 14:02] er InvokeWithLayer i princippet ikke en krykke?

Vasily, [16.07.18 14:02] Dette er den eneste måde

Vadim Goncharov, [16.07.18 14:02] hvilket i det væsentlige burde betyde, at man bliver enige om laget i begyndelsen af ​​sessionen

Det følger i øvrigt, at klientnedgradering ikke leveres

Opdateringer, dvs. type Updates i skemaet er det, hvad serveren sender til klienten, ikke som svar på en API-anmodning, men uafhængigt, når en hændelse opstår. Dette er et komplekst emne, som vil blive diskuteret i et andet indlæg, men indtil videre er det vigtigt at vide, at serveren gemmer opdateringer, selv når klienten er offline.

Således, hvis du nægter at pakke ind hver pakke for at angive dens version, dette fører logisk til følgende mulige problemer:

  • serveren sender opdateringer til klienten, selv før klienten har informeret om, hvilken version den understøtter
  • hvad skal jeg gøre efter at have opgraderet klienten?
  • der garantierat serverens mening om lagnummeret ikke vil ændre sig under processen?

Tror du, det er rent teoretisk spekulation, og i praksis kan det ikke ske, fordi serveren er skrevet korrekt (i det mindste er den testet godt)? Ha! Uanset hvordan det er!

Det er præcis, hvad vi løb ind i i august. Den 14. august var der beskeder om, at noget var ved at blive opdateret på Telegram-serverne... og derefter i loggene:

2019-08-15 09:28:35.880640 MSK warn  main: ANON:87: unknown object type: 0x80d182d1 at TL/Object.pm line 213.
2019-08-15 09:28:35.751899 MSK warn  main: ANON:87: unknown object type: 0xb5223b0f at TL/Object.pm line 213.

og så flere megabyte stakspor (nå, samtidig blev logningen rettet). Når alt kommer til alt, hvis noget ikke genkendes i din TL, er det binært ved signatur, længere nede af linjen ALLE går, bliver afkodning umulig. Hvad skal du gøre i sådan en situation?

Nå, det første, der falder nogen i tankerne, er at afbryde forbindelsen og prøve igen. hjalp ikke. Vi googler CRC32 - det viste sig at være objekter fra skema 73, selvom vi arbejdede på 82. Vi ser nøje på loggene - der er identifikatorer fra to forskellige skemaer!

Måske ligger problemet udelukkende i vores uofficielle klient? Nej, vi lancerer Telegram Desktop 1.2.17 (versionen leveres i en række Linux-distributioner), den skriver til undtagelsesloggen: MTP Unexpected type id #b5223b0f read in MTPMessageMedia...

Kritik af protokollen og organisatoriske tilgange til Telegram. Del 1, teknisk: erfaring med at skrive en klient fra bunden - TL, MT

Google viste, at et lignende problem allerede var sket for en af ​​de uofficielle klienter, men så var versionsnumrene og dermed antagelserne anderledes...

Så hvad skal vi gøre? Vasily og jeg gik fra hinanden: han forsøgte at opdatere kredsløbet til 91, jeg besluttede at vente et par dage og prøve på 73. Begge metoder virkede, men da de er empiriske, er der ingen forståelse for, hvor mange versioner op eller ned du har brug for at hoppe, eller hvor længe du skal vente .

Senere var jeg i stand til at genskabe situationen: vi starter klienten, slukker den, kompilerer kredsløbet til et andet lag, genstarter, fanger problemet igen, vender tilbage til den forrige - ups, ingen mængde kredsløbsskift og klienten genstarter i en få minutter vil hjælpe. Du vil modtage en blanding af datastrukturer fra forskellige lag.

Forklaring? Som du kan gætte ud fra forskellige indirekte symptomer, består serveren af ​​mange processer af forskellige typer på forskellige maskiner. Mest sandsynligt satte den server, der er ansvarlig for at "buffere", i køen, hvad dens overordnede gav den, og de gav det i den ordning, der var på plads på tidspunktet for genereringen. Og indtil denne kø er "rådden", kunne der ikke gøres noget ved det.

Måske... men det her er en frygtelig krykke?!.. Nej, før vi tænker på skøre ideer, lad os se på koden for de officielle kunder. I Android-versionen finder vi ikke nogen TL-parser, men vi finder en heftig fil (GitHub nægter at røre ved den) med (af)serialisering. Her er kodestykkerne:

public static class TL_message_layer68 extends TL_message {
    public static int constructor = 0xc09be45f;
//...
//еще пачка подобных
//...
    public static class TL_message_layer47 extends TL_message {
        public static int constructor = 0xc992e15c;
        public static Message TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) {
            Message result = null;
            switch (constructor) {
                case 0x1d86f70e:
                    result = new TL_messageService_old2();
                    break;
                case 0xa7ab1991:
                    result = new TL_message_old3();
                    break;
                case 0xc3060325:
                    result = new TL_message_old4();
                    break;
                case 0x555555fa:
                    result = new TL_message_secret();
                    break;
                case 0x555555f9:
                    result = new TL_message_secret_layer72();
                    break;
                case 0x90dddc11:
                    result = new TL_message_layer72();
                    break;
                case 0xc09be45f:
                    result = new TL_message_layer68();
                    break;
                case 0xc992e15c:
                    result = new TL_message_layer47();
                    break;
                case 0x5ba66c13:
                    result = new TL_message_old7();
                    break;
                case 0xc06b9607:
                    result = new TL_messageService_layer48();
                    break;
                case 0x83e5de54:
                    result = new TL_messageEmpty();
                    break;
                case 0x2bebfa86:
                    result = new TL_message_old6();
                    break;
                case 0x44f9b43d:
                    result = new TL_message_layer104();
                    break;
                case 0x1c9b1027:
                    result = new TL_message_layer104_2();
                    break;
                case 0xa367e716:
                    result = new TL_messageForwarded_old2(); //custom
                    break;
                case 0x5f46804:
                    result = new TL_messageForwarded_old(); //custom
                    break;
                case 0x567699b3:
                    result = new TL_message_old2(); //custom
                    break;
                case 0x9f8d60bb:
                    result = new TL_messageService_old(); //custom
                    break;
                case 0x22eb6aba:
                    result = new TL_message_old(); //custom
                    break;
                case 0x555555F8:
                    result = new TL_message_secret_old(); //custom
                    break;
                case 0x9789dac4:
                    result = new TL_message_layer104_3();
                    break;

eller

    boolean fixCaption = !TextUtils.isEmpty(message) &&
    (media instanceof TLRPC.TL_messageMediaPhoto_old ||
     media instanceof TLRPC.TL_messageMediaPhoto_layer68 ||
     media instanceof TLRPC.TL_messageMediaPhoto_layer74 ||
     media instanceof TLRPC.TL_messageMediaDocument_old ||
     media instanceof TLRPC.TL_messageMediaDocument_layer68 ||
     media instanceof TLRPC.TL_messageMediaDocument_layer74)
    && message.startsWith("-1");

Hmm... ser vildt ud. Men sandsynligvis, dette er genereret kode, så okay?.. Men det understøtter bestemt alle versioner! Sandt nok er det ikke klart, hvorfor alt er blandet sammen, hemmelige chats og alt muligt _old7 ligner på en eller anden måde ikke maskingenerering... Dog blev jeg mest af alt blæst bagover

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Gutter, kan I ikke engang bestemme, hvad der er inde i et lag?! Nå, okay, lad os sige, at "to" blev udgivet med en fejl, ja, det sker, men TRE?.. Med det samme samme rake igen? Hvad er det for en pornografi, undskyld?

I kildekoden til Telegram Desktop sker der i øvrigt en lignende ting - hvis det er tilfældet, ændrer flere commits i træk til skemaet ikke dets lagnummer, men retter noget. I forhold, hvor der ikke er nogen officiel kilde til data for ordningen, hvor kan de så hentes fra, bortset fra den officielle klients kildekode? Og tager man det derfra, kan man ikke være sikker på, at skemaet er helt korrekt, før man tester alle metoderne.

Hvordan kan dette overhovedet testes? Jeg håber, at fans af enheds-, funktionelle og andre tests vil dele i kommentarerne.

Okay, lad os se på et andet stykke kode:

public static class TL_folders_deleteFolder extends TLObject {
    public static int constructor = 0x1c295881;

    public int folder_id;

    public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) {
        return Updates.TLdeserialize(stream, constructor, exception);
    }

    public void serializeToStream(AbstractSerializedData stream) {
        stream.writeInt32(constructor);
        stream.writeInt32(folder_id);
    }
}

//manually created

//RichText start
public static abstract class RichText extends TLObject {
    public String url;
    public long webpage_id;
    public String email;
    public ArrayList<RichText> texts = new ArrayList<>();
    public RichText parentRichText;

    public static RichText TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) {
        RichText result = null;
        switch (constructor) {
            case 0x1ccb966a:
                result = new TL_textPhone();
                break;
            case 0xc7fb5e01:
                result = new TL_textSuperscript();
                break;

Denne kommentar "manuelt oprettet" antyder, at kun en del af denne fil blev skrevet manuelt (kan du forestille dig hele vedligeholdelsesmareridtet?), og resten var maskingenereret. Men så opstår et andet spørgsmål - at kilderne er tilgængelige ikke helt (a la GPL-klatter i Linux-kernen), men dette er allerede et emne for anden del.

Men nok. Lad os gå videre til protokollen, hvorpå al denne serialisering kører.

MT Proto

Så lad os åbne generel beskrivelse и detaljeret beskrivelse af protokollen og det første vi snubler over er terminologien. Og med en overflod af alt. Generelt ser dette ud til at være en proprietær funktion af Telegram - at kalde ting forskelligt forskellige steder, eller forskellige ting med ét ord, eller omvendt (f.eks. i en API på højt niveau, hvis du ser en mærkatpakke, er det ikke hvad du troede).

For eksempel betyder "besked" og "session" noget andet her end i den sædvanlige Telegram-klientgrænseflade. Nå, alt er klart med beskeden, den kan fortolkes i OOP-termer eller blot kaldes ordet "pakke" - dette er et lavt transportniveau, der er ikke de samme beskeder som i grænsefladen, der er mange servicemeddelelser . Men sessionen... men først.

transportlag

Det første er transport. De vil fortælle os om 5 muligheder:

  • TCP
  • Websocket
  • Websocket over HTTPS
  • HTTP
  • HTTPS

Vasily, [15.06.18 15:04] Der er også UDP-transport, men det er ikke dokumenteret

Og TCP i tre varianter

Den første ligner UDP over TCP, hver pakke indeholder et sekvensnummer og crc
Hvorfor er det så smertefuldt at læse dokumenter på en vogn?

Nå, der er det nu TCP allerede i 4 varianter:

  • forkortet
  • Mellem
  • Polstret mellem
  • Fuld

Nå, ok, polstret mellemliggende for MTProxy, dette blev senere tilføjet på grund af velkendte begivenheder. Men hvorfor to versioner mere (tre i alt), når man kunne klare sig med én? Alle fire adskiller sig i det væsentlige kun i, hvordan man indstiller længden og nyttelasten af ​​hoved-MTProto, som vil blive diskuteret yderligere:

  • i Forkortet er det 1 eller 4 bytes, men ikke 0xef, så kroppen
  • i Intermediate er dette 4 bytes længde og et felt, og første gang klienten skal sende 0xeeeeeeee for at angive, at det er mellemliggende
  • i Fuld det mest vanedannende, set fra en netværkers synspunkt: længde, sekvensnummer, og IKKE DEN, der hovedsageligt er MTProto, body, CRC32. Ja, alt dette er oven på TCP. Hvilket giver os pålidelig transport i form af en sekventiel bytestrøm; ingen sekvenser er nødvendige, især kontrolsummer. Okay, nu vil nogen indvende mod mig, at TCP har en 16-bit kontrolsum, så datakorruption sker. Fantastisk, men vi har faktisk en kryptografisk protokol med hashes længere end 16 bytes, alle disse fejl - og endnu flere - vil blive fanget af et SHA-mismatch på et højere niveau. Der er INGEN mening i CRC32 oven i dette.

Lad os sammenligne Abridged, hvor en byte i længden er mulig, med Intermediate, som retfærdiggør "I tilfælde af, at 4-byte datajustering er nødvendig", hvilket er noget vrøvl. Hvad, det menes, at Telegram-programmører er så inkompetente, at de ikke kan læse data fra en socket ind i en tilpasset buffer? Du skal stadig gøre dette, fordi læsning kan returnere et hvilket som helst antal bytes (og der er også proxy-servere, for eksempel...). Eller på den anden side, hvorfor blokere Abridged, hvis vi stadig vil have heftig polstring oven på 16 bytes - spar 3 bytes undertiden ?

Man får det indtryk, at Nikolai Durov virkelig godt kan lide at genopfinde hjul, inklusive netværksprotokoller, uden noget egentligt praktisk behov.

Andre transportmuligheder, inkl. Web og MTProxy vil vi ikke overveje nu, måske i et andet indlæg, hvis der er en anmodning. Om den samme MTProxy, lad os først huske nu, at kort efter udgivelsen i 2018 lærte udbydere hurtigt at blokere den, beregnet til bypass blokeringVed pakke størrelse! Og også det faktum, at MTProxy-serveren skrevet (igen af ​​Waltman) i C var alt for bundet til Linux-specifikationer, selvom dette slet ikke var påkrævet (Phil Kulin vil bekræfte), og at en lignende server enten i Go eller Node.js ville passer i mindre end hundrede linjer.

Men vi vil drage konklusioner om disse menneskers tekniske færdigheder i slutningen af ​​afsnittet efter at have overvejet andre spørgsmål. For nu, lad os gå videre til OSI lag 5, session - som de placerede MTProto session på.

Nøgler, beskeder, sessioner, Diffie-Hellman

De placerede det der ikke helt korrekt... En session er ikke den samme session, som er synlig i interfacet under Aktive sessioner. Men i rækkefølge.

Kritik af protokollen og organisatoriske tilgange til Telegram. Del 1, teknisk: erfaring med at skrive en klient fra bunden - TL, MT

Så vi modtog en bytestreng af kendt længde fra transportlaget. Dette er enten en krypteret besked eller almindelig tekst - hvis vi stadig er på nøgleaftalestadiet og faktisk gør det. Hvilket af de begreber, der kaldes "nøgle", taler vi om? Lad os afklare dette problem for Telegram-teamet selv (jeg undskylder for at have oversat min egen dokumentation fra engelsk med en træt hjerne kl. 4 om morgenen, det var lettere at lade nogle sætninger være som de er):

Der er to enheder kaldet Session - en i brugergrænsefladen for officielle klienter under "aktuelle sessioner", hvor hver session svarer til en hel enhed / OS.
Den anden - MTProto session, som har meddelelsens sekvensnummer (i et lavt niveau) i sig, og som kan vare mellem forskellige TCP-forbindelser. Flere MTProto-sessioner kan installeres på samme tid, for eksempel for at fremskynde download af filer.

Mellem disse to sessioner der er et koncept tilladelse. I det degenererede tilfælde kan vi sige det UI session er det samme som tilladelse, men ak, alt er kompliceret. Lad os se:

  • Brugeren på den nye enhed genererer først auth_key og binder det til konto, for eksempel via SMS - det er derfor tilladelse
  • Det skete inde i den første MTProto session, som har session_id inde i dig selv.
  • På dette trin, kombinationen tilladelse и session_id kunne kaldes instans - dette ord forekommer i dokumentationen og koden for nogle klienter
  • Derefter kan klienten åbne flere MTProto sessioner under samme auth_key - til samme DC.
  • Så en dag skal klienten anmode om filen fra en anden DC - og til denne DC vil der blive genereret en ny auth_key !
  • At informere systemet om, at det ikke er en ny bruger, der tilmelder sig, men den samme tilladelse (UI session), bruger klienten API-kald auth.exportAuthorization i hjemmet DC auth.importAuthorization i det nye DC.
  • Alt er det samme, flere kan være åbne MTProto sessioner (hver med sin egen session_id) til denne nye DC, under hans auth_key.
  • Endelig kan klienten ønske Perfect Forward Secrecy. Hver auth_key Det var permanent nøgle - pr DC - og klienten kan ringe auth.bindTempAuthKey til brug midlertidig auth_key - og igen kun én temp_auth_key pr. DC, fælles for alle MTProto sessioner til denne DC.

Bemærk, at salt (og fremtidige salte) er også en på auth_key de der. delt mellem alle MTProto sessioner til samme DC.

Hvad betyder "mellem forskellige TCP-forbindelser"? Så dette betyder noget som autorisationscookie på en hjemmeside - den bevarer (overlever) mange TCP-forbindelser til en given server, men en dag går den dårlig. Kun i modsætning til HTTP, i MTProto bliver meddelelser inden for en session sekventielt nummereret og bekræftet; hvis de kom ind i tunnelen, blev forbindelsen afbrudt - efter etablering af en ny forbindelse, vil serveren venligst sende alt i denne session, som den ikke leverede i den forrige TCP forbindelse.

Ovenstående oplysninger er dog opsummeret efter mange måneders undersøgelse. I mellemtiden implementerer vi vores klient fra bunden? - lad os gå tilbage til begyndelsen.

Så lad os generere auth_keyDiffie-Hellman versioner fra Telegram. Lad os prøve at forstå dokumentationen...

Vasily, [19.06.18 20:05] data_with_hash := SHA1(data) + data + (enhver tilfældig bytes); sådan, at længden er lig med 255 bytes;
krypteret_data := RSA(data_med_hash, server_offentlig_nøgle); et 255-byte langt tal (big endian) hæves til den nødvendige magt over det nødvendige modul, og resultatet gemmes som et 256-byte tal.

De har noget dope DH

Ligner ikke en sund persons DH
Der er ikke to offentlige nøgler i dx

Nå, i sidste ende blev dette ordnet, men en rest var tilbage - bevis på arbejdet udføres af klienten, at han var i stand til at faktorisere antallet. Type beskyttelse mod DoS-angreb. Og RSA-nøglen bruges kun én gang i én retning, hovedsagelig til kryptering new_nonce. Men selvom denne tilsyneladende simple operation vil lykkes, hvad skal du så stå over for?

Vasily, [20.06.18/00/26 XNUMX:XNUMX] Jeg er ikke nået til den appid anmodning endnu

Jeg sendte denne anmodning til DH

Og i transportdokken står der, at den kan reagere med 4 bytes af en fejlkode. Det er alt

Nå, han fortalte mig -404, hvad så?

Så jeg sagde til ham: "Fang dit bullshit krypteret med en servernøgle med et fingeraftryk som dette, jeg vil have DH," og det svarede med en dum 404

Hvad ville du synes om dette serversvar? Hvad skal man gøre? Der er ingen at spørge (men mere om det i anden del).

Her klares al interesse på kajen

Jeg har ikke andet at lave, jeg drømte bare om at konvertere tal frem og tilbage

To 32 bit numre. Jeg pakkede dem ligesom alle andre

Men nej, disse to skal først føjes til linjen som BE

Vadim Goncharov, [20.06.18 15:49] og på grund af dette 404?

Vasily, [20.06.18 15:49] JA!

Vadim Goncharov, [20.06.18 15:50] så jeg forstår ikke, hvad han "ikke fandt"

Vasily, [20.06.18 15:50] cirka

Jeg kunne ikke finde en sådan nedbrydning i primære faktorer %)

Vi klarede ikke engang fejlrapportering

Vasily, [20.06.18 20:18] Åh, der er også MD5. Allerede tre forskellige hashes

Nøglefingeraftrykket beregnes som følger:

digest = md5(key + iv)
fingerprint = substr(digest, 0, 4) XOR substr(digest, 4, 4)

SHA1 og sha2

Så lad os sige det auth_key vi modtog 2048 bits i størrelse ved hjælp af Diffie-Hellman. Hvad er det næste? Dernæst opdager vi, at de nederste 1024 bit af denne nøgle ikke bruges på nogen måde ... men lad os tænke over dette for nu. På dette trin har vi en delt hemmelighed med serveren. En analog af TLS-sessionen er blevet etableret, hvilket er en meget dyr procedure. Men serveren ved stadig ikke noget om, hvem vi er! Ikke endnu, faktisk. bemyndigelse. De der. hvis du tænkte i termer af "login-password", som du engang gjorde i ICQ, eller i det mindste "login-key", som i SSH (for eksempel på nogle gitlab/github). Vi fik en anonym. Hvad hvis serveren fortæller os "disse telefonnumre betjenes af en anden DC"? Eller endda "dit telefonnummer er forbudt"? Det bedste, vi kan gøre, er at beholde nøglen i håb om, at den vil være nyttig og ikke vil rådne til den tid.

Vi "modtog" den i øvrigt med forbehold. Har vi for eksempel tillid til serveren? Hvad hvis det er falsk? Kryptografiske kontroller ville være nødvendige:

Vasily, [21.06.18 17:53] De tilbyder mobilklienter at tjekke et 2kbit-nummer for primalitet%)

Men det er slet ikke klart, nafeijoa

Vasily, [21.06.18 18:02] Dokumentet siger ikke, hvad man skal gøre, hvis det viser sig ikke at være enkelt

Ikke sagt. Lad os se, hvad den officielle Android-klient gør i dette tilfælde? EN det er hvad (og ja, hele filen er interessant) - som de siger, så lader jeg bare dette ligge her:

278     static const char *goodPrime = "c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d930f48198a0aa7c14058229493d22530f4dbfa336f6e0ac925139543aed44cce7c3720fd51f69458705ac68cd4fe6b6b13abdc9746512969328454f18faf8c595f642477fe96bb2a941d5bcd1d4ac8cc49880708fa9b378e3c4f3a9060bee67cf9a4a4a695811051907e162753b56b0f6b410dba74d8a84b2a14b3144e0ef1284754fd17ed950d5965b4b9dd46582db1178d169c6bc465b0d6ff9ca3928fef5b9ae4e418fc15e83ebea0f87fa9ff5eed70050ded2849f47bf959d956850ce929851f0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5b";
279   if (!strcasecmp(prime, goodPrime)) {

Nej, selvfølgelig er den der stadig nogle Der er test for et tals primalitet, men personligt har jeg ikke længere tilstrækkelig viden om matematik.

Okay, vi har hovednøglen. For at logge ind, dvs. sende anmodninger, skal du udføre yderligere kryptering ved hjælp af AES.

Meddelelsesnøglen er defineret som de 128 midterste bits af SHA256 i meddelelseslegemet (inklusive session, meddelelses-id osv.), inklusive udfyldningsbytes, foranstillet af 32 bytes taget fra autorisationsnøglen.

Vasily, [22.06.18 14:08] Gennemsnit, tæve, bidder

Fik auth_key. Alle. Ud over dem ... fremgår det ikke af dokumentet. Du er velkommen til at studere open source-koden.

Bemærk, at MTProto 2.0 kræver fra 12 til 1024 bytes udfyldning, stadig under forudsætning af, at den resulterende meddelelseslængde er delelig med 16 bytes.

Så hvor meget polstring skal du tilføje?

Og ja, der er også en 404 i tilfælde af en fejl

Hvis nogen omhyggeligt studerede diagrammet og teksten i dokumentationen, bemærkede de, at der ikke er nogen MAC der. Og at AES bruges i en bestemt IGE-tilstand, som ikke bruges andre steder. De skriver selvfølgelig om dette i deres FAQ... Her er selve beskednøglen også SHA-hashen af ​​de dekrypterede data, der bruges til at tjekke integriteten - og i tilfælde af mismatch, dokumentationen af ​​en eller anden grund anbefaler lydløst at ignorere dem (men hvad med sikkerhed, hvad hvis de knækker os?).

Jeg er ikke en kryptograf, måske er der ikke noget galt med denne tilstand i dette tilfælde set fra et teoretisk synspunkt. Men jeg kan klart nævne et praktisk problem ved at bruge Telegram Desktop som eksempel. Den krypterer den lokale cache (alle disse D877F783D5D3EF8C) på samme måde som beskeder i MTProto (kun i dette tilfælde version 1.0), dvs. først beskednøglen, derefter selve dataene (og et eller andet sted til side de vigtigste store auth_key 256 bytes, uden hvilke msg_key ubrugelig). Så problemet bliver mærkbart på store filer. Du skal nemlig beholde to kopier af dataene - krypteret og dekrypteret. Og hvis der er megabyte, eller streaming video, for eksempel?.. Klassiske skemaer med MAC efter chifferteksten giver dig mulighed for at læse den stream, straks transmittere den. Men med MTProto bliver du nødt til det i første omgang krypter eller dekrypter hele beskeden, først derefter overføre den til netværket eller til disken. Derfor, i de nyeste versioner af Telegram Desktop i cachen i user_data Et andet format bruges også - med AES i CTR-tilstand.

Vasily, [21.06.18 01:27] Åh, jeg fandt ud af, hvad IGE er: IGE var det første forsøg på en "autentificeringskrypteringstilstand", oprindeligt for Kerberos. Det var et mislykket forsøg (det giver ikke integritetsbeskyttelse) og måtte fjernes. Det var begyndelsen på en 20-årig søgen efter en autentificeringskrypteringstilstand, der fungerer, som for nylig kulminerede i tilstande som OCB og GCM.

Og nu argumenterne fra vognsiden:

Holdet bag Telegram, ledet af Nikolai Durov, består af seks ACM-mestre, halvdelen af ​​dem Ph.D'er i matematik. Det tog dem omkring to år at udrulle den nuværende version af MTProto.

Det er sjovt. To år på lavere niveau

Eller du kunne bare tage tls

Okay, lad os sige, at vi har lavet krypteringen og andre nuancer. Er det endelig muligt at sende forespørgsler serialiseret i TL og deserialisere svarene? Så hvad og hvordan skal du sende? Her, lad os sige, metoden initConnection, måske er det det?

Vasily, [25.06.18 18:46] Initialiserer forbindelse og gemmer information på brugerens enhed og applikation.

Den accepterer app_id, device_model, system_version, app_version og lang_code.

Og nogle forespørgsler

Dokumentation som altid. Du er velkommen til at studere open source

Hvis alt var nogenlunde klart med invokeWithLayer, hvad er der så galt her? Det viser sig, lad os sige, at vi har - klienten havde allerede noget at spørge serveren om - der er en anmodning, som vi ønskede at sende:

Vasily, [25.06.18 19:13] At dømme efter koden er det første opkald pakket ind i dette lort, og selve lortet er pakket ind i invokewithlayer

Hvorfor kunne initConnection ikke være et separat opkald, men skal være en indpakning? Ja, som det viste sig, skal det gøres hver gang i begyndelsen af ​​hver session, og ikke én gang, som med hovednøglen. Men! Det kan ikke kaldes af en uautoriseret bruger! Nu er vi nået til det stadie, hvor det er anvendeligt Denne dokumentationsside - og den fortæller os, at...

Kun en lille del af API-metoderne er tilgængelige for uautoriserede brugere:

  • auth.sendCode
  • auth.resendCode
  • account.getPassword
  • auth.checkPassword
  • auth.checkTelefon
  • auth.signUp
  • auth.signIn
  • auth.importAuthorization
  • help.getConfig
  • help.getNearestDc
  • help.getAppUpdate
  • help.getCdnConfig
  • langpack.getLangPack
  • langpack.getStrings
  • langpack.getDifference
  • langpack.getLanguages
  • langpack.getLanguage

Den allerførste af dem, auth.sendCode, og der er den elskede første anmodning, hvor vi sender api_id og api_hash, og hvorefter vi modtager en SMS med en kode. Og hvis vi er i den forkerte DC (telefonnumre her i landet betjenes f.eks. af en anden), så får vi en fejl med nummeret på den ønskede DC. Hjælp os for at finde ud af, hvilken IP-adresse efter DC-nummer du skal oprette forbindelse til help.getConfig. På et tidspunkt var der kun 5 tilmeldinger, men efter de berømte begivenheder i 2018 er antallet steget markant.

Lad os nu huske, at vi nåede til dette stadie på serveren anonymt. Er det ikke for dyrt bare at få en IP-adresse? Hvorfor ikke gøre dette og andre operationer i den ukrypterede del af MTProto? Jeg hører indvendingen: "hvordan kan vi sikre, at det ikke er RKN, der vil svare med falske adresser?" Til dette husker vi, at i almindelighed officielle kunder RSA-nøgler er indlejret, dvs. kan du bare skilt denne information. Faktisk bliver dette allerede gjort for information om at omgå blokering, som klienter modtager gennem andre kanaler (logisk set kan dette ikke gøres i selve MTProto; du skal også vide, hvor du skal oprette forbindelse).

OKAY. På dette stadium af klientautorisation er vi endnu ikke autoriseret og har ikke registreret vores ansøgning. Vi vil bare lige nu se, hvad serveren reagerer på metoder, der er tilgængelige for en uautoriseret bruger. Og her…

Vasily, [10.07.18 14:45] https://core.telegram.org/method/help.getConfig

config#7dae33e0 [...] = Config;
help.getConfig#c4f9186b = Config;

https://core.telegram.org/api/datacenter

config#232d5905 [...] = Config;
help.getConfig#c4f9186b = Config;

I ordningen kommer først i anden række

I tdesktop-skemaet er den tredje værdi

Ja, siden da er dokumentationen selvfølgelig blevet opdateret. Selvom det snart kan blive irrelevant igen. Hvordan skal en nybegynder udvikler vide det? Måske vil de informere dig, hvis du registrerer din ansøgning? Vasily gjorde dette, men desværre sendte de ham ikke noget (igen, vi taler om dette i anden del).

...Du har bemærket, at vi allerede på en eller anden måde er flyttet til API'en, dvs. til næste niveau, og gik glip af noget i MTProto-emnet? Ingen overraskelse:

Vasily, [28.06.18 02:04] Mm, de roder gennem nogle af algoritmerne på e2e

Mtproto definerer krypteringsalgoritmer og nøgler for begge domæner, samt lidt af en indpakningsstruktur

Men de blander konstant forskellige niveauer af stakken, så det er ikke altid klart, hvor mtproto sluttede og det næste niveau begyndte

Hvordan blandes de? Nå, her er den samme midlertidige nøgle til PFS, for eksempel (forresten, Telegram Desktop kan ikke gøre det). Det udføres af en API-anmodning auth.bindTempAuthKey, dvs. fra øverste niveau. Men det forstyrrer samtidig kryptering på det lavere niveau - efter det skal du for eksempel gøre det igen initConnection osv., dette er ikke bare normal anmodning. Det specielle er også, at du kun kan have EN midlertidig nøgle pr. DC, selvom feltet auth_key_id i hver besked giver dig mulighed for at ændre nøglen i det mindste hver besked, og at serveren har ret til at "glemme" den midlertidige nøgle til enhver tid - dokumentationen siger ikke, hvad man skal gøre i dette tilfælde... ja, hvorfor kunne man har du ikke flere nøgler, som med et sæt fremtidige salte, og ?..

Der er et par andre ting, der er værd at bemærke om MTProto-temaet.

Meddelelsesbeskeder, msg_id, msg_seqno, bekræftelser, pings i den forkerte retning og andre idiosynkrasier

Hvorfor har du brug for at vide om dem? Fordi de "lækker" til et højere niveau, og du skal være opmærksom på dem, når du arbejder med API'en. Lad os antage, at vi ikke er interesserede i msg_key; det lavere niveau har dekrypteret alt for os. Men inde i de dekrypterede data har vi følgende felter (også længden af ​​dataene, så vi ved, hvor udfyldningen er, men det er ikke vigtigt):

  • salt - int64
  • session_id - int64
  • message_id — int64
  • seq_no - int32

Lad os minde dig om, at der kun er ét salt for hele DC. Hvorfor vide om hende? Ikke kun fordi der er en anmodning get_future_salts, som fortæller dig hvilke intervaller der vil være gyldige, men også fordi hvis dit salt er "råddent", så vil beskeden (anmodningen) simpelthen gå tabt. Serveren vil selvfølgelig rapportere det nye salt ved at udstede new_session_created - men med den gamle bliver du nødt til at sende den igen på en eller anden måde, f.eks. Og dette problem påvirker applikationsarkitekturen.

Serveren har lov til at droppe sessioner helt og reagere på denne måde af mange årsager. Hvad er egentlig en MTProto-session fra klientsiden? Det er to tal session_id и seq_no beskeder i denne session. Nå, og den underliggende TCP-forbindelse, selvfølgelig. Lad os sige, at vores klient stadig ikke ved, hvordan man gør mange ting, han afbrød forbindelsen og genoprettede forbindelsen. Hvis dette skete hurtigt - den gamle session fortsatte i den nye TCP-forbindelse, øg seq_no yderligere. Hvis det tager lang tid, kan serveren slette det, for på sin side er det også en kø, som vi fandt ud af.

Hvad skal det være seq_no? Åh, det er et vanskeligt spørgsmål. Prøv ærligt at forstå, hvad der menes:

Indholdsrelateret besked

En meddelelse, der kræver en eksplicit bekræftelse. Disse omfatter alle bruger- og mange servicemeddelelser, stort set alle med undtagelse af containere og kvitteringer.

Meddelelsessekvensnummer (msg_seqno)

Et 32-bit tal svarende til det dobbelte af antallet af "indholdsrelaterede" meddelelser (dem, der kræver bekræftelse, og især dem, der ikke er containere), oprettet af afsenderen før denne meddelelse og efterfølgende øget med én, hvis den aktuelle meddelelse er en indholdsrelateret besked. En beholder genereres altid efter hele dens indhold; derfor er dens sekvensnummer større end eller lig med sekvensnumrene på meddelelserne indeholdt i den.

Hvad er det for et cirkus med en stigning på 1, og så en anden med 2?.. Jeg formoder, at de oprindeligt betød "den mindst signifikante bit for ACK, resten er et tal", men resultatet er ikke helt det samme - især, det kommer ud, kan sendes flere bekræftelser, der har det samme seq_no! Hvordan? Nå, for eksempel sender serveren os noget, sender det, og vi forbliver selv tavse, og svarer kun med servicebeskeder, der bekræfter modtagelsen af ​​dens beskeder. I dette tilfælde vil vores udgående bekræftelser have samme udgående nummer. Hvis du er bekendt med TCP og tænkte, at det på en eller anden måde lyder vildt, men det virker ikke særlig vildt, for i TCP seq_no ændres ikke, men bekræftelsen går til seq_no på den anden side vil jeg skynde mig at forarge dig. Bekræftelser gives i MTProto IKKEseq_no, som i TCP, men ved msg_id !

Hvad er dette msg_id, det vigtigste af disse felter? En unik besked-id, som navnet antyder. Det er defineret som et 64-bit tal, hvoraf de laveste bits igen har "server-ikke-server"-magien, og resten er et Unix-tidsstempel, inklusive brøkdelen, forskudt 32 bit til venstre. De der. tidsstempel i sig selv (og meddelelser med tider, der afviger for meget, vil blive afvist af serveren). Heraf viser det sig, at der generelt er tale om en identifikator, der er global for klienten. Givet det - lad os huske session_id - vi er garanteret: Under ingen omstændigheder kan en besked beregnet til én session sendes til en anden session. Det vil sige, det viser sig, at der allerede er tre niveau - session, sessionsnummer, besked-id. Hvorfor sådan overkomplikation, dette mysterium er meget stort.

således msg_id brug for...

RPC: anmodninger, svar, fejl. Bekræftelser.

Som du måske har bemærket, er der ingen speciel "lave en RPC-anmodning"-type eller funktion nogen steder i diagrammet, selvom der er svar. Vi har trods alt indholdsrelaterede beskeder! Det er, nogen beskeden kunne være en anmodning! Eller ikke at være. Trods alt, hver Der er msg_id. Men der er svar:

rpc_result#f35c6d01 req_msg_id:long result:Object = RpcResult;

Det er her, det er angivet, hvilken besked dette er et svar på. Derfor skal du på det øverste niveau af API'en huske, hvad nummeret på din forespørgsel var - jeg tror ikke, der er behov for at forklare, at arbejdet er asynkront, og der kan være flere anmodninger i gang på samme tid, svarene, som kan returneres i vilkårlig rækkefølge? I princippet, fra dette og fejlmeddelelser som ingen arbejdere, kan arkitekturen bag dette spores: serveren, der opretholder en TCP-forbindelse med dig, er en front-end balancer, den videresender anmodninger til backends og samler dem tilbage via message_id. Det ser ud til, at alt her er klart, logisk og godt.

Ja?.. Og hvis du tænker over det? Selve RPC-svaret har jo også et felt msg_id! Behøver vi at råbe til serveren "du svarer ikke på mit svar!"? Og ja, hvad var der om konfirmationer? Om side beskeder om beskeder fortæller os, hvad der er

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

og det skal gøres ved hver side. Men ikke altid! Hvis du har modtaget et RpcResult, fungerer det selv som en bekræftelse. Det vil sige, at serveren kan svare på din anmodning med MsgsAck - som "Jeg har modtaget den." RpcResult kan reagere med det samme. Det kunne være begge dele.

Og ja, du skal stadig svare på svaret! Bekræftelse. Ellers vil serveren anse det for ikke at kunne leveres og sende det tilbage til dig igen. Selv efter genforbindelse. Men her opstår selvfølgelig spørgsmålet om timeouts. Lad os se på dem lidt senere.

Lad os i mellemtiden se på mulige forespørgselsudførelsesfejl.

rpc_error#2144ca19 error_code:int error_message:string = RpcError;

Åh, vil nogen udbryde, her er et mere humant format – der er en streg! Tag dig god tid. Her liste over fejl, men selvfølgelig ikke komplet. Af den lærer vi, at koden er noget som HTTP-fejl (selvfølgelig respekteres semantikken af ​​svarene ikke, nogle steder er de fordelt tilfældigt blandt koderne), og linjen ser ud som CAPITAL_LETTERS_AND_NUMBERS. For eksempel PHONE_NUMBER_OCCUPIED eller FILE_PART_Х_MISSING. Nå, det vil sige, du skal stadig bruge denne linje parse. For eksempel FLOOD_WAIT_3600 vil betyde, at du skal vente en time, og PHONE_MIGRATE_5, at et telefonnummer med dette præfiks skal registreres i 5. DC. Vi har et typesprog, ikke? Vi har ikke brug for et argument fra en streng, det gør almindelige, okay.

Igen, dette er ikke på siden med servicemeddelelser, men som det allerede er sædvanligt med dette projekt, kan informationen findes på en anden dokumentationsside. Eller kaste mistanke. For det første, se, tastning/lagovertrædelse - RpcError kan indlejres RpcResult. Hvorfor ikke udenfor? Hvad tog vi ikke højde for?.. Derfor, hvor er garantien for det RpcError må IKKE indlejres i RpcResult, men være direkte eller indlejret i en anden type?.. Og hvis den ikke kan, hvorfor er den så ikke på øverste niveau, dvs. den mangler req_msg_id ? ..

Men lad os fortsætte med servicemeddelelser. Klienten tror måske, at serveren tænker i lang tid og fremsætter denne vidunderlige anmodning:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Der er tre mulige svar på dette spørgsmål, igen krydsende med bekræftelsesmekanismen; forsøg på at forstå, hvad de skal være (og hvad den generelle liste over typer, der ikke kræver bekræftelse) overlades til læseren som hjemmearbejde (bemærk: informationen i Telegram Desktop-kildekoden er ikke komplet).

Narkotikamisbrug: beskedstatusser

Generelt efterlader mange steder i TL, MTProto og Telegram generelt en følelse af stædighed, men af ​​høflighed, takt og andre bløde værdier Vi tav høfligt om det, og censurerede uanstændigheden i dialogerne. Men dette stedОdet meste af siden handler om beskeder om beskeder Det er chokerende selv for mig, der har arbejdet med netværksprotokoller i lang tid og har set cykler af forskellig grad af skævhed.

Det starter uskyldigt, med bekræftelser. Dernæst fortæller de os om

bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int error_code:int = BadMsgNotification;
bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int error_code:int new_server_salt:long = BadMsgNotification;

Nå, alle, der begynder at arbejde med MTProto, bliver nødt til at håndtere dem; i den "korrigerede - genkompilerede - lancerede" cyklus er det en almindelig ting at få talfejl eller salt, der har formået at gå dårligt under redigeringer. Der er dog to punkter her:

  1. Det betyder, at den oprindelige besked går tabt. Vi skal lave nogle køer, det ser vi på senere.
  2. Hvad er disse mærkelige fejlnumre? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64... hvor er de andre tal, Tommy?

I dokumentationen står der:

Hensigten er, at fejlkodeværdier grupperes (error_code >> 4): for eksempel svarer koderne 0x40 — 0x4f til fejl i beholdernedbrydning.

men for det første et skift i den anden retning, og for det andet er det lige meget, hvor er de andre koder? I forfatterens hoved?.. Det er dog bagateller.

Afhængighed begynder i beskeder om beskedstatusser og beskedkopier:

  • Anmodning om meddelelsesstatusoplysninger
    Hvis en af ​​parterne ikke har modtaget information om status for sine udgående meddelelser i et stykke tid, kan den udtrykkeligt anmode den anden part om det:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Informationsmeddelelse om status for meddelelser
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Her, info er en streng, der indeholder præcis én byte af meddelelsesstatus for hver meddelelse fra den indgående msg_ids-liste:

    • 1 = intet er kendt om beskeden (msg_id for lavt, den anden part kan have glemt det)
    • 2 = besked ikke modtaget (msg_id falder inden for rækkevidden af ​​lagrede identifikatorer, men den anden part har bestemt ikke modtaget en sådan besked)
    • 3 = besked ikke modtaget (msg_id for høj, men den anden part har bestemt ikke modtaget den endnu)
    • 4 = besked modtaget (bemærk, at dette svar samtidig er en kvittering for modtagelse)
    • +8 = besked allerede bekræftet
    • +16 = besked, der ikke kræver bekræftelse
    • +32 = RPC-forespørgsel indeholdt i meddelelsen, der behandles, eller behandlingen er allerede fuldført
    • +64 = indholdsrelateret svar på besked, der allerede er genereret
    • +128 = den anden part ved med sikkerhed, at beskeden allerede er modtaget
      Dette svar kræver ikke en bekræftelse. Det er en anerkendelse af den relevante msgs_state_req i sig selv.
      Bemærk, at hvis det pludselig viser sig, at den anden part ikke har en besked, der ser ud til at være sendt til sig, kan beskeden blot sendes igen. Selvom den anden part skulle modtage to kopier af beskeden på samme tid, vil duplikatet blive ignoreret. (Hvis der er gået for lang tid, og det originale msg_id ikke længere er gyldigt, skal meddelelsen pakkes ind i msg_copy).
  • Frivillig kommunikation af status for meddelelser
    Hver af parterne kan frivilligt informere den anden part om status for de meddelelser, der er transmitteret af den anden part.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Udvidet frivillig kommunikation af status for én besked
    ...
    msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long bytes:int status:int = MsgDetailedInfo;
    msg_new_detailed_info#809db6df answer_msg_id:long bytes:int status:int = MsgDetailedInfo;
  • Eksplicit anmodning om at gensende meddelelser
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    Fjernparten svarer straks ved at gensende de anmodede meddelelser […]
  • Eksplicit anmodning om at gensende svar
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    Fjernparten svarer straks ved at sende igen svar til de anmodede beskeder […]
  • Besked kopier
    I nogle situationer skal en gammel besked med et msg_id, der ikke længere er gyldigt, sendes igen. Derefter pakkes det ind i en kopibeholder:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Når den er modtaget, behandles beskeden, som om indpakningen ikke var der. Men hvis det er kendt med sikkerhed, at meddelelsen orig_message.msg_id blev modtaget, behandles den nye meddelelse ikke (mens den og orig_message.msg_id samtidig bekræftes). Værdien af ​​orig_message.msg_id skal være lavere end containerens msg_id.

Lad os endda tie stille om hvad msgs_state_info igen stikker ørerne på den ufærdige TL ud (vi havde brug for en vektor af bytes, og i de nederste to bits var der en enum, og i de højere to bits var der flag). Pointen er en anden. Er der nogen der forstår hvorfor alt dette er i praksis? i en rigtig klient nødvendigt?.. Med besvær, men man kan forestille sig nogle fordele, hvis en person er engageret i debugging, og i en interaktiv tilstand - spørg serveren hvad og hvordan. Men her er anmodningerne beskrevet rundtur.

Det følger heraf, at hver part ikke kun skal kryptere og sende beskeder, men også gemme data om sig selv, om svarene på dem, i et ukendt tidsrum. Dokumentationen beskriver hverken tidspunkterne eller den praktiske anvendelighed af disse funktioner. på ingen måde. Det mest fantastiske er, at de faktisk bruges i koden for officielle kunder! Tilsyneladende fik de at vide noget, som ikke var med i den offentlige dokumentation. Forstå ud fra koden hvorfor, er ikke længere så simpelt som i tilfældet med TL - det er ikke en (relativt) logisk isoleret del, men en brik knyttet til applikationsarkitekturen, dvs. vil kræve betydeligt mere tid at forstå applikationskoden.

Pings og timings. Køer.

Hvis vi husker gættene om serverarkitekturen (distribution af anmodninger på tværs af backends), følger en ret trist ting - trods alle leveringsgarantierne i TCP (enten bliver data leveret, eller også bliver du informeret om bruddet, men dataene vil blive leveret, indtil problemet opstår), at bekræftelser i selve MTProto - ingen garantier. Serveren kan nemt miste eller smide din besked ud, og der kan ikke gøres noget ved det, bare brug forskellige typer krykker.

Og først og fremmest - beskedkøer. Nå, med én ting var alt indlysende lige fra begyndelsen - en ubekræftet besked skal gemmes og sendes igen. Og efter hvilket tidspunkt? Og narren kender ham. Måske løser disse afhængige servicemeddelelser på en eller anden måde dette problem med krykker, f.eks. i Telegram Desktop er der omkring 4 køer svarende til dem (måske flere, som allerede nævnt, for dette skal du dykke mere seriøst ind i dets kode og arkitektur; samtidig tid, vi Vi ved, at det ikke kan tages som en prøve; et vist antal typer fra MTProto-skemaet bruges ikke i det).

Hvorfor sker dette? Sandsynligvis var serverprogrammørerne ude af stand til at sikre pålidelighed i klyngen, eller endda buffering på frontbalanceren, og overførte dette problem til klienten. Af fortvivlelse forsøgte Vasily at implementere en alternativ mulighed, med kun to køer, ved hjælp af algoritmer fra TCP - måling af RTT til serveren og justering af størrelsen af ​​"vinduet" (i beskeder) afhængigt af antallet af ubekræftede anmodninger. Det vil sige, at sådan en grov heuristik til at vurdere serverens belastning er, hvor mange af vores anmodninger den kan tygge på samme tid og ikke tabe.

Nå, det vil du forstå, ikke? Hvis du skal implementere TCP igen oven på en protokol, der kører over TCP, indikerer dette en meget dårligt designet protokol.

Åh ja, hvorfor har du brug for mere end én kø, og hvad betyder det alligevel for en person, der arbejder med en API på højt niveau? Se, du laver en anmodning, serialiserer den, men ofte kan du ikke sende den med det samme. Hvorfor? Fordi svaret bliver msg_id, som er midlertidigаJeg er en etiket, hvis tildeling bedst udskydes til så sent som muligt - i tilfælde af at serveren afviser det på grund af tidsforstyrrelser mellem os og ham (selvfølgelig kan vi lave en krykke, der flytter vores tid fra nuet til serveren ved at tilføje et delta beregnet ud fra serverens svar - officielle klienter gør dette, men det er råt og unøjagtigt på grund af buffering). Derfor, når du foretager en anmodning med et lokalt funktionskald fra biblioteket, går meddelelsen gennem følgende trin:

  1. Den ligger i én kø og venter på kryptering.
  2. Udpeget msg_id og beskeden gik til en anden kø - mulig videresendelse; sendes til stikkontakten.
  3. a) Serveren svarede MsgsAck - beskeden blev leveret, vi sletter den fra "anden kø".
    b) Eller omvendt, han kunne ikke lide noget, svarede han badmsg - send igen fra "en anden kø"
    c) Intet vides, beskeden skal sendes igen fra en anden kø - men det vides ikke præcist hvornår.
  4. Serveren svarede endelig RpcResult - selve svaret (eller fejlen) - ikke bare leveret, men også behandlet.

Måske, kan brugen af ​​containere delvist løse problemet. Dette er, når en masse beskeder er pakket i én, og serveren svarede med en bekræftelse på dem alle på én gang, i én msg_id. Men han vil også afvise denne pakke, hvis noget gik galt, i sin helhed.

Og på dette tidspunkt spiller ikke-tekniske overvejelser ind. Erfaringsmæssigt har vi set mange krykker, og derudover vil vi nu se flere eksempler på dårlig rådgivning og arkitektur – er det i sådanne forhold værd at stole på og træffe sådanne beslutninger? Spørgsmålet er retorisk (selvfølgelig ikke).

Hvad taler vi om? Hvis du stadig kan spekulere på emnet "narkobeskeder om beskeder" med indvendinger som "du er dum, du forstod ikke vores geniale plan!" (så skriv dokumentationen først, som normale mennesker skal, med begrundelse og eksempler på pakkeudveksling, så snakkes vi), så er timings/timeouts et rent praktisk og specifikt spørgsmål, alt her har været kendt længe. Hvad fortæller dokumentationen os om timeouts?

En server anerkender normalt modtagelsen af ​​en meddelelse fra en klient (normalt en RPC-forespørgsel) ved hjælp af et RPC-svar. Hvis der kommer et svar i lang tid, kan en server først sende en kvitteringsbekræftelse og noget senere selve RPC-svaret.

En klient anerkender normalt modtagelsen af ​​en meddelelse fra en server (normalt et RPC-svar) ved at tilføje en bekræftelse til den næste RPC-forespørgsel, hvis den ikke transmitteres for sent (hvis den genereres f.eks. 60-120 sekunder efter modtagelsen af en besked fra serveren). Men hvis der i en længere periode ikke er grund til at sende beskeder til serveren, eller hvis der er et stort antal ikke-bekræftede beskeder fra serveren (f.eks. over 16), sender klienten en selvstændig bekræftelse.

... jeg oversætter: vi selv ved ikke, hvor meget og hvordan vi har brug for det, så lad os antage, at lad det være sådan her.

Og om ping:

Ping-beskeder (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

Et svar returneres normalt til den samme forbindelse:

pong#347773c5 msg_id:long ping_id:long = Pong;

Disse meddelelser kræver ikke bekræftelse. En pong transmitteres kun som svar på et ping, mens et ping kan startes af begge sider.

Udskudt lukning af forbindelse + PING

ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong;

Virker som ping. Efter at dette er modtaget, starter serveren desuden en timer, som lukker den aktuelle forbindelse disconnect_delay sekunder senere, medmindre den modtager en ny besked af samme type, som automatisk nulstiller alle tidligere timere. Hvis klienten f.eks. sender disse ping en gang hvert 60. sekund, kan den indstille disconnect_delay lig med 75 sekunder.

Er du skør?! Om 60 sekunder vil toget ind på stationen, afsætte og samle passagerer op og igen miste kontakten i tunnelen. Om 120 sekunder, mens du hører den, vil den ankomme til en anden, og forbindelsen vil højst sandsynligt bryde. Nå, det er tydeligt, hvor benene kommer fra - "Jeg hørte en ringetone, men ved ikke, hvor den er", der er Nagls algoritme og muligheden TCP_NODELAY, beregnet til interaktivt arbejde. Men undskyld mig, hold fast i dens standardværdi - 200 Millisekunder Hvis du virkelig ønsker at afbilde noget lignende og spare på et par mulige pakker, så udskyd det i 5 sekunder, eller hvad "Brugeren skriver..."-meddelelsestimeout nu er. Men ikke mere.

Og til sidst, pings. Det vil sige at kontrollere TCP-forbindelsens livlighed. Det er sjovt, men for omkring 10 år siden skrev jeg en kritisk tekst om budbringeren på vores fakultets kollegium - forfatterne der pingede også serveren fra klienten og ikke omvendt. Men 3. års studerende er én ting, og et internationalt kontor er en anden, ikke?..

Først et lille pædagogisk program. En TCP-forbindelse kan, i mangel af pakkeudveksling, leve i uger. Dette er både godt og skidt, afhængigt af formålet. Det er godt, hvis du havde en SSH-forbindelse åben til serveren, du rejste dig fra computeren, genstartede routeren, vendte tilbage til dit sted - sessionen gennem denne server blev ikke revet (du skrev ikke noget, der var ingen pakker) , det er praktisk. Det er dårligt, hvis der er tusindvis af klienter på serveren, som hver optager ressourcer (hej, Postgres!), og klientens vært kan have genstartet for længe siden - men vi ved ikke om det.

Chat/IM-systemer falder ind i det andet tilfælde af en yderligere årsag - onlinestatusser. Hvis brugeren "faldt af", skal du informere hans samtalepartnere om dette. Ellers vil du få den fejl, som skaberne af Jabber lavede (og rettet i 20 år) - brugeren har afbrudt forbindelsen, men de fortsætter med at skrive beskeder til ham i den tro, at han er online (som også gik helt tabt på disse få minutter før afbrydelsen blev opdaget). Nej, muligheden TCP_KEEPALIVE, som mange mennesker, der ikke forstår, hvordan TCP-timere fungerer, skubber overalt (ved at indstille vilde værdier som titusvis af sekunder), vil ikke hjælpe her - du skal sørge for, at ikke kun OS-kernen af brugerens maskine er i live, men fungerer også normalt, i stand til at reagere, og selve applikationen (tror du, den ikke kan fryse? Telegram Desktop på Ubuntu 18.04 frøs for mig mere end én gang).

Derfor skal du pinge server klient, og ikke omvendt - hvis klienten gør dette, hvis forbindelsen er brudt, vil ping ikke blive leveret, målet vil ikke blive nået.

Hvad ser vi på Telegram? Det er præcis det modsatte! Nå, altså. Formelt set kan begge sider selvfølgelig pinge hinanden. I praksis bruger klienter en krykke ping_delay_disconnect, som indstiller timeren på serveren. Nå, undskyld mig, det er ikke op til klienten at bestemme, hvor længe han vil bo der uden ping. Serveren ved, baseret på dens belastning, bedre. Men, selvfølgelig, hvis du ikke har noget imod ressourcerne, så vil du være din egen onde Pinocchio, og en krykke vil gøre det...

Hvordan skulle det have været designet?

Jeg mener, at ovenstående fakta klart indikerer, at Telegram/VKontakte-teamet ikke er særlig kompetent inden for transport (og lavere) niveau af computernetværk og deres lave kvalifikationer i relevante spørgsmål.

Hvorfor viste det sig at være så kompliceret, og hvordan kan Telegram-arkitekter forsøge at gøre indsigelse? Det faktum, at de forsøgte at lave en session, der overlever TCP-forbindelsesbrud, dvs. det, der ikke blev leveret nu, leverer vi senere. De forsøgte sikkert også at lave en UDP-transport, men de stødte på vanskeligheder og opgav den (det er derfor, dokumentationen er tom - der var ikke noget at prale af). Men på grund af manglende forståelse for, hvordan netværk i almindelighed og TCP i særdeleshed fungerer, hvor man kan stole på det, og hvor man selv skal gøre det (og hvordan), og et forsøg på at kombinere dette med kryptografi “two birds with one stone”, dette er resultatet.

Hvordan var det nødvendigt? Baseret på det faktum, at msg_id er et tidsstempel, der er nødvendigt fra et kryptografisk synspunkt for at forhindre gentagelsesangreb, er det en fejl at knytte en unik identifikationsfunktion til den. Derfor, uden at ændre den nuværende arkitektur fundamentalt (når opdateringsstrømmen genereres, er det et API-emne på højt niveau for en anden del af denne serie af indlæg), ville man være nødt til at:

  1. Serveren, der holder TCP-forbindelsen til klienten, påtager sig ansvaret - hvis den har læst fra socket, bedes du anerkende, behandle eller returnere en fejl, uden tab. Så er bekræftelsen ikke en vektor af id'er, men blot "den sidst modtagne seq_no" - bare et tal, som i TCP (to tal - din seq og den bekræftede). Vi er altid inden for sessionen, er vi ikke?
  2. Tidsstemplet for at forhindre gentagelsesangreb bliver et separat felt, a la nonce. Det er tjekket, men påvirker ikke andet. Nok og uint32 - hvis vores salt ændres mindst hver halve dag, kan vi allokere 16 bits til lavordensbits af en heltal del af den aktuelle tid, resten - til en brøkdel af et sekund (som nu).
  3. Fjernet msg_id overhovedet - ud fra synspunktet om at skelne anmodninger på backends, er der for det første klient-id'et, og for det andet session-id'et, sammenkæde dem. Derfor er kun én ting tilstrækkelig som en anmodnings-id seq_no.

Dette er heller ikke den mest succesfulde mulighed; en komplet tilfældig kan tjene som en identifikator - dette er allerede gjort i high-level API, når du sender en besked, forresten. Det ville være bedre helt at lave arkitekturen om fra relativ til absolut, men dette er et emne for en anden del, ikke dette indlæg.

API?

Ta-daam! Så efter at have kæmpet gennem en sti fuld af smerte og krykker, var vi endelig i stand til at sende enhver anmodning til serveren og modtage alle svar på dem, samt modtage opdateringer fra serveren (ikke som svar på en anmodning, men den selv sender os, gerne PUSH, hvis nogen er det tydeligere på den måde).

Bemærk, nu vil der være det eneste eksempel i Perl i artiklen! (for dem, der ikke er bekendt med syntaksen, er det første argument for velsignelse objektets datastruktur, det andet er dets klasse):

2019.10.24 12:00:51 $1 = {
'cb' => 'TeleUpd::__ANON__',
'out' => bless( {
'filter' => bless( {}, 'Telegram::ChannelMessagesFilterEmpty' ),
'channel' => bless( {
'access_hash' => '-6698103710539760874',
'channel_id' => '1380524958'
}, 'Telegram::InputPeerChannel' ),
'pts' => '158503',
'flags' => 0,
'limit' => 0
}, 'Telegram::Updates::GetChannelDifference' ),
'req_id' => '6751291954012037292'
};
2019.10.24 12:00:51 $1 = {
'in' => bless( {
'req_msg_id' => '6751291954012037292',
'result' => bless( {
'pts' => 158508,
'flags' => 3,
'final' => 1,
'new_messages' => [],
'users' => [],
'chats' => [
bless( {
'title' => 'Хулиномика',
'username' => 'hoolinomics',
'flags' => 8288,
'id' => 1380524958,
'access_hash' => '-6698103710539760874',
'broadcast' => 1,
'version' => 0,
'photo' => bless( {
'photo_small' => bless( {
'volume_id' => 246933270,
'file_reference' => '
'secret' => '1854156056801727328',
'local_id' => 228648,
'dc_id' => 2
}, 'Telegram::FileLocation' ),
'photo_big' => bless( {
'dc_id' => 2,
'local_id' => 228650,
'file_reference' => '
'secret' => '1275570353387113110',
'volume_id' => 246933270
}, 'Telegram::FileLocation' )
}, 'Telegram::ChatPhoto' ),
'date' => 1531221081
}, 'Telegram::Channel' )
],
'timeout' => 300,
'other_updates' => [
bless( {
'pts_count' => 0,
'message' => bless( {
'post' => 1,
'id' => 852,
'flags' => 50368,
'views' => 8013,
'entities' => [
bless( {
'length' => 20,
'offset' => 0
}, 'Telegram::MessageEntityBold' ),
bless( {
'length' => 18,
'offset' => 480,
'url' => 'https://alexeymarkov.livejournal.com/[url_вырезан].html'
}, 'Telegram::MessageEntityTextUrl' )
],
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'text' => '???? 165',
'data' => 'send_reaction_0'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'data' => 'send_reaction_1',
'text' => '???? 9'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'message' => 'А вот и новая книга! 
// [текст сообщения вырезан чтоб не нарушать правил Хабра о рекламе]
напечатаю.',
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'date' => 1571724559,
'edit_date' => 1571907562
}, 'Telegram::Message' ),
'pts' => 158508
}, 'Telegram::UpdateEditChannelMessage' ),
bless( {
'pts' => 158508,
'message' => bless( {
'edit_date' => 1571907589,
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'date' => 1571807301,
'message' => 'Почему Вы считаете Facebook плохой компанией? Можете прокомментировать? По-моему, это шикарная компания. Без долгов, с хорошей прибылью, а если решат дивы платить, то и еще могут нехило подорожать.
Для меня ответ совершенно очевиден: потому что Facebook делает ужасный по качеству продукт. Да, у него монопольное положение и да, им пользуется огромное количество людей. Но мир не стоит на месте. Когда-то владельцам Нокии было смешно от первого Айфона. Они думали, что лучше Нокии ничего быть не может и она навсегда останется самым удобным, красивым и твёрдым телефоном - и доля рынка это красноречиво демонстрировала. Теперь им не смешно.
Конечно, рептилоиды сопротивляются напору молодых гениев: так Цукербергом был пожран Whatsapp, потом Instagram. Но всё им не пожрать, Паша Дуров не продаётся!
Так будет и с Фейсбуком. Нельзя всё время делать говно. Кто-то когда-то сделает хороший продукт, куда всё и уйдут.
#соцсети #facebook #акции #рептилоиды',
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'data' => 'send_reaction_0',
'text' => '???? 452'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'text' => '???? 21',
'data' => 'send_reaction_1'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'entities' => [
bless( {
'length' => 199,
'offset' => 0
}, 'Telegram::MessageEntityBold' ),
bless( {
'length' => 8,
'offset' => 919
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'offset' => 928,
'length' => 9
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'length' => 6,
'offset' => 938
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'length' => 11,
'offset' => 945
}, 'Telegram::MessageEntityHashtag' )
],
'views' => 6964,
'flags' => 50368,
'id' => 854,
'post' => 1
}, 'Telegram::Message' ),
'pts_count' => 0
}, 'Telegram::UpdateEditChannelMessage' ),
bless( {
'message' => bless( {
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'data' => 'send_reaction_0',
'text' => '???? 213'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'data' => 'send_reaction_1',
'text' => '???? 8'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'views' => 2940,
'entities' => [
bless( {
'length' => 609,
'offset' => 348
}, 'Telegram::MessageEntityItalic' )
],
'flags' => 50368,
'post' => 1,
'id' => 857,
'edit_date' => 1571907636,
'date' => 1571902479,
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'message' => 'Пост про 1С вызвал бурную полемику. Человек 10 (видимо, 1с-программистов) единодушно написали:
// [текст сообщения вырезан чтоб не нарушать правил Хабра о рекламе]
Я бы добавил, что блестящая у 1С дистрибуция, а маркетинг... ну, такое.'
}, 'Telegram::Message' ),
'pts_count' => 0,
'pts' => 158508
}, 'Telegram::UpdateEditChannelMessage' ),
bless( {
'pts' => 158508,
'pts_count' => 0,
'message' => bless( {
'message' => 'Здравствуйте, расскажите, пожалуйста, чем вредит экономике 1С?
// [текст сообщения вырезан чтоб не нарушать правил Хабра о рекламе]
#софт #it #экономика',
'edit_date' => 1571907650,
'date' => 1571893707,
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'flags' => 50368,
'post' => 1,
'id' => 856,
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'data' => 'send_reaction_0',
'text' => '???? 360'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'data' => 'send_reaction_1',
'text' => '???? 32'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'views' => 4416,
'entities' => [
bless( {
'offset' => 0,
'length' => 64
}, 'Telegram::MessageEntityBold' ),
bless( {
'offset' => 1551,
'length' => 5
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'length' => 3,
'offset' => 1557
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'offset' => 1561,
'length' => 10
}, 'Telegram::MessageEntityHashtag' )
]
}, 'Telegram::Message' )
}, 'Telegram::UpdateEditChannelMessage' )
]
}, 'Telegram::Updates::ChannelDifference' )
}, 'MTProto::RpcResult' )
};
2019.10.24 12:00:51 $1 = {
'in' => bless( {
'update' => bless( {
'user_id' => 2507460,
'status' => bless( {
'was_online' => 1571907651
}, 'Telegram::UserStatusOffline' )
}, 'Telegram::UpdateUserStatus' ),
'date' => 1571907650
}, 'Telegram::UpdateShort' )
};
2019.10.24 12:05:46 $1 = {
'in' => bless( {
'chats' => [],
'date' => 1571907946,
'seq' => 0,
'updates' => [
bless( {
'max_id' => 141719,
'channel_id' => 1295963795
}, 'Telegram::UpdateReadChannelInbox' )
],
'users' => []
}, 'Telegram::Updates' )
};
2019.10.24 13:01:23 $1 = {
'in' => bless( {
'server_salt' => '4914425622822907323',
'unique_id' => '5297282355827493819',
'first_msg_id' => '6751307555044380692'
}, 'MTProto::NewSessionCreated' )
};
2019.10.24 13:24:21 $1 = {
'in' => bless( {
'chats' => [
bless( {
'username' => 'freebsd_ru',
'version' => 0,
'flags' => 5440,
'title' => 'freebsd_ru',
'min' => 1,
'photo' => bless( {
'photo_small' => bless( {
'local_id' => 328733,
'volume_id' => 235140688,
'dc_id' => 2,
'file_reference' => '
'secret' => '4426006807282303416'
}, 'Telegram::FileLocation' ),
'photo_big' => bless( {
'dc_id' => 2,
'file_reference' => '
'volume_id' => 235140688,
'local_id' => 328735,
'secret' => '71251192991540083'
}, 'Telegram::FileLocation' )
}, 'Telegram::ChatPhoto' ),
'date' => 1461248502,
'id' => 1038300508,
'democracy' => 1,
'megagroup' => 1
}, 'Telegram::Channel' )
],
'users' => [
bless( {
'last_name' => 'Panov',
'flags' => 1048646,
'min' => 1,
'id' => 82234609,
'status' => bless( {}, 'Telegram::UserStatusRecently' ),
'first_name' => 'Dima'
}, 'Telegram::User' )
],
'seq' => 0,
'date' => 1571912647,
'updates' => [
bless( {
'pts' => 137596,
'message' => bless( {
'flags' => 256,
'message' => 'Создать джейл с именем покороче ??',
'to_id' => bless( {
'channel_id' => 1038300508
}, 'Telegram::PeerChannel' ),
'id' => 119634,
'date' => 1571912647,
'from_id' => 82234609
}, 'Telegram::Message' ),
'pts_count' => 1
}, 'Telegram::UpdateNewChannelMessage' )
]
}, 'Telegram::Updates' )
};

Ja, ikke en spoiler med vilje - hvis du ikke har læst den endnu, så fortsæt og gør det!

Åh, vent~~... hvordan ser det her ud? Noget meget velkendt... måske er dette datastrukturen af ​​en typisk web-API i JSON, bortset fra at klasser også er knyttet til objekter?..

Så sådan bliver det... Hvad handler det om, kammerater?.. Så mange anstrengelser - og vi stoppede for at hvile, hvor webprogrammørerne lige begyndt?..Ville bare JSON over HTTPS ikke være enklere?! Hvad fik vi i bytte? Var indsatsen det værd?

Lad os evaluere, hvad TL+MTProto gav os, og hvilke alternativer der er mulige. Nå, HTTP, som fokuserer på anmodning-svar-modellen, passer dårligt, men i det mindste noget oven i TLS?

Kompakt serialisering. Når jeg ser denne datastruktur, der ligner JSON, husker jeg, at der er binære versioner af den. Lad os markere MsgPack som utilstrækkeligt udvideligt, men der er for eksempel CBOR - i øvrigt en standard beskrevet i RFC 7049. Det er bemærkelsesværdigt for det faktum, at det definerer mærker, som en ekspansionsmekanisme, og blandt allerede standardiseret ledig:

  • 25 + 256 - udskiftning af gentagne linjer med en reference til linjenummeret, sådan en billig komprimeringsmetode
  • 26 - serialiseret Perl-objekt med klassenavn og konstruktørargumenter
  • 27 - serialiseret sproguafhængigt objekt med typenavn og konstruktørargumenter

Nå, jeg prøvede at serialisere de samme data i TL og i CBOR med streng og objektpakning aktiveret. Resultatet begyndte at variere til fordel for CBOR et sted fra en megabyte:

cborlen=1039673 tl_len=1095092

således konklusion: Der er væsentligt enklere formater, som ikke er underlagt problemet med synkroniseringsfejl eller ukendt identifikator, med sammenlignelig effektivitet.

Hurtig oprettelse af forbindelse. Det betyder nul RTT efter genforbindelse (når nøglen allerede er blevet genereret én gang) - gældende fra den allerførste MTProto besked, men med nogle forbehold - ramt samme salt, sessionen er ikke rådden osv. Hvad tilbyder TLS os i stedet for? Citat om emnet:

Når du bruger PFS i TLS, TLS session billetter (RFC 5077) for at genoptage en krypteret session uden at genforhandle nøgler og uden at gemme nøgleoplysninger på serveren. Når den første forbindelse åbnes og nøgler oprettes, krypterer serveren forbindelsestilstanden og sender den til klienten (i form af en sessionsbillet). Når forbindelsen genoptages, sender klienten følgelig en sessionsbillet, inklusive sessionsnøglen, tilbage til serveren. Selve billetten er krypteret med en midlertidig nøgle (sessionsbilletnøgle), som lagres på serveren og skal distribueres blandt alle frontend-servere, der behandler SSL i klyngeløsninger.[10] Således kan indførelsen af ​​en sessionsbillet krænke PFS, hvis midlertidige servernøgler kompromitteres, for eksempel når de opbevares i lang tid (OpenSSL, nginx, Apache gemmer dem som standard i hele programmets varighed; populære websteder bruger nøglen i flere timer, op til dage).

Her er RTT ikke nul, du skal som minimum udveksle ClientHello og ServerHello, hvorefter klienten kan sende data sammen med Finished. Men her skal vi huske på, at vi ikke har nettet, med dets flok nyåbnede forbindelser, men en messenger, hvis forbindelse ofte er en og mere eller mindre langlivede, relativt korte forespørgsler til websider - alt er multiplekset internt. Det vil sige, at det er ganske acceptabelt, hvis vi ikke stødte på en rigtig dårlig metrosektion.

Glemt noget andet? Skriv i kommentarerne.

Fortsættes!

I anden del af denne serie af indlæg vil vi ikke overveje tekniske, men organisatoriske spørgsmål - tilgange, ideologi, grænseflade, holdning til brugere osv. Baseret dog på de tekniske oplysninger, der blev præsenteret her.

Den tredje del vil fortsætte med at analysere den tekniske komponent / udviklingserfaring. Du vil især lære:

  • fortsættelse af pandemonium med de mange forskellige TL-typer
  • ukendte ting om kanaler og supergrupper
  • hvorfor dialoger er værre end liste
  • om absolut vs relativ meddelelsesadressering
  • hvad er forskellen mellem foto og billede
  • hvordan emoji forstyrrer kursiv tekst

og andre krykker! Bliv hængende!

Kilde: www.habr.com

Tilføj en kommentar