Kritika protokola i organizacionih pristupa Telegrama. Dio 1, tehnički: iskustvo pisanja klijenta od nule - TL, MT

Nedavno su na Habréu sve češće počeli da se pojavljuju postovi o tome koliko je Telegram dobar, koliko su braća Durov briljantna i iskusna u izgradnji mrežnih sistema itd. U isto vrijeme, vrlo malo ljudi se zaista uživjelo u tehnički uređaj - najviše, oni koriste prilično jednostavan (i prilično drugačiji od MTProto) JSON-based Bot API, i obično jednostavno prihvataju na vjeru sve pohvale i PR koji se vrte oko glasnika. Prije skoro godinu i po, moj kolega iz nevladine organizacije Eshelon Vasily (nažalost, njegov račun na Habréu je izbrisan zajedno sa nacrtom) počeo je ispočetka pisati svoj Telegram klijent na Perlu, a kasnije se pridružio i autor ovih redova. Zašto Perl, neki će odmah upitati? Jer takvi projekti već postoje na drugim jezicima, zapravo nije u tome stvar, može postojati bilo koji drugi jezik na kojem ne postoji gotova biblioteka, te prema tome autor mora ići do kraja od nule. Štaviše, kriptografija je stvar povjerenja, ali provjerite. Sa proizvodom koji ima za cilj sigurnost, ne možete se jednostavno osloniti na gotovu biblioteku proizvođača i slijepo joj vjerovati (međutim, ovo je tema za drugi dio). Trenutno biblioteka radi prilično dobro na "prosječnom" nivou (omogućava vam da napravite bilo koji API zahtjev).

Međutim, u ovoj seriji postova neće biti puno kriptografije ili matematike. Ali bit će tu mnogo drugih tehničkih detalja i arhitektonskih štaka (takođe korisnih za one koji neće pisati od nule, već će koristiti biblioteku na bilo kojem jeziku). Dakle, glavni cilj je bio pokušati implementirati klijenta od nule prema službenoj dokumentaciji. Odnosno, pretpostavimo da je izvorni kod službenih klijenata zatvoren (opet, u drugom dijelu ćemo detaljnije pokriti temu činjenice da je to istina to se događa tako), ali, kao u stara vremena, na primjer, postoji standard poput RFC-a - da li je moguće napisati klijenta samo prema specifikaciji, "bez gledanja" u izvorni kod, bio on službeni (Telegram Desktop, mobilni), ili nezvanični Teleton?

Sadržaj:

Dokumentacija... postoji, zar ne? Da li je istina?..

Fragmenti bilješki za ovaj članak počeli su se prikupljati prošlog ljeta. Sve ovo vrijeme na službenoj web stranici https://core.telegram.org Dokumentacija je bila od sloja 23, tj. zaglavio negdje 2014. (sjećate se, tada nije bilo ni kanala?). Naravno, u teoriji, to nam je trebalo omogućiti da implementiramo klijenta s funkcionalnošću u to vrijeme 2014. godine. Ali i u ovom stanju dokumentacija je bila, prvo, nepotpuna, a drugo, na mjestima je bila kontradiktorna. Bilo je to prije nešto više od mjesec dana, u septembru 2019 slučajno Otkriveno je da postoji veliko ažuriranje dokumentacije na sajtu, za potpuno noviji Layer 105, uz napomenu da sada sve treba ponovo pročitati. Doista, mnogi članci su revidirani, ali mnogi su ostali nepromijenjeni. Stoga, kada čitate kritike u nastavku o dokumentaciji, treba imati na umu da neke od ovih stvari više nisu relevantne, ali neke su još uvijek sasvim relevantne. Na kraju krajeva, 5 godina u modernom svijetu nije samo dugo, već vrlo puno. Od tada (posebno ako ne uzmete u obzir odbačene i oživljene geochat stranice od tada), broj API metoda u shemi je porastao sa sto na više od dvjesto pedeset!

Odakle početi kao mladi autor?

Nije bitno da li pišete od nule ili koristite, na primjer, gotove biblioteke kao što su Teleton za Python ili Madeline za PHP, u svakom slučaju, prvo će vam trebati registrirajte svoju prijavu - dobiti parametre api_id и api_hash (oni koji su radili sa VKontakte API-jem odmah razumiju) po kojima će server identificirati aplikaciju. Ovo morati učinite to iz pravnih razloga, ali ćemo više o tome zašto bibliotečki autori ne mogu to objaviti u drugom dijelu. Možda ćete biti zadovoljni vrijednostima testa, iako su one vrlo ograničene - činjenica je da se sada možete registrirati samo jedan aplikaciju, stoga ne žurite bezglavo u nju.

Sada, sa tehničke strane, trebalo bi da nas zanima da nakon registracije dobijemo obaveštenja od Telegrama o ažuriranju dokumentacije, protokola itd. Odnosno, moglo bi se pretpostaviti da je stranica s dokovima jednostavno napuštena i nastavila raditi posebno s onima koji su počeli stvarati klijente, jer lakše je. Ali ne, ništa slično nije primijećeno, nikakve informacije nisu stigle.

A ako pišete od nule, onda je korištenje dobivenih parametara zapravo još daleko. Iako https://core.telegram.org/ i govori o njima u Getting Started pre svega, u stvari, prvo ćete morati da implementirate MTProto protokol - ali ako si vjerovao raspored prema OSI modelu na kraju stranice za opći opis protokola, onda je potpuno uzaludno.

Zapravo, i prije i nakon MTProta, na nekoliko nivoa odjednom (kako kažu strani mrežari koji rade u jezgru OS-a, kršenje sloja), velika, bolna i užasna tema će stati na put...

Binarna serijalizacija: TL (Jezik tipova) i njegova šema, i slojevi, i mnoge druge strašne riječi

Ova tema je, zapravo, ključna za probleme Telegrama. I biće mnogo strašnih reči ako pokušate da udubite u to.

Dakle, evo dijagrama. Ako vam ova riječ padne na pamet, recite, JSON shema, ispravno ste mislili. Cilj je isti: neki jezik za opisivanje mogućeg skupa prenesenih podataka. Tu se sličnosti završavaju. Ako sa stranice MTProto protokol, ili iz izvornog stabla zvaničnog klijenta, pokušaćemo da otvorimo neku šemu, videćemo nešto poput:

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;

Osoba koja to prvi put vidi intuitivno će moći prepoznati samo dio napisanog - pa, to su očigledno strukture (mada gdje je ime, lijevo ili desno?), u njima ima polja, nakon čega slijedi tip nakon dvotočka... vjerovatno. Ovdje u ugaonim zagradama vjerovatno postoje šabloni kao u C++ (u stvari, ne baš). A šta znače svi ostali simboli, upitnici, uzvičnici, procenti, heš znakovi (i očigledno znače različite stvari na različitim mjestima), ponekad prisutni, a ponekad ne, heksadecimalni brojevi - i najvažnije, kako izvući iz ovoga regularan (koji server neće odbiti) bajt stream? Morat ćete pročitati dokumentaciju (da, postoje veze do šeme u JSON verziji u blizini - ali to ne čini ništa jasnijim).

Otvorite stranicu Serijalizacija binarnih podataka i zaronite u magični svijet gljiva i diskretne matematike, nešto slično matanu u 4. godini. Abeceda, tip, vrijednost, kombinator, funkcionalni kombinator, normalna forma, kompozitni tip, polimorfni tip... i to je sve samo prva stranica! Sledeće vas čeka TL Language, koji, iako već sadrži primjer trivijalnog zahtjeva i odgovora, uopće ne daje odgovor na tipičnije slučajeve, što znači da ćete morati da se probijate kroz prepričavanje matematike prevedene s ruskog na engleski na još osam ugrađenih stranice!

Čitaoci upoznati s funkcionalnim jezicima i automatskim zaključivanjem tipova će, naravno, vidjeti jezik opisa na ovom jeziku, čak i iz primjera, kao mnogo poznatiji, i mogu reći da to zapravo nije loše u principu. Prigovori na ovo su:

  • da, svrhu zvuči dobro, ali avaj, ona nije postignuto
  • Obrazovanje na ruskim univerzitetima varira čak i među IT specijalnostima - nisu svi pohađali odgovarajući kurs
  • Konačno, kao što ćemo vidjeti, u praksi je tako nije potrebno, budući da se koristi samo ograničeni podskup čak i TL koji je opisan

Kao što je rečeno LeoNerd na kanalu #perl u FreeNode IRC mreži, pokušavajući implementirati kapiju iz Telegrama u Matrix (prijevod citata je netačan iz sjećanja):

Čini se kao da je neko prvi put uveden u teoriju kucanja, uzbudio se i počeo da se igra s njom, ne mareći baš da li je to potrebno u praksi.

Uvjerite se sami, da li potreba za golim tipovima (int, long, itd.) kao nečim elementarnim ne postavlja pitanja - u konačnici oni se moraju implementirati ručno - na primjer, hajde da pokušamo da izvedemo iz njih vektora. To je, u stvari, niz, ako nastale stvari nazovete pravim imenom.

Ali prije

Kratak opis podskupa TL sintakse za one koji ne čitaju zvaničnu dokumentaciju

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;

Definicija uvijek počinje Konstruktor, nakon čega po želji (u praksi - uvijek) kroz simbol # mora biti CRC32 iz normaliziranog niza opisa ovog tipa. Zatim slijedi opis polja; ako postoje, tip može biti prazan. Sve se ovo završava znakom jednakosti, imenom tipa kojem ovaj konstruktor - to jest, u stvari, podtip - pripada. Momak desno od znaka jednakosti je polimorfna - to jest, može mu odgovarati nekoliko specifičnih tipova.

Ako se definicija pojavljuje nakon reda ---functions---, tada će sintaksa ostati ista, ali značenje će biti drugačije: konstruktor će postati ime RPC funkcije, polja će postati parametri (pa, to jest, ostat će potpuno ista data struktura, kao što je opisano u nastavku , to će jednostavno biti dodijeljeno značenje), a "polimorfni tip" - tip vraćenog rezultata. Istina, on će i dalje ostati polimorfan - upravo definiran u odjeljku ---types---, ali ovaj konstruktor se “neće uzeti u obzir”. Preopterećenje tipova pozvanih funkcija njihovim argumentima, tj. Iz nekog razloga, nekoliko funkcija sa istim imenom, ali različitim potpisima, kao u C++, nije predviđeno u TL-u.

Zašto "konstruktor" i "polimorfni" ako nije OOP? Pa, u stvari, nekome će biti lakše razmišljati o ovome u OOP terminima - polimorfni tip kao apstraktna klasa, a konstruktori su njegovi direktni potomci, i final u terminologiji brojnih jezika. U stvari, naravno, samo ovdje sličnost sa stvarnim preopterećenim konstruktorskim metodama u OO programskim jezicima. Pošto su ovdje samo strukture podataka, ne postoje metode (iako je daljnji opis funkcija i metoda prilično sposoban stvoriti zabunu u glavi da postoje, ali to je druga stvar) - konstruktor možete zamisliti kao vrijednost iz koji se gradi tip prilikom čitanja toka bajtova.

Kako se to događa? Deserijalizator, koji uvijek čita 4 bajta, vidi vrijednost 0xcrc32 - i razume šta će se sledeće desiti field1 sa tipom int, tj. čita tačno 4 bajta, na ovom polju iznad sa tipom PolymorType čitaj. Vidi 0x2crc32 i shvata da postoje još dva polja, prvo long, što znači da čitamo 8 bajtova. I onda opet složeni tip, koji je deserializiran na isti način. Na primjer, Type3 može se deklarisati u krugu čim dva konstruktora, odnosno, tada se moraju sastati bilo koji 0x12abcd34, nakon čega trebate pročitati još 4 bajta int, ili 0x6789cdef, nakon čega neće biti ništa. Bilo šta drugo - morate izbaciti izuzetak. U svakom slučaju, nakon ovoga se vraćamo na čitanje 4 bajta int margine field_c в constructorTwo i time završavamo čitanje našeg PolymorType.

