Crítica al protocol i als plantejaments organitzatius de Telegram. Part 1, tècnica: experiència d'escriure un client des de zero - TL, MT

Recentment, a Habré han començat a aparèixer més sovint publicacions sobre com de bo és Telegram, com de brillants i experimentats són els germans Durov en la construcció de sistemes de xarxa, etc. Al mateix temps, molt poques persones s'han submergit realment en el dispositiu tècnic; com a molt, utilitzen una API de bot basat en JSON bastant senzilla (i força diferent de l'MTProto) i normalment només accepten sobre la fe tots els elogis i les relacions públiques que giren al voltant del missatger. Fa gairebé un any i mig, el meu company de l'ONG Eshelon Vasily (malauradament, el seu compte a Habré es va esborrar juntament amb l'esborrany) va començar a escriure el seu propi client de Telegram des de zero en Perl, i més tard es va incorporar l'autor d'aquestes línies. Per què Perl, alguns preguntaran immediatament? Perquè aquests projectes ja existeixen en altres idiomes, de fet, aquest no és el punt, podria haver-hi qualsevol altre idioma on no hi hagi biblioteca ja feta, i en conseqüència l'autor ha d'anar fins al final des de zero. A més, la criptografia és una qüestió de confiança, però verificar. Amb un producte orientat a la seguretat, no podeu confiar simplement en una biblioteca ja feta del fabricant i confiar-hi cegament (no obstant això, aquest és un tema per a la segona part). De moment, la biblioteca funciona força bé a nivell "mitjana" (permet fer qualsevol sol·licitud d'API).

Tanmateix, no hi haurà gaire criptografia o matemàtiques en aquesta sèrie de publicacions. Però hi haurà molts altres detalls tècnics i crosses arquitectòniques (també útils per a aquells que no escriguin des de zero, però utilitzaran la biblioteca en qualsevol idioma). Per tant, l'objectiu principal era intentar implementar el client des de zero segons documentació oficial. És a dir, suposem que el codi font dels clients oficials està tancat (de nou, a la segona part tractarem amb més detall el tema del fet que això és cert. passa així), però, com en els vells temps, per exemple, hi ha un estàndard com RFC: és possible escriure un client només segons l'especificació, "sense mirar" el codi font, ja sigui oficial (Telegram Desktop, mòbil), o Telethon no oficial?

Taula de continguts:

La documentació... existeix, oi? És cert?..

L'estiu passat es van començar a recollir fragments de notes d'aquest article. Tot aquest temps al web oficial https://core.telegram.org La documentació era a partir de la capa 23, és a dir. enganxat en algun lloc del 2014 (recordeu, ni tan sols hi havia canals aleshores?). Per descomptat, en teoria, això ens hauria d'haver permès implementar un client amb funcionalitat en aquell moment el 2014. Però fins i tot en aquest estat, la documentació era, en primer lloc, incompleta i, en segon lloc, en alguns llocs es contradiu. Fa poc més d'un mes, al setembre del 2019, ho va ser accidentalment Es va descobrir que hi havia una gran actualització de la documentació del lloc, per a la capa 105 força recent, amb una nota que ara cal tornar a llegir-ho tot. De fet, molts articles van ser revisats, però molts es van mantenir sense canvis. Per tant, quan llegiu les crítiques a continuació sobre la documentació, heu de tenir en compte que algunes d'aquestes coses ja no són rellevants, però algunes encara ho són bastant. Després de tot, 5 anys al món modern no són només molt de temps, sinó molt molt de. Des d'aquells temps (especialment si no teniu en compte els llocs de geoxat descartats i reviscuts des d'aleshores), el nombre de mètodes API de l'esquema ha crescut de cent a més de dos-cents cinquanta!

Per on començar com a jove autor?

No importa si escriviu des de zero o utilitzeu, per exemple, biblioteques ja fetes com Telethon per a Python o Madeline per a PHP, en qualsevol cas, necessitareu primer registre la seva sol·licitud - obtenir paràmetres api_id и api_hash (aquells que han treballat amb l'API VKontakte entenen immediatament) pel qual el servidor identificarà l'aplicació. Això haver de feu-ho per raons legals, però a la segona part parlarem més sobre per què els autors de biblioteques no poden publicar-lo. Pot ser que estigueu satisfet amb els valors de la prova, tot i que són molt limitats; el fet és que ara us podeu registrar només un aplicació, així que no us precipiteu.

Ara, des del punt de vista tècnic, ens hauria d'interessar que després de registrar-nos haguéssim de rebre notificacions de Telegram sobre actualitzacions de documentació, protocol, etc. És a dir, es podria suposar que el lloc amb els molls estava simplement abandonat i es va continuar treballant específicament amb els que van començar a fer clients, perquè és més fàcil. Però no, no es va observar res semblant, no va arribar cap informació.

I si escriviu des de zero, l'ús dels paràmetres obtinguts encara està molt lluny. Encara que https://core.telegram.org/ i parla d'ells a Primer de tot, de fet, primer hauràs d'implementar-los Protocol MTProto - però si creguessis disseny segons el model OSI al final de la pàgina per obtenir una descripció general del protocol, llavors és completament en va.

De fet, tant abans com després de MTProto, a diversos nivells alhora (com diuen els usuaris de xarxa estrangers que treballen al nucli del sistema operatiu, violació de la capa), un tema gran, dolorós i terrible s'interposarà en el camí...

Serialització binària: TL (Type Language) i el seu esquema, i capes, i moltes altres paraules de por

Aquest tema, de fet, és la clau dels problemes de Telegram. I hi haurà moltes paraules terribles si intentes aprofundir-hi.

Així doncs, aquí teniu el diagrama. Si aquesta paraula et ve al cap, digues: Esquema JSON, has pensat correctament. L'objectiu és el mateix: algun llenguatge per descriure un possible conjunt de dades transmeses. Aquí és on acaben les semblances. Si de la pàgina Protocol MTProto, o des de l'arbre font del client oficial, intentarem obrir algun esquema, veurem alguna cosa com:

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;

Una persona que vegi això per primera vegada serà capaç de reconèixer intuïtivament només una part del que està escrit; bé, aparentment són estructures (tot i que on és el nom, a l'esquerra o a la dreta?), hi ha camps, després de la qual un tipus segueix després de dos punts... probablement. Aquí entre claudàtors angulars probablement hi hagi plantilles com en C++ (de fet, no realment). I què signifiquen tots els altres símbols, signes d'interrogació, signes d'exclamació, percentatges, signes hash (i, òbviament, signifiquen coses diferents en diferents llocs), de vegades presents i de vegades no, nombres hexadecimals, i el més important, com treure'n d'això correcte (que no serà rebutjat pel servidor) flux de bytes? Haureu de llegir la documentació (sí, hi ha enllaços a l'esquema a la versió JSON a prop, però això no ho fa més clar).

Obriu la pàgina Serialització de dades binàries i submergir-se en el món màgic dels bolets i les matemàtiques discretes, una cosa semblant al matan de 4t curs. Alfabet, tipus, valor, combinador, combinador funcional, forma normal, tipus compost, tipus polimòrfic... i això és només la primera pàgina! El següent t'espera Llenguatge TL, que, tot i que ja conté un exemple de petició i resposta trivials, no dóna resposta en absolut a casos més típics, la qual cosa vol dir que hauràs de recórrer un relat de matemàtiques traduïts del rus a l'anglès en altres vuit incrustats. pàgines!

Els lectors familiaritzats amb els llenguatges funcionals i la inferència de tipus automàtica, per descomptat, veuran el llenguatge de descripció en aquest idioma, fins i tot a partir de l'exemple, com molt més familiar, i poden dir que en principi això no és dolent. Les objeccions a això són:

  • sí, el propòsit sona bé, però per desgràcia, ella no aconseguit
  • L'educació a les universitats russes varia fins i tot entre les especialitats de TI: no tothom ha fet el curs corresponent
  • Finalment, com veurem, a la pràctica ho és no obligatori, ja que només s'utilitza un subconjunt limitat fins i tot del TL que es va descriure

Com s’ha dit LeonNerd al canal #perl a la xarxa FreeNode IRC, que va intentar implementar una porta de Telegram a Matrix (la traducció de la cita no és precisa de la memòria):

Sembla que algú es va introduir a la teoria de tipus per primera vegada, es va emocionar i va començar a intentar jugar-hi, sense importar-li realment si era necessari a la pràctica.

Comproveu vosaltres mateixos, si la necessitat dels tipus nus (int, long, etc.) com a element elemental no planteja preguntes, en última instància, s'han d'implementar manualment, per exemple, intentem derivar-ne. vector. És a dir, de fet, matriu, si anomeneu les coses resultants pel seu nom propi.

Però abans

Una breu descripció d'un subconjunt de sintaxi de TL per a aquells que no llegeixen la documentació oficial

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;

La definició sempre comença dissenyador, després del qual opcionalment (a la pràctica - sempre) mitjançant el símbol # ha de ser CRC32 de la cadena de descripció normalitzada d'aquest tipus. A continuació ve una descripció dels camps; si existeixen, el tipus pot estar buit. Tot això acaba amb un signe igual, el nom del tipus al qual pertany aquest constructor, és a dir, de fet, el subtipus. El noi a la dreta del signe d'igualtat és polimòrfica - és a dir, li poden correspondre diversos tipus específics.

Si la definició es produeix després de la línia ---functions---, aleshores la sintaxi seguirà sent la mateixa, però el significat serà diferent: el constructor es convertirà en el nom de la funció RPC, els camps es convertiran en paràmetres (bé, és a dir, es mantindrà exactament la mateixa estructura donada, tal com es descriu a continuació , aquest serà simplement el significat assignat) i el "tipus polimòrfic" - el tipus del resultat retornat. És cert que encara es mantindrà polimòrfica, només es defineix a la secció ---types---, però aquest constructor "no serà considerat". Sobrecàrrega dels tipus de funcions cridades pels seus arguments, és a dir. Per alguna raó, diverses funcions amb el mateix nom però signatures diferents, com en C++, no estan previstes al TL.

Per què "constructor" i "polimòrfic" si no és OOP? Bé, de fet, serà més fàcil per a algú pensar en això en termes OOP: un tipus polimòrfic com a classe abstracta, i els constructors són les seves classes descendents directes, i final en la terminologia de diverses llengües. De fet, és clar, només aquí semblança amb mètodes constructors reals sobrecarregats en llenguatges de programació OO. Com que aquí només hi ha estructures de dades, no hi ha mètodes (tot i que la descripció de funcions i mètodes més enllà és capaç de crear confusió al capdavant que existeixen, però això és una qüestió diferent): podeu pensar en un constructor com un valor de quin s'està construint escriviu quan llegiu un flux de bytes.

Com passa això? El deserialitzador, que sempre llegeix 4 bytes, veu el valor 0xcrc32 - i entén què passarà després field1 amb tipus int, és a dir llegeix exactament 4 bytes, en aquest el camp superior amb el tipus PolymorType llegir. veu 0x2crc32 i entén que hi ha dos camps més enllà, primer long, el que significa que llegim 8 bytes. I de nou un tipus complex, que es deserialitza de la mateixa manera. Per exemple, Type3 podrien declarar-se al circuit tan aviat com dos constructors, respectivament, s'hauran de trobar qualsevol 0x12abcd34, després del qual heu de llegir 4 bytes més intO 0x6789cdef, després del qual no hi haurà res. Qualsevol altra cosa: heu de llançar una excepció. De totes maneres, després d'això tornem a llegir 4 bytes int camps field_c в constructorTwo i amb això acabem de llegir el nostre PolymorType.

Finalment, si et deixen atrapar 0xdeadcrc per constructorThree, aleshores tot es complica. El nostre primer camp és bit_flags_of_what_really_present amb tipus # - de fet, això és només un àlies per al tipus nat, que significa "nombre natural". És a dir, de fet, unsigned int és, per cert, l'únic cas quan els nombres sense signe es produeixen en circuits reals. Per tant, a continuació hi ha una construcció amb un signe d'interrogació, el que significa que aquest camp només estarà present al cable si el bit corresponent s'estableix al camp referit (aproximadament com un operador ternari). Per tant, suposem que aquest bit s'ha establert, el que significa que més hem de llegir un camp com Type, que en el nostre exemple té 2 constructors. Un està buit (només està format per l'identificador), l'altre té un camp ids amb tipus ids:Vector<long>.

Podríeu pensar que tant les plantilles com els genèrics es troben en els pros o en Java. Però no. Gairebé. Això l’únic cas d'utilitzar claudàtors angulars en circuits reals, i només s'utilitza per a Vector. En un flux de bytes, aquests seran 4 CRC32 bytes per al propi tipus de Vector, sempre el mateix, després 4 bytes: el nombre d'elements de la matriu, i després aquests mateixos elements.

Afegiu-hi el fet que la serialització sempre es produeix en paraules de 4 bytes, tots els tipus en són múltiples; també es descriuen els tipus integrats. bytes и string amb la serialització manual de la longitud i aquesta alineació per 4 - bé, sembla que sona normal i fins i tot relativament efectiu? Tot i que es diu que TL és una serialització binària eficaç, a l'infern, amb l'expansió de gairebé qualsevol cosa, fins i tot els valors booleans i les cadenes d'un sol caràcter a 4 bytes, JSON encara serà molt més gruixut? Mireu, fins i tot els camps innecessaris es poden saltar amb senyals de bits, tot és força bo, i fins i tot extensible per al futur, així que per què no afegir nous camps opcionals al constructor més tard?

Però no, si llegiu no la meva breu descripció, sinó la documentació completa, i penseu en la implementació. En primer lloc, el CRC32 del constructor es calcula segons la línia normalitzada de la descripció de text de l'esquema (elimina espais en blanc addicionals, etc.), de manera que si s'afegeix un camp nou, la línia de descripció del tipus canviarà i, per tant, el seu CRC32 i , en conseqüència, serialització. I què faria l'antic client si rebés un camp amb banderes noves posades i no sap què fer-hi després?...

En segon lloc, recordem-ho CRC32, que s'utilitza aquí essencialment com a funcions de hash per determinar de manera única quin tipus s'està (des)serialitzant. Aquí ens trobem davant del problema de les col·lisions, i no, la probabilitat no és una entre 232, sinó molt més gran. Qui va recordar que CRC32 està dissenyat per detectar (i corregir) errors en el canal de comunicació i, en conseqüència, millora aquestes propietats en detriment d'altres? Per exemple, no li importa reordenar els bytes: si calculeu CRC32 a partir de dues línies, a la segona intercanvieu els primers 4 bytes amb els següents: serà el mateix. Quan la nostra entrada són cadenes de text de l'alfabet llatí (i una mica de puntuació), i aquests noms no són especialment aleatoris, la probabilitat d'aquesta reordenació augmenta molt.

Per cert, qui va comprovar què hi havia? de veritat CRC32? Un dels primers codis font (fins i tot abans de Waltman) tenia una funció hash que multiplicava cada caràcter pel nombre 239, tan estimat per aquesta gent, ha ha!

Finalment, d'acord, ens vam adonar que els constructors amb un tipus de camp Vector<int> и Vector<PolymorType> tindrà un CRC32 diferent. Què passa amb el rendiment en línia? I des d'un punt de vista teòric, això passa a formar part del tipus? Suposem que passem una matriu de deu mil nombres, bé amb Vector<int> tot està clar, la longitud i 40000 bytes més. I si això Vector<Type2>, que només consta d'un camp int i està sol en el tipus: hem de repetir 10000xabcdef0 34 vegades i després 4 bytes? int, o el llenguatge ens pot INDEPENDIR del constructor fixedVec i en lloc de 80000 bytes, transferir de nou només 40000?

Aquesta no és una pregunta teòrica ociosa: imagineu-vos que rebeu una llista d'usuaris del grup, cadascun dels quals té un identificador, nom i cognoms; la diferència en la quantitat de dades transferides a través d'una connexió mòbil pot ser significativa. És precisament l'efectivitat de la serialització de Telegram el que se'ns anuncia.

Tan…

Vector, que mai es va publicar

Si intenteu navegar per les pàgines de descripció de combinadors i així successivament, veureu que un vector (i fins i tot una matriu) està intentant formalment sortir a través de tuples de diversos fulls. Però al final s'obliden, es salta el pas final i simplement es dóna una definició d'un vector, que encara no està lligat a un tipus. Què passa? En idiomes programació, especialment els funcionals, és força típic descriure l'estructura de manera recursiva: el compilador amb la seva avaluació mandrosa ho entendrà i ho farà tot ell mateix. En llengua serialització de dades el que es necessita és EFICIÈNCIA: n'hi ha prou de descriure senzillament список, és a dir estructura de dos elements: el primer és un element de dades, el segon és la mateixa estructura o un espai buit per a la cua (paquet (cons) a Lisp). Però això, òbviament, requerirà de cadascuna L'element gasta 4 bytes addicionals (CRC32 en el cas de TL) per descriure el seu tipus. També es pot descriure fàcilment una matriu mida fixa, però en el cas d'una matriu de longitud desconeguda per endavant, ens interrompem.

Per tant, com que TL no permet sortir un vector, s'havia d'afegir al costat. En definitiva, la documentació diu:

La serialització sempre utilitza el mateix constructor "vector" (const 0x1cb5c415 = crc32 ("vector t:Type # [ t ] = Vector t") que no depèn del valor específic de la variable de tipus t.

El valor del paràmetre opcional t no està implicat en la serialització, ja que es deriva del tipus de resultat (sempre conegut abans de la deserialització).

Fes una ullada més de prop: vector {t:Type} # [ t ] = Vector t - però enlloc Aquesta definició en si no diu que el primer nombre hagi de ser igual a la longitud del vector! I no ve de cap lloc. Això és un fet que cal tenir en compte i implementar amb les mans. En altres llocs, la documentació fins i tot esmenta honestament que el tipus no és real:

El pseudotipus polimòrfic Vector t és un "tipus" el valor del qual és una seqüència de valors de qualsevol tipus t, ja sigui en caixa o nu.

... però no s'hi centra. Quan tu, cansat de passar per l'extensió de les matemàtiques (potser fins i tot les coneixes des d'un curs universitari), decideixes renunciar i mirar com treballar-les a la pràctica, la impressió que et queda al cap és que això és seriós. Les matemàtiques al nucli, van ser clarament inventades per Cool People (dos matemàtics - guanyador de l'ACM), i no qualsevol. L'objectiu - lluir - s'ha assolit.

Per cert, sobre el nombre. Us ho recordem # és un sinònim nat, nombre natural:

Hi ha expressions tipus (tipus-expr) i expressions numèriques (nat-expr). Tanmateix, es defineixen de la mateixa manera.

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

però a la gramàtica es descriuen de la mateixa manera, és a dir. Aquesta diferència s'ha de tornar a recordar i posar en pràctica manualment.

Bé, sí, tipus de plantilles (vector<int>, vector<User>) tenen un identificador comú (#1cb5c415), és a dir si sabeu que la convocatòria s'anuncia com

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

llavors ja no esteu esperant només un vector, sinó un vector d'usuaris. Més precisament, hauria de espera: en codi real, cada element, si no és un tipus nu, tindrà un constructor, i en bona manera en la implementació caldria comprovar-ho, però ens van enviar exactament a tots els elements d'aquest vector aquest tipus? Què passaria si fos algun tipus de PHP, en el qual una matriu pot contenir diferents tipus en diferents elements?

En aquest punt, comenceu a pensar: és necessari un TL? Potser per al carro seria possible fer servir un serialitzador humà, el mateix protobuf que ja existia llavors? Aquesta era la teoria, mirem la pràctica.

Implementacions de TL existents al codi

TL va néixer a les profunditats de VKontakte fins i tot abans dels famosos esdeveniments amb la venda de les accions de Durov i (probablement), fins i tot abans que comencés el desenvolupament de Telegram. I en codi obert codi font de la primera implementació pots trobar un munt de crosses divertides. I el llenguatge en si es va implementar allà més completament que ara a Telegram. Per exemple, els hash no s'utilitzen en absolut a l'esquema (és a dir, un pseudotipus integrat (com un vector) amb un comportament desviat). O

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

però considerem, per tal de ser complet, rastrejar, per dir-ho així, l'evolució del Gegant del Pensament.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

O aquesta bonica:

    static const char *reserved_words_polymorhic[] = {

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

      };

Aquest fragment tracta de plantilles com:

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

Aquesta és la definició d'un tipus de plantilla hashmap com a vector de parells de tipus int. En C++ es veuria com això:

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

tan, alpha - paraula clau! Però només en C++ pots escriure T, però hauries d'escriure alfa, beta... Però no més de 8 paràmetres, aquí s'acaba la fantasia. Sembla que hi havia una vegada a Sant Petersburg uns diàlegs com aquest:

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

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

Però es tractava de la primera implementació publicada de TL "en general". Passem a considerar les implementacions als propis clients de Telegram.

Paraula a Vasily:

Vasily, [09.10.18 17:07] Sobretot, el cul està calent perquè van crear un munt d'abstraccions, i després els van clavar un forrellat i van cobrir el generador de codi amb crosses.
Com a resultat, primer des de dock pilot.jpg
A continuació, des del codi dzhekichan.webp

Per descomptat, de persones familiaritzades amb algorismes i matemàtiques, podem esperar que hagin llegit Aho, Ullmann i estiguin familiaritzats amb les eines que s'han convertit en estàndards de facto a la indústria al llarg de les dècades per escriure els seus compiladors DSL, oi?...

Per telegrama-cli és Vitaly Valtman, com es pot entendre per l'aparició del format TLO fora dels seus límits (cli), un membre de l'equip; ara s'ha assignat una biblioteca per a l'anàlisi de TL per separat, quina és la impressió d'ella Analitzador TL? ..

16.12 04:18 Vasily: Crec que algú no dominava lex+yacc
16.12 04:18 Vasily: No puc explicar-ho d'una altra manera
16.12 04:18 Vasily: bé, o se'ls va pagar pel nombre de línies en VK
16.12 04:19 Vasily: 3k+ línies, etc.<censored> en lloc d'un analitzador

Potser una excepció? A veure com fa Aquest és el client OFICIAL - 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);

Més de 1100 línies en Python, un parell d'expressions regulars + casos especials com un vector, que, per descomptat, es declara a l'esquema com hauria de ser segons la sintaxi TL, però es van basar en aquesta sintaxi per analitzar-la... La pregunta sorgeix, per què va ser tot un miracle?иÉs més en capes si ningú l'analitzarà segons la documentació de totes maneres?!

Per cert... Recordeu que vam parlar de la comprovació CRC32? Per tant, al generador de codi de Telegram Desktop hi ha una llista d'excepcions per a aquells tipus en què es calcula el CRC32 no coincideix amb el que s'indica a l'esquema!

Vasily, [18.12/22 49:XNUMX] i aquí pensaria si cal un TL així
si volgués ficar-me amb implementacions alternatives, començaria a inserir salts de línia, la meitat dels analitzadors es trencaran en definicions de diverses línies.
tdesktop, però, també

Recordeu el punt sobre una línia, hi tornarem una mica més tard.

D'acord, telegram-cli no és oficial, Telegram Desktop és oficial, però què passa amb els altres? Qui sap?... Al codi del client d'Android no hi havia cap analitzador d'esquemes (que planteja preguntes sobre el codi obert, però això és per a la segona part), però hi havia diverses altres peces de codi divertides, però més sobre elles a la subsecció a continuació.

Quines altres preguntes planteja la serialització a la pràctica? Per exemple, van fer moltes coses, és clar, amb camps de bits i camps condicionals:

Vasily: flags.0? true
significa que el camp és present i és igual a cert si la bandera està activada

Vasily: flags.1? int
significa que el camp està present i s'ha de deserialitzar

Vasily: Cul, no et preocupis pel que estàs fent!
Vasily: Hi ha una menció en algun lloc del document que cert és un tipus nu de longitud zero, però és impossible muntar res del seu document.
Vasily: A les implementacions de codi obert tampoc no és així, però hi ha un munt de crosses i suports

Què passa amb Telethon? Mirant cap al tema de MTProto, un exemple: a la documentació hi ha peces d'aquest tipus, però el signe % només es descriu com a "corresponent a un determinat tipus nu", és a dir. als exemples següents hi ha un error o alguna cosa no documentada:

Vasily, [22.06.18 18:38] En un sol lloc:

msg_container#73f1f8dc messages:vector message = MessageContainer;

En una altra:

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

I aquestes són dues grans diferències, a la vida real apareix algun tipus de vector nu

No he vist una definició vectorial nua i no n'he trobat cap

L'anàlisi s'escriu a mà en teletó

En el seu diagrama es comenta la definició msg_container

De nou, la pregunta segueix sent al voltant del %. No està descrit.

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

Vasily, [22.06.18 19:23] Però el seu analitzador TL en motors normals probablement tampoc no menjarà això

// parsed manually

TL és una bella abstracció, ningú la implementa completament

I % no està en la seva versió de l'esquema

Però aquí la documentació es contradiu, així que idk

Es va trobar a la gramàtica, simplement podrien haver-se oblidat de descriure la semàntica

Has vist el document a TL, no ho pots esbrinar sense mig litre

"Bé, diguem", dirà un altre lector, "que critiques alguna cosa, així que mostra'm com s'ha de fer".

Vasily respon: “Pel que fa a l'analitzador, m'agraden coses com

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

d'alguna manera m'agrada millor que

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

o

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

aquest és el lexer COMPLET:

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

aquells. més senzill és dir-ho suaument".

En general, com a resultat, l'analitzador i el generador de codi per al subconjunt de TL realment utilitzat encaixen en aproximadament 100 línies de gramàtica i ~ 300 línies del generador (comptant totes les print's generat), incloent-hi els tipus d'informació per a la introspecció a cada classe. Cada tipus polimòrfic es converteix en una classe base abstracta buida, i els constructors en hereten i tenen mètodes per a la serialització i la deserialització.

Manca de tipus en el llenguatge de tipus

Escriure fort és bo, oi? No, això no és un holivar (tot i que prefereixo els llenguatges dinàmics), sinó un postulat en el marc de la TL. En base a això, l'idioma ens hauria de proporcionar tot tipus de controls. Bé, d'acord, potser no ell mateix, sinó la implementació, però almenys hauria de descriure-les. I quin tipus d'oportunitats volem?

En primer lloc, les limitacions. Aquí veiem a la documentació per pujar fitxers:

Aleshores, el contingut binari del fitxer es divideix en parts. Totes les peces han de tenir la mateixa mida ( mida_part ) i s'han de complir les condicions següents:

  • part_size % 1024 = 0 (divisible per 1 KB)
  • 524288 % part_size = 0 (512 KB han de ser divisibles uniformement per part_size)

L'última part no ha de complir aquestes condicions, sempre que la seva mida sigui inferior a part_size.

Cada part ha de tenir un número de seqüència, part_fitxer, amb un valor que oscil·la entre 0 i 2,999.

Després de particionar el fitxer, heu de triar un mètode per desar-lo al servidor. Ús upload.saveBigFilePart en cas que la mida completa del fitxer sigui superior a 10 MB i upload.saveFilePart per a fitxers més petits.
[…] es pot retornar un dels errors d'entrada de dades següents:

  • FILE_PARTS_INVALID — Nombre de parts no vàlid. El valor no està entre 1..3000

Hi ha alguna cosa d'això al diagrama? És d'alguna manera expressable amb TL? No. Però perdoneu, fins i tot el Turbo Pascal de l'avi va ser capaç de descriure els tipus especificats intervals. I sabia una cosa més, ara més conegut com enum - un tipus que consisteix en una enumeració d'un nombre fix (petit) de valors. En idiomes com C - numèric, tingueu en compte que fins ara només hem parlat de tipus números. Però també hi ha matrius, cadenes... per exemple, estaria bé descriure que aquesta cadena només pot contenir un número de telèfon, oi?

Res d'això està al TL. Però n'hi ha, per exemple, a l'esquema JSON. I si algú més podria discutir sobre la divisibilitat de 512 KB, que això encara s'ha de comprovar al codi, assegureu-vos que el client simplement no podia enviar un número fora de rang 1..3000 (i l'error corresponent no podria haver sorgit) hauria estat possible, oi?...

Per cert, sobre errors i valors de retorn. Fins i tot els que han treballat amb TL es desdibuixen els ulls; això no ens ho vam adonar immediatament cadascú una funció a TL pot retornar no només el tipus de retorn descrit, sinó també un error. Però això no es pot deduir de cap manera utilitzant el propi TL. Per descomptat, ja està clar i no cal res a la pràctica (tot i que de fet, RPC es pot fer de diferents maneres, hi tornarem més endavant) - però què passa amb la puresa dels conceptes de matemàtiques de tipus abstractes? del món celestial?... Vaig agafar el remolcador, així que coincideix.

I finalment, què passa amb la llegibilitat? Bé, allà, en general, m'agradaria descripció Teniu-ho bé a l'esquema (a l'esquema JSON, de nou, ho és), però si ja esteu esforçats amb això, què passa amb el costat pràctic, almenys de manera trivial, mirant les diferències durant les actualitzacions? Comproveu-ho vosaltres mateixos a exemples reals:

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

o

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

Depèn de tothom, però GitHub, per exemple, es nega a destacar els canvis dins de línies tan llargues. El joc “troba 10 diferències”, i el que el cervell veu immediatament és que els inicis i els finals en ambdós exemples són els mateixos, cal llegir tediosament en algun lloc del mig... Al meu entendre, això no és només en teoria, però purament visual brut i descuidat.

Per cert, sobre la puresa de la teoria. Per què necessitem camps de bits? No sembla que ells? olor dolent des del punt de vista de la teoria de tipus? L'explicació es pot veure en versions anteriors del diagrama. Al principi, sí, així era, per cada esternut es creava un nou tipus. Aquests rudiments encara existeixen en aquesta forma, per exemple:

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;

Però ara imagineu-vos, si teniu 5 camps opcionals a la vostra estructura, necessitareu 32 tipus per a totes les opcions possibles. Explosió combinatòria. Així, la puresa cristal·lina de la teoria TL es va trencar una vegada més contra el cul de ferro colat de la dura realitat de la serialització.

A més, en alguns llocs aquests mateixos nois violen la seva pròpia tipologia. Per exemple, a MTProto (següent capítol) la resposta es pot comprimir amb Gzip, tot està bé, excepte que es violen les capes i el circuit. Una vegada més, no va ser el propi RpcResult el que es va collir, sinó el seu contingut. Bé, per què fer això?... Vaig haver de tallar una crossa perquè la compressió funcionés a qualsevol lloc.

O un altre exemple, una vegada vam descobrir un error: es va enviar InputPeerUser en comptes de InputUser. O viceversa. Però va funcionar! És a dir, al servidor no li importava el tipus. Com pot ser això? La resposta ens la poden donar fragments de codi de telegram-cli:

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

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

En altres paraules, aquí és on es fa la serialització MANUALMENT, codi no generat! Potser el servidor s'implementa d'una manera similar?... En principi, això funcionarà si es fa una vegada, però com es pot donar suport més endavant durant les actualitzacions? És per això que es va inventar l'esquema? I aquí passem a la següent pregunta.

Versioning. Capes

Per què les versions esquemàtiques s'anomenen capes només es pot especular a partir de la història dels esquemes publicats. Pel que sembla, al principi els autors van pensar que es podien fer coses bàsiques utilitzant l'esquema sense canvis, i només quan fos necessari, per a peticions concretes, indiquen que s'estaven fent amb una versió diferent. En principi, fins i tot una bona idea, i el nou serà, per dir-ho, "barrejat", col·locat per sobre del vell. Però a veure com es va fer. És cert que no vaig poder mirar-ho des del principi; és divertit, però el diagrama de la capa base simplement no existeix. Les capes van començar amb 2. La documentació ens parla d'una característica especial de TL:

Si un client admet la capa 2, s'ha d'utilitzar el constructor següent:

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

A la pràctica, això significa que abans de cada trucada a l'API, un int amb el valor 0x289dd1f6 s'ha d'afegir abans del número de mètode.

Sona normal. Però què va passar després? Després va aparèixer

invokeWithLayer3#b7475268 query:!X = X;

Llavors, què passa? Com podeu endevinar,

invokeWithLayer4#dea0d430 query:!X = X;

Divertida? No, és massa aviat per riure, pensa-hi cada una sol·licitud d'una altra capa s'ha d'embolicar en un tipus tan especial; si tots són diferents per a vosaltres, com podeu distingir-los? I afegir només 4 bytes al davant és un mètode bastant eficient. Tan,

invokeWithLayer5#417a57ae query:!X = X;

Però és obvi que al cap d'un temps això es convertirà en una mena de bacanal. I la solució va arribar:

Actualització: a partir de la capa 9, mètodes auxiliars invokeWithLayerN només es pot utilitzar juntament amb initConnection

Hura! Després de 9 versions, finalment vam arribar al que es feia als protocols d'Internet als anys 80: acordar la versió una vegada al començament de la connexió!

Llavors, què passa?...

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

Però ara encara pots riure. Només després de 9 capes més, finalment es va afegir un constructor universal amb un número de versió, que només cal cridar-se una vegada al començament de la connexió, i el significat de les capes semblava haver desaparegut, ara només és una versió condicional, com ara a tot arreu. Problema resolt.

Exactament?...

Vasily, [16.07.18 14:01] Fins i tot divendres vaig pensar:
El teleservidor envia esdeveniments sense una sol·licitud. Les sol·licituds s'han d'embolicar a InvokeWithLayer. El servidor no embolcalla les actualitzacions; no hi ha cap estructura per embolicar les respostes i les actualitzacions.

Aquells. el client no pot especificar la capa en què vol actualitzacions

Vadim Goncharov, [16.07.18 14:02] InvokeWithLayer no és una crossa en principi?

Vasily, [16.07.18 14:02] Aquesta és l'única manera

Vadim Goncharov, [16.07.18 14:02] que bàsicament hauria de significar posar-se d'acord en la capa al començament de la sessió

Per cert, es dedueix que no s'ofereix una baixa de nivell del client

Actualitzacions, és a dir. tipus Updates a l'esquema, això és el que el servidor envia al client no com a resposta a una sol·licitud d'API, sinó de manera independent quan es produeix un esdeveniment. Aquest és un tema complex que es tractarà en una altra publicació, però de moment és important saber que el servidor guarda les actualitzacions fins i tot quan el client està fora de línia.

Per tant, si et negues a embolicar de cadascuna paquet per indicar la seva versió, això condueix lògicament als possibles problemes següents:

  • el servidor envia actualitzacions al client fins i tot abans que el client hagi informat quina versió admet
  • què he de fer després d'actualitzar el client?
  • qui? garantiesque l'opinió del servidor sobre el número de capa no canviarà durant el procés?

Creus que això és pura especulació teòrica, i a la pràctica això no pot passar, perquè el servidor està escrit correctament (almenys, està ben provat)? Ha! No importa com sigui!

Això és exactament el que ens vam trobar a l'agost. El 14 d'agost hi havia missatges que s'estava actualitzant alguna cosa als servidors de Telegram... i després als registres:

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.

i després diversos megabytes de traces de pila (bé, al mateix temps es va solucionar el registre). Després de tot, si alguna cosa no es reconeix al vostre TL, és binari per signatura, més avall TOTS va, la descodificació serà impossible. Què heu de fer en una situació així?

Bé, el primer que li ve al cap és desconnectar i tornar-ho a provar. No va ajudar. Busquem a Google CRC32: aquests van resultar ser objectes de l'esquema 73, tot i que vam treballar en el 82. Observem amb atenció els registres: hi ha identificadors de dos esquemes diferents!

Potser el problema és purament en el nostre client no oficial? No, iniciem Telegram Desktop 1.2.17 (versió subministrada en diverses distribucions de Linux), escriu al registre d'excepcions: MTP Identitat de tipus inesperat #b5223b0f llegit a MTPMessageMedia...

Crítica al protocol i als plantejaments organitzatius de Telegram. Part 1, tècnica: experiència d'escriure un client des de zero - TL, MT

Google va demostrar que un problema similar ja li havia passat a un dels clients no oficials, però després els números de versió i, en conseqüència, els supòsits eren diferents...

Aleshores, què hem de fer? Vasily i jo ens vam separar: ell va intentar actualitzar el circuit al 91, vaig decidir esperar uns dies i provar el 73. Els dos mètodes van funcionar, però com que són empírics, no s'entén quantes versions amunt o avall necessites. per saltar, o quant de temps cal esperar.

Més tard vaig poder reproduir la situació: iniciem el client, l'apagam, recompilem el circuit a una altra capa, reiniciem, tornem a detectar el problema, tornem a l'anterior: vaja, sense canvis de circuit i el client es reinicia durant un temps. uns minuts ajudaran. Rebràs una barreja d'estructures de dades de diferents capes.

Explicació? Com podeu endevinar per diversos símptomes indirectes, el servidor consta de molts processos de diferents tipus en diferents màquines. El més probable és que el servidor que s'encarrega del "buffering" va posar a la cua el que li van donar els seus superiors i ho van donar en l'esquema que hi havia en el moment de la generació. I fins que aquesta cua "podrida", no es podia fer res.

Potser... però això és una crossa terrible?!.. No, abans de pensar en idees boges, mirem el codi dels clients oficials. A la versió d'Android no trobem cap analitzador TL, però sí que trobem un fitxer voluminós (GitHub es nega a retocar-lo) amb (des)serialització. Aquests són els fragments de codi:

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;

o

    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... sembla salvatge. Però, probablement, això és codi generat, aleshores, d'acord?... Però sens dubte és compatible amb totes les versions! És cert que no està clar per què tot es barreja, xats secrets i tot tipus de coses _old7 d'alguna manera no sembla la generació de màquines... No obstant això, sobretot em va sorprendre

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Nois, ni tan sols podeu decidir què hi ha dins d'una capa?! Bé, d'acord, diguem que "dos" es van llançar amb un error, bé, passa, però TRES?... De seguida, el mateix rasclet de nou? Quina mena de pornografia és aquesta, ho sento?...

Per cert, al codi font de Telegram Desktop passa una cosa semblant: si és així, diverses confirmacions seguides a l'esquema no canvien el seu número de capa, sinó que arreglen alguna cosa. En condicions en què no hi ha una font oficial de dades per a l'esquema, d'on es pot obtenir, excepte el codi font del client oficial? I si l'agafeu des d'allà, no podeu estar segur que l'esquema sigui completament correcte fins que proveu tots els mètodes.

Com es pot provar això? Espero que els aficionats a les proves d'unitat, funcionals i altres ho comparteixin als comentaris.

D'acord, mirem un altre fragment de codi:

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;

Aquest comentari "creat manualment" suggereix que només una part d'aquest fitxer es va escriure manualment (us imagineu tot el malson de manteniment?), i la resta es va generar per màquina. Tanmateix, sorgeix una altra pregunta: que les fonts estiguin disponibles no completament (a la GPL blobs al nucli de Linux), però aquest ja és un tema per a la segona part.

Però prou. Passem al protocol sobre el qual s'executa tota aquesta serialització.

MT Proto

Així doncs, obrim descripció general и descripció detallada del protocol i el primer que ens ensopeguem és la terminologia. I amb abundància de tot. En general, sembla ser una característica pròpia de Telegram: trucar les coses de manera diferent en diferents llocs, o coses diferents amb una paraula, o viceversa (per exemple, en una API d'alt nivell, si veieu un paquet d'adhesius, no ho és. el que pensaves).

Per exemple, "missatge" i "sessió" signifiquen una cosa diferent aquí que a la interfície habitual del client de Telegram. Bé, tot està clar amb el missatge, es podria interpretar en termes OOP, o simplement anomenar-se la paraula "paquet": aquest és un nivell de transport baix, no hi ha els mateixos missatges que a la interfície, hi ha molts missatges de servei. . Però la sessió... però primer és el primer.

capa de transport

El primer és el transport. Ens explicaran 5 opcions:

  • TCP
  • Connexió web
  • Websocket a través d'HTTPS
  • HTTP
  • HTTPS

Vasily, [15.06.18 15:04] També hi ha transport UDP, però no està documentat

I TCP en tres variants

El primer és similar a UDP sobre TCP, cada paquet inclou un número de seqüència i un crc
Per què és tan dolorós llegir documents en un carro?

Bé, aquí està ara TCP ja en 4 variants:

  • Ampliat
  • Intermedi
  • Encoixinat intermedi
  • Complet

Bé, d'acord, encoixinat intermedi per a MTProxy, això es va afegir més tard a causa d'esdeveniments coneguts. Però, per què dues versions més (tres en total) quan us podríeu tirar endavant una? Les quatre es diferencien essencialment només en com configurar la longitud i la càrrega útil del MTProto principal, que es tractarà més endavant:

  • a Abreujat és 1 o 4 bytes, però no 0xef, llavors el cos
  • a Intermedi això és de 4 bytes de longitud i un camp, i la primera vegada que el client ha d'enviar 0xeeeeeeee per indicar que és Intermedi
  • al complet el més addictiu, des del punt de vista d'un networker: longitud, número de seqüència, i NO EL que és principalment MTProto, body, CRC32. Sí, tot això està a la part superior de TCP. El que ens proporciona un transport fiable en forma d'un flux de bytes seqüencial; no calen seqüències, especialment sumes de control. D'acord, ara algú m'oposarà que TCP té una suma de comprovació de 16 bits, de manera que es produeix la corrupció de dades. Genial, però en realitat tenim un protocol criptogràfic amb hash de més de 16 bytes, tots aquests errors, i fins i tot més, seran detectats per un desajust de SHA a un nivell superior. No hi ha cap punt a CRC32 a sobre d'això.

Comparem Abreujat, en el qual és possible un byte de longitud, amb Intermediate, que justifica "En cas que es necessiti una alineació de dades de 4 bytes", que és una tonteria. Què, es creu que els programadors de Telegram són tan incompetents que no poden llegir dades d'un sòcol a un buffer alineat? Encara ho heu de fer, perquè la lectura us pot retornar qualsevol nombre de bytes (i també hi ha servidors proxy, per exemple...). O, d'altra banda, per què bloquejar Abreviat si encara tindrem un farciment important a sobre de 16 bytes: estalviem 3 bytes de vegades ?

Es fa la impressió que a Nikolai Durov li agrada molt reinventar les rodes, inclosos els protocols de xarxa, sense cap necessitat pràctica real.

Altres opcions de transport, incl. Web i MTProxy, no considerarem ara, potser en una altra publicació, si hi ha una sol·licitud. Sobre aquest mateix MTProxy, només recordem ara que poc després del seu llançament el 2018, els proveïdors van aprendre ràpidament a bloquejar-lo, destinat a bloqueig de bypassPer mida del paquet! I també el fet que el servidor MTProxy escrit (de nou per Waltman) en C estava massa lligat a les especificitats de Linux, tot i que això no era necessari (en confirmarà Phil Kulin), i que un servidor similar ja sigui a Go o Node.js. encaixa en menys de cent línies.

Però extreurem conclusions sobre l'alfabetització tècnica d'aquestes persones al final de l'apartat, després de considerar altres qüestions. De moment, passem a la capa OSI 5, sessió, on van col·locar la sessió MTProto.

Claus, missatges, sessions, Diffie-Hellman

El van col·locar allà no del tot correctament... Una sessió no és la mateixa sessió que es veu a la interfície a Sessions actives. Però en ordre.

Crítica al protocol i als plantejaments organitzatius de Telegram. Part 1, tècnica: experiència d'escriure un client des de zero - TL, MT

Així que vam rebre una cadena de bytes de longitud coneguda de la capa de transport. Es tracta d'un missatge xifrat o de text sense format, si encara estem en l'etapa de l'acord de claus i realment ho estem fent. De quins dels conceptes anomenats "clau" estem parlant? Aclarim aquest tema per al mateix equip de Telegram (demano disculpes per haver traduït la meva pròpia documentació de l'anglès amb el cervell cansat a les 4 de la matinada, era més fàcil deixar algunes frases tal com estan):

Hi ha dues entitats anomenades sessió - un a la interfície d'usuari dels clients oficials a "sessions actuals", on cada sessió correspon a un dispositiu/SO sencer.
El segon és Sessió MTProto, que té el número de seqüència del missatge (en un sentit de baix nivell) i quin pot durar entre diferents connexions TCP. Es poden instal·lar diverses sessions MTProto alhora, per exemple, per accelerar la descàrrega de fitxers.

Entre aquests dos sessions hi ha un concepte autorització. En el cas degenerat, ho podem dir sessió d'IU és el mateix que autorització, però per desgràcia, tot és complicat. Mirem:

  • L'usuari del nou dispositiu genera primer clau_auth i l'uneix al compte, per exemple mitjançant SMS, per això autorització
  • Va passar dins del primer Sessió MTProto, que té session_id dins tu mateix.
  • En aquest pas, la combinació autorització и session_id es podria anomenar instance - aquesta paraula apareix a la documentació i codi d'alguns clients
  • Aleshores, el client pot obrir alguns Sessions MTProto sota el mateix clau_auth - al mateix DC.
  • Aleshores, un dia el client haurà de sol·licitar l'arxiu un altre DC - i per aquest DC se'n generarà un de nou clau_auth !
  • Informar al sistema que no és un nou usuari qui es registra, sinó el mateix autorització (sessió d'IU), el client utilitza trucades d'API auth.exportAuthorization a casa DC auth.importAuthorization al nou DC.
  • Tot és igual, poden haver-hi diversos oberts Sessions MTProto (cadascú amb el seu session_id) a aquest nou DC, sota seva clau_auth.
  • Finalment, el client pot voler Perfect Forward Secrecy. Cada clau_auth era permanent clau -per DC- i el client pot trucar auth.bindTempAuthKey per al seu ús temporal clau_auth - i de nou, només un clau_auth_temp per DC, comú a tots Sessions MTProto a aquest DC.

Tingueu en compte que sal (i les futures sals) també és una clau_auth aquells. compartit entre tots Sessions MTProto al mateix DC.

Què significa "entre diferents connexions TCP"? Així que això vol dir alguna cosa com galeta d'autorització en un lloc web: persisteix (sobreviu) moltes connexions TCP a un servidor determinat, però un dia es fa malbé. Només a diferència d'HTTP, a MTProto els missatges dins d'una sessió es numeren i es confirmen seqüencialment; si van entrar al túnel, la connexió es va trencar; després d'establir una nova connexió, el servidor enviarà tot allò que no va lliurar en aquesta sessió en l'anterior. Connexió TCP.

Tanmateix, la informació anterior es resumeix després de molts mesos d'investigació. Mentrestant, estem implementant el nostre client des de zero? - Tornem al principi.

Així que generem auth_key en Versions Diffie-Hellman de Telegram. Intentem entendre la documentació...

Vasily, [19.06.18 20:05] dades_amb_hash := SHA1(dades) + dades + (qualsevol bytes aleatoris); tal que la longitud sigui igual a 255 bytes;
dades_xifrades := RSA (dades_amb_hash, clau_publica_servidor); un nombre de 255 bytes de llarg (big endian) s'eleva a la potència necessària sobre el mòdul requerit, i el resultat s'emmagatzema com un nombre de 256 bytes.

Tenen una mica de droga DH

No sembla la DH d'una persona sana
No hi ha dues claus públiques a dx

Bé, al final això es va solucionar, però va quedar un residu: el client fa una prova de treball que va poder factoritzar el nombre. Tipus de protecció contra atacs DoS. I la clau RSA només s'utilitza una vegada en una direcció, essencialment per al xifratge new_nonce. Però si bé aquesta operació aparentment senzilla tindrà èxit, a què us haureu d'enfrontar?

Vasily, [20.06.18/00/26 XNUMX:XNUMX] Encara no he arribat a la sol·licitud d'aplicació

Vaig enviar aquesta sol·licitud a DH

I, al moll de transport diu que pot respondre amb 4 bytes d'un codi d'error. Això és tot

Bé, em va dir -404, i què?

Així que li vaig dir: "Atrapa la teva merda xifrada amb una clau de servidor amb una empremta digital com aquesta, vull DH", i em va respondre amb un estúpid 404.

Què pensaries d'aquesta resposta del servidor? Què fer? No hi ha ningú a qui preguntar (però més sobre això a la segona part).

Aquí tot l'interès es fa al moll

No tinc res més a fer, només somiava convertir números d'anada i tornada

Dos números de 32 bits. Els vaig empaquetar com tothom

Però no, aquests dos s'han d'afegir primer a la línia com a BE

Vadim Goncharov, [20.06.18 15:49] i per això 404?

Vasily, [20.06.18 15:49] SÍ!

Vadim Goncharov, [20.06.18 15:50] així que no entenc què pot "no va trobar"

Vasily, [20.06.18 15:50] aproximadament

No he pogut trobar aquesta descomposició en factors primers%)

Ni tan sols vam gestionar els informes d'errors

Vasily, [20.06.18 20:18] Ah, també hi ha MD5. Ja hi ha tres hash diferents

L'empremta digital de la clau es calcula de la següent manera:

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

SHA1 i sha2

Així que posem-ho auth_key vam rebre 2048 bits de mida utilitzant Diffie-Hellman. Que segueix? A continuació, descobrim que els 1024 bits inferiors d'aquesta clau no s'utilitzen de cap manera... però pensem-hi de moment. En aquest pas, tenim un secret compartit amb el servidor. S'ha establert un anàleg de la sessió TLS, que és un procediment molt car. Però el servidor encara no sap res de qui som! Encara no, de fet. autorització. Aquells. si penseu en termes de "contrasenya d'inici de sessió", com vau fer una vegada a ICQ, o almenys "clau de sessió", com a SSH (per exemple, en algun gitlab/github). Vam rebre un anònim. Què passa si el servidor ens diu "aquests números de telèfon són atès per un altre DC"? O fins i tot "el teu número de telèfon està prohibit"? El millor que podem fer és guardar la clau amb l'esperança que ens sigui útil i no es pudrirà aleshores.

Per cert, l'hem "rebut" amb reserves. Per exemple, confiem en el servidor? I si és fals? Es necessitarien comprovacions criptogràfiques:

Vasily, [21.06.18 17:53] Ofereixen als clients mòbils comprovar un nombre de 2 kbit per a la primalitat%)

Però no està gens clar, nafeijoa

Vasily, [21.06.18 18:02] El document no diu què fer si resulta que no és senzill

No dit. Vegem què fa el client oficial d'Android en aquest cas? A això és (i sí, tot el fitxer és interessant) - com diuen, només ho deixaré aquí:

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

No, és clar que encara hi és alguns Hi ha proves per a la primalitat d'un nombre, però personalment ja no tinc prou coneixements de matemàtiques.

D'acord, tenim la clau mestra. Per iniciar sessió, és a dir. Envieu sol·licituds, heu de realitzar més xifratge mitjançant AES.

La clau del missatge es defineix com els 128 bits mitjans del SHA256 del cos del missatge (incloent-hi la sessió, l'identificador del missatge, etc.), inclosos els bytes de farciment, davant de 32 bytes extrets de la clau d'autorització.

Vasily, [22.06.18 14:08] Mitjà, gossa, bits

Rebut auth_key. Tots. Més enllà d'ells... no queda clar del document. No dubteu a estudiar el codi font obert.

Tingueu en compte que MTProto 2.0 requereix de 12 a 1024 bytes de farciment, encara que subjecte a la condició que la longitud del missatge resultant sigui divisible per 16 bytes.

Llavors, quant farciment hauríeu d'afegir?

I sí, també hi ha un 404 en cas d'error

Si algú va estudiar detingudament el diagrama i el text de la documentació, es va adonar que no hi ha MAC. I aquest AES s'utilitza en un determinat mode IGE que no s'utilitza en cap altre lloc. Ells, per descomptat, escriuen sobre això a les seves PMF... Aquí, com ara, la clau del missatge en si és també el hash SHA de les dades desxifrades, que s'utilitza per comprovar la integritat i, en cas de no coincidir, la documentació per algun motiu. recomana ignorar-los en silenci (però què passa amb la seguretat, què passa si ens trenquen?).

No sóc criptògraf, potser no hi ha res dolent amb aquesta modalitat en aquest cas des del punt de vista teòric. Però puc anomenar clarament un problema pràctic, utilitzant Telegram Desktop com a exemple. Xifra la memòria cau local (tots aquests D877F783D5D3EF8C) de la mateixa manera que els missatges a MTProto (només en aquest cas la versió 1.0), és a dir. primer la clau del missatge, després les dades en si (i en algun lloc a part de la principal big auth_key 256 bytes, sense els quals msg_key inútil). Per tant, el problema es nota en fitxers grans. És a dir, heu de mantenir dues còpies de les dades: xifrades i desxifrades. I si hi ha megabytes, o streaming de vídeo, per exemple?... Els esquemes clàssics amb MAC després del text xifrat permeten llegir-lo en flux, transmetent-lo immediatament. Però amb MTProto ho hauràs de fer al principi xifrar o desxifrar tot el missatge, només després transferir-lo a la xarxa o al disc. Per tant, a les últimes versions de Telegram Desktop a la memòria cau user_data També s'utilitza un altre format, amb AES en mode CTR.

Vasily, [21.06.18 01:27] Ah, vaig descobrir què és IGE: IGE va ser el primer intent d'un "mode d'encriptació d'autenticació", originalment per a Kerberos. Va ser un intent fallit (no proporciona protecció d'integritat) i es va haver d'eliminar. Aquest va ser el començament d'una recerca de 20 anys per a un mode de xifratge d'autenticació que funcioni, que recentment va culminar amb modes com OCB i GCM.

I ara els arguments des del costat del carro:

L'equip que hi ha darrere de Telegram, dirigit per Nikolai Durov, està format per sis campions ACM, la meitat d'ells doctors en matemàtiques. Van trigar uns dos anys a llançar la versió actual de MTProto.

És graciós. Dos anys al nivell inferior

O simplement pots prendre tls

D'acord, suposem que hem fet el xifratge i altres matisos. Finalment, és possible enviar sol·licituds serialitzades en TL i deserialitzar les respostes? Aleshores, què i com heu d'enviar? Aquí, diguem-ne, el mètode initConnection, potser és això?

Vasily, [25.06.18 18:46] Inicialitza la connexió i desa informació al dispositiu i l'aplicació de l'usuari.

Accepta app_id, device_model, system_version, app_version i lang_code.

I alguna consulta

Documentació com sempre. No dubteu a estudiar el codi obert

Si tot estava aproximadament clar amb invokeWithLayer, què passa aquí? Resulta que, diguem que tenim, el client ja tenia alguna cosa per preguntar al servidor, hi ha una sol·licitud que volíem enviar:

Vasily, [25.06.18 19:13] A jutjar pel codi, la primera trucada s'embolica en aquesta merda, i la merda en si està embolicada en invokewithlayer

Per què initConnection no podria ser una trucada independent, però ha de ser un embolcall? Sí, com va resultar, s'ha de fer cada cop al començament de cada sessió, i no una vegada, com passa amb la tecla principal. Però! No pot ser trucat per un usuari no autoritzat! Ara hem arribat a l'etapa on és aplicable Aquest pàgina de documentació - i ens diu que...

Només una petita part dels mètodes de l'API està disponible per a usuaris no autoritzats:

  • 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

El primer d'ells, auth.sendCode, i hi ha aquesta estimada primera sol·licitud en què enviem api_id i api_hash, i després rebem un SMS amb un codi. I si ens trobem al DC equivocat (els números de telèfon d'aquest país són atesos per un altre, per exemple), rebrem un error amb el número del DC desitjat. Per esbrinar quina adreça IP per número de DC us heu de connectar, ajudeu-nos help.getConfig. En un moment només hi havia 5 entrades, però després dels famosos esdeveniments del 2018, el nombre ha augmentat significativament.

Ara recordem que hem arribat a aquesta fase al servidor de forma anònima. No és massa car obtenir només una adreça IP? Per què no fer això i altres operacions a la part no xifrada de MTProto? Sento l'objecció: "com podem assegurar-nos que no sigui RKN qui respondrà amb adreces falses?" A això recordem que, en general, els clients oficials Les claus RSA estan incrustades, és a dir només pots signe aquesta informació. De fet, això ja s'està fent per obtenir informació sobre eludir el bloqueig que reben els clients a través d'altres canals (lògicament, això no es pot fer a MTProto mateix; també cal saber on connectar-se).

D'ACORD. En aquesta fase d'autorització del client, encara no estem autoritzats i no hem registrat la nostra aplicació. Només volem veure de moment què respon el servidor als mètodes disponibles per a un usuari no autoritzat. I aquí…

Vasily, [10.07.18 14:45] https://core.telegram.org/method/help.getConfig

config#7dae33e0 [...] = Config;
help.getConfig#c4f9186b = Config;

https://core.telegram.org/api/datacenter

config#232d5905 [...] = Config;
help.getConfig#c4f9186b = Config;

En l'esquema, el primer és el segon

A l'esquema tdesktop el tercer valor és

Sí, des de llavors, és clar, la documentació s'ha actualitzat. Tot i que aviat pot tornar a ser irrellevant. Com ho ha de saber un desenvolupador novell? Potser si registres la teva sol·licitud, t'informaran? Vasily va fer això, però per desgràcia, no li van enviar res (de nou, en parlarem a la segona part).

...Us heu adonat que d'alguna manera ja hem passat a l'API, és a dir. al següent nivell i us heu perdut alguna cosa al tema MTProto? Cap sorpresa:

Vasily, [28.06.18 02:04] Mm, estan remenant alguns dels algorismes a e2e

Mtproto defineix algorismes i claus de xifratge per als dos dominis, així com una mica d'estructura d'embolcall

Però barregen constantment diferents nivells de la pila, de manera que no sempre està clar on va acabar mtproto i on va començar el següent nivell.

Com es barregen? Bé, aquí teniu la mateixa clau temporal per a PFS, per exemple (per cert, Telegram Desktop no ho pot fer). S'executa mitjançant una sol·licitud d'API auth.bindTempAuthKey, és a dir des del nivell superior. Però al mateix temps interfereix amb el xifratge al nivell inferior; després, per exemple, cal tornar-ho a fer. initConnection etc., això no només petició normal. El que també és especial és que només podeu tenir UNA clau temporal per DC, encara que el camp auth_key_id en cada missatge us permet canviar la clau almenys cada missatge, i que el servidor té dret a "oblidar" la clau temporal en qualsevol moment - la documentació no diu què fer en aquest cas... bé, per què no podria No tens diverses claus, com amb un conjunt de sals futures, i?...

Hi ha algunes altres coses que val la pena destacar sobre el tema MTProto.

Missatges de missatges, msg_id, msg_seqno, confirmacions, pings en la direcció equivocada i altres idiosincràsies

Per què necessites saber-ne? Perquè "fuguen" a un nivell superior, i cal que tingueu en compte quan treballeu amb l'API. Suposem que no estem interessats en msg_key; el nivell inferior ho ha desxifrat tot per a nosaltres. Però dins de les dades desxifrades tenim els camps següents (també la longitud de les dades, de manera que sabem on és el farciment, però això no és important):

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

Us recordem que només hi ha una sal per a tot el DC. Per què saber d'ella? No només perquè hi hagi una petició get_future_salts, que us indica quins intervals seran vàlids, però també perquè si la vostra sal està "podrida", llavors el missatge (sol·licitud) simplement es perdrà. El servidor, per descomptat, informarà de la nova sal mitjançant l'emissió new_session_created - però amb l'antic l'hauràs de tornar a enviar d'alguna manera, per exemple. I aquest problema afecta l'arquitectura de l'aplicació.

El servidor pot abandonar sessions per complet i respondre d'aquesta manera per moltes raons. En realitat, què és una sessió MTProto des del costat del client? Aquests són dos números session_id и seq_no missatges dins d'aquesta sessió. Bé, i la connexió TCP subjacent, és clar. Diguem que el nostre client encara no sap com fer moltes coses, es va desconnectar, es va tornar a connectar. Si això va passar ràpidament: la sessió antiga continuava a la nova connexió TCP, augmenta seq_no més lluny. Si triga molt de temps, el servidor podria esborrar-lo, perquè al seu costat també és una cua, com hem pogut saber.

Què hauria de ser seq_no? Oh, aquesta és una pregunta complicada. Intenta entendre sincerament què volia dir:

Missatge relacionat amb el contingut

Un missatge que requereix un reconeixement explícit. Aquests inclouen tots els missatges d'usuari i molts de servei, pràcticament tots amb l'excepció dels contenidors i els agraïments.

Número de seqüència del missatge (msg_seqno)

Un nombre de 32 bits igual al doble del nombre de missatges "relacionats amb el contingut" (els que requereixen reconeixement, i en particular els que no són contenidors) creats pel remitent abans d'aquest missatge i que s'incrementen posteriorment en un si el missatge actual és un missatge relacionat amb el contingut. Un contenidor sempre es genera després de tot el seu contingut; per tant, el seu número de seqüència és superior o igual als números de seqüència dels missatges que hi conté.

Quin tipus de circ és aquest amb un increment d'1, i després un altre de 2?... Sospito que inicialment volien dir "el bit menys significatiu per a ACK, la resta és un número", però el resultat no és del tot el mateix: en particular, surt, es pot enviar alguns confirmacions que tenen el mateix seq_no! Com? Bé, per exemple, el servidor ens envia alguna cosa, l'envia, i nosaltres mateixos romanem en silenci, només responent amb missatges de servei que confirmen la recepció dels seus missatges. En aquest cas, les nostres confirmacions de sortida tindran el mateix número de sortida. Si esteu familiaritzat amb TCP i penseu que això sona d'alguna manera salvatge, però no sembla gaire salvatge, perquè en TCP seq_no no canvia, però la confirmació va a seq_no a l'altra banda, m'afanyaré a molestar-te. Les confirmacions es proporcionen a MTProto NO en seq_no, com en TCP, però per msg_id !

Què és això msg_id, el més important d'aquests camps? Un identificador de missatge únic, com el seu nom indica. Es defineix com un nombre de 64 bits, els bits més baixos dels quals tornen a tenir la màgia "servidor-no-servidor", i la resta és una marca de temps Unix, inclosa la part fraccionada, desplaçada 32 bits a l'esquerra. Aquells. marca de temps per se (i els missatges amb temps que difereixen massa seran rebutjats pel servidor). D'això resulta que en general es tracta d'un identificador global per al client. Tenint en compte això, recordem-ho session_id - tenim garantits: En cap cas es pot enviar un missatge destinat a una sessió a una sessió diferent. És a dir, resulta que ja n'hi ha 03:00 nivell: sessió, número de sessió, identificador del missatge. Per què tanta complicació, aquest misteri és molt gran.

Per tant, msg_id necessària per...

RPC: peticions, respostes, errors. Confirmacions.

Com haureu notat, no hi ha cap tipus o funció especial de "fer una sol·licitud RPC" en cap lloc del diagrama, tot i que hi ha respostes. Després de tot, tenim missatges relacionats amb el contingut! Això és, qualsevol el missatge podria ser una petició! O no ser-ho. Després de tot, de cadascuna hi msg_id. Però hi ha respostes:

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

Aquí és on s'indica a quin missatge és una resposta. Per tant, al nivell superior de l'API, haureu de recordar quin era el número de la vostra sol·licitud; crec que no cal explicar que el treball és asíncron i que hi pot haver diverses sol·licituds en curs al mateix temps, les respostes a quines es poden retornar en qualsevol ordre? En principi, a partir d'això i dels missatges d'error com cap treballador, es pot rastrejar l'arquitectura que hi ha darrere: el servidor que manté una connexió TCP amb tu és un equilibrador de front-end, reenvia les sol·licituds als backends i les recull a través de message_id. Sembla que aquí tot és clar, lògic i bo.

Sí?.. I si t'ho penses? Al cap i a la fi, la pròpia resposta RPC també té un camp msg_id! Hem de cridar al servidor "no responeu a la meva resposta!"? I sí, què hi havia de les confirmacions? Sobre la pàgina missatges sobre missatges ens diu què és

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

i s'ha de fer per cada costat. Però no sempre! Si heu rebut un RpcResult, serveix com a confirmació. És a dir, el servidor pot respondre a la vostra sol·licitud amb MsgsAck, com ara "L'he rebut". RpcResult pot respondre immediatament. Podrien ser tots dos.

I sí, encara has de respondre la resposta! Confirmació. En cas contrari, el servidor considerarà que no es pot lliurar i us el tornarà a enviar. Fins i tot després de la reconnexió. Però aquí, per descomptat, sorgeix el problema dels temps morts. Vegem-los una mica més endavant.

Mentrestant, mirem els possibles errors d'execució de consultes.

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

Oh, algú exclamarà, aquí hi ha un format més humà: hi ha una línia! Pren-te el teu temps. Aquí llista d'errors, però per descomptat no complet. D'això ens assabentem que el codi és alguna cosa com Errors HTTP (bé, és clar, no es respecta la semàntica de les respostes, en alguns llocs es distribueixen aleatòriament entre els codis) i la línia sembla CAPITAL_LETTERS_AND_NUMBERS. Per exemple, PHONE_NUMBER_OCCUPIED o FILE_PART_Х_MISSING. Bé, és a dir, encara necessitareu aquesta línia analitzar. Per exemple FLOOD_WAIT_3600 significarà que hauràs d'esperar una hora, i PHONE_MIGRATE_5, que cal registrar un número de telèfon amb aquest prefix al 5è DC. Tenim un llenguatge tipus, oi? No necessitem un argument d'una cadena, els normals ho faran, d'acord.

De nou, això no es troba a la pàgina de missatges de servei, però, com ja és habitual amb aquest projecte, la informació es pot trobar en una altra pàgina de documentació. O llançar sospita. En primer lloc, mira, escrivint/infracció de capa - RpcError es pot niar-hi RpcResult. Per què no fora? Què no hem tingut en compte?.. En conseqüència, on és la garantia que RpcError pot NO estar incrustat RpcResult, però estar directament o imbricat en un altre tipus?... I si no pot, per què no és al nivell superior, és a dir. falta req_msg_id ? ..

Però continuem amb els missatges de servei. El client pot pensar que el servidor està pensant durant molt de temps i fer aquesta meravellosa petició:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Hi ha tres possibles respostes a aquesta pregunta, de nou entrecreuant-se amb el mecanisme de confirmació; intentar entendre què haurien de ser (i quina és la llista general de tipus que no requereixen confirmació) es deixa al lector com a deures (nota: la informació a el codi font de Telegram Desktop no està complet).

Drogodependència: estats dels missatges

En general, molts llocs a TL, MTProto i Telegram en general deixen una sensació de tossuderia, però per educació, tacte i altres habilitats suaus Ens vam callar educadament i vam censurar les obscenitats dels diàlegs. No obstant això, aquest llocОla major part de la pàgina tracta missatges sobre missatges És impactant fins i tot per a mi, que he estat treballant amb protocols de xarxa durant molt de temps i he vist bicicletes de diferents graus de tort.

Comença de manera innocua, amb confirmacions. A continuació ens expliquen

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;

Bé, tothom que comenci a treballar amb MTProto s'haurà de tractar amb ells; en el cicle "corregit - recompilat - llançat", obtenir errors de nombre o sal que s'hagi anat malament durant les edicions és una cosa habitual. Tanmateix, aquí hi ha dos punts:

  1. Això vol dir que es perd el missatge original. Hem de crear algunes cues, ho veurem més endavant.
  2. Quins són aquests estranys números d'error? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64... on són els altres números, Tommy?

La documentació diu:

La intenció és que els valors error_code s'agrupin (error_code >> 4): per exemple, els codis 0x40 — 0x4f corresponen a errors en la descomposició del contenidor.

però, en primer lloc, un canvi en l'altra direcció, i en segon lloc, no importa, on són els altres codis? En el cap de l'autor?.. No obstant això, aquestes són petiteses.

L'addicció comença en missatges sobre estats de missatges i còpies de missatges:

  • Sol·licitud d'informació d'estat del missatge
    Si cap de les parts no ha rebut informació sobre l'estat dels seus missatges de sortida durant un temps, pot sol·licitar-ho explícitament a l'altra part:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Missatge informatiu sobre l'estat dels missatges
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Aquí, info és una cadena que conté exactament un byte d'estat del missatge per a cada missatge de la llista msg_ids entrant:

    • 1 = no se sap res del missatge (msg_id és massa baix, és possible que l'altra part l'hagi oblidat)
    • 2 = missatge no rebut (msg_id entra dins del rang d'identificadors emmagatzemats; tanmateix, l'altra part certament no ha rebut cap missatge com aquest)
    • 3 = missatge no rebut (msg_id massa alt; tanmateix, l'altra part certament encara no l'ha rebut)
    • 4 = missatge rebut (tingueu en compte que aquesta resposta també és al mateix temps un justificant de recepció)
    • +8 = missatge ja reconegut
    • +16 = missatge que no requereix reconeixement
    • +32 = La consulta RPC continguda al missatge que s'està processant o ja s'ha completat
    • +64 = resposta relacionada amb el contingut al missatge ja generat
    • +128 = l'altra part sap amb certesa que el missatge ja s'ha rebut
      Aquesta resposta no requereix cap reconeixement. És un reconeixement dels msgs_state_req rellevants, en si mateix.
      Tingueu en compte que si de sobte resulta que l'altra part no té un missatge que sembli que se li ha enviat, el missatge simplement es pot tornar a enviar. Fins i tot si l'altra part ha de rebre dues còpies del missatge al mateix temps, el duplicat s'ignorarà. (Si ha passat massa temps i el msg_id original ja no és vàlid, el missatge s'ha d'embolicar a msg_copy).
  • Comunicació voluntària de l'estat dels missatges
    Qualsevol de les parts pot informar voluntàriament a l'altra part de l'estat dels missatges transmesos per l'altra part.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Comunicació voluntària ampliada de l'estat d'un missatge
    ...
    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;
  • Sol·licitud explícita per tornar a enviar missatges
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    La part remota respon immediatament tornant a enviar els missatges sol·licitats […]
  • Sol·licitud explícita per tornar a enviar les respostes
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    La part remota respon immediatament tornant a enviar respostes als missatges sol·licitats […]
  • Còpies de missatges
    En algunes situacions, s'ha de tornar a enviar un missatge antic amb un msg_id que ja no és vàlid. A continuació, s'embolica en un contenidor de còpia:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Un cop rebut, el missatge es processa com si l'embolcall no hi fos. Tanmateix, si se sap amb certesa que s'ha rebut el missatge orig_message.msg_id, aleshores el missatge nou no es processa (mentre que al mateix temps s'accepten ell i orig_message.msg_id). El valor d'orig_message.msg_id ha de ser inferior al msg_id del contenidor.

Fins i tot callem sobre què msgs_state_info de nou les orelles del TL inacabat estan sortint (necessàvem un vector de bytes, i als dos bits inferiors hi havia una enumeració, i als dos bits superiors hi havia banderes). El punt és diferent. Algú entén per què tot això és a la pràctica? en un client real necessari?.. Amb dificultat, però es pot imaginar algun benefici si una persona es dedica a la depuració i en un mode interactiu: pregunteu al servidor què i com. Però aquí es descriuen les peticions viatge d'anada i tornada.

Es dedueix que cada part no només ha de xifrar i enviar missatges, sinó que també ha d'emmagatzemar dades sobre si mateix, sobre les respostes a ells, durant un període de temps desconegut. La documentació no descriu ni els temps ni l'aplicabilitat pràctica d'aquestes característiques. de cap manera. El més sorprenent és que s'utilitzen en el codi dels clients oficials! Pel que sembla, els van dir una cosa que no estava inclosa a la documentació pública. Entendre des del codi per què?, ja no és tan senzill com en el cas de TL: no és una part (relativament) lògicament aïllada, sinó una peça lligada a l'arquitectura de l'aplicació, és a dir. requerirà molt més temps per entendre el codi de l'aplicació.

Pings i temps. Cues.

De tot, si recordem les conjectures sobre l'arquitectura del servidor (distribució de les sol·licituds entre els backends), se segueix una cosa més aviat trista, malgrat totes les garanties de lliurament en TCP (o es lliuren les dades o se us informarà sobre el buit, però les dades es lliuraran abans que es produeixi el problema), que les confirmacions al mateix MTProto - sense garanties. El servidor pot perdre o llençar el vostre missatge fàcilment, i no es pot fer res, només feu servir diferents tipus de crosses.

I, en primer lloc, les cues de missatges. Bé, amb una cosa tot era obvi des del principi: s'ha d'emmagatzemar i enviar un missatge no confirmat. I després de quina hora? I el bufó el coneix. Potser aquests missatges de servei addictes solucionen d'alguna manera aquest problema amb crosses, per exemple, a Telegram Desktop hi ha unes 4 cues corresponents (potser més, com ja s'ha dit, per a això cal aprofundir en el seu codi i arquitectura més seriosament; alhora temps, sabem que no es pot prendre com a mostra; no s'utilitzen un cert nombre de tipus de l'esquema MTProto).

Per què passa això? Probablement, els programadors del servidor no van poder assegurar la fiabilitat dins del clúster, ni tan sols la memòria intermèdia a l'equilibrador frontal, i van transferir aquest problema al client. Desesperat, Vasily va intentar implementar una opció alternativa, amb només dues cues, utilitzant algorismes de TCP: mesurant l'RTT al servidor i ajustant la mida de la "finestra" (en missatges) en funció del nombre de sol·licituds no confirmades. És a dir, una heurística tan aproximada per avaluar la càrrega del servidor és quantes de les nostres sol·licituds pot mastegar al mateix temps i no perdre.

Bé, això és, ho entens, oi? Si heu d'implementar TCP de nou a sobre d'un protocol que s'executa sobre TCP, això indica un protocol molt mal dissenyat.

Sí, per què necessiteu més d'una cua i, de totes maneres, què significa això per a una persona que treballa amb una API d'alt nivell? Mireu, feu una sol·licitud, la serialitzeu, però sovint no la podeu enviar immediatament. Per què? Perquè la resposta serà msg_id, que és temporalаSóc una etiqueta, l'assignació de la qual és millor posposar-se fins al més tard possible, en cas que el servidor la rebutgi a causa d'un desajust de temps entre nosaltres i ell (per descomptat, podem fer una crossa que canviï el nostre temps del present). al servidor afegint un delta calculat a partir de les respostes del servidor: els clients oficials ho fan, però és brut i inexacte a causa de la memòria intermèdia). Per tant, quan feu una sol·licitud amb una trucada de funció local des de la biblioteca, el missatge passa per les etapes següents:

  1. Es troba en una cua i espera el xifratge.
  2. Nomenat msg_id i el missatge va anar a una altra cua - possible reenviament; enviar al sòcol.
  3. a) El servidor va respondre MsgsAck: el missatge es va lliurar, l'eliminem de l'"altra cua".
    b) O viceversa, no li agradava alguna cosa, va respondre badmsg - tornar a enviar des d'"una altra cua"
    c) No se sap res, el missatge s'ha de tornar a enviar des d'una altra cua, però no se sap exactament quan.
  4. El servidor finalment va respondre RpcResult - la resposta real (o error) - no només lliurada, sinó també processada.

