Kritika protokolu a organizačných prístupov Telegramu. Časť 1, technická: skúsenosti s písaním klientov od nuly - TL, MT

V poslednej dobe sa na Habrého začali častejšie objavovať príspevky o tom, aký dobrý je Telegram, akí sú bratia Durovovci brilantní a skúsení v budovaní sieťových systémov atď. Zároveň sa len veľmi málo ľudí skutočne ponorilo do technického zariadenia - nanajvýš používajú pomerne jednoduché (a dosť odlišné od MTProto) založené na JSON Bot API a zvyčajne akceptujú na viere všetky tie chvály a PR, ktoré sa točia okolo messengera. Takmer pred rokom a pol začal môj kolega z mimovládnej organizácie Eshelon Vasilij (bohužiaľ, jeho účet na Habré bol spolu s návrhom vymazaný) písať od nuly vlastného klienta Telegramu v Perle a neskôr sa pridal aj autor týchto riadkov. Prečo práve Perl, niektorí sa hneď opýtajú? Pretože takéto projekty už existujú v iných jazykoch. V skutočnosti o to nejde, mohol by existovať akýkoľvek iný jazyk, kde hotová knižnica, a podľa toho musí autor prejsť celú cestu od nuly. Okrem toho je kryptografia vecou dôvery, ale overte. Pri produkte zameranom na bezpečnosť sa nemôžete jednoducho spoľahnúť na hotovú knižnicu od výrobcu a slepo jej dôverovať (to je však téma na druhú časť). V súčasnosti knižnica funguje celkom dobre na „priemernej“ úrovni (umožňuje vám zadávať akékoľvek API požiadavky).

V tejto sérii príspevkov však nebude veľa kryptografie ani matematiky. Ale bude tam veľa ďalších technických detailov a architektonických barličiek (užitočných aj pre tých, ktorí nebudú písať od nuly, ale budú používať knižnicu v akomkoľvek jazyku). Hlavným cieľom teda bolo pokúsiť sa implementovať klienta od nuly podľa oficiálnej dokumentácie. To znamená, že predpokladajme, že zdrojový kód oficiálnych klientov je uzavretý (opäť sa v druhej časti budeme podrobnejšie venovať téme, že je to pravda stane sa to tak), ale ako napríklad za starých čias existuje štandard ako RFC - je možné napísať klienta iba podľa špecifikácie, „bez pozerania“ na zdrojový kód, či už oficiálny (Telegram Desktop, mobil), alebo neoficiálny Telethon?

Obsah:

Dokumentácia... existuje, však? Je to pravda?..

Útržky poznámok k tomuto článku sa začali zbierať minulé leto. To všetko na oficiálnej stránke https://core.telegram.org Dokumentácia bola k 23. vrstve, t.j. uviazol niekde v roku 2014 (pamätáte si, že vtedy neexistovali ani kanály?). Samozrejme, teoreticky nám to malo umožniť implementovať klienta s funkcionalitou v tom čase v roku 2014. Ale aj v tomto stave bola dokumentácia po prvé neúplná, po druhé si miestami odporovala. Pred viac ako mesiacom, v septembri 2019, to bolo náhodou Zistilo sa, že na stránke bola veľká aktualizácia dokumentácie pre celkom nedávnu vrstvu 105 s poznámkou, že teraz je potrebné všetko prečítať znova. Mnoho článkov bolo revidovaných, ale mnohé zostali nezmenené. Preto pri čítaní nižšie uvedenej kritiky dokumentácie by ste mali mať na pamäti, že niektoré z týchto vecí už nie sú relevantné, ale niektoré sú stále celkom. Veď 5 rokov v modernom svete nie je len dlhá doba, ale veľmi veľa. Od tých čias (najmä ak neberiete do úvahy odvtedy vyradené a oživené geochatové stránky) sa počet API metód v schéme rozrástol zo sto na viac ako dvestopäťdesiat!

Kde začať ako mladý autor?

Nezáleží na tom, či píšete od začiatku alebo používate napríklad hotové knižnice Telethon pre Python alebo Madeline pre PHP, v každom prípade budete najskôr potrebovať zaregistrujte svoju žiadosť - získať parametre api_id и api_hash (tí, ktorí pracovali s VKontakte API, okamžite pochopia), podľa ktorých server identifikuje aplikáciu. Toto musieť urobiť to z právnych dôvodov, ale viac o tom, prečo to autori knižníc nemôžu publikovať, si povieme v druhej časti. S testovacími hodnotami môžete byť spokojní, aj keď sú veľmi obmedzené - faktom je, že teraz sa môžete registrovať len jeden aplikáciu, takže sa do nej bezhlavo nehrňte.

Teraz by nás z technického hľadiska malo zaujímať, že po registrácii by nám od Telegramu mali chodiť notifikácie o aktualizáciách dokumentácie, protokolu a pod. To znamená, že by sa dalo predpokladať, že stránka s dokmi bola jednoducho opustená a pokračovala v práci konkrétne s tými, ktorí začali vytvárať klientov, pretože je to jednoduchšie. Ale nie, nič také nebolo dodržané, neprišli žiadne informácie.

A ak píšete od nuly, tak použitie získaných parametrov je vlastne ešte ďaleko. Hoci https://core.telegram.org/ a hovorí o nich v časti Začíname v prvom rade, v skutočnosti ich budete musieť najskôr implementovať Protokol MTProto - ale keby si veril rozloženie podľa modelu OSI na konci stránky pre všeobecný popis protokolu, potom je to úplne márne.

V skutočnosti pred aj po MTProto na niekoľkých úrovniach naraz (ako hovoria zahraniční networkeri pracujúci v jadre OS, narušenie vrstvy) sa do cesty postaví veľká, bolestivá a hrozná téma...

Binárna serializácia: TL (Type Language) a jeho schéma, vrstvy a mnoho ďalších strašidelných slov

Táto téma je v skutočnosti kľúčom k problémom Telegramu. A bude veľa hrozných slov, ak sa do toho pokúsite ponoriť.

Takže, tu je schéma. Ak vám toto slovo príde na myseľ, povedzte: Schéma JSON, Myslel si správne. Cieľ je rovnaký: nejaký jazyk na opis možnej množiny prenášaných údajov. Tu sa podobnosti končia. Ak zo stránky protokol MTProto, alebo zo zdrojového stromu oficiálneho klienta, skúsime otvoriť nejakú schému, uvidíme niečo ako:

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;

Človek, ktorý to vidí prvýkrát, bude intuitívne schopný rozpoznať len časť toho, čo je napísané - no, sú to zrejme štruktúry (hoci kde je názov, vľavo alebo vpravo?), sú v nich polia, po ktorom nasleduje typ za dvojbodkou... pravdepodobne. Tu v lomených zátvorkách sú pravdepodobne šablóny ako v C++ (v skutočnosti nie celkom). A čo znamenajú všetky ostatné symboly, otázniky, výkričníky, percentá, značky hash (a samozrejme znamenajú rôzne veci na rôznych miestach), niekedy prítomné a niekedy nie, hexadecimálne čísla - a čo je najdôležitejšie, ako sa z toho dostať doprava (ktorý server neodmietne) byte stream? Budete si musieť prečítať dokumentáciu (áno, v blízkosti sú odkazy na schému vo verzii JSON - ale to nie je jasnejšie).

Otvorte stránku Serializácia binárnych údajov a ponorte sa do čarovného sveta húb a diskrétnej matematiky, niečo podobné ako matan v 4. roč. Abeceda, typ, hodnota, kombinátor, funkčný kombinátor, normálna forma, zložený typ, polymorfný typ... a to je všetko len prvá strana! Ďalej vás čaká Jazyk TL, ktorý síce už obsahuje príklad triviálnej požiadavky a odpovede, no na typickejšie prípady odpoveď vôbec neposkytuje, čiže sa budete musieť prebrodiť prerozprávaním matematiky preloženej z ruštiny do angličtiny na ďalších ôsmich vložených stránky!

Čitatelia oboznámení s funkčnými jazykmi a automatickým odvodzovaním typu, samozrejme, budú vnímať jazyk popisu v tomto jazyku, dokonca aj z príkladu, ako oveľa známejší a môžu povedať, že to v zásade nie je zlé. Námietky k tomu sú:

  • Áno, účel znie to dobre, ale bohužiaľ, ona nedosiahnuté
  • Vzdelávanie na ruských univerzitách sa líši aj medzi IT špecializáciami – nie každý absolvoval príslušný kurz
  • Nakoniec, ako uvidíme, v praxi to tak je nie je potrebné, pretože sa používa iba obmedzená podmnožina dokonca TL, ktorá bola opísaná

Ako už bolo povedané LeonNerd na kanáli #perl v sieti FreeNode IRC, ktorí sa pokúsili implementovať bránu z Telegramu do Matrixu (preklad citátu je z pamäte nepresný):

Zdá sa, že niekto sa prvýkrát zoznámil s teóriou typov, nadchlo ho to a začal sa s tým pohrávať, pričom mu bolo úplne jedno, či je to potrebné v praxi.

Presvedčte sa sami, či potreba holého písma (int, long, atď.) ako niečoho elementárneho nevyvoláva otázky – v konečnom dôsledku ich treba implementovať manuálne – napríklad skúsme z nich odvodiť vektor. To je v skutočnosti pole, ak výsledné veci nazývate pravými menami.

Ale predtým

Krátky popis podmnožiny syntaxe TL pre tých, ktorí nečítajú oficiálnu dokumentáciu

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;

Definícia vždy začína návrhár, po ktorom voliteľne (v praxi - vždy) cez symbol # musí byť CRC32 z normalizovaného reťazca popisu tohto typu. Ďalej nasleduje popis polí; ak existujú, typ môže byť prázdny. Toto všetko končí znakom rovnosti, názvom typu, ku ktorému tento konštruktor - teda v skutočnosti podtyp - patrí. Chlap napravo od znamienka rovnosti je polymorfný - to znamená, že jej môže zodpovedať niekoľko konkrétnych typov.

Ak sa definícia nachádza za riadkom ---functions---, potom syntax zostane rovnaká, ale význam bude iný: konštruktor sa stane názvom funkcie RPC, polia sa stanú parametrami (dobre, to znamená, že zostane presne tá istá daná štruktúra, ako je popísané nižšie , bude to jednoducho priradený význam) a „polymorfný typ “ - typ vráteného výsledku. Pravda, stále zostane polymorfný – práve definovaný v sekcii ---types---, ale tento konštruktor „nebude braný do úvahy“. Preťažovanie typov volaných funkcií ich argumentmi, t.j. Z nejakého dôvodu nie je v TL uvedených niekoľko funkcií s rovnakým názvom, ale rôznymi podpismi, ako v C++.

Prečo „konštruktor“ a „polymorfný“, ak to nie je OOP? V skutočnosti bude pre niekoho jednoduchšie premýšľať o tom v podmienkach OOP - polymorfný typ ako abstraktná trieda a konštruktory sú jej priamymi potomkami a final v terminológii viacerých jazykov. V skutočnosti, samozrejme, iba tu podobnosť so skutočne preťaženými metódami konštruktorov v programovacích jazykoch OO. Keďže tu sú len dátové štruktúry, neexistujú žiadne metódy (hoci popis funkcií a metód ďalej je celkom schopný spôsobiť zmätok v hlave, že existujú, ale to je iná vec) - konštruktor si môžete predstaviť ako hodnotu z ktoré sa buduje zadajte pri čítaní bajtového toku.

Ako sa to stane? Deserializátor, ktorý vždy číta 4 bajty, vidí hodnotu 0xcrc32 - a chápe, čo sa bude diať ďalej field1 s typom int, t.j. číta presne 4 bajty, na tomto prekrývajúcom poli s typom PolymorType čítať. Vidí 0x2crc32 a chápe, že najprv existujú dve polia long, čo znamená, že čítame 8 bajtov. A potom opäť komplexný typ, ktorý sa deserializuje rovnakým spôsobom. Napríklad, Type3 by mohli byť vyhlásené v okruhu, akonáhle dvaja konštruktéri, resp., potom sa musia stretnúť buď 0x12abcd34, po ktorom musíte prečítať ďalšie 4 bajty intAlebo 0x6789cdef, po ktorom nebude nič. Čokoľvek iné - musíte vyhodiť výnimku. Každopádne, potom sa vrátime k čítaniu 4 bajtov int poľa field_c в constructorTwo a tým končíme čítanie nášho PolymorType.