Konačno, ako vas uhvate 0xdeadcrc do constructorThree, onda sve postaje komplikovanije. Naše prvo polje je bit_flags_of_what_really_present sa tipom # - u stvari, ovo je samo pseudonim za tip nat, što znači "prirodni broj". To jest, u stvari, unsigned int je, inače, jedini slučaj kada se nepredpisani brojevi javljaju u realnim kolima. Dakle, sledeća je konstrukcija sa upitnikom, što znači da će ovo polje – biti prisutno na žici samo ako je odgovarajući bit postavljen u polje na koje se poziva (približno kao ternarni operator). Dakle, pretpostavimo da je ovaj bit postavljen, što znači da dalje trebamo pročitati polje poput Type, koji u našem primjeru ima 2 konstruktora. Jedan je prazan (sastoji se samo od identifikatora), drugi ima polje ids sa tipom ids:Vector<long>.

Možda mislite da su i šabloni i generici u profesionalcima ili Javi. Ali ne. Skoro. Ovo jedini slučaju upotrebe ugaonih zagrada u realnim kolima, a koristi se SAMO za Vector. U toku bajtova, to će biti 4 CRC32 bajta za sam tip Vector, uvijek isti, zatim 4 bajta - broj elemenata niza, a zatim sami ovi elementi.

Dodajte tome činjenicu da se serijalizacija uvijek događa u riječima od 4 bajta, svi tipovi su višestruki - ugrađeni tipovi su također opisani bytes и string sa ručnom serijalizacijom dužine i ovim poravnanjem za 4 - pa, čini se da zvuči normalno pa čak i relativno efikasno? Iako se za TL tvrdi da je efikasna binarna serijalizacija, dođavola s njima, s proširenjem gotovo svega, čak i Booleovih vrijednosti i nizova od jednog karaktera na 4 bajta, hoće li JSON i dalje biti mnogo deblji? Gledajte, čak i nepotrebna polja se mogu preskočiti bitnim zastavicama, sve je sasvim dobro, pa čak i proširivo za budućnost, pa zašto kasnije ne dodati nova opciona polja u konstruktor?..

Ali ne, ako pročitate ne moj kratak opis, već punu dokumentaciju, i razmislite o implementaciji. Prvo, CRC32 konstruktora se izračunava prema normalizovanoj liniji tekstualnog opisa šeme (ukloni dodatni razmak, itd.) - tako da ako se doda novo polje, linija opisa tipa će se promeniti, a samim tim i njen CRC32 i , posljedično, serijalizacija. A šta bi stari klijent uradio da dobije polje sa novim postavljenim zastavama, a ne zna šta dalje sa njima?..

Drugo, prisjetimo se CRC32, koji se ovdje koristi u suštini kao hash funkcije da se jedinstveno odredi koji se tip (de)serijalizuje. Ovdje smo suočeni s problemom sudara - i ne, vjerovatnoća nije jedan prema 232, već mnogo veća. Ko se sjetio da je CRC32 dizajniran za otkrivanje (i ispravljanje) grešaka u komunikacijskom kanalu, te shodno tome poboljšava ova svojstva na štetu drugih? Na primjer, nije ga briga za preuređivanje bajtova: ako izračunate CRC32 iz dva reda, u drugom zamijenite prva 4 bajta sa sljedeća 4 bajta - bit će isto. Kada su naš unos tekstualni nizovi iz latinične abecede (i malo interpunkcije), a ova imena nisu posebno nasumična, vjerovatnoća takvog preuređivanja uvelike raste.

Usput, ko je provjerio šta ima? stvarno CRC32? Jedan od ranih izvornih kodova (čak i prije Waltmana) imao je hash funkciju koja je množila svaki znak sa brojem 239, tako voljenu ovim ljudima, ha ha!

Konačno, u redu, shvatili smo da su konstruktori sa tipom polja Vector<int> и Vector<PolymorType> će imati drugačiji CRC32. Šta je sa on-line performansom? I sa teorijske tačke gledišta, da li ovo postaje dio tipa? Recimo da prosledimo niz od deset hiljada brojeva, dobro sa Vector<int> sve je jasno, dužina i još 40000 bajtova. Šta ako ovo Vector<Type2>, koji se sastoji od samo jednog polja int i samo je u tipu - da li treba da ponovimo 10000xabcdef0 34 puta i onda 4 bajta int, ili je jezik u stanju da ga NEZAVISNI za nas od konstruktora fixedVec i umjesto 80000 bajtova, prenijeti opet samo 40000?

Ovo uopće nije prazno teoretsko pitanje - zamislite da dobijete listu korisnika grupe, od kojih svaki ima ID, ime, prezime - razlika u količini podataka koji se prenose putem mobilne veze može biti značajna. Upravo nam se reklamira efikasnost Telegram serijalizacije.

Pa ...

Vector, koji nikada nije objavljen

Ako pokušate da prođete kroz stranice opisa kombinatora i tako dalje, vidjet ćete da vektor (pa čak i matrica) formalno pokušava da se izbaci kroz torke od nekoliko listova. Ali na kraju zaborave, posljednji korak se preskače i jednostavno se daje definicija vektora, koji još nije vezan za tip. Sta je bilo? Na jezicima programiranje, posebno funkcionalnih, sasvim je tipično strukturu opisati rekurzivno - kompajler sa svojom lijenom evaluacijom sve će razumjeti i učiniti sam. Na jeziku serijalizacija podataka ono što je potrebno je EFIKASNOST: dovoljno je jednostavno opisati lista, tj. struktura dva elementa - prvi je element podataka, drugi je ista sama struktura ili prazan prostor za rep (pak (cons) u Lisp). Ali ovo će očito zahtijevati svake od njih element troši dodatna 4 bajta (CRC32 u slučaju u TL) da opiše svoj tip. Niz se takođe može lako opisati fiksna veličina, ali u slučaju niza unaprijed nepoznate dužine, prekidamo.

Stoga, pošto TL ne dozvoljava izlaz vektora, morao je biti dodat sa strane. Na kraju dokumentacija kaže:

Serijalizacija uvijek koristi isti konstruktor "vektor" (const 0x1cb5c415 = crc32("vector t:Type # [ t ] = Vector t") koji ne ovisi o specifičnoj vrijednosti varijable tipa t.

Vrijednost opcionalnog parametra t nije uključena u serijalizaciju jer je izvedena iz tipa rezultata (uvijek poznat prije deserializacije).

Pogledajte izbliza: vector {t:Type} # [ t ] = Vector t - ali Nigdje Ova definicija sama po sebi ne kaže da prvi broj mora biti jednak dužini vektora! I ne dolazi niotkuda. Ovo je datost koju treba imati na umu i implementirati svojim rukama. Na drugom mjestu, dokumentacija čak iskreno spominje da tip nije stvaran:

Polimorfni pseudotip Vector t je "tip" čija je vrijednost niz vrijednosti bilo kojeg tipa t, bilo u okvirima ili golim.

... ali se ne fokusira na to. Kada, umorni od gaženja kroz natezanje matematike (možda vam je čak poznato sa nekog univerzitetskog kursa), odlučite da odustanete i zapravo pogledate kako da s njom radite u praksi, u glavi vam ostaje utisak da je ovo ozbiljno Matematika u suštini, jasno je da su je izmislili Cool People (dva matematičara - ACM pobednik), a ne bilo ko. Cilj - pokazati se - je postignut.

Usput, o broju. Da vas podsjetimo na to # to je sinonim nat, prirodni broj:

Postoje izrazi tipa (type-expr) i numeričke izraze (nat-expr). Međutim, oni su definisani na isti način.

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

ali su u gramatici opisani na isti način, tj. Ova razlika se opet mora zapamtiti i implementirati ručno.

Pa, da, tipovi šablona (vector<int>, vector<User>) imaju zajednički identifikator (#1cb5c415), tj. ako znate da je poziv najavljen kao

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

onda više ne čekate samo vektor, već vektor korisnika. Preciznije, trebalo bi čekaj - u stvarnom kodu svaki element, ako nije goli tip, imaće konstruktor, a na dobar način u implementaciji bi bilo potrebno provjeriti - ali poslani smo tačno u svakom elementu ovog vektora taj tip? Šta ako je to neka vrsta PHP-a, u kojoj niz može sadržavati različite tipove u različitim elementima?

U ovom trenutku počinjete razmišljati - da li je takav TL potreban? Možda bi za kolica bilo moguće koristiti ljudski serijalizator, isti protobuf koji je tada već postojao? To je bila teorija, pogledajmo praksu.

Postojeće TL implementacije u kodu

TL je rođen u dubinama VKontaktea i prije poznatih događaja s prodajom Durovovog udjela i (sigurno), čak i prije početka razvoja Telegrama. I to u otvorenom kodu izvorni kod prve implementacije možete naći puno smiješnih štaka. I sam jezik je tamo implementiran potpunije nego što je sada u Telegramu. Na primjer, hešovi se uopće ne koriste u shemi (što znači ugrađeni pseudotip (poput vektora) sa devijantnim ponašanjem). Or

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

ali hajde da razmotrimo, radi kompletnosti, da pratimo, da tako kažemo, evoluciju Diva misli.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Ili ova prelijepa:

    static const char *reserved_words_polymorhic[] = {

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

      };

Ovaj fragment govori o šablonima kao što su:

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

Ovo je definicija tipa šablona hashmap kao vektor parova int - Type. U C++ bi to izgledalo otprilike ovako:

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

tako, alpha - ključna riječ! Ali samo u C++ možete pisati T, ali treba pisati alfa, beta... Ali ne više od 8 parametara, tu se fantazija završava. Čini se da su se nekada u Sankt Peterburgu vodili ovakvi dijalozi:

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

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

Ali radilo se o prvoj objavljenoj implementaciji TL-a “općenito”. Pređimo na razmatranje implementacija u samim Telegram klijentima.

Reč Vasiliju:

Vasilij, [09.10.18 17:07] Najviše od svega, guzica je vrela jer su napravili gomilu apstrakcija, a onda zabili šraf na njih, i pokrili generator koda štakama
Kao rezultat, prvo iz dock pilot.jpg
Zatim iz koda dzhekichan.webp

Naravno, od ljudi koji su upoznati sa algoritmima i matematikom, možemo očekivati ​​da su pročitali Aho, Ullmanna i da su upoznati sa alatima koji su postali de facto standard u industriji tokom decenija za pisanje njihovih DSL kompajlera, zar ne?..

By telegram-cli je Vitalij Valtman, kao što se može shvatiti iz pojave TLO formata izvan njegovih (cli) granica, član tima - sada je dodijeljena biblioteka za TL raščlanjivanje odvojeno, kakav je utisak o njoj TL parser? ..

16.12 04:18 Vasilij: Mislim da neko nije savladao lex+yacc
16.12 04:18 Vasilij: Ne mogu drugačije da objasnim
16.12 04:18 Vasilij: pa, ili su plaćeni za broj linija u VK
16.12 04:19 Vasilij: 3k+ redova itd.<censored> umjesto parsera

Možda izuzetak? Da vidimo kako radi Ovo je ZVANIČNI klijent - 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+ redova u Pythonu, par regularnih izraza + specijalni slučajevi poput vektora, koji je, naravno, deklariran u shemi kako bi trebao biti prema TL sintaksi, ali su se oslanjali na ovu sintaksu da ga raščlane... Postavlja se pitanje zašto je sve to bilo čudo?иViše je slojevit ako ga ionako niko neće raščlaniti prema dokumentaciji?!

Usput... Sjećate se da smo razgovarali o CRC32 provjeri? Dakle, u generatoru kodova Telegram Desktop postoji lista izuzetaka za one tipove u kojima je izračunat CRC32 ne odgovara sa onim prikazanim na dijagramu!

Vasilij, [18.12/22 49:XNUMX] i ovdje bih razmislio da li je potreban takav TL
ako bih se htio petljati s alternativnim implementacijama, počeo bih umetati prijelome reda, polovina parsera će se prekinuti na višerednim definicijama
tdesktop, međutim, takođe

Zapamtite poentu o jednolineru, na to ćemo se vratiti malo kasnije.

Dobro, telegram-cli je nezvaničan, Telegram Desktop je zvaničan, ali šta je sa ostalima? Ko zna?.. U kodu Android klijenta uopće nije bilo raščlanjivanja šeme (što postavlja pitanja o otvorenom kodu, ali ovo je za drugi dio), ali je bilo još nekoliko smiješnih dijelova koda, ali više o njima u pododjeljak u nastavku.

Koja još pitanja postavlja serijalizacija u praksi? Na primjer, radili su puno stvari, naravno, sa bitnim poljima i uslovnim poljima:

Vasilij: flags.0? true
znači da je polje prisutno i da je jednako istinito ako je zastavica postavljena

Vasilij: flags.1? int
znači da je polje prisutno i da ga treba deserijalizirati

Vasilij: Guzice, ne brini šta radiš!
Vasilij: Negdje u dokumentu se spominje da je istina goli tip nulte dužine, ali je nemoguće sastaviti bilo šta iz njihovog dokumenta
Vasily: Ni u open source implementacijama to nije slučaj, ali postoji gomila štaka i oslonaca

Šta je sa Telethonom? Gledajući unaprijed na temu MTProto, primjer - u dokumentaciji postoje takvi komadi, ali znak % opisuje se samo kao “odgovara datom golom tipu”, tj. u primjerima ispod postoji ili greška ili nešto nedokumentirano:

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

msg_container#73f1f8dc messages:vector message = MessageContainer;

u drugačijem:

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

A to su dvije velike razlike, u stvarnom životu dolazi nekakav goli vektor

Nisam vidio golu definiciju vektora i nisam je naišao

Analiza se piše rukom u teletonu

U njegovom dijagramu definicija je komentarisana msg_container

Opet, ostaje pitanje oko %. Nije opisano.

Vadim Gončarov, [22.06.18 19:22] i na tdesktop-u?

Vasily, [22.06.18 19:23] Ali njihov TL parser na regularnim motorima najvjerovatnije neće ni ovo pojesti

// parsed manually

TL je prekrasna apstrakcija, niko je ne implementira u potpunosti

A % nije u njihovoj verziji sheme

Ali ovdje je dokumentacija kontradiktorna, tako da

Nađeno je u gramatici, mogli su jednostavno zaboraviti da opišu semantiku

Videli ste dokument na TL-u, ne možete da shvatite bez pola litre

„Pa, ​​recimo“, reći će drugi čitalac, „da kritikujete nešto, pa mi pokažite kako to treba da se radi“.

Vasilij odgovara: „Što se tiče parsera, volim stvari poput

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

nekako mi se više sviđa

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

ili

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

ovo je CIJELI lekser:

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

one. jednostavnije je blago rečeno.”

Općenito, kao rezultat toga, parser i generator koda za stvarno korišteni podskup TL-a uklapaju se u otprilike 100 gramatičkih linija i ~300 linija generatora (računajući sve printgenerisani kod), uključujući tipove informacija za introspekciju u svakom razredu. Svaki polimorfni tip se pretvara u praznu apstraktnu osnovnu klasu, a konstruktori nasljeđuju od nje i imaju metode za serijalizaciju i deserializaciju.

Nedostatak tipova u jeziku tipova

Jako kucanje je dobra stvar, zar ne? Ne, ovo nije holivar (iako preferiram dinamičke jezike), već postulat u okviru TL-a. Na osnovu toga, jezik bi nam trebao pružiti sve vrste provjera. Pa dobro, možda ne on sam, nego implementacija, ali bi ih barem trebao opisati. I kakve prilike želimo?

Prije svega, ograničenja. Ovdje vidimo u dokumentaciji za upload fajlova:

Binarni sadržaj datoteke se zatim dijeli na dijelove. Svi dijelovi moraju biti iste veličine ( part_size ) i moraju biti ispunjeni sljedeći uslovi:

  • part_size % 1024 = 0 (djeljivo sa 1KB)
  • 524288 % part_size = 0 (512KB mora biti jednako djeljivo sa part_size)

Posljednji dio ne mora zadovoljiti ove uvjete, pod uvjetom da je njegova veličina manja od part_size.

Svaki deo treba da ima redni broj, file_part, sa vrijednošću u rasponu od 0 do 2,999.

Nakon što je fajl particioniran, potrebno je da izaberete metod za njegovo spremanje na server. Koristi upload.saveBigFilePart u slučaju da je puna veličina datoteke veća od 10 MB i upload.saveFilePart za manje fajlove.
[…] može se vratiti jedna od sljedećih grešaka u unosu podataka:

  • FILE_PARTS_INVALID — Nevažeći broj dijelova. Vrijednost nije između 1..3000

Ima li nešto od ovoga na dijagramu? Da li se to nekako može izraziti korištenjem TL-a? br. Ali izvinite, čak je i djedov Turbo Pascal mogao opisati navedene tipove rasponi. I znao je još jednu stvar, sada poznatiju kao enum - tip koji se sastoji od nabrajanja fiksnog (malog) broja vrijednosti. U jezicima kao što je C - numerički, imajte na umu da smo do sada govorili samo o tipovima brojevi. Ali postoje i nizovi, stringovi... na primjer, bilo bi lijepo opisati da ovaj niz može sadržavati samo broj telefona, zar ne?

Ništa od ovoga nema u TL-u. Ali postoji, na primjer, u JSON shemi. A ako bi neko drugi mogao raspravljati o djeljivosti 512 KB, da to još treba provjeriti u kodu, onda se pobrinite da klijent jednostavno nije mogao poslati broj izvan dometa 1..3000 (a odgovarajuća greška nije mogla nastati) bilo bi moguće, zar ne?..

Usput, o greškama i povratnim vrijednostima. Čak i oni koji su radili sa TL zamagljuju oči - to nam nije odmah sinulo svaki funkcija u TL-u zapravo može vratiti ne samo opisani tip povrata, već i grešku. Ali to se ni na koji način ne može zaključiti koristeći sam TL. Naravno, to je već jasno i nema potrebe za ničim u praksi (iako se u stvari RPC može raditi na različite načine, na to ćemo se vratiti kasnije) - ali šta je sa čistoćom koncepata matematike apstraktnih tipova sa nebeskog sveta?.. Pokupio sam tegljač - pa ga uskladi.

I na kraju, šta je sa čitljivošću? Pa, tu bih, općenito, volio opis imate ga tačno u šemi (u JSON šemi, opet, jeste), ali ako ste već napregnuti s tim, šta je s praktičnom stranom - barem trivijalno gledati razlike tokom ažuriranja? Uvjerite se sami na stvarni primjeri:

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

ili

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

Zavisi od svakoga, ali GitHub, na primjer, odbija da istakne promjene unutar tako dugih redova. Igra "pronađi 10 razlika", a ono što mozak odmah vidi je da su počeci i krajevi u oba primjera isti, treba zamorno čitati negdje u sredini... Po meni to nije samo u teoriji, ali čisto vizuelno prljavo i aljkavo.

Usput, o čistoći teorije. Zašto su nam potrebna bitna polja? Zar se ne čini da oni miris loše sa stanovišta teorije tipova? Objašnjenje se može vidjeti u ranijim verzijama dijagrama. U početku, da, tako je bilo, za svako kijanje se stvarao novi tip. Ovi rudimenti još uvijek postoje u ovom obliku, na primjer:

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;

Ali sada zamislite, ako imate 5 opcionih polja u svojoj strukturi, onda će vam trebati 32 tipa za sve moguće opcije. Kombinatorna eksplozija. Tako se kristalna čistoća TL teorije još jednom razbila o guzicu od livenog gvožđa surove realnosti serijalizacije.

Osim toga, na nekim mjestima ovi momci i sami krše vlastitu tipologiju. Na primjer, u MTProto-u (sljedeće poglavlje) odgovor se može komprimirati pomoću Gzipa, sve je u redu - osim što su slojevi i kolo narušeni. Još jednom, nije požnjeven sam RpcResult, već njegov sadržaj. Pa, zašto ovo?.. Morao sam seći u štaku da kompresija radi bilo gdje.

Ili drugi primjer, jednom smo otkrili grešku - poslana je InputPeerUser umjesto InputUser. Ili obrnuto. Ali uspjelo je! Odnosno, server nije mario za tip. Kako ovo može biti? Odgovor nam mogu dati fragmenti koda iz telegram-cli:

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

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

Drugim riječima, ovdje se vrši serijalizacija MANUALLY, nije generirani kod! Možda je server implementiran na sličan način?.. U principu, ovo će funkcionisati ako se uradi jednom, ali kako se to može podržati kasnije tokom ažuriranja? Da li je zbog toga izmišljena šema? I ovdje prelazimo na sljedeće pitanje.

Versioniranje. Slojevi

Zašto se šematske verzije nazivaju slojevima može se samo nagađati na osnovu istorije objavljenih šema. Očigledno, u početku su autori mislili da se osnovne stvari mogu raditi pomoću nepromijenjene šeme, a samo tamo gdje je to potrebno, za određene zahtjeve, naznačiti da se rade koristeći drugu verziju. U principu, čak i dobra ideja - a novo će biti, takoreći, "pomiješano", naslagano na staro. Ali da vidimo kako je to urađeno. Istina, nisam to mogao pogledati od samog početka - smiješno je, ali dijagram osnovnog sloja jednostavno ne postoji. Slojevi su počeli sa 2. Dokumentacija nam govori o posebnoj TL osobini:

Ako klijent podržava sloj 2, tada se mora koristiti sljedeći konstruktor:

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

U praksi, to znači da prije svakog API poziva, int sa vrijednošću 0x289dd1f6 mora se dodati ispred broja metode.

Zvuči normalno. Ali šta se dalje dogodilo? Onda se pojavio

invokeWithLayer3#b7475268 query:!X = X;

Šta je sledeće? Kao što možete pretpostaviti,

invokeWithLayer4#dea0d430 query:!X = X;

Smiješno? Ne, prerano je za smeh, razmislite o tome svaki zahtjev iz drugog sloja treba umotati u tako poseban tip - ako ih imate sve različite, kako ih drugačije možete razlikovati? A dodavanje samo 4 bajta ispred je prilično efikasan metod. dakle,

invokeWithLayer5#417a57ae query:!X = X;

Ali očito je da će to nakon nekog vremena postati neka vrsta vakhanalije. I došlo je rešenje:

Ažuriranje: Počevši od sloja 9, pomoćne metode invokeWithLayerN može se koristiti samo zajedno sa initConnection

Ura! Nakon 9 verzija, konačno smo došli do onoga što je rađeno u internet protokolima 80-ih godina - dogovaranje verzije jednom na početku konekcije!

Pa šta je sledeće?..

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

Ali sada se još možete smijati. Tek nakon još 9 slojeva konačno je dodat univerzalni konstruktor sa brojem verzije, koji treba pozvati samo jednom na početku veze, a značenje slojeva kao da je nestalo, sada je to samo uslovna verzija, kao svuda drugde. Problem riješen.

Upravo?..

Vasilij, [16.07.18 14:01] Čak sam u petak mislio:
Teleserver šalje događaje bez zahtjeva. Zahtjevi moraju biti umotani u InvokeWithLayer. Server ne omota ažuriranja; ne postoji struktura za omotavanje odgovora i ažuriranja.

One. klijent ne može odrediti sloj u kojem želi ažuriranja

Vadim Gončarov, [16.07.18 14:02] nije li InvokeWithLayer u principu štaka?

Vasilij, [16.07.18 14:02] Ovo je jedini način

Vadim Gončarov, [16.07.18 14:02] što bi u suštini trebalo da znači dogovor o sloju na početku sesije

Usput, iz toga proizilazi da klijent nije predviđen

Ažuriranja, tj. tip Updates u šemi, ovo je ono što server šalje klijentu ne kao odgovor na API zahtjev, već nezavisno kada se dogodi neki događaj. Ovo je složena tema o kojoj će biti riječi u drugom postu, ali za sada je važno znati da server sprema ažuriranja čak i kada je klijent van mreže.

Dakle, ako odbijete zamotati svake od njih paket da naznači njegovu verziju, to logično vodi do sljedećih mogućih problema:

  • server šalje ažuriranja klijentu čak i prije nego što je klijent obavijestio koju verziju podržava
  • šta da radim nakon nadogradnje klijenta?
  • ko garancijeda se mišljenje servera o broju sloja neće promijeniti tokom procesa?

Mislite li da je ovo čisto teoretska spekulacija, a u praksi se to ne može dogoditi, jer je server ispravno napisan (barem je dobro testiran)? Ha! Kako god da je!

Upravo na ovo smo naišli u avgustu. 14. avgusta stigle su poruke da se nešto ažurira na Telegram serverima... a zatim i u logovima:

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 zatim nekoliko megabajta tragova steka (pa, istovremeno je popravljeno evidentiranje). Uostalom, ako nešto nije prepoznato u vašem TL-u, to je binarno po potpisu, dalje niz liniju SVE dekodiranje će postati nemoguće. Šta učiniti u takvoj situaciji?

Pa, prva stvar koja nekome padne na pamet je da prekine vezu i pokuša ponovo. Nije pomoglo. Guglamo CRC32 - ispostavilo se da su to objekti iz šeme 73, iako smo radili na 82. Pažljivo gledamo dnevnike - postoje identifikatori iz dvije različite šeme!

Možda je problem isključivo u našem nezvaničnom klijentu? Ne, pokrećemo Telegram Desktop 1.2.17 (verzija se isporučuje u brojnim distribucijama Linuxa), on piše u dnevnik izuzetaka: MTP Neočekivani ID tipa #b5223b0f pročitan u MTPMessageMedia…

Kritika protokola i organizacionih pristupa Telegrama. Dio 1, tehnički: iskustvo pisanja klijenta od nule - TL, MT

Gugl je pokazao da se sličan problem već desio jednom od nezvaničnih klijenata, ali su tada brojevi verzija i, shodno tome, pretpostavke bili drugačiji...

Šta da radimo? Vasilij i ja smo se razišli: on je pokušao ažurirati kolo na 91, ja sam odlučio pričekati nekoliko dana i isprobati 73. Obje metode su uspjele, ali pošto su empirijske, nema razumijevanja koliko verzija gore ili dolje vam treba da skočite ili koliko dugo trebate čekati.

Kasnije sam uspeo da reproduciram situaciju: pokrećemo klijenta, isključujemo ga, ponovo kompajliramo kolo na drugi sloj, restartujemo, ponovo otkrivamo problem, vraćamo se na prethodni - ups, nema količine prebacivanja kola i klijent se ponovo pokreće za nekoliko minuta će pomoći. Dobit ćete mješavinu struktura podataka iz različitih slojeva.

Objašnjenje? Kao što možete pretpostaviti iz različitih indirektnih simptoma, server se sastoji od mnogo procesa različitih tipova na različitim mašinama. Najvjerovatnije je server koji je odgovoran za “baferovanje” stavio u red ono što su mu dali nadređeni, a oni su to dali u šemi koja je bila na snazi ​​u vrijeme generiranja. I dok ovaj red ne bude „truo“, ništa se nije moglo učiniti.

Možda... ali ovo je strašna štaka?!.. Ne, prije nego što razmišljamo o ludim idejama, pogledajmo kodeks službenih klijenata. U Android verziji ne nalazimo nikakav TL parser, ali nalazimo veliki fajl (GitHub odbija da ga doradi) sa (de)serijalizacijom. Evo isječaka koda:

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;

ili

    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... izgleda divlje. Ali, vjerovatno, ovo je generirani kod, onda u redu?.. Ali sigurno podržava sve verzije! Istina, nije jasno zašto je sve pomiješano, tajni razgovori i svašta _old7 nekako ne liče na mašinsku generaciju... Ipak, najviše od svega me je oduševilo

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Ljudi, zar ne možete ni da odlučite šta je unutar jednog sloja?! Pa dobro, recimo „dva“ su puštena sa greškom, pa, dešava se, ali TRI?.. Odmah, opet isti rake? Kakva je ovo pornografija, izvini?..

U izvornom kodu Telegram Desktopa, inače, događa se slična stvar - ako je tako, nekoliko urezivanja za redom na shemu ne mijenjaju njen broj sloja, već nešto popravljaju. U uslovima kada ne postoji službeni izvor podataka za šemu, odakle se isti mogu dobiti, osim izvornog koda zvaničnog klijenta? A ako uzmete odatle, ne možete biti sigurni da je shema potpuno ispravna dok ne testirate sve metode.

Kako se ovo uopće može testirati? Nadam se da će ljubitelji jediničnih, funkcionalnih i drugih testova podijeliti u komentarima.

U redu, pogledajmo još jedan dio koda:

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;

Ovaj komentar "ručno kreiran" sugerira da je samo dio ove datoteke napisan ručno (možete li zamisliti cijelu noćnu moru održavanja?), a ostatak je generiran strojno. Međutim, onda se postavlja drugo pitanje – da li su izvori dostupni ne u potpunosti (a la GPL blobs u Linux kernelu), ali ovo je već tema za drugi dio.

Ali dosta. Pređimo na protokol na kojem se izvodi sva ova serijalizacija.

MT Proto

Pa, hajde da otvorimo opći opis и detaljan opis protokola i prva stvar na koju naiđemo je terminologija. I sa obiljem svega. Općenito, čini se da je ovo vlasnička karakteristika Telegrama - drugačije naziva stvari na različitim mjestima, ili različite stvari jednom riječju, ili obrnuto (na primjer, u API-ju visokog nivoa, ako vidite paket naljepnica, to nije šta ste mislili).

Na primjer, "poruka" i "sesija" ovdje znače nešto drugačije nego u uobičajenom interfejsu klijenta Telegrama. Pa, sve je jasno sa porukom, može se tumačiti u OOP terminima, ili jednostavno nazvati reč "paket" - ovo je nizak, transportni nivo, nema istih poruka kao u interfejsu, ima mnogo servisnih poruka . Ali sesija... ali prvo prvo.

transportni sloj

Prva stvar je transport. Reći će nam o 5 opcija:

  • TCP
  • Websocket
  • Websocket preko HTTPS-a
  • HTTP
  • HTTPS

Vasily, [15.06.18 15:04] Postoji i UDP transport, ali nije dokumentovan

I TCP u tri varijante

Prvi je sličan UDP-u preko TCP-a, svaki paket uključuje redni broj i crc
Zašto je čitanje dokumenata na kolicima tako bolno?

Pa, evo ga sada TCP već u 4 varijante:

  • Skraćen
  • srednji
  • Podstavljeni srednji
  • Full

Pa, ok, Padded intermediate za MTProxy, ovo je kasnije dodato zbog dobro poznatih događaja. Ali zašto još dvije verzije (ukupno tri) kada se možete snaći s jednom? Sva četiri se suštinski razlikuju samo po tome kako postaviti dužinu i nosivost glavnog MTProto-a, o čemu će se dalje govoriti:

  • u skraćenom obliku je 1 ili 4 bajta, ali ne 0xef, nego tijelo
  • u Intermediate ovo je 4 bajta dužine i polje, a prvi put klijent mora poslati 0xeeeeeeee da označi da je srednji
  • u punoj najzavisniji, sa stajališta networkera: dužina, redni broj, a NE ONAJ koji je uglavnom MTProto, tijelo, CRC32. Da, sve ovo je na vrhu TCP-a. Što nam pruža pouzdan transport u obliku sekvencijalnog toka bajtova; nisu potrebne sekvence, posebno kontrolni sumi. Dobro, sad će mi neko prigovoriti da TCP ima 16-bitnu kontrolnu sumu, pa dolazi do oštećenja podataka. Odlično, ali mi zapravo imamo kriptografski protokol sa hashovima dužim od 16 bajtova, sve ove greške - pa čak i više - će biti uhvaćene SHA neusklađenošću na višem nivou. Povrh ovoga nema smisla u CRC32.

Uporedimo Abridged, u kojem je moguć jedan bajt dužine, sa Intermediate, što opravdava “U slučaju da je potrebno 4-bajtno poravnanje podataka”, što je prilično besmislica. Šta, vjeruje se da su programeri Telegrama toliko nesposobni da ne mogu čitati podatke iz socketa u usklađeni bafer? Ovo i dalje morate učiniti, jer vam čitanje može vratiti bilo koji broj bajtova (a postoje i proxy serveri, na primjer...). Ili s druge strane, zašto blokirati skraćeno ako ćemo i dalje imati pozamašan pad na vrhu od 16 bajtova - uštedjeti 3 bajta ponekad ?

Stiče se utisak da Nikolaj Durov zaista voli da iznova izmišlja točkove, uključujući i mrežne protokole, bez ikakve stvarne praktične potrebe.

Ostale mogućnosti transporta, uklj. Web i MTProxy, nećemo sada razmatrati, možda u nekom drugom postu, ako postoji zahtjev. O ovom istom MTProxy-u, samo se sada prisjetimo da su nedugo nakon njegovog objavljivanja 2018. godine, provajderi brzo naučili da ga blokiraju, namijenjen blokiranje zaobilazniceod veličina paketa! I takođe činjenica da je MTProxy server napisan (opet od strane Waltmana) u C-u bio previše vezan za Linux specifičnosti, iako to uopće nije bilo potrebno (Phil Kulin će potvrditi), te da bi sličan server bilo u Go ili Node.js stane u manje od stotinu redova.

Ali zaključke o tehničkoj pismenosti ovih ljudi izvući ćemo na kraju odjeljka, nakon razmatranja drugih pitanja. Za sada, pređimo na OSI sloj 5, sesiju - na koju su postavili MTProto sesiju.

Ključevi, poruke, sesije, Diffie-Hellman

Tamo su ga postavili ne sasvim ispravno... Sesija nije ista sesija koja je vidljiva u interfejsu pod Aktivne sesije. Ali po redu.

Kritika protokola i organizacionih pristupa Telegrama. Dio 1, tehnički: iskustvo pisanja klijenta od nule - TL, MT

Tako smo od transportnog sloja primili niz bajtova poznate dužine. Ovo je ili šifrovana poruka ili otvoreni tekst - ako smo još u fazi dogovora o ključu i zapravo to radimo. O kojem od gomile koncepata zvanih „ključ“ govorimo? Hajde da razjasnimo ovo pitanje za sam Telegram tim (izvinjavam se što sam preveo svoju dokumentaciju sa engleskog sa umornim mozgom u 4 ujutro, bilo je lakše ostaviti neke fraze kakve jesu):

Postoje dva entiteta pod nazivom sjednica - jedan u korisničkom sučelju službenih klijenata pod “trenutnim sesijama”, gdje svaka sesija odgovara cijelom uređaju/OS-u.
Drugi je MTProto sesija, koji u sebi ima redni broj poruke (u smislu niskog nivoa) i koji može trajati između različitih TCP veza. Nekoliko MTProto sesija se može instalirati istovremeno, na primjer, da bi se ubrzalo preuzimanje datoteka.

Između ovo dvoje sesije postoji koncept autorizacija. U degeneriranom slučaju, možemo to reći UI sesija je isto kao autorizacija, ali nažalost, sve je komplikovano. pogledajmo:

  • Korisnik na novom uređaju prvo generira auth_key i vezuje ga za račun, na primjer putem SMS-a - eto zašto autorizacija
  • Desilo se u prvom MTProto sesija, koji ima session_id u sebi.
  • U ovom koraku, kombinacija autorizacija и session_id mogao bi se nazvati primer - ova riječ se pojavljuje u dokumentaciji i kodu nekih klijenata
  • Tada se klijent može otvoriti nekoliko MTProto sesije pod istim auth_key - u isti DC.
  • Zatim, jednog dana klijent će morati da zatraži datoteku od drugi DC - i za ovaj DC će se generirati novi auth_key !
  • Obavijestiti sistem da se ne registruje novi korisnik, već isti autorizacija (UI sesija), klijent koristi API pozive auth.exportAuthorization u domu DC auth.importAuthorization u novom DC.
  • Sve je isto, nekoliko može biti otvoreno MTProto sesije (svako sa svojim session_id) ovom novom DC, pod njegov auth_key.
  • Konačno, klijent može htjeti savršenu prosljeđu tajnost. Svaki auth_key bio trajan ključ - po DC - i klijent može pozvati auth.bindTempAuthKey za upotrebu privremeni auth_key - i opet, samo jedan temp_auth_key po DC, zajednički za sve MTProto sesije ovom DC.

Primjetite to sol (i buduće soli) je također jedan na auth_key one. podijeljeno između svih MTProto sesije u isti DC.

Šta znači "između različitih TCP veza"? Dakle, ovo znači nešto kao autorizacijski kolačić na web stranici - opstaje (preživljava) mnoge TCP konekcije na dati server, ali jednog dana se pokvari. Samo za razliku od HTTP-a, u MTProtou se poruke unutar sesije sekvencijalno numeriraju i potvrđuju; ako su ušle u tunel, veza je prekinuta - nakon uspostavljanja nove veze, server će ljubazno poslati sve u ovoj sesiji što nije isporučio u prethodnoj TCP veza.

Međutim, gore navedene informacije su sažete nakon višemjesečne istrage. U međuvremenu, implementiramo li našeg klijenta od nule? - vratimo se na početak.

Pa hajde da generišemo auth_key na Diffie-Hellman verzije iz Telegrama. Pokušajmo razumjeti dokumentaciju...

Vasilij, [19.06.18 20:05] data_with_hash := SHA1(podaci) + podaci + (bilo koji nasumični bajt); tako da je dužina jednaka 255 bajtova;
encrypted_data := RSA(data_with_hash, server_public_key); broj od 255 bajta (big endian) se podiže na potrebnu snagu preko potrebnog modula, a rezultat se pohranjuje kao broj od 256 bajta.

Imaju malo droge DH

Ne izgleda kao DH zdrave osobe
U dx-u ne postoje dva javna ključa

Eto, na kraju je ovo riješeno, ali je ostao talog - dokaz o radu je od strane klijenta da je uspio faktorirati broj. Vrsta zaštite od DoS napada. RSA ključ se koristi samo jednom u jednom smjeru, u suštini za šifriranje new_nonce. Ali dok će ova naizgled jednostavna operacija uspjeti, s čime ćete se morati suočiti?

Vasilij, [20.06.18/00/26 XNUMX:XNUMX] Još nisam stigao do appid zahtjeva

Poslao sam ovaj zahtjev DH

A u transportnoj doku piše da može odgovoriti sa 4 bajta koda greške. To je sve

Pa, rekao mi je -404, pa šta?

Pa sam mu rekao: "Uhvati svoje sranje šifrovano serverskim ključem sa ovakvim otiskom prsta, želim DH", a ono je odgovorilo sa glupim 404

Šta mislite o ovom odgovoru servera? sta da radim? Nema se koga pitati (ali o tome u drugom dijelu).

Ovdje se sve kamate obavljaju na optuženičkoj klupi

Nemam šta drugo da radim, samo sam sanjao da pretvaram brojeve naprijed-natrag

Dva 32-bitna broja. Spakovao sam ih kao i sve ostale

Ali ne, ovo dvoje treba prvo dodati u red kao BE

Vadim Gončarov, [20.06.18 15:49] i zbog ovoga 404?

Vasilij, [20.06.18 15:49] DA!

Vadim Gončarov, [20.06.18 15:50] tako da ne razumem šta on "nije pronašao"

Vasilij, [20.06.18 15:50] otprilike

Nisam mogao naći takvu dekompoziciju na osnovne faktore%)

Nismo čak ni upravljali prijavljivanjem grešaka

Vasilij, [20.06.18 20:18] Oh, tu je i MD5. Već tri različita heša

Otisak ključa se izračunava na sljedeći način:

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

SHA1 i sha2

Pa hajde da kažemo auth_key primili smo 2048 bita u veličini koristeći Diffie-Hellman. Šta je sledeće? Zatim otkrivamo da se nižih 1024 bita ovog ključa ne koriste ni na koji način... ali razmislimo o ovome za sada. U ovom koraku imamo zajedničku tajnu sa serverom. Uspostavljen je analog TLS sesije, što je veoma skupa procedura. Ali server još uvijek ne zna ništa o tome ko smo! Ne još, zapravo. autorizacija. One. ako ste mislili u terminima "login-password", kao što ste nekada radili u ICQ-u, ili barem "login-key", kao u SSH-u (na primjer, na nekom gitlab/githubu). Dobili smo anonimnu. Šta ako nam server kaže “ove telefonske brojeve servisira drugi DC”? Ili čak "vaš broj telefona je zabranjen"? Najbolje što možemo učiniti je zadržati ključ u nadi da će biti koristan i da do tada neće pokvariti.

Inače, “primili smo” ga sa rezervom. Na primjer, vjerujemo li serveru? Šta ako je lažno? Kriptografske provjere bi bile potrebne:

Vasily, [21.06.18 17:53] Oni nude mobilnim klijentima da provjere 2kbitni broj za primarnost%)

