A Telegram protokolljának és szervezeti megközelítéseinek kritikája. 1. rész, műszaki: tapasztalat a semmiből kliens írásában - TL, MT

Az utóbbi időben egyre gyakrabban jelennek meg a Habrén posztok arról, hogy milyen jó a Telegram, milyen zseniálisak és tapasztaltak a Durov testvérek a hálózati rendszerek kiépítésében stb. Ugyanakkor nagyon kevesen merültek el igazán a technikai eszközben – legfeljebb egy meglehetősen egyszerű (és az MTProto-tól merőben eltérő) JSON-alapú Bot API-t használnak, és általában csak elfogadják. a hitről minden dicséret és PR, ami a hírnök körül forog. Majdnem másfél éve kollégám az Eshelon civil szervezetben, Vaszilij (sajnos a Habré-fiókját a piszkozattal együtt törölték) a nulláról kezdte írni saját Telegram-kliensét Perlben, majd később e sorok írója is csatlakozott hozzá. Miért Perl, azonnal megkérdezik egyesek? Mert más nyelveken már léteznek ilyen projektek, valójában nem is ez a lényeg, lehet más nyelv, ahol nincs kész könyvtár, és ennek megfelelően a szerzőnek végig kell mennie a semmiből. Ezenkívül a kriptográfia bizalom kérdése, de ellenőrizze. Egy biztonságra törekvő terméknél nem lehet egyszerűen a gyártó kész könyvtárára hagyatkozni és vakon megbízni benne (ez azonban a második rész témája). Jelenleg a könyvtár elég jól működik „átlagos” szinten (lehetővé teszi bármilyen API kérés végrehajtását).

Ebben a bejegyzéssorozatban azonban nem sok kriptográfia vagy matematika lesz. De lesz még sok más technikai részlet és építészeti mankó (azoknak is hasznos, akik nem a semmiből írnak, hanem bármilyen nyelven használják a könyvtárat). Tehát a fő cél az volt, hogy az ügyfelet a semmiből próbáljuk megvalósítani hivatalos dokumentáció szerint. Vagyis tegyük fel, hogy a hivatalos kliensek forráskódja le van zárva (a második részben ismét részletesebben foglalkozunk azzal, hogy ez igaz megtörténik szóval), de mint régen, például van egy szabvány, mint az RFC - lehetséges-e egy klienst egyedül a specifikáció szerint írni, „anélkül, hogy megnéznénk” a forráskódot, legyen az hivatalos (Telegram Desktop, mobil), vagy nem hivatalos Telethon?

Tartalomjegyzék:

Dokumentáció... létezik, igaz? Ez igaz?..

A cikk jegyzettöredékeit tavaly nyáron kezdték gyűjteni. Mindezt a hivatalos weboldalon https://core.telegram.org A dokumentáció a 23. réteg szerinti volt, i.e. megragadt valahol 2014-ben (emlékszel, akkor még csatornák sem voltak?). Természetesen elméletileg ennek lehetővé kellett volna tennie, hogy 2014-ben egy akkori funkcionalitással rendelkező klienst implementáljunk. De még ebben az állapotban is egyrészt hiányos volt a dokumentáció, másrészt helyenként önmagának is ellentmondott. Alig több mint egy hónapja, 2019 szeptemberében volt véletlenül Felfedezték, hogy az oldalon egy nagy frissítés található a dokumentációban a teljesen friss 105-ös réteghez, azzal a megjegyzéssel, hogy most mindent újra el kell olvasni. Valójában sok cikket felülvizsgáltak, de sok változatlan maradt. Ezért, amikor elolvassa a dokumentációval kapcsolatos alábbi kritikát, ne feledje, hogy ezek a dolgok már nem relevánsak, de vannak, amelyek még mindig eléggé aktuálisak. Hiszen 5 év a modern világban nem csak hosszú idő, hanem nagyon sok. Azóta (főleg, ha az azóta eldobott és újraélesztett geochat oldalakat nem vesszük figyelembe) a sémában százról több mint kétszázötvenre nőtt az API metódusok száma!

Hol kezdjem fiatal szerzőként?

Nem számít, hogy a semmiből írsz, vagy használsz például kész könyvtárakat, mint pl Telethon Pythonhoz vagy Madeline PHP-hez, mindenesetre először szüksége lesz regisztrálja jelentkezését - paramétereket kapni api_id и api_hash (azok, akik dolgoztak a VKontakte API-val, azonnal megértik), amely alapján a szerver azonosítani fogja az alkalmazást. Ez kell jogi okokból tegyük meg, de arról, hogy a könyvtári szerzők miért nem publikálhatják, a második részben fogunk még beszélni. Elégedett lehet a tesztértékekkel, bár nagyon korlátozottak – tény, hogy most már regisztrálhat csak egy alkalmazást, szóval ne rohanjon bele.

Most technikai szempontból az kell, hogy érdekeljen, hogy regisztráció után kapjunk értesítést a Telegramtól a dokumentáció, protokoll stb. frissítéseiről. Vagyis feltételezhető, hogy a dokkolóhelyet egyszerűen elhagyták, és kifejezetten azokkal dolgoztak tovább, akik elkezdtek ügyfeleket keresni, mert könnyebb. De nem, semmi ilyesmit nem figyeltek meg, nem jött információ.

És ha a semmiből ír, akkor a kapott paraméterek használata valójában még messze van. Habár https://core.telegram.org/ és beszél róluk az Első lépések című részben, sőt, először meg kell valósítania MTProto protokoll - de ha hinnéd elrendezés az OSI modell szerint az oldal végén a protokoll általános leírását, akkor teljesen hiábavaló.

Valójában az MTProto előtt és után is, egyszerre több szinten (ahogy az operációs rendszer kernelében dolgozó külföldi hálózatosok mondják, rétegsértés) egy nagy, fájdalmas és borzasztó téma kerül majd útjába...

Bináris szerializálás: TL (Type Language) és séma, valamint rétegek és sok más ijesztő szó

Ez a téma valójában a Telegram problémáinak kulcsa. És sok szörnyű szó lesz, ha megpróbálsz elmélyülni benne.

Szóval, itt a diagram. Ha ez a szó eszedbe jut, mondd: JSON-séma, Jól gondoltad. A cél ugyanaz: valamilyen nyelv a továbbított adatok lehetséges halmazának leírására. Itt ér véget a hasonlóság. Ha az oldalról MTProto protokoll, vagy a hivatalos kliens forrásfájából megpróbálunk megnyitni valami sémát, valami ilyesmit fogunk látni:

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;

Aki ezt először látja, az intuitív módon csak egy részét tudja felismerni a leírtaknak - nos, ezek látszólag szerkezetek (bár hol van a név, balra vagy jobbra?), mezők vannak bennük, ami után egy típus következik a vastagbél után... valószínűleg. Itt a szögletes zárójelben valószínűleg olyan sablonok vannak, mint a C++-ban (sőt, nem egészen). És mit jelent az összes többi szimbólum, kérdőjelek, felkiáltójelek, százalékok, hash jelek (és nyilván ezek különböző helyeken mást jelentenek), hol jelenlévő, hol nem, hexadecimális számok – és ami a legfontosabb, hogyan lehet ebből kiindulni правильный (amit nem utasít el a szerver) byte folyam? El kell olvasnia a dokumentációt (igen, a közelben vannak linkek a sémára a JSON-verzióban – de ez nem teszi világosabbá).

Nyissa meg az oldalt Bináris adatsorosítás és merüljön el a gombák és a diszkrét matematika varázslatos világában, valami hasonló a matánhoz a 4. évben. Ábécé, típus, érték, kombinátor, funkcionális kombinátor, normál forma, összetett típus, polimorf típus... és ez még csak az első oldal! A következő vár rád TL nyelv, amely bár már tartalmaz példát egy triviális kérésre és válaszra, egyáltalán nem ad választ a tipikusabb esetekre, ami azt jelenti, hogy egy másik nyolc beágyazottan kell átgázolnod az oroszról angolra fordított matematika újramondását. oldalak!

A funkcionális nyelvekben és az automatikus típuskövetkeztetésben jártas olvasók természetesen a leíró nyelvet ezen a nyelven, még a példából is sokkal ismerősebbnek fogják látni, és elmondhatják, hogy ez elvileg nem rossz. Az ezzel kapcsolatos kifogások a következők:

  • Igen, a cél jól hangzik, de sajnos ő nem sikerült elérni
  • Az orosz egyetemek oktatása még az informatikai szakterületek között is változik - nem mindenki vette át a megfelelő tanfolyamot
  • Végül, mint látni fogjuk, a gyakorlatban így van nem szükséges, mivel a leírt TL-nek is csak egy korlátozott részhalmazát használjuk

Ahogy mondták LeoNerd a csatornán #perl a FreeNode IRC hálózatban, aki megpróbált egy kaput megvalósítani a Telegramról a Matrixra (az idézet emlékezetből való fordítása pontatlan):

Olyan érzés, mintha valaki először ismerkedett meg a típuselmélettel, felizgult, és elkezdett játszani vele, nem igazán törődött azzal, hogy szükség van-e rá a gyakorlatban.

Győződjön meg saját szemével, ha a csupasz típusok (int, long, stb.) mint elemi dolgok iránti igény nem vet fel kérdéseket - végső soron manuálisan kell őket megvalósítani - például, tegyünk egy kísérletet, hogy levezethessünk belőlük vektor. Azaz valójában sor, ha a kapott dolgokat a megfelelő nevükön nevezi.

De előtte

A TL szintaxis egy részhalmazának rövid leírása azoknak, akik nem olvassák a hivatalos dokumentációt

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;

A meghatározás mindig kezdődik tervező, utána opcionálisan (a gyakorlatban - mindig) a szimbólumon keresztül # kell, hogy legyen CRC32 az ilyen típusú normalizált leírási karakterláncból. Ezután következik a mezők leírása; ha léteznek, a típus üres lehet. Mindez egyenlőségjellel végződik, annak a típusnak a nevével, amelyhez ez a konstruktor – vagyis tulajdonképpen az altípus – tartozik. Az egyenlőségjeltől jobbra lévő srác az polimorf - vagyis több konkrét típus is megfelelhet neki.

Ha a definíció a sor után következik be ---functions---, akkor a szintaxis ugyanaz marad, de a jelentés más lesz: a konstruktor az RPC függvény neve lesz, a mezők pedig paraméterek (na, vagyis pontosan ugyanaz marad az adott struktúra, ahogy az alábbiakban leírjuk , ez egyszerűen a hozzárendelt jelentés lesz), a „polimorf típus” pedig a visszaadott eredmény típusa. Igaz, továbbra is polimorf marad – csak a szakaszban definiáltuk ---types---, de ezt a konstruktort „nem veszik figyelembe”. A hívott függvénytípusok argumentumaikkal túlterhelése, pl. Valamilyen oknál fogva több azonos nevű, de eltérő aláírású függvény, mint a C++-ban, nem szerepel a TL-ben.

Miért "konstruktor" és "polimorf", ha nem OOP? Nos, valójában valakinek könnyebb lesz ezt OOP-ban gondolkodnia - polimorf típus, mint absztrakt osztály, és a konstruktorok a közvetlen leszármazott osztályai, és final számos nyelv terminológiájában. Valójában persze csak itt hasonlóság valódi túlterhelt konstruktor metódusokkal OO programozási nyelvekben. Mivel itt csak adatstruktúrákról van szó, metódusok nincsenek (bár a függvények és metódusok további leírása eléggé képes zavart kelteni a fejben, hogy léteznek, de az már más kérdés) - a konstruktort értéknek tekintheti melyik épül írja be a bájtfolyam olvasásakor.

Hogyan történik ez? A deszerializáló, amely mindig 4 bájtot olvas, látja az értéket 0xcrc32 - és megérti, mi fog ezután történni field1 típussal int, azaz pontosan 4 bájtot olvas, ezen a felső mező a típussal PolymorType olvas. Lát 0x2crc32 és megérti, hogy először két mező van távolabb long, ami azt jelenti, hogy 8 bájtot olvasunk. És akkor megint egy összetett típus, ami ugyanúgy deszerializált. Például, Type3 bejelenthető az áramkörben, amint két konstruktőr, illetve, akkor valamelyiknek meg kell felelnie 0x12abcd34, ami után még 4 bájtot kell olvasni intVagy 0x6789cdef, ami után nem lesz semmi. Bármi más - kivételt kell tennie. Mindenesetre ezek után visszatérünk a 4 bájt olvasásához int mezők field_c в constructorTwo és ezzel befejezzük a mi olvasását PolymorType.