Nakoniec, ak sa necháte chytiť 0xdeadcrc pre constructorThree, potom sa všetko skomplikuje. Naše prvé pole je bit_flags_of_what_really_present s typom # - v skutočnosti je to len alias pre tento typ nat, čo znamená „prirodzené číslo“. To je v skutočnosti int bez znamienka je, mimochodom, jediný prípad, keď sa v reálnych obvodoch vyskytujú čísla bez znamienka. Nasleduje teda konštrukcia s otáznikom, čo znamená, že toto pole - bude na drôte prítomné iba vtedy, ak je v príslušnom poli nastavený zodpovedajúci bit (približne ako ternárny operátor). Predpokladajme teda, že tento bit bol nastavený, čo znamená, že ďalej musíme čítať pole ako Type, ktorý má v našom príklade 2 konštruktory. Jeden je prázdny (pozostáva len z identifikátora), druhý má pole ids s typom ids:Vector<long>.

Môžete si myslieť, že šablóny aj generiká sú medzi profesionálmi alebo Java. Ale nie. Takmer. Toto iba prípade použitia uhlových zátvoriek v reálnych obvodoch a používa sa LEN pre Vector. V toku bajtov to budú 4 bajty CRC32 pre samotný typ Vector, vždy rovnaké, potom 4 bajty - počet prvkov poľa a potom tieto prvky samotné.

Pridajte k tomu skutočnosť, že serializácia sa vždy vyskytuje v slovách o veľkosti 4 bajtov, všetky typy sú jej násobky - sú opísané aj vstavané typy bytes и string s manuálnou serializáciou dĺžky a toto zarovnanie po 4 - no, zdá sa, že to znie normálne a dokonca relatívne efektívne? Aj keď sa TL tvrdí, že je to efektívna binárna serializácia, do pekla s rozšírením takmer čohokoľvek, dokonca aj booleovských hodnôt a jednoznakových reťazcov na 4 bajty, bude JSON stále oveľa hrubší? Pozrite, dokonca aj nepotrebné polia sa dajú preskočiť pomocou bitových príznakov, všetko je celkom dobré a dokonca rozšíriteľné do budúcnosti, tak prečo neskôr nepridať nové voliteľné polia do konštruktora?...

Ale nie, ak si neprečítate môj stručný popis, ale celú dokumentáciu a premýšľate o implementácii. Po prvé, CRC32 konštruktora sa vypočíta podľa normalizovaného riadku textového popisu schémy (odstráňte nadbytočné biele znaky atď.) - takže ak sa pridá nové pole, riadok popisu typu sa zmení a tým aj jeho CRC32 a v dôsledku toho serializácia. A čo by urobil starý klient, keby dostal pole s novými vlajkami a nevie, čo s nimi ďalej robiť?...

Po druhé, pamätajme CRC32, ktorý sa tu používa v podstate ako hašovacie funkcie jednoznačne určiť, aký typ sa (de)serializuje. Tu stojíme pred problémom kolízií – a nie, pravdepodobnosť nie je jedna ku 232, ale oveľa väčšia. Kto si pamätal, že CRC32 je navrhnutý tak, aby zisťoval (a opravoval) chyby v komunikačnom kanáli a podľa toho zlepšuje tieto vlastnosti na úkor ostatných? Napríklad sa nestará o preusporiadanie bajtov: ak vypočítate CRC32 z dvoch riadkov, v druhom vymeníte prvé 4 bajty za ďalšie 4 bajty - bude to rovnaké. Keď sú naším vstupom textové reťazce z latinskej abecedy (s trochou interpunkcie) a tieto názvy nie sú príliš náhodné, pravdepodobnosť takéhoto preskupenia sa výrazne zvyšuje.

Mimochodom, kto skontroloval, čo tam bolo? naozaj CRC32? Jeden z prvých zdrojových kódov (ešte pred Waltmanom) mal hašovaciu funkciu, ktorá vynásobila každý znak číslom 239, ktoré títo ľudia tak milujú, ha ha!

Nakoniec, dobre, uvedomili sme si, že konštruktéri s typom poľa Vector<int> и Vector<PolymorType> bude mať iný CRC32. A čo online výkon? A z teoretického hľadiska stane sa to súčasťou typu? Povedzme, že minieme pole desaťtisíc čísel, dobre s Vector<int> všetko je jasné, dĺžka a ďalších 40000 XNUMX bajtov. Čo ak toto Vector<Type2>, ktorý pozostáva len z jedného poľa int a je sám v type - musíme opakovať 10000xabcdef0 34 4 krát a potom XNUMX bajty int, alebo je jazyk schopný ho za nás NEZÁVISLOŤ od konštruktora fixedVec a namiesto 80000 40000 bajtov preniesť zase len XNUMX XNUMX?

Toto vôbec nie je prázdna teoretická otázka – predstavte si, že dostanete zoznam používateľov skupiny, z ktorých každý má svoje ID, meno, priezvisko – rozdiel v množstve dát prenesených cez mobilné pripojenie môže byť značný. Je to práve efektívnosť serializácie telegramu, ktorá je nám propagovaná.

Takže ...

Vektor, ktorý nebol nikdy vydaný

Ak sa pokúsite prehrýzť stránkami s popisom kombinátorov a podobne, uvidíte, že vektor (a dokonca aj matica) sa formálne pokúša dostať na výstup cez n-tice niekoľkých listov. Ale nakoniec zabudnú, posledný krok sa preskočí a jednoducho sa zadá definícia vektora, ktorý ešte nie je viazaný na typ. Čo sa deje? V jazykoch programovanie, hlavne funkčných, je celkom typické popisovať štruktúru rekurzívne - kompilátor s jeho lenivým vyhodnocovaním pochopí a urobí všetko sám. V jazyku serializácia dát čo je potrebné, je EFEKTÍVNOSŤ: stačí jednoducho opísať список, t.j. štruktúra dvoch prvkov - prvý je dátový prvok, druhý je rovnaká štruktúra samotná alebo prázdne miesto pre chvost (balenie (cons) v Lisp). To si však bude zrejme vyžadovať z každého prvok minie ďalšie 4 bajty (CRC32 v prípade v TL) na popis svojho typu. Pole možno tiež jednoducho opísať pevná veľkosť, ale v prípade poľa vopred neznámej dĺžky odlomíme.

Preto, keďže TL neumožňuje výstup vektora, musel byť pridaný na stranu. Nakoniec dokumentácia hovorí:

Serializácia vždy používa rovnaký konštruktor „vektor“ (const 0x1cb5c415 = crc32 („vektor t:Typ # [ t ] = Vektor t“), ktorý nie je závislý od konkrétnej hodnoty premennej typu t.

Hodnota voliteľného parametra t nie je zapojená do serializácie, pretože je odvodená od typu výsledku (známeho vždy pred deserializáciou).

Pozrieť sa na to bližšie: vector {t:Type} # [ t ] = Vector t - ale nikde Táto definícia sama o sebe nehovorí, že prvé číslo sa musí rovnať dĺžke vektora! A neprichádza odnikiaľ. Toto je danosť, ktorú treba mať na pamäti a implementovať ju rukami. Inde dokumentácia dokonca úprimne uvádza, že typ nie je skutočný:

Polymorfný pseudotyp Vector t je „typ“, ktorého hodnota je sekvencia hodnôt akéhokoľvek typu t, buď v rámčeku alebo holé.

... ale nesústredí sa na to. Keď sa už unavený brodiť sa naťahovaním matematiky (možno známej aj z vysokoškolského kurzu) rozhodnete vzdať to a pozrieť sa na to, ako sa s ňou dá v praxi pracovať, v hlave vám zanechá dojem, že toto je Vážne Matematika v jadre, tú jednoznačne vymysleli Cool People (dvaja matematici – víťaz ACM), a nie hocijaký. Cieľ – predviesť sa – bol splnený.

Mimochodom, o čísle. Pripomeňme si to # je to synonymum nat, prirodzené číslo:

Existujú typové výrazy (typ-expr) a číselné výrazy (nat-expr). Sú však definované rovnakým spôsobom.

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

ale v gramatike sú popísané rovnako, t.j. Tento rozdiel si opäť treba zapamätať a implementovať ručne.

Áno, typy šablón (vector<int>, vector<User>) majú spoločný identifikátor (#1cb5c415), t.j. ak viete, že výzva je vyhlásená ako

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

potom už nečakáte len na vektor, ale na vektor užívateľov. Presnejšie, mušt počkať - v reálnom kóde bude mať každý prvok, ak nie holý typ, konštruktor a v dobrom zmysle pri implementácii by to bolo potrebné skontrolovať - ​​ale poslali sme presne každý prvok tohto vektora ten typ? Čo keby to bol nejaký druh PHP, v ktorom pole môže obsahovať rôzne typy v rôznych prvkoch?

V tomto momente si začnete myslieť – je taký TL potrebný? Možno pre vozík by bolo možné použiť ľudský serializátor, rovnaký protobuf, ktorý už vtedy existoval? To bola teória, pozrime sa na prax.

Existujúce implementácie TL v kóde

TL sa zrodil v hlbinách VKontakte ešte pred slávnymi udalosťami s predajom Durovovho podielu a (iste), ešte pred začatím vývoja telegramu. A v open source zdrojový kód prvej implementácie môžete nájsť veľa vtipných bariel. A samotný jazyk tam bol implementovaný plnšie ako teraz v telegrame. Napríklad hash sa v schéme vôbec nepoužíva (čo znamená vstavaný pseudotyp (ako vektor) s deviantným správaním). Alebo

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

ale pre úplnosť uvažujme o stopovaní, takpovediac, evolúcie Obra myslenia.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Alebo tento krásny:

    static const char *reserved_words_polymorhic[] = {

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

      };

Tento fragment je o šablónach ako:

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

Toto je definícia typu šablóny hashmap ako vektora párov int - Typ. V C++ by to vyzeralo asi takto:

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

tak, alpha - kľúčové slovo! Ale iba v C++ môžete napísať T, ale mali by ste napísať alfa, beta... Ale nie viac ako 8 parametrov, tam končí fantázia. Zdá sa, že kedysi dávno sa v Petrohrade odohrali takéto dialógy:

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

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

Toto sa však týkalo prvej publikovanej implementácie TL „vo všeobecnosti“. Prejdime k zvažovaniu implementácií v samotných klientoch Telegramu.

Slovo Vasilijovi:

Vasily, [09.10.18 17:07] Predovšetkým je ten zadok horúci, pretože vytvorili kopu abstrakcií, potom do nich zatĺkli skrutku a zakryli generátor kódu barlami
V dôsledku toho najprv z doku pilot.jpg
Potom z kódu dzhekichan.webp

Samozrejme, od ľudí, ktorí sú oboznámení s algoritmami a matematikou, môžeme očakávať, že čítali Aho, Ullmanna a sú oboznámení s nástrojmi, ktoré sa v priebehu desaťročí stali de facto štandardom v priemysle na písanie ich DSL kompilátorov, však?

Autorom telegram-cli je Vitaly Valtman, ako je možné pochopiť z výskytu formátu TLO mimo jeho (cli) hraníc, členom tímu - teraz bola pridelená knižnica na analýzu TL oddelene, aký je z nej dojem TL syntaktický analyzátor? ..

16.12 04:18 Vasilij: Myslím, že niekto neovládal lex+yacc
16.12 04:18 Vasilij: Inak si to neviem vysvetliť
16.12 04:18 Vasilij: no, alebo boli platení za počet riadkov vo VK.
16.12 04:19 Vasilij: 3k+ riadkov atď.<censored> namiesto analyzátora

Možno výnimka? Pozrime sa ako značky Toto je OFICIÁLNY klient - Telegram Desktop:

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

1100+ riadkov v Pythone, pár regulárnych výrazov + špeciálne prípady ako vektor, ktorý je samozrejme v schéme deklarovaný tak, ako má byť podľa syntaxe TL, ale spoliehali sa na túto syntax, aby to analyzovali... Vynára sa otázka, prečo to všetko bol zázrak?иJe to viac vrstvené, ak to aj tak nikto nebude analyzovať podľa dokumentácie?!

Mimochodom... Pamätáte si, že sme hovorili o kontrole CRC32? Takže v generátore kódu Telegram Desktop existuje zoznam výnimiek pre tie typy, v ktorých je vypočítaný CRC32 nezhoduje sa s tým, ktorý je uvedený na obrázku!

Vasilij, [18.12/22 49:XNUMX] a tu by som sa zamyslel nad tým, či je potrebný taký TL
ak by som sa chcel popasovať s alternatívnymi implementáciami, začal by som vkladať zalomenia riadkov, polovica syntaktických analyzátorov sa rozbije na viacriadkových definíciách
tdesktop však tiež

Zapamätajte si bod o jednej vložke, vrátime sa k nej trochu neskôr.

Dobre, telegram-cli je neoficiálny, Telegram Desktop je oficiálny, ale čo ostatní? Kto vie?... V kóde klienta Android nebol vôbec žiadny analyzátor schém (čo vyvoláva otázky o otvorenom zdroji, ale toto je pre druhú časť), ale bolo tam niekoľko ďalších vtipných kúskov kódu, ale viac o nich v pododdiel nižšie.

Aké ďalšie otázky vyvoláva serializácia v praxi? Napríklad urobili veľa vecí, samozrejme, s bitovými poľami a podmienenými poľami:

Vasily: flags.0? true
znamená, že pole je prítomné a rovná sa true, ak je nastavený príznak

Vasily: flags.1? int
znamená, že pole je prítomné a je potrebné ho deserializovať

Vasilij: Prdel, netráp sa tým, čo robíš!
Vasily: Niekde v dokumente je zmienka, že true je holý typ s nulovou dĺžkou, ale z ich dokumentu nie je možné zostaviť čokoľvek
Vasilij: V implementáciách s otvoreným zdrojovým kódom to tiež tak nie je, ale existuje veľa bariel a podpôr

A čo Telethon? Pohľad dopredu na tému MTProto, príklad - v dokumentácii sú také kusy, ale znamenie % je opísaná len ako „zodpovedajúca danému holému typu“, t.j. v príkladoch nižšie je buď chyba, alebo niečo nezdokumentované:

Vasilij, [22.06.18 18:38] Na jednom mieste:

msg_container#73f1f8dc messages:vector message = MessageContainer;

V inom:

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

A to sú dva veľké rozdiely, v reálnom živote prichádza nejaký nahý vektor

Nevidel som holú definíciu vektora a nestretol som sa s ňou

Analýza je písaná ručne v telethone

V jeho diagrame je definícia komentovaná msg_container

Opäť zostáva otázka o %. Nie je popísaná.

Vadim Goncharov, [22.06.18 19:22] a v tdesktop?

Vasilij, [22.06.18 19:23] Ale ich TL parser na beznych motoroch s najvacsou pravdepodobnostou nezje ani toto

// parsed manually

TL je krásna abstrakcia, nikto ju nerealizuje úplne

A % nie je v ich verzii schémy

Ale tu si dokumentácia protirečí, takže idk

Našlo sa to v gramatike, jednoducho mohli zabudnúť popísať sémantiku

Videli ste dokument na TL, nemôžete na to prísť bez pol litra

"No, povedzme," povie iný čitateľ, "niečo kritizujete, tak mi ukážte, ako by sa to malo robiť."

Vasily odpovedá: „Pokiaľ ide o analyzátor, mám rád veci ako

    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 ) ); }
            ;

