Críticas al protocolo y enfoques organizativos de Telegram. Parte 1, técnica: experiencia de escribir un cliente desde cero - TL, MT

Recientemente, han comenzado a aparecer publicaciones en Habré con más frecuencia sobre lo bueno que es Telegram, lo brillantes y experimentados que son los hermanos Durov en la construcción de sistemas de red, etc. Al mismo tiempo, muy pocas personas realmente se sumergieron en el dispositivo técnico; a lo sumo, usan una API de bot basada en JSON bastante simple (y muy diferente de MTProto) y, por lo general, solo aceptan en la fe todos esos elogios y relaciones públicas que giran en torno al mensajero. Hace casi un año y medio, mi compañero de NPO Echelon Vasily (lamentablemente, su cuenta en Habré fue eliminada junto con el borrador) comenzó a escribir su propio cliente de Telegram desde cero en Perl, y luego se unió el autor de estas líneas. ¿Por qué Perl, algunos se preguntarán de inmediato? Porque ya existen proyectos de este tipo en otros idiomas, de hecho no se trata de eso, podría haber cualquier otro idioma donde biblioteca terminada, y en consecuencia el autor debe ir hasta el final desde cero. Además, la criptografía es tal cosa: confiar, pero verificar. Con un producto centrado en la seguridad, no puede simplemente confiar en la biblioteca lista para usar de un proveedor y creerlo ciegamente (sin embargo, este es un tema para más en la segunda parte). Por el momento, la biblioteca funciona bastante bien en el nivel "medio" (le permite realizar cualquier solicitud de API).

Sin embargo, no habrá mucha criptografía y matemáticas en esta serie de publicaciones. Pero habrá muchos otros detalles técnicos y muletas arquitectónicas (también será útil para aquellos que no escribirán desde cero, pero usarán la biblioteca en cualquier idioma). Entonces, el objetivo principal era tratar de implementar el cliente desde cero. según documentación oficial. Es decir, supongamos que el código fuente de los clientes oficiales está cerrado (nuevamente, en la segunda parte, revelaremos con más detalle el tema de lo que realmente es esto). es entonces), pero, como en los viejos tiempos, por ejemplo, existe un estándar como RFC: ¿es posible escribir un cliente de acuerdo con la especificación solo, "sin mirar" en el código fuente, incluso oficial (Telegram Desktop, móvil ), incluso Teletón no oficial?

Tabla de contenido:

Documentación... ¿está ahí? ¿Es verdad?..

Fragmentos de notas para este artículo comenzaron a recopilarse el verano pasado. Todo este tiempo en el sitio oficial. https://core.telegram.org la documentación era a partir de la capa 23, es decir atrapado en algún lugar en 2014 (¿recuerdas, en ese entonces ni siquiera había canales todavía?). Eso sí, en teoría, esto debería haber hecho posible implementar un cliente con funcionalidad en ese momento en 2014. Pero incluso en este estado, la documentación era, en primer lugar, incompleta y, en segundo lugar, en algunos lugares se contradecía a sí misma. Hace poco más de un mes, en septiembre de 2019, fue accidentalmente se encontró que el sitio tiene una gran actualización de la documentación, para una Capa 105 completamente nueva, con una nota de que ahora todo debe leerse nuevamente. De hecho, muchos artículos han sido revisados, pero muchos han permanecido sin cambios. Por lo tanto, cuando lea las críticas a continuación sobre la documentación, debe tener en cuenta que algunas de estas cosas ya no son relevantes, pero algunas todavía lo son. Después de todo, 5 años en el mundo moderno no es mucho, sino muy mucho. Desde entonces (especialmente si no tiene en cuenta los geochats descartados y resucitados desde entonces), ¡la cantidad de métodos API en el esquema ha aumentado de cien a más de doscientos cincuenta!

¿Por dónde empiezas como joven escritor?

No importa si escribe desde cero o usa, por ejemplo, bibliotecas listas para usar como Teletón para Python o Madeline para PHP, en cualquier caso, primero necesitarás registra tu aplicación - obtener parámetros api_id и api_hash (aquellos que trabajaron con la API de VKontakte lo entienden de inmediato) por el cual el servidor identificará la aplicación. Este tener que por razones legales, pero hablaremos más sobre por qué los autores de bibliotecas no pueden publicarlo en la segunda parte. Quizás esté satisfecho con los valores de prueba, aunque son muy limitados; el hecho es que ahora puede registrarse en su número solo uno aplicación, así que no se apresure.

Ahora bien, desde un punto de vista técnico, nos debería haber interesado el hecho de que tras el registro deberíamos recibir notificaciones de Telegram sobre actualizaciones de la documentación, protocolo, etc. Es decir, se podría suponer que el sitio con los muelles simplemente se "puntuó" y continuó trabajando específicamente con aquellos que comenzaron a hacer clientes, porque. es mas fácil. Pero no, no se observó nada de eso, no llegó ninguna información.

Y si escribe desde cero, entonces el uso de los parámetros recibidos aún está muy lejos. A pesar de https://core.telegram.org/ y habla de ellos primero en Primeros pasos, de hecho, primero debe implementar Protocolo MTProto - pero si crees disposición según el modelo OSI al final de la página de la descripción general del protocolo, luego completamente en vano.

De hecho, tanto antes como después de MTProto, en varios niveles a la vez (como dicen los usuarios de redes extranjeras que trabajan en el kernel del sistema operativo, violación de capa), un tema grande, doloroso y terrible se interpondrá en el camino ...

Serialización binaria: TL (Type Language) y su esquema, capas y muchas otras palabras de miedo

Este tema, de hecho, es la clave de los problemas de Telegram. Y habrá muchas palabras terribles si tratas de profundizar en ellas.

Entonces, esquema. Si recuerdas esta palabra, di: Esquema JSONPensaste bien. El objetivo es el mismo: algún lenguaje para describir un posible conjunto de datos transmitidos. Aquí, de hecho, es donde termina la similitud. Si de la pagina Protocolo MTProto, o desde el árbol de fuentes del cliente oficial, intentaremos abrir algún esquema, veremos algo como:

int ? = Int;
long ? = Long;
double ? = Double;
string ? = String;

vector#1cb5c415 {t:Type} # [ t ] = Vector t;

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

rpc_answer_unknown#5e2ad36e = RpcDropAnswer;
rpc_answer_dropped_running#cd78e586 = RpcDropAnswer;
rpc_answer_dropped#a43ad8b7 msg_id:long seq_no:int bytes:int = RpcDropAnswer;

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

---functions---

set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:bytes = Set_client_DH_params_answer;

ping#7abe77ec ping_id:long = Pong;
ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong;

invokeAfterMsg#cb9f372d msg_id:long query:!X = X;
invokeAfterMsgs#3dc4b4f0 msg_ids:Vector<long> query:!X = X;

account.updateProfile#78515775 flags:# first_name:flags.0?string last_name:flags.1?string about:flags.2?string = User;
account.sendChangePhoneCode#8e57deb flags:# allow_flashcall:flags.0?true phone_number:string current_number:flags.0?Bool = auth.SentCode;

Una persona que ve esto por primera vez reconocerá intuitivamente solo una parte de lo que está escrito; bueno, aparentemente son estructuras (aunque ¿dónde está el nombre, a la izquierda o a la derecha?), Hay campos en ellos, después de lo cual el tipo pasa por el colon... probablemente. Aquí, entre paréntesis angulares, probablemente haya plantillas como en C ++ (de hecho, no del todo). ¿Y qué significan todos los demás símbolos, signos de interrogación, signos de exclamación, porcentajes, redes (y obviamente significan cosas diferentes en diferentes lugares), presentes en algún lugar, pero no en algún lugar, números hexadecimales, y lo más importante, cómo obtener de esto? derecho (que no será rechazado por el servidor) flujo de bytes? Tienes que leer la documentación. (Sí, hay enlaces al esquema en la versión JSON cercana, pero esto no lo aclara).

Abriendo la página Serialización de datos binarios y sumérgete en el mundo mágico de las setas y las matemáticas discretas, algo parecido a matan en 4º. Alfabeto, tipo, valor, combinador, combinador funcional, forma normal, tipo compuesto, tipo polimórfico... ¡y eso es solo la primera página! A continuación te espera Lenguaje TL, que, aunque ya contiene un ejemplo de una solicitud y respuesta triviales, no proporciona una respuesta a los casos más típicos en absoluto, lo que significa que tendrá que pasar por la narración de las matemáticas traducidas del ruso al inglés en ocho anidados más. páginas!

Los lectores familiarizados con los lenguajes funcionales y la inferencia automática de tipos, por supuesto, vieron en este lenguaje descripciones, incluso a partir de un ejemplo, mucho más familiares, y pueden decir que, en general, esto no es malo en principio. Las objeciones a esto son:

  • sí, objetivo suena bien, pero ay no logrado
  • la educación en las universidades rusas varía incluso entre las especialidades de TI: no todos leen el curso correspondiente
  • Finalmente, como veremos, en la práctica es No requiere, ya que solo se usa un subconjunto limitado de incluso el TL que se describió

Como se dijo LeoNerd en el canal #perl en la red FreeNode IRC, tratando de implementar una puerta de Telegram a Matrix (la traducción de la cita es inexacta de memoria):

Se siente como alguien que conoció la teoría de tipos por primera vez, se entusiasmó y comenzó a intentar jugar con ella, sin importarle realmente si era necesaria en la práctica.

Vea por sí mismo si la necesidad de tipos básicos (int, long, etc.) como algo elemental no plantea preguntas; al final, deben implementarse manualmente; por ejemplo, intentemos derivar de ellos. vector. Eso es, de hecho, masivo, si llamas a las cosas resultantes por sus nombres propios.

Pero antes

Breve descripción de un subconjunto de la sintaxis TL para aquellos que no... lea la documentación oficial

constructor = Type;
myVec ids:Vector<long> = Type;

fixed#abcdef34 id:int = Type2;

fixedVec set:Vector<Type2> = FixedVec;

constructorOne#crc32 field1:int = PolymorType;
constructorTwo#2crc32 field_a:long field_b:Type3 field_c:int = PolymorType;
constructorThree#deadcrc bit_flags_of_what_really_present:# optional_field4:bit_flags_of_what_really_present.1?Type = PolymorType;

an_id#12abcd34 id:int = Type3;
a_null#6789cdef = Type3;

Siempre comienza definición diseñador, después de lo cual, opcionalmente (en la práctica, siempre) a través del símbolo # debería CRC32 de la cadena de descripción normalizada del tipo dado. Luego viene la descripción de los campos, si lo son, el tipo puede estar vacío. Todo termina con un signo igual, el nombre del tipo al que pertenece el constructor dado, es decir, el subtipo. El tipo a la derecha del signo igual es polimórfico - es decir, puede corresponder a varios tipos específicos.

Si la definición se produce después de la línea ---functions---, entonces la sintaxis seguirá siendo la misma, pero el significado será diferente: el constructor se convertirá en el nombre de la función RPC, los campos se convertirán en parámetros (bueno, es decir, seguirá siendo exactamente la misma estructura dada como se describe a continuación, será solo el significado dado), y "tipo polimórfico" es el tipo del resultado devuelto. Es cierto que seguirá siendo polimórfico, solo definido en la sección ---types---, y este constructor no será considerado. Escriba sobrecargas de funciones llamadas por sus argumentos, es decir por alguna razón, varias funciones con el mismo nombre pero una firma diferente, como en C++, no se proporcionan en TL.

¿Por qué "constructor" y "polimórfico" si no es programación orientada a objetos? Bueno, de hecho, será más fácil para alguien pensar en términos de OOP: un tipo polimórfico como una clase abstracta, y los constructores son sus clases descendientes directas, además final en la terminología de varios idiomas. De hecho, por supuesto, aquí similitud con métodos constructores reales sobrecargados en lenguajes de programación OO. Dado que aquí solo hay estructuras de datos, no hay métodos (aunque la descripción de funciones y métodos a continuación es bastante capaz de crear confusión en la cabeza sobre lo que son, pero eso es otra cosa): puede pensar en un constructor como un valor del cual siendo construido escriba al leer un flujo de bytes.