Potser, l'ús de contenidors podria resoldre parcialment el problema. Això és quan un munt de missatges s'agrupen en un, i el servidor va respondre amb una confirmació a tots alhora, en un msg_id. Però també rebutjarà aquest paquet, si alguna cosa va sortir malament, en la seva totalitat.

I en aquest punt entren en joc consideracions no tècniques. Per experiència, hem vist moltes crosses i, a més, ara veurem més exemples de mals consells i arquitectura: en aquestes condicions, val la pena confiar i prendre aquestes decisions? La pregunta és retòrica (per descomptat que no).

De què estem parlant? Si sobre el tema dels "missatges de drogues sobre missatges" encara podeu especular amb objeccions com "ets estúpid, no vas entendre el nostre pla brillant!" (per tant, escriviu primer la documentació, com ho hauria de fer la gent normal, amb justificació i exemples d'intercanvi de paquets, després en parlarem), després els temps/temps morts són una qüestió purament pràctica i específica, tot aquí es coneix des de fa molt de temps. Què ens diu la documentació sobre els temps morts?

Normalment, un servidor reconeix la recepció d'un missatge d'un client (normalment, una consulta RPC) mitjançant una resposta RPC. Si arriba una resposta molt de temps, un servidor pot enviar primer un justificant de recepció i, una mica més tard, la pròpia resposta RPC.

Un client normalment reconeix la recepció d'un missatge d'un servidor (normalment, una resposta RPC) afegint un reconeixement a la següent consulta RPC si no es transmet massa tard (si es genera, per exemple, 60-120 segons després de la recepció). d'un missatge del servidor). Tanmateix, si durant un llarg període de temps no hi ha cap motiu per enviar missatges al servidor o si hi ha un gran nombre de missatges no reconeguts del servidor (per exemple, més de 16), el client transmet un reconeixement autònom.

... tradueixo: nosaltres mateixos no sabem quant i com ho necessitem, així que suposem que sigui així.

I sobre els pings:

Missatges de ping (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

Normalment, una resposta es torna a la mateixa connexió:

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

Aquests missatges no requereixen reconeixement. Un pong només es transmet en resposta a un ping, mentre que un ping es pot iniciar per qualsevol dels dos costats.

Tancament de connexió ajornat + PING

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

Funciona com el ping. A més, un cop rebut això, el servidor inicia un temporitzador que tancarà la connexió actual disconnect_delay uns segons més tard tret que rebi un missatge nou del mateix tipus que restableixi automàticament tots els temporitzadors anteriors. Si el client envia aquests pings una vegada cada 60 segons, per exemple, pot establir disconnect_delay igual a 75 segons.

Estàs boig?! En 60 segons, el tren entrarà a l'estació, deixarà i recollirà passatgers i tornarà a perdre el contacte al túnel. En 120 segons, mentre l'escolteu, arribarà a un altre, i la connexió probablement es trencarà. Bé, està clar d'on venen les cames: "He sentit un toc, però no sé on és", hi ha l'algoritme de Nagl i l'opció TCP_NODELAY, destinada al treball interactiu. Però, disculpeu-me, manteniu el seu valor predeterminat: 200 Millisegons Si realment voleu representar alguna cosa semblant i estalviar en un parell de paquets possibles, poseu-ho 5 segons, o el que sigui el temps d'espera del missatge "L'usuari està escrivint...". Però no més.

I finalment, pings. És a dir, comprovar la vivacitat de la connexió TCP. És curiós, però fa uns 10 anys vaig escriure un text crític sobre el missatger del dormitori de la nostra facultat: els autors també van fer ping al servidor des del client, i no viceversa. Però els estudiants de 3r són una cosa i una oficina internacional és una altra, oi?...

Primer, un petit programa educatiu. Una connexió TCP, en absència d'intercanvi de paquets, pot viure durant setmanes. Això és bo i dolent, depenent del propòsit. És bo si teníeu una connexió SSH oberta al servidor, us aixequeu de l'ordinador, reinicieu l'encaminador, torneu al vostre lloc: la sessió a través d'aquest servidor no es va trencar (no heu escrit res, no hi havia paquets) , és convenient. És dolent si hi ha milers de clients al servidor, cadascun ocupant recursos (hola, Postgres!), i l'amfitrió del client pot haver-se reiniciat fa molt de temps, però no ho sabrem.

Els sistemes de xat/IM cauen en el segon cas per un motiu addicional: els estats en línia. Si l'usuari "va caure", cal que n'informeu els seus interlocutors. En cas contrari, acabaràs amb un error que els creadors de Jabber van cometre (i van corregir durant 20 anys): l'usuari s'ha desconnectat, però segueix escrivint-li missatges, creient que està en línia (que també es van perdre completament en aquests minuts abans que es descobrís la desconnexió). No, l'opció TCP_KEEPALIVE, que moltes persones que no entenen com funcionen els temporitzadors TCP introdueixen aleatòriament (en establir valors salvatges com desenes de segons), no us ajudarà aquí; heu d'assegurar-vos que no només el nucli del sistema operatiu. de la màquina de l'usuari està viva, però també funciona amb normalitat, capaç de respondre, i la pròpia aplicació (creus que no es pot congelar? Telegram Desktop a Ubuntu 18.04 em va congelar més d'una vegada).

Per això has de fer ping servidor client, i no a l'inrevés: si el client fa això, si es trenca la connexió, no es lliurarà el ping, no s'aconseguirà l'objectiu.

Què veiem a Telegram? És exactament el contrari! Bé, això és. Formalment, per descomptat, ambdues parts poden fer ping mútuament. A la pràctica, els clients fan servir una crossa ping_delay_disconnect, que estableix el temporitzador al servidor. Bé, perdoneu, no correspon al client decidir quant de temps vol viure-hi sense ping. El servidor, segons la seva càrrega, ho sap millor. Però, per descomptat, si no t'importen els recursos, llavors seràs el teu propi Pinotxo malvat, i una crossa servirà...

Com s'hauria d'haver dissenyat?

Crec que els fets anteriors indiquen clarament que l'equip de Telegram/VKontakte no és gaire competent en l'àmbit del transport (i nivell inferior) de xarxes informàtiques i la seva baixa qualificació en qüestions rellevants.

Per què va resultar ser tan complicat i com poden intentar oposar-se els arquitectes de Telegram? El fet que hagin intentat fer una sessió que sobrevisqui a les interrupcions de la connexió TCP, és a dir, el que no s'ha lliurat ara, ho lliurarem més tard. Probablement també van intentar fer un transport UDP, però van trobar dificultats i el van abandonar (per això la documentació està buida, no hi havia res de què presumir). Però a causa de la manca de comprensió de com funcionen les xarxes en general i TCP en particular, on pots confiar-hi i on cal fer-ho tu mateix (i com), i un intent de combinar-ho amb la criptografia "dos ocells amb una pedra”, aquest és el resultat.

Com era necessari? Basat en el fet que msg_id és una marca de temps necessària des del punt de vista criptogràfic per evitar atacs de repetició, és un error adjuntar-hi una funció d'identificador únic. Per tant, sense canviar fonamentalment l'arquitectura actual (quan es genera el flux d'Actualitzacions, aquest és un tema d'API d'alt nivell per a una altra part d'aquesta sèrie de publicacions), caldria:

  1. El servidor que manté la connexió TCP al client assumeix la responsabilitat: si ha llegit des del sòcol, si us plau, reconeixeu, processeu o retorneu un error, sense pèrdua. Aleshores, la confirmació no és un vector d'identificacions, sinó simplement "l'últim seq_no rebut": només un número, com a TCP (dos números: la vostra seqüència i la confirmada). Sempre estem dins de la sessió, oi?
  2. La marca de temps per evitar atacs de repetició es converteix en un camp separat, a la nonce. Està comprovat, però no afecta a res més. Prou i uint32 - si la nostra sal canvia almenys cada mig dia, podem assignar 16 bits als bits de baix ordre d'una part entera del temps actual, la resta - a una fracció de segon (com ara).
  3. Eliminat msg_id en absolut: des del punt de vista de distingir les sol·licituds als backends, hi ha, en primer lloc, l'identificador del client i, en segon lloc, l'identificador de sessió, concatenar-los. En conseqüència, només una cosa és suficient com a identificador de sol·licitud seq_no.

Aquesta tampoc no és l'opció més exitosa; una aleatoria completa podria servir com a identificador; això ja es fa a l'API d'alt nivell quan s'envia un missatge, per cert. Seria millor refer completament l'arquitectura de relatiu a absolut, però aquest és un tema d'una altra part, no d'aquesta publicació.

API?

Ta-daam! Així doncs, després d'haver lluitat per un camí ple de dolor i crosses, finalment vam poder enviar qualsevol sol·licitud al servidor i rebre qualsevol resposta, així com rebre actualitzacions del servidor (no com a resposta a una sol·licitud, sinó ell mateix). ens envia, com PUSH, si algú ho té més clar).

Atenció, ara hi haurà l'únic exemple en Perl a l'article! (per a aquells que no estiguin familiaritzats amb la sintaxi, el primer argument de bless és l'estructura de dades de l'objecte, el segon és la seva classe):

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

Sí, no és un spoiler a propòsit; si encara no l'heu llegit, endavant i feu-ho!

Oh, wai~~... què sembla això? Una cosa molt familiar... potser aquesta és l'estructura de dades d'una API web típica en JSON, excepte que les classes també s'adjunten als objectes?...

Així és com resulta... De què va, companys?... Tant d'esforç - i ens vam aturar a descansar on els programadors web tot just començant?...No seria més senzill només JSON per HTTPS?! Què hem rebut a canvi? Ha valgut la pena l'esforç?

Avaluem què ens ha donat TL+MTProto i quines alternatives són possibles. Bé, HTTP, que se centra en el model de sol·licitud-resposta, no encaixa bé, però almenys alguna cosa a sobre de TLS?

Serialització compacta. Veient aquesta estructura de dades, semblant a JSON, recordo que n'hi ha versions binàries. Marquem MsgPack com a insuficient extensible, però hi ha, per exemple, CBOR, per cert, un estàndard descrit a RFC 7049. Destaca pel fet que defineix etiquetes, com a mecanisme d'expansió, i entre ja estandarditzat disponible:

  • 25 + 256: substitució de línies repetides per una referència al número de línia, un mètode de compressió tan barat
  • 26 - objecte Perl serialitzat amb el nom de la classe i els arguments del constructor
  • 27 - objecte serialitzat independent del llenguatge amb el nom del tipus i els arguments del constructor

Bé, he intentat serialitzar les mateixes dades en TL i en CBOR amb l'empaquetament de cadena i objectes habilitat. El resultat va començar a variar a favor del CBOR a partir d'un megabyte:

cborlen=1039673 tl_len=1095092

Per tant, conclusió: Hi ha formats substancialment més senzills que no estan subjectes al problema de fallada de sincronització o identificador desconegut, amb una eficiència comparable.

Establiment ràpid de connexió. Això significa zero RTT després de la reconnexió (quan la clau ja s'ha generat una vegada), aplicable des del primer missatge MTProto, però amb algunes reserves, toca la mateixa sal, la sessió no està podrida, etc. Què ens ofereix TLS en canvi? Cita sobre el tema:

Quan s'utilitza PFS a TLS, tiquets de sessió TLS (RFC 5077) per reprendre una sessió xifrada sense tornar a negociar les claus i sense emmagatzemar la informació de les claus al servidor. En obrir la primera connexió i crear claus, el servidor xifra l'estat de connexió i el transmet al client (en forma de bitllet de sessió). En conseqüència, quan es reprèn la connexió, el client envia un tiquet de sessió, inclosa la clau de sessió, al servidor. El tiquet en si està xifrat amb una clau temporal (clau del tiquet de sessió), que s'emmagatzema al servidor i s'ha de distribuir entre tots els servidors d'interfície que processen SSL en solucions agrupades.[10]. Per tant, la introducció d'un bitllet de sessió pot violar PFS si les claus temporals del servidor estan compromeses, per exemple, quan s'emmagatzemen durant molt de temps (OpenSSL, nginx, Apache les emmagatzemen per defecte durant tota la durada del programa; els llocs populars utilitzen la clau durant diverses hores, fins a dies).

Aquí el RTT no és zero, cal intercanviar almenys ClientHello i ServerHello, després del qual el client pot enviar dades juntament amb Finished. Però aquí hem de recordar que no tenim la web, amb el seu munt de connexions recentment obertes, sinó un missatger, la connexió del qual sovint és una i més o menys de llarga durada, sol·licituds relativament breus a pàgines web: tot està multiplexat. internament. És a dir, és bastant acceptable si no ens trobem amb un tram de metro molt dolent.

Has oblidat alguna cosa més? Escriu als comentaris.

Continuarà!

A la segona part d'aquesta sèrie d'articles es plantejaran qüestions no tècniques, sinó organitzatives: enfocaments, ideologia, interfície, actitud cap als usuaris, etc. Basat, però, en la informació tècnica que es va presentar aquí.

La tercera part continuarà analitzant el component tècnic/experiència de desenvolupament. Aprendràs, en particular:

  • continuació del pandemoni amb la varietat de tipus TL
  • coses desconegudes sobre canals i supergrups
  • per què els diàlegs són pitjors que la llista
  • sobre l'adreçament de missatges absolut vs relatiu
  • quina diferència hi ha entre foto i imatge
  • com els emoji interfereixen amb el text en cursiva

i altres crosses! Estigueu atents!

Font: www.habr.com

Afegeix comentari