nejako lepšie ako

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;
}

alebo

        # 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)

toto je CELÝ lexer:

    ---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];

tie. jednoduchšie je to povedať mierne."

Vo všeobecnosti sa analyzátor a generátor kódu pre skutočne používanú podmnožinu TL zmestí do približne 100 riadkov gramatiky a ~ 300 riadkov generátora (počítajúc všetky printvygenerovaný kód) vrátane informácií o typoch na introspekciu v každej triede. Každý polymorfný typ sa zmení na prázdnu abstraktnú základnú triedu a konštruktéri z nej dedia a majú metódy na serializáciu a deserializáciu.

Nedostatok typov v jazyku písma

Silné písanie je dobrá vec, však? Nie, toto nie je holivar (aj keď ja preferujem dynamické jazyky), ale postulát v rámci TL. Na základe nej by nám mal jazyk zabezpečiť všemožné kontroly. No dobre, možno nie on sám, ale realizácia, ale mal by ich aspoň opísať. A aké príležitosti chceme?

V prvom rade obmedzenia. Tu vidíme v dokumentácii pre nahrávanie súborov:

Binárny obsah súboru sa potom rozdelí na časti. Všetky časti musia mať rovnakú veľkosť ( veľkosť_časti ) a musia byť splnené tieto podmienky:

  • part_size % 1024 = 0 (deliteľné 1 kB)
  • 524288 % part_size = 0 (512 kB musí byť rovnomerne deliteľných hodnotou part_size)

Posledná časť nemusí spĺňať tieto podmienky za predpokladu, že jej veľkosť je menšia ako veľkosť_časti.

Každá časť by mala mať poradové číslo, súbor_časť, s hodnotou od 0 do 2,999 XNUMX.

Po rozdelení súboru na oddiely musíte vybrať spôsob jeho uloženia na server. Použite upload.saveBigFilePart v prípade, že je plná veľkosť súboru väčšia ako 10 MB a upload.saveFilePart pre menšie súbory.
[…] môže sa vrátiť jedna z nasledujúcich chýb zadávania údajov:

  • FILE_PARTS_INVALID — Neplatný počet častí. Hodnota nie je medzi 1..3000

Je niečo z toho v diagrame? Dá sa to nejako vyjadriť pomocou TL? Nie Ale prepáčte, dokonca aj dedkov Turbo Pascal dokázal opísať špecifikované typy rozsahy. A vedel ešte jednu vec, teraz známejšiu ako enum - typ pozostávajúci z enumerácie pevného (malého) počtu hodnôt. V jazykoch ako C - numerické si všimnite, že doteraz sme hovorili len o typoch čísla. Ale sú tam aj polia, reťazce... napríklad by sa hodilo popísať, že tento reťazec môže obsahovať len telefónne číslo, však?

Nič z toho nie je v TL. Ale existuje napríklad v schéme JSON. A ak by niekto mohol argumentovať o deliteľnosti 512 KB, že to ešte treba skontrolovať v kóde, potom sa uistite, že klient jednoducho nemohol poslať číslo mimo rozsahu 1..3000 (a príslušná chyba nemohla vzniknúť) bolo by to možné, nie?..

Mimochodom, o chybách a návratových hodnotách. Dokonca aj tí, ktorí pracovali s TL, rozmazávajú oči - to nám hneď nesvitlo každý jeden funkcia v TL môže skutočne vrátiť nielen popísaný návratový typ, ale aj chybu. Ale to sa nedá žiadnym spôsobom odvodiť pomocou samotného TL. Jasné, už je to jasné a v praxi nie je nič potrebné (aj keď v skutočnosti sa RPC dá robiť rôznymi spôsobmi, k tomu sa ešte vrátime) – ale čo Čistota pojmov matematiky abstraktných typov? z nebeského sveta?... zdvihol som remorkér - tak ho prirovnaj.

A nakoniec, čo čítanosť? No, tam by som vo všeobecnosti chcel opis máte to správne v schéme (v schéme JSON opäť je), ale ak ste s tým už namáhaní, čo potom praktická stránka – aspoň triviálne sa pozrieť na rozdiely počas aktualizácií? Presvedčte sa sami na skutočné príklady:

-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;

alebo

-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;

Záleží na každom, ale napríklad GitHub odmieta zvýrazňovať zmeny vo vnútri tak dlhých radov. Hra “nájdi 10 rozdielov” a čo mozog hneď vidí je, že začiatky a konce v oboch príkladoch sú rovnaké, treba únavne čítať niekde v strede... Podľa mňa to nie je len teoreticky, ale čisto vizuálne špinavý a nedbalý.

Mimochodom, o čistote teórie. Prečo potrebujeme bitové polia? Nezdá sa, že oni vôňa zlé z pohľadu teórie typov? Vysvetlenie je možné vidieť v starších verziách diagramu. Spočiatku áno, bolo to tak, pre každé kýchnutie sa vytvoril nový typ. Tieto základy stále existujú v tejto forme, napríklad:

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;

Ale teraz si predstavte, ak máte vo svojej štruktúre 5 voliteľných polí, potom budete potrebovať 32 typov pre všetky možné možnosti. Kombinatorický výbuch. Krištáľová čistota teórie TL sa teda opäť rozbila o liatinový zadok tvrdej reality serializácie.

Navyše, miestami títo chlapíci sami porušujú vlastnú typológiu. Napríklad v MTProto (ďalšia kapitola) sa dá odozva skomprimovať Gzipom, všetko je v poriadku - až na to, že sú narušené vrstvy a obvod. Opäť sa nezožal samotný RpcResult, ale jeho obsah. No, prečo to robiť?... Musel som rezať do barlí, aby kompresia fungovala kdekoľvek.

Alebo iný príklad, raz sme objavili chybu - bola odoslaná InputPeerUser namiesto InputUser. Alebo naopak. Ale podarilo sa! To znamená, že server sa nestaral o typ. Ako to môže byť? Odpoveď nám môžu dať fragmenty kódu z telegramu-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);

Inými slovami, tu sa vykonáva serializácia MANUÁLNE, nie vygenerovaný kód! Možno je server implementovaný podobným spôsobom?... V zásade to bude fungovať, ak sa to urobí raz, ale ako to možno podporiť neskôr počas aktualizácií? Je to dôvod, prečo bola schéma vynájdená? A tu prejdeme k ďalšej otázke.

Verziovanie. Vrstvy

Prečo sa schematické verzie nazývajú vrstvy, možno len špekulovať na základe histórie publikovaných schém. Pôvodne si autori zrejme mysleli, že základné veci sa dajú robiť pomocou nezmenenej schémy a len tam, kde je to potrebné, pri konkrétnych požiadavkách uviesť, že sa robia s použitím inej verzie. V zásade aj dobrý nápad – a nový bude akoby „zmiešaný“, navrstvený na starý. Ale pozrime sa, ako sa to podarilo. Je pravda, že som sa na to nemohol pozrieť od samého začiatku - je to zábavné, ale schéma základnej vrstvy jednoducho neexistuje. Vrstvy začali 2. Dokumentácia nám hovorí o špeciálnej funkcii TL:

Ak klient podporuje vrstvu 2, musí sa použiť nasledujúci konštruktor:

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

V praxi to znamená, že pred každým volaním API int s hodnotou 0x289dd1f6 treba pridať pred číslo metódy.

Znie to normálne. Čo sa však stalo potom? Potom sa objavil

invokeWithLayer3#b7475268 query:!X = X;

Takže čo bude ďalej? Ako asi tušíte,

invokeWithLayer4#dea0d430 query:!X = X;

smiešne? Nie, na smiech je priskoro, mysli na to každý požiadavku z inej vrstvy je potrebné zabaliť do takého špeciálneho typu - ak sú pre vás všetky iné, ako inak ich môžete rozlíšiť? A pridanie iba 4 bajtov dopredu je celkom efektívna metóda. takže,

invokeWithLayer5#417a57ae query:!X = X;

Ale je zrejmé, že po chvíli sa z toho stane nejaká bakchanália. A prišlo riešenie:

Aktualizácia: Počnúc vrstvou 9, pomocné metódy invokeWithLayerN možno použiť len spolu s initConnection

Hurá! Po 9 verziách sme sa konečne dostali k tomu, čo sa robilo v internetových protokoloch ešte v 80-tych rokoch - dohodnúť sa na verzii raz na začiatku spojenia!

Tak čo ďalej?..

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

Ale teraz sa môžete stále smiať. Až po ďalších 9 vrstvách konečne pribudol univerzálny konštruktor s číslom verzie, ktorý treba volať len raz na začiatku spojenia a význam vrstiev akoby zmizol, teraz je to už len podmienená verzia, napr. všade inde. Problém je vyriešený.

