Critiche al protocollo e agli approcci organizzativi di Telegram. Parte 1, tecnica: esperienza di scrittura di un cliente da zero - TL, MT

Di recente, su Habré hanno iniziato ad apparire più spesso post su quanto sia buono Telegram, quanto siano brillanti ed esperti i fratelli Durov nella costruzione di sistemi di rete, ecc. Allo stesso tempo, pochissime persone si sono davvero immerse nel dispositivo tecnico: al massimo usano un'API Bot basata su JSON abbastanza semplice (e molto diversa da MTProto) e di solito accettano semplicemente sulla fede tutte quelle lodi e pubbliche relazioni che ruotano attorno al messaggero. Quasi un anno e mezzo fa, il mio collega della NPO Echelon Vasily (purtroppo il suo account su Habré è stato cancellato insieme alla bozza) ha iniziato a scrivere da zero il suo client Telegram in Perl, e in seguito si è unito l'autore di queste righe. Perché Perl, qualcuno chiederà immediatamente? Perché ci sono già progetti del genere in altre lingue, infatti non è questo il punto, potrebbe esserci qualsiasi altra lingua dove libreria finita, e di conseguenza l'autore deve andare fino in fondo da zero. Inoltre, la crittografia è una cosa del genere: fidati, ma verifica. Con un prodotto incentrato sulla sicurezza, non puoi semplicemente fare affidamento sulla libreria già pronta di un fornitore e crederci ciecamente (tuttavia, questo è un argomento per ulteriori informazioni nella seconda parte). Al momento la libreria funziona abbastanza bene a livello “medio” (consente di effettuare eventuali richieste API).

Tuttavia, non ci saranno molta crittografia e matematica in questa serie di post. Ma ci saranno tanti altri dettagli tecnici e stampelle architettoniche (sarà utile anche a chi non scriverà da zero, ma utilizzerà la libreria in qualsiasi lingua). Quindi, l'obiettivo principale era provare a implementare il client da zero secondo la documentazione ufficiale. Cioè, supponiamo che il codice sorgente dei client ufficiali sia chiuso (di nuovo, nella seconda parte, riveleremo più in dettaglio l'argomento di cosa sia veramente ci so), ma, come ai vecchi tempi, ad esempio, esiste uno standard come RFC - è possibile scrivere un client solo secondo le specifiche, "senza sbirciare" alla fonte, anche ufficiale (Telegram Desktop, mobile) , anche Telethon non ufficiale?

Sommario:

La documentazione... c'è? È vero?..

Frammenti di appunti per questo articolo hanno cominciato a essere raccolti la scorsa estate. Tutto questo tempo sul sito ufficiale https://core.telegram.org la documentazione era al livello 23, cioè bloccato da qualche parte nel 2014 (ricordate, allora non c'erano ancora nemmeno i canali?). Naturalmente, in teoria, ciò avrebbe dovuto consentire di implementare un client con funzionalità in quel momento nel 2014. Ma anche in questo stato, la documentazione era, in primo luogo, incompleta e, in secondo luogo, in alcuni punti si contraddiceva. Poco più di un mese fa, a settembre 2019, lo era accidentalmente è emerso che il sito ha un ampio aggiornamento della documentazione, per un livello 105 completamente nuovo, con una nota che ora è necessario rileggere tutto. In effetti, molti articoli sono stati rivisti, ma molti sono rimasti invariati. Pertanto, quando leggi le critiche di seguito sulla documentazione, dovresti tenere presente che alcune di queste cose non sono più rilevanti, ma altre lo sono ancora. Dopotutto, 5 anni nel mondo moderno non sono solo tanti, ma molto molti. Da allora (soprattutto se non si tiene conto dei geochat scartati e resuscitati da allora), il numero di metodi API nello schema è passato da cento a più di duecentocinquanta!

Da dove inizi come giovane scrittore?