Végül, ha elkapják 0xdeadcrc a constructorThree, akkor minden bonyolultabbá válik. Első terepen az bit_flags_of_what_really_present típussal # - valójában ez csak egy álneve a típusnak nat, azaz "természetes szám". Vagyis valójában az unsigned int az egyetlen eset, amikor előjel nélküli számok fordulnak elő valós áramkörökben. Tehát a következő egy kérdőjellel ellátott konstrukció, ami azt jelenti, hogy ez a mező - csak akkor lesz jelen a vezetéken, ha a megfelelő bit be van állítva a hivatkozott mezőben (körülbelül, mint egy hármas operátor). Tehát tegyük fel, hogy ez a bit be van állítva, ami azt jelenti, hogy tovább kell olvasnunk egy hasonló mezőt Type, amely példánkban 2 konstruktorral rendelkezik. Az egyik üres (csak az azonosítóból áll), a másik mezőt tartalmaz ids típussal ids:Vector<long>.

Azt gondolhatja, hogy mind a sablonok, mind a generikusok a profikban vagy a Java-ban vannak. De nem. Majdnem. Ez az egyetlen szögletes zárójelek valós áramkörökben való használata esetén, és CSAK Vectorhoz használják. Egy bájtfolyamban ez 4 CRC32 bájt lesz magának a Vector típusnak, mindig ugyanaz, majd 4 bájt - a tömbelemek száma, majd maguk ezek az elemek.

Tegyük hozzá, hogy a szerializálás mindig 4 bájtos szavakban történik, minden típus ennek többszöröse - a beépített típusokat is leírjuk bytes и string a hossz kézi sorosításával és ezzel a 4-gyel történő igazítással - nos, úgy tűnik, hogy normálisnak és még viszonylag hatékonynak is hangzik? Bár a TL-ről azt állítják, hogy hatékony bináris szerializáció, a pokolba is, ha szinte bármit, még a logikai értékeket és az egykarakteres karakterláncokat is 4 bájtra bővítik, a JSON még mindig sokkal vastagabb lesz? Nézze, a felesleges mezőket is átugorhatják a bitjelzők, minden nagyon jó, sőt még bővíthető is a jövőre nézve, akkor miért ne adnánk később új opcionális mezőket a konstruktorhoz?..

De nem, ha nem az én rövid leírásomat olvasod, hanem a teljes dokumentációt, és elgondolkozol a megvalósításon. Először is a konstruktor CRC32-je a séma szöveges leírásának normalizált sora szerint kerül kiszámításra (felesleges szóköz eltávolítása stb.) - tehát ha új mezőt adunk hozzá, akkor megváltozik a típusleíró sor, így a CRC32 ill. , következésképpen a sorozatosítás. És mit tenne a régi ügyfél, ha kapna egy mezőt új zászlókkal, és nem tudja, mihez kezdjen vele?

Másodszor, emlékezzünk CRC32, amelyet itt lényegében úgy használunk hash függvények egyedileg meghatározza, hogy milyen típust (de)szerializálnak. Itt az ütközések problémájával állunk szemben – és nem, ennek a valószínűsége nem egy a 232-hez, hanem sokkal nagyobb. Ki emlékezett rá, hogy a CRC32 a kommunikációs csatorna hibáinak észlelésére (és kijavítására) készült, és ennek megfelelően javítja ezeket a tulajdonságokat mások kárára? Például nem törődik a bájtok átrendezésével: ha két sorból számítja ki a CRC32-t, a másodikban az első 4 bájtot felcseréli a következő 4 bájttal - ugyanaz lesz. Ha a bemenetünk a latin ábécé szövegei (és egy kis írásjelek), és ezek a nevek nem különösebben véletlenszerűek, akkor az ilyen átrendeződés valószínűsége jelentősen megnő.

Egyébként ki ellenőrizte, mi van ott? tényleg CRC32? Az egyik korai forráskódnak (még Waltman előtt is) volt egy hash függvénye, amely minden karaktert megszorzott 239-cel, amit ezek az emberek annyira szerettek, ha ha!

Végül oké, rájöttünk, hogy a konstruktorok mezőtípussal Vector<int> и Vector<PolymorType> más lesz a CRC32. Mi a helyzet az online teljesítménnyel? És elméleti szempontból ez a típus részévé válik-e? Tegyük fel, hogy átadunk egy tízezer számból álló tömböt, jól Vector<int> minden világos, a hossza és még 40000 XNUMX bájt. Mi van, ha ezt Vector<Type2>, amely csak egy mezőből áll int és egyedül van a típusban - meg kell ismételnünk a 10000xabcdef0-et 34 4-szer, majd XNUMX bájtot int, vagy a nyelv képes FÜGGETLENÜL FÜGGETNI azt számunkra a konstruktortól fixedVec és 80000 bájt helyett megint csak 40000-et vigyél át?

Ez egyáltalán nem tétlen elméleti kérdés - képzeld el, hogy kapsz egy listát a csoportos felhasználókról, mindegyiknek van azonosítója, keresztneve, vezetékneve - a mobilkapcsolaton átvitt adatmennyiség különbsége jelentős lehet. Pontosan a Telegram-szerializálás hatékonyságát hirdetik nekünk.

Így…

Vector, amely soha nem jelent meg

Ha megpróbálja átgázolni a kombinátorok leírásának oldalain és így tovább, látni fogja, hogy egy vektort (és még egy mátrixot is) formálisan több lapból álló sorokon keresztül próbál kiadni. De a végén elfelejtik, az utolsó lépést kihagyják, és egyszerűen megadják a vektor definícióját, amely még nincs típushoz kötve. Mi a helyzet? A nyelvekben programozás, különösen a funkcionálisaknál, meglehetősen jellemző a szerkezet rekurzív leírása - a fordító a lusta kiértékelésével mindent megért és megcsinál maga. A nyelvben adatsorosítás amire szükség van, az a HATÉKONYSÁG: elég egyszerűen leírni lista, azaz két elem szerkezete - az első egy adatelem, a második maga ugyanaz a struktúra vagy egy üres hely a farok számára (csomag (cons) Lispben). De ehhez nyilván szükség lesz mindegyikből Az elem további 4 bájtot költ (TL-ben a CRC32-t) a típusának leírására. Egy tömb is könnyen leírható fix méretű, de egy előre ismeretlen hosszúságú tömb esetén letörjük.

Ezért, mivel a TL nem engedi meg a vektor kiadását, ezt oldalra kellett hozzáadni. Végül a dokumentáció ezt írja:

A szerializálás mindig ugyanazt a konstruktor „vektort” használja (const 0x1cb5c415 = crc32(“vector t:Type # [ t ] = Vector t”), amely nem függ a t típusú változó konkrét értékétől.

Az opcionális t paraméter értéke nem vesz részt a szerializálásban, mivel az eredménytípusból származik (ez a deszerializálás előtt mindig ismert).

Nézze meg közelebbről: vector {t:Type} # [ t ] = Vector t - de sehol Ez a definíció önmagában nem azt mondja, hogy az első számnak meg kell egyeznie a vektor hosszával! És nem jön sehonnan. Ez egy adottság, amelyet szem előtt kell tartani, és saját kezűleg kell megvalósítani. Máshol a dokumentáció még őszintén megemlíti, hogy a típus nem az igazi:

A vektor t polimorf pszeudotípusa egy „típus”, amelynek értéke tetszőleges t típusú értékek sorozata, akár bekeretezett, akár csupasz.

... de nem koncentrál rá. Amikor belefáradsz a matematika (talán még egy egyetemi kurzusról ismert) feszítésébe való gázolásba, úgy döntesz, hogy feladod, és megnézed, hogyan dolgozz vele a gyakorlatban, az a benyomás marad a fejedben, hogy ez komoly. A matematika a lényeg, egyértelműen a Cool People találta ki (két matematikus – ACM győztes), és nem akárki. A célt - a megmutatkozást - sikerült elérni.

Egyébként a számról. Hadd emlékeztessük erre # ez szinonimája nat, természetes szám:

Vannak típuskifejezések (típus-kifejezés) és numerikus kifejezések (nat-expr). De ezeket ugyanúgy határozzák meg.

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

de a nyelvtanban ugyanúgy le vannak írva, i.e. Ezt a különbséget ismét emlékezni kell, és kézzel kell megvalósítani.

Nos, igen, sablontípusok (vector<int>, vector<User>) közös azonosítójuk van (#1cb5c415), azaz ha tudja, hogy a hívást a következő néven jelentették be

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

akkor már nem csak egy vektorra vársz, hanem a felhasználók vektorára. Pontosabban, kellene várj - a valós kódban minden elemnek, ha nem is csupasz típusnak lesz konstruktora, és jó értelemben az implementációban is ellenőrizni kellene - de ennek a vektornak minden elemében pontosan elküldtük azt a típust? Mi van, ha valami PHP-ről van szó, amelyben egy tömb különböző típusú elemeket tartalmazhat?

Ezen a ponton elkezdesz gondolkodni - szükség van egy ilyen TL-re? Esetleg a kosárhoz lehetne emberi szerializálót használni, ugyanazt a protobufot, ami akkor már létezett? Ez volt az elmélet, nézzük a gyakorlatot.

Meglévő TL implementációk a kódban

A TL a VKontakte mélyén született még a híres események előtt, Durov részesedésének eladásával és (biztosan), még a Telegram fejlesztése előtt. És nyílt forráskódban az első megvalósítás forráskódja sok vicces mankót találhat. És magát a nyelvet ott teljesebben implementálták, mint most a Telegramban. Például a kivonatokat egyáltalán nem használják a sémában (ez egy deviáns viselkedésű beépített pszeudotípust (mint egy vektort) jelent). Vagy

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

de tekintsük át a teljesség kedvéért, hogy úgymond nyomon kövessük a GondolatÓriás fejlődését.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Vagy ez a gyönyörű:

    static const char *reserved_words_polymorhic[] = {

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

      };

Ez a részlet olyan sablonokról szól, mint:

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

Ez a hashmap sablontípus definíciója, mint int - típus párok vektora. C++-ban valahogy így nézne ki:

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

így, alpha - kulcsszó! De csak C++-ban lehet T-t írni, de alpha-t, béta-t kellene írni... De nem több 8 paraméternél, ott a fantázia vége. Úgy tűnik, egyszer Szentpéterváron néhány ehhez hasonló párbeszéd zajlott:

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

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

De ez a TL első közzétett megvalósításáról szólt „általában”. Térjünk át a Telegram-klienseken belüli implementációk mérlegelésére.

Szó Vaszilijhoz:

Vaszilij, [09.10.18 17:07] Leginkább azért meleg a szamár, mert létrehoztak egy csomó absztrakciót, majd ráütöttek egy csavart, és mankóval letakarták a kódgenerátort
Ennek eredményeként először a dock pilot.jpg-ből
Majd a dzhekichan.webp kódból

Természetesen az algoritmusokban és a matematikában jártas emberektől elvárhatjuk, hogy olvassák az Aho-t, Ullmann-t, és ismerjék azokat az eszközöket, amelyek az évtizedek során de facto szabványossá váltak az iparágban a DSL-fordítóik megírásához, igaz?

Szerző távirat-cli Vitalij Valtman, amint a TLO formátum (kliens) határain kívüli előfordulásából is megérthető, a csapat tagja - most kiosztottak egy könyvtárat a TL elemzéshez külön, mi a benyomása róla TL elemző? ..

16.12 04:18 Vaszilij: Szerintem valaki nem sajátította el a lex+yacct
16.12 04:18 Vaszilij: Nem tudom másképp megmagyarázni
16.12 04:18 Vaszilij: hát vagy a VK-ban fizettek a sorokért
16.12 04:19 Vaszilij: 3k+ sor stb.<censored> elemző helyett

Talán kivétel? Lássuk hogyan csinál Ez a HIVATALOS kliens - 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+ sor Pythonban, pár reguláris kifejezés + speciális esetek mint egy vektor, ami persze úgy van deklarálva a sémában, ahogy a TL szintaxisa szerint kell, de erre a szintaxisra hagyatkoztak az elemzésnél... Felmerül a kérdés, miért volt az egész csoda?иRétegesebb, ha úgysem fogja senki a dokumentáció szerint elemezni?!

Mellesleg... Emlékszel, beszéltünk a CRC32 ellenőrzéséről? Tehát a Telegram Desktop kódgenerátorban van egy lista a kivételekről azokra a típusokra, amelyekben a számított CRC32 nem egyezik ábrán jelzettel!

Vaszilij, [18.12/22 49:XNUMX] és itt elgondolkoznék azon, hogy kell-e ilyen TL
ha alternatív megvalósításokkal akarnék vacakolni, elkezdenék sortöréseket beszúrni, többsoros definíciókon az elemzők fele megszakad
tdesktop azonban szintén

Emlékezzen a one-liner lényegére, kicsit később visszatérünk rá.

Oké, a telegram-cli nem hivatalos, a Telegram Desktop hivatalos, de mi van a többivel? Ki tudja?.. Az Android kliens kódjában egyáltalán nem volt sémaelemző (ami kérdéseket vet fel a nyílt forráskóddal kapcsolatban, de ez a második rész), de volt még több vicces kódrészlet, de ezekről bővebben a alszakaszban.

Milyen további kérdéseket vet fel a sorozatosítás a gyakorlatban? Például sok mindent csináltak, természetesen bitmezőkkel és feltételes mezőkkel:

Vaszilij: flags.0? true
azt jelenti, hogy a mező jelen van, és igaz, ha a zászló be van állítva

Vaszilij: flags.1? int
azt jelenti, hogy a mező jelen van, és deszerializálni kell

Vaszilij: Szamár, ne törődj azzal, amit csinálsz!
Vaszilij: Valahol a doksiban van említve, hogy a true egy csupasz nulla hosszúságú típus, de lehetetlen bármit is összerakni a dokijukból
Vaszilij: A nyílt forráskódú implementációkban ez sem így van, de van egy csomó mankó és támasz

Mi a helyzet a Telethonnal? Előre tekintve az MTProto témáját, egy példa - a dokumentációban vannak ilyen darabok, de a jel % csak úgy írják le, hogy „megfelel egy adott csupasz típusnak”, azaz. az alábbi példákban vagy hiba van, vagy valami nem dokumentált:

Vaszilij, [22.06.18 18:38] Egy helyen:

msg_container#73f1f8dc messages:vector message = MessageContainer;

Másképp:

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

És ez két nagy különbség, a való életben jön valami csupasz vektor

Nem láttam puszta vektordefiníciót, és nem is találkoztam ilyennel

Az elemzést kézzel írják telethonban

Diagramján a definíciót kommentálják msg_container

A kérdés ismét a % körül marad. Nincs leírva.

Vadim Goncharov, [22.06.18 19:22] és tdesktopban?

Vaszilij, [22.06.18 19:23] De a normál motorokon lévő TL elemzőjük valószínűleg ezt sem fogja megenni

// parsed manually

A TL egy gyönyörű absztrakció, senki sem valósítja meg teljesen

És a % nem szerepel a rendszer verziójában

De itt a dokumentáció önmagának mond ellent, szóval idk

A nyelvtanban megtalálták, egyszerűen elfelejthették leírni a szemantikát

Láttad a dokumentumot a TL-en, fél liter nélkül nem tudod kitalálni

„Nos, mondjuk – mondja egy másik olvasó –, hogy kritizálsz valamit, hát mutasd meg, hogyan kell csinálni.”

Vaszilij így válaszol: „Ami az elemzőt illeti, szeretem az ilyeneket

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

valahogy jobban tetszik mint

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

vagy

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

ez az EGÉSZ lexer:

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

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

azok. enyhén szólva egyszerűbb."

Általában ennek eredményeként a TL ténylegesen használt részhalmazának értelmezője és kódgenerátora körülbelül 100 nyelvtani sorba és a generátor ~300 sorába illeszkedik (az összes printáltal generált kód), beleértve a típusinformációs zsemléket az önvizsgálathoz minden osztályban. Minden polimorf típus üres absztrakt alaposztállyá alakul, és a konstruktorok ebből öröklik, és vannak metódusai a szerializáláshoz és deszerializáláshoz.

Típushiány a típusnyelvben

Az erős gépelés jó dolog, nem? Nem, ez nem holivar (bár én inkább a dinamikus nyelveket szeretem), hanem egy posztulátum a TL keretein belül. Ez alapján a nyelvnek mindenféle ellenőrzést kell biztosítania számunkra. Na jó, lehet, hogy nem ő maga, hanem a megvalósítás, de legalább le kellene írnia őket. És milyen lehetőségekre vágyunk?

Először is a korlátok. Ezt látjuk a fájlok feltöltésének dokumentációjában:

A fájl bináris tartalma ezután részekre van osztva. Minden alkatrésznek azonos méretűnek kell lennie ( rész_méret ), és a következő feltételeknek kell teljesülniük:

  • part_size % 1024 = 0 (osztható 1 KB-val)
  • 524288 % part_size = 0 (512 KB-nak egyenletesen oszthatónak kell lennie a részmérettel)

Az utolsó résznek nem kell megfelelnie ezeknek a feltételeknek, feltéve, hogy mérete kisebb, mint a part_size.

Minden résznek sorszámmal kell rendelkeznie, fájl_rész, 0 és 2,999 közötti értékkel.

A fájl particionálása után ki kell választania a kiszolgálón való mentés módját. Használat upload.saveBigFilePart abban az esetben, ha a fájl teljes mérete meghaladja a 10 MB-ot és upload.saveFilePart kisebb fájlokhoz.
[…] az alábbi adatbeviteli hibák egyike adható vissza:

  • FILE_PARTS_INVALID — Érvénytelen részek száma. Az érték nem között van 1..3000

Van ebből valami a diagramon? Ez valahogy kifejezhető TL használatával? Nem. De elnézést, még a nagypapa Turbo Pascalja is le tudta írni a megadott típusokat tartományok. És tudott még egy dolgot, ma ismertebb nevén enum - rögzített (kis) számú érték felsorolásából álló típus. Az olyan nyelvekben, mint a C - numeric, vegye figyelembe, hogy eddig csak típusokról beszéltünk szám. De vannak tömbök, karakterláncok is... például jó lenne leírni, hogy ez a karakterlánc csak telefonszámot tartalmazhat, nem?

Ezek egyike sem szerepel a TL-ben. De van például a JSON-sémában. És ha valaki más vitatkozna az 512 KB oszthatóságával kapcsolatban, hogy ezt még kódban kell ellenőrizni, akkor győződjön meg arról, hogy a kliens egyszerűen nem tudta tartományon kívüli számot küld 1..3000 (és a megfelelő hiba fel sem merülhetett volna) lehetséges lett volna, nem?..

Egyébként a hibákról és a visszatérési értékekről. Még azok is elhomályosítják a szemüket, akik TL-lel dolgoztak – ez nem tűnt fel nekünk azonnal mindegyik egy függvény a TL-ben valójában nem csak a leírt visszatérési típust tudja visszaadni, hanem hibát is. De ez semmilyen módon nem vezethető le magát a TL-t használva. Persze ez már most világos, és a gyakorlatban nincs szükség semmire (bár valójában az RPC-t többféleképpen is meg lehet csinálni, erre később még visszatérünk) - de mi a helyzet az absztrakt típusok matematikája fogalmainak tisztaságával a mennyei világból?.. Felvettem a vontatót – hát illik hozzá.

És végül, mi a helyzet az olvashatósággal? Nos, általában, szeretném leírás rendben van-e a sémában (a JSON-sémában megint az), de ha már feszült vagy vele, akkor mi van a gyakorlati oldallal - legalábbis triviális, ha megnézed a különbségeket a frissítések során? Nézze meg saját szemével a címen valós példák:

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

vagy

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

Ez mindenkitől függ, de például a GitHub nem hajlandó kiemelni az ilyen hosszú sorokon belüli változásokat. A „találj 10 különbséget” játék, és amit az agy azonnal lát, az az, hogy a kezdetek és a végek mindkét példában ugyanazok, fárasztóan kell olvasni valahol a közepén... Véleményem szerint ez nem csak elméletben van így, hanem pusztán vizuálisan piszkos és hanyag.

Egyébként az elmélet tisztaságáról. Miért van szükségünk bitmezőkre? Nem úgy tűnik, hogy ők szag rossz a típuselmélet szempontjából? A magyarázat a diagram korábbi verzióiban látható. Eleinte igen, így volt, minden tüsszentésre új típus jött létre. Ezek a kezdetek még mindig léteznek ebben a formában, például:

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;

De most képzelje el, ha 5 választható mezője van a struktúrájában, akkor 32 típusra lesz szüksége az összes lehetséges opcióhoz. Kombinatorikus robbanás. Így a TL-elmélet kristálytisztasága ismét összetört a sorozatosítás zord valóságának öntöttvas seggével szemben.

Ráadásul egyes helyeken ezek a srácok maguk is megsértik saját tipológiájukat. Például az MTProto-ban (következő fejezet) a választ a Gzip tömörítheti, minden rendben van - kivéve, hogy a rétegek és az áramkör megsérül. Megint nem magát az RpcResultot aratták le, hanem annak tartalmát. Na, ezt minek?.. Mankóba kellett vágnom, hogy bárhol működjön a kompresszió.

Vagy egy másik példa, egyszer hibát fedeztünk fel – elküldtük InputPeerUser helyett InputUser. Vagy fordítva. De sikerült! Vagyis a szervert nem érdekelte a típus. Hogy lehet ez? A választ a telegram-cli kódrészletei adhatják meg nekünk:

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

Más szóval, itt történik a szerializálás MANUÁLISAN, nem generált kód! Lehet, hogy a szerver is hasonló módon van implementálva?.. Ez elvileg működni fog, ha egyszer megcsinálják, de hogyan lehet később a frissítések során támogatni? Ezért találták ki a sémát? És itt áttérünk a következő kérdésre.

Verziószámítás. Rétegek

Hogy miért nevezik a sémaváltozatokat rétegeknek, azt csak találgatni lehet a publikált sématörténet alapján. Nyilvánvalóan eleinte a szerzők úgy gondolták, hogy az alapvető dolgokat változatlan sémával is meg lehet tenni, és csak ott, ahol szükséges, konkrét kérések esetén jelezték, hogy más verziót használnak. Elvileg még jó ötlet is – és az új úgymond „vegyes” lesz, a régi tetejére rétegezve. De lássuk, hogyan sikerült. Igaz, a kezdetektől nem tudtam megnézni - ez vicces, de az alapréteg diagramja egyszerűen nem létezik. A rétegek 2-vel kezdődtek. A dokumentáció egy speciális TL-szolgáltatásról szól:

Ha egy kliens támogatja a Layer 2-t, akkor a következő konstruktort kell használni:

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

A gyakorlatban ez azt jelenti, hogy minden API hívás előtt egy int az értékkel 0x289dd1f6 a módszer száma elé kell adni.

Normálisan hangzik. De mi történt ezután? Aztán megjelent

invokeWithLayer3#b7475268 query:!X = X;

Szóval mi következik? Ahogy sejtheti,

invokeWithLayer4#dea0d430 query:!X = X;

Vicces? Nem, korai még nevetni, gondolj bele minden egy másik rétegtől érkező kérést ilyen speciális típusba kell csomagolni - ha ezek mind különböznek számodra, hogyan tudnád másképp megkülönböztetni őket? És csak 4 bájt hozzáadása elég hatékony módszer. Így,

invokeWithLayer5#417a57ae query:!X = X;

De nyilvánvaló, hogy egy idő után ez valamiféle bakchanáliává válik. És jött a megoldás:

Frissítés: A 9-es rétegtől kezdődően, segítő módszerek invokeWithLayerN csak együtt használható initConnection

Hurrá! 9 verzió után végre elérkeztünk ahhoz, amit az Internet protokollokban még a 80-as években csináltak - egyszer a csatlakozás elején egyeztetve a verziót!

Szóval mi következik?...

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

De most még nevethetsz. Csak újabb 9 réteg után került végre egy verziószámos univerzális konstruktor, amit a kapcsolat elején csak egyszer kell meghívni, és a rétegek jelentése mintha eltűnt volna, most már csak egy feltételes verzió, pl. mindenhol máshol. Probléma megoldódott.

Pontosan?..

Vaszilij, [16.07.18 14:01] Még pénteken is azt gondoltam:
A teleszerver kérés nélkül küldi el az eseményeket. A kéréseket InvokeWithLayerbe kell csomagolni. A szerver nem csomagolja be a frissítéseket, nincs struktúra a válaszok és frissítések csomagolására.

Azok. az ügyfél nem tudja megadni azt a réteget, amelyben frissítéseket szeretne

Vadim Goncharov, [16.07.18 14:02] az InvokeWithLayer elvileg nem mankó?

Vaszilij, [16.07.18 14:02] Ez az egyetlen út

Vadim Goncharov, [16.07.18 14:02] ami lényegében azt jelenti, hogy a munkamenet elején meg kell állapodni a rétegben

Ebből egyébként az következik, hogy az ügyfél visszaminősítése nem biztosított

Frissítések, pl. típus Updates a sémában ezt küldi el a szerver a kliensnek nem API kérésre válaszul, hanem önállóan, ha esemény bekövetkezik. Ez egy összetett téma, amelyről egy másik bejegyzésben lesz szó, de egyelőre fontos tudni, hogy a szerver akkor is menti a frissítéseket, amikor a kliens offline állapotban van.

Így, ha nem hajlandó becsomagolni mindegyikből csomag a verziójának jelzésére, ez logikusan a következő lehetséges problémákhoz vezet:

  • a szerver frissítéseket küld a kliensnek még azelőtt, hogy az ügyfél tájékoztatta volna, hogy melyik verziót támogatja
  • mit tegyek a kliens frissítése után?
  • aki garanciákathogy a folyamat során nem változik a szerver véleménye a rétegszámról?

Ön szerint ez pusztán elméleti spekuláció, és a gyakorlatban ez nem fordulhat elő, mert a szerver helyesen van megírva (legalábbis jól van tesztelve)? Ha! Mindegy, hogy van!

Pontosan ebbe futottunk bele augusztusban. Augusztus 14-én olyan üzenetek érkeztek, hogy valami frissül a Telegram szerverein... majd a naplókban:

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.

és utána több megabájt veremnyomok (na jó, ezzel egy időben a naplózás is ki lett javítva). Végül is, ha valamit nem ismer fel a TL-jében, az aláírás alapján bináris, a sorban lejjebb ÖSSZES megy, a dekódolás lehetetlenné válik. Mit kell tenni ilyen helyzetben?

Nos, az első dolog, ami mindenkinek eszébe jut, az az, hogy bontsa ki a kapcsolatot, és próbálja újra. Nem segített. A Google CRC32-t bekerestük - ezekről kiderült, hogy a 73-as sémából származó objektumok, bár dolgoztunk a 82-n. Alaposan megnézzük a naplókat - két különböző sémából származó azonosítók vannak!

Lehet, hogy a probléma pusztán a nem hivatalos ügyfelünkben van? Nem, elindítjuk a Telegram Desktop 1.2.17-et (számos Linux-disztribúcióban megtalálható verzió), ez írja a kivételnaplóba: MTP Váratlan típusazonosító #b5223b0f olvasható az MTPMessageMedia-ban…

A Telegram protokolljának és szervezeti megközelítéseinek kritikája. 1. rész, műszaki: tapasztalat a semmiből kliens írásában - TL, MT

A Google kimutatta, hogy az egyik nem hivatalos kliensnél már előfordult hasonló probléma, de akkor a verziószámok és ennek megfelelően a feltételezések is mások voltak...

Szóval mit kéne tennünk? Vaszilijjal szétváltunk: megpróbálta frissíteni az áramkört 91-re, én úgy döntöttem, várok néhány napot, és kipróbálom a 73-at. Mindkét módszer működött, de mivel empirikusak, nem értjük, hány verzióra van szüksége felfelé vagy lefelé. ugrani, vagy mennyi ideig kell várnia .

Később sikerült reprodukálnom a helyzetet: elindítjuk a klienst, kikapcsoljuk, újrafordítjuk az áramkört egy másik rétegre, újraindítjuk, újra elkapjuk a problémát, visszatérünk az előzőhöz - hoppá, nincs áramkörváltás, és a kliens újraindul egy ideig. néhány perc segít. Különböző rétegekből származó adatszerkezetek keverékét fogja megkapni.

Magyarázat? A különféle közvetett tünetekből sejthető, hogy a szerver számos különböző típusú folyamatból áll, különböző gépeken. Valószínűleg a „pufferelésért” felelős szerver azt tette be a sorba, amit a felettesei adtak neki, és a generáláskor érvényes séma szerint adták. És amíg ez a sor „rohadt” nem volt, semmit sem lehetett tenni ellene.

Talán... de ez iszonyatos mankó?!.. Nem, mielőtt őrült ötleteken gondolkoznánk, nézzük meg a hivatalos ügyfelek kódját. Az Android verzióban nem találunk TL-elemzőt, de találunk egy jókora fájlt (a GitHub nem hajlandó hozzányúlni) (de)szerializálással. Íme a kódrészletek:

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;

vagy

    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... vadul néz ki. De valószínűleg ez generált kód, akkor oké?... De biztosan támogatja az összes verziót! Igaz, nem világos, hogy miért keveredik össze minden, titkos csevegés és mindenféle _old7 valahogy nem úgy néz ki, mint a gépgeneráció... Leginkább azonban lenyűgözött

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Srácok, nem tudjátok eldönteni, hogy mi van egy rétegben?! Jó, oké, tegyük fel, hogy „kettőt” engedtek ki hibával, hát megesik, de HÁROM?.. Mindjárt megint ugyanaz a gereblye? Miféle pornográfia ez, bocsánat?

A Telegram Desktop forráskódjában egyébként hasonló történik - ha igen, akkor egymás után több commit a sémára nem módosítja a rétegszámát, hanem javít valamit. Olyan körülmények között, ahol nincs hivatalos adatforrás a konstrukcióhoz, honnan szerezhető be, kivéve a hivatalos ügyfél forráskódját? És ha onnan veszed, nem lehetsz biztos abban, hogy a séma teljesen helyes, amíg nem teszteled az összes módszert.

Hogy lehet ezt egyáltalán tesztelni? Remélem, az egység-, funkcionális és egyéb tesztek rajongói megosztják majd a megjegyzésekben.

Oké, nézzünk egy másik kódrészletet:

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;

Ez a „manuálisan létrehozott” megjegyzés azt sugallja, hogy ennek a fájlnak csak egy része íródott manuálisan (el tudod képzelni az egész karbantartási rémálmot?), a többit pedig gép generálta. Felmerül azonban egy másik kérdés – hogy a források rendelkezésre állnak-e nem teljesen (a la GPL blobok a Linux kernelben), de ez már a második rész témája.

De elég. Térjünk át a protokollra, amelyen ez a sorozatosítás fut.

MT Proto

Szóval, nyissunk Általános leírása и a protokoll részletes leírása és az első dolog, amibe belebotlunk, az a terminológia. És rengeteg mindennel. Általában úgy tűnik, hogy ez a Telegram szabadalmaztatott funkciója – különböző helyeken másként, vagy egy szóval máshogyan hívja a dolgokat, vagy fordítva (például magas szintű API-ban, ha matricacsomagot lát, akkor nem amit gondoltál).

Például az „üzenet” és a „munkamenet” itt mást jelent, mint a szokásos Telegram kliens felületén. Nos, az üzenettel minden világos, értelmezhető OOP-ban, vagy egyszerűen csak "csomag" szónak nevezhető - ez egy alacsony, szállítási szint, nem ugyanazok az üzenetek vannak, mint a felületen, sok szolgáltatási üzenet van . De az ülés... de először a dolgok.

Szállítási réteg

Az első dolog a közlekedés. 5 lehetőséget mondanak el nekünk:

  • TCP
  • Websocket
  • Websocket HTTPS-en keresztül
  • HTTP
  • HTTPS

Vaszilij, [15.06.18 15:04] Van UDP szállítás is, de ez nincs dokumentálva

És a TCP három változatban

Az első hasonló a TCP feletti UDP-hez, minden csomag tartalmaz egy sorszámot és egy crc-t
Miért olyan fájdalmas a kocsin lévő dokumentumok olvasása?

Nos, most itt van TCP már 4 változatban:

  • rövidített
  • Közbülső
  • Párnázott köztes
  • Tele

Nos, ok, párnázott köztes MTProxy-hoz, ezt később a jól ismert események miatt adták hozzá. De minek még két verzió (összesen három), ha eggyel is boldogulsz? Mind a négy lényegében csak abban különbözik, hogy hogyan kell beállítani a fő MTProto hosszát és hasznos teherbírását, amelyet a továbbiakban tárgyalunk:

  • Abridgedben 1 vagy 4 bájt, de nem 0xef, akkor a törzs
  • Az Intermediate-ban ez 4 bájt hosszúságú és egy mező, és az első alkalommal, amikor a kliensnek el kell küldenie 0xeeeeeeee jelezni, hogy középfokú
  • in Full a legfüggőbb, a hálózatépítő szemszögéből: hossza, sorszám, és NEM AZ, ami főleg MTProto, body, CRC32. Igen, mindez a TCP tetején van. Ez megbízható szállítást biztosít számunkra szekvenciális bájtfolyam formájában; nincs szükség sorozatokra, különösen ellenőrző összegekre. Oké, most valaki kifogásolni fogja, hogy a TCP-nek 16 bites ellenőrző összege van, így adatsérülés történik. Remek, de valójában van egy kriptográfiai protokollunk 16 bájtnál hosszabb hashekkel, ezeket a hibákat - és még többet is - egy magasabb szintű SHA eltérés fogja el. A CRC32-nek ezen felül semmi értelme.

Hasonlítsuk össze az Abridged-et, amelyben egy bájt hosszúság lehetséges, az Intermediate-tel, ami indokolja, hogy „Ha 4 bájtos adatigazításra van szükség”, ami elég nonszensz. Mi az, úgy gondolják, hogy a Telegram programozói annyira inkompetensek, hogy nem tudnak adatokat olvasni egy foglalatból egy igazított pufferbe? Ezt továbbra is meg kell tennie, mert az olvasás akárhány bájtot visszaadhat (és pl. proxy szerverek is vannak...). Vagy másrészt, miért blokkolja le az Abridged-et, ha a 16 byte-on továbbra is jókora kitöltés marad – spóroljon 3 bájtot néha ?

Az embernek az a benyomása, hogy Nyikolaj Durov nagyon szereti újra feltalálni a kerekeket, beleértve a hálózati protokollokat is, minden valódi gyakorlati igény nélkül.

Egyéb szállítási lehetőségek, pl. Web és MTProxy, most nem vesszük figyelembe, talán egy másik bejegyzésben, ha van kérés. Ugyanerről az MTProxyról most emlékezzünk csak arra, hogy nem sokkal a 2018-as megjelenése után a szolgáltatók gyorsan megtanulták blokkolni azt bypass blokkolásÁltal csomag mérete! És az a tény is, hogy a (ismét Waltman által) C nyelven írt MTProxy szerver túlzottan a Linux sajátosságaihoz volt kötve, bár erre egyáltalán nem volt szükség (Phil Kulin megerősíti), és hogy egy hasonló szerver akár Go-ban, akár Node.js-ben száznál kevesebb sorba fér bele.

De ezeknek az embereknek a technikai műveltségére vonatkozóan a rész végén, más kérdések átgondolása után következtetéseket vonunk le. Egyelőre térjünk át az OSI 5. rétegére, a session-re – amelyre az MTProto sessiont helyezték el.

Kulcsok, üzenetek, munkamenetek, Diffie-Hellman

Nem teljesen helyesen helyezték oda... A munkamenet nem ugyanaz, mint ami a felületen az Aktív munkamenetek alatt látható. De sorrendben.

A Telegram protokolljának és szervezeti megközelítéseinek kritikája. 1. rész, műszaki: tapasztalat a semmiből kliens írásában - TL, MT

Tehát egy ismert hosszúságú bájt sztringet kaptunk a szállítási rétegtől. Ez vagy titkosított üzenet, vagy egyszerű szöveg – ha még mindig a kulcsmegállapodás szakaszában vagyunk, és valóban megtesszük. A „kulcsnak” nevezett fogalmak közül melyikről beszélünk? Tisztázzuk ezt a kérdést magának a Telegram csapatának (elnézést, hogy hajnali 4-kor fáradt aggyal fordítottam le a saját dokumentációmat angolról, könnyebb volt néhány mondatot úgy hagyni, ahogy vannak):

Két entitást hívnak ülés - egy a hivatalos kliensek felhasználói felületén az „aktuális munkamenetek” alatt, ahol minden munkamenet egy teljes eszköznek / operációs rendszernek felel meg.
A második MTProto munkamenet, amelyben az üzenet sorszáma (alacsony szintű értelemben) szerepel, és amely tarthat a különböző TCP-kapcsolatok között. Egyszerre több MTProto munkamenet is telepíthető, például a fájlletöltés felgyorsítása érdekében.

E kettő között ülések van egy koncepció meghatalmazás. Degenerált esetben azt mondhatjuk UI munkamenet ugyanaz mint meghatalmazás, de sajnos minden bonyolult. Nézzük:

  • Az új eszköz felhasználója először generál autentikációs kulcs és számlához köti, például SMS-ben – ezért meghatalmazás
  • Az elsőben történt MTProto munkamenet, amely session_id magadban.
  • Ennél a lépésnél a kombináció meghatalmazás и session_id lehetett nevezni példa - ez a szó néhány ügyfél dokumentációjában és kódjában szerepel
  • Ezután az ügyfél megnyithatja néhány MTProto munkamenetek ugyanaz alatt autentikációs kulcs - ugyanarra a DC-re.
  • Ezután egy napon az ügyfélnek le kell kérnie a fájlt egy másik DC - és ehhez a DC-hez egy új generálódik autentikációs kulcs !
  • Tájékoztassa a rendszert, hogy nem új felhasználó regisztrál, hanem ugyanaz meghatalmazás (UI munkamenet), az ügyfél API-hívásokat használ auth.exportAuthorization otthoni DC-ben auth.importAuthorization az új DC-ben.
  • Minden ugyanaz, több is nyitva lehet MTProto munkamenetek (mindegyiknek megvan a maga session_id) ehhez az új DC-hez, alatt övé autentikációs kulcs.
  • Végül, az ügyfél tökéletes továbbítási titkosságot akarhat. Minden autentikációs kulcs ez volt állandó kulcs - DC-nként - és a kliens hívhat auth.bindTempAuthKey használatra ideiglenes autentikációs kulcs - és megint csak egy temp_auth_key DC-nként, mindenkinél közös MTProto munkamenetek ehhez a DC-hez.

Vegye figyelembe (és a jövőbeni sók) is egy az autentikációs kulcs azok. mindenki között megosztva MTProto munkamenetek ugyanarra a DC-re.

Mit jelent a „különböző TCP-kapcsolatok között”? Tehát ez azt jelenti valami hasonló engedélyezési süti egy weboldalon - sok TCP-kapcsolatot fennmarad (túlél) egy adott szerverrel, de egy nap elromlik. Csak a HTTP-től eltérően az MTProto-ban a munkameneten belüli üzenetek sorszámozásra és megerősítésre kerülnek, ha beléptek az alagútba, a kapcsolat megszakadt - az új kapcsolat létrehozása után a szerver szívessen elküld mindent ebben a munkamenetben, amit az előzőben nem kézbesített. TCP kapcsolat.

A fenti információkat azonban sok hónapos vizsgálat után összegeztük. Addig is a semmiből valósítjuk meg ügyfelünket? - térjünk vissza az elejére.

Tehát generáljunk auth_key on Diffie-Hellman verziók a Telegramból. Próbáljuk megérteni a dokumentációt...

Vaszilij, [19.06.18 20:05] data_with_hash := SHA1(data) + data + (bármilyen véletlen bájt); úgy, hogy a hossza 255 bájt legyen;
titkosított_adatok := RSA(adat_kivonattal, szerver_nyilvános_kulcs); egy 255 bájtos számot (big endian) a szükséges teljesítményre emelünk a szükséges modulus felett, és az eredményt 256 bájtos számként tároljuk.

Van valami dögös DH-juk

Nem úgy néz ki, mint egy egészséges ember DH-ja
A dx-ben nincs két nyilvános kulcs

Nos, ezt végül sikerült rendezni, de maradvány maradt – a munka igazolását az ügyfél végzi, hogy tudta a számot. A DoS támadások elleni védelem típusa. Az RSA kulcsot pedig csak egyszer használják egy irányba, lényegében titkosításhoz new_nonce. De míg ez az egyszerűnek tűnő művelet sikerülni fog, mivel kell szembenéznie?

Vaszilij, [20.06.18/00/26 XNUMX:XNUMX] Még nem értem el az appid kérést

Ezt a kérést elküldtem a DH-nak

A szállítódokkban pedig azt írja, hogy 4 bájt hibakóddal tud válaszolni. Ez minden

Nos, azt mondta nekem -404, akkor mi van?

Így hát azt mondtam neki: „Fogd el a kiszolgálókulccsal titkosított baromságodat ilyen ujjlenyomattal, DH-t akarok”, és egy hülye 404-el válaszolt.

Mi a véleményetek erről a szerver válaszról? Mit kell tenni? Nincs kit kérdezni (de erről bővebben a második részben).

Itt minden érdeklődés a dokkon történik

Nincs más dolgom, csak arról álmodoztam, hogy számokat oda-vissza konvertálok

Két 32 bites szám. Bepakoltam őket, mint mindenki mást

De nem, ezt a kettőt először BE-ként kell hozzáadni a sorhoz

Vadim Goncsarov, [20.06.18 15:49] és emiatt 404?

Vaszilij, [20.06.18 15:49] IGEN!

Vadim Goncharov, [20.06.18 15:50] szóval nem értem, mit tud "nem talált"

Vaszilij [20.06.18 15:50] körülbelül

Nem találtam ilyen főtényezőkre való bontást)

Még a hibajelentést sem kezeltük

Vaszilij, [20.06.18 20:18] Ó, van MD5 is. Már három különböző hash

A kulcs ujjlenyomatának kiszámítása a következőképpen történik:

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

SHA1 és sha2

Szóval tegyük fel auth_key Diffie-Hellman segítségével 2048 bitet kaptunk. Mi a következő lépés? Ezután rájövünk, hogy ennek a kulcsnak az alsó 1024 bitjét semmilyen módon nem használjuk... de most gondolkozzunk el ezen. Ennél a lépésnél van egy közös titok a szerverrel. Létrehozták a TLS-munkamenet analógját, ami nagyon költséges eljárás. De a szerver még mindig nem tud semmit arról, hogy kik vagyunk! Még nem, sőt. engedélyezés. Azok. ha a „login-password”-ban gondolkodtál, mint egykor az ICQ-ban, vagy legalább „bejelentkezési kulcsban”, mint az SSH-ban (például valamilyen gitlabon/githubon). Kaptunk egy névtelent. Mi van, ha a szerver azt mondja nekünk, hogy „ezeket a telefonszámokat egy másik DC szolgálja ki”? Vagy akár „tiltották a telefonszámát”? A legjobb, amit tehetünk, hogy megtartjuk a kulcsot abban a reményben, hogy hasznos lesz, és addigra nem romlik el.

Mi egyébként fenntartásokkal „fogadtuk”. Például megbízunk a szerverben? Mi van, ha hamis? Kriptográfiai ellenőrzésekre lenne szükség:

Vaszilij, [21.06.18 17:53] A mobilklienseknek felajánlják, hogy ellenőrizzék a 2 kbit-es szám elsődlegességét.

De ez egyáltalán nem világos, nafeijoa

Vaszilij, [21.06.18 18:02] A dokumentum nem írja le, hogy mit kell tenni, ha kiderül, hogy nem egyszerű

Nem mondták. Lássuk, mit csinál ebben az esetben a hivatalos Android kliens? A ez az (és igen, az egész fájl érdekes) - ahogy mondani szokás, ezt itt hagyom:

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

Nem, természetesen még mindig ott van néhány Vannak tesztek egy szám elsődlegességére, de személy szerint már nincs elegendő matematikai ismeretem.

Oké, megvan a főkulcs. A bejelentkezéshez, pl. kérések küldéséhez további titkosítást kell végrehajtania az AES használatával.

Az üzenetkulcs az üzenettörzs SHA128 256 középső bitjeként van definiálva (beleértve a munkamenetet, az üzenetazonosítót stb.), beleértve a kitöltési bájtokat is, amelyeket az engedélyezési kulcsból vett 32 bájt előz meg.

Vaszilij, [22.06.18 14:08] Átlagos, szuka, bit

Kapott auth_key. Minden. Rajtuk kívül... nem derül ki a dokumentumból. Nyugodtan tanulmányozza a nyílt forráskódot.

Vegye figyelembe, hogy az MTProto 2.0 12-1024 bájt kitöltést igényel, még mindig azzal a feltétellel, hogy az eredményül kapott üzenethossz osztható 16 bájttal.

Tehát mennyi párnázást kell hozzáadni?

És igen, hiba esetén van 404 is

Ha valaki figyelmesen tanulmányozta a dokumentáció diagramját és szövegét, észrevette, hogy ott nincs MAC. És hogy az AES-t egy bizonyos IGE módban használják, amelyet sehol máshol nem használnak. Erről persze írnak a GYIK-ben... Itt például maga az üzenetkulcs egyben a visszafejtett adatok SHA-kivonata is, ami az integritás ellenőrzésére szolgál - és ha nem egyezik, akkor valamiért a dokumentációt. azt javasolja, hogy figyelmen kívül hagyjuk őket (de mi van a biztonsággal, mi van, ha megtörnek minket?).

Nem vagyok kriptográfus, talán elméleti szempontból nincs is ezzel a móddal semmi baj. De egyértelműen meg tudok nevezni egy gyakorlati problémát, példaként a Telegram Desktopot használva. A helyi gyorsítótárat (mind ezeket a D877F783D5D3EF8C-t) ugyanúgy titkosítja, mint az MTProto üzeneteit (csak ebben az esetben az 1.0-s verzió), pl. először az üzenetkulcsot, majd magát az adatot (és valahol a fő big auth_key 256 bájt, ami nélkül msg_key hiábavaló). Tehát a probléma a nagy fájlok esetén észrevehetővé válik. Nevezetesen, meg kell őriznie az adatok két példányát - titkosítva és visszafejtve. És ha van megabájt, vagy például streaming videó?... A klasszikus sémák, amelyekben a MAC a titkosított szöveg után van, lehetővé teszik, hogy olvassa a streamet, azonnal továbbítsa. De az MTProto-val muszáj lesz eleinte titkosítsa vagy visszafejtse a teljes üzenetet, csak ezután vigye át a hálózatra vagy a lemezre. Ezért a Telegram Desktop legújabb verzióiban a gyorsítótárban user_data Egy másik formátumot is használnak - AES-sel CTR módban.

Vaszilij, [21.06.18 01:27] Ó, rájöttem, mi az IGE: az IGE volt az első kísérlet a „hitelesítő titkosítási módra”, eredetileg a Kerberoshoz. Sikertelen kísérlet volt (nem nyújt integritásvédelmet), és el kellett távolítani. Ez volt a 20 éves kutatás kezdete egy működő hitelesítő titkosítási módra, amely nemrégiben csúcsosodott ki olyan módokban, mint az OCB és a GCM.

És most az érvek a kosár oldaláról:

A Telegram mögött álló csapat, Nikolai Durov vezetésével, hat ACM-bajnokból áll, akiknek fele matematikából doktorált. Körülbelül két évbe telt az MTProto jelenlegi verziójának bevezetése.

Ez vicces. Két év az alsó szinten

Vagy csak tls-t vehetsz

Oké, tegyük fel, hogy elvégeztük a titkosítást és az egyéb árnyalatokat. Lehet végre TL-ben szerializált kéréseket küldeni és a válaszokat deszerializálni? Szóval mit és hogyan kell küldeni? Íme, mondjuk a módszer initConnection, talán ez az?

Vaszilij, [25.06.18 18:46] Inicializálja a kapcsolatot, és elmenti az információkat a felhasználó eszközén és alkalmazásán.

Elfogadja az app_id, device_model, system_version, app_version és lang_code paramétereket.

És egy kis lekérdezés

Dokumentáció, mint mindig. Nyugodtan tanulmányozza a nyílt forráskódot

Ha minden nagyjából egyértelmű volt az invokeWithLayerrel, akkor mi a baj? Kiderült, tegyük fel, hogy – a kliensnek már volt mit kérdeznie a szervertől – van egy kérés, amit el akartunk küldeni:

Vaszilij, [25.06.18 19:13] A kódból ítélve az első hívás ebbe a szarba, maga a szar pedig invokewithlayerbe van csomagolva

Miért nem lehet az initConnection egy külön hívás, hanem egy wrappernek kell lennie? Igen, mint kiderült, ezt minden alkalommal meg kell tenni minden munkamenet elején, és nem egyszer, mint a fő kulcsnál. De! Illetéktelen felhasználó nem hívhatja fel! Elérkeztünk ahhoz a szakaszhoz, ahol alkalmazható Ezt dokumentációs oldal – és ez azt mondja nekünk, hogy...

Az API-metódusoknak csak egy kis része érhető el illetéktelen felhasználók számára:

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

A legelső közülük, auth.sendCode, és van az a dédelgetett első kérés, amelyben elküldjük az api_id-t és az api_hash-t, és utána kapunk egy kódot tartalmazó SMS-t. És ha rossz DC-ben vagyunk (ebben az országban a telefonszámokat például egy másik szolgálja ki), akkor hibaüzenetet kapunk a kívánt DC számával. Segítsen nekünk, hogy megtudja, melyik IP-címhez DC-szám alapján kell csatlakoznia help.getConfig. Egy időben csak 5 nevezés érkezett, de a 2018-as év jeles eseményei után a szám jelentősen megnőtt.

Most emlékezzünk arra, hogy a szerveren névtelenül jutottunk el idáig. Nem túl drága csak IP-címet szerezni? Miért nem végezheti el ezt és más műveleteket az MTProto titkosítatlan részében? Hallom a kifogást: „Hogyan biztosíthatjuk, hogy ne az RKN válaszoljon hamis címekkel?” Erre emlékezünk, hogy általában a hivatalos ügyfelek Az RSA kulcsok be vannak ágyazva, azaz tudod csak Iratkozz fel ez az információ. Valójában ez már megtörténik a kliensek más csatornákon keresztül kapott blokkolás megkerülésével kapcsolatos információkért (logikailag ez magában az MTProtoban nem lehetséges; tudnia kell, hogy hova kell csatlakozni).

RENDBEN. Az ügyfélengedélyezés jelenlegi szakaszában még nem vagyunk jogosultak, és nem regisztráltuk kérelmünket. Egyelőre csak azt szeretnénk látni, hogy a szerver mit reagál az illetéktelen felhasználók számára elérhető módszerekre. És itt…

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

A rendszerben az első a második

A tdesktop sémában a harmadik érték

Igen, azóta természetesen frissült a dokumentáció. Bár hamarosan újra irrelevánssá válhat. Honnan kell tudnia egy kezdő fejlesztőnek? Lehet, hogy ha regisztrálja a jelentkezését, értesítenek? Vaszilij ezt megtette, de sajnos nem küldtek neki semmit (erről ismét a második részben beszélünk).

...Észrevetted, hogy már valahogy áttértünk az API-ra, pl. a következő szintre, és kihagyott valamit az MTProto témában? Nem meglepetés:

Vaszilij, [28.06.18 02:04] Hm, az e2e néhány algoritmusában turkálnak

Az Mtproto titkosítási algoritmusokat és kulcsokat határoz meg mindkét tartományhoz, valamint egy kicsit a burkolóstruktúrát

De folyamatosan keverik a verem különböző szintjeit, így nem mindig világos, hol ért véget az mtproto és hol kezdődött a következő szint

Hogyan keverednek? Nos, itt van például ugyanaz az ideiglenes kulcs a PFS-hez (egyébként a Telegram Desktop nem tudja megtenni). Egy API-kérés hajtja végre auth.bindTempAuthKey, azaz a legfelső szintről. De ugyanakkor zavarja az alsó szintű titkosítást - például utána újra meg kell tennie initConnection stb., ez nem az éppen normál kérés. Az is különleges, hogy DC-nként csak EGY ideiglenes kulcsod lehet, bár a mező auth_key_id minden üzenetben lehetővé teszi, hogy legalább minden üzenetben módosítsa a kulcsot, és hogy a szervernek joga van bármikor „elfelejteni” az ideiglenes kulcsot – a dokumentáció nem írja le, hogy ilyenkor mit kell tenni... nos, miért is lehetne Nincs több kulcsod, mint egy sor jövőbeli sónál, és?...

Van még néhány dolog, amit érdemes megjegyezni az MTProto témával kapcsolatban.

Üzenetüzenetek, msg_id, msg_seqno, megerősítések, rossz irányú pingek és egyéb sajátosságok

Miért kell tudni róluk? Mert magasabb szintre „szivárognak”, és az API-val való munka során tisztában kell lenni velük. Tegyük fel, hogy nem érdekel minket az msg_key; az alsó szint mindent visszafejtett helyettünk. De a visszafejtett adatokon belül a következő mezők vannak (az adatok hossza is, tehát tudjuk, hol van a kitöltés, de ez nem fontos):

  • só - int64
  • session_id - int64
  • üzenet_azonosítója — int64
  • seq_no - int32

Emlékeztetjük Önöket, hogy az egész DC-hez csak egy só tartozik. Miért kell tudni róla? Nem csak azért, mert van egy kérés get_future_salts, amely megmondja, hogy mely intervallumok lesznek érvényesek, de azért is, mert ha a só „rohadt”, akkor az üzenet (kérés) egyszerűen elveszik. A szerver természetesen kiadással jelenti az új sót new_session_created - de a régivel valahogy újra kell majd küldeni pl. És ez a probléma az alkalmazás architektúráját érinti.

A szerver számos okból megszakíthatja a munkameneteket, és így válaszolhat. Valójában mi az MTProto munkamenet kliens oldalról? Ez két szám session_id и seq_no üzeneteket ezen a munkameneten belül. Nos, és természetesen a mögöttes TCP-kapcsolat. Tegyük fel, hogy ügyfelünk még mindig nem tudja, hogyan kell csinálni sok mindent, lekapcsolta, majd újracsatlakozott. Ha ez gyorsan történt - a régi munkamenet az új TCP-kapcsolatban folytatódott, növelje meg seq_no további. Ha sokáig tart, a szerver törölheti, mert az oldalán ez is egy sor, mint megtudtuk.

Mi legyen az seq_no? Ó, ez egy trükkös kérdés. Próbáld meg őszintén megérteni, hogy mit jelentett:

Tartalomhoz kapcsolódó üzenet

Egy üzenet, amely kifejezett visszaigazolást igényel. Ezek magukban foglalják az összes felhasználói és sok szolgáltatási üzenetet, gyakorlatilag az összeset, kivéve a tárolókat és a nyugtákat.

Üzenetsorszám (msg_seqno)

Egy 32 bites szám, amely megegyezik a feladó által az üzenet előtt létrehozott „tartalomhoz kapcsolódó” üzenetek számának kétszeresével (azok, amelyek nyugtázást igényelnek, és különösen azok, amelyek nem tárolók), és ezt követően eggyel növelve, ha az aktuális üzenet tartalommal kapcsolatos üzenet. Egy tároló mindig a teljes tartalma után jön létre; ezért sorszáma nagyobb vagy egyenlő, mint a benne foglalt üzenetek sorszáma.

Micsoda cirkusz ez 1-es, majd 2-es növekedéssel?.. Gyanítom, hogy kezdetben „az ACK-nél a legkisebb szignifikáns bitet, a többi egy számot” jelentették, de az eredmény nem egészen ugyanaz - konkrétan kijön, lehet küldeni néhány ugyanazokkal a megerősítésekkel seq_no! Hogyan? Nos, például a szerver küld nekünk valamit, elküldi, mi magunk pedig csendben maradunk, csak az üzeneteinek beérkezését igazoló szervizüzenetekkel válaszolunk. Ebben az esetben a kimenő visszaigazolásainknak ugyanaz lesz a kimenő száma. Ha ismeri a TCP-t, és úgy gondolja, hogy ez valahogy vadul hangzik, de nem tűnik túl vadnak, mert a TCP-ben seq_no nem változik, de a megerősítés megy seq_no a másik oldalon pedig sietni foglak felzaklatni. A megerősítéseket az MTProto tartalmazza NEM on seq_no, mint a TCP-ben, de by msg_id !

Mi ez msg_id, ezek közül a legfontosabbak? Egyedi üzenetazonosító, ahogy a neve is sugallja. Ez egy 64 bites szám, amelynek legalacsonyabb bitjei ismét a „szerver-nem-szerver” varázslattal rendelkeznek, a többi pedig egy Unix időbélyeg, beleértve a tört részt is, 32 bittel balra tolva. Azok. időbélyeg önmagában (és a túlságosan eltérő időpontokkal rendelkező üzeneteket a szerver elutasítja). Ebből kiderül, hogy általában ez egy globális azonosító az ügyfél számára. Tekintettel arra, hogy – emlékezzünk session_id - garantáltan: Az egyik munkamenetre szánt üzenet semmilyen körülmények között nem küldhető másik munkamenetbe. Vagyis kiderül, hogy már van három szint - munkamenet, munkamenet száma, üzenetazonosító. Miért ilyen túlbonyolítás, ez a rejtély nagyon nagy.

Így msg_id szükséges a...

RPC: kérések, válaszok, hibák. Megerősítések.

Amint azt észrevetted, a diagramon sehol nincs speciális "RPC kérés készítése" típus vagy funkció, bár vannak válaszok. Hiszen vannak tartalommal kapcsolatos üzeneteink! vagyis bármilyen az üzenet lehet kérés! Vagy nem lenni. Végül, mindegyikből van msg_id. De vannak válaszok:

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

Itt jelzi, hogy ez melyik üzenetre válaszol. Ezért az API legfelső szintjén emlékeznie kell arra, hogy mi volt a kérésének száma - szerintem nem kell magyarázni, hogy a munka aszinkron, és egyszerre több kérés is folyamatban lehet, a válaszok amelyre tetszőleges sorrendben lehet visszaküldeni? Elvileg ebből és a no worker hibaüzenetekből követhető a mögötte lévő architektúra: a veled TCP kapcsolatot fenntartó szerver egy front-end balancer, továbbítja a kéréseket a háttérrendszereknek és visszagyűjti message_id. Úgy tűnik, hogy itt minden világos, logikus és jó.

Igen?.. És ha belegondolsz? Hiszen magának az RPC-válasznak is van mezője msg_id! Kiabálnunk kell a szerverrel, hogy „nem válaszolsz a válaszomra!”? És igen, mi volt ott a megerősítésekkel? Az oldalról üzenetek üzenetekről megmondja, mi az

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

és ezt mindkét oldalnak meg kell tennie. De nem mindig! Ha RpcResult-ot kapott, az maga is megerősítésként szolgál. Ez azt jelenti, hogy a szerver az MsgsAck üzenettel válaszolhat a kérésére – például: „Megkaptam”. Az RpcResult azonnal reagál. Lehet mindkettő.

És igen, még mindig meg kell válaszolnia a választ! Megerősítés. Ellenkező esetben a szerver kézbesíthetetlennek tekinti, és újra visszaküldi Önnek. Újracsatlakozás után is. De itt természetesen felmerül az időtúllépések kérdése. Nézzük őket egy kicsit később.

Addig is nézzük meg a lehetséges lekérdezés-végrehajtási hibákat.

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

Ó, valaki felkiált, itt egy humánusabb formátum - van egy sor! Nem kell kapkodni. Itt hibák listája, de természetesen nem teljes. Abból megtudjuk, hogy a kód az valami hasonló HTTP hibák (na persze a válaszok szemantikáját nem tartják tiszteletben, helyenként véletlenszerűen oszlanak el a kódok között), és a sor úgy néz ki CAPITAL_LETTERS_AND_NUMBERS. Például PHONE_NUMBER_OCCUPIED vagy FILE_PART_Х_HIÁNYZIK. Nos, még mindig szüksége lesz erre a sorra elemezni. Például FLOOD_WAIT_3600 azt jelenti, hogy várnia kell egy órát, és PHONE_MIGRATE_5, hogy ezzel az előtaggal rendelkező telefonszámot az 5. DC-ben kell regisztrálni. Van egy típusnyelvünk, igaz? Nincs szükségünk egy karakterláncból származó érvre, a szokásosak is megteszik, oké.

Ez megint nem a szolgáltatási üzenetek oldalán található, de ahogy az ennél a projektnél már megszokott, az információ megtalálható egy másik dokumentációs oldalon. vagy gyanút vetni. Először is nézze meg, gépelés/rétegsértés - RpcError beágyazható RpcResult. Miért nem kint? Mit nem vettünk figyelembe?.. Ennek megfelelően hol a garancia arra RpcError NEM lehet beágyazni RpcResult, de legyen közvetlenül vagy más típusba ágyazva?.. És ha nem tud, akkor miért nincs a legfelső szinten, pl. hiányzik req_msg_id ? ..

De folytassuk a szolgáltatási üzenetekkel. Az ügyfél azt gondolhatja, hogy a szerver sokáig gondolkodik, és ezt a csodálatos kérést teheti:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Erre a kérdésre három lehetséges válasz adható, amelyek ismét metszenek a megerősítési mechanizmust; annak megértése, hogy ezeknek minek kell lenniük (és melyek azok a típusok általános listája, amelyek nem igényelnek megerősítést), házi feladatként hagyjuk az olvasóra (megjegyzés: a a Telegram Desktop forráskódja nem teljes).

Drogfüggőség: üzenetállapotok

Általánosságban elmondható, hogy a TL-ben, az MTProto-ban és általában a Telegram-ban sok hely makacsság érzését hagyja maga után, de udvariasságból, tapintatból és másokból puha készségek Udvariasan hallgattunk róla, és cenzúráztuk a párbeszédek trágárságait. Azonban ez a helyОaz oldal nagy része arról szól üzenetek üzenetekről Ez még számomra is megdöbbentő, aki már régóta hálózati protokollokkal dolgozom, és láttam már különböző fokú görbületű bicikliket.

Ártalmatlanul kezdődik, megerősítésekkel. Ezután mesélnek nekünk

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;

Nos, mindenkinek meg kell küzdenie velük, aki elkezd dolgozni az MTProto-val, a "javított - újrafordított - elindított" ciklusban gyakori dolog a számhibák vagy a szerkesztések során elromlott sózás. Itt azonban van két pont:

  1. Ez azt jelenti, hogy az eredeti üzenet elveszett. Létre kell hoznunk néhány sort, ezt később megnézzük.
  2. Mik ezek a furcsa hibaszámok? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64... hol van a többi szám, Tommy?

A dokumentációban ez áll:

A szándék az, hogy az error_code értékeket csoportosítsák (error_code >> 4): például a 0x40 – 0x4f kódok a konténerbontás hibáinak felelnek meg.

de egyrészt eltolódás a másik irányba, másodszor pedig mindegy, hol van a többi kód? A szerző fejében?.. Ezek azonban apróságok.

A függőség az üzenetek állapotáról és az üzenetmásolatokról szóló üzenetekben kezdődik:

  • Üzenetállapot-információkérés
    Ha valamelyik fél egy ideig nem kapott tájékoztatást kimenő üzeneteinek állapotáról, akkor ezt kifejezetten kérheti a másik féltől:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Tájékoztató üzenet az üzenetek állapotáról
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Itt, info egy karakterlánc, amely pontosan egy bájt üzenetállapotot tartalmaz minden egyes üzenethez a bejövő msg_ids listából:

    • 1 = semmit sem tudunk az üzenetről (túl alacsony az msg_id, lehet, hogy a másik fél elfelejtette)
    • 2 = üzenet nem érkezett (az msg_id a tárolt azonosítók tartományába esik; a másik fél azonban biztosan nem kapott ilyen üzenetet)
    • 3 = az üzenet nem érkezett meg (az msg_id túl magas; a másik fél azonban még biztosan nem kapta meg)
    • 4 = üzenet érkezett (vegye figyelembe, hogy ez a válasz egyben a beérkezés visszaigazolása is)
    • +8 = az üzenet már nyugtázva
    • +16 = nyugtázást nem igénylő üzenet
    • +32 = Az RPC-lekérdezés az üzenetben található feldolgozás alatt, vagy a feldolgozás már befejeződött
    • +64 = tartalomhoz kapcsolódó válasz a már generált üzenetre
    • +128 = a másik fél pontosan tudja, hogy az üzenet már megérkezett
      Ez a válasz nem igényel elismerést. Ez a vonatkozó msgs_state_req elismerése, önmagában.
      Vegye figyelembe, hogy ha hirtelen kiderül, hogy a másik félnek nincs olyan üzenete, mintha elküldték volna, az üzenetet egyszerűen újra elküldheti. Még akkor is, ha a másik fél egyidejűleg két példányt is kap az üzenetből, a másolatot a rendszer figyelmen kívül hagyja. (Ha túl sok idő telt el, és az eredeti msg_id már nem érvényes, az üzenetet az msg_copy-ba kell csomagolni).
  • Az üzenetek állapotának önkéntes közlése
    Bármelyik fél önkéntesen tájékoztathatja a másik felet a másik fél által továbbított üzenetek állapotáról.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Egy üzenet állapotának kiterjesztett önkéntes közlése
    ...
    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;
  • Kifejezett kérés az üzenetek újraküldésére
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    A távoli fél azonnal válaszol a kért üzenetek újbóli elküldésével […]
  • Kifejezett kérés a válaszok újraküldésére
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    A távoli fél azonnal válaszol újraküldéssel válaszok a kért üzenetekre […]
  • Üzenetmásolatok
    Egyes helyzetekben a már nem érvényes msg_id azonosítójú régi üzenetet újra el kell küldeni. Ezután egy másolótartályba csomagolják:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    A beérkezés után az üzenet feldolgozása úgy történik, mintha nem lenne ott a csomagolóanyag. Ha azonban biztosan tudjuk, hogy az orig_message.msg_id üzenet érkezett, akkor az új üzenet feldolgozása nem történik meg (miközben az üzenet és az orig_message.msg_id is nyugtázásra kerül). Az orig_message.msg_id értékének kisebbnek kell lennie a tároló msg_id értékénél.

Még azt is hallgassuk el, hogy mit msgs_state_info megint kilógnak a befejezetlen TL fülei (bájtvektor kellett, és az alsó két bitben enum volt, a felső két bitben pedig flagek). A lényeg más. Érti valaki, hogy miért van mindez a gyakorlatban? egy igazi ügyfélben szükséges?.. Nehezen, de elképzelhető némi haszon, ha egy személy hibakereséssel foglalkozik, és interaktív módban - kérdezze meg a szervert, hogy mit és hogyan. De itt le vannak írva a kérések körút.

Ebből következik, hogy minden félnek nemcsak titkosítania és elküldenie kell az üzeneteket, hanem adatokat is kell tárolnia magáról, a rájuk adott válaszokról, ismeretlen ideig. A dokumentáció nem írja le sem ezeknek a funkcióknak az időzítését, sem a gyakorlati alkalmazhatóságát. semmiképpen. A legcsodálatosabb az, hogy a hivatalos ügyfelek kódjában valóban használatosak! Nyilván olyat mondtak nekik, ami nem szerepelt a nyilvános dokumentációban. Értsd meg a kódból miért, már nem olyan egyszerű, mint a TL esetében - ez nem egy (viszonylag) logikailag elszigetelt rész, hanem az alkalmazás architektúrához kötött darab, pl. lényegesen több időt igényel az alkalmazás kódjának megértése.

Pingek és időzítések. Sorok.

Mindenből, ha emlékszünk a szerver architektúrával kapcsolatos találgatásokra (a kérések eloszlása ​​a háttérrendszerek között), egy meglehetősen szomorú dolog következik - a TCP-ben minden kézbesítési garancia ellenére (vagy kézbesítik az adatokat, vagy értesülsz a hiányosságról, de az adatokat a probléma fellépése előtt kézbesítjük), hogy magában az MTProto-ban visszaigazolások - nincs garancia. A szerver könnyen elveszítheti vagy kidobhatja az üzenetet, és ez ellen semmit sem lehet tenni, csak különböző típusú mankókat kell használni.

És mindenekelőtt - üzenetsorok. Nos, egy dologban minden nyilvánvaló volt a kezdetektől fogva: egy meg nem erősített üzenetet el kell tárolni és újra el kell küldeni. És mennyi idő után? És a bolond ismeri őt. Talán ezek a függő szolgáltatási üzenetek valahogy megoldják ezt a problémát mankóval, mondjuk a Telegram Desktopban körülbelül 4 sor felel meg nekik (talán több is, mint már említettük, ehhez komolyabban kell elmélyedni a kódjában és az architektúrában; ugyanakkor Tudjuk, hogy nem vehető mintaként, az MTProto sémából bizonyos számú típust nem használnak benne).

Miért történik ez? Valószínűleg a kiszolgáló programozói nem tudták biztosítani a fürtön belüli megbízhatóságot, vagy akár az elülső kiegyenlítő pufferolását, és ezt a problémát átvitték a kliensre. Kétségbeesésből Vaszilij megpróbált egy alternatív lehetőséget megvalósítani, mindössze két sorral, a TCP algoritmusaival - mérte az RTT-t a szerverhez, és beállította az „ablak” méretét (üzenetekben) a meg nem erősített kérések számától függően. Vagyis egy ilyen durva heurisztika a szerver terhelésének felmérésére az, hogy hány kérésünket képes egyszerre megrágni, és nem veszíteni.

Nos, ez érti, ugye? Ha a TCP-t újra meg kell valósítania egy TCP-n futó protokollon, az nagyon rosszul megtervezett protokollt jelez.

Ó, igen, miért kell egynél több sor, és mit jelent ez egy magas szintű API-val dolgozó ember számára? Nézze, kérelmet nyújt be, sorosozza, de gyakran nem tudja azonnal elküldeni. Miért? Mert a válasz az lesz msg_id, ami átmenetiаCímke vagyok, aminek a kiosztását a lehető legkésőbbre halasztani - hátha a szerver elutasítja a köztünk és közte lévő idő eltérése miatt (persze csinálhatunk olyan mankót, ami eltolja az időnket a jelentől a szerverhez a szerver válaszaiból számított delta hozzáadásával - a hivatalos kliensek ezt teszik, de ez a pufferelés miatt nyers és pontatlan). Ezért amikor a könyvtárból helyi függvényhívással kér egy kérést, az üzenet a következő szakaszokon megy keresztül:

  1. Egy sorban fekszik, és titkosításra vár.
  2. Kijelölt msg_id és az üzenet egy másik sorba került - lehetséges továbbítás; küldje el az aljzatba.
  3. a) A szerver MsgsAck-et válaszolt - az üzenetet kézbesítettük, töröljük az „egyéb sorból”.
    b) Vagy fordítva, valami nem tetszett neki, rossz üzenetet válaszolt - újraküldés „egy másik sorból”
    c) Semmi sem ismert, az üzenetet egy másik sorból újra el kell küldeni - de nem tudni, hogy pontosan mikor.
  4. A szerver végül válaszolt RpcResult - a tényleges válasz (vagy hiba) - nemcsak kézbesítve, hanem feldolgozva is.