Ali to uopšte nije jasno, nafeijoa

Vasilij, [21.06.18 18:02] U dokumentu ne piše šta treba učiniti ako se pokaže da nije jednostavno

Nije rečeno. Hajde da vidimo šta zvanični Android klijent radi u ovom slučaju? A to je to (i da, cijeli fajl je zanimljiv) - kako kažu, ostaviću samo ovo:

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

Ne, naravno da je još tu neke Postoje testovi za primarnost broja, ali ja lično više nemam dovoljno znanja iz matematike.

Ok, imamo glavni ključ. Za prijavu, tj. slati zahtjeve, potrebno je izvršiti daljnju enkripciju, koristeći AES.

Ključ poruke je definiran kao 128 srednjih bitova SHA256 tijela poruke (uključujući sesiju, ID poruke, itd.), uključujući bajtove dodavanja, uz 32 bajta preuzeta iz autorizacijskog ključa.

Vasilij, [22.06.18 14:08] Prosjek, kučko, bitovi

Primljeno auth_key. Sve. Mimo njih... nije jasno iz dokumenta. Slobodno proučite otvoreni izvorni kod.

Imajte na umu da MTProto 2.0 zahtijeva od 12 do 1024 bajtova dopuna, i dalje pod uslovom da rezultujuća dužina poruke bude deljiva sa 16 bajtova.