Presne tak?...

Vasilij, [16.07.18 14:01] Ešte v piatok som si myslel:
Teleserver odosiela udalosti bez vyžiadania. Požiadavky musia byť zabalené do InvokeWithLayer. Server nezalamuje aktualizácie, neexistuje žiadna štruktúra na zalamovanie odpovedí a aktualizácií.

Tie. klient nemôže určiť vrstvu, v ktorej chce aktualizácie

Vadim Goncharov, [16.07.18 14:02] nie je InvokeWithLayer v princípe barlička?

Vasilij, [16.07.18 14:02] Toto je jediná cesta

Vadim Goncharov, [16.07.18 14:02] čo by v podstate malo znamenať dohodnúť sa na vrstve na začiatku relácie

Mimochodom, z toho vyplýva, že downgrade klienta nie je poskytovaný

Aktualizácie, t.j. typu Updates v schéme je to to, čo server posiela klientovi nie ako odpoveď na požiadavku API, ale nezávisle, keď nastane udalosť. Toto je zložitá téma, o ktorej sa bude diskutovať v inom príspevku, ale zatiaľ je dôležité vedieť, že server ukladá aktualizácie, aj keď je klient offline.

Ak teda odmietnete zabaliť z každého balík na označenie jeho verzie, to logicky vedie k nasledujúcim možným problémom:

  • server posiela aktualizácie klientovi ešte predtým, ako klient informuje, ktorú verziu podporuje
  • čo mám robiť po aktualizácii klienta?
  • kto zárukyže názor servera na číslo vrstvy sa počas procesu nezmení?

Myslíte si, že je to čisto teoretická špekulácia a v praxi sa to nemôže stať, pretože server je napísaný správne (aspoň je dobre otestovaný)? Ha! Bez ohľadu na to, ako to je!

Presne na toto sme narazili v auguste. 14. augusta sa objavili správy, že na serveroch telegramu sa niečo aktualizuje... a potom v protokoloch:

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.

a potom niekoľko megabajtov stôp zásobníka (dobre, v rovnakom čase bolo opravené protokolovanie). Koniec koncov, ak niečo nie je rozpoznané vo vašom TL, je to binárne podľa podpisu, ďalej v riadku VŠETKY dekódovanie bude nemožné. Čo by ste mali robiť v takejto situácii?

Prvá vec, ktorá každému napadne, je odpojiť sa a skúsiť to znova. Nepomohlo. Vygooglime si CRC32 – ukázalo sa, že ide o objekty zo schémy 73, hoci sme pracovali na 82. Pozorne si prezeráme protokoly – sú tam identifikátory z dvoch rôznych schém!

Možno je problém čisto v našom neoficiálnom klientovi? Nie, spúšťame Telegram Desktop 1.2.17 (verzia dodávaná v niekoľkých distribúciách Linuxu), zapisuje sa do denníka výnimiek: MTP Neočakávané ID typu #b5223b0f načítané v MTPMessageMedia…

Kritika protokolu a organizačných prístupov Telegramu. Časť 1, technická: skúsenosti s písaním klientov od nuly - TL, MT

Google ukázal, že podobný problém sa už jednému z neoficiálnych klientov vyskytol, no potom boli čísla verzií a podľa toho aj predpoklady iné...

Čo by sme teda mali robiť? Vasily a ja sme sa rozišli: pokúsil sa aktualizovať obvod na 91, rozhodol som sa počkať niekoľko dní a vyskúšať 73. Obidve metódy fungovali, ale keďže sú empirické, nie je jasné, koľko verzií nahor alebo nadol potrebujete skočiť, alebo ako dlho musíte čakať .

Neskôr som bol schopný reprodukovať situáciu: spustíme klienta, vypneme ho, prekompilujeme okruh na inú vrstvu, reštartujeme, znova zachytíme problém, vrátime sa k predchádzajúcemu - ups, žiadne prepínanie okruhov a reštart klienta pár minút pomôže. Dostanete mix dátových štruktúr z rôznych vrstiev.

Vysvetlenie? Ako môžete uhádnuť z rôznych nepriamych symptómov, server pozostáva z mnohých procesov rôznych typov na rôznych počítačoch. S najväčšou pravdepodobnosťou server, ktorý je zodpovedný za „ukladanie do vyrovnávacej pamäte“, vložil do frontu to, čo mu dali jeho nadriadení, a dali to podľa schémy, ktorá bola v čase generovania. A kým sa tento rad „neprehnil“, nedalo sa s tým nič robiť.

Možno... ale toto je strašná barlička?!.. Nie, skôr ako sa zamyslíme nad bláznivými nápadmi, pozrime sa na kódex oficiálnych klientov. Vo verzii pre Android nenájdeme žiadny TL parser, ale nájdeme tu obsiahly súbor (GitHub ho odmieta retušovať) s (de)serializáciou. Tu sú útržky kódu:

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;

alebo

    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... vyzerá to divoko. Ale pravdepodobne je to vygenerovaný kód, dobre?... Ale určite podporuje všetky verzie! Je pravda, že nie je jasné, prečo je všetko zmiešané, tajné rozhovory a všetky druhy _old7 nejako nevyzerajú ako strojová generácia... Najviac ma však uchvátilo

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Chlapci, ani sa neviete rozhodnúť, čo je vo vnútri jednej vrstvy?! Dobre, dobre, povedzme, že „dva“ boli uvoľnené s chybou, no, to sa stáva, ale TROJA?... Hneď znova rovnaké hrable? Čo je to za pornografiu, prepáčte?...

Mimochodom, v zdrojovom kóde Telegram Desktop sa deje podobná vec - ak áno, niekoľko commitov v rade do schémy nezmení číslo vrstvy, ale niečo opraví. V podmienkach, kde neexistuje oficiálny zdroj údajov pre schému, odkiaľ ich možno získať, okrem zdrojového kódu oficiálneho klienta? A ak to vezmete odtiaľ, nemôžete si byť istí, že schéma je úplne správna, kým neotestujete všetky metódy.

Ako sa to dá vôbec otestovať? Dúfam, že fanúšikovia jednotkových, funkčných a iných testov sa podelia v komentároch.

Dobre, pozrime sa na ďalší kus kódu:

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;

Tento komentár „vytvorené ručne“ naznačuje, že iba časť tohto súboru bola napísaná ručne (viete si predstaviť celú nočnú moru údržby?) a zvyšok bol vygenerovaný strojovo. Potom však vyvstáva ďalšia otázka – že zdroje sú dostupné nie úplne (a la GPL bloby v linuxovom jadre), ale toto je už téma pre druhú časť.

Ale dosť. Prejdime k protokolu, nad ktorým beží celá táto serializácia.

MT Proto

Tak teda otvorme všeobecný popis и podrobný popis protokolu a prvé, na čo narazíme, je terminológia. A s hojnosťou všetkého. Vo všeobecnosti sa zdá, že je to vlastná funkcia telegramu – nazývať veci rôzne na rôznych miestach alebo rôzne veci jedným slovom alebo naopak (ak napríklad v rozhraní API na vysokej úrovni vidíte balík nálepiek, nie je to čo si myslel).

Napríklad „správa“ a „relácia“ tu znamenajú niečo iné ako v bežnom klientskom rozhraní Telegramu. So správou je všetko jasné, mohla by sa interpretovať v podmienkach OOP alebo jednoducho nazvať slovo „paket“ - toto je nízka úroveň prenosu, nie sú tam rovnaké správy ako v rozhraní, existuje veľa servisných správ . Ale relácia... ale najskôr.

transportná vrstva

Prvá vec je doprava. Povedia nám o 5 možnostiach:

  • TCP
  • Websocket
  • Websocket cez HTTPS
  • HTTP
  • HTTPS

Vasilij, [15.06.18 15:04] Existuje aj prenos UDP, ale nie je zdokumentovaný

A TCP v troch variantoch

Prvý je podobný UDP cez TCP, každý paket obsahuje poradové číslo a crc
Prečo je čítanie dokumentov na vozíku také bolestivé?

Tak a je to tu TCP už v 4 variantoch:

  • skrátený
  • Stredná
  • Polstrovaná stredná
  • plne

No dobre, polstrovaný medziprodukt pre MTProxy, toto bolo neskôr pridané kvôli známym udalostiam. Ale prečo dve ďalšie verzie (celkovo tri), keď si vystačíte s jednou? Všetky štyri sa v podstate líšia iba v tom, ako nastaviť dĺžku a užitočné zaťaženie hlavného MTProto, o čom sa bude diskutovať ďalej:

  • v skrátenom režime je to 1 alebo 4 bajty, ale nie 0xef, potom telo
  • v Intermediate sú to 4 bajty dĺžky a pole a prvýkrát musí klient odoslať 0xeeeeeeee na označenie, že ide o stredne pokročilý
  • v plnej miere najnávykovejšie z pohľadu networkera: dĺžka, poradové číslo, a NIE TEN, ktorý je hlavne MTProto, telo, CRC32. Áno, toto všetko je na vrchole TCP. Čo nám poskytuje spoľahlivý prenos vo forme sekvenčného toku bajtov; nie sú potrebné žiadne sekvencie, najmä kontrolné súčty. Dobre, teraz mi niekto namietne, že TCP má 16-bitový kontrolný súčet, takže dochádza k poškodeniu dát. Skvelé, ale v skutočnosti máme kryptografický protokol s hashmi dlhšími ako 16 bajtov, všetky tieto chyby – a ešte viac – zachytí nesúlad SHA na vyššej úrovni. CRC32 navyše nemá žiadny zmysel.

Porovnajme Skrátený, v ktorom je možný jeden bajt dĺžky, s Intermediate, čo ospravedlňuje „V prípade, že je potrebné zarovnanie 4-bajtových dát“, čo je celkom nezmysel. Čo, verí sa, že programátori telegramov sú tak nekompetentní, že nedokážu čítať dáta zo zásuvky do zarovnanej vyrovnávacej pamäte? Stále to musíte urobiť, pretože čítanie vám môže vrátiť ľubovoľný počet bajtov (a existujú napríklad aj proxy servery...). Alebo na druhej strane, prečo blokovať skrátené, ak budeme mať stále navrchu 16 bajtov mohutnú výplň – ušetríme 3 bajty niekedy ?

Človek má dojem, že Nikolaj Durov skutočne rád vynájde kolesá vrátane sieťových protokolov bez skutočnej praktickej potreby.

Ďalšie možnosti dopravy vr. Web a MTProxy teraz nebudeme uvažovať, možno v inom príspevku, ak bude požiadavka. O tomto istom MTProxy si teraz pripomeňme, že krátko po jeho vydaní v roku 2018 sa poskytovatelia rýchlo naučili blokovať ho, určený pre blokovanie bypassuPodľa veľkosť balíka! A tiež skutočnosť, že server MTProxy napísaný (opäť Waltmanom) v jazyku C bol príliš viazaný na špecifiká Linuxu, hoci to nebolo vôbec potrebné (Phil Kulin potvrdí), a že podobný server v Go alebo Node.js by zmestí sa do menej ako sto riadkov.

Závery o technickej gramotnosti týchto ľudí však vyvodíme na konci časti, po zvážení ďalších otázok. Prejdime zatiaľ k OSI vrstve 5, session – na ktorú umiestnili MTProto session.

Kľúče, správy, relácie, Diffie-Hellman

Umiestnili to tam nie úplne správne... Relácia nie je tá istá relácia, ktorá je viditeľná v rozhraní pod Aktívne relácie. Ale v poriadku.

Kritika protokolu a organizačných prístupov Telegramu. Časť 1, technická: skúsenosti s písaním klientov od nuly - TL, MT

Z transportnej vrstvy sme teda dostali bajtový reťazec známej dĺžky. Toto je buď zašifrovaná správa, alebo obyčajný text – ak sme stále vo fáze kľúčovej dohody a skutočne to robíme. O ktorom z tých pojmov nazývaných „kľúč“ hovoríme? Objasnime tento problém samotnému telegramovému tímu (ospravedlňujem sa za preklad mojej vlastnej dokumentácie z angličtiny s unaveným mozgom o 4:XNUMX, bolo jednoduchšie nechať niektoré frázy tak, ako sú):