Talán, konténerek használata részben megoldhatja a problémát. Ez az, amikor egy csomó üzenetet egybe csomagolnak, és a szerver egyben, egyben mindegyikre megerősítést válaszolt. msg_id. De ezt a csomagot is teljes egészében elutasítja, ha valami elromlott.

És ezen a ponton nem technikai megfontolások jönnek szóba. Tapasztalatból sok mankót láttunk, ráadásul most még több példát fogunk látni a rossz tanácsokra, építészetre – ilyen körülmények között érdemes bízni és ilyen döntéseket hozni? A kérdés költői (persze nem).

Miről beszélünk? Ha a „kábítószer-üzenetek az üzenetekről” témában még mindig tud olyan ellenvetésekkel spekulálni, mint „hülye vagy, nem értetted meg a zseniális tervünket!” (tehát előbb írjátok meg a dokumentációt, ahogy normális embernek kell, indoklással és a csomagcsere példáival, aztán megbeszéljük), aztán az időzítések/időtúllépések tisztán gyakorlati és konkrét kérdés, itt már régen minden ismert. Mit mond a dokumentáció az időtúllépésekről?

A szerver általában egy RPC-válasz segítségével nyugtázza az ügyféltől érkező üzenet (általában RPC-lekérdezés) fogadását. Ha a válasz hosszú ideig érkezik, a szerver először küldhet egy nyugtát, majd valamivel később magát az RPC-választ.

