Kritikk av protokollen og organisatoriske tilnærminger til Telegram. Del 1, teknisk: erfaring med å skrive en klient fra bunnen av - TL, MT

Nylig har innlegg om hvor bra Telegram er, hvor geniale og erfarne Durov-brødrene er med å bygge nettverkssystemer osv. begynt å dukke opp oftere på Habré. Samtidig er det veldig få som virkelig har fordypet seg i den tekniske enheten - på det meste bruker de en ganske enkel (og ganske forskjellig fra MTProto) JSON-basert Bot API, og aksepterer vanligvis bare på tro all ros og PR som dreier seg om budbringeren. For nesten et og et halvt år siden begynte min kollega ved Eshelon NGO Vasily (dessverre, hans konto på Habré ble slettet sammen med utkastet) å skrive sin egen Telegram-klient fra bunnen av i Perl, og senere ble forfatteren av disse linjene med. Hvorfor Perl, vil noen umiddelbart spørre? Fordi slike prosjekter allerede eksisterer på andre språk. Dette er faktisk ikke poenget, det kan være et hvilket som helst annet språk der det ikke finnes ferdige bibliotek, og følgelig må forfatteren gå hele veien fra bunnen av. Dessuten er kryptografi et spørsmål om tillit, men verifiser. Med et produkt rettet mot sikkerhet kan du ikke bare stole på et ferdig bibliotek fra produsenten og stole blindt på det (men dette er et tema for den andre delen). For øyeblikket fungerer biblioteket ganske bra på "gjennomsnittlig" nivå (lar deg gjøre alle API-forespørsler).

Imidlertid vil det ikke være mye kryptografi eller matematikk i denne serien med innlegg. Men det vil være mange andre tekniske detaljer og arkitektoniske krykker (også nyttig for de som ikke vil skrive fra bunnen av, men vil bruke biblioteket på hvilket som helst språk). Så hovedmålet var å prøve å implementere klienten fra bunnen av ifølge offisiell dokumentasjon. Det vil si, la oss anta at kildekoden til offisielle klienter er stengt (igjen, i den andre delen vil vi dekke mer detaljert temaet om at dette er sant det skjer så), men som i gamle dager, for eksempel, er det en standard som RFC - er det mulig å skrive en klient i henhold til spesifikasjonen alene, "uten å se" på kildekoden, det være seg offisiell (Telegram Desktop, mobil), eller uoffisiell Telethon?

Bekreftelse:

Dokumentasjon ... den finnes, ikke sant? Er det sant?..

Fragmenter av notater til denne artikkelen begynte å bli samlet i fjor sommer. Hele denne tiden på den offisielle nettsiden https://core.telegram.org Dokumentasjonen var per lag 23, d.v.s. satt fast et sted i 2014 (husk at det ikke en gang var kanaler den gang?). Selvfølgelig burde dette i teorien ha tillatt oss å implementere en klient med funksjonalitet på det tidspunktet i 2014. Men selv i denne tilstanden var dokumentasjonen for det første ufullstendig, og for det andre på steder den motsier seg selv. For en drøy måned siden, i september 2019, var det det uhell Det ble oppdaget at det var en stor oppdatering av dokumentasjonen på siden, for det ganske ferske Layer 105, med en merknad om at nå må alt leses på nytt. Faktisk ble mange artikler revidert, men mange forble uendret. Når du leser kritikken nedenfor om dokumentasjonen, bør du derfor huske på at noen av disse tingene ikke lenger er relevante, men noen er ganske. Tross alt er 5 år i den moderne verden ikke bare lang tid, men veldig mye av. Siden den gang (spesielt hvis du ikke tar hensyn til de kasserte og gjenopplivede geochat-nettstedene siden den gang), har antallet API-metoder i ordningen vokst fra hundre til mer enn to hundre og femti!

Hvor skal man begynne som ung forfatter?

Det spiller ingen rolle om du skriver fra bunnen av eller bruker for eksempel ferdige biblioteker som Telethon for Python eller Madeline for PHP, i alle fall trenger du først registrere søknaden din - få parametere api_id и api_hash (de som har jobbet med VKontakte API forstår umiddelbart) som serveren vil identifisere applikasjonen med. Dette gjør det av juridiske årsaker, men vi skal snakke mer om hvorfor biblioteksforfattere ikke kan publisere det i den andre delen. Du kan være fornøyd med testverdiene, selv om de er svært begrensede - faktum er at nå kan du registrere deg bare en app, så ikke skynd deg hodestups inn i den.

Nå, fra et teknisk synspunkt, bør vi være interessert i at vi etter registrering skal motta varsler fra Telegram om oppdateringer av dokumentasjon, protokoll osv. Det vil si at man kunne anta at stedet med bryggene rett og slett ble forlatt og fortsatte å jobbe spesifikt med de som begynte å lage kunder, fordi det er lettere. Men nei, ingenting slikt ble observert, ingen informasjon kom.

Og hvis du skriver fra bunnen av, så er det faktisk et stykke unna å bruke de oppnådde parameterne. Selv om https://core.telegram.org/ og snakker om dem i Komme i gang først og fremst, faktisk må du først implementere MTProto-protokoll - men hvis du trodde layout i henhold til OSI-modellen på slutten av siden for en generell beskrivelse av protokollen, så er det helt forgjeves.

Faktisk, både før og etter MTProto, på flere nivåer samtidig (som utenlandske nettverkere som jobber i OS-kjernen sier, lagbrudd), vil et stort, smertefullt og forferdelig tema komme i veien...

Binær serialisering: TL (Type Language) og dets skjema, og lag, og mange andre skumle ord

Dette emnet er faktisk nøkkelen til Telegrams problemer. Og det blir mange forferdelige ord hvis du prøver å fordype deg i det.

Så her er diagrammet. Hvis dette ordet kommer til deg, si, JSON -skjema, du tenkte riktig. Målet er det samme: et språk for å beskrive et mulig sett med overførte data. Det er her likhetene slutter. Hvis fra siden MTProto-protokoll, eller fra kildetreet til den offisielle klienten, vil vi prøve å åpne et skjema, vi vil se noe sånt 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 som ser dette for første gang vil intuitivt bare kunne gjenkjenne en del av det som er skrevet - vel, dette er tilsynelatende strukturer (selv om hvor er navnet, til venstre eller til høyre?), det er felt i dem, hvoretter en type følger etter et kolon... sannsynligvis. Her i vinkelparentes er det sannsynligvis maler som i C++ (faktisk, ikke egentlig). Og hva betyr alle de andre symbolene, spørsmålstegn, utropstegn, prosenter, hash-tegn (og tydeligvis betyr de forskjellige ting på forskjellige steder), noen ganger tilstede og noen ganger ikke, heksadesimale tall - og viktigst av alt, hvordan komme fra dette правильный (som ikke vil bli avvist av serveren) byte stream? Du må lese dokumentasjonen (ja, det er lenker til skjemaet i JSON-versjonen i nærheten - men det gjør det ikke klarere).

Åpne siden Binær data serialisering og dykk inn i den magiske verdenen av sopp og diskret matematikk, noe som ligner på matan i det 4. året. Alfabet, type, verdi, kombinator, funksjonell kombinator, normal form, sammensatt type, polymorf type... og det er bare den første siden! Neste venter på deg TL Språk, som, selv om den allerede inneholder et eksempel på en triviell forespørsel og svar, ikke gir noe svar i det hele tatt på mer typiske tilfeller, noe som betyr at du må vasse gjennom en gjenfortelling av matematikk oversatt fra russisk til engelsk på ytterligere åtte embedded sider!

Lesere som er kjent med funksjonelle språk og automatisk typeslutning vil selvfølgelig se beskrivelsesspråket på dette språket, selv fra eksempelet, som mye mer kjent, og kan si at dette faktisk ikke er dårlig i prinsippet. Innvendingene mot dette er:

  • ja, mål høres bra ut, men akk, hun ikke oppnådd
  • Utdanning ved russiske universiteter varierer selv blant IT-spesialiteter - ikke alle har tatt tilsvarende kurs
  • Til slutt, som vi skal se, er det i praksis det ikke nødvendig, siden bare en begrenset delmengde av selv TL-en som ble beskrevet blir brukt

Som sagt Leonerd på kanalen #perl i FreeNode IRC-nettverket, som prøvde å implementere en port fra Telegram til Matrix (oversettelsen av sitatet er unøyaktig fra minnet):

Det føles som om noen ble introdusert for typeteori for første gang, ble begeistret og begynte å prøve å leke med det, uten egentlig å bry seg om det var nødvendig i praksis.

Se selv om behovet for bare-typer (int, long, etc.) som noe elementært ikke reiser spørsmål - til syvende og sist må de implementeres manuelt - for eksempel, la oss ta et forsøk på å utlede fra dem vektor. Det vil si, faktisk array, hvis du kaller de resulterende tingene ved deres riktige navn.

Men før

En kort beskrivelse av et undersett av TL-syntaks for de som ikke leser den offisielle dokumentasjonen

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;

Definisjon starter alltid designeren, hvoretter valgfritt (i praksis - alltid) gjennom symbolet # bør CRC32 fra den normaliserte beskrivelsesstrengen av denne typen. Deretter kommer en beskrivelse av feltene; hvis de finnes, kan typen være tom. Alt dette ender med et likhetstegn, navnet på typen som denne konstruktøren - altså faktisk undertypen - tilhører. Fyren til høyre for likhetstegnet er polymorf - det vil si at flere spesifikke typer kan tilsvare det.

Hvis definisjonen kommer etter linjen ---functions---, da vil syntaksen forbli den samme, men betydningen vil være forskjellig: konstruktøren vil bli navnet på RPC-funksjonen, feltene vil bli parametere (vel, det vil si at den forblir nøyaktig den samme gitte strukturen, som beskrevet nedenfor , vil dette ganske enkelt være den tildelte betydningen), og den "polymorfe typen" - typen av det returnerte resultatet. Riktignok vil det fortsatt forbli polymorf - bare definert i avsnittet ---types---, men denne konstruktøren vil "ikke bli vurdert". Overbelastning av typene kalte funksjoner ved deres argumenter, dvs. Av en eller annen grunn er flere funksjoner med samme navn, men forskjellige signaturer, som i C++, ikke gitt i TL.

Hvorfor "konstruktør" og "polymorf" hvis det ikke er OOP? Vel, faktisk vil det være lettere for noen å tenke på dette i OOP-termer - en polymorf type som en abstrakt klasse, og konstruktører er dens direkte etterkommerklasser, og final i terminologien til en rekke språk. Faktisk, selvfølgelig, bare her likheten med reelle overbelastede konstruktørmetoder i OO-programmeringsspråk. Siden her kun er datastrukturer finnes det ingen metoder (selv om beskrivelsen av funksjoner og metoder videre er ganske i stand til å skape forvirring i hodet om at de eksisterer, men det er en annen sak) – man kan tenke på en konstruktør som en verdi fra hvilken blir bygget skriv når du leser en bytestrøm.

Hvordan skjer dette? Deserializeren, som alltid leser 4 byte, ser verdien 0xcrc32 - og forstår hva som vil skje videre field1 med type int, dvs. leser nøyaktig 4 byte, på dette det overliggende feltet med typen PolymorType lese. Ser 0x2crc32 og forstår at det er to felt lenger, først long, som betyr at vi leser 8 byte. Og så igjen en kompleks type, som er deserialisert på samme måte. For eksempel, Type3 kunne deklareres i kretsen så snart to konstruktører, henholdsvis, så må de møtes enten 0x12abcd34, hvoretter du må lese 4 byte til intEller 0x6789cdef, hvoretter det ikke blir noe. Noe annet - du må kaste et unntak. Uansett, etter dette går vi tilbake til å lese 4 byte int felt field_c в constructorTwo og med det leser vi ferdig vår PolymorType.

