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

Nedavno su se na Habréu sve češće počeli pojavljivati ​​postovi o tome koliko je Telegram dobar, koliko su braća Durov briljantna i iskusna u izgradnji mrežnih sustava itd. U isto vrijeme, vrlo malo ljudi se stvarno udubilo u tehnički uređaj - najviše, oni koriste prilično jednostavan (i prilično različit od MTProto) Bot API temeljen na JSON-u, i obično samo prihvaćaju na vjeri sve pohvale i PR koji se vrti oko glasnika. Prije gotovo godinu i pol, moj kolega u nevladinoj organizaciji Eshelon Vasily (nažalost, njegov račun na Habréu je izbrisan zajedno s nacrtom) počeo je ispočetka pisati vlastiti Telegram klijent u Perlu, a kasnije se pridružio i autor ovih redaka. Zašto Perl, odmah će se zapitati neki? Jer takvi projekti već postoje na drugim jezicima. Zapravo, nije u tome poanta, može postojati bilo koji drugi jezik u kojem nema gotova biblioteka, pa sukladno tome autor mora ići do kraja od nule. Štoviše, kriptografija je stvar povjerenja, ali provjerite. S proizvodom koji je usmjeren na 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). Trenutačno biblioteka radi prilično dobro na "prosječnoj" razini (omogućuje vam da postavljate bilo kakve API zahtjeve).