A kliens általában úgy nyugtázza a szervertől érkező üzenet (általában RPC-válasz) vételét, hogy a következő RPC-lekérdezéshez egy nyugtát ad, ha az nem kerül továbbításra túl későn (ha az üzenet a fogadás után 60-120 másodperccel jön létre a szervertől érkező üzenet). Ha azonban hosszú ideig nincs ok arra, hogy üzeneteket küldjön a szervernek, vagy ha nagy számú nyugtázatlan üzenet érkezik a szervertől (mondjuk 16 felett), akkor a kliens önálló nyugtát küld.

... Lefordítom: mi magunk sem tudjuk, mennyi és hogyan kell, ezért tegyük fel, hogy legyen ilyen.

És a pingekről:

Ping üzenetek (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

A válasz általában ugyanarra a kapcsolatra érkezik vissza:

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

Ezek az üzenetek nem igényelnek nyugtázást. A pong csak a pingre válaszul kerül továbbításra, míg a ping bármelyik oldal kezdeményezhet.

Elhalasztott kapcsolatzárás + PING

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

Úgy működik, mint a ping. Ezen túlmenően, miután ez megérkezik, a szerver elindít egy időzítőt, amely másodpercekkel később lezárja az aktuális kapcsolatot, hacsak nem kap új, azonos típusú üzenetet, amely automatikusan visszaállítja az összes korábbi időzítőt. Ha az ügyfél például 60 másodpercenként egyszer elküldi ezeket a pingeket, akkor a disconnect_delay értéket 75 másodpercre állíthatja be.

Őrült vagy?! 60 másodpercen belül a vonat belép az állomásra, leszáll és felveszi az utasokat, majd ismét elveszíti a kapcsolatot az alagútban. 120 másodperc múlva, amíg hallod, egy másikhoz érkezik, és nagy valószínűséggel megszakad a kapcsolat. Nos, egyértelmű, hogy honnan jönnek a lábak - „Csengést hallottam, de nem tudom, hol van”, ott van a Nagl algoritmus és a TCP_NODELAY opció, amelyet interaktív munkára szántak. De elnézést kérek, tartsa meg az alapértelmezett értéket - 200 Millimásodpercig Ha valóban szeretne valami hasonlót ábrázolni, és spórolni néhány lehetséges csomagon, akkor halassza el 5 másodpercre, vagy bármi másra, a „Felhasználó gépel...” üzenet időkorlátja most van. De nem több.

És végül ping. Vagyis a TCP kapcsolat életképességének ellenőrzése. Vicces, de úgy 10 éve írtam egy kritikus szöveget karunk kollégiumának hírnökéről – az ottani szerzők is a kliensről pingálták a szervert, és nem fordítva. De egy dolog a 3. éves hallgató, és más a nemzetközi iroda, nem igaz?

Először is egy kis oktatási program. Egy TCP kapcsolat csomagcsere hiányában hetekig is élhet. Ez jó és rossz is, a céltól függően. Jó, ha nyitva volt a szerver felé SSH kapcsolat, felálltál a gépről, újraindítottad a routert, visszatértél a helyedre - ezen a szerveren keresztül nem szakadt el a munkamenet (nem írtál be semmit, nem voltak csomagok) , kényelmes. Rossz, ha több ezer kliens van a szerveren, mindegyik erőforrást foglal el (helló, Postgres!), és a kliens gazdagépe már régen újraindulhatott – de erről nem fogunk tudni.

A chat/IM rendszerek egy további ok miatt esnek a második esetbe – az online állapotok miatt. Ha a felhasználó „leesett”, erről tájékoztatnia kell beszélgetőpartnereit. Ellenkező esetben a Jabber készítői által elkövetett (és 20 éven át javított) hibához fog vezetni – a felhasználó megszakadt, de továbbra is üzeneteket írnak neki, azt hiszik, hogy online van (ami szintén teljesen elveszett ezekben). néhány perccel azelőtt, hogy a kapcsolat megszakadt volna). Nem, a TCP_KEEPALIVE opció, amelyet sok olyan ember, aki nem érti a TCP időzítők működését, véletlenszerűen dobja be (a vad értékek, például több tíz másodperc beállításával), itt nem fog segíteni – meg kell győződnie arról, hogy ne csak az operációs rendszer kernel a felhasználó gépének része él, de normálisan működik, válaszol, és maga az alkalmazás (szerinted nem tud lefagyni? Ubuntu 18.04-en a Telegram Desktop nem egyszer lefagyott nekem).