Non importa se scrivi da zero o usi, ad esempio, librerie già pronte come Telethon per Python o Madeline per PHP, in ogni caso, avrai prima bisogno registra la tua candidatura - ottenere parametri api_id и api_hash (chi ha lavorato con l'API VKontakte capisce subito) con cui il server identificherà l'applicazione. Questo devo per motivi legali, ma parleremo più approfonditamente del motivo per cui gli autori di biblioteche non possono pubblicarlo nella seconda parte. Forse sarai soddisfatto dei valori del test, sebbene siano molto limitati: il fatto è che ora puoi registrarti sul tuo numero solo uno applicazione, quindi non correre a capofitto.

Ora, dal punto di vista tecnico, ci sarebbe dovuto interessare il fatto che dopo la registrazione avremmo dovuto ricevere notifiche da Telegram su aggiornamenti della documentazione, del protocollo, ecc. Cioè, si potrebbe presumere che il sito con i moli sia stato semplicemente "segnato" e abbia continuato a lavorare specificamente con coloro che hanno iniziato a creare clienti, perché. è più facile. Ma no, non è stato osservato nulla del genere, non è arrivata alcuna informazione.

E se scrivi da zero, l'uso dei parametri ricevuti è in realtà ancora lontano. Sebbene https://core.telegram.org/ e ne parla prima in Guida introduttiva, infatti, devi prima implementare Protocollo MTProto - ma se credi layout secondo il modello OSI alla fine della pagina della descrizione generale del protocollo, poi del tutto invano.

Infatti, sia prima che dopo MTProto, a più livelli contemporaneamente (come dicono i networker stranieri che lavorano nel kernel del sistema operativo, violazione del livello), un argomento grande, doloroso e terribile si intrometterà ...

Serializzazione binaria: TL (Type Language) e il suo schema, i livelli e molte altre parole spaventose

Questo argomento, infatti, è la chiave dei problemi di Telegram. E ci saranno molte parole terribili se provi ad approfondire.

Quindi, schema. Se ricordi questa parola, dì: Schema JSONHai pensato bene. L'obiettivo è lo stesso: un linguaggio per descrivere un possibile insieme di dati trasmessi. Questo, infatti, è dove finisce la somiglianza. Se dalla pagina Protocollo MTProto, o dall'albero dei sorgenti del client ufficiale, proveremo ad aprire qualche schema, vedremo qualcosa del tipo:

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 che lo vede per la prima volta riconoscerà intuitivamente solo una parte di ciò che è scritto - beh, queste sono apparentemente strutture (anche se dov'è il nome, a sinistra oa destra?), Ci sono campi in esse, dopodiché il tipo passa attraverso i due punti... probabilmente. Qui, tra parentesi angolari, ci sono probabilmente dei template come in C++ (infatti, non proprio). E cosa significano tutti gli altri simboli, punti interrogativi, punti esclamativi, percentuali, reticoli (e ovviamente significano cose diverse in luoghi diversi), presenti da qualche parte, ma non da qualche parte, numeri esadecimali - e soprattutto, come ottenere da questo diritto (che non verrà rifiutato dal server) flusso di byte? Devi leggere la documentazione (Sì, ci sono collegamenti allo schema nella versione JSON nelle vicinanze, ma questo non lo rende più chiaro).

Apertura della pagina Serializzazione dei dati binari e tuffati nel magico mondo dei funghi e della matematica discreta, qualcosa di simile a matan nel 4° anno. Alfabeto, tipo, valore, combinatore, combinatore funzionale, forma normale, tipo composto, tipo polimorfico... e questa è solo la prima pagina! Il prossimo ti aspetta Lingua TL, che, sebbene contenga già un esempio di banale richiesta e risposta, non fornisce affatto una risposta a casi più tipici, il che significa che dovrai guadare la rivisitazione della matematica tradotta dal russo all'inglese su altri otto nidificati pagine!

I lettori che hanno familiarità con i linguaggi funzionali e l'inferenza automatica del tipo, ovviamente, hanno visto in questa lingua descrizioni, anche da un esempio, molto più familiari, e possono dire che questo generalmente non è male in linea di principio. Le obiezioni a questo sono:

  • sì, scopo suona bene, ma ahimè non raggiunto
  • l'istruzione nelle università russe varia anche tra le specialità informatiche: non tutti leggono il corso corrispondente
  • Infine, come vedremo, in pratica lo è Non richiede, poiché viene utilizzato solo un sottoinsieme limitato anche del TL descritto

Come detto Leone Nerd sul canale #perl sulla rete IRC FreeNode, cercando di implementare un gate da Telegram a Matrix (la traduzione della citazione è imprecisa a memoria):

Sembra che qualcuno a cui è stata introdotta la teoria dei tipi per la prima volta, si sia entusiasmato e abbia iniziato a provare a giocarci, senza preoccuparsi se fosse necessario nella pratica.

Guarda tu stesso se la necessità di tipi nudi (int, long, ecc.) come qualcosa di elementare non solleva domande - alla fine devono essere implementati manualmente - ad esempio, proviamo a derivare da essi vettore. Cioè, infatti, массив, se chiami le cose risultanti con i loro nomi propri.

Ma prima

Breve descrizione di un sottoinsieme della sintassi TL per coloro che non... leggi la documentazione ufficiale

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;

Inizia sempre la definizione progettista, dopodiché, facoltativamente (in pratica, sempre) tramite il simbolo # deve essere CRC32 dalla stringa di descrizione normalizzata del tipo specificato. Poi viene la descrizione dei campi, se lo sono - il tipo può essere vuoto. Tutto termina con un segno di uguale, il nome del tipo a cui appartiene il dato costruttore, cioè il sottotipo. Il tipo a destra del segno di uguale è polimorfico - cioè, può corrispondere a diversi tipi specifici.

Se la definizione si trova dopo la riga ---functions---, quindi la sintassi rimarrà la stessa, ma il significato sarà diverso: il costruttore diventerà il nome della funzione RPC, i campi diventeranno parametri (beh, cioè rimarrà esattamente la stessa struttura data come descritto di seguito, sarà solo il significato dato), e "tipo polimorfico ' è il tipo del risultato restituito. È vero, rimarrà comunque polimorfico, appena definito nella sezione ---types---, e questo costruttore non verrà considerato. Digita gli overload delle funzioni chiamate in base ai loro argomenti, ad es. per qualche motivo, diverse funzioni con lo stesso nome ma una firma diversa, come in C++, non sono fornite in TL.

Perché "costruttore" e "polimorfico" se non è OOP? Ebbene, in effetti, sarà più facile per qualcuno pensarci in termini di OOP: un tipo polimorfico come classe astratta, e i costruttori sono le sue classi discendenti dirette, inoltre final nella terminologia di un certo numero di lingue. In effetti, ovviamente, qui somiglianza con veri metodi di costruzione sovraccarichi nei linguaggi di programmazione OO. Poiché qui ci sono solo strutture di dati, non ci sono metodi (sebbene la descrizione di funzioni e metodi di seguito sia abbastanza in grado di creare confusione nella testa su cosa siano, ma si tratta di qualcos'altro) - puoi pensare a un costruttore come a valore da cui in costruzione type durante la lettura di un flusso di byte.

Come succede? Il deserializzatore, che legge sempre 4 byte, vede il valore 0xcrc32 - e capisce cosa succederà dopo field1 con il tipo int, cioè. legge esattamente 4 byte, su questo campo sovrastante con type PolymorType Leggere. Vede 0x2crc32 e capisce che ci sono altri due campi, primo long, quindi leggiamo 8 byte. E poi ancora un tipo complesso, che viene deserializzato allo stesso modo. Per esempio, Type3 potrebbe essere dichiarato nello schema non appena due costruttori, rispettivamente, devono incontrarsi ulteriormente 0x12abcd34, dopodiché è necessario leggere altri 4 byte intO 0x6789cdef, dopodiché non ci sarà più nulla. Qualsiasi altra cosa: devi lanciare un'eccezione. In ogni caso, dopo si torna a leggere 4 byte int поля field_c в constructorTwo e su questo finiamo di leggere il nostro PolymorType.

Infine, se catturato 0xdeadcrc per constructorThree, allora le cose si complicano. Il nostro primo campo bit_flags_of_what_really_present con il tipo # - in effetti, questo è solo un alias per il tipo natche significa "numero naturale". Cioè, in effetti, unsigned int è l'unico caso, tra l'altro, in cui si trovano numeri senza segno negli schemi reali. Quindi, la prossima è una costruzione con un punto interrogativo, il che significa che questo è il campo - sarà presente sul filo solo se il bit corrispondente è impostato nel campo referenziato (approssimativamente come un operatore ternario). Quindi, supponiamo che questo bit fosse attivo, quindi devi leggere un campo come Type, che nel nostro esempio ha 2 costruttori. Uno è vuoto (consiste solo di un identificatore), l'altro ha un campo ids con il tipo ids:Vector<long>.

Potresti pensare che sia i modelli che i generici siano buoni o Java. Ma no. Quasi. Questo единственный caso di parentesi angolari in circuiti reali, ed è utilizzato SOLO per Vector. In un flusso di byte, saranno 4 byte CRC32 per il tipo Vector stesso, sempre lo stesso, quindi 4 byte: il numero di elementi dell'array e quindi questi elementi stessi.

A questo si aggiunge il fatto che la serializzazione avviene sempre in parole di 4 byte, tutti i tipi ne sono multipli - vengono descritti anche i tipi incorporati bytes и string con la serializzazione manuale della lunghezza e questo allineamento di 4 - beh, sembra normale e anche relativamente efficiente? Sebbene si affermi che TL sia un'efficiente serializzazione binaria, ma al diavolo loro, con l'espansione di qualsiasi cosa, anche valori booleani e stringhe di un solo carattere fino a 4 byte, JSON sarà ancora molto più spesso? Guarda, anche i campi non necessari possono essere saltati dai flag di bit, va tutto bene e anche estensibile per il futuro, hai aggiunto nuovi campi opzionali al costruttore in seguito? ..

Ma no, se non leggi la mia breve descrizione, ma la documentazione completa e pensi all'implementazione. In primo luogo, il CRC32 del costruttore viene calcolato dalla stringa di descrizione del testo dello schema normalizzato (rimuovere gli spazi bianchi aggiuntivi, ecc.), quindi se viene aggiunto un nuovo campo, la stringa di descrizione del tipo cambierà, e quindi il suo CRC32 e, di conseguenza, la serializzazione. E cosa farebbe il vecchio cliente se ricevesse un campo con nuovi flag impostati, ma non sapesse cosa farne dopo? ..

In secondo luogo, ricordiamo CRC32, che qui è usato essenzialmente come funzioni hash per determinare in modo univoco quale tipo viene (de) serializzato. Qui ci troviamo di fronte al problema delle collisioni - e no, la probabilità non è una su 232, ma molto di più. Chi si è ricordato che CRC32 è progettato per rilevare (e correggere) errori nel canale di comunicazione e, di conseguenza, migliorare queste proprietà a scapito degli altri? Ad esempio, non le interessa la permutazione dei byte: se conti CRC32 da due righe, nella seconda scambierai i primi 4 byte con i successivi 4 byte - sarà lo stesso. Quando abbiamo stringhe di testo dall'alfabeto latino (e un po' di punteggiatura) come input, e questi nomi non sono particolarmente casuali, la probabilità di una tale permutazione aumenta notevolmente.

A proposito, chi ha controllato cosa c'era davvero CRC32? In una delle prime fonti (anche prima di Waltman) c'era una funzione hash che moltiplicava ogni carattere per il numero 239, così amato da queste persone, ah ah!

Alla fine, ok, ci siamo resi conto che i costruttori con un tipo di campo Vector<int> и Vector<PolymorType> avrà CRC32 diverso. E per quanto riguarda la presentazione sulla linea? E in termini di teoria, diventa parte del tipo? Diciamo che passiamo una matrice di diecimila numeri, beh, con Vector<int> tutto è chiaro, la lunghezza e altri 40000 byte. E se questo Vector<Type2>, che consiste in un solo campo int ed è l'unico nel tipo - dobbiamo ripetere 10000xabcdef0 34 volte e poi 4 byte int, oppure il linguaggio è in grado di VISUALIZZARLO per noi dal costruttore fixedVec e invece di 80000 byte, trasferisci di nuovo solo 40000?

Questa non è affatto una domanda teorica oziosa - immagina di ottenere un elenco di utenti del gruppo, ognuno dei quali ha un ID, nome, cognome - la differenza nella quantità di dati trasferiti tramite una connessione mobile può essere significativa. È l'efficacia della serializzazione di Telegram che ci viene pubblicizzata.

Quindi ...

Vettore, che non può essere dedotto

Se provi a sfogliare le pagine di descrizione dei combinatori e dintorni, vedrai che un vettore (e persino una matrice) sta formalmente cercando di dedurre diversi fogli attraverso le tuple. Ma alla fine vengono martellati, il passaggio finale viene saltato e viene semplicemente data la definizione di un vettore, che non è nemmeno legato a un tipo. Qual è il problema qui? Nelle lingue di programmazione, specialmente quelli funzionali, è abbastanza tipico descrivere la struttura in modo ricorsivo: il compilatore con la sua valutazione pigra capirà tutto e lo farà. Nel linguaggio serializzazione dei dati ma ci vuole EFFICIENZA: basta descrivere elenco, cioè. una struttura di due elementi: il primo è un elemento dati, il secondo è la stessa struttura stessa o uno spazio vuoto per la coda (pack (cons) in Lisp). Ma questo ovviamente richiederà ogni L'elemento impiega inoltre 4 byte (CRC32 nel caso di TL) per descriverne il tipo. È facile descrivere un array taglia unica, ma nel caso di un array di lunghezza precedentemente sconosciuta, ci interrompiamo.

Quindi, poiché TL non ti consente di emettere un vettore, è stato necessario aggiungerlo a lato. Alla fine la documentazione dice:

La serializzazione utilizza sempre lo stesso costruttore “vector” (const 0x1cb5c415 = crc32(“vector t:Type # [ t ] = Vector t”) che non dipende dal valore specifico della variabile di tipo t.

Il valore del parametro facoltativo t non è coinvolto nella serializzazione poiché deriva dal tipo di risultato (sempre noto prima della deserializzazione).

Dai un'occhiata più da vicino: vector {t:Type} # [ t ] = Vector t - ma da nessuna parte la definizione stessa non dice che il primo numero deve essere uguale alla lunghezza del vettore! E non segue da nessuna parte. Questo è un dato che devi tenere a mente e implementare con le tue mani. Altrove, la documentazione menziona persino onestamente che il tipo è falso:

Lo pseudotipo polimorfico Vector t è un "tipo" il cui valore è una sequenza di valori di qualsiasi tipo t, boxed o bare.

… ma non si concentra su di esso. Quando tu, stanco di guadare lo stiramento della matematica (forse anche a te noto da un corso universitario), decidi di segnare e guardare come lavorarci effettivamente nella pratica, l'impressione rimane nella tua testa: qui Serious Mathematics si basa su , ovviamente Cool People (due matematici -vincitore dell'ACM), e non uno qualunque. L'obiettivo - sfoggiare - è stato raggiunto.

A proposito, riguardo al numero. Richiamare # è un sinonimo nat, numero naturale:

Esistono espressioni di tipo (tipoexpr) ed espressioni numeriche (espressione-nat). Tuttavia, sono definiti allo stesso modo.

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

ma in grammatica sono descritti allo stesso modo, cioè anche questa differenza deve essere ricordata e messa in pratica a mano.

Bene, sì, tipi di modello (vector<int>, vector<User>) hanno un identificatore comune (#1cb5c415), cioè. se sai che la chiamata è dichiarata come

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

allora stai aspettando non solo un vettore, ma un vettore di utenti. Più precisamente, dovrebbe aspetta - nel codice reale, ogni elemento, se non un tipo nudo, avrà un costruttore, e in senso buono nell'implementazione sarebbe necessario controllare - e siamo stati inviati esattamente in ogni elemento di questo vettore quel tipo? E se fosse una specie di PHP, in cui l'array può contenere tipi diversi in elementi diversi?

A questo punto, inizi a chiederti: è necessaria una tale TL? Forse per il carrello sarebbe possibile utilizzare il serializzatore umano, lo stesso protobuf che esisteva già allora? Era teoria, diamo un'occhiata alla pratica.

Implementazioni TL esistenti nel codice

TL è nato nelle viscere di VKontakte ancor prima dei noti eventi con la vendita della quota di Durov e (sicuramente), anche prima dello sviluppo di Telegram. E in open source fonti della prima implementazione puoi trovare molte stampelle divertenti. E il linguaggio stesso è stato implementato in modo più completo lì di quanto non lo sia ora in Telegram. Ad esempio, gli hash non vengono utilizzati affatto nello schema (ovvero lo pseudotipo integrato (come un vettore) con comportamento deviante). O

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

ma consideriamo per completezza il quadro, per tracciare, per così dire, l'evoluzione del Gigante del Pensiero.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Oppure questa bellissima:

    static const char *reserved_words_polymorhic[] = {

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

      };

Questo frammento riguarda modelli, come:

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

Questa è la definizione del tipo di modello hashmap, come vettore di coppie int - Type. In C++ sarebbe simile a questo:

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

quindi qui alpha - parola chiave! Ma solo in C++ puoi scrivere T, ma devi scrivere alpha, beta... Ma non più di 8 parametri, la fantasia finiva su theta. Quindi sembra che una volta a San Pietroburgo ci fossero approssimativamente tali dialoghi:

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

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

Ma si trattava della prima implementazione prevista di TL "in generale". Passiamo alla considerazione delle implementazioni nei client Telegram effettivi.

Parola di Basilio:

Vasily, [09.10.18/17/07 XNUMX:XNUMX] Soprattutto, il culo è caldo per il fatto che hanno rovinato un mucchio di astrazioni, e poi hanno martellato un bullone su di loro e hanno messo le stampelle sul codegeger
Di conseguenza, prima dal porto il pilot.jpg
Quindi dal codice jekichan.webp

Naturalmente, da persone che hanno familiarità con algoritmi e matematica, possiamo aspettarci che abbiano letto Aho, Ullman e abbiano familiarità con gli strumenti standard del settore de facto per scrivere i loro compilatori DSL nel corso dei decenni, giusto? ..

Autore telegramma-cli è Vitaliy Valtman, come si può capire dall'occorrenza del formato TLO al di fuori dei suoi limiti (cli), un membro del team - ora è allocata la libreria per l'analisi di TL separatamentequal è l'impressione di lei parser TL? ..

16.12 04:18 Vasily: secondo me qualcuno non ha imparato lex + yacc
16.12 04:18 Vasily: altrimenti non posso spiegarlo
16.12 04:18 Vasily: beh, o sono stati pagati per il numero di righe in VK
16.12 04:19 Vasily: oltre 3k linee di altri<censored> invece di un parser

Forse un'eccezione? Vediamo come делает questo è il client UFFICIALE — 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);

Oltre 1100 righe in Python, un paio di espressioni regolari + casi speciali del tipo vettoriale, che, ovviamente, è dichiarato nello schema come dovrebbe essere secondo la sintassi TL, ma lo mettono su questa sintassi, analizzalo di più ... La domanda è: perché preoccuparsi di tutto questo miracoloиpiù puff, se nessuno lo analizzerà comunque secondo la documentazione ?!

A proposito... Ricordi che abbiamo parlato del controllo CRC32? Quindi, nel generatore di codice di Telegram Desktop c'è un elenco di eccezioni per quei tipi in cui il CRC32 non corrisponde come indicato nello schema!

Vasily, [18.12 22:49] e qui dovresti pensare se è necessario un tale TL
se volessi pasticciare con implementazioni alternative, inizierei a inserire interruzioni di riga, metà dei parser si interromperà su definizioni multilinea
tdesktop, tuttavia, anche

Ricorda il punto sulle battute, ci torneremo un po 'più tardi.

Ok, telegram-cli non è ufficiale, Telegram Desktop è ufficiale, ma per quanto riguarda gli altri? E chi lo sa?.. Nel codice del client Android, non c'era alcun parser di schema (il che solleva domande sull'open source, ma questa è per la seconda parte), ma c'erano molti altri pezzi di codice divertenti, ma su di loro in la sottosezione sottostante.

Quali altre domande solleva in pratica la serializzazione? Ad esempio, hanno sbagliato, ovviamente, con campi di bit e campi condizionali:

vasily: flags.0? true
significa che il campo è presente e vero se il flag è impostato

vasily: flags.1? int
significa che il campo è presente e deve essere deserializzato

Vasily: Culo, non bruciare, cosa stai facendo!
Vasily: Da qualche parte nel documento c'è una menzione che vero è un tipo nudo di lunghezza zero, ma non è realistico raccogliere qualcosa dai loro documenti
Vasily: Non esiste nemmeno una cosa del genere nelle implementazioni aperte, ma ci sono molte stampelle e oggetti di scena

Che ne dici di un Telethon? Guardando avanti sull'argomento di MTProto, un esempio: ci sono tali pezzi nella documentazione, ma il segno % è descritto solo come "corrispondente al dato tipo nudo", cioè negli esempi seguenti, un errore o qualcosa di non documentato:

Vasily, [22.06.18/18/38 XNUMX:XNUMX] In un posto:

msg_container#73f1f8dc messages:vector message = MessageContainer;

In modo diverso:

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

E queste sono due grandi differenze, nella vita reale arriva una specie di vettore nudo

Non ho visto definizioni vettoriali nude e non l'ho trovato

Analisi scritta in telethon a mano

Il suo schema commentava la definizione msg_container

Ancora una volta, la domanda rimane circa%. Non è descritto.

Vadim Goncharov, [22.06.18/19/22 XNUMX:XNUMX] e in tdesktop?

Vasily, [22.06.18/19/23 XNUMX:XNUMX] Ma il loro parser TL sui regolatori probabilmente non lo mangerà neanche

// parsed manually

TL è una bellissima astrazione, nessuno la implementa completamente

E non c'è % nella loro versione dello schema

Ma qui la documentazione si contraddice, quindi xs

È stato trovato in grammatica, potevano semplicemente dimenticare di descrivere la semantica

Bene, hai visto il molo su TL, non puoi capirlo senza mezzo litro

"Bene, diciamo", dirà un altro lettore, "critici tutto, quindi mostralo come dovrebbe".

Vasily risponde: “per quanto riguarda il parser, ho bisogno di cose come

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

in qualche modo più simile a

struct tree *parse_args4 (void) {
  PARSE_INIT (type_args4);
  struct parse so = save_parse ();
  PARSE_TRY (parse_optional_arg_def);
  if (S) {
    tree_add_child (T, S);
  } else {
    load_parse (so);
  }
  if (LEX_CHAR ('!')) {
    PARSE_ADD (type_exclam);
    EXPECT ("!");
  }
  PARSE_TRY_PES (parse_type_term);
  PARSE_OK;
}

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)

questo è l'INTERO lexer:

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

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

quelli. più semplice è usare un eufemismo."

In generale, alla fine, il parser e il generatore di codice per il sottoinsieme effettivamente utilizzato di TL rientrano in circa 100 righe di grammatica e ~ 300 righe del generatore (comprese tutte printcodice generato da), inclusi gadget di tipo, informazioni di tipo per l'introspezione in ogni classe. Ogni tipo polimorfico viene trasformato in una classe base astratta vuota e i costruttori ereditano da essa e dispongono di metodi per la serializzazione e la deserializzazione.

Mancanza di tipi nel linguaggio dei tipi

La digitazione forte va bene, giusto? No, questo non è un holivar (anche se preferisco i linguaggi dinamici), ma un postulato all'interno di TL. Sulla base di ciò, la lingua dovrebbe fornirci tutti i tipi di controlli. Bene, va bene, non lasciarlo, ma l'implementazione, ma dovrebbe almeno descriverli. E quali opportunità vogliamo?

Prima di tutto i vincoli. Qui vediamo nella documentazione per il caricamento dei file:

Il contenuto binario del file viene quindi suddiviso in parti. Tutte le parti devono avere le stesse dimensioni ( dimensione_parte ) e devono essere soddisfatte le seguenti condizioni:

  • part_size % 1024 = 0 (divisibile per 1KB)
  • 524288 % part_size = 0 (512KB devono essere equamente divisibili per part_size)

L'ultima parte non deve soddisfare queste condizioni, a condizione che la sua dimensione sia inferiore a part_size.

Ogni parte dovrebbe avere un numero di sequenza, parte_file, con un valore compreso tra 0 e 2,999.

Dopo che il file è stato partizionato è necessario scegliere un metodo per salvarlo sul server. utilizzo upload.saveBigFilePart nel caso in cui la dimensione completa del file sia superiore a 10 MB e upload.saveFilePart per file più piccoli.
[…] può essere restituito uno dei seguenti errori di immissione dei dati:

  • FILE_PARTS_INVALID - Numero di parti non valido. Il valore non è compreso tra 1..3000

Qualcuno di questi è presente nello schema? È in qualche modo esprimibile per mezzo di TL? NO. Ma scusatemi, anche il vecchio Turbo Pascal era in grado di descrivere i tipi dati da gamme. E poteva fare un'altra cosa, ora meglio conosciuta come enum - un tipo costituito da un'enumerazione di un numero fisso (piccolo) di valori. In linguaggi come C - numerico, intendiamoci, finora abbiamo parlato solo di tipi. numeri. Ma ci sono anche array, stringhe... per esempio, sarebbe carino descrivere che questa stringa può contenere solo un numero di telefono, giusto?

Niente di tutto questo è in TL. Ma c'è, ad esempio, in JSON Schema. E se qualcun altro può obiettare sulla divisibilità di 512 KB che questo deve ancora essere verificato nel codice, assicurati che il client semplicemente non poteva inviare numero fuori portata 1..3000 (e l'errore corrispondente non potrebbe essersi verificato) sarebbe possibile, giusto? ..

A proposito, sugli errori e sui valori restituiti. L'occhio è offuscato anche per coloro che hanno lavorato con TL - non ce ne siamo accorti subito ogni una funzione in TL può effettivamente restituire non solo il tipo di ritorno descritto, ma anche un errore. Ma questo non è deducibile per mezzo del TL stesso. Certo, è comunque comprensibile e nafig non è necessario nella pratica (sebbene in realtà RPC possa essere fatto in modi diversi, torneremo su questo) - ma per quanto riguarda la purezza dei concetti di matematica dei tipi astratti dal celeste mondo? .. Afferrò il rimorchiatore - quindi partita.

E infine, per quanto riguarda la leggibilità? Bene, lì, in generale, mi piacerebbe descrizione ha ragione nello schema (di nuovo, è nello schema JSON), ma se è già teso, che dire del lato pratico - almeno è banale guardare le differenze durante gli aggiornamenti? Guarda tu stesso a esempi reali:

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

A qualcuno piace, ma GitHub, ad esempio, si rifiuta di evidenziare i cambiamenti all'interno di file così lunghe. Il gioco "trova 10 differenze", e ciò che il cervello vede immediatamente è che l'inizio e la fine sono gli stessi in entrambi gli esempi, devi leggere noiosamente da qualche parte nel mezzo ... Secondo me, questo non è solo in teoria, ma sembra puramente visivamente sporco e trasandato.

A proposito, sulla purezza della teoria. Perché sono necessari i campi di bit? Non sembrano odore male dal punto di vista della teoria dei tipi? Una spiegazione può essere vista nelle versioni precedenti dello schema. All'inizio sì, era così, per ogni starnuto veniva creato un nuovo tipo. Questi rudimenti sono ancora presenti in questa forma, ad esempio:

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;

Ma ora immagina di avere 5 campi opzionali nella tua struttura, quindi hai bisogno di 32 tipi per tutte le possibili opzioni. esplosione combinatoria. Quindi la purezza cristallina della teoria TL si è schiantata ancora una volta contro il culo di ghisa della dura realtà della serializzazione.

Inoltre, in alcuni punti questi stessi ragazzi violano la propria battitura. Ad esempio, in MTProto (prossimo capitolo) la risposta può essere compressa da Gzip, tutto è sensato, tranne la violazione di livelli e schema. Una volta, e non ha raccolto lo stesso RpcResult, ma il suo contenuto. Bene, perché farlo? .. Ho dovuto tagliare una stampella in modo che la compressione funzionasse ovunque.

O un altro esempio, una volta abbiamo trovato un errore - inviato InputPeerUser invece di InputUser. O vice versa. Ma ha funzionato! Cioè, al server non importava del tipo. Come può essere? La risposta, forse, sarà suggerita da frammenti di codice da 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);