¿Como sucedió esto? El deserializador, que siempre lee 4 bytes, ve el valor 0xcrc32 - y entiende lo que sucederá después field1 con tipo int, es decir. lee exactamente 4 bytes, en este campo superpuesto con tipo PolymorType leer. ve 0x2crc32 y entiende que hay dos campos más, primero long, entonces leemos 8 bytes. Y luego nuevamente un tipo complejo, que se deserializa de la misma manera. Por ejemplo, Type3 podría declararse en el esquema tan pronto como dos constructores, respectivamente, además deban cumplir 0x12abcd34, después de lo cual necesita leer otros 4 bytes intO 0x6789cdef, después de lo cual no habrá nada. Cualquier otra cosa, debe lanzar una excepción. En cualquier caso, después de eso volvemos a leer 4 bytes. int campo field_c в constructorTwo y en eso terminamos de leer nuestro PolymorType.

Finalmente, si es atrapado 0xdeadcrc para constructorThree, entonces las cosas se complican más. Nuestro primer campo bit_flags_of_what_really_present con tipo # - de hecho, esto es solo un alias para el tipo natque significa "número natural". Es decir, de hecho, int sin signo es el único caso, por cierto, cuando se encuentran números sin signo en esquemas reales. Entonces, lo siguiente es una construcción con un signo de interrogación, lo que significa que este es el campo: estará presente en el cable solo si el bit correspondiente se establece en el campo al que se hace referencia (aproximadamente como un operador ternario). Entonces, supongamos que este bit estaba activado, entonces necesita leer un campo como Type, que en nuestro ejemplo tiene 2 constructores. Uno está vacío (consiste solo en un identificador), el otro tiene un campo ids con tipo ids:Vector<long>.

Puede pensar que tanto las plantillas como los genéricos son buenos o Java. Pero no. Casi. Este единственный caso de corchetes angulares en circuitos reales, y SOLO se usa para Vector. En un flujo de bytes, serán 4 bytes CRC32 para el tipo Vector en sí, siempre el mismo, luego 4 bytes: la cantidad de elementos de la matriz y luego estos elementos en sí.

Agregue a esto el hecho de que la serialización siempre ocurre en palabras de 4 bytes, todos los tipos son múltiplos de él; también se describen los tipos incorporados bytes и string con la serialización manual de la longitud y esta alineación por 4 - bueno, ¿parece sonar normal e incluso relativamente eficiente? Aunque se afirma que TL es una serialización binaria eficiente, pero al diablo con ellos, con la expansión de cualquier cosa, incluso valores booleanos y cadenas de un solo carácter de hasta 4 bytes, ¿JSON seguirá siendo mucho más grueso? Mire, incluso los campos innecesarios se pueden omitir mediante indicadores de bits, todo está bien e incluso es extensible para el futuro, ¿agregó nuevos campos opcionales al constructor más tarde? ...

Pero no, si no lees mi breve descripción, sino la documentación completa, y piensas en la implementación. En primer lugar, el CRC32 del constructor se calcula mediante la cadena de descripción del texto del esquema normalizado (elimine los espacios en blanco adicionales, etc.), por lo que si se agrega un nuevo campo, la cadena de descripción del tipo cambiará y, por lo tanto, su CRC32 y, en consecuencia, la serialización. ¿Y qué haría el antiguo cliente si recibiera un campo con nuevas banderas configuradas, pero no supiera qué hacer con ellas a continuación?...

En segundo lugar, recordemos CRC32, que se utiliza aquí esencialmente como funciones hash para determinar de forma única qué tipo se está (des) serializando. Aquí nos enfrentamos al problema de las colisiones, y no, la probabilidad no es una en 232, sino mucho más. ¿Quién recordó que CRC32 está diseñado para detectar (y corregir) errores en el canal de comunicación y, en consecuencia, mejorar estas propiedades en detrimento de otras? Por ejemplo, a ella no le importa la permutación de bytes: si cuenta CRC32 desde dos líneas, en la segunda intercambiará los primeros 4 bytes con los siguientes 4 bytes, será lo mismo. Cuando tenemos cadenas de texto del alfabeto latino (y un poco de puntuación) como entrada, y estos nombres no son particularmente aleatorios, la probabilidad de tal permutación aumenta considerablemente.

Por cierto, ¿quién revisó lo que había allí? realmente CRC32? En una de las primeras fuentes (incluso antes de Waltman) había una función hash que multiplicaba cada carácter por el número 239, tan querido por esta gente, ¡ja, ja!

Finalmente, nos dimos cuenta de que los constructores con un tipo de campo Vector<int> и Vector<PolymorType> tendrá diferentes CRC32. ¿Y la presentación en la línea? Y en términos de teoría, se convierte en parte del tipo? Digamos que pasamos una matriz de diez mil números, bueno, con Vector<int> todo está claro, la longitud y otros 40000 bytes. y si esto Vector<Type2>, que consta de un solo campo int y es el único en el tipo: ¿necesitamos repetir 10000xabcdef0 34 veces y luego 4 bytes? int, o el lenguaje puede MOSTRAR esto para nosotros desde el constructor fixedVec y en lugar de 80000 bytes, transferir de nuevo solo 40000?

Esta no es una pregunta teórica ociosa en absoluto: imagine que obtiene una lista de usuarios grupales, cada uno de los cuales tiene una identificación, nombre, apellido: la diferencia en la cantidad de datos transferidos a través de una conexión móvil puede ser significativa. Es la efectividad de la serialización de Telegram lo que se nos anuncia.

Así que ...

Vector, que no se pudo deducir

Si intenta leer las páginas de descripción de los combinadores y demás, verá que un vector (e incluso una matriz) está tratando formalmente de deducir varias hojas a través de tuplas. Pero al final se martillan, se salta el paso final y simplemente se da la definición de un vector, que tampoco está vinculado a un tipo. ¿Qué pasa aquí? en idiomas programacion, especialmente los funcionales, es bastante típico describir la estructura de forma recursiva: el compilador con su evaluación perezosa comprenderá todo y lo hará. en idioma serialización de datos pero se necesita EFICIENCIA: basta simplemente con describir lista, es decir. una estructura de dos elementos: el primero es un elemento de datos, el segundo es la misma estructura en sí misma o un espacio vacío para la cola (paquete (cons) en ceceo). Pero esto obviamente requerirá cada El elemento adicionalmente gasta 4 bytes (CRC32 en el caso de TL) para describir su tipo. Es fácil describir una matriz tamaño fijo, pero en el caso de una matriz de una longitud previamente desconocida, nos separamos.

Entonces, dado que TL no le permite generar un vector, tuvo que agregarse al costado. En última instancia, la documentación dice:

La serialización siempre usa el mismo "vector" constructor (const 0x1cb5c415 = crc32 ("vector t: Tipo # [ t ] = Vector t") que no depende del valor específico de la variable de tipo t.

El valor del parámetro opcional t no está involucrado en la serialización ya que se deriva del tipo de resultado (siempre conocido antes de la deserialización).

Mira más de cerca: vector {t:Type} # [ t ] = Vector t - pero en ninguna parte ¡la definición en sí no dice que el primer número debe ser igual a la longitud del vector! Y no sigue de ninguna parte. Este es un hecho que debe tener en cuenta e implementar con sus manos. En otros lugares, la documentación incluso menciona honestamente que el tipo es falso:

El pseudotipo polimórfico Vector t es un “tipo” cuyo valor es una secuencia de valores de cualquier tipo t, ya sea en caja o desnuda.

… pero no se centra en ello. Cuando usted, cansado de vadear la extensión de las matemáticas (quizás incluso las conozca de un curso universitario), decide puntuar y ver cómo se trabaja realmente con ellas en la práctica, la impresión permanece en su cabeza: aquí Serious Mathematics se basa en , obviamente Cool People (dos matemáticos -ganadores del ACM), y no cualquiera. El objetivo, derrochar, se ha logrado.

Por cierto, sobre el número. Recordar # es un sinonimo nat, número natural:

Hay expresiones de tipo (typeexpr) y expresiones numéricas (expr-nat). Sin embargo, se definen de la misma manera.

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

pero en gramática se describen de la misma manera, es decir, esta diferencia nuevamente debe ser recordada y puesta en la implementación a mano.

Bueno, sí, tipos de plantillas (vector<int>, vector<User>) tienen un identificador común (#1cb5c415), es decir. si sabe que la llamada se declara como

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

entonces está esperando no solo un vector, sino un vector de usuarios. Más precisamente, debería espere: en código real, cada elemento, si no es un tipo simple, tendrá un constructor, y en el buen sentido en la implementación sería necesario verificar, y nos enviaron exactamente en cada elemento de este vector ese tipo? ¿Y si fuera algún tipo de PHP, en el que la matriz puede contener diferentes tipos en diferentes elementos?

En este punto, comienza a preguntarse: ¿se necesita tal TL? ¿Quizás para el carro sería posible usar el serializador humano, el mismo protobuf que ya existía entonces? Era teoría, veamos la práctica.

Implementaciones TL existentes en el código

TL nació en las entrañas de VKontakte incluso antes de los conocidos eventos con la venta de la participación de Durov y (seguramente), incluso antes del desarrollo de Telegram. Y en código abierto fuentes de la primera implementación Puedes encontrar muchas muletas divertidas. Y el lenguaje en sí se implementó allí de manera más completa que ahora en Telegram. Por ejemplo, los hashes no se utilizan en absoluto en el esquema (es decir, el pseudotipo incorporado (como un vector) con comportamiento desviado). O

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

pero consideremos, en aras de la exhaustividad, el cuadro, a fin de rastrear, por así decirlo, la evolución del Gigante del Pensamiento.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

O esta hermosa:

    static const char *reserved_words_polymorhic[] = {

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

      };

Este fragmento trata sobre plantillas, como:

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

Esta es la definición del tipo de plantilla hashmap, como un vector de pares int - Tipo. En C++ se vería así:

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

así que aquí alpha - palabra clave! Pero solo en C++ puedes escribir T, pero tienes que escribir alfa, beta... Pero no más de 8 parámetros, la fantasía terminó en theta. Entonces parece que una vez en San Petersburgo hubo aproximadamente los siguientes diálogos:

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

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

Pero se trataba de la primera implementación presentada de TL "en general". Pasemos a la consideración de las implementaciones en los clientes reales de Telegram.

Palabra de Basilio:

Vasily, [09.10.18/17/07 XNUMX:XNUMX] Sobre todo, el trasero está caliente por el hecho de que arruinaron un montón de abstracciones, y luego les clavaron un perno y le pusieron muletas al codificador.
Como resultado, primero desde los muelles el piloto.jpg
Luego desde el código jekichan.webp

Por supuesto, de personas familiarizadas con algoritmos y matemáticas, podemos esperar que hayan leído Aho, Ullman, y que estén familiarizados con las herramientas estándar de facto de la industria para escribir sus compiladores DSL durante décadas, ¿verdad?...

Por el autor telegrama-cli es Vitaliy Valtman, como se puede entender por la aparición del formato TLO fuera de sus límites (cli), un miembro del equipo; ahora se asigna la biblioteca para analizar TL por separadocual es la impresion de ella analizador TL? ..

16.12 04:18 Vasily: en mi opinión, alguien no ha dominado lex + yacc
16.12 04:18 Vasily: de lo contrario no puedo explicarlo
16.12 04:18 Vasily: bueno, o les pagaron por la cantidad de líneas en VK
16.12 04:19 Vasily: 3k+ líneas de otros<censored> en lugar de un analizador

¿Quizás una excepción? veamos como delatar este es el cliente OFICIAL — Telegram Desktop:

    nametype = re.match(r'([a-zA-Z.0-9_]+)(#[0-9a-f]+)?([^=]*)=s*([a-zA-Z.<>0-9_]+);', line);
    if (not nametype):
      if (not re.match(r'vector#1cb5c415 {t:Type} # [ t ] = Vector t;', line)):
         print('Bad line found: ' + line);

Más de 1100 líneas en Python, un par de expresiones regulares + casos especiales del tipo vectorial, que, por supuesto, se declara en el esquema como debería ser de acuerdo con la sintaxis TL, pero lo pusieron en esta sintaxis, lo analizan más ... La pregunta es, ¿por qué molestarse con todo este milagro?иmás bocanada, si nadie va a analizarlo de acuerdo con la documentación de todos modos?

Por cierto... ¿Recuerdas que hablamos del cheque CRC32? Entonces, en el generador de código de Telegram Desktop hay una lista de excepciones para aquellos tipos en los que el CRC32 calculado no coincide como se indica en el diagrama!

Vasily, [18.12 22:49] y aquí deberías pensar si se necesita tal TL
si quisiera meterme con implementaciones alternativas, comenzaría a insertar saltos de línea, la mitad de los analizadores se romperán en definiciones de varias líneas
tdesktop, sin embargo, también

Recuerde el punto sobre las frases ingeniosas, volveremos a él un poco más tarde.

De acuerdo, telegram-cli no es oficial, Telegram Desktop es oficial, pero ¿qué pasa con los demás? ¿Y quién sabe?... En el código del cliente de Android, no había ningún analizador de esquemas (lo que plantea preguntas sobre el código abierto, pero esto es para la segunda parte), pero había varias otras piezas de código divertidas, pero sobre ellas en la subsección a continuación.

¿Qué otras preguntas plantea la serialización en la práctica? Por ejemplo, la cagaron, por supuesto, con campos de bits y campos condicionales:

vasily: flags.0? true
significa que el campo está presente y es verdadero si la bandera está configurada

vasily: flags.1? int
significa que el campo está presente y necesita ser deserializado

Vasily: Culo, no te quemes, ¿qué estás haciendo?
Vasily: En algún lugar del documento se menciona que true es un tipo desnudo de longitud cero, pero no es realista recopilar algo de sus documentos.
Vasily: Tampoco existe tal cosa en las implementaciones abiertas, pero hay muchas muletas y accesorios.

¿Qué tal una Teletón? Mirando hacia el futuro sobre el tema de MTProto, un ejemplo: hay tales piezas en la documentación, pero el signo % solo se describe como "correspondiente al tipo desnudo dado", es decir en los ejemplos a continuación, ya sea un error o algo no documentado:

Vasily, [22.06.18/18/38 XNUMX:XNUMX] En un lugar:

msg_container#73f1f8dc messages:vector message = MessageContainer;

En un diferente:

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

Y estas son dos grandes diferencias, en la vida real viene una especie de vector desnudo

No he visto definiciones de vectores simples y no las he encontrado

Análisis escrito en teletón a mano

Su esquema comentó la definición. msg_container

Una vez más, la pregunta sigue siendo sobre%. No se describe.

Vadim Goncharov, [22.06.18/19/22 XNUMX:XNUMX] y en tdesktop?

Vasily, [22.06.18/19/23 XNUMX:XNUMX] Pero su analizador TL en los reguladores probablemente tampoco lo comerá

// parsed manually

TL es una hermosa abstracción, nadie la implementa por completo

Y no hay % en su versión del esquema.

Pero aquí la documentación se contradice, así que xs

Se encontró en la gramática, simplemente podrían olvidarse de describir la semántica.

Bueno, viste el muelle en TL, no puedes resolverlo sin medio litro.

“Bueno, digamos”, dirá otro lector, “lo criticas todo, así que muéstralo como se debe”.

Vasily responde: "en cuanto al analizador, necesito cosas como

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

de alguna manera más como que

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

o

        # Regex to match the whole line
        match = re.match(r'''
            ^                  # We want to match from the beginning to the end
            ([w.]+)           # The .tl object can contain alpha_name or namespace.alpha_name
            (?:
                #             # After the name, comes the ID of the object
                ([0-9a-f]+)    # The constructor ID is in hexadecimal form
            )?                 # If no constructor ID was given, CRC32 the 'tl' to determine it

            (?:s              # After that, we want to match its arguments (name:type)
                {?             # For handling the start of the '{X:Type}' case
                w+            # The argument name will always be an alpha-only name
                :              # Then comes the separator between name:type
                [wd<>#.?!]+  # The type is slightly more complex, since it's alphanumeric and it can
                               # also have Vector<type>, flags:# and flags.0?default, plus :!X as type
                }?             # For handling the end of the '{X:Type}' case
            )*                 # Match 0 or more arguments
            s                 # Leave a space between the arguments and the equal
            =
            s                 # Leave another space between the equal and the result
            ([wd<>#.?]+)     # The result can again be as complex as any argument type
            ;$                 # Finally, the line should always end with ;
            ''', tl, re.IGNORECASE | re.VERBOSE)

este es el lexer ENTERO:

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

aquellos. más simple es decirlo suavemente".

En general, al final, el analizador sintáctico y el generador de código para el subconjunto realmente utilizado de TL encajan en aproximadamente 100 líneas de gramática y ~ 300 líneas del generador (incluidas todas print's código generado), incluyendo tipo de golosinas, tipo de información para la introspección en cada clase. Cada tipo polimórfico se convierte en una clase base abstracta vacía, y los constructores heredan de él y tienen métodos de serialización y deserialización.

Falta de tipos en el lenguaje tipográfico

La escritura fuerte es buena, ¿verdad? No, esto no es un holivar (aunque prefiero los lenguajes dinámicos), sino un postulado dentro de TL. En base a ello, el lenguaje debería proporcionarnos todo tipo de comprobaciones. Bueno, está bien, no lo dejes, sino la implementación, pero al menos debería describirlos. ¿Y qué oportunidades queremos?

En primer lugar, las limitaciones. Aquí vemos en la documentación para subir archivos:

A continuación, el contenido binario del archivo se divide en partes. Todas las piezas deben tener el mismo tamaño ( tamaño_parte ) y se deben cumplir las siguientes condiciones:

  • part_size % 1024 = 0 (divisible por 1 KB)
  • 524288 % part_size = 0 (512 KB deben ser divisibles uniformemente por part_size)

La última parte no tiene que cumplir estas condiciones, siempre que su tamaño sea menor que part_size.

Cada parte debe tener un número de secuencia, parte_archivo, con un valor que va de 0 a 2,999.

Después de particionar el archivo, debe elegir un método para guardarlo en el servidor. usar subir.saveBigFilePart en caso de que el tamaño total del archivo sea superior a 10 MB y subir.saveFilePart para archivos más pequeños.
[…] se puede devolver uno de los siguientes errores de entrada de datos:

  • FILE_PARTS_INVALID - Número de piezas no válido. El valor no está entre 1..3000

¿Alguno de estos está presente en el esquema? ¿Es de alguna manera expresable por medio de TL? No. Pero disculpe, incluso el antiguo Turbo Pascal fue capaz de describir los tipos dados por rangos. Y podría hacer una cosa más, ahora más conocida como enum - un tipo que consiste en una enumeración de un número fijo (pequeño) de valores. En lenguajes como C - numérico, fíjate, hasta ahora solo hemos hablado de tipos. numeros. Pero también hay arreglos, cadenas... por ejemplo, sería bueno describir que esta cadena solo puede contener un número de teléfono, ¿no?

Nada de esto está en TL. Pero lo hay, por ejemplo, en JSON Schema. Y si alguien más puede objetar sobre la divisibilidad de 512 KB que aún debe verificarse en el código, entonces asegúrese de que el cliente simplemente no podría Enviar número fuera de rango 1..3000 (y no podria haber surgido el error correspondiente) seria posible no?..

Por cierto, sobre errores y valores de retorno. El ojo está borroso incluso para aquellos que han trabajado con TL: no nos dimos cuenta de inmediato de que cada uno una función en TL en realidad puede devolver no solo el tipo de retorno descrito, sino también un error. Pero esto no es deducible por medio de la propia TL. Por supuesto, es comprensible de todos modos y nafig no es necesario en la práctica (aunque, de hecho, RPC se puede hacer de diferentes maneras, volveremos a esto), pero ¿qué pasa con la Pureza de los conceptos de Matemáticas de Tipos Abstractos del cielo? mundo? .. Agarró el tirón - tan partido.

Y finalmente, ¿qué pasa con la legibilidad? Bueno, allí, en general, me gustaría descripción tenerlo bien en el esquema (nuevamente, está en el esquema JSON), pero si ya está tenso, ¿qué pasa con el lado práctico? ¿Al menos es trillado ver las diferencias durante las actualizaciones? Compruébelo usted mismo en ejemplos reales:

-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 alguien le gusta, pero GitHub, por ejemplo, se niega a resaltar los cambios dentro de líneas tan largas. El juego "encuentra 10 diferencias", y lo que el cerebro ve de inmediato es que los comienzos y los finales son los mismos en ambos ejemplos, debes leer tediosamente en algún lugar en el medio ... En mi opinión, esto no es solo en teoría, pero se ve puramente visual sucio y descuidado.

Por cierto, sobre la pureza de la teoría. ¿Por qué se necesitan campos de bits? ¿No les parece oler malo desde el punto de vista de la teoría de tipos? Se puede ver una explicación en versiones anteriores del esquema. Al principio sí, era así, se creaba un nuevo tipo por cada estornudo. Estos rudimentos todavía están allí en esta forma, por ejemplo:

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;

Pero ahora imagine que tiene 5 campos opcionales en su estructura, entonces necesita 32 tipos para todas las opciones posibles. explosión combinatoria. Así que la pureza cristalina de la teoría TL una vez más se estrelló contra el culo de hierro fundido de la dura realidad de la serialización.

Además, en algunos lugares, estos mismos tipos violan su propia tipificación. Por ejemplo, en MTProto (siguiente capítulo) la respuesta puede ser comprimida por Gzip, todo es sensato, excepto por la violación de capas y esquemas. Una vez, y no cosechó el RpcResult en sí, sino su contenido. Bueno, ¿por qué hacer esto?.. Tuve que cortar una muleta para que la compresión funcionara en cualquier lugar.

Otro ejemplo, una vez encontramos un error: enviado InputPeerUser en lugar de InputUser. O viceversa. ¡Pero funcionó! Es decir, al servidor no le importaba el tipo. ¿Cómo puede ser esto? La respuesta, tal vez, la pidan fragmentos de código de telegram-cli:

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

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

En otras palabras, aquí se realiza la serialización. MANUALMENTE, código no generado! ¿Quizás el servidor está implementado de manera similar? En principio, esto funcionará si se hace una vez, pero ¿cómo se puede respaldar con actualizaciones más adelante? ¿No es para eso para lo que era el plan? Y luego pasamos a la siguiente pregunta.

Versionado. Capas

Solo se puede adivinar por qué las versiones de esquema se denominan capas en función del historial de esquemas publicados. Aparentemente, al principio, a los autores les pareció que las cosas básicas se pueden hacer en un esquema sin cambios, y solo cuando sea necesario, indicar a solicitudes específicas que se están haciendo de acuerdo con una versión diferente. En principio, incluso una buena idea, y la nueva voluntad, por así decirlo, se "mezclará", se superpondrá a la anterior. Pero veamos cómo se hizo. Es cierto que no fue posible mirar desde el principio; es divertido, pero el esquema de la capa base simplemente no existe. Las capas comenzaron en 2. La documentación nos informa sobre una característica especial de TL:

Si un cliente admite la capa 2, se debe usar el siguiente constructor:

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

En la práctica, esto significa que antes de cada llamada a la API, un int con el valor 0x289dd1f6 debe agregarse antes del número de método.

Suena bien. Pero, ¿qué pasó después? Entonces vino

invokeWithLayer3#b7475268 query:!X = X;

Entonces, ¿qué sigue? Como es fácil de adivinar

invokeWithLayer4#dea0d430 query:!X = X;

¿Divertido? No, es muy temprano para reír, piensa en lo que cada una solicitud de otra capa debe envolverse en un tipo tan especial: si los tiene todos diferentes, ¿de qué otra manera distinguirlos? Y agregar solo 4 bytes al frente es un método bastante eficiente. Entonces

invokeWithLayer5#417a57ae query:!X = X;

Pero es obvio que después de un tiempo se convertirá en una bacanal. Y llegó la solución:

Actualización: a partir de la capa 9, métodos auxiliares invokeWithLayerN se puede usar junto con initConnection

¡Hurra! Después de 9 versiones, finalmente llegamos a lo que se hizo en los protocolos de Internet en los años 80: ¡negociación de versión una vez al comienzo de la conexión!

Entonces, ¿qué sigue?..

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

Y ahora puedes reír. Solo después de otras 9 capas, finalmente se agregó un constructor universal con un número de versión, que debe llamarse solo una vez al comienzo de la conexión, y el significado de las capas parece haber desaparecido, ahora es solo una versión condicional, como en todos lados. Problema resuelto.

¿Bien?..

Vasily, [16.07.18/14/01 XNUMX:XNUMX] El viernes pensé:
El teleservidor envía eventos sin una solicitud. Las solicitudes deben incluirse en InvokeWithLayer. El servidor no empaqueta las actualizaciones, no hay una estructura para empaquetar las respuestas y las actualizaciones.

Aquellos. el cliente no puede especificar la capa en la que quiere actualizaciones

Vadim Goncharov, [16.07.18/14/02 XNUMX:XNUMX] ¿No es InvokeWithLayer una muleta en principio?

Vasily, [16.07.18/14/02 XNUMX:XNUMX PM] Esta es la única manera

Vadim Goncharov, [16.07.18/14/02 XNUMX:XNUMX] que, en esencia, debería significar capas al comienzo de la sesión

Por cierto, de esto se deduce que no se proporciona una degradación del cliente

Actualizaciones, es decir tipo Updates en el esquema, esto es lo que el servidor envía al cliente no en respuesta a una solicitud de API, sino por sí solo cuando ocurre un evento. Este es un tema complejo que se tratará en otra publicación, pero por ahora es importante saber que el servidor acumula actualizaciones incluso cuando el cliente está desconectado.

Así, al negarse a envolver cada paquete para indicar su versión, de ahí que lógicamente surjan los siguientes posibles problemas:

  • el servidor envía actualizaciones al cliente antes de que el cliente haya dicho qué versión admite
  • ¿Qué se debe hacer después de actualizar el cliente?
  • que garantíasque la opinión del servidor sobre el número de capa no cambiará en el proceso?

¿Crees que esto es un pensamiento puramente teórico, y en la práctica esto no puede suceder, porque el servidor está escrito correctamente (en cualquier caso, está bien probado)? ¡Ja! ¡No importa cómo!

Esto es exactamente con lo que nos encontramos en agosto. El 14 de agosto, aparecieron mensajes de que algo se estaba actualizando en los servidores de Telegram... y luego en los registros:

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.

y luego unos pocos megabytes de seguimientos de pila (bueno, al mismo tiempo, se arregló el registro). Después de todo, si algo no se reconoció en su TL, es binario por firmas, más adelante en la secuencia TODO va, la decodificación será imposible. ¿Qué hacer en tal situación?

Pues bien, lo primero que se le ocurre a cualquiera es desconectarse e intentarlo de nuevo. No ayudó. Buscamos en Google CRC32: estos resultaron ser objetos del esquema 73, aunque trabajamos en el esquema 82. Miramos cuidadosamente los registros: ¡hay identificadores de dos esquemas diferentes!

¿Quizás el problema está puramente en nuestro cliente no oficial? No, ejecutamos Telegram Desktop 1.2.17 (la versión suministrada con varias distribuciones de Linux), escribe en el registro de excepciones: MTP Id. de tipo inesperado #b5223b0f leído en MTPMessageMedia...

Críticas al protocolo y enfoques organizativos de Telegram. Parte 1, técnica: experiencia de escribir un cliente desde cero - TL, MT

Google mostró que un problema similar ya le había ocurrido a uno de los clientes no oficiales, pero luego los números de versión y, en consecuencia, las suposiciones eran diferentes...

¿Entonces lo que hay que hacer? Vasily y yo nos separamos: él trató de actualizar el esquema a 91, decidí esperar unos días y probar a 73. Ambos métodos funcionaron, pero como son empíricos, no hay comprensión de cuántas versiones necesitas para saltar o hacia abajo, ni cuánto tiempo tienes que esperar.

Más tarde, logré reproducir la situación: iniciamos el cliente, lo apagamos, volvemos a compilar el esquema en otra capa, reiniciamos, volvemos a detectar el problema, volvemos al anterior: ¡vaya!, no cambiamos el esquema y reiniciamos el cliente durante varios unos minutos ayudarán. Recibirá una combinación de estructuras de datos de diferentes capas.

¿Explicación? Como puede adivinar por los diversos síntomas indirectos, el servidor consta de muchos tipos diferentes de procesos en diferentes máquinas. Lo más probable es que uno de los servidores que se encarga del “buffering” pusiera en cola lo que le daban los superiores, y lo daban en el esquema que estaba en el momento de la generación. Y hasta que esta cola no estuviera "podrida", no se podía hacer nada al respecto.

A menos que... ¡¿pero esto es una muleta terrible?!... No, antes de pensar en ideas locas, veamos el código de clientes oficiales. En la versión de Android, no encontramos ningún analizador TL, pero encontramos un archivo pesado (github se niega a colorearlo) con (des) serialización. Aquí están los fragmentos de código:

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... parece una locura. Pero, probablemente, este es un código generado, ¿de acuerdo?... ¡Pero ciertamente es compatible con todas las versiones! Es cierto que no está claro por qué todo está mezclado en un montón, y los chats secretos y todo tipo de _old7 de alguna manera no es similar a la generación de máquinas ... Sin embargo, sobre todo me volví loco de

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Chicos, ¿ni siquiera pueden decidir dentro de una capa? Bueno, está bien, "dos", digamos, se lanzaron con un error, bueno, sucede, pero ¿TRES? .. ¿Inmediatamente otra vez en el mismo rake? Que clase de pornografia es esta, perdon?..

Por cierto, algo similar sucede en las fuentes de Telegram Desktop; si es así, y varias confirmaciones seguidas en el esquema no cambian su número de capa, sino que arreglan algo. En condiciones en las que no hay una fuente de datos oficial para el esquema, ¿dónde puedo obtenerlos, excepto de las fuentes oficiales del cliente? Y a partir de ahí, no puede estar seguro de que el esquema sea completamente correcto hasta que pruebe todos los métodos.

¿Cómo se puede probar esto? Espero que los fanáticos de las pruebas unitarias, funcionales y de otro tipo compartan los comentarios.

Bien, veamos otro fragmento de código:

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;

Ese comentario "creado manualmente" aquí sugiere que solo una parte de este archivo está escrito a mano (¿puedes imaginar la pesadilla del mantenimiento?), y el resto es generado por máquina. Sin embargo, surge otra pregunta: que las fuentes estén disponibles no completamente (a la blobs bajo la GPL en el kernel de Linux), pero esto ya es tema para la segunda parte.

Pero suficiente Pasemos al protocolo sobre el cual persigue toda esta serialización.

Prototipo MT

Así que abramos descripción general и descripción detallada del protocolo y lo primero con lo que tropezamos es con la terminología. Y con abundancia de todo. En general, esto parece ser una marca registrada de Telegram: llamar cosas en diferentes lugares de diferentes maneras, o cosas diferentes en una palabra, o viceversa (por ejemplo, en una API de alto nivel si ve un paquete de pegatinas, esto no es lo que pensabas).

Por ejemplo, "mensaje" (mensaje) y "sesión" (sesión): aquí significan algo diferente que en la interfaz habitual del cliente de Telegram. Bueno, todo está claro con el mensaje, podría interpretarse en términos de OOP, o simplemente llamarse la palabra "paquete": este es un nivel de transporte bajo, no hay los mismos mensajes que en la interfaz, hay muchos de los de servicio. Pero la sesión... pero lo primero es lo primero.

Nivel de transporte

Lo primero es el transporte. Se nos informará sobre 5 opciones:

  • TCP
  • enchufe web
  • Websocket sobre HTTPS
  • HTTP
  • HTTPS

Vasily, [15.06.18/15/04 XNUMX:XNUMX] Y también hay transporte UDP, pero no está documentado

Y TCP en tres variantes

El primero es similar a UDP sobre TCP, cada paquete incluye un número de secuencia y un crc
¿Por qué es tan doloroso leer muelles en un carro?

bueno ahí ahora TCP ya en 4 variantes:

  • Reducido
  • Intermedio
  • intermedio acolchado
  • Full

Ok, intermedio acolchado para MTProxy, esto se agregó más tarde debido a eventos conocidos. Pero, ¿por qué dos versiones más (tres en total), cuando se podía hacer una? Los cuatro difieren esencialmente solo en cómo establecer la longitud y la carga útil del propio MTProto principal, que se analizará más adelante:

  • en abreviado es de 1 o 4 bytes pero no 0xef entonces cuerpo
  • en Intermedio esto es de 4 bytes de largo y un campo, y la primera vez el cliente debe enviar 0xeeeeeeee para indicar que es Intermedio
  • en Full, el más adictivo, desde el punto de vista de un networker: longitud, número de secuencia, y NO EL QUE es básicamente MTProto, cuerpo, CRC32. Sí, todo esto sobre TCP. Lo que nos proporciona un transporte fiable en forma de flujo de bytes en serie, no se necesitan secuencias, especialmente sumas de comprobación. Bien, ahora se me objetará que TCP tiene una suma de verificación de 16 bits, por lo que se producen daños en los datos. Genial, excepto que en realidad tenemos un protocolo criptográfico con hashes de más de 16 bytes, todos estos errores, e incluso más, se detectarán en una discrepancia SHA en un nivel superior. No hay ningún punto en CRC32 sobre esto.

Comparemos Abreviado, donde es posible un byte de longitud, con Intermedio, que justifica "En caso de que se necesite una alineación de datos de 4 bytes", lo cual es bastante absurdo. ¿Qué, se cree que los programadores de Telegram son tan torpes que no pueden leer datos del socket en un búfer alineado? Todavía tienes que hacer esto, porque la lectura puede devolverte cualquier número de bytes (y también hay servidores proxy, por ejemplo...). O, por otro lado, ¿por qué molestarse con Abridged si todavía tenemos grandes rellenos de 16 bytes en la parte superior? Ahorre 3 bytes a veces ?

Da la impresión de que a Nikolai Durov le gusta mucho inventar bicicletas, incluidos los protocolos de red, sin una necesidad práctica real.

Otras opciones de transporte, incl. Web y MTProxy, no consideraremos ahora, tal vez en otra publicación, si hay una solicitud. Solo recordaremos ahora sobre este mismo MTProxy que poco después de su lanzamiento en 2018, los proveedores aprendieron rápidamente a bloquearlo exactamente, destinado a desvío de bloquePor tamaño del paquete! Y también el hecho de que el servidor MTProxy escrito (nuevamente por Waltman) en C estaba innecesariamente vinculado a las especificaciones de Linux, aunque no era necesario en absoluto (Phil Kulin lo confirmará), y que un servidor similar en Go o en Node.js caben menos de cien líneas.

Pero sacaremos conclusiones sobre la alfabetización técnica de estas personas al final de la sección, después de considerar otras cuestiones. Por ahora, pasemos a la quinta capa OSI, sesión, en la que colocaron la sesión MTProto.

Claves, mensajes, sesiones, Diffie-Hellman

No lo pusieron allí del todo correctamente... Una sesión no es la misma sesión que está visible en la interfaz en Sesiones activas. Pero en orden.

Críticas al protocolo y enfoques organizativos de Telegram. Parte 1, técnica: experiencia de escribir un cliente desde cero - TL, MT

Aquí hemos recibido una cadena de bytes de longitud conocida de la capa de transporte. Este es un mensaje encriptado o texto sin formato, si todavía estamos en la etapa de negociación clave y realmente lo estamos haciendo. ¿De cuál del montón de conceptos llamados "clave" estamos hablando? Aclaremos este tema para el propio equipo de Telegram (me disculpo por traducir mi propia documentación del inglés a un cerebro cansado a las 4 de la mañana, era más fácil dejar algunas frases como están):

Hay dos entidades llamadas Sesión - uno en la interfaz de usuario de los clientes oficiales en "sesiones actuales", donde cada sesión corresponde a un dispositivo/SO completo.
El segundo - Sesión MTProto, que tiene un número de secuencia de mensaje (en un sentido de bajo nivel) y que puede durar entre diferentes conexiones TCP. Se pueden configurar varias sesiones de MTProto al mismo tiempo, por ejemplo, para acelerar la descarga de archivos.

entre estos dos sesiones es el concepto autorización. En el caso degenerado, se puede decir que sesión de interfaz de usuario es lo mismo que autorizaciónPero, por desgracia, es complicado. Miramos:

  • El usuario en el nuevo dispositivo primero genera Clave de autenticación y lo vincula a la cuenta, por ejemplo, por SMS, por eso autorización
  • Ocurrió dentro de la primera Sesión MTProto, que tiene session_id dentro de ti mismo
  • En este paso, la combinación autorización и session_id podría ser nombrado ejemplo - esta palabra se encuentra en la documentación y el código de algunos clientes
  • Luego, el cliente puede abrir varios Sesiones de MTProto bajo el mismo Clave de autenticación - al mismo DC.
  • Entonces, un día, el cliente necesita solicitar un archivo de otra corriente continua - y para este DC se generará uno nuevo Clave de autenticación !
  • Para decirle al sistema que no se trata de un nuevo usuario registrándose, sino del mismo autorización (sesión de interfaz de usuario), el cliente usa llamadas API auth.exportAuthorization en casa DC auth.importAuthorization en el nuevo CC.
  • De todos modos, puede haber varios abiertos Sesiones de MTProto (cada uno con lo suyo) session_id) a este nuevo DC, bajo su Clave de autenticación.
  • Finalmente, el cliente puede querer Perfect Forward Secrecy. Cada Clave de autenticación era permanente clave - por DC - y el cliente puede llamar auth.bindTempAuthKey para usar temporal Clave de autenticación - y de nuevo, solo uno temp_auth_key por DC, común a todos Sesiones de MTProto a este CC.

Tenga en cuenta que sal (y futuras sales) también uno en Clave de autenticación aquellos. compartido entre todos Sesiones de MTProto al mismo CC.

¿Qué significa "entre diferentes conexiones TCP"? significa que esto algo como cookie de autorización en un sitio web: persiste (sobrevive) a muchas conexiones TCP a este servidor, pero un día fallará. Solo que a diferencia de HTTP, en MTProto, dentro de la sesión, los mensajes se numeran secuencialmente y se confirman, ingresaron al túnel, la conexión se interrumpió: después de establecer una nueva conexión, el servidor amablemente enviará todo lo que no entregó en esta sesión. conexión TCP anterior.

Sin embargo, la información anterior es un apretón después de muchos meses de litigio. Mientras tanto, ¿estamos implementando nuestro cliente desde cero? - Volvamos al principio.

Entonces generamos auth_key en versiones de Diffie-Hellman de Telegram. Tratemos de entender la documentación...

Vasily, [19.06.18/20/05 1:255] data_with_hash := SHAXNUMX(data) + data + (cualquier byte aleatorio); tal que la longitud sea igual a XNUMX bytes;
datos_cifrados:= RSA(datos_con_hash, servidor_clave_pública); un número largo de 255 bytes (big endian) se eleva a la potencia requerida sobre el módulo requerido y el resultado se almacena como un número de 256 bytes.

Tienen algo de droga DH

No parece el DH de una persona sana.
No hay dos claves públicas en dx

Bueno, al final, lo descubrimos, pero el sedimento permaneció: el cliente realiza una prueba de trabajo de que pudo factorizar el número. Tipo de protección contra ataques DoS. Y la clave RSA solo se usa una vez en una dirección, esencialmente para el cifrado new_nonce. Pero mientras esta operación aparentemente simple tiene éxito, ¿a qué tendrá que enfrentarse?

Vasily, [20.06.18/00/26 XNUMX:XNUMX] Todavía no he llegado a la solicitud de aplicación

Envié una solicitud a DH

Y, en el muelle del transporte está escrito que puede responder con 4 bytes del código de error. Y eso es

Bueno, me dijo -404, ¿y qué?

Aquí estoy para él: “atrapa tu efigna encriptada con la clave del servidor con una huella de tal y tal, quiero DH”, y responde estupidamente 404

¿Qué pensaría usted de tal respuesta del servidor? ¿Qué hacer? No hay nadie a quien preguntar (pero más sobre eso en la segunda parte).

Aquí todo el interés en el muelle es hacer

No tengo nada más que hacer, solo soñé con convertir números de un lado a otro

Dos números de 32 bits. Los empaqué como todos los demás

Pero no, son estos dos los que necesitas primero en una línea como BE

Vadim Goncharov, [20.06.18/15/49 404:XNUMX] y debido a este XNUMX?

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

Vadim Goncharov, [20.06.18/15/50 XNUMX:XNUMX p. m.] así que no entiendo qué puede "no encontrar"

Vasili, [20.06.18 15:50] acerca de

No encontré tal descomposición en divisores simples%)

Ni siquiera se dominó el informe de errores

Vasily, [20.06.18/20/18 5:XNUMX p. m.] Oh, también hay MDXNUMX. Ya tres hashes diferentes

La huella dactilar de la llave se calcula de la siguiente manera:

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

SHA1 y sha2

Así que pongamos auth_key 2048 bits de tamaño que obtuvimos según Diffie-Hellman. ¿Que sigue? Luego descubrimos que los 1024 bits inferiores de esta clave no se usan de ninguna manera... pero pensemos en esto por ahora. En este paso, tenemos un secreto compartido con el servidor. Se ha establecido un análogo de una sesión TLS, un procedimiento muy costoso. ¡Pero el servidor aún no sabe nada sobre quiénes somos! Todavía no, en realidad autorización. Aquellos. si pensó en términos de "contraseña de inicio de sesión", como solía ser en ICQ, o al menos "clave de inicio de sesión", como en SSH (por ejemplo, en algunos gitlab / github). Tenemos anónimo. ¿Y si el servidor nos responde "estos números de teléfono los atiende otro DC"? ¿O incluso "tu número de teléfono está prohibido"? Lo mejor que podemos hacer es guardar la clave con la esperanza de que todavía sea útil y no esté podrida para entonces.

Por cierto, lo "recibimos" con reservas. Por ejemplo, ¿confiamos en el servidor? ¿Es falso? Necesitamos comprobaciones criptográficas:

Vasily, [21.06.18/17/53 2:XNUMX p. m.] Ofrecen a los clientes móviles verificar un número de XNUMXkbit por simplicidad%)