Til slutt, hvis du blir tatt 0xdeadcrc for constructorThree, da blir alt mer komplisert. Vårt første felt er bit_flags_of_what_really_present med type # – faktisk er dette bare et alias for typen nat, som betyr "naturlig tall". Det vil si at usignert int forresten er det eneste tilfellet når usignerte tall forekommer i reelle kretser. Så, neste er en konstruksjon med et spørsmålstegn, noe som betyr at dette feltet - det vil være til stede på ledningen bare hvis den tilsvarende biten er satt i feltet det refereres til (omtrent som en ternær operatør). Så la oss anta at denne biten ble satt, noe som betyr at vi trenger å lese et felt som Type, som i vårt eksempel har 2 konstruktører. Den ene er tom (består kun av identifikatoren), den andre har et felt ids med type ids:Vector<long>.

Du tror kanskje at både maler og generikk er i proffene eller Java. Men nei. Nesten. Dette den eneste tilfelle av bruk av vinkelbraketter i ekte kretser, og det brukes KUN for Vector. I en bytestrøm vil disse være 4 CRC32 byte for selve Vector-typen, alltid de samme, deretter 4 byte - antall array-elementer, og deretter disse elementene i seg selv.

Legg til dette det faktum at serialisering alltid skjer i ord på 4 byte, alle typer er multipler av det - de innebygde typene er også beskrevet bytes и string med manuell serialisering av lengden og denne justeringen med 4 - vel, det ser ut til å høres normalt ut og til og med relativt effektivt? Selv om TL hevdes å være en effektiv binær serialisering, til helvete med dem, med utvidelsen av omtrent hva som helst, til og med boolske verdier og enkelttegnsstrenger til 4 byte, vil JSON fortsatt være mye tykkere? Se, selv unødvendige felt kan hoppes over av bit-flagg, alt er ganske bra, og til og med utvidbart for fremtiden, så hvorfor ikke legge til nye valgfrie felt til konstruktøren senere?

Men nei, hvis du ikke leser min korte beskrivelse, men hele dokumentasjonen, og tenker på gjennomføringen. For det første beregnes CRC32 til konstruktøren i henhold til den normaliserte linjen i tekstbeskrivelsen av skjemaet (fjern ekstra mellomrom, etc.) - så hvis et nytt felt legges til, vil typebeskrivelseslinjen endres, og derav dens CRC32 og , følgelig serialisering. Og hva ville den gamle klienten gjort hvis han mottok et felt med nye flagg satt, og han ikke vet hva han skal gjøre med dem neste gang?

For det andre, la oss huske CRC32, som brukes her hovedsakelig som hash-funksjoner for unikt å bestemme hvilken type som blir (av)serialisert. Her står vi overfor problemet med kollisjoner – og nei, sannsynligheten er ikke én av 232, men mye større. Hvem husket at CRC32 er designet for å oppdage (og rette) feil i kommunikasjonskanalen, og dermed forbedrer disse egenskapene til skade for andre? For eksempel bryr den seg ikke om å omorganisere byte: hvis du beregner CRC32 fra to linjer, bytter du i den andre de første 4 bytene med de neste 4 bytene - det vil være det samme. Når vår input er tekststrenger fra det latinske alfabetet (og litt tegnsetting), og disse navnene ikke er spesielt tilfeldige, øker sannsynligheten for en slik omorganisering betraktelig.

Hvem sjekket forresten hva som var der? virkelig CRC32? En av de tidlige kildekodene (selv før Waltman) hadde en hash-funksjon som multipliserte hvert tegn med tallet 239, så elsket av disse menneskene, ha ha!

Til slutt, ok, innså vi at konstruktører med en felttype Vector<int> и Vector<PolymorType> vil ha forskjellig CRC32. Hva med online ytelse? Og fra et teoretisk synspunkt, blir dette en del av typen? La oss si at vi passerer en rekke med ti tusen tall, vel med Vector<int> alt er klart, lengden og ytterligere 40000 XNUMX byte. Hva om dette Vector<Type2>, som kun består av ett felt int og det er alene i typen - trenger vi å gjenta 10000xabcdef0 34 4 ganger og deretter XNUMX byte int, eller språket er i stand til å UAVHENGE det for oss fra konstruktøren fixedVec og i stedet for 80000 40000 byte, overføre igjen bare XNUMX XNUMX?

Dette er ikke et inaktivt teoretisk spørsmål i det hele tatt - forestill deg at du mottar en liste over gruppebrukere, som hver har en id, fornavn, etternavn - forskjellen i mengden data som overføres over en mobilforbindelse kan være betydelig. Det er nettopp effektiviteten av Telegram-serialisering som annonseres for oss.

Så…

Vector, som aldri ble utgitt

Hvis du prøver å gå gjennom sidene med beskrivelser av kombinatorer og så videre, vil du se at en vektor (og til og med en matrise) formelt prøver å sendes ut gjennom tupler av flere ark. Men til slutt glemmer de, det siste trinnet hoppes over, og en definisjon av en vektor er ganske enkelt gitt, som ennå ikke er knyttet til en type. Hva er i veien? På språk programmering, spesielt funksjonelle, er det ganske typisk å beskrive strukturen rekursivt - kompilatoren med sin late evaluering vil forstå og gjøre alt selv. På språket dataserialisering det som trengs er EFFEKTIVITET: det er nok å enkelt beskrive список, dvs. struktur av to elementer - den første er et dataelement, den andre er den samme strukturen i seg selv eller et tomt rom for halen (pakke (cons) i Lisp). Men dette vil selvsagt kreve av hver element bruker ytterligere 4 byte (CRC32 i tilfellet i TL) for å beskrive typen. En matrise kan også enkelt beskrives fast størrelse, men i tilfelle av en rekke av ukjent lengde på forhånd, bryter vi av.

Derfor, siden TL ikke tillater utmating av en vektor, måtte den legges til på siden. Til syvende og sist sier dokumentasjonen:

Serialisering bruker alltid den samme konstruktøren "vektor" (const 0x1cb5c415 = crc32("vektor t:Type # [ t ] = vektor t") som ikke er avhengig av den spesifikke verdien til variabelen av typen t.

Verdien til den valgfrie parameteren t er ikke involvert i serialiseringen siden den er avledet fra resultattypen (alltid kjent før deserialiseringen).

Ta en nærmere titt: vector {t:Type} # [ t ] = Vector t - men steds Denne definisjonen i seg selv sier ikke at det første tallet må være lik lengden på vektoren! Og det kommer ikke fra noe sted. Dette er en gitt som må huskes og implementeres med hendene. Andre steder nevner dokumentasjonen til og med ærlig at typen ikke er ekte:

Vector t polymorfe pseudotype er en "type" hvis verdi er en sekvens av verdier av hvilken som helst type t, enten innrammet eller bare.

... men fokuserer ikke på det. Når du, lei av å vasse gjennom tøyningen av matematikk (kanskje til og med kjent for deg fra et universitetskurs), bestemmer deg for å gi opp og faktisk ser på hvordan du kan jobbe med det i praksis, er inntrykket i hodet ditt at dette er seriøst Matematikk i kjernen, det ble tydelig oppfunnet av Cool People (to matematikere - ACM-vinner), og ikke hvem som helst. Målet – å vise seg frem – er nådd.

Forresten, om antallet. La oss minne deg på det # det er et synonym nat, naturlig tall:

Det er typeuttrykk (type-expr) og numeriske uttrykk (nat-expr). Imidlertid er de definert på samme måte.

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

men i grammatikken er de beskrevet på samme måte, dvs. Denne forskjellen må igjen huskes og implementeres manuelt.

Vel, ja, maltyper (vector<int>, vector<User>) har en felles identifikator (#1cb5c415), dvs. hvis du vet at samtalen er annonsert som

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

da venter du ikke lenger på bare en vektor, men en vektor av brukere. Mer presist, bør vent - i ekte kode vil hvert element, hvis ikke en bare type, ha en konstruktør, og på en god måte i implementeringen ville det være nødvendig å sjekke - men vi ble sendt nøyaktig i hvert element i denne vektoren den typen? Hva om det var en slags PHP, der en matrise kan inneholde forskjellige typer i forskjellige elementer?

På dette tidspunktet begynner du å tenke - er en slik TL nødvendig? Kanskje for vognen ville det være mulig å bruke en menneskelig serializer, den samme protobuf som allerede eksisterte da? Det var teorien, la oss se på praksis.

Eksisterende TL-implementeringer i kode

TL ble født i dypet av VKontakte selv før de berømte hendelsene med salg av Durovs andel og (sikkert), selv før utviklingen av Telegram begynte. Og i åpen kildekode kildekoden til den første implementeringen du kan finne mange morsomme krykker. Og selve språket ble implementert der mer fullstendig enn det er nå i Telegram. For eksempel brukes ikke hasher i det hele tatt i skjemaet (som betyr en innebygd pseudotype (som en vektor) med avvikende oppførsel). Eller

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

men la oss vurdere, for fullstendighetens skyld, å spore, så å si, utviklingen av Tankens Kjempe.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Eller denne vakre:

    static const char *reserved_words_polymorhic[] = {

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

      };

Dette fragmentet handler om maler som:

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

Dette er definisjonen av en hashmap-maltype som en vektor av int - Type-par. I C++ vil det se omtrent slik ut:

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

så, alpha - nøkkelord! Men bare i C++ kan du skrive T, men du bør skrive alfa, beta... Men ikke mer enn 8 parametere, det er der fantasien slutter. Det ser ut til at det en gang i St. Petersburg fant noen dialoger som dette sted:

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

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

Men dette handlet om den første publiserte implementeringen av TL "generelt". La oss gå videre til å vurdere implementeringer i Telegram-klientene selv.

Ord til Vasily:

Vasily, [09.10.18 17:07] Mest av alt er rumpa varm fordi de skapte en haug med abstraksjoner, og deretter hamret en bolt på dem, og dekket kodegeneratoren med krykker
Som et resultat, først fra dock pilot.jpg
Så fra koden dzhekichan.webp

Selvfølgelig, fra folk som er kjent med algoritmer og matematikk, kan vi forvente at de har lest Aho, Ullmann, og er kjent med verktøyene som har blitt de facto standard i bransjen i løpet av tiårene for å skrive DSL-kompilatorene deres, ikke sant?

Av forfatteren telegram-cli er Vitaly Valtman, som det kan forstås fra forekomsten av TLO-formatet utenfor dets (cli) grenser, et medlem av teamet - nå er et bibliotek for TL-parsing blitt tildelt separat, hva er inntrykket av henne TL-parser? ..

16.12 04:18 Vasily: Jeg tror noen ikke mestret lex+yacc
16.12 04:18 Vasily: Jeg kan ikke forklare det på annen måte
16.12 04:18 Vasily: vel, eller de ble betalt for antall linjer i VK
16.12 04:19 Vasily: 3k+ linjer osv.<censored> i stedet for en parser

Kanskje et unntak? La oss se hvordan gjør Dette er den OFFISIELLE klienten - 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 uttrykk + spesielle tilfeller som en vektor, som selvfølgelig er deklarert i skjemaet slik det skal være i henhold til TL-syntaksen, men de stolte på denne syntaksen for å analysere den... Spørsmålet oppstår, hvorfor var det hele et mirakel?иDet er mer lagdelt hvis ingen skal analysere det i henhold til dokumentasjonen uansett?!

Forresten... Husker du at vi snakket om CRC32-sjekking? Så i Telegram Desktop-kodegeneratoren er det en liste over unntak for de typene der den beregnede CRC32 passer ikke med den som er angitt i diagrammet!

Vasily, [18.12/22 49:XNUMX] og her vil jeg tenke på om en slik TL er nødvendig
hvis jeg ville rote med alternative implementeringer, ville jeg begynt å sette inn linjeskift, halvparten av parserne vil bryte på flerlinjede definisjoner
tdesktop, men også

Husk poenget om one-liner, vi kommer tilbake til det litt senere.

Ok, telegram-cli er uoffisielt, Telegram Desktop er offisielt, men hva med de andre? Hvem vet? .. I Android-klientkoden var det ingen skjemaparser i det hele tatt (noe som reiser spørsmål om åpen kildekode, men dette er for den andre delen), men det var flere andre morsomme kodebiter, men mer om dem i underseksjon nedenfor.

Hvilke andre spørsmål reiser serialisering i praksis? For eksempel gjorde de mange ting, selvfølgelig, med bitfelt og betingede felt:

Vasily: flags.0? true
betyr at feltet er tilstede og er lik sant hvis flagget er satt

Vasily: flags.1? int
betyr at feltet er tilstede og må deserialiseres

Vasily: Ass, ikke bekymre deg for hva du gjør!
Vasily: Det er en omtale et sted i dokumentet at sant er en null-lengde type, men det er umulig å sette sammen noe fra dokumentet deres
Vasily: I åpen kildekode-implementeringer er ikke dette tilfellet heller, men det er en haug med krykker og støtter

Hva med Telethon? Ser vi fremover til emnet MTProto, et eksempel - i dokumentasjonen er det slike stykker, men tegnet % det beskrives bare som "tilsvarende en gitt bare-type", dvs. i eksemplene nedenfor er det enten en feil eller noe udokumentert:

Vasily, [22.06.18 18:38] På ett sted:

msg_container#73f1f8dc messages:vector message = MessageContainer;

I en annen:

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

Og dette er to store forskjeller, i det virkelige liv kommer en slags naken vektor

Jeg har ikke sett en naken vektordefinisjon og har ikke kommet over en

Analyse skrives for hånd i teleton

I diagrammet hans er definisjonen kommentert msg_container

Igjen gjenstår spørsmå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å vanlige motorer vil mest sannsynlig ikke spise dette heller

// parsed manually

TL er en vakker abstraksjon, ingen implementerer den helt

Og % er ikke i deres versjon av ordningen

Men her motsier dokumentasjonen seg selv, så idk

Det ble funnet i grammatikken, de kunne rett og slett ha glemt å beskrive semantikken

Du så dokumentet på TL, du kan ikke finne ut av det uten en halv liter

"Vel, la oss si," vil en annen leser si, "du kritiserer noe, så vis meg hvordan det bør gjøres."

Vasily svarer: «Når det gjelder parseren, liker jeg 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 ) ); }
            ;