Dakle, koliko paddinga trebate dodati?

I da, postoji i 404 u slučaju greške

Ako je neko pažljivo proučio dijagram i tekst dokumentacije, primijetio je da tamo nema MAC-a. I taj AES se koristi u određenom IGE modu koji se ne koristi nigdje drugdje. Oni, naravno, pišu o tome u svojim FAQ... Evo, kao, sam ključ poruke je i SHA hash dešifriranih podataka, koji se koristi za provjeru integriteta - a u slučaju neusklađenosti i dokumentacije iz nekog razloga preporučuje da ih tiho ignorišemo (ali šta je sa bezbednošću, šta ako nas slome?).

Nisam kriptograf, možda u ovom slučaju nema ništa loše sa teorijske tačke gledišta. Ali mogu jasno navesti praktičan problem, koristeći Telegram Desktop kao primjer. Šifruje lokalnu keš memoriju (sve ove D877F783D5D3EF8C) na isti način kao i poruke u MTProtou (samo u ovom slučaju verzija 1.0), tj. prvo ključ poruke, zatim sami podaci (i negdje po strani glavni veliki auth_key 256 bajtova, bez kojih msg_key beskorisno). Dakle, problem postaje vidljiv na velikim datotekama. Naime, potrebno je čuvati dvije kopije podataka – šifrovanu i dešifrovanu. A ako postoje megabajti, ili streaming videa, na primjer?.. Klasične šeme sa MAC-om nakon šifriranog teksta omogućavaju vam da pročitate njegov stream, odmah ga prenosite. Ali sa MTProto ćete morati na početku šifrirajte ili dešifrirajte cijelu poruku, pa je tek onda prenesite na mrežu ili na disk. Stoga, u najnovijim verzijama Telegrama Desktop u kešu u user_data Koristi se i drugi format - sa AES-om u CTR modu.

Vasilij, [21.06.18 01:27] Oh, saznao sam šta je IGE: IGE je bio prvi pokušaj "provjere autentičnosti načina šifriranja", prvobitno za Kerberos. Bio je to neuspješan pokušaj (ne pruža zaštitu integriteta) i morao je biti uklonjen. To je bio početak 20-godišnje potrage za načinom šifriranja autentifikacije koji radi, a koji je nedavno kulminirao u modovima poput OCB i GCM.

A sada argumenti sa strane kolica:

Tim koji stoji iza Telegrama, predvođen Nikolajem Durovom, sastoji se od šest ACM šampiona, od kojih polovina doktori matematike. Trebalo im je oko dvije godine da uvedu trenutnu verziju MTProto-a.

To je smiješno. Dvije godine na nižem nivou

Ili možete samo uzeti tls

U redu, recimo da smo obavili šifriranje i druge nijanse. Da li je konačno moguće poslati zahtjeve serijalizirane u TL-u i deserijalizirati odgovore? Dakle, šta i kako poslati? Evo, recimo, metode initConnection, možda je to to?

Vasilij, [25.06.18 18:46] Inicijalizira vezu i čuva informacije na korisnikovom uređaju i aplikaciji.

Prihvata app_id, device_model, system_version, app_version i lang_code.

I neki upit

Dokumentacija kao i uvek. Slobodno proučite open source

Ako je sve bilo približno jasno sa invokeWithLayer, šta onda nije u redu? Ispada, recimo da imamo - klijent je već imao nešto da pita server o čemu - postoji zahtjev koji smo htjeli poslati:

Vasily, [25.06.18 19:13] Sudeći po kodu, prvi poziv je umotan u ovo sranje, a samo sranje je umotano u invokewithlayer

Zašto initConnection ne bi mogao biti poseban poziv, već mora biti omot? Da, kako se ispostavilo, to se mora učiniti svaki put na početku svake sesije, a ne jednom, kao kod glavnog ključa. Ali! Ne može ga pozvati neovlašteni korisnik! Sada smo došli do faze u kojoj je to primjenjivo Ovaj stranica dokumentacije - i to nam govori da...