Pero no está nada claro, nafeijoa

Vasily, [21.06.18/18/02 XNUMX:XNUMX] El muelle no dice qué hacer si resultó no ser simple

No dicho. ¿Veamos qué hace el cliente oficial para Android en este caso? A eso es lo que (y sí, todo el archivo es interesante allí) - como dicen, lo dejaré aquí:

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

No, por supuesto que hay algunos hay controles para la simplicidad de un número, pero personalmente ya no tengo suficientes conocimientos en matemáticas.

Vale, tenemos la llave maestra. Para iniciar sesión, es decir enviar solicitudes, es necesario realizar un cifrado adicional, ya que utiliza AES.

La clave del mensaje se define como los 128 bits intermedios del SHA256 del cuerpo del mensaje (incluida la sesión, el ID del mensaje, etc.), incluidos los bytes de relleno, precedidos por 32 bytes tomados de la clave de autorización.

Vasily, [22.06.18/14/08 XNUMX:XNUMX] Perras promedio

Recibido auth_key. Todo. Además de ellos ... no está claro desde los muelles. Siéntase libre de estudiar el código fuente abierto.

Tenga en cuenta que MTProto 2.0 requiere de 12 a 1024 bytes de relleno, aún sujeto a la condición de que la longitud del mensaje resultante sea divisible por 16 bytes.