liker det liksom bedre enn

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. enklere er å si det mildt.»

Generelt, som et resultat, passer parseren og kodegeneratoren for den faktisk brukte delmengden av TL inn i omtrent 100 linjer med grammatikk og ~300 linjer i generatoren (teller alle printsin genererte kode), inkludert typeinformasjonsboller for introspeksjon i hver klasse. Hver polymorf type blir til en tom abstrakt baseklasse, og konstruktører arver fra den og har metoder for serialisering og deserialisering.

Mangel på typer i typespråket

Sterk skriving er en god ting, ikke sant? Nei, dette er ikke en holivar (selv om jeg foretrekker dynamiske språk), men et postulat innenfor rammen av TL. Basert på det skal språket gi alle slags sjekker for oss. Vel, ok, kanskje ikke han selv, men gjennomføringen, men han burde i det minste beskrive dem. Og hva slags muligheter ønsker vi?

Først av alt, begrensninger. Her ser vi i dokumentasjonen for opplasting av filer:

Filens binære innhold deles deretter opp i deler. Alle deler må ha samme størrelse ( del_størrelse ) og følgende betingelser må være oppfylt:

  • part_size % 1024 = 0 (delelig med 1KB)
  • 524288 % part_size = 0 (512KB må være jevnt delelig med part_size)

Den siste delen trenger ikke å tilfredsstille disse betingelsene, forutsatt at størrelsen er mindre enn part_size.

Hver del skal ha et sekvensnummer, fil_del, med en verdi fra 0 til 2,999.

Etter at filen har blitt partisjonert, må du velge en metode for å lagre den på serveren. Bruk upload.saveBigFilePart i tilfelle den fulle størrelsen på filen er mer enn 10 MB og upload.saveFilePart for mindre filer.
[…] en av følgende datainndatafeil kan returneres:

  • FILE_PARTS_INVALID — Ugyldig antall deler. Verdien er ikke mellom 1..3000

Er noe av dette i diagrammet? Kan dette på en eller annen måte uttrykkes med TL? Nei. Men unnskyld meg, selv bestefars Turbo Pascal var i stand til å beskrive de spesifiserte typene områder. Og han visste en ting til, nå bedre kjent som enum - en type som består av en oppregning av et fast (lite) antall verdier. På språk som C - numerisk, merk at vi så langt bare har snakket om typer tall. Men det finnes også arrays, strenger... for eksempel ville det vært fint å beskrive at denne strengen bare kan inneholde et telefonnummer, ikke sant?

Ingenting av dette er i TL. Men det er for eksempel i JSON Schema. Og hvis noen andre kan krangle om delebarheten til 512 KB, at dette fortsatt må sjekkes i kode, så sørg for at klienten ganske enkelt kunne ikke sende et nummer utenfor rekkevidde 1..3000 (og den tilsvarende feilen kunne ikke ha oppstått) det hadde vært mulig, ikke sant?..

Forresten om feil og returverdier. Selv de som har jobbet med TL gjør øynene uskarpe - det gikk ikke opp for oss umiddelbart hver og en en funksjon i TL kan faktisk returnere ikke bare den beskrevne returtypen, men også en feil. Men dette kan ikke utledes på noen måte ved å bruke TL selv. Selvfølgelig er det allerede klart og det er ikke behov for noe i praksis (selv om RPC faktisk kan gjøres på forskjellige måter, vi kommer tilbake til dette senere) - men hva med renheten til begrepene Mathematics of Abstract Types fra den himmelske verden?.. Jeg tok opp slepebåten - så match den.

Og til slutt, hva med lesbarhet? Vel, der, generelt, vil jeg gjerne beskrivelse ha det rett i skjemaet (i JSON-skjemaet, igjen, det er det), men hvis du allerede er anstrengt med det, hva med den praktiske siden - i det minste trivielt å se på forskjellene under oppdateringer? Se selv på virkelige 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 avhenger av alle, men GitHub, for eksempel, nekter å markere endringer innenfor så lange linjer. Spillet "finn 10 forskjeller", og det hjernen umiddelbart ser er at begynnelsen og slutten i begge eksemplene er de samme, du må lese kjedelig et sted i midten... Etter min mening er dette ikke bare i teorien, men rent visuelt skitten og slurvete.

Forresten, om renheten i teorien. Hvorfor trenger vi bitfelt? Ser det ikke ut til at de lukt dårlig fra typeteoriens synspunkt? Forklaringen kan sees i tidligere versjoner av diagrammet. Til å begynne med, ja, det var slik det var, for hvert nys ble det laget en ny type. Disse rudimentene eksisterer fortsatt i denne formen, 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 forestill deg nå, hvis du har 5 valgfrie felt i strukturen din, vil du trenge 32 typer for alle mulige alternativer. Kombinatorisk eksplosjon. Dermed knuste krystallrenheten til TL-teorien nok en gang mot støpejernsræva til den harde virkeligheten med serialisering.

I tillegg bryter disse gutta noen steder selv sin egen typologi. For eksempel, i MTProto (neste kapittel) kan responsen komprimeres av Gzip, alt er i orden - bortsett fra at lagene og kretsen er brutt. Nok en gang var det ikke RpcResult i seg selv som ble høstet, men innholdet. Vel, hvorfor gjøre dette?.. Jeg måtte kutte i en krykke for at kompresjonen skulle fungere hvor som helst.

Eller et annet eksempel, vi oppdaget en gang en feil - den ble sendt InputPeerUser i stedet for InputUser. Eller vice versa. Men det fungerte! Det vil si at serveren ikke brydde seg om typen. Hvordan kan dette være? Svaret kan gis til oss av 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);

Det er med andre ord her serialisering gjøres MANUELT, ikke generert kode! Kanskje serveren er implementert på lignende måte?.. I prinsippet vil dette fungere hvis det gjøres en gang, men hvordan kan det støttes senere under oppdateringer? Er det derfor ordningen ble oppfunnet? Og her går vi videre til neste spørsmål.

Versjonskontroll. Lag

Hvorfor de skjematiske versjonene kalles lag kan bare spekuleres basert på historien til publiserte skjemaer. Tilsynelatende trodde forfatterne først at grunnleggende ting kunne gjøres ved å bruke den uendrede ordningen, og bare der det var nødvendig, for spesifikke forespørsler, indikerer de at de ble gjort med en annen versjon. I prinsippet er til og med en god idé - og det nye vil så å si være "blandet", lagt på toppen av det gamle. Men la oss se hvordan det ble gjort. Riktignok var jeg ikke i stand til å se på det helt fra begynnelsen - det er morsomt, men diagrammet over basislaget eksisterer rett og slett ikke. Lag startet med 2. Dokumentasjonen forteller oss om en spesiell TL-funksjon:

Hvis en klient støtter Layer 2, må følgende konstruktør brukes:

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

I praksis betyr dette at før hvert API-kall, en int med verdien 0x289dd1f6 må legges til før metodenummeret.

Høres normalt ut. Men hva skjedde videre? Så dukket opp

invokeWithLayer3#b7475268 query:!X = X;

Så hva er neste? Som du kanskje gjetter,

invokeWithLayer4#dea0d430 query:!X = X;

Morsom? Nei, det er for tidlig å le, tenk på det hver en forespørsel fra et annet lag må pakkes inn i en så spesiell type - hvis de alle er forskjellige for deg, hvordan kan du ellers skille dem? Og å legge til bare 4 byte foran er en ganske effektiv metode. Så,

invokeWithLayer5#417a57ae query:!X = X;

Men det er åpenbart at etter en stund vil dette bli en slags bacchanalia. Og løsningen kom:

Oppdatering: Starter med lag 9, hjelpemetoder invokeWithLayerN kan kun brukes sammen med initConnection

Hurra! Etter 9 versjoner kom vi endelig til det som ble gjort i internettprotokoller tilbake på 80-tallet - ble enige om versjonen en gang i begynnelsen av tilkoblingen!

Så hva er neste?..

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

Men nå kan du fortsatt le. Først etter ytterligere 9 lag ble det endelig lagt til en universell konstruktør med et versjonsnummer, som bare må kalles en gang i begynnelsen av tilkoblingen, og meningen med lagene så ut til å ha forsvunnet, nå er det bare en betinget versjon, som alle andre steder. Problem løst.