Samo mali dio API metoda dostupan je neovlaštenim korisnicima:

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

Prvi od njih, auth.sendCode, a tu je i onaj najdraži prvi zahtjev u kojem šaljemo api_id i api_hash, a nakon toga dobijamo SMS sa kodom. A ako smo u pogrešnom DC-u (telefonske brojeve u ovoj zemlji, na primjer, opslužuje druga), onda ćemo dobiti grešku sa brojem željenog DC-a. Da saznate na koju IP adresu po DC broju trebate se povezati, pomozite nam help.getConfig. Nekada je bilo samo 5 prijava, ali nakon poznatih događaja 2018. broj se značajno povećao.

Sada se prisjetimo da smo do ove faze na serveru došli anonimno. Nije li preskupo samo dobiti IP adresu? Zašto ne uradite ovo i druge operacije u nešifrovanom delu MTProto-a? Čujem prigovor: “kako da budemo sigurni da neće RKN odgovoriti lažnim adresama?” Na to se sjećamo da su, općenito, službeni klijenti RSA ključevi su ugrađeni, tj. možeš li samo potpisati ove informacije. Zapravo, to se već radi za informacije o zaobilaženju blokiranja koje klijenti primaju preko drugih kanala (logično, to se ne može učiniti u samom MTProto-u; također morate znati gdje da se povežete).

UREDU. U ovoj fazi autorizacije klijenta, još nismo ovlašteni i nismo registrovali našu aplikaciju. Za sada samo želimo da vidimo šta server reaguje na metode dostupne neovlašćenom korisniku. I ovdje…

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;

U šemi prvo dolazi drugo

U tdesktop šemi treća vrijednost je

Da, od tada je, naravno, dokumentacija ažurirana. Iako bi uskoro moglo ponovo postati nebitno. Kako bi početnik programer trebao znati? Možda će vas obavijestiti ako registrujete svoju prijavu? Vasilij je to učinio, ali nažalost, nisu mu ništa poslali (opet ćemo o tome u drugom dijelu).

...Primijetili ste da smo već nekako prešli na API, tj. na sljedeći nivo, a propustili ste nešto u temi MTProto? nema iznenađenja:

Vasily, [28.06.18 02:04] Mm, preturaju po nekim algoritmima na e2e

Mtproto definira algoritme šifriranja i ključeve za oba domena, kao i malo strukture omotača

Ali oni stalno miješaju različite nivoe steka, tako da nije uvijek jasno gdje je mtproto završio i gdje je počeo sljedeći nivo

Kako se miješaju? Pa, evo istog privremenog ključa za PFS, na primjer (usput, Telegram Desktop to ne može). Izvršava se putem API zahtjeva auth.bindTempAuthKey, tj. sa najvišeg nivoa. Ali u isto vrijeme ometa enkripciju na nižem nivou - nakon toga, na primjer, morate to učiniti ponovo initConnection itd., ovo nije samo normalan zahtjev. Ono što je također posebno je da možete imati samo JEDAN privremeni ključ po DC-u, iako polje auth_key_id u svakoj poruci vam omogućava da promenite ključ barem u svakoj poruci, i da server ima pravo da "zaboravi" privremeni ključ u bilo kom trenutku - u dokumentaciji ne piše šta da se radi u ovom slučaju...pa zašto ne bi 'nemate nekoliko ključeva, kao kod seta budućih soli, i?..

Postoji nekoliko drugih stvari koje vredi napomenuti u vezi sa temom MTProto.

Poruke poruka, msg_id, msg_seqno, potvrde, pingovi u pogrešnom smjeru i druge idiosinkrazije

Zašto trebate znati o njima? Zato što „cure“ na viši nivo i morate ih biti svjesni kada radite s API-jem. Pretpostavimo da nas msg_key ne zanima; niži nivo je sve dešifrovao za nas. Ali unutar dešifriranih podataka imamo sljedeća polja (takođe i dužinu podataka, tako da znamo gdje je padding, ali to nije važno):

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

Podsjetimo, postoji samo jedna sol za cijeli DC. Zašto znati za nju? Ne samo zato što postoji zahtjev get_future_salts, koji vam govori koji će intervali biti važeći, ali i zato što ako je vaša sol “trula”, onda će se poruka (zahtjev) jednostavno izgubiti. Server će, naravno, prijaviti novu sol izdavanjem new_session_created - ali sa starim ćete ga morati nekako ponovo poslati, na primjer. I ovaj problem utiče na arhitekturu aplikacije.

Serveru je dozvoljeno da potpuno odustane od sesije i odgovori na ovaj način iz mnogo razloga. Zapravo, šta je MTProto sesija sa strane klijenta? Ovo su dva broja session_id и seq_no poruke u okviru ove sesije. Pa, i osnovna TCP veza, naravno. Recimo da naš klijent još uvijek ne zna raditi mnoge stvari, isključio se i ponovo spojio. Ako se to dogodilo brzo - stara sesija se nastavila u novoj TCP vezi, povećajte seq_no dalje. Ako potraje duže, server bi mogao da ga izbriše, jer je sa njegove strane i red, kako smo saznali.

Šta bi trebalo da bude seq_no? Oh, to je zeznuto pitanje. Pokušajte iskreno shvatiti na šta se mislilo:

Poruka u vezi sa sadržajem

Poruka koja zahtijeva eksplicitnu potvrdu. To uključuje sve korisničke i mnoge servisne poruke, gotovo sve sa izuzetkom kontejnera i potvrda.

Redni broj poruke (msg_seqno)

32-bitni broj jednak dvostrukom broju poruka koje se odnose na sadržaj (onih koje zahtijevaju potvrdu, a posebno onih koje nisu kontejneri) koje je stvorio pošiljatelj prije ove poruke i naknadno uvećan za jedan ako je trenutna poruka poruka vezana za sadržaj. Kontejner se uvijek generiše nakon cijelog sadržaja; stoga je njegov redni broj veći ili jednak rednim brojevima poruka koje se nalaze u njemu.

Kakav je ovo cirkus sa prirastom za 1, pa još za 2?.. Pretpostavljam da su u početku značili "najmanji značajni bit za ACK, ostalo je broj", ali rezultat nije sasvim isti - posebno, izlazi, može se poslati nekoliko potvrde koje imaju iste seq_no! Kako? Pa, na primjer, server nam nešto pošalje, pošalje, a mi sami šutimo, samo odgovaramo servisnim porukama koje potvrđuju prijem njegovih poruka. U ovom slučaju, naše odlazne potvrde će imati isti odlazni broj. Ako ste upoznati sa TCP-om i mislite da ovo zvuči nekako divlje, ali ne izgleda baš divlje, jer u TCP-u seq_no se ne mijenja, ali potvrda ide na seq_no s druge strane, požuriću da vas uznemirim. Potvrde su date u MTProto NE na seq_no, kao u TCP-u, ali po msg_id !

Šta je ovo msg_id, najvažnije od ovih polja? Jedinstveni identifikator poruke, kao što ime govori. Definisan je kao 64-bitni broj, čiji najniži bitovi opet imaju magiju “server-ne-server”, a ostatak je Unix vremenska oznaka, uključujući razlomak, pomaknut za 32 bita ulijevo. One. vremenska oznaka sama po sebi (a poruke sa vremenima koja se previše razlikuju će biti odbijene od strane servera). Iz ovoga proizlazi da je to općenito identifikator koji je globalan za klijenta. S obzirom na to - da se podsetimo session_id - garantovano nam je: Ni pod kojim okolnostima se poruka namijenjena jednoj sesiji ne može poslati u drugu sesiju. Odnosno, ispostavilo se da već postoji tri nivo - sesija, broj sesije, id poruke. Zašto tolika prekompliciranje, ova misterija je veoma velika.

Tako msg_id potrebno za...

RPC: zahtjevi, odgovori, greške. Potvrde.

Kao što ste možda primijetili, nigdje u dijagramu ne postoji poseban tip ili funkcija "napravi RPC zahtjev", iako postoje odgovori. Uostalom, imamo poruke vezane za sadržaj! To je, bilo koji poruka bi mogla biti zahtjev! Ili ne biti. Nakon svega, svake od njih je msg_id. Ali ima odgovora:

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

Ovdje je naznačeno na koju poruku je ovo odgovor. Stoga ćete na najvišem nivou API-ja morati zapamtiti koji je bio broj vašeg zahtjeva - mislim da nema potrebe objašnjavati da je rad asinhroni, te da može biti više zahtjeva u toku istovremeno, na koje se odgovori mogu vratiti bilo kojim redoslijedom? U principu, iz ovoga i poruka o greškama kao da nema radnika, može se pratiti arhitektura koja stoji iza ovoga: server koji održava TCP vezu s vama je front-end balanser, prosljeđuje zahtjeve backendima i prikuplja ih nazad putem message_id. Čini se da je ovdje sve jasno, logično i dobro.

Da?.. A ako razmislite o tome? Uostalom, i sam RPC odgovor takođe ima polje msg_id! Da li trebamo vikati na server “ne odgovaraš na moj odgovor!”? I da, šta je bilo sa potvrdama? O stranici poruke o porukama govori nam šta je

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

i to mora da uradi svaka strana. Ali ne uvek! Ako ste dobili RpcResult, on sam po sebi služi kao potvrda. Odnosno, server može odgovoriti na vaš zahtjev sa MsgsAck - kao, "Primio sam." RpcResult može odmah odgovoriti. Moglo bi biti oboje.

I da, još uvijek morate odgovoriti na odgovor! Potvrda. U suprotnom, server će ga smatrati nedostupnim i ponovo vam ga poslati. Čak i nakon ponovnog povezivanja. Ali ovdje se, naravno, postavlja pitanje tajm-auta. Pogledajmo ih malo kasnije.

U međuvremenu, pogledajmo moguće greške u izvršavanju upita.

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

Ma, neko će uskliknuti, evo humanijeg formata - postoji red! Uzmi si vremena. Evo spisak grešaka, ali naravno ne kompletan. Iz njega saznajemo da je kod nešto kao HTTP greške (pa, naravno, semantika odgovora se ne poštuje, na nekim mjestima su nasumično raspoređeni među kodovima), a linija izgleda kao VELIKA_SLOVA_AND_NUMBERS. Na primjer, PHONE_NUMBER_OCCUPIED ili FILE_PART_H_MISSING. Pa, to jest, i dalje će vam trebati ova linija analizirati. Na primjer FLOOD_WAIT_3600 će značiti da morate čekati sat vremena, i PHONE_MIGRATE_5, da telefonski broj sa ovim prefiksom mora biti registrovan u 5. DC. Imamo jezik pisanja, zar ne? Ne treba nam argument iz stringa, dovoljni su i obični, u redu.

Opet, ovo nije na stranici servisnih poruka, ali, kao što je već uobičajeno kod ovog projekta, informacije se mogu pronaći na drugoj stranici dokumentacije. Or baci sumnju. Prvo, pogledajte, kucanje/kršenje sloja - RpcError mogu se ugnijezditi RpcResult. Zašto ne napolju? Šta nismo uzeli u obzir?.. Prema tome, gde je to garancija RpcError se NE mogu ugraditi RpcResult, ali biti direktno ili ugniježđen u drugom tipu?.. A ako ne može, zašto nije na najvišem nivou, tj. nedostaje req_msg_id ? ..

Ali nastavimo o servisnim porukama. Klijent može pomisliti da server dugo razmišlja i uputiti ovaj divan zahtjev:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Postoje tri moguća odgovora na ovo pitanje, koji se opet ukrštaju sa mehanizmom potvrde; pokušaj da se shvati šta bi oni trebali biti (i koja je opšta lista tipova za koje nije potrebna potvrda) ostavlja se čitaocu kao domaći zadatak (napomena: informacije u izvorni kod za Telegram Desktop nije potpun).