Entonces, ¿cuánto relleno poner?

Y sí, aquí también, 404 en caso de error

Si alguien estudió cuidadosamente el diagrama y el texto de la documentación, notó que no hay MAC allí. Y ese AES se usa en algún modo IGE que no se usa en ningún otro lado. Ellos, por supuesto, escriben sobre esto en sus preguntas frecuentes... Aquí, como la clave del mensaje en sí misma, es al mismo tiempo el hash SHA de los datos descifrados utilizados para verificar la integridad, y en caso de discrepancia, la documentación para alguna razón recomienda ignorarlos en silencio (pero ¿qué pasa con la seguridad, nos rompen de repente?).

No soy criptógrafo, tal vez en este modo en este caso no haya nada de malo desde un punto de vista teórico. Pero definitivamente puedo nombrar un problema práctico, usando el ejemplo de Telegram Desktop. Cifra el caché local (todos estos D877F783D5D3EF8C) de la misma manera que los mensajes en MTProto (solo en este caso, la versión 1.0), es decir primero la clave del mensaje, luego los datos en sí (y en algún lugar aparte de la gran auth_key 256 bytes, sin los cuales msg_key inútil). Entonces, el problema se vuelve notable en archivos grandes. Es decir, debe conservar dos copias de los datos: cifrada y descifrada. ¿Y si hay megas, o video streaming, por ejemplo?.. Los esquemas clásicos con MAC tras el texto cifrado permiten leerlo streaming, transfiriéndolo inmediatamente. Y con MTProto tienes que al principio cifre o descifre todo el mensaje, solo luego transfiéralo a la red o al disco. Por lo tanto, en las últimas versiones de Telegram Desktop en el caché en user_data ya se usa otro formato, con AES en modo CTR.

Vasily, [21.06.18/01/27 20:XNUMX a. m.] Oh, descubrí qué es IGE: IGE fue el primer intento de un "modo de encriptación de autenticación", originalmente para Kerberos. Fue un intento fallido (no proporciona protección de integridad) y tuvo que eliminarse. Ese fue el comienzo de una búsqueda de XNUMX años para un modo de encriptación de autenticación que funcione, que recientemente culminó en modos como OCB y GCM.

Y ahora los argumentos desde el lado del carro:

El equipo detrás de Telegram, dirigido por Nikolai Durov, consta de seis campeones de ACM, la mitad de ellos doctorados en matemáticas. Les tomó alrededor de dos años implementar la versión actual de MTProto.

que gracioso Dos años al nivel inferior

O simplemente podríamos tomar tls

Bien, digamos que hemos hecho encriptación y otros matices. ¿Podemos finalmente enviar solicitudes serializadas en TL y deserializar las respuestas? Entonces, ¿qué se debe enviar y cómo? Aquí está el método initConexióntal vez esto es todo?

Vasily, [25.06.18/18/46 XNUMX:XNUMX p. m.] Inicializa la conexión y guarda información en el dispositivo y la aplicación del usuario.

Acepta app_id, device_model, system_version, app_version y lang_code.

y alguna consulta

Documentación como siempre. Siéntase libre de estudiar el código abierto