Existujú dve entity tzv zasadnutí - jedna v používateľskom rozhraní oficiálnych klientov pod „aktuálnymi reláciami“, kde každá relácia zodpovedá celému zariadeniu / OS.
Druhá je relácia MTProto, ktorá má v sebe poradové číslo správy (v nízkoúrovňovom zmysle) a ktorá môže trvať medzi rôznymi TCP spojeniami. Súčasne je možné nainštalovať niekoľko relácií MTProto, napríklad na urýchlenie sťahovania súborov.

Medzi týmito dvoma zasadnutí existuje koncept povolenia. V degenerovanom prípade to môžeme povedať relácia používateľského rozhrania je to isté ako povolenia, ale bohužiaľ, všetko je komplikované. Pozri:

  • Používateľ na novom zariadení najskôr vygeneruje auth_key a viaže ho na účet, napríklad cez SMS – preto povolenia
  • Stalo sa to vo vnútri prvého relácia MTProto, ktorý má session_id vo svojom vnútri.
  • V tomto kroku kombinácia povolenia и session_id dalo by sa nazvať inštancie - toto slovo sa vyskytuje v dokumentácii a kóde niektorých klientov
  • Potom môže klient otvoriť niektorí MTProto relácie pod tým istým auth_key - do rovnakého DC.
  • Potom jedného dňa bude musieť klient požiadať o súbor ďalší DC - a pre tento DC sa vygeneruje nový auth_key !
  • Informovať systém, že sa neregistruje nový používateľ, ale ten istý povolenia (relácia používateľského rozhrania), klient používa volania API auth.exportAuthorization v domácom DC auth.importAuthorization v novom DC.
  • Všetko je rovnaké, niekoľko môže byť otvorených MTProto relácie (každý má svoje session_id) do tohto nového DC, pod jeho auth_key.
  • Nakoniec môže klient chcieť Perfect Forward Secrecy. Každý auth_key bolo stály kľúč - na DC - a klient môže volať auth.bindTempAuthKey na použitie dočasný auth_key - a opäť len jeden temp_auth_key na DC, spoločné pre všetkých MTProto relácie do tohto DC.

Všimnite si, že soľ (a budúce soli) je tiež jedným z auth_key tie. zdieľané medzi všetkými MTProto relácie do rovnakého DC.

Čo znamená „medzi rôznymi pripojeniami TCP“? Takže toto znamená niečo ako autorizačný súbor cookie na webovej stránke - pretrváva (prežije) veľa TCP spojení s daným serverom, ale jedného dňa sa pokazí. Len na rozdiel od HTTP sú v MTProto správy v rámci relácie sekvenčne očíslované a potvrdzované, ak vstúpili do tunela, spojenie sa prerušilo - po nadviazaní nového spojenia server láskavo odošle v tejto relácii všetko, čo nedoručil v predchádzajúcej TCP spojenie.

Vyššie uvedené informácie sú však zhrnuté po mnohých mesiacoch vyšetrovania. Implementujeme medzitým nášho klienta od nuly? - vráťme sa na začiatok.

Poďme teda generovať auth_key na Verzie Diffie-Hellman z Telegramu. Skúsme porozumieť dokumentácii...

Vasily, [19.06.18 20:05] data_with_hash := SHA1(data) + data + (akekolvek nahodne bajty); tak, aby sa dĺžka rovnala 255 bajtom;
zašifrované_údaje := RSA(údaje_s_hashom, verejný_kľúč_servera); 255-bajtové číslo (big endian) sa zvýši na požadovaný výkon nad požadovaným modulom a výsledok sa uloží ako 256-bajtové číslo.

Majú nejaké drogy DH

Nevyzerá to ako DH zdravého človeka
V dx neexistujú dva verejné kľúče

No, nakoniec sa to vyriešilo, ale ostal zvyšok - dôkaz o práci robí klient, že dokázal číslo vypočítať. Typ ochrany pred útokmi DoS. A kľúč RSA sa používa iba raz v jednom smere, v podstate na šifrovanie new_nonce. Ale zatiaľ čo táto zdanlivo jednoduchá operácia bude úspešná, čomu budete musieť čeliť?

Vasily, [20.06.18/00/26 XNUMX:XNUMX] Ešte som sa nedostal k žiadosti o aplikáciu

Poslal som túto žiadosť na DH

A v transportnom doku hovorí, že môže odpovedať 4 bajtmi chybového kódu. To je všetko

No, povedal mi -404, tak čo?

Tak som mu povedal: „Chyť svoje kecy zašifrované pomocou serverového kľúča s odtlačkom prsta, ako je tento, chcem DH,“ a ono odpovedalo hlúpym 404

Čo by ste si mysleli o tejto odpovedi servera? Čo robiť? Nie je sa koho pýtať (ale o tom v druhej časti).

Tu sa všetky úroky robia na lavici obžalovaných

Nič iné mi nezostáva, len som sníval o prevode čísel tam a späť

Dve 32-bitové čísla. Zbalil som ich ako každý iný

Ale nie, tieto dve treba pridať do riadku najskôr ako BE

Vadim Goncharov, [20.06.18 15:49] a kvôli tomuto 404?

Vasilij, [20.06.18 15:49] ÁNO!

Vadim Goncharov, [20.06.18 15:50] tak nechápem, čo môže "nenašiel"

Vasilij, [20.06.18 15:50] o

Nenašiel som taký rozklad na prvočiniteľa %)

Nezvládli sme ani hlásenie chýb

Vasily, [20.06.18 20:18] Oh, je tu aj MD5. Už tri rôzne hashe

Kľúčový odtlačok prsta sa vypočíta takto:

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

SHA1 a sha2

Tak si to povedzme auth_key dostali sme veľkosť 2048 bitov pomocou Diffie-Hellmana. Čo bude ďalej? Ďalej zistíme, že spodných 1024 bitov tohto kľúča sa žiadnym spôsobom nepoužíva... ale teraz sa nad tým zamyslime. V tomto kroku máme zdieľané tajomstvo so serverom. Bol vytvorený analóg relácie TLS, čo je veľmi nákladný postup. Ale server stále nevie nič o tom, kto sme! Vlastne ešte nie. autorizáciu. Tie. ak si myslel v pojmoch “login-password”, ako kedysi v ICQ, alebo aspoň “login-key”, ako v SSH (napríklad na nejakom gitlab/github). Dostali sme anonymný. Čo ak nám server povie „tieto telefónne čísla obsluhuje iný DC“? Alebo dokonca „vaše telefónne číslo je zakázané“? Najlepšie, čo môžeme urobiť, je držať kľúč v nádeji, že bude užitočný a dovtedy nezhnije.

Mimochodom, „prijali“ sme to s rezervou. Napríklad, veríme serveru? Čo ak je to falošné? Budú potrebné kryptografické kontroly:

Vasily, [21.06.18 17:53] Mobilným klientom ponúkajú kontrolu 2kbitového čísla na primalitu%)

Ale to vôbec nie je jasné, nafeijoa

Vasily, [21.06.18 18:02] Dokument neuvádza, čo robiť, ak sa ukáže, že to nie je jednoduché

Nie je povedané. Pozrime sa, čo v tomto prípade robí oficiálny Android klient? A to je čo (a áno, celý súbor je zaujímavý) - ako sa hovorí, nechám tu len toto:

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

Nie, samozrejme, že tam stále je niektorí Existujú testy na prvočíslo, ale osobne už nemám dostatočné znalosti z matematiky.

Dobre, máme hlavný kľúč. Pre prihlásenie t.j. odosielať požiadavky, musíte vykonať ďalšie šifrovanie pomocou AES.

Kľúč správy je definovaný ako 128 stredných bitov SHA256 tela správy (vrátane relácie, ID správy atď.), vrátane vyplňovacích bajtov, pred ktorými je 32 bajtov prevzatých z autorizačného kľúča.

Vasilij, [22.06.18 14:08] Priemer, mrcha, bitky

Mám auth_key. Všetky. Okrem nich... to z dokumentu nie je jasné. Neváhajte a preštudujte si otvorený zdrojový kód.

Upozorňujeme, že MTProto 2.0 vyžaduje od 12 do 1024 bajtov výplne, stále však podlieha podmienke, že výsledná dĺžka správy bude deliteľná 16 bajtmi.

Koľko výplne by ste teda mali pridať?

A áno, je tam aj 404 v prípade chyby

Ak si niekto pozorne preštudoval schému a text dokumentácie, všimol si, že tam nie je MAC. A že AES sa používa v určitom režime IGE, ktorý sa nikde inde nepoužíva. Samozrejme, píšu o tom vo svojich FAQ... Tu, ako napríklad, samotný kľúč správy je tiež SHA hash dešifrovaných údajov, ktorý sa používa na kontrolu integrity - a v prípade nesúladu dokumentácia z nejakého dôvodu odporúča mlčky ich ignorovať (ale čo bezpečnosť, čo ak nás zlomia?).

Nie som kryptograf, možno na tomto móde v tomto prípade z teoretického hľadiska nie je nič zlé. Ale môžem jasne pomenovať praktický problém, ako príklad použijem Telegram Desktop. Šifruje lokálnu vyrovnávaciu pamäť (všetky tieto D877F783D5D3EF8C) rovnako ako správy v MTProto (iba v tomto prípade verzia 1.0), t.j. najprv kľúč správy, potom samotné údaje (a niekde vedľa hlavného veľkého auth_key 256 bajtov, bez ktorých msg_key zbytočné). Problém sa teda prejaví pri veľkých súboroch. Konkrétne si musíte ponechať dve kópie údajov – zašifrovanú a dešifrovanú. A ak sú tam megabajty, alebo napríklad streamované video?... Klasické schémy s MAC po zašifrovanom texte umožňujú čítanie streamu a okamžitého prenosu. Ale s MTProto budete musieť na začiatku zašifrovať alebo dešifrovať celú správu, až potom ju preniesť do siete alebo na disk. Preto v najnovších verziách Telegram Desktop vo vyrovnávacej pamäti v user_data Používa sa aj iný formát – s AES v režime CTR.

Vasily, [21.06.18 01:27] Oh, zistil som, čo je IGE: IGE bol prvý pokus o „režim overovania šifrovania“, pôvodne pre Kerberos. Bol to neúspešný pokus (neposkytuje ochranu integrity) a musel byť odstránený. To bol začiatok 20-ročného úsilia o overenie fungujúceho šifrovacieho režimu, ktorý nedávno vyvrcholil režimami ako OCB a GCM.

A teraz argumenty zo strany košíka:

Tím za Telegramom, ktorý vedie Nikolaj Durov, pozostáva zo šiestich šampiónov ACM, polovica z nich sú doktorandi z matematiky. Trvalo im asi dva roky, kým spustili aktuálnu verziu MTProto.

To je vtipné. Dva roky na nižšej úrovni

Alebo si môžete vziať tls

Dobre, povedzme, že sme vykonali šifrovanie a ďalšie nuansy. Je konečne možné posielať požiadavky serializované v TL a deserializovať odpovede? Čo a ako teda poslať? Tu je, povedzme, metóda initConnection, možno je to ono?

Vasily, [25.06.18 18:46] Inicializuje pripojenie a ukladá informácie na zariadení a aplikácii používateľa.

Akceptuje app_id, device_model, system_version, app_version a lang_code.

A nejaký dotaz

Dokumentácia ako vždy. Neváhajte a preštudujte si open source

Ak bolo s invokeWithLayer všetko približne jasné, tak čo je tu zlé? Ukázalo sa, že povedzme, že máme - klient sa už mal servera na niečo opýtať - existuje požiadavka, ktorú sme chceli poslať:

Vasily, [25.06.18 19:13] Súdiac podľa kódu, prvý hovor je zabalený do tohto svinstva a samotné svinstvo je zabalené do invokewithlayer

Prečo nemôže byť initConnection samostatným volaním, ale musí to byť obal? Áno, ako sa ukázalo, musí sa to robiť zakaždým na začiatku každej relácie, a nie raz, ako pri hlavnom kľúči. Ale! Neoprávnený používateľ ho nemôže zavolať! Teraz sme sa dostali do štádia, kedy je to použiteľné Toto dokumentačná stránka - a hovorí nám, že...