Međutim, u ovoj seriji postova neće biti puno kriptografije ili matematike. Ali bit će mnogo drugih tehničkih detalja i arhitektonskih štaka (također 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 (ponovno, 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 - je li moguće napisati klijenta samo prema specifikaciji, "bez gledanja" izvornog koda, bio on službeni (Telegram Desktop, mobitel), ili neslužbeni Telethon?

Oglašavanje:

Dokumentacija... postoji, zar ne? To je istina?..

Fragmenti bilješki za ovaj članak počeli su se prikupljati prošlog ljeta. Sve ovo vrijeme na službenoj stranici https://core.telegram.org Dokumentacija je bila od sloja 23, tj. zapeo negdje u 2014. (sjećate se, tada još nije ni bilo kanala?). Naravno, u teoriji, to nam je trebalo omogućiti implementaciju klijenta s funkcionalnošću u to vrijeme 2014. godine. Ali čak iu ovakvom stanju dokumentacija je bila, prvo, nepotpuna, a drugo, mjestimično je proturječila sama sebi. Bilo je to prije nešto više od mjesec dana, u rujnu 2019 slučajno Otkriveno je da postoji veliko ažuriranje dokumentacije na stranici, za potpuno recentni 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, trebate imati na umu da neke od ovih stvari više nisu relevantne, ali neke su još uvijek sasvim. Uostalom, 5 godina u modernom svijetu nije samo dugo vrijeme, već vrlo Puno. Od tih vremena (pogotovo ako ne uzmete u obzir odbačene i oživljene geochat stranice od tada), broj API metoda u shemi je narastao sa stotinu na više od dvjesto pedeset!

Odakle početi kao mladi autor?

Nije važno pišete li od nule ili koristite, na primjer, gotove biblioteke poput Teleton za Python ili Madeline za PHP, u svakom slučaju, prvo ćete trebati registrirajte svoju prijavu - dobiti parametre api_id и api_hash (oni koji su radili s VKontakte API odmah razumiju) po kojem će poslužitelj identificirati aplikaciju. Ovaj morati učiniti iz pravnih razloga, no o tome zašto autori knjižnica to ne mogu objaviti više ćemo govoriti u drugom dijelu. Možda ćete biti zadovoljni testnim vrijednostima, iako su vrlo ograničene - činjenica je da se sada možete registrirati samo jedan aplikaciju, stoga ne srljajte bezglavo u nju.

Sada, s tehničke točke gledišta, treba nas zanimati činjenica da nakon registracije trebamo primati obavijesti od Telegrama o ažuriranju dokumentacije, protokola itd. Odnosno, moglo bi se pretpostaviti da je mjesto s dokovima jednostavno napušteno i nastavilo raditi posebno s onima koji su počeli stvarati klijente, jer lakše je. Ali ne, ništa takvo nije uoč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 Prvim koracima prije svega, u stvari, prvo ćete morati implementirati MTProto protokol - ali kad bi vjerovao raspored prema OSI modelu na kraju stranice za opći opis protokola, onda je potpuno uzaludno.

Zapravo, i prije i poslije MTProto-a, na nekoliko razina odjednom (kako kažu strani umrežači koji rade u jezgri OS-a, LayerBREAK) ispriječit će se velika, bolna i strašna tema...

Binarna serijalizacija: TL (Type Language) i njegova shema, slojevi i mnoge druge strašne riječi

Ova je tema, zapravo, ključ Telegramovih problema. I bit će puno strašnih riječi ako pokušate proniknuti u to.

Dakle, evo dijagrama. Ako ti ova riječ padne na pamet, reci: JSON shema, Dobro ste mislili. Cilj je isti: neki jezik za opisivanje mogućeg skupa prenesenih podataka. Tu sličnosti prestaju. Ako sa stranice MTProto protokol, ili iz izvornog stabla službenog klijenta, pokušat ćemo otvoriti neku shemu, vidjet ć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 ovo prvi put vidi intuitivno će moći prepoznati samo dio napisanog - pa to su naizgled strukture (iako gdje je naziv, lijevo ili desno?), u njima su polja, nakon čega iza dvotačke slijedi tip... vjerojatno. Ovdje u uglastim zagradama vjerojatno postoje predlošci kao u C++ (zapravo, ne baš). I što znače svi drugi simboli, upitnici, uskličnici, postoci, hash znakovi (i očito znače različite stvari na različitim mjestima), ponekad prisutni, a ponekad ne, heksadecimalni brojevi - i najvažnije, kako iz ovoga doći pravo (koji poslužitelj neće odbiti) tok bajtova? Morat ćete pročitati dokumentaciju (da, postoje poveznice na shemu u JSON verziji u blizini - ali to ne čini ništa jasnijim).

Otvorite stranicu Serijalizacija binarnih podataka i uronite u čarobni svijet gljiva i diskretne matematike, nešto slično matanu u 4. godini. Abeceda, tip, vrijednost, kombinator, funkcionalni kombinator, normalan oblik, kompozitni tip, polimorfni tip... i to je sve samo prva stranica! Sljedeći vas čeka TL jezik, 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 se morati probijati kroz prepričavanje matematike prevedeno s ruskog na engleski na još osam ugrađenih stranice!

Čitatelji koji su upoznati s funkcionalnim jezicima i automatskim zaključivanjem tipa će, naravno, vidjeti jezik opisa u ovom jeziku, čak i iz primjera, kao puno poznatiji i mogu reći da to zapravo nije loše u principu. Prigovori ovome su:

  • Da, cilj zvuči dobro, ali jao, ona nije postignuto
  • Obrazovanje na ruskim sveučilištima razlikuje se čak i među IT specijalnostima - nisu svi pohađali odgovarajući tečaj
  • Konačno, kao što ćemo vidjeti, u praksi je tako ne zahtijeva, budući da se koristi samo ograničeni podskup čak i TL-a koji je opisan

Kao što je rečeno Leon Štreberu na kanalu #perl u mreži FreeNode IRC, koji je pokušao implementirati vrata s Telegrama na Matrix (prijevod citata prema sjećanju nije točan):

Čini se kao da se netko prvi put upoznao s teorijom tipa, oduševio se i počeo se igrati s njom, ne mareći je li to potrebno u praksi.

Uvjerite se sami, ako potreba za golim tipovima (int, long, itd.) kao nečim elementarnim ne izaziva pitanja - u konačnici oni se moraju implementirati ručno - na primjer, pokušajmo izvesti iz njih vektor. To je, zapravo, niz, ako nastale stvari nazovete pravim imenom.

Ali prije

Kratak opis podskupa TL sintakse za one koji ne čitaju službenu 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 dizajner, nakon čega po želji (u praksi - uvijek) preko simbola # mora biti CRC32 iz normaliziranog niza opisa ove vrste. Zatim slijedi opis polja; ako postoje, tip može biti prazan. Ovo sve završava znakom jednakosti, imenom tipa kojem ovaj konstruktor – odnosno, zapravo podtip – pripada. Momak desno od znaka jednakosti je polimorfna - odnosno može mu odgovarati nekoliko specifičnih tipova.

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

Zašto "konstruktor" i "polimorfni" ako nije OOP? Pa, zapravo, nekome će biti lakše razmišljati o ovome u terminima OOP-a - polimorfni tip kao apstraktna klasa, a konstruktori su njezine izravne klase potomci, i final u terminologiji niza jezika. Zapravo, naravno, samo ovdje sličnost sa stvarnim preopterećenim metodama konstruktora u OO programskim jezicima. Budući da su ovdje samo strukture podataka, nema metoda (iako je daljnji opis funkcija i metoda sasvim sposoban stvoriti zabunu u glavi da one postoje, ali to je druga stvar) - o konstruktoru možete razmišljati kao o vrijednosti iz koji se gradi tip prilikom čitanja toka bajtova.

Kako se to događa? Deserijalizator, koji uvijek čita 4 bajta, vidi vrijednost 0xcrc32 - i razumije što će se sljedeće dogoditi field1 s tipom int, tj. čita točno 4 bajta, na ovom polju iznad s tipom PolymorType čitati. vidi 0x2crc32 i razumije da postoje još dva polja, prvo long, što znači da čitamo 8 bajtova. I onda opet složeni tip, koji se deserijalizira na isti način. Na primjer, Type3 može biti deklariran u krugu čim dva konstruktora, odnosno, onda se moraju susresti bilo koji 0x12abcd34, nakon čega je potrebno pročitati još 4 bajta intIli 0x6789cdef, nakon čega neće biti ništa. Bilo što drugo - morate baciti iznimku. U svakom slučaju, nakon ovoga se vraćamo na čitanje 4 bajta int polja field_c в constructorTwo i time završavamo čitanje našeg PolymorType.

Konačno, ako te uhvate 0xdeadcrc za constructorThree, onda sve postaje kompliciranije. Naše prvo polje je bit_flags_of_what_really_present s tipom # - zapravo, ovo je samo alias za taj tip nat, što znači "prirodni broj". To jest, zapravo, unsigned int je, usput, jedini slučaj kada se nepredpisani brojevi pojavljuju u stvarnim sklopovima. Dakle, sljedeća je konstrukcija s upitnikom, što znači da će ovo polje - biti prisutno na žici samo ako je odgovarajući bit postavljen u polju na koje se upućuje (približno kao ternarni operator). Dakle, pretpostavimo da je ovaj bit postavljen, što znači da dalje moramo čitati polje poput Type, koji u našem primjeru ima 2 konstruktora. Jedan je prazan (sastoji se samo od identifikatora), drugi ima polje ids s tipom ids:Vector<long>.

Možda mislite da su i predlošci i generici u prednosti ili Javi. Ali ne. Skoro. Ovaj jedini slučaju korištenja kutnih zagrada u stvarnim krugovima, a koristi se SAMO za Vector. U nizu bajtova to će biti 4 bajta CRC32 za sam tip Vector, uvijek isti, zatim 4 bajta - broj elemenata niza, a zatim sami ovi elementi.

Dodajte ovome činjenicu da se serijalizacija uvijek događa u riječima od 4 bajta, svi tipovi su višekratnici toga - ugrađeni tipovi su također opisani bytes и string s ručnom serijalizacijom duljine i ovim poravnanjem za 4 - pa, čini se da zvuči normalno i čak relativno učinkovito? Iako se za TL tvrdi da je učinkovita binarna serijalizacija, ali kvragu s njima, s proširenjem gotovo svega, čak i Booleovih vrijednosti i nizova od jednog znaka na 4 bajta, hoće li JSON i dalje biti mnogo deblji? Gledajte, čak i nepotrebna polja mogu se preskočiti bit flagovima, sve je sasvim dobro, pa čak i proširivo za budućnost, pa zašto kasnije ne dodati nova opcijska polja u konstruktor?..

Ali ne, ako ne pročitate moj kratki opis, već potpunu dokumentaciju i razmislite o implementaciji. Prvo, CRC32 konstruktora izračunava se prema normaliziranom retku tekstualnog opisa sheme (uklonite dodatne razmake, itd.) - tako da ako se doda novo polje, redak opisa tipa će se promijeniti, a time i njegov CRC32 i , posljedično, serijalizacija. A što bi stari klijent napravio kada bi dobio polje s novim postavljenim zastavicama, a ne zna što dalje s njima?..

Drugo, podsjetimo se CRC32, koji se ovdje uglavnom koristi kao hash funkcije kako bi se jedinstveno odredilo koji se tip (de)serializira. Ovdje se susrećemo s problemom sudara – i ne, vjerojatnost nije jedan prema 232, nego puno veća. Tko se sjetio da je CRC32 dizajniran za otkrivanje (i ispravljanje) grešaka u komunikacijskom kanalu, te u skladu s tim poboljšava ta svojstva nauštrb drugih? Na primjer, nije ga briga za preuređivanje bajtova: ako izračunate CRC32 iz dva retka, 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 interpunkcijskih znakova), a ti nazivi nisu osobito nasumični, vjerojatnost takvog preuređivanja uvelike se povećava.

Inače, tko je provjeravao što tamo ima? stvarno CRC32? Jedan od ranih izvornih kodova (čak i prije Waltmana) imao je funkciju raspršivanja koja je množila svaki znak s brojem 239, toliko voljenom od ovih ljudi, ha ha!

Konačno, u redu, shvatili smo da konstruktori s tipom polja Vector<int> и Vector<PolymorType> imat će drugačiji CRC32. Što je s on-line izvedbom? A s teorijske točke gledišta, postaje li ovo dio tipa? Recimo da proslijedimo niz od deset tisuća brojeva, dobro sa Vector<int> sve je jasno, dužina i još 40000 XNUMX bajtova. Što ako ovo Vector<Type2>, koji se sastoji od samo jednog polja int i sam je u tipu - trebamo li ponoviti 10000xabcdef0 34 4 puta i zatim XNUMX bajta int, ili jezik to može NEOVISITI za nas od konstruktora fixedVec i umjesto 80000 bajtova prenijeti opet samo 40000?

Ovo uopće nije prazno teoretsko pitanje - zamislite da dobijete popis korisnika grupe od kojih svaki ima ID, ime, prezime - razlika u količini podataka prenesenih mobilnom vezom može biti značajna. Reklamira nam se upravo učinkovitost Telegramove serijalizacije.

Tako…

Vector, koji nikada nije objavljen

Ako pokušate proći kroz stranice opisa kombinatora i tako dalje, vidjet ćete da se vektor (pa čak i matrica) formalno pokušava ispisati kroz torke od nekoliko listova. Ali na kraju zaborave, posljednji korak se preskoči i jednostavno se zada definicija vektora koja još nije vezana za tip. Što je bilo? U jezicima programiranje, posebno funkcionalnih, prilično je tipično opisati strukturu rekurzivno - prevodilac sa svojom lijenom evaluacijom će razumjeti i učiniti sve sam. U jeziku serijalizacija podataka ono što je potrebno je UČINKOVITOST: dovoljno je jednostavno opisati список, tj. struktura od dva elementa - prvi je podatkovni element, drugi je sama ista struktura ili prazan prostor za rep (pack (cons) u Lispu). Ali to će očito zahtijevati svaki element troši dodatna 4 bajta (CRC32 u slučaju u TL-u) za opis svoje vrste. Niz se također može lako opisati fiksna veličina, no u slučaju niza unaprijed nepoznate duljine prekidamo.

Stoga, budući da TL ne dopušta izlaz vektora, morao se dodati sa strane. U konačnici dokumentacija kaže:

Serijalizacija uvijek koristi isti konstruktor “vektor” (const 0x1cb5c415 = crc32(“vektor t:Tip # [ t ] = Vektor t”) koji ne ovisi o specifičnoj vrijednosti varijable tipa t.

Vrijednost opcijskog parametra t nije uključena u serijalizaciju budući da je izvedena iz tipa rezultata (uvijek poznatog prije deserijalizacije).

Pogledaj bolje: vector {t:Type} # [ t ] = Vector t - ali nigdje Sama ova definicija ne kaže da prvi broj mora biti jednak duljini vektora! I ne dolazi niotkuda. To je datost koju treba imati na umu i implementirati svojim rukama. Na drugom mjestu, dokumentacija čak iskreno spominje da tip nije pravi:

Polimorfni pseudotip vektora t je "tip" čija je vrijednost niz vrijednosti bilo kojeg tipa t, bilo uokvirenih ili golih.

... ali se ne fokusira na to. Kada, umorni od natezanja matematike (možda poznate i sa fakulteta), odlučite odustati i zapravo pogledati kako to raditi u praksi, u vašoj glavi ostaje dojam da je ovo ozbiljno Matematika u srži, očito su je izmislili Cool People (dvojica matematičara - pobjednici ACM-a), a ne bilo tko. Cilj - pokazati se - je postignut.

Usput, o broju. Podsjetimo da # to je sinonim nat, prirodni broj:

Postoje izrazi tipa (tip-izraz) i numerički izrazi (nat-ekspr). Međutim, oni su definirani na isti način.

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

ali u gramatici su opisani na isti način, tj. Ovu razliku opet treba zapamtiti i staviti u implementaciju ručno.

Pa, da, vrste predložaka (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>;

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

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

Postojeće TL implementacije u kodu

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

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

ali razmotrimo, radi potpunosti, da pratimo, da tako kažemo, evoluciju Diva Misli.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Ili ovaj lijepi:

    static const char *reserved_words_polymorhic[] = {

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

      };

Ovaj fragment govori o predlošcima poput:

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

Ovo je definicija tipa predloška hashmapa kao vektora parova int - tip. 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 napisati T, ali trebali biste napisati alpha, beta... Ali ne više od 8 parametara, tu fantazija prestaje. Čini se da su se jednom davno u Sankt Peterburgu vodili dijalozi poput ovog:

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

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

Ali ovdje se radilo o prvoj objavljenoj implementaciji TL-a "općenito". Prijeđimo na razmatranje implementacija u samim Telegram klijentima.

Riječ Vasiliju:

Vasily, [09.10.18 17:07] Najviše od svega, guzica je vruća jer su napravili hrpu apstrakcija, a zatim zabili vijak na njih, a generator koda pokrili štakama
Kao rezultat toga, prvo iz dock pilot.jpg
Zatim iz koda dzhekichan.webp

Naravno, od ljudi koji su upoznati s algoritmima i matematikom možemo očekivati ​​da su čitali Aho, Ullmanna i da su upoznati s alatima koji su postali de facto standard u industriji tijekom desetljeća za pisanje njihovih DSL prevodilaca, zar ne?..

Od strane autora telegram-cli je Vitaly Valtman, kao što se može razumjeti iz pojave TLO formata izvan njegovih (cli) granica, član tima - sada je dodijeljena biblioteka za raščlanjivanje TL-a odvojeno, kakav je dojam o njoj TL parser? ..

16.12 04:18 Vasilij: Mislim da netko nije savladao lex+yacc
16.12 04:18 Vasily: Ne mogu to drugačije objasniti
16.12 04:18 Vasily: dobro, ili su plaćeni za broj redaka u VK
16.12 04:19 Vasilij: 3k+ linija itd.<censored> umjesto parsera

Možda izuzetak? Da vidimo kako čini Ovo je SLUŽBENI 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+ redaka u Pythonu, par regularnih izraza + posebni slučajevi poput vektora, koji je, naravno, deklariran u shemi kako bi trebao biti prema TL sintaksi, ali oni su se oslonili na ovu sintaksu da ga analiziraju... Postavlja se pitanje zašto je sve to bilo čudo?иSlojevitije je ako to ionako nitko neće raščlaniti po dokumentaciji?!

Usput... Sjećate li se da smo razgovarali o CRC32 provjeri? Dakle, u generatoru koda Telegram Desktop postoji popis iznimaka za one vrste u kojima je izračunati CRC32 ne podudara se s onim prikazanim na dijagramu!

Vasily, [18.12/22 49:XNUMX] i ovdje bih razmislio je li potreban takav TL
da se želim petljati s alternativnim implementacijama, počeo bih umetati prijelome redaka, pola parsera će se pokvariti na definicijama s više redaka
tdesktop, međutim, također

Sjetite se točke o jednolinijskim, vratit ćemo se na to malo kasnije.

U redu, telegram-cli je neslužbeni, Telegram Desktop je službeni, ali što je s ostalima? Tko zna?.. U kodu Android klijenta uopće nije bilo parsera sheme (što otvara pitanja o otvorenom kodu, ali ovo je za drugi dio), ali bilo je nekoliko drugih smiješnih dijelova koda, ali više o njima u pododjeljak u nastavku.

Koja još pitanja serijalizacija postavlja u praksi? Na primjer, učinili su puno stvari, naravno, s bitnim poljima i uvjetnim poljima:

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

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

Vasilij: Dušo, ne brini što radiš!
Vasily: Negdje u dokumentu se spominje da je true goli tip nulte duljine, ali nemoguće je sastaviti bilo što iz njihovog dokumenta
Vasily: U implementacijama otvorenog koda ni to nije slučaj, ali postoji hrpa štaka i potpora

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

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

msg_container#73f1f8dc messages:vector message = MessageContainer;

U drugom:

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

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

Nisam vidio definiciju golog vektora i nisam je naišao

Analiza se piše rukom u teletonu

U njegovom dijagramu definicija je komentirana msg_container

Opet ostaje pitanje o %. Nije opisano.

Vadim Goncharov, [22.06.18 19:22] a u tdesktopu?

Vasily, [22.06.18 19:23] Ali njihov TL parser na običnim motorima najvjerojatnije neće jesti ni ovo

// parsed manually

TL je prekrasna apstrakcija, nitko je ne implementira u potpunosti

A % nije u njihovoj verziji sheme

Ali ovdje je dokumentacija proturječna, pa idk

Pronađeno je u gramatici, mogli su jednostavno zaboraviti opisati semantiku

Vidjeli ste dokument na TL, ne možete to shvatiti bez pola litre

“Pa, recimo”, reći će drugi čitatelj, “ti nešto kritiziraš, pa mi pokaži kako to treba učiniti.”

Vasily odgovara: “Što se tiče parsera, volim takve stvari

    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 bolje nego

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

oni. jednostavnije je blago rečeno.”

Općenito, kao rezultat toga, parser i generator koda za stvarno korišteni podskup TL-a stanu u otprilike 100 redaka gramatike i ~300 redaka generatora (računajući sve printgenerirani kod), uključujući gomile informacija o vrsti za introspekciju u svakoj klasi. Svaki polimorfni tip pretvara se u praznu apstraktnu baznu klasu, a konstruktori nasljeđuju od nje i imaju metode za serijalizaciju i deserijalizaciju.

Nedostatak tipova u jeziku tipa

Snažno tipkanje je dobra stvar, zar ne? Ne, ovo nije holivar (iako više volim dinamičke jezike), već postulat u okviru TL-a. Na temelju njega, jezik bi nam trebao omogućiti sve vrste provjera. Dobro, dobro, možda ne on sam, nego implementacija, ali trebao bi ih barem opisati. A kakve mogućnosti želimo?

Prije svega, ograničenja. Ovdje vidimo u dokumentaciji za učitavanje datoteka:

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

  • part_size % 1024 = 0 (djeljivo s 1KB)
  • 524288 % part_size = 0 (512KB mora biti ravnomjerno djeljivo s part_size)

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

Svaki dio treba imati redni broj, dio_datoteke, s vrijednošću u rasponu od 0 do 2,999.

Nakon što je datoteka particionirana, morate odabrati metodu za njezino spremanje na poslužitelj. Koristiti upload.saveBigFilePart u slučaju da je puna veličina datoteke veća od 10 MB i upload.saveFilePart za manje datoteke.
[…] može se vratiti jedna od sljedećih pogrešaka pri unosu podataka:

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

Je li nešto od ovoga na dijagramu? Može li se to nekako izraziti pomoću TL-a? Ne. Ali oprostite, čak je i djedov Turbo Pascal mogao opisati navedene tipove rasponi. A 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, nizovi... na primjer, bilo bi lijepo opisati da taj niz može sadržavati samo telefonski broj, zar ne?

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

Usput, o pogreškama i povratnim vrijednostima. Čak i oni koji su radili s TL-om zamagljuju oči - to nam nije odmah sinulo svaki funkcija u TL-u zapravo može vratiti ne samo opisani tip povrata, već i pogrešku. Ali to se ni na koji način ne može zaključiti pomoću samog TL-a. Naravno, to je već jasno i nema potrebe za bilo čim u praksi (iako se zapravo RPC može učiniti na različite načine, vratit ćemo se na to kasnije) - ali što je s Čistoćom koncepata Matematike apstraktnih tipova s džennetskog svijeta?.. Podigao sam tegljač - pa ga spoji.

I na kraju, što je s čitljivošću? Pa, tamo, općenito, volio bih opis imati to točno u shemi (u JSON shemi, opet, jest), ali ako ste već nategnuti s tim, što je onda s praktičnom stranom - barem trivijalnim gledanjem razlika tijekom ažuriranja? Uvjerite se sami na pravi 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;

Ovisi o svima, ali GitHub, primjerice, odbija istaknuti promjene unutar tako dugih linija. Igra “pronađi 10 razlika”, a ono što mozak odmah vidi je da su počeci i krajevi u oba primjera isti, treba mučno čitati negdje u sredini... Po meni, nije to samo u teoriji, nego ali čisto vizualno prljav i neuredan.

Usput, o čistoći teorije. Zašto su nam bitna polja? Ne čini li se da oni miris loše sa stajališta teorije tipova? Objašnjenje se može vidjeti u ranijim verzijama dijagrama. U početku, da, tako je bilo, za svaki kihanje stvarao se 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 izbornih polja u svojoj strukturi, tada će vam trebati 32 vrste za sve moguće opcije. Kombinatorna eksplozija. Tako se kristalna čistoća TL teorije ponovno razbila o guzicu od lijevanog željeza surove stvarnosti serijalizacije.

Osim toga, na nekim mjestima ti tipovi sami krše vlastitu tipologiju. Na primjer, u MTProto (sljedeće poglavlje) odgovor se može komprimirati Gzipom, sve je u redu - osim što su slojevi i sklop povrijeđeni. Još jednom, nije požnjeven sam RpcResult, već njegov sadržaj. Pa, zašto to?.. Morao sam rezati štaku kako bi kompresija radila bilo gdje.

Ili drugi primjer, jednom smo otkrili pogrešku - poslana je InputPeerUser umjesto InputUser. Ili obrnuto. Ali uspjelo je! Odnosno, poslužitelj nije mario za vrstu. Kako to 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 radi serijalizacija RUČNO, nije generirani kod! Možda je poslužitelj implementiran na sličan način?.. U principu, ovo će raditi ako se jednom napravi, ali kako to može biti podržano kasnije tijekom ažuriranja? Je li zbog toga izmišljena shema? I tu prelazimo na sljedeće pitanje.

Verziranje. Slojevi

Zašto se shematske verzije nazivaju slojevima može se samo nagađati na temelju povijesti objavljenih shema. Očito su autori isprva mislili da se temeljne stvari mogu napraviti nepromijenjenom shemom, a samo tamo gdje je potrebno, za specifične zahtjeve, naznačili su da se rade drugom verzijom. U principu, čak i dobra ideja - a novo će biti, takoreći, "pomiješano", naslagano na staro. Ali da vidimo kako je to učinjeno. Istina, nisam ga mogao pogledati od samog početka - smiješno je, ali dijagram osnovnog sloja jednostavno ne postoji. Slojevi su počeli s 2. Dokumentacija nam govori o posebnoj značajki TL-a:

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

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

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

Zvuči normalno. Ali što se zatim dogodilo? Zatim se pojavio

invokeWithLayer3#b7475268 query:!X = X;

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

invokeWithLayer4#dea0d430 query:!X = X;

smiješno? Ne, prerano je smijati se, razmislite o tome svaki zahtjev drugog sloja treba zamotati u tako poseban tip - ako su svi različiti za vas, kako ih drugačije možete razlikovati? A dodavanje samo 4 bajta ispred je prilično učinkovita metoda. Tako,

invokeWithLayer5#417a57ae query:!X = X;

Ali očito je da će to nakon nekog vremena postati nekakva bakanalija. I stiglo je rješenje:

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

hura! Nakon 9 verzija, konačno smo došli do onoga što se u internetskim protokolima radilo još 80-ih - dogovaranje verzije jednom na početku povezivanja!

Pa što je sljedeće?..

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

Ali sada se još uvijek možete smijati. Tek nakon još 9 slojeva, konačno je dodan univerzalni konstruktor s brojem verzije, koji se mora pozvati samo jednom na početku povezivanja, a smisao slojeva kao da je nestao, sada je to samo uvjetna verzija, npr. bilo gdje. Problem riješen.

Točno?..

Vasily, [16.07.18 14:01] Čak sam i u petak pomislio:
Teleserver šalje događaje bez zahtjeva. Zahtjevi moraju biti omotani u InvokeWithLayer. Poslužitelj ne omotava ažuriranja; ne postoji struktura za omotavanje odgovora i ažuriranja.

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

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

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

Vadim Goncharov, [16.07.18 14:02] što bi u biti trebalo značiti dogovor o sloju na početku sesije

Usput, iz toga slijedi da nije predviđena niža verzija klijenta

Ažuriranja, tj. tip Updates u shemi, to je ono što poslužitelj šalje klijentu ne kao odgovor na API zahtjev, već neovisno kada se dogodi događaj. Ovo je složena tema o kojoj ćemo raspravljati u drugom postu, ali za sada je važno znati da poslužitelj sprema ažuriranja čak i kada je klijent izvan mreže.

Stoga, ako odbijete zamotati svaki paket za označavanje njegove verzije, to logično dovodi do sljedećih mogućih problema:

  • poslužitelj šalje ažuriranja klijentu čak i prije nego što je klijent obavijestio koju verziju podržava
  • što trebam učiniti nakon nadogradnje klijenta?
  • koji jamstvada se mišljenje poslužitelja o broju sloja neće promijeniti tijekom procesa?

Mislite li da je ovo čisto teoretsko nagađanje, au praksi se to ne može dogoditi, jer je poslužitelj ispravno napisan (barem je dobro testiran)? Ha! Ma kako je!

Upravo na to smo naletjeli u kolovozu. 14. kolovoza pojavile su se poruke da se nešto ažurira na Telegram serverima... pa 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 (dobro, u isto vrijeme je ispravljeno zapisivanje). Uostalom, ako nešto nije prepoznato u vašem TL-u, to je binarno prema potpisu, dalje niz liniju SVE ide, dekodiranje će postati nemoguće. Što učiniti u takvoj situaciji?

Pa, prvo što svakome padne na pamet je prekinuti vezu i pokušati ponovo. Nije pomoglo. Guglamo CRC32 - pokazalo se da su to objekti iz sheme 73, iako smo radili na 82. Pažljivo gledamo zapise - postoje identifikatori iz dvije različite sheme!

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

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

Google je pokazao da se sličan problem već dogodio jednom od neslužbenih klijenata, no tada su brojevi verzija, a shodno tome i pretpostavke, bili drugačiji...

Dakle, što da radimo? Vasily i ja smo se razdvojili: on je pokušao ažurirati krug na 91, ja sam odlučio pričekati nekoliko dana i isprobati 73. Obje su metode radile, ali budući da su empirijske, nema razumijevanja koliko vam je verzija gore ili niže potrebno skočiti ili koliko dugo trebate čekati .

Kasnije sam uspio reproducirati situaciju: pokrenemo klijenta, isključimo ga, ponovno kompajliramo krug na drugi sloj, ponovno pokrenemo, ponovno uhvatimo problem, vratimo se na prethodni - ups, nema količine prebacivanja kruga i klijent se ponovno pokreće na neko vrijeme pomoći će nekoliko minuta. Primit ćete mješavinu struktura podataka iz različitih slojeva.

Obrazloženje? Kao što možete pogoditi iz raznih neizravnih simptoma, poslužitelj se sastoji od mnogih procesa različitih vrsta na različitim strojevima. Najvjerojatnije je poslužitelj koji je zadužen za “buffering” stavio u red čekanja ono što su mu nadređeni dali, a oni su to dali u shemi koja je bila na snazi ​​u trenutku generiranja. I dok ovaj red nije “istrunuo”, tu se ništa nije moglo učiniti.

Možda... ali ovo je užasna štaka?!.. Ne, prije razmišljanja o ludim idejama, pogledajmo šifru službenih klijenata. U Android verziji ne nalazimo nikakav TL parser, ali nalazimo pozamašnu datoteku (GitHub je odbija popraviti) s (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, vjerojatno, ovo je generirani kod, onda u redu?.. Ali sigurno podržava sve verzije! Istina, nije jasno zašto se sve miješa, tajni razgovori i svašta _old7 nekako ne izgledaju kao strojna generacija... Ipak, najviše me od svega oduševio

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Ljudi, zar ne možete odlučiti što je unutar jednog sloja?! Dobro, dobro, recimo "dvojka" je puštena s greškom, dobro, događa se, ali TRI?.. Odmah, opet isti rake? Kakva je ovo pornografija, izvinite?..

Usput, u izvornom kodu Telegram Desktopa događa se slična stvar - ako je tako, nekoliko uzastopnih obveza na shemu ne mijenja njezin broj sloja, već nešto popravlja. U uvjetima kada ne postoji službeni izvor podataka za shemu, odakle se isti mogu dobiti, osim izvornog koda službenog klijenta? A ako uzmete odatle, ne možete biti sigurni da je shema potpuno točna dok ne testirate sve metode.

Kako se to 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 stvoren" sugerira da je samo dio ove datoteke napisan ručno (možete li zamisliti cijelu noćnu moru održavanja?), a ostatak je strojno generiran. No, onda se postavlja drugo pitanje - da li su izvori dostupni ne potpuno (a la GPL blobs u Linux kernelu), ali to je već tema za drugi dio.

Ali dovoljno. Prijeđimo na protokol na kojem se pokreće sva ova serijalizacija.

MT Proto

Dakle, otvorimo Opći opis и detaljan opis protokola a prvo o što se spotaknemo je terminologija. I to s obiljem svega. Općenito, čini se da je ovo vlasnička značajka Telegrama - različito nazivanje stvari na različitim mjestima ili različite stvari jednom riječju ili obrnuto (na primjer, u API-ju visoke razine, ako vidite paket naljepnica, to nije što ste mislili).

Na primjer, "poruka" i "sesija" ovdje znače nešto drugačije nego u uobičajenom klijentskom sučelju Telegrama. Pa, s porukom je sve jasno, može se tumačiti OOP terminima, ili jednostavno nazvati riječju "paket" - ovo je niska, transportna razina, nema istih poruka kao u sučelju, ima mnogo servisnih poruka . Ali seansa... ali prvo o svemu.

transportni sloj

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

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

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

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ćena
  • Srednji
  • Podstavljeni intermedijer
  • puni

Pa, ok, podstavljeni posrednik za MTProxy, ovo je kasnije dodano zbog dobro poznatih događaja. Ali zašto još dvije verzije (ukupno tri) kada biste mogli proći s jednom? Sva četiri se bitno razlikuju samo u načinu postavljanja duljine i nosivosti glavnog MTProtoa, o čemu ćemo dalje raspravljati:

  • u skraćenom je 1 ili 4 bajta, ali ne 0xef, zatim tijelo
  • u srednjem to je 4 bajta duljine i polje, a prvi put klijent mora poslati 0xeeeeeeee kako bi se naznačilo da je posredni
  • u potpunosti najzarazniji, s točke gledišta mrežnika: duljina, redni broj, a NE ONAJ koji je uglavnom MTProto, tijelo, CRC32. Da, sve je ovo povrh TCP-a. Što nam pruža pouzdan prijenos u obliku sekvencijalnog toka bajtova; nisu potrebne sekvence, pogotovo kontrolni zbrojevi. Dobro, sad će mi netko prigovoriti da TCP ima 16-bitni kontrolni zbroj, pa dolazi do kvarenja podataka. Sjajno, ali mi zapravo imamo kriptografski protokol s hashovima duljim od 16 bajtova, sve te pogreške - pa čak i više - bit će uhvaćene neusklađenošću SHA na višoj razini. Nema smisla u CRC32 povrh ovoga.

Usporedimo Skraćeni, u kojem je moguć jedan bajt duljine, sa Srednjim, koji opravdava “U slučaju da je potrebno usklađivanje podataka od 4 bajta,” što je prava besmislica. Što, vjeruje se da su programeri Telegrama toliko nesposobni da ne mogu čitati podatke iz utičnice u usklađeni međuspremnik? To ipak morate učiniti, jer vam čitanje može vratiti koliko god bajtova (a postoje i proxy poslužitelji, na primjer...). Ili, s druge strane, zašto blokirati Abridged ako ćemo i dalje imati pozamašno punjenje povrh 16 bajtova - uštedite 3 bajta ponekad ?

Stječe se dojam da Nikolai Durov jako voli ponovno izumljivati ​​kotače, uključujući mrežne protokole, bez ikakve stvarne praktične potrebe.

Ostale mogućnosti prijevoza, uklj. Web i MTProxy, nećemo sada razmatrati, možda u drugom postu, ako bude zahtjeva. O tom istom MTProxyju, sjetimo se sada samo da su ubrzo nakon izlaska 2018. pružatelji brzo naučili blokirati ga, namijenjen blokiranje premosnicePo Veličina Paketa! A također i činjenica da je MTProxy poslužitelj napisan (opet od strane Waltmana) u C-u bio previše vezan za specifičnosti Linuxa, iako to uopće nije bilo potrebno (Phil Kulin će potvrditi), te da bi sličan poslužitelj bilo u Go ili Node.js stane u manje od sto redaka.

No zaključke o tehničkoj pismenosti tih ljudi izvest ćemo na kraju odjeljka, nakon razmatranja drugih pitanja. Za sada prijeđimo na OSI sloj 5, session – na koji su postavili MTProto session.

Ključevi, poruke, sesije, Diffie-Hellman

Tu su ga smjestili ne sasvim ispravno... Sesija nije ista sesija koja je vidljiva u sučelju pod Aktivne sesije. Ali po redu.

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

Tako smo od transportnog sloja primili niz bajtova poznate duljine. Ovo je ili šifrirana poruka ili čisti tekst - ako smo još u fazi dogovora o ključu i to zapravo radimo. O kojem od hrpe pojmova koji se nazivaju "ključ" govorimo? Razjasnimo ovo pitanje samoj ekipi Telegrama (ispričavam se što sam preveo vlastitu dokumentaciju s engleskog s umornim mozgom u 4 ujutro, lakše je bilo ostaviti neke fraze kakve jesu):

Postoje dva entiteta tzv Sjednica - jedan u korisničkom sučelju službenih klijenata pod “trenutačnim sesijama”, gdje svaka sesija odgovara cijelom uređaju/OS-u.
Drugi - MTProto sesija, koji u sebi ima redni broj poruke (u smislu niske razine), i koji može trajati između različitih TCP veza. Nekoliko MTProto sesija može se instalirati u isto vrijeme, na primjer, kako bi se ubrzalo preuzimanje datoteka.

Između ove dvojice sjednice postoji koncept autorizacija. U degeneriranom slučaju to možemo reći UI sesija je isto što i autorizacija, ali nažalost, sve je komplicirano. Pogledajmo:

  • Korisnik na novom uređaju prvo generira aut_key i veže ga za račun, na primjer putem SMS-a - eto zašto autorizacija
  • To se dogodilo unutar prve MTProto sesija, koji ima session_id unutar sebe.
  • U ovom koraku kombinacija autorizacija и session_id moglo se nazvati primjer - ova riječ se pojavljuje u dokumentaciji i kodu nekih klijenata
  • Zatim, klijent može otvoriti više MTProto sesije pod istim aut_key - u isti DC.
  • Zatim će jednog dana klijent morati zatražiti datoteku od drugi DC - i za ovaj DC će se generirati novi aut_key !
  • Obavijestiti sustav da se ne prijavljuje novi korisnik, već isti autorizacija (UI sesija), klijent koristi API pozive auth.exportAuthorization u domu DC auth.importAuthorization u novom DC-u.
  • Sve je isto, nekoliko ih može biti otvoreno MTProto sesije (svako sa svojim session_id) na ovaj novi DC, pod njegov aut_key.
  • Konačno, klijent može htjeti Perfect Forward Secrecy. Svaki aut_key to je bio trajan ključ - po DC - i klijent može nazvati auth.bindTempAuthKey za upotrebu privremen aut_key - i opet samo jedan temp_auth_key po DC, zajednički za sve MTProto sesije ovom DC-u.

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

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

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

Dakle, generirajmo auth_key na Diffie-Hellman verzije iz Telegrama. Pokušajmo razumjeti dokumentaciju...

Vasily, [19.06.18 20:05] data_with_hash := SHA1(podaci) + podaci + (bilo koji nasumični bajtovi); tako da je duljina jednaka 255 bajtova;
šifrirani_podaci := RSA(podaci_s_hashom, javni_ključ_poslužitelja); broj od 255 bajta (big endian) se podiže na potrebnu snagu nad potrebnim modulom, a rezultat se pohranjuje kao broj od 256 bajta.

Imaju nešto droge DH

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

Eto, na kraju se to riješilo, ali je ostao talog - dokaz o radu je naručitelj da je uspio faktorizirati broj. Vrsta zaštite od DoS napada. A RSA ključ se koristi samo jednom u jednom smjeru, uglavnom za enkripciju new_nonce. No iako će ova naizgled jednostavna operacija uspjeti, s čime ćete se morati suočiti?

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

Poslao sam ovaj zahtjev DH-u

A u transportnom docku piše da može odgovoriti s 4 bajta koda pogreške. To je sve

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

Pa sam mu rekao: "Uhvati svoje sranje šifrirano poslužiteljskim ključem s ovakvim otiskom prsta, želim DH", a on je odgovorio s glupim 404

Što mislite o ovom odgovoru poslužitelja? Što uraditi? Nema se koga pitati (ali o tome u drugom dijelu).

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

Nemam što drugo raditi, samo sam sanjao pretvaranje brojeva naprijed-natrag

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

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

Vadim Goncharov, [20.06.18 15:49] i zbog ovoga 404?

Vasily, [20.06.18 15:49] DA!

Vadim Goncharov, [20.06.18 15:50] pa ne razumijem što on može "nije našao"

Vasilij, [20.06.18 15:50] oko

Nisam mogao pronaći takvu dekompoziciju na proste faktore%)

Nismo čak ni uspjeli prijaviti pogreške

Vasily, [20.06.18 20:18] Oh, tu je i MD5. Već tri različita hasha

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

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

SHA1 i sha2

Pa stavimo to auth_key dobili smo veličinu od 2048 bita koristeći Diffie-Hellman. Što je sljedeće? Zatim otkrivamo da se donja 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 s poslužiteljem. Uspostavljen je analog TLS sesije, što je vrlo skup postupak. Ali poslužitelj još uvijek ne zna ništa o tome tko smo mi! Ne još, zapravo. autorizacija. Oni. ako ste razmišljali u terminima "login-password", kao što ste nekada radili u ICQ-u, ili barem "login-key", kao u SSH (na primjer, na nekom gitlabu/githubu). Dobili smo anonimnu. Što ako nam poslužitelj kaže "ove telefonske brojeve servisira drugi DC"? Ili čak "vaš telefonski broj je zabranjen"? Najbolje što možemo učiniti je zadržati ključ u nadi da će biti koristan i da se do tada neće pokvariti.

Inače, “primili” smo ga s rezervom. Na primjer, vjerujemo li poslužitelju? Što ako je lažno? Bit će potrebne kriptografske provjere:

Vasily, [21.06.18 17:53] Mobilnim klijentima nude provjeru primalnosti 2kbit broja%)

Ali uopće nije jasno, nafeijoa

Vasily, [21.06.18 18:02] Dokument ne kaže što učiniti ako se pokaže da nije jednostavno

Nije rečeno. Da vidimo što službeni Android klijent radi u ovom slučaju? A to je što (i da, cijeli fajl je zanimljiv) - kako kažu, ostavit ću ovo ovdje:

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

Ne, naravno da je još tu neki Postoje testovi za primalnost broja, ali ja osobno više nemam dovoljno znanja iz matematike.

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

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

Vasily, [22.06.18 14:08] Prosječno, kučko, malo

dobio auth_key. Svi. Izvan njih... nije jasno iz dokumenta. Slobodno proučite otvoreni kod.

Imajte na umu da MTProto 2.0 zahtijeva od 12 do 1024 bajta ispune, još uvijek podložno uvjetu da duljina rezultirajuće poruke bude djeljiva sa 16 bajtova.

Dakle, koliko podstava trebate dodati?

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

Ako je netko pažljivo proučavao 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-ovima... Ovdje je, kao, sam ključ poruke također SHA hash dekriptiranih podataka, koji se koristi za provjeru integriteta - i u slučaju nepodudaranja, dokumentacija iz nekog razloga preporučuje tiho ignoriranje (ali što je sa sigurnošću, što ako nas slome?).

Nisam kriptograf, možda nema ništa loše u ovom načinu rada u ovom slučaju s teorijske točke gledišta. Ali mogu jasno navesti praktični problem, koristeći Telegram Desktop kao primjer. Šifrira lokalnu predmemoriju (svi ovi D877F783D5D3EF8C) na isti način kao poruke u MTProto (samo u ovom slučaju verzija 1.0), tj. prvo ključ poruke, zatim sami podaci (i negdje sa strane glavni veliki auth_key 256 bajtova, bez kojih msg_key beskoristan). Dakle, problem postaje vidljiv na velikim datotekama. Naime, trebate čuvati dvije kopije podataka – šifriranu i dekriptiranu. A ako postoje megabajti, ili streaming video, na primjer?.. Klasične sheme s MAC-om nakon šifriranog teksta omogućuju vam da ga čitate u toku, odmah ga odašilju. Ali s MTProto ćete morati na prvi šifrirati ili dekriptirati cijelu poruku, tek onda je prenijeti na mrežu ili na disk. Stoga se u najnovijim verzijama Telegram Desktop u predmemoriju u user_data Također se koristi i drugi format - s AES u CTR modu.

Vasily, [21.06.18 01:27] Oh, otkrio sam što je IGE: IGE je bio prvi pokušaj "načina šifriranja s provjerom autentičnosti", izvorno za Kerberos. Bio je to neuspjeli pokušaj (ne pruža zaštitu integriteta) i morao se ukloniti. To je bio početak 20-godišnje potrage za autentifikacijskim načinom šifriranja koji funkcionira, što je nedavno kulminiralo u načinima poput OCB i GCM.

A sada argumenti sa strane košarice:

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

To je zabavno. Dvije godine na nižoj razini

Ili jednostavno možete uzeti tls

U redu, recimo da smo napravili enkripciju i ostale nijanse. Je li konačno moguće slati zahtjeve serijalizirane u TL i deserijalizirati odgovore? Dakle, što i kako trebate poslati? Evo, recimo, metode initConnection, možda je to to?

Vasily, [25.06.18 18:46] Inicijalizira vezu i sprema podatke na korisnikov uređaj i aplikaciju.

Prihvaća app_id, device_model, system_version, app_version i lang_code.

I neki upit

Dokumentacija kao i uvijek. Slobodno proučavajte otvoreni kod

Ako je sve bilo približno jasno s invokeWithLayer, što onda nije u redu? Ispostavilo se da, recimo, imamo - klijent je već imao nešto pitati poslužitelja - 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 može biti zaseban poziv, nego mora biti omotač? Da, kako se pokazalo, to se mora učiniti svaki put na početku svake sesije, a ne jednom, kao s glavnim ključem. Ali! Ne može ga pozvati neovlašteni korisnik! Sada smo došli do faze u kojoj je primjenjiv Ovaj stranica s dokumentacijom - 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.prijava
  • auth.prijava
  • auth.importAuthorization
  • pomoć.getConfig
  • pomoć.getNearestDc
  • pomoć.getAppUpdate
  • pomoć.getCdnConfig
  • langpack.getLangPack
  • langpack.getStrings
  • langpack.getDifference
  • langpack.getLanguages
  • langpack.getLanguage

Prvi od njih, auth.sendCode, a tu je i onaj dragi prvi zahtjev u kojem šaljemo api_id i api_hash, a nakon čega nam stiže SMS s kodom. A ako smo u krivom DC-u (telefonske brojeve u ovoj zemlji opslužuje druga, na primjer), tada ćemo dobiti pogrešku s brojem željenog DC-a. Da biste saznali na koju IP adresu prema DC broju se trebate spojiti, pomozite nam help.getConfig. Nekada je bilo samo 5 prijava, ali nakon poznatih događaja 2018. broj se znatno povećao.

Prisjetimo se da smo do ove faze na poslužitelju došli anonimno. Nije li preskupo dobiti samo IP adresu? Zašto ne izvršiti ovu i druge operacije u nešifriranom dijelu MTProto? Čujem prigovor: "Kako možemo biti sigurni da RKN nije taj koji će odgovoriti s lažnim adresama?" Na ovo se sjećamo da su, općenito, službeni klijenti RSA ključevi su ugrađeni, tj. možete li samo подписать ova informacija. Zapravo, to se već radi za informacije o zaobilaženju blokiranja koje klijenti dobivaju preko drugih kanala (logično, to se ne može učiniti u samom MTProto; također morate znati gdje se spojiti).

U REDU. U ovoj fazi autorizacije klijenta još nismo autorizirani i nismo registrirali našu aplikaciju. Za sada samo želimo vidjeti što poslužitelj reagira na metode koje su dostupne neovlaštenom 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 shemi, prvi dolazi drugi

U tdesktop shemi treća vrijednost je

Da, od tada je, naravno, dokumentacija ažurirana. Iako bi uskoro moglo opet postati nebitno. Kako programer početnik treba znati? Možda ako registrirate svoju prijavu, oni će vas obavijestiti? Vasily 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ću razinu, a nešto ste propustili 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 obje domene, kao i malo strukture omotača

Ali stalno miješaju različite razine stoga, tako da nije uvijek jasno gdje je mtproto završio, a sljedeća razina počela

Kako se miješaju? Pa, ovdje je isti privremeni ključ za PFS, na primjer (usput, Telegram Desktop to ne može). Izvršava se API zahtjevom auth.bindTempAuthKey, tj. s najviše razine. Ali u isto vrijeme ometa enkripciju na nižoj razini - nakon nje, na primjer, morate to učiniti ponovno 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 omogućuje promjenu ključa barem u svakoj poruci, te da poslužitelj ima pravo “zaboraviti” privremeni ključ u bilo kojem trenutku - dokumentacija ne kaže što učiniti u ovom slučaju... pa, zašto bi mogao Zar nemate nekoliko ključeva, kao kod seta budućih soli, i?..

Postoji još nekoliko stvari koje vrijedi spomenuti o MTProto temi.

Poruke s porukama, msg_id, msg_seqno, potvrde, pingovi u krivom smjeru i druge idiosinkrazije

Zašto trebate znati o njima? Jer oni "cure" na višu razinu i morate ih biti svjesni kada radite s API-jem. Pretpostavimo da nas ne zanima msg_key; niža razina nam je sve dešifrirala. Ali unutar dešifriranih podataka imamo sljedeća polja (također duljinu podataka, tako da znamo gdje je padding, ali to nije važno):

  • sol - int64
  • session_id - int64
  • ID_poruke — 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 valjani, ali i zato što ako vam je sol “pokvarena”, onda će se poruka (zahtjev) jednostavno izgubiti. Poslužitelj će, naravno, prijaviti novu sol izdavanjem new_session_created - ali sa starim ćete ga morati nekako ponovno poslati npr. I ovaj problem utječe na arhitekturu aplikacije.

Poslužitelju je dopušteno potpuno prekinuti sesije i odgovoriti na ovaj način iz mnogo razloga. Zapravo, što je MTProto sesija sa strane klijenta? Ovo su dva broja session_id и seq_no poruka unutar ove sesije. Pa, i temeljna TCP veza, naravno. Recimo da naš klijent još uvijek ne zna kako raditi mnoge stvari, isključio se, ponovno spojio. Ako se to dogodilo brzo - stara sesija nastavila se u novoj TCP vezi, povećajte seq_no unaprijediti. Ako dugo traje, server bi ga mogao obrisati, jer je sa svoje strane također red čekanja, kako doznajemo.

Što bi trebalo biti seq_no? Oh, to je škakljivo pitanje. Pokušajte iskreno razumjeti što se mislilo:

Poruka vezana uz sadržaj

Poruka koja zahtijeva izričitu potvrdu. To uključuje sve korisničke i mnoge servisne poruke, gotovo sve s izuzetkom spremnika i potvrda.

Redni broj poruke (msg_seqno)

32-bitni broj jednak dvostrukom broju poruka "povezanih sa sadržajem" (onih koje zahtijevaju potvrdu, a posebno onih koje nisu spremnici) koje je stvorio pošiljatelj prije ove poruke i naknadno uvećan za jedan ako je trenutna poruka poruka vezana uz sadržaj. Spremnik se uvijek generira nakon cijelog sadržaja; stoga je njegov redni broj veći ili jednak sekvencijskim brojevima poruka sadržanih u njemu.

Kakav je ovo cirkus s povećanjem za 1, pa još jednom za 2?.. Sumnjam da su u početku mislili “najmanji bit za ACK, ostalo je broj”, ali rezultat nije baš isti - posebno, izlazi, može se poslati više potvrde koje imaju iste seq_no! Kako? Pa, na primjer, poslužitelj nam nešto šalje, šalje, a mi sami šutimo, samo odgovaramo servisnim porukama kojima potvrđujemo primitak njegovih poruka. U tom će slučaju naše odlazne potvrde imati isti odlazni broj. Ako ste upoznati s TCP-om i mislite da ovo zvuči nekako divlje, ali čini se da nije baš divlje, jer u TCP-u seq_no ne mijenja, ali ide potvrda seq_no s druge strane, požurit ću vas uznemiriti. Potvrde su dostupne u MTProto NE na seq_no, kao u TCP-u, ali prema msg_id !

Što je to msg_id, najvažnije od ovih polja? Jedinstveni identifikator poruke, kao što naziv sugerira. Definiran je kao 64-bitni broj, čiji najniži bitovi opet imaju magiju "poslužitelj-ne-poslužitelj", a ostatak je Unix vremenska oznaka, uključujući razlomak, pomaknut 32 bita ulijevo. Oni. timestamp per se (i poruke s vremenima koja se previše razlikuju bit će odbačene od strane poslužitelja). Iz ovoga ispada da je općenito ovo identifikator koji je globalan za klijenta. S obzirom na to – prisjetimo se session_id - zajamčeno nam je: Ni pod kojim uvjetima poruka namijenjena jednoj sesiji ne može se poslati u drugu sesiju. Odnosno, ispada da već postoji tri razina - sesija, broj sesije, ID poruke. Čemu takvo prekompliciranje, taj misterij je vrlo velik.

Dakle, msg_id potrebno za...

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

Kao što ste možda primijetili, nigdje u dijagramu nema posebne vrste ili funkcije "napravi RPC zahtjev", iako postoje odgovori. Uostalom, imamo poruke vezane uz sadržaj! To je, bilo koji poruka bi mogla biti zahtjev! Ili ne biti. Nakon svega, svaki tu je msg_id. Ali postoje odgovori:

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

Ovdje je naznačeno na koju poruku je ovo odgovor. Stoga ćete na najvišoj razini API-ja morati zapamtiti koji je bio broj vašeg zahtjeva - mislim da nema potrebe objašnjavati da je rad asinkroni, i može biti nekoliko zahtjeva u tijeku u isto vrijeme, na koje se odgovori mogu vratiti bilo kojim redoslijedom? U principu, iz ovoga i poruka o pogrešci kao što nema radnika, može se pratiti arhitektura koja stoji iza ovoga: poslužitelj koji održava TCP vezu s vama je front-end balanser, on prosljeđuje zahtjeve pozadinama i prikuplja ih natrag putem message_id. Čini se da je ovdje sve jasno, logično i dobro.

Da?.. A ako bolje razmislite? Uostalom, i sam RPC odgovor također ima polje msg_id! Moramo li viknuti poslužitelju "ne odgovaraš na moj odgovor!"? I da, što je bilo s potvrdama? O stranici poruke o porukama govori nam što jest

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

i to mora učiniti svaka strana. Ali ne uvijek! Ako ste primili RpcResult, on sam služi kao potvrda. To jest, poslužitelj može odgovoriti na vaš zahtjev s MsgsAck - poput, "Primio sam." RpcResult može odmah odgovoriti. Moglo bi biti oboje.

I da, još morate odgovoriti na odgovor! Potvrda. U suprotnom, poslužitelj će smatrati da se ne može isporučiti i ponovno vam ga poslati natrag. Čak i nakon ponovnog povezivanja. Ali ovdje se, naravno, postavlja pitanje timeouta. Pogledajmo ih malo kasnije.

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

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

Joj, uzviknut će netko, evo humanijeg formata – postoji linija! Uzmite si vremena. Ovdje popis 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 ovako CAPITAL_LETTERS_AND_NUMBERS. Na primjer, PHONE_NUMBER_OCCUPIED ili FILE_PART_H_MISSING. Pa, to jest, i dalje ćete trebati ovu liniju raščlaniti, Na primjer FLOOD_WAIT_3600 to će značiti da morate čekati sat vremena, i PHONE_MIGRATE_5, da telefonski broj s ovim prefiksom mora biti prijavljen u 5. DC. Imamo tipski jezik, zar ne? Ne treba nam argument iz niza, obični će poslužiti, u redu.

Opet, ovo se ne nalazi na stranici servisnih poruka, ali kao što je već uobičajeno u ovom projektu, informacije se mogu pronaći na drugoj stranici dokumentacije, ili baciti sumnju. Prvo, pogledajte, povreda tipkanja/sloja - RpcError može se ugnijezditi RpcResult. Zašto ne vani? Što nismo uzeli u obzir?.. Prema tome, gdje je garancija da RpcError NE smije se ugrađivati ​​u RpcResult, ali biti izravno ili ugniježđen u drugu vrstu?.. A ako ne može, zašto nije na najvišoj razini, tj. nedostaje ga req_msg_id ? ..

No, nastavimo s servisnim porukama. Klijent može pomisliti da poslužitelj 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 ponovno presijecaju s mehanizmom potvrde; pokušaj razumijevanja što bi oni trebali biti (i koji je opći popis tipova koji ne zahtijevaju potvrdu) ostavljen je čitatelju kao domaća zadaća (napomena: informacije u izvorni kod Telegram Desktopa nije potpun).

Ovisnost o drogama: statusi poruka

Općenito, mnoga mjesta u TL, MTProto i općenito Telegramu ostavljaju osjećaj tvrdoglavosti, ali zbog pristojnosti, takta i dr. meke vještine O tome smo pristojno šutjeli, a psovke u dijalozima cenzurirali. Međutim, ovo mjestoОvećina stranice je o poruke o porukama Šokantno je čak i za mene, koji već dugo radim s mrežnim protokolima i vidio sam bicikle različitih stupnjeva krivudavosti.

Počinje bezazleno, s potvrdama. Dalje nam govore o

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

Pa, svatko tko počne raditi s MTProto morat će se suočiti s njima; u ciklusu "ispravljeno - ponovno kompajlirano - pokrenuto" dobivanje brojčanih pogrešaka ili soli koja se uspjela pokvariti tijekom uređivanja je uobičajena stvar. Međutim, ovdje postoje dvije točke:

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

U dokumentaciji stoji:

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

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 informacije o statusu svojih odlaznih poruka neko vrijeme, može to izričito zatražiti od druge strane:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Informativna poruka o statusu poruka
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Ovdje, info je niz koji sadrži točno jedan bajt statusa poruke za svaku poruku s 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 primitka)
    • +8 = poruka je već potvrđena
    • +16 = poruka koja ne zahtijeva potvrdu
    • +32 = RPC upit sadržan u poruci koja se obrađuje ili je obrada već dovršena
    • +64 = odgovor na poruku vezan uz sadržaj već je generiran
    • +128 = druga strana sigurno zna da je poruka već primljena
      Za ovaj odgovor nije potrebna potvrda. To je samo po sebi potvrda relevantnog msgs_state_req.
      Imajte na umu da ako se iznenada ispostavi da druga strana nema poruku koja izgleda kao da joj je poslana, poruka se jednostavno može ponovno poslati. Čak i ako druga strana primi dvije kopije poruke u isto vrijeme, duplikat će biti zanemaren. (Ako je prošlo previše vremena i izvorni msg_id više nije važeći, poruku treba zamotati 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širena dobrovoljna komunikacija 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 odgovori na tražene poruke […]
  • Kopije poruka
    U nekim situacijama potrebno je ponovno poslati staru poruku s msg_id-om koji više nije valjan. Zatim se zamota u spremnik za kopiranje:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Jednom primljena poruka se obrađuje kao da omot nije tu. Međutim, ako se sa sigurnošću zna da je poruka orig_message.msg_id primljena, tada se nova poruka ne obrađuje (a istovremeno se ona i orig_message.msg_id potvrđuju). Vrijednost orig_message.msg_id mora biti niža od msg_id spremnika.

Čak i prešutimo što msgs_state_info opet strše uši nedovršenog TL-a (trebao nam je vektor bajtova, i u niža dva bita je bio enum, a u viša dva bita su bile zastavice). Poanta je drugačija. Jel ikome jasno zašto je sve to u praksi? u stvarnom klijentu potrebno?.. S poteškoćama, ali može se zamisliti neka korist ako se osoba bavi otklanjanjem pogrešaka, i to u interaktivnom načinu - pitajte poslužitelja što i kako. Ali ovdje su opisani zahtjevi povratno putovanje.

Iz toga slijedi da svaka strana mora ne samo šifrirati i slati poruke, već i pohraniti podatke o sebi, o odgovorima na njih, na nepoznato vrijeme. Dokumentacija ne opisuje niti vremena niti praktičnu primjenjivost ovih značajki. ni na koji način. Ono što je najnevjerojatnije je da se zapravo koriste u kodu službenih klijenata! Očito im je rečeno nešto što nije ušlo u javnu dokumentaciju. Razumjeti iz šifre zašto, više nije tako jednostavan kao u slučaju TL-a - to nije (relativno) logički izolirani dio, već dio vezan uz arhitekturu aplikacije, tj. zahtijevat će znatno više vremena za razumijevanje koda aplikacije.

Pingovi i vremena. Redovi čekanja.

Iz svega, ako se sjetimo nagađanja o arhitekturi poslužitelja (distribucija zahtjeva po backendovima), slijedi jedna prilično tužna stvar - unatoč 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 se pojavi problem), da potvrde u samom MTProto-u - nema garancija. Poslužitelj može lako izgubiti ili izbaciti vašu poruku, a tu se ništa ne može učiniti, samo se koriste različite vrste štaka.

I prije svega - redovi poruka. Pa, s jednom stvari sve je bilo očito od samog početka - nepotvrđena poruka mora biti pohranjena i ponovno poslana. I nakon koliko vremena? I šaljivdžija ga poznaje. Možda te ovisne servisne poruke nekako uz pomoć štaka riješe ovaj problem, recimo u Telegram Desktopu im odgovaraju oko 4 reda čekanja (možda i više, kao što je već spomenuto, za to treba ozbiljnije proniknuti u njegov kod i arhitekturu; pritom vremena, znamo da se ne može uzeti kao uzorak, određeni broj tipova iz MTProto sheme se u njemu ne koristi).

Zašto se ovo događa? Vjerojatno programeri poslužitelja nisu mogli osigurati pouzdanost unutar klastera, pa čak ni međuspremnik na prednjem balanseru, pa su ovaj problem prenijeli na klijenta. Iz očaja, Vasily je pokušao implementirati alternativnu opciju, sa samo dva reda čekanja, koristeći algoritme iz TCP-a - mjerenje RTT-a prema poslužitelju i prilagođavanje veličine "prozora" (u porukama) ovisno o broju nepotvrđenih zahtjeva. To jest, takva gruba heuristika za procjenu opterećenja poslužitelja je koliko naših zahtjeva može žvakati u isto vrijeme i ne izgubiti.

Pa, to jest, razumijete, zar ne? Ako morate ponovno implementirati TCP povrh protokola koji radi preko TCP-a, to znači da je protokol vrlo loše dizajniran.

O da, zašto vam treba više od jednog reda i što to uopće znači za osobu koja radi s API-jem visoke razine? Gledajte, napravite zahtjev, serijalizirate ga, ali često ga ne možete poslati odmah. Zašto? Jer odgovor će biti msg_id, što je privremenoаJa sam oznaka čiju je dodjelu najbolje odgoditi za što kasnije - u slučaju da je poslužitelj odbije zbog neusklađenosti vremena između nas i njega (naravno, možemo napraviti štaku koja nam pomiče vrijeme iz sadašnjosti poslužitelju dodavanjem delte izračunate iz odgovora poslužitelja - službeni klijenti to rade, ali je grubo i netočno zbog međuspremnika). Stoga, kada uputite zahtjev s lokalnim pozivom funkcije iz knjižnice, poruka prolazi kroz sljedeće faze:

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

Možda, korištenje kontejnera moglo bi djelomično riješiti problem. To je kada se hrpa poruka spakira u jednu, a server odgovori potvrdom na sve odjednom, u jednoj msg_id. Ali on će također odbiti ovaj paket, ako nešto pođe po zlu, u cijelosti.

I u ovom trenutku netehnička razmatranja stupaju na scenu. Iz iskustva smo vidjeli mnoge štake, a osim toga, sada ćemo vidjeti još primjera loših savjeta i arhitekture - isplati li se u takvim uvjetima vjerovati i donositi takve odluke? Pitanje je retoričko (naravno da nije).

O čemu pričamo? Ako na temu “droga poruka o porukama” još možete spekulirati prigovorima poput “glup si, nisi razumio naš briljantni plan!” (dakle prvo napišite dokumentaciju, kako normalni ljudi trebaju, s obrazloženjem i primjerima razmjene paketa, pa ćemo pričati), onda tajmingi/timeouti su čisto praktično i specifično pitanje, ovdje se sve odavno zna. Što nam dokumentacija govori o vremenskim ograničenjima?

Poslužitelj obično potvrđuje primitak poruke od klijenta (obično, RPC upit) koristeći RPC odgovor. Ako odgovor dugo čeka, poslužitelj može prvo poslati potvrdu primitka, a nešto kasnije i sam RPC odgovor.

Klijent obično potvrđuje primitak poruke od poslužitelja (obično, RPC odgovor) dodavanjem potvrde sljedećem RPC upitu ako nije poslan prekasno (ako je generiran, recimo, 60-120 sekundi nakon primitka poruke s poslužitelja). Međutim, ako dulje vrijeme nema razloga za slanje poruka poslužitelju ili ako postoji veliki broj nepotvrđenih poruka s poslužitelja (recimo preko 16), klijent šalje samostalnu potvrdu.

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

A 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, poslužitelj pokreće mjerač vremena koji će zatvoriti trenutnu vezu disconnect_delay nekoliko sekundi kasnije osim ako ne primi novu poruku iste vrste koja automatski resetira sve prethodne mjerače vremena. Ako klijent šalje te pingove svakih 60 sekundi, na primjer, može postaviti disconnect_delay na 75 sekundi.

Jesi li poludio?! Za 60 sekundi vlak će ući u stanicu, iskrcati i pokupiti putnike i opet izgubiti kontakt u tunelu. Za 120 sekundi, dok ga čujete, stići će do drugog, a veza će najvjerojatnije prekinuti. Dobro, jasno je odakle dolaze noge - "čuo sam zvono, ali ne znam gdje je", tu je Naglov algoritam i opcija TCP_NODELAY, namijenjena interaktivnom radu. Ali, oprostite, zadržite zadanu vrijednost - 200 Milisekundi Ako stvarno želite prikazati nešto slično i uštedjeti na mogućih par paketa, onda to odgodite na 5 sekundi, ili koliko god da je sada istek vremena poruke "Korisnik piše...". Ali ne više.

I na kraju, pingovi. Odnosno, provjera živosti TCP veze. Smiješno, ali prije 10-ak godina napisao sam kritički tekst o messengeru studentskog doma našeg fakulteta - i tamo su autori pingali server od klijenta, a ne obrnuto. Ali studenti 3. godine su jedno, a međunarodni ured je drugo, zar ne?..

Prvo, mali edukativni program. TCP veza, u nedostatku razmjene paketa, može živjeti tjednima. To je i dobro i loše, ovisno o namjeni. Dobro je ako ste imali otvorenu SSH vezu s poslužiteljem, ustali ste s računala, ponovno pokrenuli router, vratili se na svoje mjesto - sesija preko ovog poslužitelja nije prekinuta (ništa niste upisali, nije bilo paketa) , zgodno je. Loše je ako postoje tisuće klijenata na poslužitelju, od kojih svaki zauzima resurse (bok, Postgres!), a klijentov host se možda davno ponovno pokrenuo - ali mi o tome nećemo znati.

Chat/IM sustavi spadaju u drugi slučaj iz još jednog razloga - online statusa. Ako je korisnik "otpao", morate o tome obavijestiti njegove sugovornike. U suprotnom, završit ćete s greškom koju su tvorci Jabbera napravili (i ispravljali 20 godina) - korisnik se odspojio, ali mu nastavljaju pisati poruke, vjerujući da je online (koje su se također potpuno izgubile u ovim nekoliko minuta prije nego što je prekid veze otkriven). Ne, opcija TCP_KEEPALIVE, koju mnogi ljudi koji ne razumiju kako rade TCP tajmeri bacaju nasumično (postavljanjem divljih vrijednosti kao što su deseci sekundi), ovdje neće pomoći - morate biti sigurni da ne samo kernel OS-a korisničkog stroja je živ, ali i normalno funkcionira, može odgovoriti, a sama aplikacija (mislite li da se ne može zamrznuti? Telegram Desktop na Ubuntu 18.04 zamrznuo mi se više puta).

Zato morate pingati server klijent, a ne obrnuto - ako klijent to učini, ako se veza prekine, ping neće biti isporučen, cilj neće biti postignut.

Što vidimo na Telegramu? Upravo je suprotno! Pa, to jest. Formalno, naravno, obje strane mogu pingati jedna drugu. U praksi klijenti koriste štaku ping_delay_disconnect, koji postavlja mjerač vremena na poslužitelju. Pa, oprostite, nije na klijentu da odlučuje koliko dugo želi živjeti tamo bez pinga. Poslužitelj, na temelju svog opterećenja, zna bolje. Ali, naravno, ako vam ne smetaju resursi, onda ćete sami biti svoj zli Pinokio, a štaka će poslužiti...

Kako je trebao biti dizajniran?

Vjerujem da gore navedene činjenice jasno pokazuju da tim Telegrama/VKontaktea nije baš kompetentan u području transportne (i niže) razine računalnih mreža i njihove niske kvalifikacije u relevantnim pitanjima.

Zašto je ispalo tako komplicirano i kako se Telegram arhitekti mogu protiviti? To što su pokušali napraviti sesiju koja preživi prekide TCP veze, odnosno ono što sada nije isporučeno, isporučit ćemo kasnije. Vjerojatno su pokušali napraviti i UDP transport, ali su naišli na poteškoće i odustali (zato je dokumentacija prazna - nije se imalo čime pohvaliti). Ali zbog nedostatka razumijevanja kako mreže općenito, a posebno TCP rade, gdje se na njih možete osloniti, a gdje to trebate učiniti sami (i kako), te pokušaja da se to kombinira s kriptografijom “dvije ptice s jedan kamen”, to je rezultat.

Kako je to bilo potrebno? Na temelju činjenice da msg_id je vremenska oznaka neophodna s kriptografske točke gledišta za sprječavanje napada ponavljanja, pogrešno je priložiti joj funkciju jedinstvenog identifikatora. Stoga, bez temeljne promjene trenutne arhitekture (kada se generira tok ažuriranja, to je tema API-ja visoke razine za drugi dio ove serije postova), trebalo bi:

  1. Poslužitelj koji drži TCP vezu s klijentom preuzima odgovornost - ako je pročitao iz utičnice, potvrdite, obradite ili vratite pogreš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). Uvijek smo unutar sesije, zar ne?
  2. Vremenska oznaka za sprječavanje napada ponavljanjem postaje zasebno polje, a la nonce. Provjereno je, ali ne utječe ni na što drugo. Dosta i uint32 - ako se naša sol mijenja barem svakih pola dana, možemo dodijeliti 16 bitova nižim bitovima cijelog broja trenutnog vremena, a ostatak - frakcijskom dijelu sekunde (kao sada).
  3. Uklonjeno msg_id uopće - sa stajališta razlikovanja zahtjeva na pozadini, tu je, prvo, ID klijenta, i drugo, ID sesije, spojite ih. Prema tome, samo jedna stvar je dovoljna kao identifikator zahtjeva seq_no.

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

API?

Bože! Dakle, nakon što smo se borili kroz put pun boli i štaka, konačno smo bili u mogućnosti slati zahtjeve poslužitelju i primati odgovore na njih, kao i primati ažuriranja od poslužitelja (ne kao odgovor na zahtjev, nego on sam šalje nam, like PUSH, ako ikome 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, ne 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 priložene objektima?..

Pa ovako ispada... O čemu se radi, drugovi?.. Toliko truda - i stali smo odmoriti se kod Web programera. tek počinje?..Ne bi li samo JSON preko HTTPS-a bio jednostavniji?! Što smo dobili u zamjenu? Je li trud vrijedan toga?

Procijenimo što nam je TL+MTProto dao i koje su alternative moguće. Dobro, HTTP, koji se fokusira na model zahtjev-odgovor, loše se uklapa, ali barem nešto povrh TLS-a?

Kompaktna serijalizacija. Gledajući ovu strukturu podataka, sličnu JSON-u, sjećam se da postoje njezine 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 definira oznake, kao mehanizam za proširenje, i među već standardizirani dostupno:

  • 25 + 256 - zamjena ponovljenih redaka referencom na broj retka, tako jeftina metoda kompresije
  • 26 - serijalizirani Perl objekt s nazivom klase i argumentima konstruktora
  • 27 - serijalizirani objekt neovisan o jeziku s imenom tipa i argumentima konstruktora

Pa, pokušao sam serijalizirati iste podatke u TL i u CBOR s omogućenim pakiranjem nizova i objekata. Rezultat je počeo varirati u korist CBOR-a negdje od megabajta:

cborlen=1039673 tl_len=1095092

Dakle, zaključak: Postoje znatno jednostavniji formati koji nisu podložni problemu kvara sinkronizacije ili nepoznatog identifikatora, s usporedivom učinkovitošću.

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

Kada koristite PFS u TLS-u, ulaznice TLS sesije (RFC 5077) za nastavak šifrirane sesije bez ponovnog dogovaranja ključeva i bez pohranjivanja informacija o ključu na poslužitelju. Prilikom otvaranja prve veze i kreiranja ključeva, poslužitelj kriptira stanje veze i šalje ga klijentu (u obliku sesijske karte). U skladu s tim, kada se veza nastavi, klijent šalje kartu sesije, uključujući ključ sesije, natrag na poslužitelj. Sama ulaznica je šifrirana privremenim ključem (session ticket key), koji je pohranjen na poslužitelju i mora se distribuirati među svim frontend poslužiteljima koji obrađuju SSL u klasteriranim rješenjima.[10] Dakle, uvođenje ulaznice za sesiju može prekršiti PFS ako su privremeni ključevi poslužitelja ugroženi, na primjer, kada su pohranjeni dulje vrijeme (OpenSSL, nginx, Apache pohranjuju ih prema zadanim postavkama za cijelo trajanje programa; popularne stranice koriste ključ nekoliko sati, do dana).

Ovdje RTT nije nula, morate razmijeniti barem ClientHello i ServerHello, nakon čega klijent može poslati podatke zajedno s Finished. Ali ovdje se trebamo sjetiti da nemamo web s hrpom novootvorenih veza, već glasnik čija je veza često jedna i više-manje dugotrajni, relativno kratki zahtjevi prema web stranicama - sve je višestruko interno. Odnosno, sasvim je prihvatljivo da nismo naišli na jako loš dio metroa.

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

Nastavit će se!

U drugom dijelu ove serije postova razmotrit ćemo ne tehnička, već organizacijska pitanja - pristupe, ideologiju, sučelje, odnos prema korisnicima itd. Međutim, na temelju tehničkih informacija koje su ovdje predstavljene.

Treći dio nastavit će analizirati tehničku komponentu / razvojno iskustvo. Posebno ćete naučiti:

  • nastavak pandemonija s raznolikošću TL tipova
  • nepoznate stvari o kanalima i supergrupama
  • zašto su dijalozi gori od popisa
  • o apsolutnom i relativnom adresiranju poruka
  • koja je razlika između fotografije i slike
  • kako emoji ometaju tekst u kurzivu

i druge štake! Ostanite s nama!

Izvor: www.habr.com

Dodajte komentar