Si todo estaba más o menos claro con invoqueWithLayer, entonces, ¿qué es? Resulta que supongamos que tenemos, el cliente ya tenía algo que preguntarle al servidor, hay una solicitud que queríamos enviar:

Vasily, [25.06.18/19/13 XNUMX:XNUMX] A juzgar por el código, la primera llamada está envuelta en esta basura, y la basura en sí está en invocar con capa

¿Por qué initConnection no podría ser una llamada separada, pero debe ser un contenedor? Sí, resultó que debe hacerse cada vez al comienzo de cada sesión, y no una sola vez, como con la clave principal. ¡Pero! ¡No puede ser llamado por un usuario no autorizado! Aquí hemos llegado a la etapa en que es aplicable este página de documentación - y nos dice que...

Solo una pequeña parte de los métodos de la API están disponibles para usuarios no autorizados:

  • autenticación.enviarCode
  • auth.resendCode
  • cuenta.getPassword
  • auth.checkContraseña
  • auth.checkTeléfono
  • autenticación.registrarse
  • autenticación.iniciar sesión
  • auth.importAutorización
  • ayuda.getConfig
  • ayuda.getNearestDc
  • ayuda.getAppUpdate
  • ayuda.getCdnConfig
  • langpack.getLangPack
  • langpack.getStrings
  • langpack.getDiferencia
  • langpack.getIdiomas
  • langpack.getIdioma

El primero de ellos auth.sendCode, y está esa preciada primera petición en la que enviaremos api_id y api_hash, y tras la cual recibimos un SMS con un código. Y si llegamos al DC equivocado (los números de teléfono de este país los atiende otro, por ejemplo), entonces recibiremos un error con el número del DC deseado. Para saber a qué dirección IP necesitamos conectarnos por el número de DC, nos ayudará help.getConfig. Una vez hubo solo 5 entradas, pero después de los conocidos eventos de 2018, el número ha aumentado significativamente.

Ahora recordemos que llegamos en esta etapa al servidor anónimo. ¿No es demasiado caro simplemente obtener una dirección IP? ¿Por qué no hacer esta y otras operaciones en la parte no cifrada de MTProto? Escucho una objeción: "¿cómo puede asegurarse de que no sea el RKN el que responderá con direcciones falsas?". A esto recordamos que, de hecho, en clientes oficiales claves RSA incrustadas, es decir. tu puedes sólo firmar esta informacion. En realidad, esto ya se hace para obtener información sobre cómo eludir bloqueos que los clientes reciben a través de otros canales (es lógico que esto no se pueda hacer en el propio MTProto, porque aún necesita saber dónde conectarse).

DE ACUERDO. En esta etapa de autorización del cliente, aún no estamos autorizados y no hemos registrado nuestra aplicación. Solo queremos ver por ahora qué responde el servidor a los métodos disponibles para un usuario no autorizado. Y aquí…

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

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

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

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

En el esquema, el primero, el segundo viene

En el esquema tdesktop, el tercer valor es

Sí, desde entonces, por supuesto, la documentación se ha actualizado. Aunque pronto puede volver a ser irrelevante. ¿Y cómo debería saberlo un desarrollador novato? ¿Quizás si registras tu aplicación, te informarán? Vasily hizo esto, pero, por desgracia, no se le envió nada (nuevamente, hablaremos de esto en la segunda parte).

... Te diste cuenta de que ya nos hemos movido de alguna manera a la API, es decir al siguiente nivel y te perdiste algo en el tema MTProto? Nada sorprendente:

Vasily, [28.06.18/02/04 2:XNUMX a.m.] Mm, están revisando algunos de los algoritmos en eXNUMXe

Mtproto define algoritmos de encriptación y claves para ambos dominios, así como una especie de estructura contenedora

Pero constantemente mezclan diferentes niveles de pila, por lo que no siempre está claro dónde terminó mtproto y comenzó el siguiente nivel.

¿Cómo se mezclan? Bueno, aquí está la misma clave temporal para PFS, por ejemplo (por cierto, Telegram Desktop no sabe cómo hacerlo). Se ejecuta mediante una solicitud de API. auth.bindTempAuthKey, es decir. desde el nivel superior. Pero al mismo tiempo, interfiere con el cifrado en el nivel inferior; después, por ejemplo, debe volver a hacerlo. initConnection etc., esto no es sólo petición normal. Por separado, también ofrece que solo puede tener UNA clave temporal en el DC, aunque el campo auth_key_id en cada mensaje le permite cambiar la clave al menos en cada mensaje, y que el servidor tiene derecho a "olvidar" la clave temporal en cualquier momento; qué hacer en este caso, la documentación no dice ... bueno, por qué no seria posible tener varias claves, como con un conjunto de sales del futuro, pero?..

Hay algunas otras cosas que vale la pena señalar en el tema MTProto.

Mensajes de mensaje, msg_id, msg_seqno, reconocimientos, pings en la dirección incorrecta y otras idiosincrasias

¿Por qué necesita saber acerca de ellos? Porque "filtran" un nivel más alto, y usted necesita conocerlos cuando trabaja con la API. Supongamos que no estamos interesados ​​en msg_key, el nivel inferior descifra todo por nosotros. Pero dentro de los datos descifrados, tenemos los siguientes campos (también la longitud de los datos para saber dónde está el relleno, pero esto no es importante):

  • sal-int64
  • sesión_id - int64
  • id_mensaje - int64
  • seq_no-int32

Recordemos que la sal es una para todo el DC. ¿Por qué saber de ella? No solo porque hay una solicitud get_future_salts, que indica qué intervalos serán válidos, pero también porque si su sal está "podrida", el mensaje (solicitud) simplemente se perderá. El servidor, por supuesto, informará la nueva sal emitiendo new_session_created - pero con el anterior tendrás que reenviar de alguna manera, por ejemplo. Y esta pregunta afecta a la arquitectura de la aplicación.

El servidor puede descartar sesiones por completo y responder de esta manera por muchas razones. En realidad, ¿qué es una sesión de MTProto desde el lado del cliente? estos son dos numeros session_id и seq_no mensajes dentro de esta sesión. Bueno, y la conexión TCP subyacente, por supuesto. Digamos que nuestro cliente todavía no sabe hacer muchas cosas, desconectado, reconectado. Si esto sucedió rápidamente: la sesión anterior continuó en la nueva conexión TCP, aumente seq_no más. Si tarda mucho, el servidor podría borrarlo, porque de su lado también es una cola, como nos enteramos.

Que deberia ser seq_no? Oh, esa es una pregunta difícil. Trate de entender honestamente lo que se quiere decir:

Mensaje relacionado con el contenido

Un mensaje que requiere un acuse de recibo explícito. Estos incluyen todos los mensajes de usuario y muchos de servicio, prácticamente todos con la excepción de contenedores y reconocimientos.

Número de secuencia del mensaje (msg_seqno)

Un número de 32 bits equivalente al doble del número de mensajes "relacionados con el contenido" (aquellos que requieren acuse de recibo y, en particular, aquellos que no son contenedores) creados por el remitente antes de este mensaje y posteriormente incrementado en uno si el mensaje actual es un mensaje relacionado con el contenido. Un contenedor siempre se genera después de todo su contenido; por lo tanto, su número de secuencia es mayor o igual a los números de secuencia de los mensajes contenidos en él.

¿Qué tipo de circo es este con un incremento de 1 y luego otro 2?... Sospecho que el significado original era "bit bajo para ACK, el resto es un número", pero el resultado no es del todo correcto, en particular, resulta que se puede enviar varios confirmaciones que tienen el mismo seq_no! ¿Cómo? Bueno, por ejemplo, el servidor nos envía algo, envía y nosotros mismos callamos, solo respondemos con mensajes de confirmación del servicio sobre la recepción de sus mensajes. En este caso, nuestras confirmaciones de salida tendrán el mismo número de salida. Si está familiarizado con TCP y pensó que esto suena un poco loco, pero parece no ser muy descabellado, porque en TCP seq_no no cambia, y la confirmación va a seq_no el otro lado - entonces me apresuro a molestar. Llegan las confirmaciones a MTProto NO en seq_no, como en TCP, pero msg_id !

Que es esto msg_id, el más importante de estos campos? El ID único del mensaje, como sugiere el nombre. Se define como un número de 64 bits, los bits menos significativos de los cuales nuevamente tienen magia de servidor-no-servidor, y el resto es una marca de tiempo de Unix, incluida la parte fraccionaria, desplazada 32 bits a la izquierda. Aquellos. marca de tiempo per se (y los mensajes con tiempos demasiado diferentes serán rechazados por el servidor). De esto resulta que, en general, este es un identificador que es global para el cliente. Mientras - recuerda session_id - estamos garantizados: Bajo ninguna circunstancia se puede enviar un mensaje destinado a una sesión a una sesión diferente. Es decir, resulta que ya hay tres nivel: sesión, número de sesión, ID de mensaje. Por qué tanta complicación, este misterio es muy grande.

Por lo tanto, msg_id necesitado para…

RPC: solicitudes, respuestas, errores. Confirmaciones.

Como habrás notado, no hay ningún tipo o función especial "hacer una solicitud RPC" en ninguna parte del esquema, aunque hay respuestas. Después de todo, ¡tenemos mensajes relacionados con el contenido! Eso es, cualquier ¡El mensaje puede ser una solicitud! O no ser. Después de todo, cada есть msg_id. Y aquí están las respuestas:

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

Aquí es donde se indica a qué mensaje se trata de una respuesta. Por lo tanto, en el nivel superior de la API, deberá recordar qué número tenía su solicitud; creo que no es necesario explicar que el trabajo es asíncrono y que puede haber varias solicitudes al mismo tiempo, cuyas respuestas se puede devolver en cualquier orden? En principio, a partir de esto y de los mensajes de error como ningún trabajador, se puede rastrear la arquitectura detrás de esto: el servidor que mantiene una conexión TCP con usted es un equilibrador de front-end, dirige las solicitudes a los backends y las recopila. message_id. Todo parece ser claro, lógico y bueno aquí.

¿Sí?.. ¿Y si lo piensas? Después de todo, la respuesta RPC en sí también tiene un campo msg_id! ¿Necesitamos gritarle al servidor “¡no estás respondiendo a mi respuesta!”? Y sí, ¿qué había acerca de la confirmación? Acerca de la página mensajes sobre mensajes nos dice que es

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

y cada lado debe hacerlo. ¡Pero no siempre! Si recibe un RpcResult, sirve como reconocimiento en sí mismo. Es decir, el servidor puede responder a su solicitud con MsgsAck, como "Lo recibí". Puede responder inmediatamente a RpcResult. Podría ser ambos.

Y sí, ¡todavía tienes que responder la respuesta! Confirmación. De lo contrario, el servidor lo considerará no entregado y se lo devolverá. Incluso después de la reconexión. Pero aquí, por supuesto, surgirá la cuestión de los tiempos muertos. Veámoslos un poco más tarde.

Mientras tanto, consideremos posibles errores en la ejecución de consultas.

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

Oh, alguien exclamará, aquí hay un formato más humano: ¡hay una línea! Tome su tiempo. Aquí lista de errorespero ciertamente no completa. De él aprendemos que el código es − algo como Errores de HTTP (bueno, claro, no se respeta la semántica de las respuestas, en algunos lugares se distribuyen por códigos al azar), y la cadena queda así LETRAS_MAYUSCULAS_Y_NUMEROS. Por ejemplo, PHONE_NUMBER_OCCUPIED o FILE_PART_X_MISSING. Bueno, eso es, todavía tienes que esta línea analizar gramaticalmente. Por ejemplo, FLOOD_WAIT_3600 significará que tendrá que esperar una hora, y PHONE_MIGRATE_5que el número de teléfono con este prefijo debe estar registrado en el 5to DC. Tenemos un tipo de lenguaje, ¿verdad? No necesitamos un argumento de la cadena, las expresiones regulares servirán, cho.

