MTProto 传输协议

以下是 MTProto 传输协议列表(完整说明请参见 ISO/OSI 概述):

服务器通过请求头识别这些不同的协议(并将它们与 HTTP 区分开来)。此外,还可以使用以下传输特性:

可以在tdlib和MadelineProto中看到这些协议的示例实现。

简略

最轻便的协议。

有效载荷结构:

+-+----...----+
|l|  payload  |
+-+----...----+
OR

+-+---+----...----+
|h|len|  payload  +
+-+---+----...----+

在向底层套接字发送任何内容之前(参见传输协议),客户端必须先将数据0xef作为第一个字节发送(服务器不会0xef在第一个响应中将数据作为第一个字节发送)。
然后,有效负载会被封装在以下信封中:

如果数据包长度除以 4 小于 127:

如果数据包长度除以 4 大于或等于 127,则必须改用以下信封:

可以为此传输启用快速确认 (Quick ACK) 。

要向服务器请求对加密的 MTProto 有效负载的快速 ACK,请使用以下信封发送消息,而不是使用上面指定的信封。

如果数据包长度除以 4 小于 127:

如果数据包长度除以 4 大于或等于 127,则必须改用以下信封:

服务器将通过交换(即反转 ACK 令牌的 4 个字节的顺序)并将它们作为独立的 4 字节数据包发送(不带长度标头)来快速发送 ACK 令牌。

+----+
|dcba|
+----+

这些快速 ACK 数据包很容易与普通的缩减数据包区分开来,因为第一个字节的最高有效位总是被设置(因为快速 ACK 令牌的最后一个字节的最高有效位被设置,并且由于它们被交换,最后一个字节会排在前面),而来自服务器的普通有效载荷数据包的长度/头部总是小于或等于 127(因此普通有效载荷的最高有效位不会被设置)。

中间的

如果需要 4 字节数据对齐,可以使用原始协议的中间版本。

有效载荷结构:

+----+----...----+
+len.+  payload  +
+----+----...----+

在向底层套接字发送任何内容之前(参见传输协议),客户端必须首先发送0xeeeeeeee第一个整数(四个字节,服务器不会0xeeeeeeee在第一个回复中发送第一个整数)。
然后,有效负载会被封装在以下信封中:

可以为此传输启用快速确认 (Quick ACK) 。

要从服务器请求对加密的 MTProto 有效负载的快速 ACK,请在编码之前将其添加0x80000000到len字段中(相当于执行len = len | (1 << 31),即设置长度的最高有效位)。

服务器将以独立的 4 字节数据包的形式发送快速 ACK 令牌,不带长度标头。

+----+
|abcd|
+----+

这些快速 ACK 数据包很容易与普通的中间数据包区分开来,因为快速 ACK 令牌的最后一个字节的最高有效位总是被设置,而尝试将 ACK 令牌解码为小端 32 位整数总是会产生一个大于或等于 1 的值0x80000000,这永远不可能是一个有效的数据包长度。

带衬垫的中级

中间协议的填充版本,用于启用混淆功能绕过 ISP 封锁

在向底层套接字发送任何内容之前(参见传输协议),客户端必须首先发送0xdddddddd第一个整数(四个字节,服务器不会0xdddddddd在第一个回复中发送第一个整数)。
然后,有效负载会被封装在以下信封中:

+----+----...----+----...----+
|tlen|  payload  |  padding  |
+----+----...----+----...----+

信封说明:

可以为此传输启用快速确认 (Quick ACK) 。

要从服务器请求对加密的 MTProto 有效负载的快速 ACK,请在编码之前将其添加0x80000000到len字段中(相当于执行len = len | (1 << 31),即设置长度的最高有效位)。

服务器将以独立的 8 到 16 字节数据包(不包括数据包本身的长度,按常规方式编码)的形式发送快速 ACK 令牌,其中包含一个所有位都已设置的 4 字节标头,后跟 ACK 令牌(abcd),后跟 0 到 8 个随机填充字节。