Nøyaktig?..

Vasily, [16.07.18 14:01] Selv på fredag ​​tenkte jeg:
Teleserveren sender hendelser uten forespørsel. Forespørsler må pakkes inn i InvokeWithLayer. Serveren bryter ikke oppdateringer; det er ingen struktur for å pakke inn svar og oppdateringer.

De. klienten kan ikke spesifisere laget han vil ha oppdateringer i

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

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

Vadim Goncharov, [16.07.18 14:02] som egentlig burde bety å bli enige om laget i begynnelsen av økten

For øvrig følger det at klientnedgradering ikke er gitt

Oppdateringer, dvs. type Updates i ordningen er dette hva serveren sender til klienten, ikke som svar på en API-forespørsel, men uavhengig når en hendelse inntreffer. Dette er et komplekst tema som vil bli diskutert i et annet innlegg, men foreløpig er det viktig å vite at serveren lagrer oppdateringer selv når klienten er offline.

Altså, hvis du nekter å pakke inn av hver pakken for å indikere versjonen, fører dette logisk til følgende mulige problemer:

  • serveren sender oppdateringer til klienten selv før klienten har informert om hvilken versjon den støtter
  • hva skal jeg gjøre etter å ha oppgradert klienten?
  • som garantierat serverens mening om lagnummeret ikke vil endre seg under prosessen?

Tror du dette er rent teoretisk spekulasjon, og i praksis kan dette ikke skje, fordi serveren er skrevet riktig (i det minste er den testet godt)? Ha! Uansett hvordan det er!

Det var akkurat dette vi møtte i august. 14. august kom det meldinger om at noe ble oppdatert på Telegram-serverne... og deretter 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 deretter flere megabyte med stabelspor (vel, samtidig ble loggingen fikset). Tross alt, hvis noe ikke gjenkjennes i TL-en din, er det binært av signatur, lenger ned i linjen ALLE går, vil dekoding bli umulig. Hva bør du gjøre i en slik situasjon?

Vel, det første som kommer til noen er å koble fra og prøve igjen. Hjelpet ikke. Vi googler CRC32 - dette viste seg å være objekter fra skjema 73, selv om vi jobbet med 82. Vi ser nøye på loggene - det er identifikatorer fra to forskjellige skjemaer!

Kanskje ligger problemet rent i vår uoffisielle klient? Nei, vi lanserer Telegram Desktop 1.2.17 (versjon leveres i en rekke Linux-distribusjoner), den skriver til unntaksloggen: MTP Unexpected type id #b5223b0f lest i MTPMessageMedia...

Kritikk av protokollen og organisatoriske tilnærminger til Telegram. Del 1, teknisk: erfaring med å skrive en klient fra bunnen av - TL, MT

Google viste at et lignende problem allerede hadde skjedd med en av de uoffisielle klientene, men da var versjonsnumrene og følgelig antakelsene forskjellige...

Så hva bør vi gjøre? Vasily og jeg gikk fra hverandre: han prøvde å oppdatere kretsen til 91, jeg bestemte meg for å vente noen dager og prøve på 73. Begge metodene fungerte, men siden de er empiriske, er det ingen forståelse for hvor mange versjoner opp eller ned du trenger å hoppe, eller hvor lenge du må vente .

Senere klarte jeg å gjenskape situasjonen: vi starter klienten, slår den av, kompilerer kretsen på nytt til et annet lag, starter på nytt, fanger opp problemet igjen, går tilbake til den forrige - ups, ingen kretsbytte og klienten starter på nytt for en noen minutter vil hjelpe. Du vil motta en blanding av datastrukturer fra forskjellige lag.

Forklaring? Som du kan gjette ut fra ulike indirekte symptomer, består serveren av mange prosesser av forskjellige typer på forskjellige maskiner. Mest sannsynlig la serveren som er ansvarlig for å "buffere" inn i køen det overordnede ga den, og de ga det i ordningen som var på plass på generasjonstidspunktet. Og inntil denne køen "råtten", kunne ingenting gjøres med det.

Kanskje... men dette er en forferdelig krykke?!.. Nei, før vi tenker på sprø ideer, la oss se på koden til de offisielle kundene. I Android-versjonen finner vi ingen TL-parser, men vi finner en heftig fil (GitHub nekter å røre den opp) med (de)serialisering. Her er kodebitene:

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 vilt ut. Men, sannsynligvis, dette er generert kode, så ok? .. Men det støtter absolutt alle versjoner! Riktignok er det ikke klart hvorfor alt er blandet sammen, hemmelige chatter og alt mulig _old7 ser liksom ikke ut som maskingenerering... Men mest av alt ble jeg imponert av

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Gutter, kan dere ikke engang bestemme hva som er inne i ett lag?! Vel, ok, la oss si at "to" ble utgitt med en feil, vel, det skjer, men TRE?.. Med en gang, samme rake igjen? Hva slags pornografi er dette, beklager?

I kildekoden til Telegram Desktop skjer forresten en lignende ting - i så fall endrer ikke flere forpliktelser på rad til ordningen lagnummeret, men fikser noe. I forhold der det ikke er noen offisiell datakilde for ordningen, hvor kan de hentes fra, bortsett fra kildekoden til den offisielle klienten? Og tar du det derfra kan du ikke være sikker på at opplegget er helt riktig før du tester alle metodene.

Hvordan kan dette i det hele tatt testes? Jeg håper fans av enhetstester, funksjonelle og andre tester vil dele i kommentarene.

Ok, la oss se på en annen kodebit:

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 kommentaren "manuelt opprettet" antyder at bare en del av denne filen ble skrevet manuelt (kan du forestille deg hele vedlikeholdsmarerittet?), og resten var maskingenerert. Men da dukker det opp et annet spørsmål - at kildene er tilgjengelige ikke helt (a la GPL-klatter i Linux-kjernen), men dette er allerede et tema for andre del.

Men nok. La oss gå videre til protokollen som all denne serialiseringen kjører på toppen av.

MT Proto

Så la oss åpne generell beskrivelse и detaljert beskrivelse av protokollen og det første vi snubler over er terminologien. Og med en overflod av alt. Generelt ser dette ut til å være en proprietær funksjon i Telegram - å kalle ting annerledes på forskjellige steder, eller forskjellige ting med ett ord, eller omvendt (for eksempel i et høyt nivå API, hvis du ser en klistremerkepakke, er det ikke hva du trodde).

For eksempel betyr "melding" og "økt" noe annet her enn i det vanlige Telegram-klientgrensesnittet. Vel, alt er klart med meldingen, den kan tolkes i OOP-termer, eller bare kalles ordet "pakke" - dette er et lavt transportnivå, det er ikke de samme meldingene som i grensesnittet, det er mange tjenestemeldinger . Men økten... men først.

transportlag

Det første er transport. De vil fortelle oss om 5 alternativer:

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

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

Og TCP i tre varianter

Den første ligner på UDP over TCP, hver pakke inneholder et sekvensnummer og crc
Hvorfor er det så vondt å lese dokumenter på en vogn?

Vel, der er det nå TCP allerede i 4 varianter:

  • forkortet
  • Mellom
  • Polstret mellomledd
  • Full

Vel, ok, polstret mellomliggende for MTProxy, dette ble senere lagt til på grunn av kjente hendelser. Men hvorfor to versjoner til (tre totalt) når du kunne klare deg med én? Alle fire er i hovedsak bare forskjellige i hvordan du stiller inn lengden og nyttelasten til hoved-MTProto, som vil bli diskutert videre:

  • i Forkortet er det 1 eller 4 byte, men ikke 0xef, da kroppen
  • i Intermediate er dette 4 byte lengde og et felt, og første gang klienten må sende 0xeeeeeeee for å indikere at det er mellomliggende
  • i sin helhet den mest vanedannende, sett fra en nettverksspillers synspunkt: lengde, sekvensnummer, og IKKE DEN som hovedsakelig er MTProto, body, CRC32. Ja, alt dette er på toppen av TCP. Som gir oss pålitelig transport i form av en sekvensiell bytestrøm; ingen sekvenser er nødvendig, spesielt sjekksummer. Ok, nå vil noen protestere mot meg at TCP har en 16-bits kontrollsum, så datakorrupsjon skjer. Flott, men vi har faktisk en kryptografisk protokoll med hashes lengre enn 16 byte, alle disse feilene – og enda flere – vil bli fanget opp av en SHA-mismatch på et høyere nivå. Det er INGEN vits i CRC32 på toppen av dette.

La oss sammenligne Abridged, der en byte med lengde er mulig, med Intermediate, som rettferdiggjør "I tilfelle 4-byte datajustering er nødvendig," som er ganske tull. Hva, det antas at Telegram-programmerere er så inkompetente at de ikke kan lese data fra en socket inn i en justert buffer? Du må fortsatt gjøre dette, fordi lesing kan returnere deg et hvilket som helst antall byte (og det finnes også proxy-servere, for eksempel...). Eller på den annen side, hvorfor blokkere Abridged hvis vi fortsatt vil ha heftig polstring på toppen av 16 byte - spar 3 byte noen ganger ?

Man får inntrykk av at Nikolai Durov virkelig liker å finne opp hjul på nytt, inkludert nettverksprotokoller, uten noe reelt praktisk behov.

Andre transportmuligheter, inkl. Web og MTProxy vil vi ikke vurdere nå, kanskje i et annet innlegg, hvis det er en forespørsel. Om denne samme MTProxy, la oss bare huske nå at kort tid etter utgivelsen i 2018 lærte leverandører raskt å blokkere den, beregnet på bypass blokkeringAv pakkestørrelse! Og også det faktum at MTProxy-serveren skrevet (igjen av Waltman) i C var altfor knyttet til Linux-spesifikasjoner, selv om dette ikke var nødvendig i det hele tatt (Phil Kulin vil bekrefte), og at en lignende server enten i Go eller Node.js ville passer i mindre enn hundre linjer.

Men vi vil trekke konklusjoner om den tekniske leseferdigheten til disse menneskene på slutten av avsnittet, etter å ha vurdert andre problemstillinger. For nå, la oss gå videre til OSI-lag 5, økt - som de plasserte MTProto-økten på.

Nøkler, meldinger, økter, Diffie-Hellman

De plasserte den der ikke helt riktig... En økt er ikke den samme økten som er synlig i grensesnittet under Aktive økter. Men i rekkefølge.

Kritikk av protokollen og organisatoriske tilnærminger til Telegram. Del 1, teknisk: erfaring med å skrive en klient fra bunnen av - TL, MT

Så vi mottok en bytestreng med kjent lengde fra transportlaget. Dette er enten en kryptert melding eller ren tekst - hvis vi fortsatt er på nøkkelavtalestadiet og faktisk gjør det. Hvilket av begrepene kalt "nøkkel" snakker vi om? La oss avklare dette problemet for Telegram-teamet selv (jeg beklager at jeg oversatte min egen dokumentasjon fra engelsk med en sliten hjerne klokken 4, det var lettere å la noen setninger være som de er):

Det er to enheter som kalles Session - en i brukergrensesnittet til offisielle klienter under "gjeldende økter", der hver økt tilsvarer en hel enhet / OS.
Den andre - MTProto-økt, som har sekvensnummeret til meldingen (i et lavt nivå) i seg, og som kan vare mellom forskjellige TCP-tilkoblinger. Flere MTProto-økter kan installeres samtidig, for eksempel for å øke hastigheten på nedlasting av filer.