Ezért kell pingelni szerveren kliens, és nem fordítva - ha a kliens ezt teszi, ha a kapcsolat megszakad, a ping nem érkezik meg, a cél nem érhető el.

Mit látunk a Telegramon? Pont az ellenkezője! Nos, ez van. Formálisan persze mindkét fél pingálhatja egymást. A gyakorlatban az ügyfelek mankót használnak ping_delay_disconnect, amely beállítja az időzítőt a szerveren. Nos, elnézést, nem az ügyfél dönti el, meddig akar ott lakni ping nélkül. A szerver a terhelése alapján jobban tudja. De persze ha nem bánod az erőforrásokat, akkor te leszel a saját gonosz Pinokkió, és egy mankó is megteszi...

Hogyan kellett volna megtervezni?

Úgy gondolom, hogy a fenti tények egyértelműen azt mutatják, hogy a Telegram/VKontakte csapata nem túl kompetens a számítógépes hálózatok szállítása (és alacsonyabb szintű) területén, és alacsony képzettsége a releváns kérdésekben.

Miért lett ilyen bonyolult, és hogyan próbálhatnak kifogást emelni a Telegram építészei? Az, hogy megpróbáltak olyan munkamenetet készíteni, amely túléli a TCP kapcsolat megszakadásait, vagyis ami most nem volt kézbesítve, azt később szállítjuk. Valószínűleg megpróbáltak UDP-transzportot is készíteni, de nehézségekbe ütköztek és elhagyták (ezért üres a dokumentáció - nem volt mit dicsekedni). De a hálózatok általában és különösen a TCP működésével kapcsolatos félreértés miatt, ahol támaszkodhat rá, és hol kell saját maga megtennie (és hogyan), valamint az a kísérlet, hogy ezt a titkosítással kombinálják „két legyet egy csapásra, ” az eredmény ekkora holttest lett.