In altre parole, qui la serializzazione è fatta MANUALE, codice non generato! Forse il server è implementato in modo simile?.. In linea di principio, funzionerà se fatto una volta, ma come puoi supportarlo in seguito con gli aggiornamenti? Non era a questo che serviva lo schema? E poi passiamo alla domanda successiva.

Versionamento. Strati

Il motivo per cui le versioni dello schema sono chiamate livelli può essere indovinato solo in base alla cronologia degli schemi pubblicati. Apparentemente, all'inizio gli autori sembravano che le cose di base potessero essere fatte secondo uno schema invariato, e solo dove necessario, indicare a richieste specifiche che venivano fatte secondo una versione diversa. In linea di principio, anche una buona idea - e il nuovo, per così dire, "si mescolerà", si sovrapporrà al vecchio. Ma vediamo come è stato fatto. È vero, non è stato possibile guardare fin dall'inizio: è divertente, ma lo schema del livello base semplicemente non esiste. I livelli sono iniziati da 2. La documentazione ci parla di una speciale funzione TL:

Se un client supporta il livello 2, è necessario utilizzare il seguente costruttore:

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

In pratica, questo significa che prima di ogni chiamata API, un int con il valore 0x289dd1f6 deve essere aggiunto prima del numero del metodo.

Suona bene. Ma cosa è successo dopo? Poi è arrivato

invokeWithLayer3#b7475268 query:!X = X;

Quindi qual è il prossimo? Come è facile intuire

invokeWithLayer4#dea0d430 query:!X = X;

Divertente? No, è troppo presto per ridere, pensa a cosa ogni una richiesta da un altro livello deve essere racchiusa in un tipo così speciale: se li hai tutti diversi, in quale altro modo distinguerli? E aggiungere solo 4 byte davanti è un metodo piuttosto efficiente. COSÌ

invokeWithLayer5#417a57ae query:!X = X;

Ma è ovvio che dopo un po' diventerà un baccanale. E la soluzione è arrivata:

Aggiornamento: a partire dal livello 9, metodi di supporto invokeWithLayerN può essere utilizzato insieme a initConnection

Evviva! Dopo 9 versioni, siamo finalmente arrivati ​​a ciò che è stato fatto nei protocolli Internet negli anni '80: negoziazione della versione una volta all'inizio della connessione!

Quindi qual è il prossimo?..

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

E ora puoi ridere. Solo dopo altri 9 livelli è stato finalmente aggiunto un costruttore universale con un numero di versione, che deve essere chiamato solo una volta all'inizio della connessione, e il significato nei livelli sembra essere scomparso, ora è solo una versione condizionale, come in qualsiasi altro posto. Problema risolto.

Giusto?..