Nuevamente, esto no está en la página de mensajes de servicio, pero, como ya es habitual con este proyecto, se puede encontrar información en otra página de documentación. O despertar sospechas. Primero, mira, violación de escritura/capas - RpcError se puede invertir en RpcResult. ¿Por qué no afuera? ¿Qué no hemos tenido en cuenta?... Entonces, ¿dónde está la garantía de que RpcError no se puede invertir en RpcResult, pero estar directamente o anidado en otro tipo? carece req_msg_id ? ..

Pero sigamos con los mensajes de servicio. El cliente puede considerar que el servidor está pensando durante mucho tiempo y hacer una solicitud tan maravillosa:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

Hay tres posibles respuestas a la misma, cruzando nuevamente con el mecanismo de confirmación, para tratar de entender cuáles deberían ser (y cuál es la lista de tipos que no requieren confirmación en general), se deja como tarea al lector (nota: el la información en las fuentes de Telegram Desktop no está completa).

Adicción: estados de publicación de mensajes

En general, muchos lugares en TL, MTProto y Telegram en general dejan una sensación de terquedad, pero por cortesía, tacto y demás. habilidades blandas cortésmente guardamos silencio al respecto, y las obscenidades en los diálogos fueron censuradas. Sin embargo, este lugarОla mayor parte de la página sobre mensajes sobre mensajes causa conmoción incluso para mí, que he estado trabajando con protocolos de red durante mucho tiempo y he visto bicicletas de diversos grados de curvatura.

Comienza inofensivamente, con confirmaciones. A continuación, se nos habla de

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;

Bueno, todo el que empiece a trabajar con MTProto tendrá que enfrentarse a ellos, en el ciclo de “corregido - recompilado - lanzado”, es común tener errores numéricos o sal que se ha podrido durante las ediciones. Sin embargo, hay dos puntos aquí:

  1. De ello se deduce que el mensaje original se pierde. Necesitamos cercar algunas colas, lo consideraremos más adelante.
  2. ¿Qué son esos extraños números de error? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64... ¿dónde están los demás números, Tommy?

La documentación dice:

La intención es que los valores de error_code se agrupen (error_code >> 4): por ejemplo, los códigos 0x40 - 0x4f corresponden a errores en la descomposición del contenedor.

pero, en primer lugar, un cambio en la otra dirección, y en segundo lugar, ¿no importa dónde están el resto de los códigos? ¿En la cabeza del autor?.. Sin embargo, estas son bagatelas.

La adicción comienza en los mensajes de estado de las publicaciones y en las copias de las publicaciones:

  • Solicitud de información sobre el estado del mensaje
    Si alguna de las partes no ha recibido información sobre el estado de sus mensajes salientes durante un tiempo, puede solicitarlo explícitamente a la otra parte:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Mensaje informativo sobre el estado de los mensajes
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Aquí, info es una cadena que contiene exactamente un byte de estado de mensaje para cada mensaje de la lista de msg_ids entrantes:

    • 1 = no se sabe nada sobre el mensaje (msg_id demasiado bajo, la otra parte puede haberlo olvidado)
    • 2 = mensaje no recibido (msg_id se encuentra dentro del rango de identificadores almacenados; sin embargo, la otra parte ciertamente no ha recibido un mensaje como ese)
    • 3 = mensaje no recibido (msg_id demasiado alto; sin embargo, la otra parte ciertamente no lo ha recibido todavía)
    • 4 = mensaje recibido (tenga en cuenta que esta respuesta también es al mismo tiempo un acuse de recibo)
    • +8 = mensaje ya reconocido
    • +16 = mensaje que no requiere acuse de recibo
    • +32 = consulta RPC contenida en el mensaje que se está procesando o el procesamiento ya se completó
    • +64 = respuesta relacionada con el contenido del mensaje ya generado
    • +128 = la otra parte sabe con certeza que el mensaje ya se recibió
      Esta respuesta no requiere reconocimiento. Es un reconocimiento de msgs_state_req relevante, en sí mismo.
      Tenga en cuenta que si de repente resulta que la otra parte no tiene un mensaje que parezca que se le ha enviado, simplemente se puede volver a enviar el mensaje. Incluso si la otra parte debe recibir dos copias del mensaje al mismo tiempo, se ignorará el duplicado. (Si ha pasado demasiado tiempo y el msg_id original ya no es válido, el mensaje debe envolverse en msg_copy).
  • Comunicación Voluntaria de Estado de Mensajes
    Cualquiera de las partes puede informar voluntariamente a la otra parte sobre el estado de los mensajes transmitidos por la otra parte.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Comunicación voluntaria ampliada del estado de un mensaje
    ...
    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;
  • Solicitud explícita para reenviar mensajes
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    La parte remota responde inmediatamente reenviando los mensajes solicitados […]
  • Solicitud explícita para reenviar respuestas
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    La parte remota responde inmediatamente reenviando respuestas a los mensajes solicitados […]
  • Copias de mensajes
    En algunas situaciones, es necesario volver a enviar un mensaje antiguo con un msg_id que ya no es válido. Luego, se envuelve en un contenedor de copia:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Una vez recibido, el mensaje se procesa como si el envoltorio no estuviera allí. Sin embargo, si se sabe con certeza que se recibió el mensaje orig_message.msg_id, entonces el nuevo mensaje no se procesa (mientras que, al mismo tiempo, se confirman este y orig_message.msg_id). El valor de orig_message.msg_id debe ser inferior al msg_id del contenedor.

Guardemos silencio incluso sobre el hecho de que en msgs_state_info nuevamente, las orejas de la TL sin terminar sobresalen (necesitábamos un vector de bytes, y en los dos bits inferiores de la enumeración, y en los indicadores de bits más antiguos). El punto es otra cosa. ¿Alguien entiende por qué todo esto es en la práctica? en cliente real necesario?.. Con dificultad, pero puede imaginar algún beneficio si una persona se dedica a la depuración y en un modo interactivo: pregúntele al servidor qué y cómo. Pero las solicitudes se describen aquí. en ambas direcciones.

De esto se deduce que cada parte no solo debe cifrar y enviar mensajes, sino también almacenar datos sobre ellos, sobre las respuestas a ellos y durante un período de tiempo desconocido. La documentación no describe los tiempos ni la aplicabilidad práctica de estas características. de ninguna manera. ¡Lo más sorprendente es que en realidad se usan en el código de los clientes oficiales! Aparentemente, les dijeron algo que no estaba incluido en la documentación abierta. Entender del código por qué, ya no es tan simple como en el caso de TL: esta no es una parte lógicamente aislada (comparativamente), sino una pieza vinculada a la arquitectura de la aplicación, es decir, requerirá mucho más tiempo para comprender el código de la aplicación.

Pings y tiempos. Colas.

De todo, si recuerda las conjeturas sobre la arquitectura del servidor (distribución de solicitudes entre backends), sigue algo bastante aburrido, a pesar de todas las garantías de entrega que en TCP (o los datos han sido entregados, o se le informará sobre el romper, pero los datos se entregarán hasta el momento del problema), que confirma en el propio MTProto - sin garantías. El servidor puede perder o tirar fácilmente su mensaje, y no se puede hacer nada al respecto, solo para cercar muletas de varios tipos.

Y antes que nada, colas de mensajes. Bueno, por un lado, todo fue obvio desde el principio: un mensaje no confirmado debe almacenarse y reenviarse. ¿Y después de qué hora? Y el bufón lo conoce. Tal vez esos mensajes de servicio de adictos solucionen de alguna manera este problema con muletas, digamos, en Telegram Desktop hay alrededor de 4 colas correspondientes a ellos (tal vez más, como ya se mencionó, para esto es necesario profundizar más en su código y arquitectura; al mismo tiempo tiempo, sabemos que no se puede tomar como muestra, en él no se utilizan cierto número de tipos del esquema MTProto).

¿Por qué está pasando esto? Probablemente, los programadores del servidor no pudieron garantizar la confiabilidad dentro del clúster, o al menos incluso el almacenamiento en búfer en el balanceador frontal, y trasladaron este problema al cliente. Desesperado, Vasily intentó implementar una opción alternativa, con solo dos colas, utilizando algoritmos de TCP: midiendo el RTT al servidor y ajustando el tamaño de la "ventana" (en mensajes) según la cantidad de solicitudes no reconocidas. Es decir, una heurística tan aproximada para estimar la carga del servidor: cuántas de nuestras solicitudes puede procesar al mismo tiempo y no perder.

Bueno, eso es, lo entiendes, ¿verdad? Si tiene que implementar TCP nuevamente sobre un protocolo que funciona sobre TCP, esto indica un protocolo muy mal diseñado.

Oh, sí, ¿por qué se necesita más de una cola y, en general, qué significa esto para una persona que trabaja con una API de alto nivel? Mira, haces una solicitud, la serializas, pero muchas veces es imposible enviarla de inmediato. ¿Por qué? Porque la respuesta será msg_id, que es temporalаSoy una etiqueta, cuya cita es mejor posponer lo más tarde posible; de ​​repente, el servidor la rechazará debido a una falta de coincidencia de tiempo entre nosotros y ella (por supuesto, podemos hacer una muleta que cambie nuestro tiempo del presente a la hora del servidor agregando un delta calculado a partir de las respuestas del servidor; los clientes oficiales hacen esto, pero este método es tosco e inexacto debido al almacenamiento en búfer). Entonces, cuando realiza una solicitud con una llamada de función local desde la biblioteca, el mensaje pasa por las siguientes etapas:

  1. Se encuentra en la misma cola y está esperando el cifrado.
  2. Fijado msg_id y el mensaje fue a otra cola - posible reenvío; enviar al zócalo.
  3. a) El servidor respondió MsgsAck: el mensaje fue entregado, lo eliminamos de la "otra cola".
    b) O viceversa, no le gustó algo, respondió badmsg - reenviamos desde la "otra cola"
    c) No se sabe nada, es necesario reenviar el mensaje desde otra cola, pero no se sabe exactamente cuándo.
  4. El servidor finalmente respondió RpcResult - la respuesta real (o error) - no solo entregada, sino también procesada.

Tal vez, el uso de contenedores podría solucionar parcialmente el problema. Esto es cuando un montón de mensajes se empaquetan en uno, y el servidor respondió con un reconocimiento a todos a la vez, con un msg_id. Pero también rechazará este paquete, si algo salió mal, también todo.

Y en este punto entran en juego consideraciones no técnicas. Por experiencia, hemos visto muchas muletas, y además, ahora veremos más ejemplos de malos consejos y arquitectura - en tales condiciones, ¿merece la pena confiar y tomar tales decisiones? La pregunta es retórica (por supuesto que no).

¿De qué estamos hablando? Si sobre el tema "mensajes de adictos sobre mensajes" todavía puedes especular con objeciones como "¡eres estúpido, no entendiste nuestra brillante idea!" (así que primero escriba la documentación, como debería hacerlo la gente normal, con fundamentos y ejemplos de intercambio de paquetes, luego hablaremos), luego los tiempos / tiempos de espera son un problema puramente práctico y específico, todo se sabe desde hace mucho tiempo aquí. Pero, ¿qué nos dice la documentación sobre los tiempos de espera?

Un servidor suele acusar recibo de un mensaje de un cliente (normalmente, una consulta RPC) mediante una respuesta RPC. Si una respuesta tarda mucho en llegar, un servidor puede enviar primero un acuse de recibo y, un poco más tarde, la propia respuesta RPC.

Un cliente normalmente reconoce la recepción de un mensaje de un servidor (generalmente, una respuesta RPC) agregando un reconocimiento a la siguiente consulta RPC si no se transmite demasiado tarde (si se genera, digamos, 60-120 segundos después de la recepción). de un mensaje del servidor). Sin embargo, si durante un largo período de tiempo no hay motivo para enviar mensajes al servidor o si hay una gran cantidad de mensajes del servidor sin confirmar (por ejemplo, más de 16), el cliente transmite un reconocimiento independiente.