Mellom disse to sesjoner det er et konsept autorisasjon. I det degenererte tilfellet kan vi si det UI-økt er det samme som autorisasjon, men dessverre, alt er komplisert. La oss se:

  • Brukeren på den nye enheten genererer først auth_key og binder det til konto, for eksempel via SMS - det er derfor autorisasjon
  • Det skjedde inne i den første MTProto-økt, som har session_id inne i deg selv.
  • På dette trinnet, kombinasjonen autorisasjon и session_id kunne kalles f.eks - dette ordet vises i dokumentasjonen og koden til enkelte klienter
  • Deretter kan klienten åpne noen MTProto-økter under det samme auth_key - til samme DC.
  • Så en dag må klienten be om filen fra en annen DC - og for denne DC vil en ny bli generert auth_key !
  • Å informere systemet om at det ikke er en ny bruker som registrerer seg, men den samme autorisasjon (UI-økt), bruker klienten API-kall auth.exportAuthorization hjemme DC auth.importAuthorization i den nye DC.
  • Alt er likt, flere kan være åpne MTProto-økter (hver med sin egen session_id) til denne nye DC, under hans auth_key.
  • Til slutt kan klienten ønske Perfect Forward Secrecy. Hver auth_key det var permanent nøkkel - per DC - og klienten kan ringe auth.bindTempAuthKey for bruk midlertidig auth_key - og igjen, bare én temp_auth_key per DC, felles for alle MTProto-økter til denne DC.

Legg merke til det salt (og fremtidige salter) er også en på auth_key de. delt mellom alle MTProto-økter til samme DC.

Hva betyr "mellom forskjellige TCP-tilkoblinger"? Så dette betyr noe som autorisasjonsinformasjonskapsel på et nettsted - den vedvarer (overlever) mange TCP-tilkoblinger til en gitt server, men en dag går den dårlig. Bare i motsetning til HTTP, i MTProto blir meldinger i en økt sekvensielt nummerert og bekreftet; hvis de kom inn i tunnelen, ble forbindelsen brutt - etter å ha opprettet en ny tilkobling, vil serveren vennligst sende alt i denne økten som den ikke leverte i forrige TCP-tilkobling.

Informasjonen ovenfor er imidlertid oppsummert etter mange måneders etterforskning. I mellomtiden, implementerer vi kunden vår fra bunnen av? - la oss gå tilbake til begynnelsen.

Så la oss generere auth_keyDiffie-Hellman-versjoner fra Telegram. La oss prøve å forstå dokumentasjonen...

Vasily, [19.06.18 20:05] data_with_hash := SHA1(data) + data + (alle tilfeldige byte); slik at lengden er lik 255 byte;
krypterte_data := RSA(data_med_hash, server_offentlig_nøkkel); et 255-byte langt tall (big endian) heves til den nødvendige makten over den nødvendige modulen, og resultatet lagres som et 256-byte tall.

De har noe dope DH

Ser ikke ut som en sunn persons DH
Det er ikke to offentlige nøkler i dx

Vel, til slutt ble dette ordnet, men en rest gjensto - bevis på arbeid er utført av klienten på at han var i stand til å faktorisere antallet. Type beskyttelse mot DoS-angrep. Og RSA-nøkkelen brukes bare én gang i én retning, hovedsakelig for kryptering new_nonce. Men selv om denne tilsynelatende enkle operasjonen vil lykkes, hva må du møte?

Vasily, [20.06.18/00/26 XNUMX:XNUMX] Jeg har ikke kommet til den aktuelle forespørselen ennå

Jeg sendte denne forespørselen til DH

Og i transportdokken står det at den kan svare med 4 byte med en feilkode. Det er alt

Vel, han fortalte meg -404, så hva?

Så jeg sa til ham: "Fang tullet ditt kryptert med en servernøkkel med et fingeravtrykk som dette, jeg vil ha DH," og det svarte med en dum 404

Hva ville du synes om dette serversvaret? Hva å gjøre? Det er ingen å spørre (men mer om det i andre del).

Her gjøres all interesse på kaien

Jeg har ikke noe annet å gjøre, jeg drømte bare om å konvertere tall frem og tilbake

To 32 bit tall. Jeg pakket dem som alle andre

Men nei, disse to må legges til linjen først som BE

Vadim Goncharov, [20.06.18 15:49] og på grunn av dette 404?

Vasily, [20.06.18 15:49] JA!

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

Vasily, [20.06.18 15:50] omtrent

Jeg kunne ikke finne en slik dekomponering i primfaktorer%)

Vi klarte ikke engang feilrapportering

Vasily, [20.06.18 20:18] Å, det er også MD5. Allerede tre forskjellige hasjer

Nøkkelfingeravtrykket beregnes som følger:

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

SHA1 og sha2

Så la oss si det auth_key vi mottok 2048 biter i størrelse ved å bruke Diffie-Hellman. Hva blir det neste? Deretter oppdager vi at de nedre 1024 bitene av denne nøkkelen ikke brukes på noen måte ... men la oss tenke på dette for nå. På dette trinnet har vi en delt hemmelighet med serveren. En analog av TLS-sesjonen er etablert, som er en svært kostbar prosedyre. Men serveren vet fortsatt ingenting om hvem vi er! Ikke ennå, faktisk. autorisasjon. De. hvis du tenkte i termer av "påloggingspassord", som du en gang gjorde i ICQ, eller i det minste "påloggingsnøkkel", som i SSH (for eksempel på noen gitlab/github). Vi fikk en anonym. Hva om serveren forteller oss "disse telefonnumrene betjenes av en annen DC"? Eller til og med "telefonnummeret ditt er utestengt"? Det beste vi kan gjøre er å beholde nøkkelen i håp om at den kommer til nytte og ikke blir råtten da.

Vi "mottok" den forresten med forbehold. Stoler vi for eksempel på serveren? Hva om det er falskt? Kryptografiske kontroller vil være nødvendig:

Vasily, [21.06.18 17:53] De tilbyr mobilklienter å sjekke et 2kbit-nummer for primalitet%)

Men det er ikke klart i det hele tatt, nafeijoa

Vasily, [21.06.18 18:02] Dokumentet sier ikke hva du skal gjøre hvis det viser seg å ikke være enkelt

Ikke sagt. La oss se hva den offisielle Android-klienten gjør i dette tilfellet? EN det er hva (og ja, hele filen er interessant) - som de sier, jeg lar dette være her:

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

Nei, selvfølgelig er den der fortsatt noen Det er tester for primaliteten til et tall, men personlig har jeg ikke lenger tilstrekkelig kunnskap om matematikk.

Ok, vi har hovednøkkelen. For å logge inn, dvs. sende forespørsler, må du utføre ytterligere kryptering ved å bruke AES.

Meldingsnøkkelen er definert som de 128 midterste bitene av SHA256 til meldingsteksten (inkludert sesjon, meldings-ID, etc.), inkludert utfyllingsbyte, forankret av 32 byte hentet fra autorisasjonsnøkkelen.

Vasily, [22.06.18 14:08] Gjennomsnittlig, tispe, biter

Fikk det auth_key. Alle. Utover dem ... er det ikke klart av dokumentet. Studer gjerne den åpne kildekoden.

Merk at MTProto 2.0 krever fra 12 til 1024 byte med utfylling, fortsatt under forutsetning av at den resulterende meldingslengden er delelig med 16 byte.

Så hvor mye polstring bør du legge til?

Og ja, det er også en 404 i tilfelle feil

Hvis noen nøye studerte diagrammet og teksten til dokumentasjonen, la de merke til at det ikke er noen MAC der. Og at AES brukes i en viss IGE-modus som ikke brukes andre steder. De skriver selvfølgelig om dette i deres FAQ... Her er meldingsnøkkelen i seg selv også SHA-hashen til de dekrypterte dataene, brukt til å sjekke integriteten - og i tilfelle mismatch, dokumentasjonen av en eller annen grunn anbefaler å ignorere dem i stillhet (men hva med sikkerhet, hva om de knekker oss?).

Jeg er ingen kryptograf, kanskje det ikke er noe galt med denne modusen i dette tilfellet fra et teoretisk synspunkt. Men jeg kan tydelig nevne et praktisk problem, ved å bruke Telegram Desktop som et eksempel. Den krypterer den lokale cachen (alle disse D877F783D5D3EF8C) på samme måte som meldinger i MTProto (bare i dette tilfellet versjon 1.0), dvs. først meldingsnøkkelen, deretter selve dataene (og et sted til side de store auth_key 256 byte, uten hvilke msg_key ubrukelig). Så problemet blir merkbart på store filer. Du må nemlig beholde to kopier av dataene - kryptert og dekryptert. Og hvis det er megabyte, eller streaming video, for eksempel? .. Klassiske ordninger med MAC etter chifferteksten lar deg lese den streame, umiddelbart overføre den. Men med MTProto må du i begynnelsen krypter eller dekrypter hele meldingen, bare deretter overføre den til nettverket eller til disken. Derfor, i de nyeste versjonene av Telegram Desktop i cachen i user_data Et annet format brukes også - med AES i CTR-modus.

Vasily, [21.06.18 01:27] Å, jeg fant ut hva IGE er: IGE var det første forsøket på en "autentiseringskrypteringsmodus", opprinnelig for Kerberos. Det var et mislykket forsøk (det gir ikke integritetsbeskyttelse), og måtte fjernes. Det var begynnelsen på en 20 år lang søken etter en autentiseringskrypteringsmodus som fungerer, som nylig kulminerte i moduser som OCB og GCM.

Og nå argumentene fra vognsiden:

Teamet bak Telegram, ledet av Nikolai Durov, består av seks ACM-mestere, halvparten av dem Ph.D-er i matte. Det tok dem omtrent to år å rulle ut den nåværende versjonen av MTProto.

Det er morsomt. To år på lavere nivå

Eller du kan bare ta tls

Ok, la oss si at vi har gjort krypteringen og andre nyanser. Er det endelig mulig å sende forespørsler serialisert i TL og deserialisere svarene? Så hva og hvordan skal du sende? Her, la oss si, metoden initConnection, kanskje dette er det?

Vasily, [25.06.18 18:46] Initialiserer tilkobling og lagrer informasjon på brukerens enhet og applikasjon.

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

Og noen spørringer

Dokumentasjon som alltid. Studer gjerne åpen kildekode

Hvis alt var omtrent klart med invokeWithLayer, hva er så galt her? Det viser seg, la oss si at vi har - klienten allerede hadde noe å spørre serveren om - det er en forespørsel vi ønsket å sende:

Vasily, [25.06.18 19:13] Etter koden å dømme, er den første samtalen pakket inn i denne dritten, og selve dritten er pakket inn i invokewithlayer

Hvorfor kunne ikke initConnection være et eget anrop, men må være et omslag? Ja, som det viste seg, må det gjøres hver gang i begynnelsen av hver økt, og ikke en gang, som med hovednøkkelen. Men! Den kan ikke ringes opp av en uautorisert bruker! Nå har vi nådd det stadiet hvor det er aktuelt Denne dokumentasjonssiden - og den forteller oss at...