Vasily, [16.07.18/14/01 XNUMX:XNUMX] Venerdì ho pensato:
Il teleserver invia eventi senza richiesta. Le richieste devono essere racchiuse in InvokeWithLayer. Il server non esegue il wrapping degli aggiornamenti, non esiste una struttura per il wrapping di risposte e aggiornamenti.

Quelli. il cliente non può specificare il livello in cui desidera gli aggiornamenti

Vadim Goncharov, [16.07.18/14/02 XNUMX:XNUMX] InvokeWithLayer non è una stampella in linea di principio?

Vasily, [16.07.18/14/02 XNUMX:XNUMX] Questo è l'unico modo

Vadim Goncharov, [16.07.18/14/02 XNUMX:XNUMX] che in sostanza dovrebbe significare stratificazione all'inizio della sessione

A proposito, ne consegue che non viene fornito un downgrade del client

Aggiornamenti, ad es. tipo Updates nello schema, questo è ciò che il server invia al client non in risposta a una richiesta API, ma da solo quando si verifica un evento. Questo è un argomento complesso che verrà trattato in un altro post, ma per ora è importante sapere che il server accumula aggiornamenti anche quando il client è offline.

Quindi, quando si rifiuta di avvolgere ogni pacchetto per indicare la sua versione, quindi sorgono logicamente i seguenti possibili problemi:

  • il server invia gli aggiornamenti al client prima che il client abbia comunicato quale versione supporta
  • cosa si dovrebbe fare dopo aver aggiornato il client?
  • che garanzieche l'opinione del server sul numero del livello non cambierà nel processo?

Pensi che questo sia un pensiero puramente teorico, e in pratica questo non può accadere, perché il server è scritto correttamente (in ogni caso è testato bene)? Ah! Non importa come!

Questo è esattamente quello che abbiamo incontrato ad agosto. Il 14 agosto, i messaggi lampeggiavano che qualcosa veniva aggiornato sui server di Telegram ... e poi nei registri:

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.

e poi alcuni megabyte di tracce dello stack (beh, allo stesso tempo, la registrazione è stata corretta). Dopotutto, se qualcosa non è stato riconosciuto nel tuo TL, è binario per firme, più avanti nel flusso TUTTI va, la decodifica diventerà impossibile. Cosa fare in una situazione del genere?

Bene, la prima cosa che viene in mente a chiunque è disconnettersi e riprovare. Non ha aiutato. Abbiamo cercato su Google CRC32: questi si sono rivelati essere oggetti dello schema 73, sebbene abbiamo lavorato sullo schema 82. Osserviamo attentamente i registri: ci sono identificatori di due schemi diversi!

Forse il problema è puramente nel nostro cliente non ufficiale? No, eseguiamo Telegram Desktop 1.2.17 (la versione fornita con un certo numero di distribuzioni Linux), scrive nel registro delle eccezioni: MTP Unexpected type id #b5223b0f read in MTPMessageMedia...

Critiche al protocollo e agli approcci organizzativi di Telegram. Parte 1, tecnica: esperienza di scrittura di un cliente da zero - TL, MT

Google ha dimostrato che un problema simile era già accaduto a uno dei client non ufficiali, ma poi i numeri di versione e, di conseguenza, i presupposti erano diversi ...

Quindi che si fa? Vasily e io ci siamo lasciati: ha provato ad aggiornare lo schema a 91, ho deciso di aspettare qualche giorno e provare a 73. Entrambi i metodi hanno funzionato, ma poiché sono empirici, non si capisce quante versioni siano necessarie per saltare o giù, né quanto tempo devi aspettare.

Successivamente, sono riuscito a riprodurre la situazione: avviamo il client, lo spegniamo, ricompiliamo lo schema su un altro livello, riavviamo, rileviamo il problema, torniamo a quello precedente - oops, non cambiare lo schema e riavviare il client per diversi i minuti aiuteranno. Riceverai un mix di strutture di dati da diversi livelli.

Spiegazione? Come puoi intuire dai vari sintomi indiretti, il server è costituito da molti diversi tipi di processi su macchine diverse. Molto probabilmente, quello dei server che è responsabile del "buffering" ha messo in coda ciò che gli hanno dato quelli superiori, e lo hanno dato nello schema che era al momento della generazione. E fino a quando questa coda non è stata "marcia", non si poteva fare nulla al riguardo.

A meno che... ma questa è una terribile stampella?!.. No, prima di pensare a idee folli, diamo un'occhiata al codice dei clienti ufficiali. Nella versione Android non troviamo alcun parser TL, ma troviamo un file pesante (github si rifiuta di colorarlo) con (de)serializzazione. Ecco i frammenti di codice:

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... sembra pazzesco. Ma, probabilmente, questo è il codice generato, allora va bene?.. Ma sicuramente supporta tutte le versioni! È vero, non è chiaro perché tutto sia mescolato in un mucchio, chat segrete e ogni sorta di _old7 in qualche modo non simile alla generazione di macchine ... Tuttavia, soprattutto sono impazzito

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Ragazzi, non riuscite nemmeno a decidere all'interno di uno strato?! Bene, va bene, "due", diciamo, sono stati rilasciati con un errore, beh, succede, ma TRE? .. Subito di nuovo sullo stesso rastrello? Che tipo di pornografia è questa, scusa? ..

A proposito, una cosa simile accade nelle fonti di Telegram Desktop - in tal caso, e diversi commit di fila allo schema non cambiano il suo numero di livello, ma correggono qualcosa. In condizioni in cui non esiste una fonte di dati ufficiale per lo schema, da dove posso ottenerla, ad eccezione delle fonti client ufficiali? E lo prendi da lì, non puoi essere sicuro che lo schema sia del tutto corretto finché non provi tutti i metodi.

Come può essere testato? Spero che i fan dell'unità, dei test funzionali e di altro tipo condividano i commenti.

Ok, diamo un'occhiata a un altro pezzo di codice:

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;