... Traduzco: nosotros mismos no sabemos cuánto y cómo es necesario, bueno, calculemos que sea así.

Y sobre los pings:

Mensajes de ping (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

Por lo general, se devuelve una respuesta a la misma conexión:

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

Estos mensajes no requieren acuses de recibo. Un pong se transmite solo en respuesta a un ping, mientras que un ping puede ser iniciado por cualquier lado.

Cierre de conexión diferido + PING

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

Funciona como ping. Además, después de recibir esto, el servidor inicia un temporizador que cerrará la conexión actual connections_delay segundos más tarde, a menos que reciba un nuevo mensaje del mismo tipo que restablece automáticamente todos los temporizadores anteriores. Si el cliente envía estos pings una vez cada 60 segundos, por ejemplo, puede establecer desconectar_retraso en 75 segundos.

¡¿Estás loco?! En 60 segundos, el tren entrará en la estación, dejará y recogerá pasajeros y volverá a perder la conexión en el túnel. En 120 segundos, mientras hurgas, llegará a otro y lo más probable es que la conexión se rompa. Bueno, está claro de dónde crecen las piernas: "Escuché un timbre, pero no sé dónde está", está el algoritmo de Nagle y la opción TCP_NODELAY, que estaba destinada al trabajo interactivo. Pero, lo siento, retrase su valor predeterminado: 200 milisegundos. Si realmente desea representar algo similar y ahorrar en un posible par de paquetes, bueno, pospóngalo, al menos durante 5 segundos, o lo que sea que el tiempo de espera del mensaje "El usuario está escribiendo ..." ahora sea igual. Pero no más.

Y finalmente, pings. Es decir, comprobar la vivacidad de una conexión TCP. Es gracioso, pero hace unos 10 años escribí un texto crítico sobre el mensajero del albergue de nuestra facultad: allí los autores también hicieron ping al servidor del cliente, y no al revés. Pero los estudiantes de tercer año son una cosa, y una oficina internacional es otra, ¿no? ..

Primero, un pequeño programa educativo. Una conexión TCP, en ausencia de intercambio de paquetes, puede durar semanas. Esto es tanto bueno como malo, dependiendo del propósito. Bueno, si tenía abierta una conexión SSH al servidor, se levantó de su computadora, reinició el enrutador de energía, regresó a su lugar: la sesión a través de este servidor no se interrumpió (no escribió nada, no había paquetes), conveniente. Es malo si hay miles de clientes en el servidor, cada uno consume recursos (¡hola, Postgres!) y el host del cliente puede haberse reiniciado hace mucho tiempo, pero no lo sabremos.

Los sistemas de chat/IM pertenecen al segundo caso por otra razón adicional: los estados en línea. Si el usuario "se cayó", es necesario informar a sus interlocutores al respecto. De lo contrario, habrá un error que cometieron los creadores de Jabber (y corrigieron durante 20 años): el usuario se desconectó, pero continúan escribiéndole mensajes, creyendo que está en línea (que también se perdieron por completo en estos pocos minutos antes). se descubrió la ruptura). No, la opción TCP_KEEPALIVE, que muchas personas que no entienden cómo funcionan los temporizadores TCP, aparece en cualquier lugar (estableciendo valores salvajes como decenas de segundos), no ayudará aquí; debe asegurarse de que no solo el núcleo del sistema operativo de la máquina del usuario está viva, pero también funciona normalmente, es capaz de responder, y la aplicación en sí (¿crees que no puede congelarse? Telegram Desktop en Ubuntu 18.04 se ha bloqueado repetidamente).

Por eso deberías hacer ping servidor cliente, y no al revés: si el cliente hace esto, cuando se interrumpe la conexión, no se entregará el ping, no se logrará el objetivo.

¿Y qué vemos en Telegram? ¡Todo es exactamente lo contrario! Bueno, es decir formalmente, por supuesto, ambos lados pueden hacer ping entre sí. En la práctica, los clientes usan una muleta ping_delay_disconnect, que activa un temporizador en el servidor. Bueno, lo siento, no es asunto del cliente decidir cuánto tiempo quiere vivir allí sin ping. El servidor, en función de su carga, sabe mejor. Pero, por supuesto, si no sientes pena por los recursos, entonces los malvados Pinocho son ellos mismos, y la muleta bajará ...

¿Cómo debería haber sido diseñado?

Creo que los hechos anteriores indican claramente la competencia no muy alta del equipo de Telegram / VKontakte en el campo del nivel de transporte (e inferior) de las redes informáticas y su baja calificación en asuntos relevantes.

¿Por qué resultó tan complicado y cómo pueden intentar objetar los arquitectos de Telegram? El hecho de que intentaron hacer una sesión que sobrevive a las roturas de la conexión TCP, es decir, lo que no entregamos ahora, lo entregaremos más tarde. Probablemente también intentaron hacer el transporte UDP, aunque tuvieron dificultades y lo abandonaron (es por eso que la documentación está vacía, no había nada de qué jactarse). Pero debido a la falta de comprensión de cómo funcionan las redes en general y TCP en particular, dónde puede confiar en ellas y dónde debe hacerlo usted mismo (y cómo), y los intentos de combinar esto con la criptografía "una toma de dos pájaros de un tiro” - tal cadáver resultó.

¿Cómo debería haber sido? Basado en el hecho de que msg_id es una marca de tiempo que es criptográficamente necesaria para evitar ataques de repetición, es un error adjuntarle una función de identificador único. Por lo tanto, sin cambiar drásticamente la arquitectura actual (cuando se forma el hilo de Actualizaciones, este es un tema de API de alto nivel para otra parte de esta serie de publicaciones), uno tendría que:

  1. El servidor que mantiene la conexión TCP con el cliente asume la responsabilidad: si restó del socket, reconozca, procese o devuelva un error, sin pérdida. Luego, la confirmación no es un vector de id, sino simplemente "el último seq_no recibido", solo un número, como en TCP (dos números: su propia secuencia y confirmada). Siempre estamos en sesión, ¿no?
  2. La marca de tiempo para evitar ataques de reproducción se convierte en un campo separado, una lance. Comprobado, pero nada más se ve afectado. Suficiente y uint32 - si nuestra sal cambia al menos cada medio día, podemos asignar 16 bits a los bits inferiores de la parte entera de la hora actual, el resto - a la parte fraccionaria de un segundo (como es ahora).
  3. Es removido msg_id en absoluto: desde el punto de vista de distinguir las solicitudes en los backends, está, en primer lugar, la identificación del cliente y, en segundo lugar, la identificación de la sesión, y concatenarlas. En consecuencia, como identificador de solicitud, solo uno es suficiente seq_no.

Tampoco es la mejor opción, un aleatorio completo podría servir como identificador; por cierto, esto ya se hace en la API de alto nivel cuando se envía un mensaje. Sería mejor rehacer la arquitectura de relativa a absoluta por completo, pero este es un tema para otra parte, no para esta publicación.

API?

¡Ta-daam! Entonces, después de haber atravesado un camino lleno de dolor y muletas, finalmente pudimos enviar cualquier solicitud al servidor y recibir respuestas, así como recibir actualizaciones del servidor (no en respuesta a una solicitud, pero nos manda ella misma, como PUSH, si alguien así mucho más claro).

¡Atención, ahora habrá el único ejemplo de Perl en el artículo! (para aquellos que no estén familiarizados con la sintaxis, el primer argumento para bendecir es la estructura de datos del objeto, el segundo es su clase):

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í, especialmente no debajo del spoiler: si no lo has leído, ¡ve y hazlo!

Oh, espe~~... ¿cómo se ve? Algo muy familiar... ¿tal vez esta es la estructura de datos de una API web típica en JSON, excepto que tal vez las clases se adjuntaron a los objetos?...

Entonces resulta ... ¿Qué es, camaradas? ... Tanto esfuerzo, y nos detuvimos a descansar donde los programadores web empezando?.. ¿No sería más fácil simplemente JSON sobre HTTPS? ¿Y qué obtuvimos a cambio? ¿Valieron la pena estos esfuerzos?

Evaluemos qué nos ha dado TL+MTProto y qué alternativas son posibles. Bueno, la solicitud-respuesta HTTP no encaja bien, pero ¿al menos algo además de TLS?

serialización compacta. Viendo esta estructura de datos, similar a JSON, se recuerda que existen sus variantes binarias. Marquemos MsgPack como insuficientemente extensible, pero existe, por ejemplo, CBOR; por cierto, el estándar descrito en RFC 7049. Es notable por el hecho de que define etiquetas, como mecanismo de extensión, y entre ya estandarizado existen:

  • 25 + 256: reemplazo de líneas duplicadas con una referencia de número de línea, un método de compresión tan económico
  • 26 - objeto Perl serializado con nombre de clase y argumentos de constructor
  • 27 - objeto independiente del lenguaje serializado con nombre de tipo y argumentos de constructor

Bueno, traté de serializar los mismos datos en TL y CBOR con el empaquetado de cadenas y objetos habilitados. El resultado comenzó a diferir a favor de CBOR en algún lugar de un megabyte:

cborlen=1039673 tl_len=1095092

Por lo tanto, salida: Hay formatos sustancialmente más simples que no están sujetos a la falla de sincronización o al problema del identificador desconocido, con una eficiencia comparable.

Establecimiento rápido de conexión. Esto significa cero RTT después de la reconexión (cuando la clave ya se generó una vez), aplicable desde el primer mensaje de MTProto, pero con algunas reservas: entraron en la misma sal, la sesión no se estropeó, etc. ¿Qué nos ofrece TLS a cambio? Cita relacionada:

Al usar PFS en TLS, los vales de sesión de TLS (RFC 5077) para reanudar la sesión cifrada sin renegociar las claves y sin almacenar la información de la clave en el servidor. Al abrir la primera conexión y generar claves, el servidor encripta el estado de la conexión y lo envía al cliente (en forma de ticket de sesión). En consecuencia, cuando se reanuda la conexión, el cliente envía un ticket de sesión, que incluye la clave de sesión, de vuelta al servidor. El ticket en sí está encriptado con una clave temporal (clave de ticket de sesión), que se almacena en el servidor y debe distribuirse a todos los servidores frontend que manejan SSL en soluciones en clúster.[10]. Por lo tanto, la introducción de un ticket de sesión puede violar PFS si las claves temporales del servidor se ven comprometidas, por ejemplo, cuando se almacenan durante mucho tiempo (OpenSSL, nginx, Apache las almacenan de forma predeterminada durante todo el tiempo que se ejecuta el programa; sitios populares utilice la tecla durante varias horas, hasta días).

Aquí RTT no es cero, debe intercambiar al menos ClientHello y ServerHello, después de lo cual, junto con Finished, el cliente ya puede enviar datos. Pero aquí debe recordarse que no tenemos la Web, con su montón de conexiones recién abiertas, sino un mensajero, cuya conexión es a menudo una y solicitudes de páginas web relativamente cortas y más o menos duraderas: todo es multiplexado en el interior. Es decir, es bastante aceptable, si no nos topamos con un tramo de metro muy malo.

¿Olvidaste algo más? Escribe en los comentarios.

¡Continuará!

En la segunda parte de esta serie de publicaciones, consideraremos cuestiones organizativas en lugar de técnicas: enfoques, ideología, interfaz, actitud hacia los usuarios, etc. Basado, sin embargo, en la información técnica que se presentó aquí.

La tercera parte continuará con el análisis del componente técnico/experiencia de desarrollo. Aprenderás en particular:

  • continuación del caos con la variedad de tipos TL
  • cosas desconocidas sobre canales y supergrupos
  • que los diálogos es peor que la lista
  • sobre el direccionamiento de mensajes absoluto vs relativo
  • cual es la diferencia entre foto e imagen
  • cómo los emoji interfieren con el texto en cursiva

y otras muletas! ¡Manténganse al tanto!

Fuente: habr.com

Añadir un comentario