Bare en liten del av API-metodene er tilgjengelige for uautoriserte brukere:

  • 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 aller første av dem, auth.sendCode, og det er den kjære første forespørselen der vi sender api_id og api_hash, og deretter mottar vi en SMS med en kode. Og hvis vi er i feil DC (telefonnumre i dette landet betjenes for eksempel av en annen), så får vi en feil med nummeret til ønsket DC. For å finne ut hvilken IP-adresse etter DC-nummer du må koble til, hjelp oss help.getConfig. På en gang var det bare 5 påmeldinger, men etter de berømte begivenhetene i 2018 har antallet økt betydelig.

La oss nå huske at vi kom til dette stadiet på serveren anonymt. Er det ikke for dyrt å bare få en IP-adresse? Hvorfor ikke gjøre dette, og andre operasjoner, i den ukrypterte delen av MTProto? Jeg hører innvendingen: "hvordan kan vi sikre at det ikke er RKN som svarer med falske adresser?" Til dette husker vi at generelt offisielle kunder RSA-nøkler er innebygd, dvs. kan du bare skilt denne informasjonen. Dette gjøres faktisk allerede for informasjon om å omgå blokkering som klienter mottar gjennom andre kanaler (logisk sett kan dette ikke gjøres i selve MTProto; du må også vite hvor du skal koble til).

OK. På dette stadiet av klientautorisasjon er vi ennå ikke autorisert og har ikke registrert søknaden vår. Vi vil bare se for nå hva serveren reagerer på metoder som er tilgjengelige for en uautorisert bruker. 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 andre rekke

I tdesktop-skjemaet er den tredje verdien

Ja, siden har selvfølgelig dokumentasjonen blitt oppdatert. Selv om det fort kan bli irrelevant igjen. Hvordan bør en nybegynner utvikler vite det? Kanskje hvis du registrerer søknaden din, vil de informere deg? Vasily gjorde dette, men dessverre, de sendte ham ikke noe (igjen, vi snakker om dette i andre del).

...Du la merke til at vi allerede på en eller annen måte har flyttet til API, dvs. til neste nivå, og gikk glipp av noe i MTProto-emnet? Ingen overraskelse:

Vasily, [28.06.18 02:04] Mm, de roter gjennom noen av algoritmene på e2e

Mtproto definerer krypteringsalgoritmer og nøkler for begge domenene, så vel som litt av en innpakningsstruktur

Men de blander hele tiden forskjellige nivåer av stabelen, så det er ikke alltid klart hvor mtproto sluttet og neste nivå begynte

Hvordan blandes de? Vel, her er den samme midlertidige nøkkelen for PFS, for eksempel (forresten, Telegram Desktop kan ikke gjøre det). Den utføres av en API-forespørsel auth.bindTempAuthKey, dvs. fra øverste nivå. Men samtidig forstyrrer det kryptering på lavere nivå - etter det, for eksempel, må du gjøre det igjen initConnection osv., dette er ikke bare vanlig forespørsel. Det som også er spesielt er at du bare kan ha EN midlertidig nøkkel per DC, selv om feltet auth_key_id i hver melding lar deg endre nøkkelen i det minste hver melding, og at serveren har rett til å "glemme" den midlertidige nøkkelen når som helst - dokumentasjonen sier ikke hva du skal gjøre i dette tilfellet... vel, hvorfor kunne ikke har du ikke flere nøkler, som med et sett med fremtidige salter, og ?..

Det er noen andre ting som er verdt å merke seg om MTProto-temaet.

Meldingsmeldinger, msg_id, msg_seqno, bekreftelser, ping i feil retning og andre særegenheter

Hvorfor trenger du å vite om dem? Fordi de "lekker" til et høyere nivå, og du må være oppmerksom på dem når du arbeider med API. La oss anta at vi ikke er interessert i msg_key; det lavere nivået har dekryptert alt for oss. Men inne i de dekrypterte dataene har vi følgende felt (også lengden på dataene, så vi vet hvor utfyllingen er, men det er ikke viktig):

  • salt - int64
  • session_id - int64
  • meldings-id — int64
  • seq_no - int32

La oss minne deg på at det bare er ett salt for hele DC. Hvorfor vite om henne? Ikke bare fordi det er en forespørsel get_future_salts, som forteller deg hvilke intervaller som vil være gyldige, men også fordi hvis saltet ditt er "råttent", så vil meldingen (forespørselen) rett og slett gå tapt. Serveren vil selvfølgelig rapportere det nye saltet ved å utstede new_session_created - men med den gamle må du sende den på nytt på en eller annen måte, for eksempel. Og dette problemet påvirker applikasjonsarkitekturen.

Serveren har lov til å droppe økter helt og svare på denne måten av mange grunner. Hva er egentlig en MTProto-økt fra klientsiden? Dette er to tall session_id и seq_no meldinger i denne økten. Vel, og den underliggende TCP-forbindelsen, selvfølgelig. La oss si at klienten vår fortsatt ikke vet hvordan han skal gjøre mange ting, han koblet fra og koblet til igjen. Hvis dette skjedde raskt - den gamle økten fortsatte i den nye TCP-tilkoblingen, øk seq_no lengre. Hvis det tar lang tid, kan serveren slette det, for på siden er det også en kø, som vi fant ut.

Hva skal det være seq_no? Å, det er et vanskelig spørsmål. Prøv å ærlig forstå hva som ble ment:

Innholdsrelatert melding

En melding som krever en eksplisitt bekreftelse. Disse inkluderer alle bruker- og mange tjenestemeldinger, praktisk talt alle med unntak av containere og bekreftelser.

Meldingssekvensnummer (msg_seqno)

Et 32-bits tall lik dobbelt så mange "innholdsrelaterte" meldinger (de som krever bekreftelse, og spesielt de som ikke er containere) opprettet av avsenderen før denne meldingen og deretter økt med én hvis gjeldende melding er en innholdsrelatert melding. En beholder genereres alltid etter hele innholdet; derfor er sekvensnummeret større enn eller lik sekvensnumrene til meldingene i den.

Hva slags sirkus er dette med en økning med 1, og så en annen med 2?.. Jeg mistenker at de i utgangspunktet betydde "den minst signifikante biten for ACK, resten er et tall", men resultatet er ikke helt det samme - spesielt, det kommer ut, kan sendes noen bekreftelser som har det samme seq_no! Hvordan? Vel, for eksempel, serveren sender oss noe, sender det, og vi selv forblir stille, og svarer bare med tjenestemeldinger som bekrefter mottak av meldingene. I dette tilfellet vil våre utgående bekreftelser ha samme utgående nummer. Hvis du er kjent med TCP og trodde at dette høres vilt ut, men det virker ikke veldig vilt, fordi i TCP seq_no endres ikke, men bekreftelsen går til seq_no på den andre siden vil jeg skynde meg å gjøre deg opprørt. Bekreftelser er gitt i MTProto IKKEseq_no, som i TCP, men ved msg_id !

Hva er dette msg_id, det viktigste av disse feltene? En unik meldingsidentifikator, som navnet antyder. Det er definert som et 64-bits tall, hvor de laveste bitene igjen har "server-ikke-server"-magien, og resten er et Unix-tidsstempel, inkludert brøkdelen, forskjøvet 32 ​​biter til venstre. De. tidsstempel i seg selv (og meldinger med tider som varierer for mye vil bli avvist av serveren). Av dette viser det seg at dette generelt er en identifikator som er global for klienten. Gitt det - la oss huske session_id - vi er garantert: Under ingen omstendigheter kan en melding ment for én økt sendes til en annen sesjon. Det vil si at det viser seg at det allerede er det tre nivå - økt, sesjonsnummer, meldings-id. Hvorfor en slik overkomplikasjon, dette mysteriet er veldig stort.

således msg_id nødvendig for...

RPC: forespørsler, svar, feil. Bekreftelser.

Som du kanskje har lagt merke til, er det ingen spesiell "gjør en RPC-forespørsel"-type eller funksjon noe sted i diagrammet, selv om det finnes svar. Vi har tross alt innholdsrelaterte meldinger! Det er, noen meldingen kan være en forespørsel! Eller ikke være det. Tross alt, av hver det er msg_id. Men det finnes svar:

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

Det er her det er indikert hvilken melding dette er et svar på. Derfor, på toppnivået av APIen, må du huske hva nummeret på forespørselen din var - jeg tror det ikke er nødvendig å forklare at arbeidet er asynkront, og det kan være flere forespørsler på gang samtidig, svarene som kan returneres i hvilken som helst rekkefølge? I prinsippet, fra dette og feilmeldinger som ingen arbeidere, kan arkitekturen bak dette spores: serveren som opprettholder en TCP-forbindelse med deg er en front-end balanserer, den videresender forespørsler til backends og samler dem tilbake via message_id. Det virker som alt her er oversiktlig, logisk og bra.

Ja?.. Og hvis du tenker deg om? Selve RPC-responsen har tross alt også et felt msg_id! Trenger vi å rope på serveren "du svarer ikke på svaret mitt!"? Og ja, hva var det med konfirmasjoner? Om side meldinger om meldinger forteller oss hva som er

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

og det må gjøres av hver side. Men ikke alltid! Hvis du mottok et RpcResult, fungerer det selv som en bekreftelse. Det vil si at serveren kan svare på forespørselen din med MsgsAck - som "Jeg mottok den." RpcResult kan svare umiddelbart. Det kan være begge deler.

Og ja, du må fortsatt svare på svaret! Bekreftelse. Ellers vil serveren vurdere det som ikke-leverbart og sende det tilbake til deg igjen. Selv etter gjentilkobling. Men her dukker selvfølgelig spørsmålet om timeouts opp. La oss se på dem litt senere.

I mellomtiden, la oss se på mulige spørringsutførelsesfeil.

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

Å, vil noen utbryte, her er et mer humant format – det er en strek! Ta den tiden du trenger. Her liste over feil, men selvfølgelig ikke komplett. Av den lærer vi at koden er noe som HTTP-feil (vel, selvfølgelig, semantikken til svarene blir ikke respektert, noen steder er de fordelt tilfeldig mellom kodene), og linjen ser ut som CAPITAL_LETTERS_AND_NUMBERS. For eksempel PHONE_NUMBER_OCCUPIED eller FILE_PART_Х_MISSING. Vel, det vil si at du fortsatt trenger denne linjen analysere. For eksempel, FLOOD_WAIT_3600 vil bety at du må vente en time, og PHONE_MIGRATE_5, at et telefonnummer med dette prefikset må registreres i 5. DC. Vi har et typespråk, ikke sant? Vi trenger ikke et argument fra en streng, vanlige vil gjøre det, ok.

Igjen, dette er ikke på tjenestemeldingssiden, men som allerede er vanlig med dette prosjektet, kan informasjonen bli funnet på en annen dokumentasjonsside. eller gi mistanke. For det første, se, skriving/lagbrudd - RpcError kan legges inn RpcResult. Hvorfor ikke ute? Hva tok vi ikke hensyn til?.. Følgelig, hvor er garantien for at RpcError kan IKKE være innebygd i RpcResult, men være direkte eller nestet i en annen type?.. Og hvis den ikke kan, hvorfor er den ikke på toppnivå, dvs. det mangler req_msg_id ? ..

Men la oss fortsette med tjenestemeldinger. Klienten kan tro at serveren tenker lenge og kommer med denne fantastiske forespørselen:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Det er tre mulige svar på dette spørsmålet, som igjen krysser bekreftelsesmekanismen; å prøve å forstå hva de skal være (og hva den generelle listen over typer som ikke krever bekreftelse) overlates til leseren som lekser (merk: informasjonen i Telegram Desktop-kildekoden er ikke fullstendig).