Hogy volt rá szükség? Az alapján, hogy msg_id egy kriptográfiai szempontból szükséges időbélyeg a visszajátszási támadások megelőzésére, hiba egyedi azonosító funkciót csatolni hozzá. Ezért a jelenlegi architektúra alapvető megváltoztatása nélkül (amikor a Frissítések adatfolyam generálódik, ez egy magas szintű API-téma ennek a bejegyzéssorozatnak egy másik részéhez), a következőket kell tenni:

  1. A kliens felé a TCP-kapcsolatot tartó szerver vállalja a felelősséget – ha olvasott a socketből, kérjük nyugtázza, dolgozza fel vagy küldjön hibát, veszteség nélkül. Ekkor a megerősítés nem azonosítók vektora, hanem egyszerűen „az utoljára kapott seq_no” - csak egy szám, mint a TCP-ben (két szám - a te szekvenciád és a megerősített). Mindig a munkameneten belül vagyunk, nem?
  2. Az ismétléses támadások megelőzésére szolgáló időbélyeg külön mezővé válik, a la nonce. Ellenőrizve van, de semmi mást nem befolyásol. Elég és uint32 - ha a sónk legalább félnaponként változik, akkor 16 bitet az aktuális idő egész részének alacsony rendű bitjeihez rendelhetünk, a többit - a másodperc töredékéhez (mint most is).
  3. Eltávolítva msg_id egyáltalán - a háttérrendszereken lévő kérések megkülönböztetése szempontjából egyrészt létezik az ügyfélazonosító, másrészt a munkamenet azonosítója, fűzze össze őket. Ennek megfelelően csak egy dolog elegendő kérésazonosítóként seq_no.