Ovisnost o drogama: statusi poruka

Općenito, mnoga mjesta u TL, MTProto i Telegram općenito ostavljaju osjećaj tvrdoglavosti, ali iz pristojnosti, takta i drugih meke vještine O tome smo pristojno ćutali, cenzurisali opscenosti u dijalozima. Međutim, ovo mjestoОveći dio stranice je o poruke o porukama To je šokantno čak i za mene, koji već dugo radim sa mrežnim protokolima i viđao sam bicikle različitog stepena iskrivljenosti.

Počinje bezazleno, sa potvrdama. Zatim nam govore o tome

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;

Pa, svako ko počne da radi sa MTProto-om moraće da se nosi sa njima; u ciklusu „ispravljeno – ponovo kompajlirano – pokrenuto” uobičajena je stvar dobijanje brojčanih grešaka ili soli koja je uspela da se pokvari tokom uređivanja. Međutim, ovdje postoje dvije tačke:

  1. To znači da je originalna poruka izgubljena. Moramo napraviti neke redove, to ćemo pogledati kasnije.
  2. Koji su to čudni brojevi grešaka? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64... gdje su ostali brojevi, Tommy?

U dokumentaciji se navodi:

Namjera je da se vrijednosti error_code grupiraju (error_code >> 4): na primjer, kodovi 0x40 — 0x4f odgovaraju greškama u dekompoziciji kontejnera.

ali, prvo, pomak u drugom smjeru, i drugo, nije važno, gdje su ostali kodovi? U autorovoj glavi?.. Ipak, to su sitnice.

Ovisnost počinje u porukama o statusima poruka i kopijama poruka:

  • Zahtjev za informacije o statusu poruke
    Ako bilo koja strana nije primila informaciju o statusu svojih odlaznih poruka neko vrijeme, može je eksplicitno zatražiti od druge strane:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Informativna poruka u vezi statusa poruka
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Evo, info je niz koji sadrži tačno jedan bajt statusa poruke za svaku poruku sa dolazne liste msg_ids:

    • 1 = ništa se ne zna o poruci (msg_id je prenizak, druga strana ga je možda zaboravila)
    • 2 = poruka nije primljena (msg_id spada u raspon pohranjenih identifikatora; međutim, druga strana sigurno nije primila takvu poruku)
    • 3 = poruka nije primljena (msg_id previsok; međutim, druga strana je sigurno još nije primila)
    • 4 = poruka primljena (imajte na umu da je ovaj odgovor ujedno i potvrda o prijemu)
    • +8 = poruka je već potvrđena
    • +16 = poruka koja ne zahtijeva potvrdu
    • +32 = RPC upit sadržan u poruci se obrađuje ili je obrada već završena
    • +64 = odgovor vezan za sadržaj na poruku koja je već generirana
    • +128 = druga strana pouzdano zna da je poruka već primljena
      Ovaj odgovor ne zahtijeva potvrdu. To je samo po sebi potvrda relevantnog msgs_state_req.
      Imajte na umu da ako se iznenada pokaže da druga strana nema poruku koja izgleda kao da joj je poslana, poruka se može jednostavno ponovo poslati. Čak i ako druga strana primi dvije kopije poruke u isto vrijeme, duplikat će biti zanemaren. (Ako je prošlo previše vremena, a originalni msg_id više nije važeći, poruka treba biti umotana u msg_copy).
  • Dobrovoljno priopćavanje statusa poruka
    Svaka strana može dobrovoljno obavijestiti drugu stranu o statusu poruka koje je druga strana poslala.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Prošireno dobrovoljno priopćavanje statusa jedne poruke
    ...
    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;
  • Eksplicitni zahtjev za ponovno slanje poruka
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    Udaljena strana odmah odgovara ponovnim slanjem traženih poruka […]
  • Eksplicitni zahtjev za ponovno slanje odgovora
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    Udaljena strana odmah odgovara ponovnim slanjem odgovore na tražene poruke […]
  • Message Copies
    U nekim situacijama, treba ponovo poslati staru poruku s msg_id koji više nije važeći. Zatim se umotava u kontejner za kopiranje:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Jednom primljena, poruka se obrađuje kao da omot nije tu. Međutim, ako se pouzdano zna da je poruka orig_message.msg_id primljena, tada se nova poruka ne obrađuje (dok se u isto vrijeme ona i orig_message.msg_id priznaju). Vrijednost orig_message.msg_id mora biti niža od msg_id kontejnera.

Hajde da prećutimo šta msgs_state_info opet vire uši nedovršenog TL-a (trebao nam je vektor bajtova, a u niža dva bita je bio enum, a u viša dva bita bile su zastavice). Poenta je drugačija. Da li neko razume zašto je sve ovo u praksi? u pravom klijentu potrebno?.. Teško, ali može se zamisliti neka korist ako se osoba bavi otklanjanjem grešaka, i to u interaktivnom načinu - pitajte server šta i kako. Ali ovdje su opisani zahtjevi povratno putovanje.

Iz toga proizilazi da svaka strana mora ne samo šifrirati i slati poruke, već i čuvati podatke o sebi, o odgovorima na njih, nepoznato vrijeme. Dokumentacija ne opisuje ni vremenske okvire ni praktičnu primenljivost ovih funkcija. Nema šanse. Ono što je najnevjerovatnije je da se oni zapravo koriste u kodu službenih klijenata! Očigledno im je rečeno nešto što nije uvršteno u javnu dokumentaciju. Shvatite iz koda zašto, više nije tako jednostavno kao u slučaju TL-a - nije (relativno) logički izoliran dio, već komad vezan za arhitekturu aplikacije, tj. će trebati znatno više vremena za razumijevanje koda aplikacije.

Pingovi i tajmingi. Redovi.

Iz svega, ako se prisjetimo nagađanja o arhitekturi servera (distribucija zahtjeva po backendovima), slijedi prilično tužna stvar - uprkos svim garancijama isporuke u TCP-u (ili su podaci isporučeni, ili ćete biti obaviješteni o praznini, ali podaci će biti isporučeni prije nego što se problem pojavi), da potvrde u samom MTProtou - nema garancija. Server može lako izgubiti ili izbaciti vašu poruku, i tu se ništa ne može učiniti, samo koristite različite vrste štaka.

I prije svega - redovi poruka. Pa, s jednim je sve bilo očito od samog početka - nepotvrđena poruka mora biti pohranjena i zamjerana. I nakon kojeg vremena? I ludak ga poznaje. Možda te zavisne servisne poruke nekako rešavaju ovaj problem sa štakama, recimo, u Telegram Desktop-u postoje oko 4 reda koji im odgovaraju (možda više, kao što je već pomenuto, za ovo treba ozbiljnije da se udubite u njegov kod i arhitekturu; istovremeno vrijeme, znamo da se ne može uzeti kao uzorak, u njemu se ne koristi određeni broj tipova iz MTProto šeme).

Zašto se ovo dešava? Verovatno serverski programeri nisu bili u stanju da obezbede pouzdanost unutar klastera, pa čak ni baferovanje na prednjem balanseru, pa su ovaj problem preneli na klijenta. Iz očaja, Vasilij je pokušao implementirati alternativnu opciju, sa samo dva reda, koristeći algoritme iz TCP-a - mjerenje RTT-a do servera i podešavanje veličine „prozora“ (u porukama) u zavisnosti od broja nepotvrđenih zahtjeva. Odnosno, tako gruba heuristika za procjenu opterećenja servera je koliko naših zahtjeva može prožvakati u isto vrijeme i ne izgubiti.

Pa, to je, razumete, zar ne? Ako morate ponovo implementirati TCP na protokol koji radi preko TCP-a, to ukazuje na vrlo loše dizajniran protokol.

Oh da, zašto vam treba više od jednog reda čekanja, i šta to uopšte znači za osobu koja radi sa API-jem visokog nivoa? Vidite, napravite zahtjev, serijalizirate ga, ali često ga ne možete odmah poslati. Zašto? Jer će odgovor biti msg_id, što je privremenoаJa sam etiketa, čije je dodjeljivanje najbolje odgoditi do što je moguće kasnije - u slučaju da je server odbije zbog neusklađenosti vremena između nas i njega (naravno, možemo napraviti štaku koja nam pomjeri vrijeme iz sadašnjosti serveru dodavanjem delte izračunate iz odgovora servera - zvanični klijenti to rade, ali je grubo i netačno zbog baferovanja). Stoga, kada uputite zahtjev s pozivom lokalne funkcije iz biblioteke, poruka prolazi kroz sljedeće faze:

  1. Leži u jednom redu i čeka šifriranje.
  2. Imenovan msg_id i poruka je otišla u drugi red čekanja - moguće prosljeđivanje; poslati u utičnicu.
  3. a) Server je odgovorio na MsgsAck - poruka je isporučena, brišemo je iz "drugog reda".
    b) Ili obrnuto, nešto mu se nije svidjelo, odgovorio je na badmsg - ponovo pošalji iz "drugog reda"
    c) Ništa nije poznato, poruku treba ponovo poslati iz drugog reda - ali se ne zna tačno kada.
  4. Server je konačno odgovorio RpcResult - stvarni odgovor (ili greška) - ne samo isporučen, već i obrađen.

Verovatno, upotreba kontejnera mogla bi djelomično riješiti problem. To je kada se gomila poruka spakuje u jednu, a server je odgovorio potvrdom na sve odjednom, u jednom msg_id. Ali on će također odbaciti ovaj čopor, ako nešto pođe po zlu, u cijelosti.

I u ovom trenutku dolaze u obzir netehnička razmatranja. Iz iskustva smo vidjeli mnogo štaka, a osim toga, sada ćemo vidjeti još primjera loših savjeta i arhitekture – da li u ovakvim uslovima vrijedi vjerovati i donositi takve odluke? Pitanje je retoričko (naravno da nije).

o cemu pricamo? Ako na temu "poruke o drogama o porukama" još uvijek možete nagađati uz primjedbe poput "ti si glup, nisi shvatio naš briljantan plan!" (pa prvo napišite dokumentaciju, kako bi normalni ljudi trebali, sa obrazloženjem i primjerima razmjene paketa, pa ćemo onda pričati), onda su tajmingi/timeouti čisto praktično i konkretno pitanje, ovdje se sve odavno zna. Šta nam dokumentacija govori o timeoutima?

Server obično potvrđuje prijem poruke od klijenta (obično, RPC upit) koristeći RPC odgovor. Ako se odgovor čeka dugo, server može prvo poslati potvrdu o prijemu, a nešto kasnije i sam RPC odgovor.

Klijent obično potvrđuje prijem poruke od servera (obično, RPC odgovor) dodavanjem potvrde sledećem RPC upitu ako nije prekasno poslana (ako je generisana, recimo, 60-120 sekundi nakon prijema poruke sa servera). Međutim, ako u dužem vremenskom periodu nema razloga za slanje poruka serveru ili ako postoji veliki broj nepriznatih poruka sa servera (recimo preko 16), klijent šalje samostalnu potvrdu.

... Prevodim: ni sami ne znamo koliko i kako nam treba, pa pretpostavimo da je tako.

I o pingovima:

Ping poruke (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

Odgovor se obično vraća na istu vezu:

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

Ove poruke ne zahtijevaju potvrde. Pong se prenosi samo kao odgovor na ping, dok ping može pokrenuti bilo koja strana.

Odgođeno zatvaranje veze + PING

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

Radi kao ping. Osim toga, nakon što je ovo primljeno, server pokreće tajmer koji će zatvoriti trenutnu vezu disconnect_delay sekundi kasnije osim ako ne primi novu poruku istog tipa koja automatski resetuje sve prethodne tajmere. Ako klijent, na primjer, šalje ove pingove svakih 60 sekundi, može postaviti disconnect_delay na 75 sekundi.

Jesi li lud?! Za 60 sekundi, voz će ući u stanicu, spustiti se i pokupiti putnike i ponovo izgubiti kontakt u tunelu. Za 120 sekundi, dok ga čujete, stići će do drugog, a veza će najvjerovatnije prekinuti. Pa, jasno je otkud noge - "Čuo sam zvonjavu, ali ne znam gdje je", tu je Naglov algoritam i opcija TCP_NODELAY, namijenjena interaktivnom radu. Ali, izvinite, zadržite njegovu zadanu vrijednost - 200 Millisekundi Ako zaista želite da prikažete nešto slično i uštedite na mogućih par paketa, onda to odložite na 5 sekundi, ili šta god da je vremensko ograničenje poruke „Korisnik kuca...“ sada. Ali ne više.

I konačno, pingovi. Odnosno, provjerava živost TCP veze. Smiješno, ali prije 10-ak godina napisao sam kritički tekst o glasniku studentskog doma našeg fakulteta - tamo su autori pingovali i server sa klijenta, a ne obrnuto. Ali studenti 3. godine su jedno, a međunarodna kancelarija drugo, zar ne?..

Prvo, mali edukativni program. TCP veza, u nedostatku razmjene paketa, može živjeti sedmicama. Ovo je i dobro i loše, u zavisnosti od svrhe. Dobro je da ste imali otvorenu SSH konekciju sa serverom, ustali ste sa kompjutera, restartovali ruter, vratili se na svoje mesto - sesija preko ovog servera nije pokidana (niste ništa ukucali, nije bilo paketa) , zgodno je. Loše je ako postoje hiljade klijenata na serveru, od kojih svaki zauzima resurse (zdravo, Postgres!), a host klijenta se možda davno ponovo pokrenuo - ali nećemo znati za to.

Chat/IM sistemi spadaju u drugi slučaj iz jednog dodatnog razloga - statusa na mreži. Ukoliko je korisnik „otpao“, o tome morate obavijestiti njegove sagovornike. U suprotnom, završićete sa greškom koju su kreatori Jabber-a napravili (i ispravljali je 20 godina) - korisnik je prekinuo vezu, ali i dalje mu pišu poruke, verujući da je na mreži (koje su se takođe potpuno izgubile u ovim nekoliko minuta prije nego što je otkriven prekid). Ne, opcija TCP_KEEPALIVE, koju mnogi ljudi koji ne razumiju kako TCP tajmeri rade nasumično ubacuju (postavljanjem divljih vrijednosti poput desetina sekundi), ovdje neće pomoći - morate se pobrinuti da ne samo jezgro OS-a korisnikova mašina je živa, ali i normalno funkcioniše, ne može da reaguje, a i sama aplikacija (mislite li da ne može da se zamrzne? Telegram Desktop na Ubuntu 18.04 mi se zamrzao više puta).

Zato morate pingovati server klijent, a ne obrnuto - ako klijent to uradi, ako je veza prekinuta, ping neće biti isporučen, cilj neće biti postignut.

Šta vidimo na Telegramu? Upravo suprotno! Pa, to je. Formalno, naravno, obje strane mogu jedna drugu pingovati. U praksi klijenti koriste štaku ping_delay_disconnect, koji postavlja tajmer na serveru. Pa, izvinite, nije na klijentu da odlučuje koliko dugo želi da živi tamo bez pinga. Server, na osnovu svog opterećenja, zna bolje. Ali, naravno, ako vam ne smetaju resursi, onda ćete sami biti svoj zli Pinokio, a štaka će učiniti...

Kako je trebalo biti dizajnirano?

Smatram da gore navedene činjenice jasno ukazuju da Telegram/VKontakte tim nije baš kompetentan u oblasti transporta (i nižeg) nivoa kompjuterskih mreža i njihove niske kvalifikacije u relevantnim pitanjima.

Zašto se ispostavilo da je tako komplikovano i kako arhitekti Telegrama mogu pokušati da prigovore? Činjenica da su pokušali da naprave sesiju koja preživi prekide TCP veze, odnosno ono što nije isporučeno sada, isporučićemo kasnije. Vjerovatno su pokušali i da naprave UDP transport, ali su naišli na poteškoće i odustali od toga (zato je dokumentacija prazna - nije se imalo čime hvaliti). Ali zbog nerazumijevanja kako mreže općenito i TCP posebno funkcioniraju, gdje se možete osloniti na to i gdje to trebate učiniti sami (i kako), te pokušaj da se ovo kombinira sa kriptografijom „dvije ptice sa jedan kamen”, ovo je rezultat.

Kako je to bilo potrebno? Na osnovu činjenice da msg_id je vremenska oznaka neophodna sa kriptografske tačke gledišta da bi se sprečili napadi ponavljanja, greška je pridodati joj jedinstvenu funkciju identifikatora. Stoga, bez suštinske promjene trenutne arhitekture (kada se generiše stream ažuriranja, to je tema visokog nivoa API-ja za drugi dio ove serije postova), trebalo bi:

  1. Server koji drži TCP vezu sa klijentom preuzima odgovornost - ako je pročitao sa utičnice, molimo vas da potvrdite, obradite ili vratite grešku, bez gubitka. Tada potvrda nije vektor ID-ova, već jednostavno “zadnji primljeni seq_no” - samo broj, kao u TCP-u (dva broja - vaš seq i potvrđeni). Stalno smo na sesiji, zar ne?
  2. Vremenska oznaka za sprečavanje napada ponavljanja postaje posebno polje, a la nonce. Provjeren je, ali ne utiče ni na šta drugo. Dosta i uint32 - ako se naša sol mijenja barem svakih pola dana, možemo dodijeliti 16 bitova nižeg reda cijelog dijela trenutnog vremena, a ostatak - razlomcima sekunde (kao sada).
  3. Uklonjeno msg_id uopšte - sa tačke gledišta razlikovanja zahteva na backendu, postoji, prvo, id klijenta, a drugo, id sesije, koji ih povezuje. Prema tome, samo jedna stvar je dovoljna kao identifikator zahtjeva seq_no.

Ovo također nije najuspješnija opcija; potpuni slučajni odabir može poslužiti kao identifikator - to se već radi u API-ju visoke razine prilikom slanja poruke, usput. Bilo bi bolje da se arhitektura potpuno prepravi iz relativne u apsolutnu, ali ovo je tema za drugi dio, a ne ovaj post.

API?

Ta-daam! Dakle, prošavši put pun bola i štaka, konačno smo bili u mogućnosti da pošaljemo bilo kakve zahtjeve serveru i dobijemo bilo kakve odgovore na njih, kao i da primamo ažuriranja sa servera (ne kao odgovor na zahtjev, već sam šalje nam, kao PUSH, ako neko tako je jasnije).

Pažnja, sada će u članku biti jedini primjer u Perlu! (za one koji nisu upoznati sa sintaksom, prvi argument bless je struktura podataka objekta, drugi je njegova klasa):

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

Da, nije namjerno spojler - ako ga još niste pročitali, samo naprijed i učinite to!

Oh, čekaj~~... kako ovo izgleda? Nešto vrlo poznato... možda je ovo struktura podataka tipičnog Web API-ja u JSON-u, osim što su klase također pridružene objektima?..

Pa ovako ispada... O čemu se radi, drugovi?.. Toliko truda - i stali smo da se odmorimo tamo gde su Web programeri tek počinje?..Zar samo JSON preko HTTPS-a ne bi bio jednostavniji?! Šta smo dobili u zamjenu? Je li trud bio vrijedan toga?

Hajde da procenimo šta nam je TL+MTProto dao i koje su alternative moguće. Pa, HTTP, koji se fokusira na model zahtjev-odgovor, ne odgovara, ali barem nešto iznad TLS-a?

Kompaktna serijalizacija. Gledajući ovu strukturu podataka, sličnu JSON-u, sjetim se da postoje njene binarne verzije. Označimo MsgPack kao nedovoljno proširiv, ali postoji, na primjer, CBOR - usput, standard opisan u RFC 7049. Značajan je po tome što definiše tags, kao mehanizam za proširenje, i među već standardizovan dostupno:

  • 25 + 256 - zamjena ponovljenih linija referencom na broj reda, tako jeftina metoda kompresije
  • 26 - serijalizirani Perl objekt s imenom klase i argumentima konstruktora
  • 27 - serijalizovani jezik nezavisan objekat sa imenom tipa i argumentima konstruktora

Pa, pokušao sam serijalizirati iste podatke u TL-u i u CBOR-u s uključenim stringovima i objektima. Rezultat je počeo da varira u korist CBOR-a negdje od megabajta:

cborlen=1039673 tl_len=1095092

Tako zaključak: Postoje znatno jednostavniji formati koji ne podliježu problemu neuspjeha sinhronizacije ili nepoznatog identifikatora, sa uporedivom efikasnošću.

Brzo uspostavljanje veze. To znači nula RTT nakon ponovnog povezivanja (kada je ključ već jednom generiran) - primjenjivo od prve MTProto poruke, ali uz neke rezerve - pogodite istu sol, sesija nije pokvarena, itd. Šta nam umjesto toga nudi TLS? Citat na temu:

Kada koristite PFS u TLS-u, TLS sesije (RFC 5077) za nastavak šifrovane sesije bez ponovnog pregovaranja o ključevima i bez pohranjivanja informacija o ključu na serveru. Prilikom otvaranja prve veze i kreiranja ključeva, server šifrira stanje veze i prenosi ga klijentu (u obliku ulaznice za sesiju). Shodno tome, kada se veza nastavi, klijent šalje tiket za sesiju, uključujući ključ sesije, nazad na server. Sam tiket je šifrovan sa privremenim ključem (session ticket key), koji se čuva na serveru i mora biti distribuiran među svim frontend serverima koji obrađuju SSL u klasterizovanim rešenjima.[10] Stoga, uvođenje tiketa za sesiju može narušiti PFS ako su privremeni serverski ključevi ugroženi, na primjer, kada se pohranjuju duže vrijeme (OpenSSL, nginx, Apache ih po defaultu pohranjuju za cijelo vrijeme trajanja programa; popularne stranice koriste ključ nekoliko sati, do dana).

Ovdje RTT nije nula, potrebno je razmijeniti barem ClientHello i ServerHello, nakon čega klijent može poslati podatke zajedno sa Finished. Ali ovdje treba imati na umu da nemamo web sa svojom gomilom novootvorenih veza, već messenger, čija je veza često jedan i manje-više dugotrajni, relativno kratki zahtjevi do web stranica - sve je multipleksirano interno. Odnosno, sasvim je prihvatljivo da nismo naišli na jako loš dio podzemne željeznice.

Zaboravili ste još nešto? Pišite u komentarima.

Nastavlja se!

U drugom dijelu ove serije postova razmatraćemo ne tehnička, već organizaciona pitanja – pristupe, ideologiju, interfejs, odnos prema korisnicima itd. Međutim, na osnovu tehničkih informacija koje su ovdje predstavljene.

Treći dio će nastaviti sa analizom tehničke komponente / razvojnog iskustva. Naučićete, posebno:

  • nastavak pandemonijuma sa raznim tipovima TL
  • nepoznate stvari o kanalima i supergrupama
  • zašto su dijalozi gori od liste
  • o apsolutnom naspram relativnog adresiranja poruka
  • koja je razlika između slike i slike
  • kako emoji ometaju kurziv tekst

i druge štake! Stay tuned!

izvor: www.habr.com

Dodajte komentar