+----+----+----+----...----+
|tlen|FFFF|abcd|  padding  |
+----+----+----+----...----+

这些快速 ACK 数据包很容易与普通的中间填充数据包区分开来,因为它们的长度始终小于或等于 16,比任何 MTProto 数据包的长度都小。
它们也可以与传输错误区分开来,因为有效载荷的前 4 个字节等于 0 0xFFFFFFFF,这不是有效的传输错误。

满的

基本的MTProto运输协议

有效载荷结构:

+----+----+----...----+----+
|len.|seq.|  payload  |crc.|
+----+----+----...----+----+

信封说明:

交通功能

此外,还可以使用以下交通功能:

快速回复

上面列出的一些 TCP 传输协议支持快速 ACK:快速 ACK 是客户端快速获取数据包接收确认的一种方式。

要请求对特定传出有效载荷进行快速确认,客户端必须设置传输信封中相应字段的最高有效位 (MSB)(如上文每个传输协议的文档中所述)。

此外,客户端必须生成并存储一个快速 ACK 令牌,并将其与传出的 MTProto 有效负载关联起来,具体方法如下:

一旦服务器成功接收、解密并接受有效载荷进行处理,服务器将使用每个传输协议文档中描述的编码,发送我们上面生成的相同快速 ACK 令牌。

请注意,收到快速 ACK并不表示消息中包含的任何 RPC 查询已成功、失败或已完成执行,它仅仅表示服务器已接收、解密并接受这些查询进行处理。

服务器仍将msgs_ack像往常一样,发送与内容相关的构造函数和有效负载中包含的方法的构造函数(这些构造函数和方法已快速确认),以及方法和构造函数的回复/错误。

传输错误

如果发生传输错误(缺少身份验证密钥、传输泛洪等),服务器可能会发送一个数据包(由选定的 MTProto 传输封装为 MTProto 有效负载),其中包含一个 4 字节的有符号小端序数字,其绝对值包含错误代码(错误本身实际上是负数)。

例如,错误代码 403 对应于 HTTP 协议本应返回相应 HTTP 错误的情况。

当 DC 找不到指定的身份验证密钥 ID 时,或者在初始 MTProto 握手期间,如果任何指定的查询不正确,或者在正常操作期间,例如,如果某些 MTProto 字段不正确(即 MTProto 数据包长度大于传输指定的数据包长度,等等),则会返回错误 404(找不到身份验证密钥)。

当在太短的时间内建立到同一 IP 的传输连接过多,或者达到任何容器/服务消息限制时,将返回错误 429(传输洪水)。

在创建身份验证密钥连接到 MTProxy或其他上下文中,如果指定了无效的 DC ID,则会返回错误 444(无效的 DC) 。

使用HTTP / HTTPS传输时,传输错误不会按上述方式传输,而是简单地作为普通的 HTTP 状态代码返回(并且必须忽略 HTTP 有效负载)。

运输混淆

使用WebSocket 传输需要进行传输混淆。