Ez sem a legsikeresebb lehetőség, egy teljes véletlenszerű azonosító szolgálhat – ez egyébként a magas szintű API-ban már megtörténik üzenetküldéskor. Jobb lenne, ha az architektúrát relatívról abszolútra teljesen átdolgoznánk, de ez egy másik rész témája, nem ennek a bejegyzésnek.

API?

Ta-daam! Így a fájdalommal és mankóval teli ösvényen átküzdve végre el tudtunk küldeni bármilyen kérést a szervernek és választ kapni rájuk, valamint frissítéseket kapni a szervertől (nem kérésre, hanem magától). küld nekünk, mint például a PUSH, ha valakinek ez így érthetőbb).

Figyelem, most az egyetlen példa Perlben lesz a cikkben! (Aki nem ismeri a szintaxist, a bless első argumentuma az objektum adatszerkezete, a második az osztálya):

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

Igen, nem szándékosan spoiler – ha még nem olvastad, tedd meg!

Ó, várj~~... ez hogy néz ki? Valami nagyon ismerős... talán ez egy tipikus webes API adatszerkezete a JSON-ban, kivéve, hogy osztályok is vannak az objektumokhoz csatolva?...

Szóval ez így alakul... Mi ez az egész, elvtársak?.. Ennyi erőfeszítés - és megálltunk pihenni, ahol a webprogramozók csak most kezdődik?..Nem lenne egyszerűbb a HTTPS-en keresztüli JSON?! Mit kaptunk cserébe? Megérte az erőfeszítés?

Nézzük meg, mit adott nekünk a TL+MTProto, és milyen alternatívák lehetségesek. Nos, a HTTP, amely a kérés-válasz modellre összpontosít, rosszul illeszkedik, de legalább valami a TLS-n felül?

Kompakt szerializálás. Látva ezt a JSON-hoz hasonló adatstruktúrát, eszembe jut, hogy vannak bináris verziói is. Az MsgPack-et jelöljük meg elégtelenül bővíthetőnek, de van például CBOR - egyébként egy szabványban leírtak. RFC 7049. Arról nevezetes, hogy meghatározza címkék, mint tágulási mechanizmus, és között már szabványosított vannak:

  • 25 + 256 - az ismétlődő sorok cseréje a sorszámra való hivatkozással, ilyen olcsó tömörítési módszer
  • 26 - sorosított Perl objektum osztálynévvel és konstruktor argumentumokkal
  • 27 - szerializált nyelvfüggetlen objektum típusnévvel és konstruktor argumentumokkal

Nos, megpróbáltam ugyanazokat az adatokat szerializálni TL-ben és CBOR-ban, engedélyezve a karakterlánc- és objektumcsomagolást. Az eredmény valahol egy megabájttól kezdett változni a CBOR javára:

cborlen=1039673 tl_len=1095092

Így következtetés: Vannak lényegesen egyszerűbb formátumok, amelyekre nem vonatkozik a szinkronizálási hiba vagy az ismeretlen azonosító, összehasonlítható hatékonysággal.

Gyors kapcsolat létrehozása. Ez nulla RTT-t jelent az újracsatlakozás után (amikor a kulcs már egyszer generált) - a legelső MTProto üzenettől kezdve, de némi fenntartással - ugyanazt a sót üti, nem rohad a munkamenet stb. Mit kínál helyettünk a TLS? Idézet a témához:

Ha PFS-t használ a TLS-ben, a TLS munkamenet jegyek (RFC 5077) a titkosított munkamenet folytatásához a kulcsok újratárgyalása és a kulcsadatok szerveren való tárolása nélkül. Az első kapcsolat megnyitásakor és a kulcsok létrehozásakor a szerver titkosítja a kapcsolat állapotát és továbbítja azt a kliensnek (munkamenetjegy formájában). Ennek megfelelően, amikor a kapcsolat újraindul, a kliens egy munkamenetjegyet küld vissza a szervernek, beleértve a munkamenetkulcsot is. Maga a jegy egy ideiglenes kulccsal (session ticket key) van titkosítva, amelyet a szerver tárol, és el kell osztani az összes SSL-t fürtözött megoldásokban feldolgozó frontend szerver között.[10] Így a munkamenetjegy bevezetése sértheti a PFS-t, ha például az ideiglenes szerverkulcsok sérülnek, például hosszú ideig tárolva (az OpenSSL, nginx, Apache alapértelmezés szerint a program teljes időtartama alatt tárolja őket; a népszerű oldalak a kulcsot több órán keresztül, akár napokig).

Itt az RTT nem nulla, ki kell cserélni legalább a ClientHello-t és a ServerHello-t, ami után a kliens a Finished-el együtt küldhet adatokat. De itt nem szabad elfelejtenünk, hogy nálunk nem a Web van, újonnan megnyílt kapcsolataival, hanem egy messenger, aminek a csatlakozása sokszor egy és többé-kevésbé hosszú életű, viszonylag rövid weblapokra irányuló kérés - minden multiplex. belsőleg. Vagyis teljesen elfogadható, ha nem egy nagyon rossz metrószakasszal találkoztunk.

Elfelejtett valamit? Írd meg kommentben.

Folytatjuk!

A bejegyzéssorozat második részében nem technikai, hanem szervezési kérdéseket fogunk megvizsgálni - megközelítések, ideológia, interfész, felhasználókhoz való hozzáállás stb. Az itt bemutatott technikai információk alapján azonban.

A harmadik rész továbbra is a műszaki komponens / fejlesztési tapasztalat elemzését folytatja. Megtanulod különösen:

  • a pandemonium folytatása a TL típusok sokféleségével
  • ismeretlen dolgok a csatornákkal és szupercsoportokkal kapcsolatban
  • miért rosszabbak a párbeszédek a névsornál
  • az abszolút vs relatív üzenetcímzésről
  • mi a különbség a fénykép és a kép között
  • hogy az emoji hogyan zavarja a dőlt szöveget

és egyéb mankók! Maradjon velünk!

Forrás: will.com

Hozzászólás