Narkotikaavhengighet: meldingsstatuser

Generelt sett etterlater mange steder i TL, MTProto og Telegram generelt en følelse av sta, men av høflighet, takt og andre myke ferdigheter Vi tiet høflig om det, og sensurerte uanstendighetene i dialogene. Men dette stedetОmesteparten av siden handler om meldinger om meldinger Det er sjokkerende selv for meg, som har jobbet med nettverksprotokoller i lang tid og har sett sykler med ulik grad av skjevhet.

Det starter uskyldig, med bekreftelser. Neste forteller de oss 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;

Vel, alle som begynner å jobbe med MTProto må forholde seg til dem; i syklusen "korrigert - rekompilert - lansert" er det vanlig å få tallfeil eller salt som har klart å gå dårlig under redigeringer. Det er imidlertid to punkter her:

  1. Dette betyr at den opprinnelige meldingen går tapt. Vi må lage noen køer, vi skal se på det senere.
  2. Hva er disse merkelige feiltallene? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64... hvor er de andre tallene, Tommy?

I dokumentasjonen står det:

Hensikten er at error_code-verdier skal grupperes (error_code >> 4): for eksempel tilsvarer kodene 0x40 — 0x4f feil i beholderdekomponering.

men for det første et skifte i den andre retningen, og for det andre spiller det ingen rolle, hvor er de andre kodene? I forfatterens hode?.. Dette er imidlertid bagateller.

Avhengighet begynner i meldinger om meldingsstatuser og meldingskopier:

  • Forespørsel om meldingsstatusinformasjon
    Hvis en av partene ikke har mottatt informasjon om statusen til sine utgående meldinger på en stund, kan den eksplisitt be om det fra den andre parten:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Informasjonsmelding om status for meldinger
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Her info er en streng som inneholder nøyaktig én byte med meldingsstatus for hver melding fra listen over innkommende msg_ids:

    • 1 = ingenting er kjent om meldingen (msg_id for lav, den andre parten kan ha glemt den)
    • 2 = melding ikke mottatt (msg_id faller innenfor rekkevidden av lagrede identifikatorer, men den andre parten har absolutt ikke mottatt en slik melding)
    • 3 = melding ikke mottatt (msg_id for høy, men den andre parten har absolutt ikke mottatt den ennå)
    • 4 = melding mottatt (merk at dette svaret også samtidig er en mottaksbekreftelse)
    • +8 = melding er allerede bekreftet
    • +16 = melding som ikke krever bekreftelse
    • +32 = RPC-spørring i meldingen som behandles eller behandlingen allerede er fullført
    • +64 = innholdsrelatert svar på melding som allerede er generert
    • +128 = den andre parten vet at meldingen allerede er mottatt
      Dette svaret krever ikke en bekreftelse. Det er en bekreftelse av den relevante msgs_state_req, i seg selv.
      Merk at hvis det plutselig viser seg at den andre parten ikke har en melding som ser ut som den er sendt til seg, kan meldingen ganske enkelt sendes på nytt. Selv om den andre parten skulle motta to kopier av meldingen samtidig, vil duplikatet bli ignorert. (Hvis det har gått for lang tid, og den originale msg_id ikke lenger er gyldig, skal meldingen pakkes inn i msg_copy).
  • Frivillig kommunikasjon av status for meldinger
    Hver av partene kan frivillig informere den andre parten om statusen til meldingene som sendes av den andre parten.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Utvidet frivillig kommunikasjon av status for én melding
    ...
    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;
  • Eksplisitt forespørsel om å sende meldinger på nytt
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    Den eksterne parten svarer umiddelbart ved å sende de forespurte meldingene på nytt […]
  • Eksplisitt forespørsel om å sende svar på nytt
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    Den eksterne parten svarer umiddelbart ved å sende på nytt svar til de forespurte meldingene […]
  • Meldingskopier
    I noen situasjoner må en gammel melding med en msg_id som ikke lenger er gyldig sendes på nytt. Deretter er den pakket inn i en kopibeholder:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Når den er mottatt, behandles meldingen som om omslaget ikke var der. Men hvis det er kjent med sikkerhet at meldingen orig_message.msg_id ble mottatt, blir ikke den nye meldingen behandlet (mens den og orig_message.msg_id samtidig bekreftes). Verdien av orig_message.msg_id må være lavere enn beholderens msg_id.

La oss til og med tie om hva msgs_state_info igjen stikker ørene til den uferdige TL ut (vi trengte en vektor med byte, og i de to nederste bitene var det en enum, og i de to høyere bitene var det flagg). Poenget er et annet. Er det noen som forstår hvorfor alt dette er i praksis? i en ekte klient nødvendig?.. Med vanskeligheter, men man kan forestille seg en fordel hvis en person er engasjert i feilsøking, og i en interaktiv modus - spør serveren hva og hvordan. Men her er forespørslene beskrevet rundtur.

Det følger at hver part ikke bare må kryptere og sende meldinger, men også lagre data om seg selv, om svarene på dem, i en ukjent tidsperiode. Dokumentasjonen beskriver verken tidspunktene eller den praktiske anvendeligheten til disse funksjonene. på ingen måte. Det som er mest utrolig er at de faktisk brukes i koden for offisielle klienter! Tilsynelatende ble de fortalt noe som ikke var inkludert i den offentlige dokumentasjonen. Forstå fra koden Hvorfor, er ikke lenger så enkelt som i tilfellet med TL - det er ikke en (relativt) logisk isolert del, men en del knyttet til applikasjonsarkitekturen, dvs. vil kreve betydelig mer tid for å forstå applikasjonskoden.

Pinger og timing. Køer.

Fra alt, hvis vi husker gjetningene om serverarkitekturen (fordeling av forespørsler på tvers av backends), følger en ganske trist ting - til tross for alle leveringsgarantiene i TCP (enten blir dataene levert, eller du vil bli informert om gapet, men dataene vil bli levert før problemet oppstår), at bekreftelser i selve MTProto - ingen garantier. Serveren kan lett miste eller kaste ut meldingen din, og ingenting kan gjøres med det, bare bruk forskjellige typer krykker.

Og først av alt - meldingskøer. Vel, med én ting var alt åpenbart helt fra begynnelsen - en ubekreftet melding må lagres og sendes på ny. Og etter hvilken tid? Og narren kjenner ham. Kanskje løser de avhengige tjenestemeldingene på en eller annen måte dette problemet med krykker, for eksempel i Telegram Desktop er det omtrent 4 køer som tilsvarer dem (kanskje flere, som allerede nevnt, for dette må du fordype deg i koden og arkitekturen mer seriøst; samtidig tid, vi Vi vet at det ikke kan tas som en prøve; et visst antall typer fra MTProto-ordningen brukes ikke i den).

Hvorfor skjer dette? Sannsynligvis var serverprogrammererne ikke i stand til å sikre pålitelighet i klyngen, eller til og med bufring på frontbalanseren, og overførte dette problemet til klienten. Av fortvilelse prøvde Vasily å implementere et alternativt alternativ, med bare to køer, ved å bruke algoritmer fra TCP - måling av RTT til serveren og justering av størrelsen på "vinduet" (i meldinger) avhengig av antall ubekreftede forespørsler. Det vil si at en så grov heuristikk for å vurdere serverens belastning er hvor mange av våre forespørsler den kan tygge på samme tid og ikke tape.

Vel, det vil si, du forstår, ikke sant? Hvis du må implementere TCP igjen på toppen av en protokoll som kjører over TCP, indikerer dette en svært dårlig utformet protokoll.

Å ja, hvorfor trenger du mer enn én kø, og hva betyr dette for en person som jobber med en API på høyt nivå? Se, du gjør en forespørsel, serialiserer den, men ofte kan du ikke sende den umiddelbart. Hvorfor? For svaret vil være msg_id, som er midlertidigаJeg er en etikett, hvis tildeling best utsettes til så sent som mulig - i tilfelle serveren avviser det på grunn av tidsforstyrrelser mellom oss og ham (selvfølgelig kan vi lage en krykke som flytter tiden vår fra nåtiden til serveren ved å legge til et delta beregnet fra serverens svar - offisielle klienter gjør dette, men det er grovt og unøyaktig på grunn av buffering). Derfor, når du foretar en forespørsel med et lokalt funksjonsanrop fra biblioteket, går meldingen gjennom følgende stadier:

  1. Den ligger i én kø og venter på kryptering.
  2. Utnevnt msg_id og meldingen gikk til en annen kø - mulig videresending; sende til stikkontakten.
  3. a) Serveren svarte MsgsAck - meldingen ble levert, vi sletter den fra "andre køen".
    b) Eller omvendt, han likte ikke noe, svarte han badmsg - send på nytt fra "en annen kø"
    c) Ingenting er kjent, meldingen må sendes på nytt fra en annen kø - men det er ikke kjent nøyaktig når.
  4. Serveren svarte til slutt RpcResult - selve svaret (eller feilen) - ikke bare levert, men også behandlet.

Kanskje, kan bruk av containere delvis løse problemet. Dette er når en haug med meldinger er pakket inn i én, og serveren svarte med en bekreftelse på dem alle samtidig, i en msg_id. Men han vil også avvise denne pakken, hvis noe gikk galt, i sin helhet.

Og på dette punktet spiller ikke-tekniske hensyn inn. Erfaringsmessig har vi sett mange krykker, og i tillegg vil vi nå se flere eksempler på dårlig råd og arkitektur – er det i slike forhold verdt å stole på og ta slike beslutninger? Spørsmålet er retorisk (selvfølgelig ikke).

Hva snakker vi om? Hvis du fortsatt kan spekulere på temaet "narkotikameldinger om meldinger" med innvendinger som "du er dum, du forsto ikke vår geniale plan!" (så skriv dokumentasjonen først, som vanlige folk skal, med begrunnelse og eksempler på pakkeutveksling, så snakkes vi), så er timings/timeouts et rent praktisk og spesifikt spørsmål, alt her har vært kjent lenge. Hva forteller dokumentasjonen oss om tidsavbrudd?

En server bekrefter vanligvis mottak av en melding fra en klient (normalt en RPC-spørring) ved hjelp av et RPC-svar. Hvis et svar er lenge på vei, kan en server først sende en mottaksbekreftelse, og noe senere, selve RPC-svaret.

En klient bekrefter vanligvis mottak av en melding fra en server (vanligvis et RPC-svar) ved å legge til en bekreftelse til neste RPC-spørring hvis den ikke er overført for sent (hvis den genereres, for eksempel, 60–120 sekunder etter mottaket av en melding fra serveren). Men hvis det over en lengre periode ikke er noen grunn til å sende meldinger til serveren eller hvis det er et stort antall ubekreftede meldinger fra serveren (for eksempel over 16), sender klienten en frittstående bekreftelse.

... jeg oversetter: vi selv vet ikke hvor mye og hvordan vi trenger det, så la oss anta at la det være slik.

Og om ping:

