MTProto 传输协议
以下是 MTProto 传输协议列表(完整说明请参见 ISO/OSI 概述):
服务器通过请求头识别这些不同的协议(并将它们与 HTTP 区分开来)。此外,还可以使用以下传输特性:
可以在tdlib和MadelineProto中看到这些协议的示例实现。
简略
最轻便的协议。
- 顶部:非常小
- 最小信封长度:1 字节
- 最大信封长度:4 字节
有效载荷结构:
+-+----...----+ |l| payload | +-+----...----+ OR +-+---+----...----+ |h|len| payload + +-+---+----...----+
在向底层套接字发送任何内容之前(参见传输协议),客户端必须先将数据0xef作为第一个字节发送(服务器不会0xef在第一个响应中将数据作为第一个字节发送)。
然后,有效负载会被封装在以下信封中:
如果数据包长度除以 4 小于 127:
- 长度:有效载荷长度除以 4,并编码为一个字节。
- 有效载荷:MTProto 有效载荷
如果数据包长度除以 4 大于或等于 127,则必须改用以下信封:
- 头部:一个字节的值0x7f(127)
- 长度:有效载荷长度除以 4,并编码为 3 个长度字节(小端序)
- 有效载荷:MTProto 有效载荷
可以为此传输启用快速确认 (Quick ACK) 。
要向服务器请求对加密的 MTProto 有效负载的快速 ACK,请使用以下信封发送消息,而不是使用上面指定的信封。
如果数据包长度除以 4 小于 127:
- 长度:有效载荷长度除以 4,加上 128,并编码为单个字节((length/4)+128或(length >> 2) | (1 << 7));即,最高有效位必须设置。
- 有效载荷:MTProto 有效载荷
如果数据包长度除以 4 大于或等于 127,则必须改用以下信封:
- 头部:一个字节的值0xff(255)
- 长度:有效载荷长度除以 4,并以 3 个长度字节(小端序)编码;与普通缩减信封相同。
- 有效载荷:MTProto 有效载荷
服务器将通过交换(即反转 ACK 令牌的 4 个字节的顺序)并将它们作为独立的 4 字节数据包发送(不带长度标头)来快速发送 ACK 令牌。
+----+ |dcba| +----+
这些快速 ACK 数据包很容易与普通的缩减数据包区分开来,因为第一个字节的最高有效位总是被设置(因为快速 ACK 令牌的最后一个字节的最高有效位被设置,并且由于它们被交换,最后一个字节会排在前面),而来自服务器的普通有效载荷数据包的长度/头部总是小于或等于 127(因此普通有效载荷的最高有效位不会被设置)。
中间的
如果需要 4 字节数据对齐,可以使用原始协议的中间版本。
- 顶部:小
- 最小信封长度:4 字节
- 最大信封长度:4 字节
有效载荷结构:
+----+----...----+ +len.+ payload + +----+----...----+
在向底层套接字发送任何内容之前(参见传输协议),客户端必须首先发送0xeeeeeeee第一个整数(四个字节,服务器不会0xeeeeeeee在第一个回复中发送第一个整数)。
然后,有效负载会被封装在以下信封中:
- 长度:有效载荷长度,编码为 4 个长度字节(小端序)
- 有效载荷:MTProto 有效载荷
可以为此传输启用快速确认 (Quick ACK) 。
要从服务器请求对加密的 MTProto 有效负载的快速 ACK,请在编码之前将其添加0x80000000到len字段中(相当于执行len = len | (1 << 31),即设置长度的最高有效位)。
服务器将以独立的 4 字节数据包的形式发送快速 ACK 令牌,不带长度标头。
+----+ |abcd| +----+
这些快速 ACK 数据包很容易与普通的中间数据包区分开来,因为快速 ACK 令牌的最后一个字节的最高有效位总是被设置,而尝试将 ACK 令牌解码为小端 32 位整数总是会产生一个大于或等于 1 的值0x80000000,这永远不可能是一个有效的数据包长度。
带衬垫的中级
- 顶部空间:中小
- 信封最小长度:随机
- 信封最大长度:随机
在向底层套接字发送任何内容之前(参见传输协议),客户端必须首先发送0xdddddddd第一个整数(四个字节,服务器不会0xdddddddd在第一个回复中发送第一个整数)。
然后,有效负载会被封装在以下信封中:
+----+----...----+----...----+ |tlen| payload | padding | +----+----...----+----...----+
信封说明:
- 总长度:有效载荷+填充长度,编码为 4 个长度字节(小端序)
- 有效载荷:MTProto 有效载荷
- 填充:长度为 的随机填充字符串0-15
可以为此传输启用快速确认 (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运输协议
- 顶部:中等
- 最小信封长度:12 字节(长度+序列号+CRC)
- 最大信封长度:12 字节(长度+序列号+CRC)
有效载荷结构:
+----+----+----...----+----+ |len.|seq.| payload |crc.| +----+----+----...----+----+
信封说明:
- 长度:长度+序列号+有效载荷+CRC长度,编码为4个长度字节(小端序,长度字段的长度也必须包含在内)。
- Seqno:此 TCP 连接的 TCP 序列号(与MTProto 序列号不同):发送的第一个数据包编号为 0,下一个数据包编号为 1,依此类推。
- 有效载荷:MTProto 有效载荷
- crc:使用长度、序列号和有效载荷一起计算的 4 个 CRC32 字节。
交通功能
此外,还可以使用以下交通功能:
快速回复
上面列出的一些 TCP 传输协议支持快速 ACK:快速 ACK 是客户端快速获取数据包接收确认的一种方式。
要请求对特定传出有效载荷进行快速确认,客户端必须设置传输信封中相应字段的最高有效位 (MSB)(如上文每个传输协议的文档中所述)。
此外,客户端必须生成并存储一个快速 ACK 令牌,并将其与传出的 MTProto 有效负载关联起来,具体方法如下:
- 取有效载荷加密部分的 SHA256 的前 32 位,前面加上授权密钥的 32 个字节(与计算消息密钥时生成的哈希相同,只是取前 32 位而不是中间 128 位)。
- 将最后一个字节的最高有效位 (MSB) 设置为 1:换句话说,将上面生成的 32 位视为小端整数,然后加到0x80000000它上面(即ack_token = msg_key_long[0:4] | (1 << 31)在小端系统上)。
一旦服务器成功接收、解密并接受有效载荷进行处理,服务器将使用每个传输协议文档中描述的编码,发送我们上面生成的相同快速 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)