Len malá časť metód API je dostupná neoprávneným používateľom:

  • auth.sendCode
  • auth.resendCode
  • account.getPassword
  • auth.checkPassword
  • auth.checkPhone
  • auth.signUp
  • auth.signIn
  • auth.importAutorizácia
  • help.getConfig
  • help.getNearestDc
  • help.getAppUpdate
  • help.getCdnConfig
  • langpack.getLangPack
  • langpack.getStrings
  • langpack.getDifference
  • langpack.getLanguages
  • langpack.getLanguage

Hneď prvý z nich, auth.sendCode, a je tu vzácna prvá žiadosť, v ktorej pošleme api_id a api_hash a po ktorej dostaneme SMS s kódom. A ak sme v nesprávnom DC (telefónne čísla v tejto krajine obsluhuje napríklad iná), dostaneme chybu s číslom požadovaného DC. Ak chcete zistiť, ku ktorej IP adrese podľa čísla DC sa musíte pripojiť, pomôžte nám help.getConfig. Jeden čas tam bolo len 5 vstupov, no po slávnych udalostiach z roku 2018 sa počet výrazne zvýšil.

Teraz si pripomeňme, že do tejto fázy sme sa dostali na serveri anonymne. Nie je príliš drahé získať len IP adresu? Prečo to a ďalšie operácie neurobiť v nešifrovanej časti MTProto? Počujem námietku: „Ako sa môžeme uistiť, že to nie je RKN, kto bude odpovedať s falošnými adresami? K tomu si pamätáme, že vo všeobecnosti oficiálni klienti RSA kľúče sú vložené, t.j. môžeš len podpísať táto informácia. V skutočnosti sa to už robí pre informácie o obídení blokovania, ktoré klienti dostávajú cez iné kanály (logicky sa to nedá urobiť v samotnom MTProto, musíte tiež vedieť, kam sa pripojiť).

OK. V tejto fáze autorizácie klienta ešte nie sme autorizovaní a nezaregistrovali sme našu aplikáciu. Chceme len teraz vidieť, ako server reaguje na metódy dostupné neoprávnenému používateľovi. A tu…

Vasilij, [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;

V schéme je prvé druhé miesto

V schéme tdesktop je tretia hodnota

Áno, odvtedy sa samozrejme dokumentácia aktualizovala. Aj keď to môže byť čoskoro opäť irelevantné. Ako by to mal vedieť začínajúci vývojár? Možno ak zaregistrujete svoju žiadosť, budú vás informovať? Vasily to urobil, ale bohužiaľ mu nič neposlali (opäť o tom budeme hovoriť v druhej časti).

...Všimli ste si, že sme už nejako prešli na API, t.j. na ďalšiu úroveň a vynechali ste niečo v téme MTProto? Žiadne prekvapenie:

Vasily, [28.06.18 02:04] Hm, prehrabávajú sa v niektorých algoritmoch na e2e

Mtproto definuje šifrovacie algoritmy a kľúče pre obe domény, ako aj malú štruktúru obalu

Neustále však miešajú rôzne úrovne zásobníka, takže nie je vždy jasné, kde skončilo mtproto a kde začala ďalšia úroveň

Ako sa miešajú? Tu je rovnaký dočasný kľúč napríklad pre PFS (mimochodom, Telegram Desktop to nedokáže). Vykonáva sa požiadavkou API auth.bindTempAuthKey, t.j. z najvyššej úrovne. Zároveň však zasahuje do šifrovania na nižšej úrovni - po ňom to napríklad musíte urobiť znova initConnection atď., toto nie je proste normálna požiadavka. Čo je tiež zvláštne, je, že môžete mať iba JEDEN dočasný kľúč na DC, aj keď pole auth_key_id v každej správe vám umožňuje zmeniť kľúč aspoň v každej správe a že server má právo kedykoľvek „zabudnúť“ dočasný kľúč - dokumentácia nehovorí, čo robiť v tomto prípade... no, prečo by nemohol Nemáte niekoľko kľúčov, ako pri súprave budúcich solí a?...

Existuje niekoľko ďalších vecí, ktoré stojí za zmienku o téme MTProto.

Správy správ, msg_id, msg_seqno, potvrdenia, ping v nesprávnom smere a iné zvláštnosti

Prečo o nich potrebujete vedieť? Pretože „unikajú“ na vyššiu úroveň a pri práci s API si ich musíte uvedomiť. Predpokladajme, že nás msg_key nezaujíma; nižšia úroveň za nás všetko dešifrovala. Vo vnútri dešifrovaných údajov však máme nasledujúce polia (tiež dĺžku údajov, takže vieme, kde je výplň, ale to nie je dôležité):

  • soľ - int64
  • session_id - int64
  • message_id — int64
  • seq_no - int32

Pripomeňme, že na celé DC je len jedna soľ. Prečo o nej vedieť? Nielen preto, že existuje žiadosť get_future_salts, ktorý vám povie, ktoré intervaly budú platné, ale aj preto, že ak je vaša soľ „zhnitá“, správa (požiadavka) sa jednoducho stratí. Server samozrejme ohlási novú soľ vydaním new_session_created - ale pri starom to budes musiet nejako preposlat napr. A tento problém ovplyvňuje architektúru aplikácie.

Server má povolené úplne zrušiť relácie a odpovedať týmto spôsobom z mnohých dôvodov. Čo je vlastne relácia MTProto zo strany klienta? Toto sú dve čísla session_id и seq_no správy v rámci tejto relácie. No a základné pripojenie TCP, samozrejme. Povedzme, že náš klient stále nevie robiť veľa vecí, odpojil sa a znova sa pripojil. Ak sa to stalo rýchlo - stará relácia pokračovala v novom pripojení TCP, zvýšte seq_no ďalej. Ak by to trvalo dlho, server by ho mohol zmazať, pretože na jeho strane je to tiež front, ako sme zistili.

Čo by to malo byť seq_no? Oh, to je záludná otázka. Pokúste sa úprimne pochopiť, čo bolo myslené:

Správa týkajúca sa obsahu

Správa vyžadujúca výslovné potvrdenie. Patria sem všetky správy používateľov a mnoho servisných správ, prakticky všetky s výnimkou kontajnerov a potvrdení.

Poradové číslo správy (msg_seqno)

32-bitové číslo, ktoré sa rovná dvojnásobku počtu správ „súvisiacich s obsahom“ (tých, ktoré vyžadujú potvrdenie, a najmä tých, ktoré nie sú kontajnermi), ktoré vytvoril odosielateľ pred touto správou a následne sa zvýši o jednu, ak je aktuálna správa správa súvisiaca s obsahom. Kontajner sa generuje vždy po jeho celom obsahu; preto je jeho poradové číslo väčšie alebo rovné poradovým číslam správ v ňom obsiahnutých.

Čo je to za cirkus s prírastkom o 1 a potom ďalším o 2?.. Mám podozrenie, že pôvodne mysleli „najmenej významný bit pre ACK, zvyšok je číslo“, ale výsledok nie je úplne rovnaký - najmä to vyjde, dá sa poslať niektorí potvrdenia, ktoré majú to isté seq_no! Ako? Napríklad server nám niečo pošle, odošle a my sami zostaneme ticho a odpovedáme iba servisnými správami potvrdzujúcimi prijatie jeho správ. V tomto prípade budú mať naše odchádzajúce potvrdenia rovnaké odchádzajúce číslo. Ak ste oboznámení s TCP a mysleli ste si, že to znie nejako divoko, ale nezdá sa to príliš divoké, pretože v TCP seq_no sa nezmení, ale potvrdenie prejde na seq_no na druhej strane, ponáhľam sa ťa rozčúliť. Potvrdenia sú poskytované v MTProto NOT na seq_no, ako v TCP, ale pomocou msg_id !

Čo to je msg_id, najdôležitejšia z týchto oblastí? Jedinečný identifikátor správy, ako už názov napovedá. Je definované ako 64-bitové číslo, ktorého najnižšie bity majú opäť kúzlo „server-not-server“ a zvyšok je časová pečiatka Unixu, vrátane zlomkovej časti, posunutá o 32 bitov doľava. Tie. časová pečiatka ako taká (a správy s príliš odlišnými časmi budú serverom odmietnuté). Z toho vyplýva, že vo všeobecnosti ide o identifikátor, ktorý je pre klienta globálny. Vzhľadom na to – pamätajme session_id - garantujeme: Správa určená pre jednu reláciu nemôže byť za žiadnych okolností odoslaná do inej relácie. To znamená, že sa ukazuje, že už existuje tri úroveň - relácia, číslo relácie, id správy. Prečo taká nadmerná komplikácia, táto záhada je veľmi veľká.

Takže, msg_id potrebné pre...

RPC: požiadavky, odpovede, chyby. Potvrdenia.

Ako ste si mohli všimnúť, nikde v diagrame nie je žiadny špeciálny typ alebo funkcia „vytvoriť požiadavku RPC“, aj keď existujú odpovede. Koniec koncov, máme správy súvisiace s obsahom! teda akýkoľvek správa môže byť žiadosťou! Alebo nebyť. Po všetkom, z každého je msg_id. Ale existujú odpovede:

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

Tu je uvedené, na ktorú správu ide. Preto si na najvyššej úrovni API budete musieť zapamätať, aké bolo číslo vašej požiadavky - myslím, že nie je potrebné vysvetľovať, že práca je asynchrónna a že môže prebiehať niekoľko požiadaviek súčasne, odpovede na ktoré možno vrátiť v ľubovoľnom poradí? V zásade sa z tohto a chybových hlásení ako žiadni pracovníci dá vysledovať architektúra za tým: server, ktorý s vami udržiava TCP spojenie, je front-end balancer, posiela požiadavky na backendy a zbiera ich späť cez message_id. Zdá sa, že tu je všetko jasné, logické a dobré.

Áno?.. A ak sa nad tým zamyslíte? Veď aj samotná RPC odozva má pole msg_id! Musíme na server kričať „neodpovedáš na moju odpoveď!“? A áno, čo tam bolo s potvrdeniami? O stránke správy o správach nám hovorí, čo je

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

a musí to urobiť každá strana. Ale nie vždy! Ak ste dostali RpcResult, slúži ako potvrdenie. To znamená, že server môže odpovedať na vašu požiadavku pomocou MsgsAck - napríklad: „Dostal som to“. RpcResult môže reagovať okamžite. Môže to byť oboje.

A áno, stále musíte odpovedať! Potvrdenie. V opačnom prípade ho server bude považovať za nedoručiteľný a pošle vám ho znova. Aj po opätovnom pripojení. Ale tu, samozrejme, vyvstáva otázka časových limitov. Pozrime sa na ne trochu neskôr.

Medzitým sa pozrime na možné chyby pri vykonávaní dotazu.

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

Ach, niekto vykríkne, tu je humánnejší formát – tam je čiara! Neponáhľaj sa. Tu zoznam chýb, ale samozrejme nie kompletné. Z nej sa dozvedáme, že kód je niečo ako Chyby HTTP (samozrejme, sémantika odpovedí nie je rešpektovaná, na niektorých miestach sú medzi kódmi rozdelené náhodne) a riadok vyzerá takto CAPITAL_LETTERS_AND_NUMBERS. Napríklad PHONE_NUMBER_OCCUPIED alebo FILE_PART_Х_MISSING. To znamená, že tento riadok budete stále potrebovať rozobrať, Napríklad FLOOD_WAIT_3600 znamená, že budete musieť hodinu počkať a PHONE_MIGRATE_5, že telefónne číslo s touto predvoľbou je potrebné zaregistrovať v 5. DC. Máme typový jazyk, však? Nepotrebujeme argument z reťazca, bežné budú stačiť, dobre.

Opäť to nie je na stránke servisných správ, ale ako to už pri tomto projekte býva, informácie sa dajú nájsť na inej stránke dokumentácie, alebo vrhnúť podozrenie. Po prvé, pozrite sa, písanie / porušenie vrstvy - RpcError dá sa vnoriť RpcResult. Prečo nie vonku? Čo sme nebrali do úvahy?... Kde je teda záruka, že RpcError NEMUSÍ byť vložené do RpcResult, ale byť priamo alebo vnorený do iného typu?.. A ak nemôže, prečo nie je na najvyššej úrovni, t.j. chýba req_msg_id ? ..

Pokračujme však servisnými správami. Klient si môže myslieť, že server dlho premýšľa a predloží túto úžasnú požiadavku:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Na túto otázku existujú tri možné odpovede, ktoré sa opäť prelínajú s mechanizmom potvrdenia; snaha pochopiť, aké by mali byť (a aký všeobecný zoznam typov, ktoré nevyžadujú potvrdenie), je ponechaná na čitateľa ako domáca úloha (poznámka: informácie v zdrojový kód Telegram Desktop nie je úplný).

Drogová závislosť: stavy správ

Vo všeobecnosti mnohé miesta v TL, MTProto a Telegrame vo všeobecnosti zanechávajú pocit tvrdohlavosti, ale zo slušnosti, taktu a iných mäkké zručnosti Slušne sme o tom mlčali a obscénnosti v dialógoch sme cenzurovali. Avšak toto miestoОväčšina stránky je o správy o správach Je to šokujúce aj pre mňa, ktorý už dlho pracujem so sieťovými protokolmi a videl som bicykle rôzneho stupňa krivosti.

Začína to neškodne, potvrdeniami. Ďalej nám povedia o

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;

Každý, kto začne pracovať s MTProto, sa s nimi bude musieť vysporiadať, v cykle „opravené - prekompilované - spustené“ je bežné, že sa pri úpravách objavia chyby v číslach alebo soľ, ktorá sa pokazila. Sú tu však dva body:

  1. To znamená, že pôvodná správa sa stratí. Musíme vytvoriť nejaké fronty, pozrieme sa na to neskôr.
  2. Aké sú tieto podivné čísla chýb? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64... kde sú ostatné čísla, Tommy?

V dokumentácii sa uvádza:

Zámerom je, že hodnoty error_code sú zoskupené (error_code >> 4): napríklad kódy 0x40 — 0x4f zodpovedajú chybám pri rozklade kontajnera.

ale po prvé, posun opačným smerom a po druhé, nezáleží na tom, kde sú ostatné kódy? V hlave autora?.. To sú však maličkosti.

Závislosť začína v správach o stave správ a kópiách správ:

  • Žiadosť o informácie o stave správy
    Ak niektorá zo strán určitý čas nedostáva informácie o stave svojich odchádzajúcich správ, môže si ich od druhej strany výslovne vyžiadať:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Informačná správa týkajúca sa stavu správ
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Tu, info je reťazec, ktorý obsahuje presne jeden bajt stavu správy pre každú správu zo zoznamu prichádzajúcich msg_ids:

    • 1 = o správe nie je nič známe (msg_id je príliš nízke, druhá strana ho možno zabudla)
    • 2 = správa neprijatá (msg_id spadá do rozsahu uložených identifikátorov; druhá strana však takúto správu určite nedostala)
    • 3 = správa neprijatá (msg_id je príliš vysoká, druhá strana ju však určite ešte nedostala)
    • 4 = správa prijatá (všimnite si, že táto odpoveď je zároveň aj potvrdením o prijatí)
    • +8 = správa už bola potvrdená
    • +16 = správa nevyžadujúca potvrdenie
    • +32 = RPC dotaz obsiahnutý v správe sa spracováva alebo je spracovanie už ukončené
    • +64 = odpoveď súvisiaca s obsahom na už vygenerovanú správu
    • +128 = druhá strana vie, že správa už bola prijatá
      Táto odpoveď nevyžaduje potvrdenie. Je to potvrdenie príslušného msgs_state_req, ako také.
      Všimnite si, že ak sa zrazu ukáže, že druhá strana nemá správu, ktorá vyzerá, že jej bola odoslaná, správu možno jednoducho odoslať znova. Aj keď by druhá strana mala dostať dve kópie správy súčasne, duplikát bude ignorovaný. (Ak uplynulo príliš veľa času a pôvodné msg_id už nie je platné, správa sa má zabaliť do msg_copy).
  • Dobrovoľné oznamovanie stavu správ
    Ktorákoľvek strana môže dobrovoľne informovať druhú stranu o stave správ odoslaných druhou stranou.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Rozšírená dobrovoľná komunikácia o stave jednej správy
    ...
    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;
  • Explicitná žiadosť o opätovné odoslanie správ
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    Vzdialená strana okamžite odpovie opätovným odoslaním požadovaných správ […]
  • Explicitná žiadosť o opätovné odoslanie odpovedí
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    Vzdialená strana okamžite odpovie opätovným odoslaním odpovede na požadované správy […]
  • Kópie správ
    V niektorých situáciách je potrebné znova odoslať starú správu s msg_id, ktorá už nie je platná. Potom sa zabalí do kópie:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Po prijatí sa správa spracuje tak, ako keby tam obal nebol. Ak je však s istotou známe, že správa orig_message.msg_id bola prijatá, potom sa nová správa nespracuje (zatiaľ čo súčasne s ňou a orig_message.msg_id sú potvrdené). Hodnota orig_message.msg_id musí byť nižšia ako msg_id kontajnera.

Pomlčme aj o čom msgs_state_info opäť trčia uši nedokončeného TL (potrebovali sme vektor bajtov a v spodných dvoch bitoch bol enum a vo vyšších dvoch bitoch boli príznaky). Pointa je iná. Chápe niekto, prečo je to všetko v praxi? u skutočného klienta potrebné?... S ťažkosťami, ale možno si predstaviť nejakú výhodu, ak sa osoba zaoberá ladením av interaktívnom režime - opýtajte sa servera, čo a ako. Ale tu sú popísané požiadavky spiatočná cesta.

Z toho vyplýva, že každá strana musí správy nielen šifrovať a odosielať, ale aj uchovávať údaje o sebe, o odpovediach na ne, na neznámy čas. Dokumentácia nepopisuje ani načasovanie, ani praktickú použiteľnosť týchto funkcií. nijako. Čo je najúžasnejšie, je, že sa skutočne používajú v kódexe oficiálnych klientov! Zrejme im povedali niečo, čo nebolo zahrnuté vo verejnej dokumentácii. Pochopte z kódu prečo, už nie je taký jednoduchý ako v prípade TL - nejde o (relatívne) logicky izolovanú časť, ale o kus naviazaný na architektúru aplikácie, t.j. bude vyžadovať podstatne viac času na pochopenie kódu aplikácie.

Pingy a načasovanie. Fronty.

Zo všetkého, ak si spomenieme na dohady o architektúre servera (distribúcia požiadaviek cez backendy), vyplýva dosť smutná vec - napriek všetkým garanciám doručenia v TCP (buď sú dáta doručené, alebo budete informovaní o medzere, ale údaje budú doručené skôr, ako sa vyskytne problém), že potvrdenie v samotnom MTProto - žiadne záruky. Server môže ľahko stratiť alebo vyhodiť vašu správu a nič sa s tým nedá robiť, stačí použiť rôzne typy bariel.

A v prvom rade - fronty správ. No s jednou vecou bolo všetko zrejmé od samého začiatku – nepotvrdenú správu treba uložiť a znova odoslať. A po akom čase? A ten šašo ho pozná. Možno, že tieto správy o závislých službách nejakým spôsobom vyriešia tento problém pomocou bariel, povedzme, v Telegram Desktop im zodpovedajú asi 4 fronty (možno viac, ako už bolo spomenuté, preto sa musíte vážnejšie ponoriť do jeho kódu a architektúry; čas, vieme, že ho nemožno brať ako vzorku, nie je v ňom použitý určitý počet typov zo schémy MTProto).

Prečo sa to deje? Programátori serverov pravdepodobne nedokázali zabezpečiť spoľahlivosť v rámci klastra alebo dokonca vyrovnávaciu pamäť na prednom balanceri a preniesli tento problém na klienta. Zo zúfalstva sa Vasily pokúsil implementovať alternatívnu možnosť iba s dvoma frontami pomocou algoritmov z TCP - meranie RTT na server a úprava veľkosti „okna“ (v správach) v závislosti od počtu nepotvrdených požiadaviek. To znamená, že taká hrubá heuristika na posúdenie zaťaženia servera je to, koľko našich požiadaviek dokáže prežuť súčasne a nestratí ich.

No, rozumieš, však? Ak musíte znova implementovať TCP nad protokol bežiaci cez TCP, znamená to veľmi zle navrhnutý protokol.

Ach áno, prečo potrebujete viac ako jeden front a čo to znamená pre človeka pracujúceho s vysokoúrovňovým API? Pozrite, zadáte požiadavku, serializujete ju, ale často ju nemôžete odoslať okamžite. prečo? Pretože odpoveď bude msg_id, ktorá je dočasnáаSom štítok, ktorého pridelenie je najlepšie odložiť na čo najneskorší čas – v prípade, že ho server odmietne z dôvodu nesúladu času medzi nami a ním (samozrejme, môžeme urobiť barličku, ktorá posunie náš čas zo súčasnosti na server pridaním delty vypočítanej z odpovedí servera – oficiálni klienti to robia, ale je to hrubé a nepresné kvôli vyrovnávacej pamäti). Preto, keď zadáte požiadavku pomocou lokálneho volania funkcie z knižnice, správa prejde týmito fázami:

  1. Leží v jednej fronte a čaká na šifrovanie.
  2. Menovaný msg_id a správa prešla do iného frontu - možné preposielanie; poslať do zásuvky.
  3. a) Server odpovedal MsgsAck - správa bola doručená, vymažeme ju z „iného frontu“.
    b) Alebo naopak, niečo sa mu nepáčilo, odpovedal badmsg - znova odoslať z „iného frontu“
    c) Nič nie je známe, správu je potrebné znova odoslať z iného frontu – nevie sa však presne kedy.
  4. Server nakoniec odpovedal RpcResult - skutočná odpoveď (alebo chyba) - nielen doručená, ale aj spracovaná.