Quel commento "creato manualmente" qui suggerisce che solo una parte di questo file è scritta a mano (riuscite a immaginare l'incubo della manutenzione?), e il resto è generato dalla macchina. Tuttavia, sorge un'altra domanda: che le fonti siano disponibili non completamente (a la blob sotto GPL nel kernel Linux), ma questo è già un argomento per la seconda parte.

Ma basta. Passiamo al protocollo in cima al quale sta inseguendo tutta questa serializzazione.

MTProto

Quindi apriamo descrizione generale и descrizione dettagliata del protocollo e la prima cosa su cui inciampiamo è la terminologia. E con abbondanza di tutto. In generale, questo sembra essere un marchio di fabbrica di Telegram: chiamare cose in luoghi diversi in modi diversi, o cose diverse in una parola, o viceversa (ad esempio, in un'API di alto livello se vedi un pacchetto di adesivi - questo non è quello che pensavi).

Ad esempio, "messaggio" (messaggio) e "sessione" (sessione) - qui significano qualcosa di diverso rispetto alla solita interfaccia del client Telegram. Bene, tutto è chiaro con il messaggio, potrebbe essere interpretato in termini di OOP, o semplicemente chiamato la parola "pacchetto" - questo è un livello di trasporto basso, non ci sono gli stessi messaggi dell'interfaccia, ce ne sono molti di quelli di servizio. Ma la sessione... ma andiamo con ordine.

strato di trasporto

La prima cosa è il trasporto. Ci verrà detto circa 5 opzioni:

  • TCP
  • presa web
  • Websocket su HTTPS
  • HTTP
  • HTTPS

Vasily, [15.06.18/15/04 XNUMX:XNUMX] E c'è anche il trasporto UDP, ma non è documentato

E TCP in tre varianti

Il primo è simile a UDP su TCP, ogni pacchetto include un numero di sequenza e un crc
Perché è così doloroso leggere le banchine su un carrello?

Bene, adesso TCP già in 4 varianti:

  • Sintesi
  • Intermedio
  • intermedio imbottito
  • Lunga

Ok, intermedio imbottito per MTProxy, questo è stato successivamente aggiunto a causa di eventi noti. Ma perché altre due versioni (tre in totale), quando una potrebbe fare? Tutti e quattro differiscono essenzialmente solo per come impostare la lunghezza e il carico utile dello stesso MTProto principale, che sarà discusso ulteriormente:

  • in Abridged è 1 o 4 byte ma non 0xef quindi body
  • in Intermedio questo è 4 byte di lunghezza e un campo, e la prima volta che il client deve inviare 0xeeeeeeee per indicare che è Intermedio
  • in Full, il più avvincente, dal punto di vista di un networker: lunghezza, numero di sequenza e NON QUELLO che è fondamentalmente MTProto, body, CRC32. Sì, tutto questo su TCP. Il che ci fornisce un trasporto affidabile sotto forma di un flusso seriale di byte, non sono necessarie sequenze, in particolare checksum. Ok, ora mi verrà obiettato che TCP ha un checksum a 16 bit, quindi si verifica il danneggiamento dei dati. Fantastico, tranne per il fatto che in realtà abbiamo un protocollo crittografico con hash più lunghi di 16 byte, tutti questi errori - e anche di più - verranno rilevati da una mancata corrispondenza SHA a un livello superiore. Non c'è alcun punto in CRC32 su questo.

Confrontiamo Abridged, dove è possibile un byte di lunghezza, con Intermediate, che giustifica "Nel caso in cui sia necessario l'allineamento dei dati a 4 byte", il che è piuttosto assurdo. Cosa, si ritiene che i programmatori di Telegram siano così goffi da non poter leggere i dati dal socket in un buffer allineato? Devi ancora farlo, perché la lettura può restituirti un numero qualsiasi di byte (e ci sono anche server proxy, per esempio ...). Oppure, d'altra parte, perché preoccuparsi di Abbreviato se abbiamo ancora riempimenti pesanti da 16 byte in alto - risparmia 3 byte a volte ?

Si ha l'impressione che Nikolai Durov ami molto inventare biciclette, compresi i protocolli di rete, senza una reale necessità pratica.

Altre opzioni di trasporto, incl. Web e MTProxy, non considereremo ora, magari in un altro post, se c'è una richiesta. Ricorderemo solo ora di questo stesso MTProxy che subito dopo il suo rilascio nel 2018, i provider hanno imparato rapidamente a bloccarlo esattamente, destinato a bypass del bloccoDa dimensione del pacchetto! E anche il fatto che il server MTProxy scritto (sempre da Waltman) in C fosse inutilmente legato alle specifiche di Linux, sebbene non fosse affatto richiesto (confermerà Phil Kulin), e che un server simile o su Go o su Node.js adattarsi a meno di cento righe.

Ma trarremo conclusioni sull'alfabetizzazione tecnica di queste persone alla fine della sezione, dopo aver considerato altre questioni. Per ora, passiamo al quinto livello OSI, sessione, su cui hanno posizionato la sessione MTProto.

Chiavi, messaggi, sessioni, Diffie-Hellman

L'hanno messo lì in modo non del tutto corretto ... Una sessione non è la stessa sessione visibile nell'interfaccia sotto Sessioni attive. Ma in ordine.

Critiche al protocollo e agli approcci organizzativi di Telegram. Parte 1, tecnica: esperienza di scrittura di un cliente da zero - TL, MT

Qui abbiamo ricevuto una stringa di byte di lunghezza nota dal livello di trasporto. Questo è un messaggio crittografato o un testo in chiaro, se siamo ancora nella fase di negoziazione chiave e lo stiamo effettivamente facendo. Di quale gruppo di concetti chiamati "chiave" stiamo parlando? Chiariamo questo problema per lo stesso team di Telegram (mi scuso per aver tradotto la mia documentazione dall'inglese a un cervello stanco alle 4 del mattino, era più facile lasciare alcune frasi così come sono):

Ci sono due entità chiamate Sessione - uno nell'interfaccia utente dei client ufficiali sotto "sessioni correnti", in cui ogni sessione corrisponde a un intero dispositivo / sistema operativo.
Il secondo - Sessione MTProto, che contiene un numero di sequenza del messaggio (in un senso di basso livello) e which può durare tra diverse connessioni TCP. È possibile configurare più sessioni MTProto contemporaneamente, ad esempio per velocizzare il download dei file.

Tra questi due sessioni è il concetto autorizzazione. Nel caso degenerato, si può dire così sessione dell'interfaccia utente equivale a autorizzazioneMa ahimè, è complicato. Noi guardiamo:

  • L'utente sul nuovo dispositivo genera prima chiave d'autenticazione e lo lega all'account, ad esempio tramite SMS, ecco perché autorizzazione
  • È successo all'interno del primo Sessione MTProto, che ha session_id dentro te stesso.
  • A questo punto, la combinazione autorizzazione и session_id potrebbe essere chiamato esempio - questa parola si trova nella documentazione e nel codice di alcuni client
  • Quindi, il client può aprirsi un po 'di Sessioni MTProto sotto lo stesso chiave d'autenticazione - allo stesso DC.
  • Quindi un giorno il cliente deve richiedere un file da un'altra CC - e per questo DC ne verrà generato uno nuovo chiave d'autenticazione !
  • Per dire al sistema che questo non è un nuovo utente che si sta registrando, ma lo stesso autorizzazione (sessione dell'interfaccia utente), il client utilizza le chiamate API auth.exportAuthorization in casa DC auth.importAuthorization nella nuova DC.
  • Tuttavia, potrebbero essercene diversi aperti Sessioni MTProto (ognuno con il suo session_id) a questo nuovo DC, art il suo chiave d'autenticazione.
  • Infine, il cliente potrebbe desiderare Perfect Forward Secrecy. Ogni chiave d'autenticazione E 'stato permanente key - per controller di dominio - e il client può chiamare auth.bindTempAuthKey per uso temporaneo chiave d'autenticazione - e ancora, solo uno temp_auth_key per DC, comune a tutti Sessioni MTProto a questa CC.

Notare quello sale (e sali futuri) anche uno su chiave d'autenticazione quelli. condiviso tra tutti Sessioni MTProto alla stessa DC.

Cosa significa "tra diverse connessioni TCP"? Significa che questo qualcosa di simile a cookie di autorizzazione su un sito Web: persiste (sopravvive) a molte connessioni TCP a questo server, ma un giorno andrà male. Solo a differenza di HTTP, in MTProto, all'interno della sessione, i messaggi sono numerati in sequenza e confermati, sono entrati nel tunnel, la connessione è stata interrotta - dopo aver stabilito una nuova connessione, il server invierà gentilmente tutto in questa sessione che non ha consegnato nel precedente connessione TCP.

Tuttavia, le informazioni di cui sopra sono una compressione dopo molti mesi di contenzioso. Nel frattempo, stiamo implementando il nostro client da zero? - torniamo all'inizio.

Quindi generiamo auth_key su versioni di Diffie-Hellman da Telegram. Cerchiamo di capire la documentazione...

Vasily, [19.06.18/20/05 1:255] data_with_hash := SHAXNUMX(data) + data + (qualsiasi byte casuale); tale che la lunghezza sia pari a XNUMX byte;
dati_criptati := RSA(dati_con_hash, server_chiave_pubblica); un numero lungo 255 byte (big endian) viene elevato alla potenza richiesta sul modulo richiesto e il risultato viene memorizzato come numero a 256 byte.

Hanno della droga DH

Non sembra il DH di una persona sana
Non ci sono due chiavi pubbliche in dx

Ebbene, alla fine, l'abbiamo capito, ma il sedimento è rimasto: una prova del lavoro è stata fatta dal cliente che è stato in grado di fattorizzare il numero. Tipo di protezione contro gli attacchi DoS. E la chiave RSA viene utilizzata solo una volta in una direzione, essenzialmente per la crittografia new_nonce. Ma mentre questa operazione apparentemente semplice riesce, cosa dovrai affrontare?

Vasily, [20.06.18/00/26 XNUMX:XNUMX] Non ho ancora ricevuto la richiesta appid

Ho inviato una richiesta a DH

E, nel dock sul trasporto è scritto che può rispondere con 4 byte del codice di errore. E questo è tutto

Beh, mi ha detto -404, e allora?

Eccomi a lui: "prendi la tua efigna crittografata con la chiave del server con un'impronta digitale di tale e tale, voglio DH", e risponde stupidamente 404

Cosa penseresti di una simile risposta del server? Cosa fare? Non c'è nessuno a cui chiedere (ma ne parleremo più avanti nella seconda parte).

Qui tutto l'interesse per il molo è da fare

Non ho nient'altro da fare, ho solo sognato di convertire i numeri avanti e indietro

Due numeri a 32 bit. Li ho impacchettati come tutti gli altri

Ma no, sono questi due che ti servono per primi in una fila come BE

Vadim Goncharov, [20.06.18/15/49 404:XNUMX] e per questo XNUMX?

Vasily, [20.06.18/15/49 XNUMX:XNUMX] SÌ!

Vadim Goncharov, [20.06.18/15/50 XNUMX:XNUMX] quindi non capisco cosa possa "non aver trovato"

Vasily, [20.06.18 15:50] circa

Non ho trovato una tale scomposizione in semplici divisori%)

Anche la segnalazione degli errori non è stata padroneggiata

Vasily, [20.06.18/20/18 5:XNUMX] Oh, c'è anche MDXNUMX. Già tre diversi hash

L'impronta digitale della chiave viene calcolata come segue:

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

SHA1 e sha2

Quindi mettiamo auth_key 2048 bit di dimensione che abbiamo ottenuto secondo Diffie-Hellman. Qual è il prossimo? Quindi scopriamo che i 1024 bit inferiori di questa chiave non vengono utilizzati in alcun modo ... ma per ora pensiamo a questo. A questo punto, abbiamo un segreto condiviso con il server. È stato stabilito un analogo di una sessione TLS, una procedura molto costosa. Ma il server non sa ancora nulla di chi siamo! Non ancora, in realtà autorizzazione. Quelli. se pensavi in ​​\uXNUMXb\uXNUMXbtermini di "password di accesso", come era in ICQ, o almeno "chiave di accesso", come in SSH (ad esempio, su alcuni gitlab / github). Siamo diventati anonimi. E se il server ci risponde "questi numeri di telefono sono serviti da un altro DC"? O anche "il tuo numero di telefono è vietato"? La cosa migliore che possiamo fare è salvare la chiave nella speranza che per allora sia ancora utile e non marcia.

A proposito, l'abbiamo "ricevuto" con riserva. Ad esempio, ci fidiamo del server? È falso? Abbiamo bisogno di controlli crittografici:

Vasily, [21.06.18/17/53 2:XNUMX] Offrono ai client mobili di controllare un numero a XNUMX kbit per semplicità%)

Ma non è affatto chiaro, nafeijoa

Vasily, [21.06.18/18/02 XNUMX:XNUMX] Il molo non dice cosa fare se si rivelasse non semplice

Non detto. Vediamo cosa fa in questo caso il client ufficiale per Android? UN ecco cosa (e sì, l'intero file è interessante lì) - come si suol dire, lo lascerò qui:

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

No, certo che lì un po ' ci sono controlli per la semplicità di un numero, ma personalmente non ho più conoscenze sufficienti in matematica.

Ok, abbiamo la chiave principale. Per accedere, ad es. inviare richieste, è necessario eseguire un'ulteriore crittografia, già utilizzando AES.

La chiave del messaggio è definita come i 128 bit centrali dello SHA256 del corpo del messaggio (inclusi sessione, ID messaggio, ecc.), inclusi i byte di riempimento, preceduti da 32 byte presi dalla chiave di autorizzazione.

Vasily, [22.06.18/14/08 XNUMX:XNUMX] Puttane nella media

ha ottenuto un auth_key. Tutto. Inoltre loro ... non è chiaro dal molo. Sentiti libero di studiare il codice open source.

Si noti che MTProto 2.0 richiede da 12 a 1024 byte di riempimento, sempre soggetto alla condizione che la lunghezza del messaggio risultante sia divisibile per 16 byte.

Quindi quanta imbottitura mettere?

E sì, anche qui 404 in caso di errore

Se qualcuno ha studiato attentamente il diagramma e il testo della documentazione, ha notato che non c'è MAC lì. E che AES viene utilizzato in una modalità IGE che non viene utilizzata da nessun'altra parte. Loro, ovviamente, ne scrivono nelle loro FAQ... Qui, ad esempio, la chiave del messaggio stessa è allo stesso tempo l'hash SHA dei dati decifrati utilizzati per verificare l'integrità - e in caso di mancata corrispondenza, la documentazione per qualche motivo consiglia di ignorarli silenziosamente (ma per quanto riguarda la sicurezza, improvvisamente ci spezza?).

Non sono un crittografo, forse in questa modalità in questo caso non c'è niente di sbagliato dal punto di vista teorico. Ma posso sicuramente nominare un problema pratico, usando l'esempio di Telegram Desktop. Crittografa la cache locale (tutti questi D877F783D5D3EF8C) allo stesso modo dei messaggi in MTProto (solo in questo caso, versione 1.0), ovvero prima la chiave del messaggio, poi i dati stessi (e da qualche parte a parte il file principale big auth_key 256 byte, senza i quali msg_key inutile). Quindi, il problema diventa evidente su file di grandi dimensioni. Vale a dire, è necessario conservare due copie dei dati: crittografate e decrittografate. E se ci sono megabyte, o video in streaming, per esempio?.. Gli schemi classici con MAC dopo il testo cifrato permettono di leggerlo in streaming, trasferendolo immediatamente. E con MTProto devi all'inizio crittografare o decrittografare l'intero messaggio, solo successivamente trasferirlo sulla rete o su disco. Pertanto, nelle ultime versioni di Telegram Desktop nella cache in user_data è già utilizzato un altro formato, con AES in modalità CTR.

Vasily, [21.06.18/01/27 20:XNUMX] Oh, ho scoperto cos'è IGE: IGE è stato il primo tentativo di una "modalità di crittografia di autenticazione", originariamente per Kerberos. È stato un tentativo fallito (non fornisce protezione dell'integrità) e doveva essere rimosso. Questo è stato l'inizio di una ricerca ventennale per una modalità di crittografia di autenticazione che funzioni, che recentemente è culminata in modalità come OCB e GCM.

E ora gli argomenti dal lato del carrello:

Il team dietro Telegram, guidato da Nikolai Durov, è composto da sei campioni ACM, metà dei quali dottorandi in matematica. Ci sono voluti circa due anni per implementare la versione attuale di MTProto.

Che cosa è divertente. Due anni al livello inferiore

Oppure potremmo semplicemente prendere tls

Ok, diciamo che abbiamo fatto la crittografia e altre sfumature. Possiamo finalmente inviare richieste serializzate in TL e deserializzare le risposte? Quindi cosa dovrebbe essere inviato e come? Ecco il metodo initConnessioneforse è questo?

Vasily, [25.06.18/18/46 XNUMX:XNUMX] Inizializza la connessione e salva le informazioni sul dispositivo e sull'applicazione dell'utente.

Accetta app_id, device_model, system_version, app_version e lang_code.

E qualche domanda

Documentazione come sempre. Sentiti libero di studiare l'open source

Se tutto era più o meno chiaro con invokeWithLayer, allora che cos'è? Si scopre che supponiamo di avere - il client aveva già qualcosa da chiedere al server - c'è una richiesta che volevamo inviare:

Vasily, [25.06.18/19/13 XNUMX:XNUMX] A giudicare dal codice, la prima chiamata è racchiusa in questa spazzatura e la spazzatura stessa è in invokewithlayer

Perché initConnection non può essere una chiamata separata, ma deve essere un wrapper? Sì, come si è scoperto, deve essere eseguito ogni volta all'inizio di ogni sessione, e non una volta, come con la chiave principale. Ma! Non può essere chiamato da un utente non autorizzato! Qui siamo giunti allo stadio in cui è applicabile Questo pagina della documentazione - e ci dice che...

Solo una piccola parte dei metodi API è disponibile per gli utenti non autorizzati:

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

Il primo di loro auth.sendCode, e c'è quella preziosa prima richiesta in cui invieremo api_id e api_hash, e dopo di che riceveremo un SMS con un codice. E se arriviamo al DC sbagliato (i numeri di telefono di questo paese sono serviti da un altro, ad esempio), riceveremo un errore con il numero del DC desiderato. Per scoprire a quale indirizzo IP dobbiamo connetterci tramite il numero DC, saremo aiutati da help.getConfig. Una volta c'erano solo 5 voci, ma dopo i noti eventi del 2018, il numero è aumentato notevolmente.

Ora ricordiamo che siamo arrivati ​​\uXNUMXb\uXNUMXbin questa fase sul server anonimo. Non è troppo costoso ottenere solo un indirizzo IP? Perché non eseguire questa e altre operazioni nella parte non crittografata di MTProto? Sento un'obiezione: "come puoi assicurarti che non sia l'RKN a rispondere con indirizzi falsi?". A questo ricordiamo che, infatti, nei clienti ufficiali chiavi RSA integrate, cioè. puoi solo cartello questa informazione. In realtà, questo è già stato fatto per informazioni sull'esclusione dei blocchi che i client ricevono attraverso altri canali (è logico che non sia possibile farlo in MTProto stesso, perché è ancora necessario sapere dove connettersi).

OK. In questa fase dell'autorizzazione del cliente, non siamo ancora autorizzati e non abbiamo registrato la nostra domanda. Vogliamo solo vedere per ora cosa risponde il server ai metodi a disposizione di un utente non autorizzato. E qui…

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;

Nello schema, arriva il primo, il secondo

Nello schema tdesktop, il terzo valore è

Sì, da allora, ovviamente, la documentazione è stata aggiornata. Anche se presto potrebbe diventare di nuovo irrilevante. E come dovrebbe saperlo uno sviluppatore alle prime armi? Forse se registri la tua domanda, ti informeranno? Vasily lo ha fatto, ma purtroppo non gli è stato inviato nulla (di nuovo, ne parleremo nella seconda parte).

... Hai notato che in qualche modo siamo già passati all'API, ad es. al livello successivo e hai perso qualcosa nel tema MTProto? Niente di sorprendente:

Vasily, [28.06.18/02/04 2:XNUMX] Mm, stanno frugando in alcuni degli algoritmi su eXNUMXe

Mtproto definisce gli algoritmi e le chiavi di crittografia per entrambi i domini, oltre a un po' di struttura wrapper

Ma mescolano costantemente diversi livelli di stack, quindi non è sempre chiaro dove finisse mtproto e iniziasse il livello successivo.

Come si mescolano? Bene, ecco la stessa chiave temporanea per PFS, ad esempio (a proposito, Telegram Desktop non sa come farlo). Viene eseguito da una richiesta API auth.bindTempAuthKey, cioè. dal livello superiore. Ma allo stesso tempo, interferisce con la crittografia al livello inferiore: dopo, ad esempio, è necessario rifarlo initConnection ecc., questo non lo è solo richiesta normale. Separatamente, fornisce anche che puoi avere solo UNA chiave temporanea sul controller di dominio, sebbene il campo auth_key_id in ogni messaggio ti consente di cambiare la chiave almeno ogni messaggio e che il server ha il diritto di "dimenticare" la chiave temporanea in qualsiasi momento - cosa fare in questo caso, la documentazione non dice ... beh, perché non sarebbe possibile avere più chiavi, come con una serie di sali futuri, ma ?..

Ci sono alcune altre cose degne di nota nel tema MTProto.

Messaggi di messaggio, msg_id, msg_seqno, riconoscimenti, ping nella direzione sbagliata e altre idiosincrasie

Perché hai bisogno di sapere su di loro? Perché "perdono" un livello più alto e devi conoscerli quando lavori con l'API. Supponiamo di non essere interessati a msg_key, il livello inferiore ha decifrato tutto per noi. Ma all'interno dei dati decifrati, abbiamo i seguenti campi (anche la lunghezza dei dati per sapere dove si trova il padding, ma questo non è importante):

  • salt-int64
  • id_sessione - int64
  • message_id - int64
  • seq_no-int32

Ricordiamo che il sale è uno per l'intero DC. Perché sapere di lei? Non solo perché c'è una richiesta get_future_salts, che indica quali intervalli saranno validi, ma anche perché se il tuo sale è "marcio", il messaggio (richiesta) andrà semplicemente perso. Il server ovviamente riporterà il nuovo sale emettendo new_session_created - ma con quello vecchio dovrai in qualche modo rispedire, per esempio. E questa domanda influisce sull'architettura dell'applicazione.

Il server può interrompere del tutto le sessioni e rispondere in questo modo per molte ragioni. In realtà, cos'è una sessione MTProto dal lato client? Questi sono due numeri session_id и seq_no messaggi all'interno di questa sessione. Bene, e la connessione TCP sottostante, ovviamente. Diciamo che il nostro cliente ancora non sa fare molte cose, disconnesso, riconnesso. Se ciò è accaduto rapidamente, la vecchia sessione è continuata nella nuova connessione TCP, aumenta seq_no ulteriore. Se ci vuole molto tempo, il server potrebbe eliminarlo, perché dalla sua parte è anche una coda, come abbiamo scoperto.

Cosa dovrebbe essere seq_no? Oh, questa è una domanda complicata. Cerca di capire onestamente cosa si intendeva:

Messaggio relativo al contenuto

Un messaggio che richiede un riconoscimento esplicito. Questi includono tutti i messaggi dell'utente e molti messaggi di servizio, praticamente tutti ad eccezione dei contenitori e dei riconoscimenti.

Numero di sequenza del messaggio (msg_seqno)

Un numero a 32 bit pari al doppio del numero di messaggi “contenuti” (quelli che richiedono riconoscimento, e in particolare quelli che non sono contenitori) creati dal mittente prima di questo messaggio e successivamente incrementati di uno se il messaggio corrente è un messaggio relativo al contenuto. Un contenitore viene sempre generato dopo il suo intero contenuto; pertanto il suo numero di sequenza è maggiore o uguale ai numeri di sequenza dei messaggi in esso contenuti.

Che tipo di circo è questo con un incremento di 1, e poi un altro 2? .. Sospetto che il significato originale fosse "bit basso per ACK, il resto è un numero", ma il risultato non è del tutto corretto - in particolare, si scopre che può essere inviato un po 'di conferme che hanno lo stesso seq_no! Come? Ebbene, ad esempio, il server ci invia qualcosa, invia e noi stessi restiamo in silenzio, rispondiamo solo con messaggi di conferma del servizio sulla ricezione dei suoi messaggi. In questo caso, le nostre conferme in uscita avranno lo stesso numero in uscita. Se hai familiarità con TCP e pensi che questo suoni un po 'folle, ma sembra non essere molto selvaggio, perché in TCP seq_no non cambia, e la conferma va a seq_no dall'altra parte - allora mi affretto a sconvolgere. Le conferme stanno arrivando su MTProto NON su seq_no, come in TCP, ma msg_id !

Cos'è questo msg_id, il più importante di questi campi? L'ID univoco del messaggio, come suggerisce il nome. È definito come un numero a 64 bit, i cui bit meno significativi hanno di nuovo la magia server-non-server, e il resto è un timestamp Unix, inclusa la parte frazionaria, spostata di 32 bit a sinistra. Quelli. timestamp di per sé (e i messaggi con orari troppo diversi verranno rifiutati dal server). Da ciò risulta che, in generale, si tratta di un identificatore globale per il client. Mentre - ricorda session_id - siamo garantiti: In nessun caso un messaggio destinato a una sessione può essere inviato a una sessione diversa. Cioè, si scopre che c'è già tre livello: sessione, numero di sessione, ID messaggio. Perché una tale complicazione eccessiva, questo mistero è molto grande.

Così, msg_id necessario per…

RPC: richieste, risposte, errori. Conferme.

Come avrai notato, non esiste un tipo o una funzione speciale "effettua una richiesta RPC" da nessuna parte nello schema, sebbene ci siano risposte. Dopotutto, abbiamo messaggi relativi ai contenuti! Questo è, qualsiasi messaggio può essere una richiesta! O non esserlo. Dopotutto, ogni c'è msg_id. Ed ecco le risposte:

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

Qui è dove viene indicato a quale messaggio si tratta di una risposta. Pertanto, al livello più alto dell'API, dovrai ricordare quale numero aveva la tua richiesta - penso che non sia necessario spiegare che il lavoro è asincrono e possono esserci più richieste contemporaneamente, le cui risposte può essere restituito in qualsiasi ordine? In linea di principio, da questo e dai messaggi di errore come no worker, è possibile risalire all'architettura alla base di questo: il server che mantiene una connessione TCP con te è un bilanciatore front-end, indirizza le richieste ai back-end e le raccoglie message_id. Tutto sembra essere chiaro, logico e buono qui.

Sì?.. E se ci pensi? Dopotutto, anche la risposta RPC ha un campo msg_id! Dobbiamo urlare al server "non rispondi alla mia risposta!"? E sì, cosa c'era nella conferma? Informazioni sulla pagina messaggi sui messaggi ci dice cos'è

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

e ciascuna parte deve farlo. Ma non sempre! Se ricevi un RpcResult, serve da solo come riconoscimento. Cioè, il server può rispondere alla tua richiesta con MsgsAck, ad esempio "L'ho ricevuto". Può rispondere immediatamente a RpcResult. Potrebbe essere entrambe le cose.

E sì, devi ancora rispondere alla risposta! Conferma. In caso contrario, il server lo considererà non consegnato e te lo rilascerà. Anche dopo la riconnessione. Ma qui, ovviamente, sorgerà la questione dei timeout. Diamo un'occhiata a loro un po 'più tardi.

Nel frattempo, consideriamo i possibili errori nell'esecuzione della query.

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

Oh, qualcuno esclamerà, ecco un formato più umano - c'è una linea! Prenditi il ​​​​tuo tempo. Qui elenco degli errorima certamente non completa. Da esso apprendiamo che il codice è - qualcosa di simile a Errori HTTP (beh, ovviamente, la semantica delle risposte non è rispettata, in alcuni punti sono distribuite da codici a caso) e la stringa sembra LETTERE_E_NUMERI_MAIUSCOLE. Ad esempio, PHONE_NUMBER_OCCUPIED o FILE_PART_X_MISSING. Bene, cioè, devi ancora seguire questa linea analizzare. Ad esempio, FLOOD_WAIT_3600 significherà che devi aspettare un'ora, e PHONE_MIGRATE_5che il numero di telefono con questo prefisso dovrebbe essere registrato nella 5a DC. Abbiamo un linguaggio tipo, giusto? Non abbiamo bisogno di un argomento dalla stringa, le espressioni regolari andranno bene, cho.

Ancora una volta, questo non è nella pagina dei messaggi di servizio, ma, come è già consuetudine con questo progetto, è possibile trovare informazioni in un'altra pagina di documentazione. o destare sospetti. Innanzitutto, guarda, violazione della digitazione/livelli - RpcError si può investire RpcResult. Perché non fuori? Cosa non abbiamo preso in considerazione?.. Di conseguenza, dov'è la garanzia che RpcError potrebbe non essere investito RpcResult, ma essere direttamente o annidato in un altro tipo? manca req_msg_id ? ..

Ma continuiamo sui messaggi di servizio. Il cliente può considerare che il server sta pensando a lungo e fare una richiesta così meravigliosa:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Tre sono le possibili risposte, sempre intersecantisi con il meccanismo di conferma, per cercare di capire quali dovrebbero essere (e qual è l'elenco delle tipologie che non richiedono conferma in generale), al lettore resta da fare un compito (nota: il le informazioni nelle fonti di Telegram Desktop non sono complete).

Dipendenza: stati dei post dei messaggi

In generale, molti posti in TL, MTProto e Telegram in generale lasciano una sensazione di testardaggine, ma per gentilezza, tatto e altri abilità morbide ne abbiamo cortesemente taciuto e le oscenità nei dialoghi sono state censurate. Tuttavia, questo postoОla maggior parte della pagina su messaggi sui messaggi provoca shock anche per me, che lavoro da tempo con i protocolli di rete e ho visto biciclette con vari gradi di curvatura.

Inizia in modo innocuo, con conferme. Successivamente, ci viene detto

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;

Bene, tutti coloro che iniziano a lavorare con MTProto dovranno affrontarli, nel ciclo "corretto - ricompilato - lanciato", ottenere errori numerici o sale che è andato a male durante le modifiche è una cosa comune. Tuttavia, ci sono due punti qui:

  1. Ne consegue che il messaggio originale è perso. Dobbiamo recintare alcune code, lo considereremo più avanti.
  2. Cosa sono quegli strani numeri di errore? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64... dove sono gli altri numeri, Tommy?

La documentazione afferma:

L'intenzione è che i valori error_code siano raggruppati (error_code >> 4): ad esempio, i codici 0x40 - 0x4f corrispondono a errori nella scomposizione del contenitore.

ma, in primo luogo, uno spostamento nella direzione opposta e, in secondo luogo, non importa dove siano gli altri codici? Nella testa dell'autore?.. Tuttavia, queste sono sciocchezze.

La dipendenza inizia nei messaggi di stato dei post e nelle copie dei post:

  • Richiesta di informazioni sullo stato del messaggio
    Se una delle parti non riceve da tempo informazioni sullo stato dei propri messaggi in uscita, può richiederle esplicitamente all'altra parte:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Messaggio informativo relativo allo stato dei messaggi
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Qui, info è una stringa che contiene esattamente un byte dello stato del messaggio per ogni messaggio dall'elenco msg_ids in entrata:

    • 1 = non si sa nulla del messaggio (msg_id troppo basso, l'interlocutore potrebbe averlo dimenticato)
    • 2 = messaggio non ricevuto (msg_id rientra nell'intervallo di identificatori memorizzati; tuttavia, l'altra parte non ha sicuramente ricevuto un messaggio del genere)
    • 3 = messaggio non ricevuto (msg_id troppo alto; comunque l'interlocutore sicuramente non lo ha ancora ricevuto)
    • 4 = messaggio ricevuto (si noti che questa risposta è allo stesso tempo anche una conferma di ricezione)
    • +8 = messaggio già riconosciuto
    • +16 = messaggio che non richiede riconoscimento
    • +32 = Query RPC contenuta nel messaggio in elaborazione o elaborazione già completata
    • +64 = risposta relativa al contenuto al messaggio già generato
    • +128 = l'altra parte sa per certo che il messaggio è già stato ricevuto
      Questa risposta non richiede un riconoscimento. È un riconoscimento del relativo msgs_state_req, in sé e per sé.
      Si noti che se si scopre improvvisamente che l'altra parte non ha un messaggio che sembra essere stato inviato, il messaggio può essere semplicemente rispedito. Anche se l'interlocutore dovesse ricevere contemporaneamente due copie del messaggio, il duplicato verrà ignorato. (Se è passato troppo tempo e il msg_id originale non è più valido, il messaggio deve essere racchiuso in msg_copy).
  • Comunicazione volontaria dello stato dei messaggi
    Ciascuna parte può informare volontariamente l'altra parte dello stato dei messaggi trasmessi dall'altra parte.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Comunicazione volontaria estesa dello stato di un messaggio
    ...
    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;
  • Richiesta esplicita di inviare nuovamente i messaggi
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    La parte remota risponde immediatamente inviando nuovamente i messaggi richiesti […]
  • Richiesta esplicita di inviare nuovamente le risposte
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    La parte remota risponde immediatamente inviando di nuovo risposte ai messaggi richiesti […]
  • Copie dei messaggi
    In alcune situazioni, un vecchio messaggio con un msg_id che non è più valido deve essere rispedito. Quindi, è racchiuso in un contenitore di copia:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Una volta ricevuto, il messaggio viene elaborato come se il wrapper non fosse presente. Tuttavia, se è noto con certezza che il messaggio orig_message.msg_id è stato ricevuto, il nuovo messaggio non viene elaborato (mentre, allo stesso tempo, esso e orig_message.msg_id vengono riconosciuti). Il valore di orig_message.msg_id deve essere inferiore a msg_id del contenitore.

Rimaniamo persino in silenzio sul fatto che in msgs_state_info ancora una volta, le orecchie del TL incompiuto sporgono (avevamo bisogno di un vettore di byte, e nei due bit inferiori di enum, e nei bit più vecchi flag). Il punto è un altro. Qualcuno capisce perché tutto questo è in pratica nel cliente reale necessario?.. Con difficoltà, ma puoi immaginare qualche vantaggio se una persona è impegnata nel debug e in modalità interattiva: chiedi al server cosa e come. Ma le richieste sono descritte qui andata e ritorno.

Ne consegue che ciascuna parte non deve solo crittografare e inviare messaggi, ma anche archiviare dati su di essi, sulle risposte ad essi e per un periodo di tempo sconosciuto. La documentazione non descrive i tempi o l'applicabilità pratica di queste funzionalità. per nulla. La cosa più sorprendente è che sono effettivamente utilizzati nel codice dei clienti ufficiali! Apparentemente, è stato detto loro qualcosa che non era incluso nella documentazione aperta. Capire dal codice perché, non è più così semplice come nel caso di TL - questa non è una parte (relativamente) logicamente isolata, ma un pezzo legato all'architettura dell'applicazione, cioè richiederà molto più tempo per comprendere il codice dell'applicazione.

Ping e tempi. Code.

Da tutto, se ricordi le ipotesi sull'architettura del server (distribuzione delle richieste tra i back-end), segue una cosa piuttosto noiosa - nonostante tutte le garanzie di consegna che in TCP (o i dati sono stati consegnati o verrai informato del break, ma i dati verranno consegnati fino al momento del problema), che conferma in MTProto stesso - nessuna garanzia. Il server può facilmente perdere o buttare via il tuo messaggio e non si può fare nulla al riguardo, solo per recintare stampelle di vario tipo.

E prima di tutto - code di messaggi. Bene, per prima cosa, tutto era ovvio fin dall'inizio: un messaggio non confermato doveva essere archiviato e rispedito. E dopo che ora? E il giullare lo conosce. Forse quei messaggi di servizio per tossicodipendenti risolvono in qualche modo questo problema con le stampelle, diciamo, in Telegram Desktop ci sono circa 4 code corrispondenti (forse di più, come già accennato, per questo è necessario approfondire più seriamente il suo codice e l'architettura; allo stesso tempo, sappiamo che non può essere preso come campione, in esso non vengono utilizzati un certo numero di tipi dello schema MTProto).

Perché sta succedendo? Probabilmente, i programmatori del server non sono stati in grado di garantire l'affidabilità all'interno del cluster, o almeno anche il buffering sul front balancer, e hanno spostato questo problema sul client. Spinto dalla disperazione, Vasily ha cercato di implementare un'opzione alternativa, con solo due code, utilizzando algoritmi da TCP - misurando l'RTT al server e regolando la dimensione della "finestra" (nei messaggi) in base al numero di richieste non riconosciute. Cioè, un'euristica così approssimativa per stimare il carico del server: quante delle nostre richieste può masticare contemporaneamente e non perdere.

Bene, cioè, capisci, giusto? Se devi implementare nuovamente il TCP sopra un protocollo che funziona su TCP, questo indica un protocollo progettato molto male.

Oh sì, perché è necessaria più di una coda e, in generale, cosa significa questo per una persona che lavora con un'API di alto livello? Guarda, fai una richiesta, lo serializzi, ma spesso è impossibile inviarlo subito. Perché? Perché la risposta sarà msg_id, che è temporaneoаSono un'etichetta, il cui appuntamento è meglio posticipare il più tardi possibile - improvvisamente il server lo rifiuterà a causa di una discrepanza temporale tra noi e lui (ovviamente, possiamo fare una stampella che sposta il nostro tempo dal presente all'ora del server aggiungendo un delta calcolato dalle risposte del server - i client ufficiali lo fanno, ma questo metodo è rozzo e impreciso a causa del buffering). Quindi, quando effettui una richiesta con una chiamata di funzione locale dalla libreria, il messaggio passa attraverso le seguenti fasi:

  1. Si trova nella stessa coda ed è in attesa di crittografia.
  2. Nominato msg_id e il messaggio è andato a un'altra coda - possibile inoltro; inviare alla presa.
  3. a) Il server ha risposto MsgsAck - il messaggio è stato consegnato, lo eliminiamo dalla "altra coda".
    b) O viceversa, non gli è piaciuto qualcosa, ha risposto badmsg - rispediamo dall '"altra coda"
    c) Non si sa nulla, è necessario inviare nuovamente il messaggio da un'altra coda, ma non si sa esattamente quando.
  4. Il server ha finalmente risposto RpcResult - la risposta effettiva (o errore) - non solo consegnata, ma anche elaborata.

Forse, l'uso dei contenitori potrebbe risolvere parzialmente il problema. Questo è quando un gruppo di messaggi è racchiuso in uno e il server ha risposto con un riconoscimento a tutti in una volta, con uno msg_id. Ma rifiuterà anche questo pacchetto, se qualcosa è andato storto, anche l'intera faccenda.

E a questo punto entrano in gioco considerazioni non tecniche. Per esperienza, abbiamo visto molte stampelle e, inoltre, ora vedremo più esempi di cattivi consigli e architettura: in tali condizioni, vale la pena fidarsi e prendere tali decisioni? La domanda è retorica (ovviamente no).

Di cosa stiamo parlando? Se sull'argomento "messaggi tossicodipendenti sui messaggi" puoi ancora speculare con obiezioni come "sei stupido, non hai capito la nostra idea geniale!" (quindi prima scrivi la documentazione, come dovrebbero fare le persone normali, con motivazioni ed esempi di scambio di pacchetti, poi parleremo), quindi i tempi / timeout sono una questione puramente pratica e specifica, qui tutto è noto da tempo. Ma cosa ci dice la documentazione sui timeout?

Un server di solito conferma la ricezione di un messaggio da un client (normalmente, una query RPC) utilizzando una risposta RPC. Se una risposta tarda ad arrivare, un server può prima inviare una conferma di ricezione e, un po' più tardi, la stessa risposta RPC.

Un client normalmente riconosce la ricezione di un messaggio da un server (di solito, una risposta RPC) aggiungendo un riconoscimento alla successiva query RPC se non viene trasmesso troppo tardi (se viene generato, diciamo, 60-120 secondi dopo la ricezione di un messaggio dal server). Tuttavia, se per un lungo periodo di tempo non c'è motivo di inviare messaggi al server o se c'è un gran numero di messaggi non riconosciuti dal server (ad esempio, oltre 16), il client trasmette un riconoscimento autonomo.

... Traduco: noi stessi non sappiamo quanto e come sia necessario, beh, valutiamo che sia così.

E sui ping:

Messaggi Ping (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

Di solito viene restituita una risposta alla stessa connessione:

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

Questi messaggi non richiedono riconoscimenti. Un pong viene trasmesso solo in risposta a un ping mentre un ping può essere avviato da entrambe le parti.

Chiusura differita della connessione + PING

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

Funziona come il ping. Inoltre, dopo che questo è stato ricevuto, il server avvia un timer che chiuderà la connessione corrente disconnect_delay secondi dopo, a meno che non riceva un nuovo messaggio dello stesso tipo che resetta automaticamente tutti i timer precedenti. Se il client invia questi ping una volta ogni 60 secondi, ad esempio, può impostare disconnect_delay uguale a 75 secondi.

Sei fuori di testa?! In 60 secondi, il treno entrerà nella stazione, scenderà e farà salire i passeggeri e perderà nuovamente la connessione nel tunnel. Tra 120 secondi, mentre stai curiosando, arriverà a un altro e molto probabilmente la connessione si interromperà. Bene, è chiaro da dove crescono le gambe: "Ho sentito uno squillo, ma non so dove sia", c'è l'algoritmo Nagle e l'opzione TCP_NODELAY, che era destinata al lavoro interattivo. Ma, scusa, ritarda il suo valore predefinito - 200 Millisecondi. Se vuoi davvero ritrarre qualcosa di simile e risparmiare su una possibile coppia di pacchetti, beh, rimandalo, almeno per 5 secondi, o qualunque sia il timeout del messaggio "L'utente sta digitando ..." è ora uguale a. Ma non più.

E infine, ping. Cioè, controllando la vivacità di una connessione TCP. È divertente, ma circa 10 anni fa ho scritto un testo critico sul messenger dell'ostello della nostra facoltà - lì gli autori hanno anche eseguito il ping del server dal client, e non viceversa. Ma gli studenti del terzo anno sono una cosa, e un ufficio internazionale è un'altra, giusto? ..

Innanzitutto, un piccolo programma educativo. Una connessione TCP, in assenza di scambio di pacchetti, può vivere per settimane. Questo è sia buono che cattivo, a seconda dello scopo. Bene, se avevi una connessione SSH al server aperta, ti sei alzato dal tuo computer, hai riavviato il router di alimentazione, sei tornato a casa tua - la sessione attraverso questo server non si è interrotta (non hai digitato nulla, non c'erano pacchetti), conveniente. È brutto se ci sono migliaia di client sul server, ognuno occupa risorse (ciao Postgres!) E l'host del client potrebbe essersi riavviato molto tempo fa, ma non lo sapremo.

I sistemi di chat/IM appartengono al secondo caso per un altro motivo aggiuntivo: gli stati online. Se l'utente "è caduto", è necessario informarne i suoi interlocutori. In caso contrario, ci sarà un errore commesso dai creatori di Jabber (e corretto per 20 anni): l'utente si è disconnesso, ma continuano a scrivergli messaggi, credendo che sia online (anche questi sono stati completamente persi in questi pochi minuti prima la rottura è stata scoperta). No, l'opzione TCP_KEEPALIVE, che molte persone che non capiscono come funzionano i timer TCP, si apre ovunque (impostando valori selvaggi come decine di secondi), non aiuterà qui - devi assicurarti che non solo il kernel del sistema operativo di la macchina dell'utente è viva, ma funziona anche normalmente, in grado di rispondere, e l'applicazione stessa (pensi che non possa bloccarsi? Telegram Desktop su Ubuntu 18.04 si è bloccato ripetutamente per me).

Ecco perché dovresti eseguire il ping Server client, e non viceversa: se il client lo fa, quando la connessione viene interrotta, il ping non verrà consegnato, l'obiettivo non viene raggiunto.

E cosa vediamo in Telegram? Tutto è esattamente l'opposto! Bene, cioè formalmente, ovviamente, entrambe le parti possono eseguire il ping a vicenda. In pratica, i clienti usano una stampella ping_delay_disconnect, che attiva un timer sul server. Beh, scusa, non è compito del cliente decidere per quanto tempo vuole vivere lì senza ping. Il server, in base al suo carico, lo sa meglio. Ma, naturalmente, se non ti dispiace per le risorse, allora i malvagi Pinocchio sono loro stessi e la stampella scenderà ...

Come avrebbe dovuto essere progettato?

Credo che i fatti di cui sopra indichino abbastanza chiaramente la competenza non molto elevata del team Telegram / VKontakte nel campo del livello di trasporto (e inferiore) delle reti di computer e la loro scarsa qualificazione in questioni pertinenti.

Perché si è rivelato così complicato e come possono provare a obiettare gli architetti di Telegram? Il fatto che abbiano provato a creare una sessione che sopravviva alle interruzioni della connessione TCP, ovvero ciò che non abbiamo consegnato ora, lo consegneremo in seguito. Probabilmente hanno anche provato a effettuare il trasporto UDP, anche se hanno incontrato difficoltà e l'hanno abbandonato (ecco perché la documentazione è vuota - non c'era nulla di cui vantarsi). Ma a causa della mancanza di comprensione di come funzionano le reti in generale e il TCP in particolare, dove puoi fare affidamento su di esso e dove devi farlo da solo (e come), e tenta di combinare questo con la crittografia "uno scatto su due uccelli con una fava” - si è scoperto un tale cadavere.

Come avrebbe dovuto essere? Basato sul fatto che msg_id è un timestamp crittograficamente necessario per prevenire attacchi di riproduzione, è un errore allegare ad esso una funzione di identificatore univoco. Pertanto, senza modificare drasticamente l'architettura attuale (quando viene formato il thread degli aggiornamenti, questo è un argomento API di alto livello per un'altra parte di questa serie di post), si dovrebbe:

  1. Il server che detiene la connessione TCP al client si assume la responsabilità: se hai sottratto dal socket, conferma, elabora o restituisci un errore, nessuna perdita. Quindi la conferma non è un vettore di id, ma semplicemente "l'ultimo seq_no ricevuto" - solo un numero, come in TCP (due numeri - il tuo seq e confermato). Siamo sempre in seduta, vero?
  2. Il timestamp per prevenire gli attacchi di replay diventa un campo separato, a la nonce. Controllato, ma nient'altro è interessato. Abbastanza e uint32 - se il nostro sale cambia almeno ogni mezza giornata, possiamo allocare 16 bit ai bit inferiori della parte intera dell'ora corrente, il resto - alla parte frazionaria di un secondo (come è ora).
  3. Retratto msg_id affatto - dal punto di vista della distinzione delle richieste sui back-end, c'è, in primo luogo, l'ID client e, in secondo luogo, l'ID sessione e li concatena. Di conseguenza, come identificatore di richiesta, solo uno è sufficiente seq_no.

Inoltre, non è l'opzione migliore, un random completo potrebbe fungere da identificatore: questo è già stato fatto nell'API di alto livello durante l'invio di un messaggio, tra l'altro. Sarebbe meglio rifare completamente l'architettura dal relativo all'assoluto, ma questo è un argomento per un'altra parte, non per questo post.

API?

Ta-daam! Quindi, dopo aver percorso un percorso pieno di dolore e stampelle, siamo finalmente riusciti a inviare eventuali richieste al server e ricevere eventuali risposte, nonché ricevere aggiornamenti dal server (non in risposta a una richiesta, ma ci invia lui stesso, come PUSH, se qualcuno è molto più chiaro).

Attenzione, ora ci sarà l'unico esempio Perl nell'articolo! (per chi non ha familiarità con la sintassi, il primo argomento di bless è la struttura dati dell'oggetto, il secondo è la sua 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ì, specialmente non sotto lo spoiler: se non l'hai letto, vai e fallo!

Oh, wai~~… che aspetto ha? Qualcosa di molto familiare ... forse questa è la struttura dei dati di una tipica API Web in JSON, tranne forse che le classi erano collegate agli oggetti? ..

Quindi si scopre ... Che cos'è, compagni? .. Tanto sforzo - e ci siamo fermati a riposare dove i programmatori Web appena iniziato?.. Non sarebbe più semplice JSON su HTTPS?! E cosa abbiamo ottenuto in cambio? Ne è valsa la pena?

Valutiamo cosa ci ha dato TL+MTProto e quali alternative sono possibili. Bene, la richiesta-risposta HTTP non va bene, ma almeno qualcosa al di sopra di TLS?

serializzazione compatta. Vedendo questa struttura dati, simile a JSON, si ricorda che esistono le sue varianti binarie. Contrassegniamo MsgPack come non sufficientemente estensibile, ma esiste, ad esempio, CBOR - a proposito, lo standard descritto in RFC 7049. È notevole per il fatto che definisce tag, come meccanismo di estensione, e tra già standardizzato ci sono:

  • 25 + 256 - sostituzione delle righe duplicate con un riferimento al numero di riga, un metodo di compressione così economico
  • 26 - oggetto Perl serializzato con nome di classe e argomenti del costruttore
  • 27 - oggetto indipendente dal linguaggio serializzato con nome del tipo e argomenti del costruttore

Bene, ho provato a serializzare gli stessi dati in TL e CBOR con l'impacchettamento di stringhe e oggetti abilitato. Il risultato ha cominciato a differire a favore di CBOR da qualche parte da un megabyte:

cborlen=1039673 tl_len=1095092

Così, conclusione: Esistono formati sostanzialmente più semplici che non sono soggetti al problema dell'errore di sincronizzazione o dell'identificatore sconosciuto, con un'efficienza comparabile.

Stabilimento veloce della connessione. Ciò significa zero RTT dopo la riconnessione (quando la chiave è già stata generata una volta) - applicabile dal primissimo messaggio MTProto, ma con alcune riserve - sono entrati nello stesso sale, la sessione non è andata a male, ecc. Cosa ci offre in cambio TLS? Citazione correlata:

Quando si utilizza PFS in TLS, i ticket di sessione TLS (RFC 5077) per riprendere la sessione crittografata senza rinegoziare le chiavi e senza memorizzare le informazioni sulla chiave sul server. Quando si apre la prima connessione e si generano le chiavi, il server crittografa lo stato della connessione e lo invia al client (sotto forma di ticket di sessione). Di conseguenza, quando la connessione viene ripristinata, il client invia al server un ticket di sessione contenente, tra l'altro, la chiave di sessione. Il ticket stesso è crittografato con una chiave temporanea (chiave del ticket di sessione), che è memorizzata sul server e deve essere distribuita a tutti i server frontend che gestiscono SSL nelle soluzioni cluster.[10]. Pertanto, l'introduzione di un ticket di sessione può violare PFS se le chiavi temporanee del server vengono compromesse, ad esempio, quando vengono archiviate per lungo tempo (OpenSSL, nginx, Apache per impostazione predefinita le memorizzano per tutto il tempo in cui il programma è in esecuzione; siti popolari utilizzare la chiave per diverse ore, fino a giorni).

Qui RTT non è zero, è necessario scambiare almeno ClientHello e ServerHello, dopodiché, insieme a Finished, il client può già inviare i dati. Ma qui va ricordato che non abbiamo il Web, con il suo mucchio di connessioni appena aperte, ma un messenger, la cui connessione è spesso una e richieste più o meno longeve, relativamente brevi di pagine Web: tutto è multiplexato all'interno. Cioè, è abbastanza accettabile, se non ci siamo imbattuti in una pessima sezione della metropolitana.

Hai dimenticato qualcos'altro? Scrivi nei commenti.

Continua!

Nella seconda parte di questa serie di post, prenderemo in considerazione questioni organizzative piuttosto che tecniche: approcci, ideologia, interfaccia, atteggiamento nei confronti degli utenti, ecc. Sulla base, tuttavia, delle informazioni tecniche che sono state presentate qui.

La terza parte proseguirà l'analisi della componente tecnica/esperienza di sviluppo. Imparerai in particolare:

  • continuazione del pandemonio con la varietà di tipi TL
  • cose sconosciute su canali e supergruppi
  • dei dialoghi è peggio del roster
  • sull'indirizzamento assoluto e relativo dei messaggi
  • qual è la differenza tra foto e immagine
  • come le emoji interferiscono con il testo in corsivo

e altre stampelle! Rimani sintonizzato!

Fonte: habr.com

Aggiungi un commento