为了防止 ISP 封锁,传输混淆是通过以下协议实现的,该协议位于 ISO/OSI 协议栈的 MTProto 传输层下(参见概述;这意味着有效载荷首先被封装在MTProto 传输层中(支持所有传输层),然后进行混淆:

在建立连接(并最终发送特定MTProto 传输协议的协议头)之前,会生成一个 64 字节(512 位)的随机初始化有效载荷。在生成过程中,必须特别注意避免随机字符串的前四个字节(即第一个整数)与已知的协议标识符之一相等(见上文)。
具体来说,前四个字节不能等于0xdddddddd(填充中间值)、0xeeeeeeee(中间值)POST、、、或任何 MTProto 服务器接受的 HTTP 方法。 第一个字节也不能等于(缩减值)。字节也不能等于,因为这表示使用初始 TCP 序列号为 0 的完整传输协议GET 。HEAD
0xef4-80x00000000

如果存在协议标识符,则必须将其插入初始化有效载荷的字节偏移量中56:如果其长度小于 4,则必须使用协议标识符本身进行填充,使其长度为 4(0xef=> 0xefefefef):之后不得发送独立的协议标识符。

此协议也(但不限于)用于连接到 MTProxies:仅在这种情况下,必须在初始化有效负载的偏移量处插入特殊编码形式的 DC ID 60。该编码形式为双字节有符号小端序的 DC ID;10000必须将其添加到 DC ID 才能连接到测试服务器;如果要连接的 DC 是媒体(而非 CDN)DC,则必须将其设为负数。

接下来,通过反转主初始化有效载荷来生成辅助初始化有效载荷。

从两个初始化有效载荷中提取两个密钥,使用偏移量的字节8-40:从主有效载荷中提取的密钥用作加密密钥,从辅助有效载荷中提取的密钥用作解密密钥。

从两个初始化有效载荷中提取两个 IV,使用偏移量的字节40-56:从主有效载荷中提取的 IV 用作加密 IV,从辅助有效载荷中提取的 IV 用作解密 IV。

只有在使用 MTProxy 时,才需要使用密钥来建立与 MTProxy 服务器的连接。密钥是一个 16 字节的字符串,通常以十六进制形式与 MTProxy 主机和端口一起分发在代理深度链接中。

通常情况下,可以找到一个 17 字节的密钥版本:这仅仅表明客户端应该使用特定的 MTProto 传输(基于第一个字节,通常是0xdd,表示应该使用填充中间协议0xdddddddd;但是,每当在密钥中遇到额外的字节时,客户端都应该默认使用填充中间传输)。

提取出的加密和解密密钥必须与秘密字符串连接起来(如果是 17 字节版本,则应忽略其第一个字节),并将该字符串的 SHA256 哈希值用作加密/解密密钥。

获得的加密和解密密钥/初始化向量 (IV) 对必须与AES-256-CTR 算法配合使用,以加密和解密所有传出和传入的有效载荷。
在处理完一个 MTProto 有效载荷后,加密/解密计数器的最终值必须用作下一个有效载荷的 IV,直到 TCP/WS 连接关闭为止。
换句话说,在连接关闭之前,需要重用两个用于加密和解密的AES-256-CTR OpenSSL 实例。

首先,必须使用加密密钥对初始化有效载荷本身进行加密。然后,56-64将加密后的初始化有效载荷的字节替换到原始初始化有效载荷中:这部分包含常量 MTProto 传输协议标识符和 DC ID(仅适用于 MTProxies)。

最终的初始化有效载荷必须在TCP 握手之后作为套接字的前 64 字节发送。

以下示例伪代码展示了如何使用混淆填充中间传输生成 MTProxy 连接有效负载(媒体 DC 4)。
警告:请勿在任何暴露于互联网的 MTProxy 中使用指定的代理密钥。

protocol := 0xdddddddd
dc := 0xfcff

while True:
    init := (56 random bytes) + protocol + dc + (2 random bytes)
    
    if init[0] == 0xef:
      continue

	first_int := substr(init, 0, 4)
	if first_int == 0x44414548 || first_int == 0x54534f50 || first_int == 0x20544547 || first_int == 0x4954504f || first_int == 0x02010316 || first_int == 0xdddddddd || first_int == 0xeeeeeeee:
      continue
      
	second_int := substr(init, 4, 4)
    if second_int == 0x00000000:
      continue
      
	break

initRev := strrev(init)

encryptKey := substr(init, 8, 32)
encryptIV := substr(init, 40, 16)

decryptKey := substr(initRev, 8, 32)
decryptIV := substr(initRev, 40, 16)

secret := substr(0xdd99999999999999999999999999999999, 1, 16)

encryptKey = SHA256(encryptKey + secret)
decryptKey = SHA256(decryptKey + secret)

encryptedInit := CTR(encryptKey, encryptIV, init)

finalInit := substr(init, 0, 56) + substr(encryptedInit, 56, 8)

write(finalInit)