Ping-meldinger (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

Et svar returneres vanligvis til samme tilkobling:

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

Disse meldingene krever ikke bekreftelse. En pong overføres bare som svar på et ping mens en ping kan startes av begge sider.

Utsatt stenging av tilkobling + PING

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

Fungerer som ping. I tillegg, etter at dette er mottatt, starter serveren en timer som vil lukke gjeldende tilkobling disconnect_delay sekunder senere med mindre den mottar en ny melding av samme type som automatisk tilbakestiller alle tidligere tidtakere. Hvis klienten for eksempel sender disse pingene en gang hvert 60. sekund, kan den sette disconnect_delay lik 75 sekunder.

Er du gal?! Om 60 sekunder skal toget inn på stasjonen, slippe av og plukke opp passasjerer, og igjen miste kontakten i tunnelen. Om 120 sekunder, mens du hører den, kommer den til en annen, og forbindelsen vil mest sannsynlig bryte. Vel, det er tydelig hvor bena kommer fra - "Jeg hørte en ringing, men vet ikke hvor den er", det er Nagls algoritme og alternativet TCP_NODELAY, beregnet for interaktivt arbeid. Men unnskyld meg, hold fast på standardverdien - 200 Millisekunder Hvis du virkelig ønsker å avbilde noe lignende og lagre på et par mulige pakker, så utsett det i 5 sekunder, eller hva "Bruker skriver..."-meldingstidsavbruddet er nå. Men ikke mer.

Og til slutt, pings. Det vil si å sjekke liveness av TCP-tilkoblingen. Det er morsomt, men for omtrent 10 år siden skrev jeg en kritisk tekst om budbringeren til fakultetets hybel - forfatterne der pinget også serveren fra klienten, og ikke omvendt. Men 3. års studenter er én ting, og et internasjonalt kontor er en annen, ikke sant?

Først et lite pedagogisk program. En TCP-forbindelse, i fravær av pakkeutveksling, kan leve i flere uker. Dette er både bra og dårlig, avhengig av formålet. Det er bra hvis du hadde en SSH-tilkobling åpen til serveren, du reiste deg fra datamaskinen, startet ruteren på nytt, kom tilbake til stedet ditt - økten gjennom denne serveren ble ikke revet (du skrev ikke noe, det var ingen pakker) , det er praktisk. Det er ille hvis det er tusenvis av klienter på serveren som hver tar opp ressurser (hei, Postgres!), og klientens vert kan ha startet på nytt for lenge siden - men vi får ikke vite om det.

Chat/IM-systemer faller inn i det andre tilfellet av en ekstra grunn - nettstatuser. Hvis brukeren "falt av", må du informere samtalepartnerne hans om dette. Ellers vil du ende opp med en feil som skaperne av Jabber gjorde (og korrigert i 20 år) - brukeren har koblet fra, men de fortsetter å skrive meldinger til ham, og tror at han er online (som også gikk helt tapt i disse noen minutter før frakoblingen ble oppdaget). Nei, alternativet TCP_KEEPALIVE, som mange mennesker som ikke forstår hvordan TCP-timere fungerer, kaster inn tilfeldig (ved å sette wild-verdier som titalls sekunder), vil ikke hjelpe her - du må sørge for at ikke bare OS-kjernen av brukerens maskin er i live, men fungerer også normalt, i stand til å svare, og selve applikasjonen (tror du den ikke kan fryse? Telegram Desktop på Ubuntu 18.04 frøs for meg mer enn én gang).

Det er derfor du må pinge serveren klient, og ikke omvendt - hvis klienten gjør dette, hvis forbindelsen er brutt, vil ping ikke bli levert, målet vil ikke bli oppnådd.

Hva ser vi på Telegram? Det er stikk motsatt! Vel, altså. Formelt sett kan selvfølgelig begge sider pinge hverandre. I praksis bruker klienter en krykke ping_delay_disconnect, som setter tidtakeren på serveren. Vel, unnskyld meg, det er ikke opp til klienten å bestemme hvor lenge han vil bo der uten ping. Serveren, basert på belastningen, vet bedre. Men, selvfølgelig, hvis du ikke har noe imot ressursene, vil du være din egen onde Pinocchio, og en krykke vil gjøre ...

Hvordan burde den vært utformet?

Jeg mener at de ovennevnte fakta tydelig indikerer at Telegram/VKontakte-teamet ikke er særlig kompetent innen transport (og lavere) nivå av datanettverk og deres lave kvalifikasjoner i relevante saker.

Hvorfor viste det seg å være så komplisert, og hvordan kan Telegram-arkitekter prøve å protestere? Det at de prøvde å lage en økt som overlever TCP-tilkoblingsbrudd, det vil si det som ikke ble levert nå, skal vi levere senere. De prøvde sannsynligvis også å lage en UDP-transport, men de møtte vanskeligheter og forlot den (det er derfor dokumentasjonen er tom - det var ingenting å skryte av). Men på grunn av manglende forståelse for hvordan nettverk generelt og TCP spesielt fungerer, hvor du kan stole på det, og hvor du må gjøre det selv (og hvordan), og et forsøk på å kombinere dette med kryptografi «two birds with one stone», dette er resultatet.

Hvordan var det nødvendig? Basert på det faktum at msg_id er et tidsstempel som er nødvendig fra et kryptografisk synspunkt for å forhindre replay-angrep, er det en feil å knytte en unik identifikatorfunksjon til den. Derfor, uten å endre den nåværende arkitekturen fundamentalt (når oppdateringsstrømmen genereres, er det et API-emne på høyt nivå for en annen del av denne serien med innlegg), må man:

  1. Serveren som holder TCP-forbindelsen til klienten tar ansvar - hvis den har lest fra stikkontakten, vennligst erkjenne, behandle eller returnere en feil, uten tap. Da er ikke bekreftelsen en vektor av ids, men ganske enkelt "den siste mottatte seq_no" - bare et tall, som i TCP (to tall - din seq og den bekreftede). Vi er alltid innenfor økten, er vi ikke?
  2. Tidsstemplet for å forhindre gjentaksangrep blir et eget felt, a la nonce. Det er sjekket, men påvirker ikke noe annet. Nok og uint32 - hvis saltet vårt endres minst hver halve dag, kan vi allokere 16 biter til lavordensbitene av en heltallsdel av gjeldende tid, resten - til en brøkdel av et sekund (som nå).
  3. Fjernet msg_id i det hele tatt - fra synspunktet om å skille forespørsler på backends, er det for det første klient-IDen, og for det andre sesjons-IDen, koble dem sammen. Følgelig er bare én ting tilstrekkelig som forespørselsidentifikator seq_no.

Dette er heller ikke det mest vellykkede alternativet; en fullstendig tilfeldighet kan tjene som en identifikator - dette gjøres forresten allerede i høynivå-API-en når du sender en melding. Det ville være bedre å gjøre om arkitekturen fullstendig fra relativ til absolutt, men dette er et tema for en annen del, ikke dette innlegget.

API?

Ta-daam! Så, etter å ha slitt oss gjennom en vei full av smerte og krykker, kunne vi endelig sende forespørsler til serveren og motta svar på dem, samt motta oppdateringer fra serveren (ikke som svar på en forespørsel, men den selv sender oss, som PUSH, hvis noen er det tydeligere på den måten).

OBS, nå vil det være det eneste eksemplet i Perl i artikkelen! (for de som ikke er kjent med syntaksen, er det første argumentet for velsignelse objektets datastruktur, det andre 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 lest den ennå, fortsett og gjør det!

Oh, wai~~... hvordan ser dette ut? Noe veldig kjent... kanskje dette er datastrukturen til en typisk Web API i JSON, bortsett fra at klasser også er knyttet til objekter?..

Så dette er hvordan det blir... Hva handler det om, kamerater?.. Så mye innsats - og vi stoppet for å hvile der nettprogrammererne akkurat i gang?..Ville ikke bare JSON over HTTPS vært enklere?! Hva fikk vi i bytte? Var innsatsen verdt det?

La oss vurdere hva TL+MTProto ga oss og hvilke alternativer som er mulige. Vel, HTTP, som fokuserer på forespørsel-svar-modellen, passer dårlig, men i det minste noe på toppen av TLS?

Kompakt serialisering. Når jeg ser denne datastrukturen, lik JSON, husker jeg at det er binære versjoner av den. La oss markere MsgPack som utilstrekkelig utvidbar, men det er for eksempel CBOR - forresten en standard beskrevet i RFC 7049. Det er bemerkelsesverdig for det faktum at det definerer tagger, som en ekspansjonsmekanisme, og blant allerede standardisert tilgjengelig:

  • 25 + 256 - erstatte gjentatte linjer med en referanse til linjenummeret, en slik billig komprimeringsmetode
  • 26 - serialisert Perl-objekt med klassenavn og konstruktørargumenter
  • 27 - serialisert språkuavhengig objekt med typenavn og konstruktørargumenter

Vel, jeg prøvde å serialisere de samme dataene i TL og i CBOR med streng- og objektpakking aktivert. Resultatet begynte å variere til fordel for CBOR et sted fra en megabyte:

cborlen=1039673 tl_len=1095092

således konklusjonen: Det finnes vesentlig enklere formater som ikke er underlagt problemet med synkroniseringsfeil eller ukjent identifikator, med sammenlignbar effektivitet.

Rask oppkobling. Dette betyr null RTT etter gjentilkobling (når nøkkelen allerede er generert én gang) - gjeldende fra den aller første MTProto-meldingen, men med noen forbehold - treffer samme salt, økten er ikke råtten osv. Hva tilbyr TLS oss i stedet? Sitat om emnet:

Når du bruker PFS i TLS, TLS øktbilletter (RFC 5077) for å gjenoppta en kryptert økt uten å forhandle nøkler på nytt og uten å lagre nøkkelinformasjon på serveren. Når du åpner den første tilkoblingen og oppretter nøkler, krypterer serveren tilkoblingstilstanden og overfører den til klienten (i form av en sesjonsbillett). Følgelig, når tilkoblingen gjenopptas, sender klienten en sesjonsbillett, inkludert øktnøkkelen, tilbake til serveren. Selve billetten er kryptert med en midlertidig nøkkel (session ticket key), som lagres på serveren og må distribueres mellom alle frontend-servere som behandler SSL i klyngeløsninger.[10] Dermed kan introduksjonen av en sesjonsbillett krenke PFS hvis midlertidige servernøkler blir kompromittert, for eksempel når de lagres i lang tid (OpenSSL, nginx, Apache lagrer dem som standard for hele programmets varighet; populære nettsteder bruker nøkkelen i flere timer, opptil dager).

Her er ikke RTT null, du må bytte minst ClientHello og ServerHello, hvoretter klienten kan sende data sammen med Finished. Men her skal vi huske at vi ikke har nettet, med dens haug med nyåpnede forbindelser, men en messenger, hvis forbindelse ofte er en og mer eller mindre langvarige, relativt korte forespørsler til websider - alt er multiplekset internt. Det vil si at det er helt akseptabelt hvis vi ikke kom over en virkelig dårlig T-baneseksjon.

Glemt noe annet? Skriv i kommentarfeltet.

Fortsettelse følger!

I den andre delen av denne serien med innlegg vil vi vurdere ikke tekniske, men organisatoriske spørsmål - tilnærminger, ideologi, grensesnitt, holdning til brukere, etc. Basert imidlertid på den tekniske informasjonen som ble presentert her.

Den tredje delen vil fortsette å analysere den tekniske komponenten / utviklingserfaringen. Du vil spesielt lære:

  • fortsettelse av pandemoniet med forskjellige TL-typer
  • ukjente ting om kanaler og supergrupper
  • hvorfor dialoger er verre enn liste
  • om absolutt vs relativ meldingsadressering
  • hva er forskjellen mellom bilde og bilde
  • hvordan emoji forstyrrer kursiv tekst

og andre krykker! Følg med!

Kilde: www.habr.com

Legg til en kommentar