Možno, použitie kontajnerov by mohlo problém čiastočne vyriešiť. To je, keď je veľa správ zbalených do jednej a server odpovedal potvrdením na všetky naraz, v jednom msg_id. Ale aj túto svorku odmietne, ak sa niečo pokazí, celú.

A v tomto bode vstupujú do hry netechnické úvahy. Zo skúseností sme videli mnohé barličky a navyše teraz uvidíme viac príkladov zlých rád a architektúry – oplatí sa v takýchto podmienkach dôverovať a robiť takéto rozhodnutia? Otázka je rečnícka (samozrejme, že nie).

o čom to hovoríme? Ak na tému „drogové správy o správach“ stále môžete špekulovať s námietkami typu „si hlúpy, nepochopil si náš skvelý plán!“ (takže najprv napíšte dokumentáciu, ako by to mali normálni ľudia, s odôvodnením a príkladmi výmeny paketov, potom sa porozprávame), potom sú načasovanie/časové limity čisto praktická a špecifická otázka, všetko je tu už dávno známe. Čo nám dokumentácia hovorí o časových limitoch?

Server zvyčajne potvrdí prijatie správy od klienta (zvyčajne dotaz RPC) pomocou odpovede RPC. Ak odpoveď prichádza dlho, server môže najskôr poslať potvrdenie o prijatí a o niečo neskôr aj samotnú odpoveď RPC.

Klient zvyčajne potvrdí prijatie správy zo servera (zvyčajne odpoveď RPC) pridaním potvrdenia k ďalšiemu dotazu RPC, ak nie je odoslaná príliš neskoro (ak je vygenerovaná napríklad 60 – 120 sekúnd po prijatí správy zo servera). Ak však po dlhú dobu nie je dôvod posielať správy na server alebo ak je zo servera veľký počet nepotvrdených správ (povedzme viac ako 16), klient odošle samostatné potvrdenie.

... Prekladám: sami nevieme, koľko a ako to potrebujeme, tak predpokladajme, že nech je to takto.

A o pingoch:

Ping správy (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

Odpoveď sa zvyčajne vráti rovnakému pripojeniu:

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

Tieto správy nevyžadujú potvrdenie. Ping sa prenáša iba ako odpoveď na ping, zatiaľ čo ping môže iniciovať ktorákoľvek strana.

Odložené zatvorenie pripojenia + PING

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

Funguje ako ping. Okrem toho po prijatí tohto servera spustí časovač, ktorý o niekoľko sekúnd neskôr ukončí aktuálne pripojenie connect_delay, pokiaľ nedostane novú správu rovnakého typu, ktorá automaticky vynuluje všetky predchádzajúce časovače. Ak klient posiela tieto pingy napríklad raz za 60 sekúnd, môže nastaviť odpojenie_delay na 75 sekúnd.

Si šialený?! Za 60 sekúnd vlak vojde do stanice, vysadí a naberie cestujúcich a opäť stratí kontakt v tuneli. O 120 sekúnd, kým ho budete počuť, príde na ďalší a spojenie sa s najväčšou pravdepodobnosťou preruší. No, je jasné, odkiaľ nohy pochádzajú - „Počul som zvonenie, ale neviem, kde to je“, existuje Naglov algoritmus a možnosť TCP_NODELAY, určená na interaktívnu prácu. Ale prepáčte, držte sa predvolenej hodnoty - 200 Millisekúnd Ak naozaj chcete znázorniť niečo podobné a ušetriť na možných pár paketoch, odložte to o 5 sekúnd, alebo ako je teraz časový limit správy „Používateľ píše...“. Ale nie viac.

A nakoniec pingy. Teda kontrola živosti TCP spojenia. Je to smiešne, ale asi pred 10 rokmi som napísal kritický text o messengerovi internátu našej fakulty - autori tam tiež pingli server od klienta a nie naopak. Ale študenti 3. ročníka sú jedna vec a medzinárodná kancelária druhá, však?...

Najprv malý vzdelávací program. Spojenie TCP môže pri absencii výmeny paketov trvať týždne. To je dobré aj zlé, v závislosti od účelu. Je dobré, ak ste mali otvorené pripojenie SSH na server, vstali ste od počítača, reštartovali router, vrátili sa na svoje miesto - relácia cez tento server nebola prerušená (nič ste nenapísali, neboli žiadne pakety) , je to pohodlné. Je zlé, ak sú na serveri tisíce klientov, z ktorých každý zaberá zdroje (ahoj, Postgres!) a hostiteľ klienta sa možno už dávno reštartoval – ale my sa o tom nedozvieme.

Chat/IM systémy spadajú do druhého prípadu z jedného dôvodu navyše – online statusov. Ak používateľ „spadol“, musíte o tom informovať svojich partnerov. V opačnom prípade skončíte s chybou, ktorú urobili tvorcovia Jabberu (a opravovali 20 rokov) - používateľ sa odpojil, ale naďalej mu píšu správy v domnení, že je online (ktoré sa tiež úplne stratili v týchto niekoľko minút pred zistením odpojenia). Nie, tu nepomôže možnosť TCP_KEEPALIVE, ktorú veľa ľudí, ktorí nerozumejú fungovaniu časovačov TCP, vhodí náhodne (nastavením divokých hodnôt, ako sú desiatky sekúnd), treba sa uistiť, že nielen jadro OS stroja používateľa je nažive, ale tiež funguje normálne, je schopný reagovať a samotná aplikácia (myslíte, že nemôže zamrznúť? Telegram Desktop na Ubuntu 18.04 mi zamrzol viac ako raz).

Preto musíte pingnúť server klient, a nie naopak - ak to klient urobí, ak sa spojenie preruší, ping sa nedoručí, cieľ sa nedosiahne.

Čo vidíme na telegrame? Je to presne naopak! No, to je. Formálne sa samozrejme môžu obe strany navzájom pingovať. V praxi klienti používajú barle ping_delay_disconnect, ktorý nastavuje časovač na serveri. No, prepáčte, nie je na klientovi, aby sa rozhodol, ako dlho tam chce žiť bez pingu. Server na základe svojej záťaže vie lepšie. Ale, samozrejme, ak vám nebudú vadiť zdroje, potom budete svojim vlastným zlým Pinocchiom a postačí vám barlička...

Ako to malo byť navrhnuté?

Domnievam sa, že uvedené skutočnosti jednoznačne svedčia o tom, že tím Telegram/VKontakte nie je príliš kompetentný v oblasti dopravnej (a nižšej) úrovne počítačových sietí a ich nízka kvalifikácia v relevantných veciach.

Prečo sa to ukázalo byť také komplikované a ako sa môžu architekti Telegramu pokúsiť namietať? Skutočnosť, že sa pokúsili vytvoriť reláciu, ktorá prežije prerušenie TCP spojenia, teda to, čo nebolo doručené teraz, dodáme neskôr. Pravdepodobne sa tiež pokúsili urobiť prenos UDP, ale narazili na ťažkosti a opustili ho (preto je dokumentácia prázdna - nebolo sa čím chváliť). Ale kvôli nepochopeniu toho, ako siete vo všeobecnosti a TCP konkrétne fungujú, kde sa na to môžete spoľahnúť a kde to musíte urobiť sami (a ako), a pokusu skombinovať to s kryptografiou „dva vtáky s jeden kameň“, toto je výsledok.

Ako to bolo potrebné? Na základe skutočnosti, že msg_id je časová pečiatka potrebná z kryptografického hľadiska na zabránenie opakovaným útokom, je chybou pripájať k nej funkciu jedinečného identifikátora. Preto bez zásadnej zmeny súčasnej architektúry (keď sa generuje stream aktualizácií, to je téma API na vysokej úrovni pre inú časť tejto série príspevkov), bude potrebné:

  1. Server, ktorý drží TCP spojenie s klientom, preberá zodpovednosť - ak čítal zo soketu, potvrďte, spracujte alebo vráťte chybu, žiadna strata. Potom potvrdenie nie je vektor id, ale jednoducho „posledné prijaté seq_no“ - iba číslo, ako v TCP (dve čísla - vaše seq a potvrdené). Sme vždy v rámci relácie, však?
  2. Časová pečiatka na zabránenie opakovaným útokom sa stáva samostatným poľom, a la nonce. Kontroluje sa, ale nič iné neovplyvňuje. Dosť a uint32 - ak sa naša soľ mení aspoň každých pol dňa, môžeme prideliť 16 bitov nižším bitom celočíselnej časti aktuálneho času, zvyšok - zlomkovej časti sekundy (ako teraz).
  3. Je odstránený msg_id vôbec - z hľadiska rozlíšenia požiadaviek na backendoch existuje po prvé ID klienta a po druhé ID relácie, ktoré ich zreťazí. Preto ako identifikátor požiadavky postačuje iba jedna vec seq_no.

Toto tiež nie je najúspešnejšia možnosť, ako identifikátor by mohol slúžiť úplný náhodný výber – to sa mimochodom už robí v API na vysokej úrovni pri odosielaní správy. Bolo by lepšie úplne prerobiť architektúru z relatívnej na absolútnu, ale to je téma na inú časť, nie na tento príspevok.

API?

Ta-daam! Takže keď sme sa prebojovali cestou plnou bolesti a bariel, konečne sme mohli posielať akékoľvek požiadavky na server a dostávať na ne odpovede, ako aj prijímať aktualizácie zo servera (nie ako odpoveď na požiadavku, ale on sám pošle nám, ako PUSH, ak je to niekomu jasnejšie).

Pozor, teraz bude v článku jediný príklad v Perle! (pre tých, ktorí nie sú oboznámení so syntaxou, prvý argument bless je dátová štruktúra objektu, druhý je jeho trieda):

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' )
};

Áno, nie zámerne spoiler - ak ste ho ešte nečítali, pokračujte a urobte to!

Oh, wai~~... ako to vyzerá? Niečo veľmi známe... možno toto je dátová štruktúra typického webového API v JSON, okrem toho, že triedy sú tiež pripojené k objektom?...

Takže takto to dopadá... O čo ide, súdruhovia?... Toľko námahy - a zastavili sme sa, aby sme si oddýchli tam, kde programátori webu práve začína?..Nebol by jednoduchší len JSON cez HTTPS?! Čo sme dostali výmenou? Stála tá námaha za to?

Poďme zhodnotiť, čo nám TL+MTProto dalo a aké sú možné alternatívy. HTTP, ktorý sa zameriava na model žiadosť-odpoveď, nie je vhodný, ale aspoň niečo navyše k TLS?

Kompaktná serializácia. Keď vidím túto dátovú štruktúru, podobnú JSON, pamätám si, že existujú jej binárne verzie. Označme MsgPack ako nedostatočne rozšíriteľný, no existuje napríklad CBOR - mimochodom štandard popísaný v r. RFC 7049. Je pozoruhodný tým, že definuje tagy, ako expanzný mechanizmus a medzi už štandardizované k dispozícii:

  • 25 + 256 - nahradenie opakovaných riadkov odkazom na číslo riadku, taká lacná metóda kompresie
  • 26 - serializovaný objekt Perl s názvom triedy a argumentmi konštruktora
  • 27 - serializovaný objekt nezávislý od jazyka s názvom typu a argumentmi konštruktora

Pokúsil som sa serializovať rovnaké údaje v TL a CBOR s povoleným balením reťazcov a objektov. Výsledok sa začal líšiť v prospech CBOR niekde od megabajtu:

cborlen=1039673 tl_len=1095092

Takže, záver: Existujú podstatne jednoduchšie formáty, ktoré nepodliehajú problému zlyhania synchronizácie alebo neznámeho identifikátora, s porovnateľnou účinnosťou.

Rýchle nadviazanie spojenia. To znamená nulové RTT po opätovnom pripojení (keď bol kľúč už raz vygenerovaný) - použiteľné od úplne prvej správy MTProto, ale s určitými výhradami - narazíte na rovnakú soľ, relácia nie je prehnitá atď. Čo nám namiesto toho ponúka TLS? Citát k téme:

Keď používate PFS v TLS, lístky relácie TLS (RFC 5077) na obnovenie šifrovanej relácie bez opätovného vyjednávania kľúčov a bez ukladania informácií o kľúčoch na server. Pri otvorení prvého spojenia a vytvorení kľúčov server zašifruje stav spojenia a odošle ho klientovi (vo forme session ticketu). V súlade s tým, keď sa spojenie obnoví, klient pošle lístok relácie, vrátane kľúča relácie, späť na server. Samotný lístok je zašifrovaný dočasným kľúčom (session ticket key), ktorý je uložený na serveri a musí byť distribuovaný medzi všetky frontend servery spracúvajúce SSL v klastrových riešeniach.[10] Zavedenie relačného lístka teda môže narušiť PFS, ak dôjde k ohrozeniu dočasných kľúčov servera, napríklad ak sú uložené na dlhú dobu (OpenSSL, nginx, Apache ich štandardne ukladajú počas celého trvania programu; obľúbené stránky používajú kľúč niekoľko hodín, až dní).

Tu RTT nie je nula, musíte si vymeniť aspoň ClientHello a ServerHello, potom môže klient posielať dáta spolu s Finished. Tu by sme však mali pamätať na to, že nemáme web s množstvom novootvorených spojení, ale messenger, ktorého spojenie je často jednou a viac-menej dlhotrvajúcou, relatívne krátkou požiadavkou na webové stránky - všetko je multiplexované interne. To znamená, že je celkom prijateľné, ak sme nenarazili na skutočne zlú časť metra.

Zabudli ste na niečo iné? Napíšte do komentárov.

Pokračovanie nabudúce!

V druhej časti tejto série príspevkov sa budeme zaoberať nie technickými, ale organizačnými otázkami - prístupy, ideológia, rozhranie, postoj k používateľom atď. Na základe technických informácií, ktoré tu boli uvedené.

Tretia časť bude pokračovať v analýze technických komponentov / skúseností s vývojom. Dozviete sa najmä:

  • pokračovanie pandemónia s rôznymi typmi TL
  • neznáme veci o kanáloch a superskupinách
  • prečo sú dialógy horšie ako zoznam
  • o absolútnom a relatívnom adresovaní správy
  • aký je rozdiel medzi fotkou a obrázkom
  • ako emotikony zasahujú do textu kurzívy

a iné barle! Zostaňte naladení!

Zdroj: hab.com

Pridať komentár