跳到主要内容

ADNL UDP - 节点间通信

ADNL通过UDP用于节点和TON组件之间的通信。它是一个低层级协议,其他更高级的TON协议(如DHT和RLDP)都是在其基础上运行的。在这篇文章中,我们将了解ADNL通过UDP在节点之间进行基本通信的工作方式。

与ADNL通过TCP不同,在UDP实现中,握手以不同的形式发生,并且使用了通道的额外层,但其他原则相似: 基于我们的私钥和预先从配置或从其他网络节点收到的对方的公钥生成加密密钥。

在ADNL的UDP版本中,连接是在从对方接收初始数据的同时建立的,如果发起方发送了'创建通道'消息,通道的密钥将被计算,并将确认通道的创建。建立通道后,进一步的通信将继续在通道内进行。

数据包结构和通信

首个数据包

让我们分析与DHT节点建立连接并获取其签名地址列表的初始化,以便了解协议的工作方式。

全局配置中找到您喜欢的节点,在dht.nodes部分。例如:

{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "fZnkoIAxrTd4xeBgVpZFRm5SvVvSx7eN3Vbe8c83YMk="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": 1091897261,
"port": 15813
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "cmaMrV/9wuaHOOyXYjoxBnckJktJqrQZ2i+YaY3ehIyiL3LkW81OQ91vm8zzsx1kwwadGZNzgq4hI4PCB/U5Dw=="
}
  1. 我们取其ED25519密钥fZnkoIAxrTd4xeBgVpZFRm5SvVvSx7eN3Vbe8c83YMk,并从base64解码。
  2. 取其IP地址1091897261,并使用服务或转换为小端字节,得到65.21.7.173
  3. 与端口结合,得到65.21.7.173:15813并建立UDP连接。

我们想要打开一个通道与节点通信并获取一些信息,主要任务是接收其签名地址列表。为此,我们将生成2个消息,第一个是创建通道

adnl.message.createChannel key:int256 date:int = adnl.Message

这里有两个参数 - key和date。作为date,我们将指定当前的unix时间戳。对于key,我们需要为通道专门生成一个新的ED25519私钥+公钥对,它们将用于初始化公共加密密钥。我们将在消息的key参数中使用生成的公钥,并暂时保存私钥。

序列化填充的TL结构,得到:

bbc373e6                                                         -- TL ID adnl.message.createChannel 
d59d8e3991be20b54dde8b78b3af18b379a62fa30e64af361c75452f6af019d7 -- key
555c8763 -- date

接下来,我们转到我们的主要查询 - 获取地址列表。为此,我们首先需要序列化其TL结构:

dht.getSignedAddressList = dht.Node

它没有参数,因此我们只需序列化它。它将只是它的id - ed4879a9

接下来,由于这是DHT协议更高级别的请求,我们需要首先将它包裹在adnl.message.query TL结构中:

adnl.message.query query_id:int256 query:bytes = adnl.Message

作为query_id,我们生成随机的32字节,作为query,我们使用我们的主要请求,包裹在字节数组中。 我们将得到:

7af98bb4                                                         -- TL ID adnl.message.query
d7be82afbc80516ebca39784b8e2209886a69601251571444514b7f17fcd8875 -- query_id
04 ed4879a9 000000 -- query

构建数据包

所有通信都是通过数据包进行的,其内容是TL结构

adnl.packetContents 
rand1:bytes -- random 7 or 15 bytes
flags:# -- bit flags, used to determine the presence of fields further
from:flags.0?PublicKey -- sender's public key
from_short:flags.1?adnl.id.short -- sender's ID
message:flags.2?adnl.Message -- message (used if there is only one message)
messages:flags.3?(vector adnl.Message) -- messages (if there are > 1)
address:flags.4?adnl.addressList -- list of our addresses
priority_address:flags.5?adnl.addressList -- priority list of our addresses
seqno:flags.6?long -- packet sequence number
confirm_seqno:flags.7?long -- sequence number of the last packet received
recv_addr_list_version:flags.8?int -- address version
recv_priority_addr_list_version:flags.9?int -- priority address version
reinit_date:flags.10?int -- connection reinitialization date (counter reset)
dst_reinit_date:flags.10?int -- connection reinitialization date from the last received packet
signature:flags.11?bytes -- signature
rand2:bytes -- random 7 or 15 bytes
= adnl.PacketContents

一旦我们序列化了所有想要发送的消息,我们就可以开始构建数据包。发送到通道的数据包与初始化通道之前发送的数据包在内容上有所不同。首先,让我们分析用于初始化的主数据包。

在通道外的初始数据交换期间,数据包的序列化内容结构前缀为对方的公钥 - 32字节。我们的公钥为32字节,数据包内容结构的序列化TL的sha256哈希 - 32字节。数据包内容使用从我们的私钥和对方的公钥(不是通道的密钥)获得的共享密钥进行加密。

序列化我们的数据包内容结构,然后逐字节解析:

89cd42d1                                                               -- TL ID adnl.packetContents
0f 4e0e7dd6d0c5646c204573bc47e567 -- rand1, 15 (0f) random bytes
d9050000 -- flags (0x05d9) -> 0b0000010111011001
-- from (present because flag's zero bit = 1)
c6b41348 -- TL ID pub.ed25519
afc46336dd352049b366c7fd3fc1b143a518f0d02d9faef896cb0155488915d6 -- key:int256
-- messages (present because flag's third bit = 1)
02000000 -- vector adnl.Message, size = 2 messages
bbc373e6 -- TL ID adnl.message.createChannel
d59d8e3991be20b54dde8b78b3af18b379a62fa30e64af361c75452f6af019d7 -- key
555c8763 -- date (date of creation)

7af98bb4 -- TL ID [adnl.message.query](/)
d7be82afbc80516ebca39784b8e2209886a69601251571444514b7f17fcd8875 -- query_id
04 ed4879a9 000000 -- query (bytes size 4, padding 3)
-- address (present because flag's fourth bit = 1), without TL ID since it is specified explicitly
00000000 -- addrs (empty vector, because we are in client mode and do not have an address on wiretap)
555c8763 -- version (usually initialization date)
555c8763 -- reinit_date (usually initialization date)
00000000 -- priority
00000000 -- expire_at

0100000000000000 -- seqno (present because flag's sixth bit = 1)
0000000000000000 -- confirm_seqno (present because flag's seventh bit = 1)
555c8763 -- recv_addr_list_version (present because flag's eighth bit = 1, usually initialization date)
555c8763 -- reinit_date (present because flag's tenth bit = 1, usually initialization date)
00000000 -- dst_reinit_date (present because flag's tenth bit = 1)
0f 2b6a8c0509f85da9f3c7e11c86ba22 -- rand2, 15 (0f) random bytes

序列化后 - 我们需要使用我们之前生成并保存的私有客户端(不是通道的)ED25519密钥对结果字节数组进行签名。 生成签名(大小为64字节)后,我们需要将其添加到数据包中,再次序列化,但现在在标志位中添加第11位,表示存在签名:

89cd42d1                                                               -- TL ID adnl.packetContents
0f 4e0e7dd6d0c5646c204573bc47e567 -- rand1, 15 (0f) random bytes
d90d0000 -- flags (0x0dd9) -> 0b0000110111011001
-- from (present because flag's zero bit = 1)
c6b41348 -- TL ID pub.ed25519
afc46336dd352049b366c7fd3fc1b143a518f0d02d9faef896cb0155488915d6 -- key:int256
-- messages (present because flag's third bit = 1)
02000000 -- vector adnl.Message, size = 2 message
bbc373e6 -- TL ID adnl.message.createChannel
d59d8e3991be20b54dde8b78b3af18b379a62fa30e64af361c75452f6af019d7 -- key
555c8763 -- date (date of creation)

7af98bb4 -- TL ID adnl.message.query
d7be82afbc80516ebca39784b8e2209886a69601251571444514b7f17fcd8875 -- query_id
04 ed4879a9 000000 -- query (bytes size 4, padding 3)
-- address (present because flag's fourth bit = 1), without TL ID since it is specified explicitly
00000000 -- addrs (empty vector, because we are in client mode and do not have an address on wiretap)
555c8763 -- version (usually initialization date)
555c8763 -- reinit_date (usually initialization date)
00000000 -- priority
00000000 -- expire_at

0100000000000000 -- seqno (present because flag's sixth bit = 1)
0000000000000000 -- confirm_seqno (present because flag's seventh bit = 1)
555c8763 -- recv_addr_list_version (present because flag's eighth bit = 1, usually initialization date)
555c8763 -- reinit_date (present because flag's tenth bit = 1, usually initialization date)
00000000 -- dst_reinit_date (present because flag's tenth bit = 1)
40 b453fbcbd8e884586b464290fe07475ee0da9df0b8d191e41e44f8f42a63a710 -- signature (present because flag's eleventh bit = 1), (bytes size 64, padding 3)
341eefe8ffdc56de73db50a25989816dda17a4ac6c2f72f49804a97ff41df502 --
000000 --
0f 2b6a8c0509f85da9f3c7e11c86ba22 -- rand2, 15 (0f) random bytes

现在我们有一个组装好的、签名的和序列化的数据包,它是一个字节数组。 为了让接收方随后验证其完整性,我们需要计算数据包的sha256哈希。例如,让这个哈希是408a2a4ed623b25a2e2ba8bbe92d01a3b5dbd22c97525092ac3203ce4044dcd2

现在让我们使用AES-CTR密码对数据包内容进行加密,使用从我们的私钥和对方的公钥(不是通道的密钥)获得的共享密钥

我们几乎准备好发送了,只剩下计算ED25519对等密钥的ID,并将所有内容连接在一起:

daa76538d99c79ea097a67086ec05acca12d1fefdbc9c96a76ab5a12e66c7ebb  -- server Key ID
afc46336dd352049b366c7fd3fc1b143a518f0d02d9faef896cb0155488915d6 -- our public key
408a2a4ed623b25a2e2ba8bbe92d01a3b5dbd22c97525092ac3203ce4044dcd2 -- sha256 content hash (before encryption)
... -- encrypted content of the packet

现在我们可以通过UDP发送我们构建的数据包,并等待响应。

作为响应,我们将收到具有类似结构的数据包,但消息不同。它将包括:

68426d4906bafbd5fe25baf9e0608cf24fffa7eca0aece70765d64f61f82f005  -- ID of our key
2d11e4a08031ad3778c5e060569645466e52bd1bd2c7b78ddd56def1cf3760c9 -- server public key, for shared key
f32fa6286d8ae61c0588b5a03873a220a3163cad2293a5dace5f03f06681e88a -- sha256 content hash (before encryption)
... -- the encrypted content of the packet

从服务器的数据包的反序列化如下:

  1. 我们检查数据包中的密钥ID,以了解该数据包是为我们准备的。
  2. 使用数据包中的服务器公钥和我们的私钥,我们计算共享密钥并解密数据包内容
  3. 比较我们收到的sha256哈希和从解密数据获得的哈希,它们必须匹配
  4. 使用adnl.packetContents TL模式开始反序列化数据包内容

数据包内容将如下所示:

89cd42d1                                                               -- TL ID adnl.packetContents
0f 985558683d58c9847b4013ec93ea28 -- rand1, 15 (0f) random bytes
ca0d0000 -- flags (0x0dca) -> 0b0000110111001010
daa76538d99c79ea097a67086ec05acca12d1fefdbc9c96a76ab5a12e66c7ebb -- from_short (because flag's first bit = 1)
02000000 -- messages (present because flag's third bit = 1)
691ddd60 -- TL ID adnl.message.confirmChannel
db19d5f297b2b0d76ef79be91ad3ae01d8d9f80fab8981d8ed0c9d67b92be4e3 -- key (server channel public key)
d59d8e3991be20b54dde8b78b3af18b379a62fa30e64af361c75452f6af019d7 -- peer_key (our public channel key)
94848863 -- date

1684ac0f -- TL ID adnl.message.answer
d7be82afbc80516ebca39784b8e2209886a69601251571444514b7f17fcd8875 -- query_id
90 48325384c6b413487d99e4a08031ad3778c5e060569645466e52bd5bd2c7b -- answer (the answer to our request, we will analyze its content in an article about DHT)
78ddd56def1cf3760c901000000e7a60d67ad071541c53d0000ee354563ee --
35456300000000000000009484886340d46cc50450661a205ad47bacd318c --
65c8fd8e8f797a87884c1bad09a11c36669babb88f75eb83781c6957bc976 --
6a234f65b9f6e7cc9b53500fbe2c44f3b3790f000000 --
000000 --
0100000000000000 -- seqno (present because flag's sixth bit = 1)
0100000000000000 -- confirm_seqno (present because flag's seventh bit = 1)
94848863 -- recv_addr_list_version (present because flag's eighth bit = 1, usually initialization date)
ee354563 -- reinit_date (present because flag's tenth bit = 1, usually initialization date)
94848863 -- dst_reinit_date (present because flag's tenth bit = 1)
40 5c26a2a05e584e9d20d11fb17538692137d1f7c0a1a3c97e609ee853ea9360ab6 -- signature (present because flag's eleventh bit = 1), (bytes size 64, padding 3)
d84263630fe02dfd41efb5cd965ce6496ac57f0e51281ab0fdce06e809c7901 --
000000 --
0f c3354d35749ffd088411599101deb2 -- rand2, 15 (0f) random bytes

服务器用两条消息回应我们:adnl.message.confirmChanneladnl.message.answer。 对于adnl.message.answer,一切都很简单,这是我们请求dht.getSignedAddressList的回答,我们将在关于DHT的文章中分析。

让我们关注adnl.message.confirmChannel,这意味着对方已确认创建通道并发送给我们其公共通道密钥。现在,有了我们的私有通道密钥和对方的公共通道密钥,我们可以计算共享密钥

现在我们计算出共享通道密钥后,我们需要从中生成2个密钥 - 一个用于加密发出的消息,另一个用于解密传入的消息。 从中生成2个密钥相当简单,第二个密钥等于共享密钥的倒序写法。例如:

Shared key : AABB2233

First key: AABB2233
Second key: 3322BBAA

剩下的就是确定哪个密钥用于什么,我们可以通过将我们的公共通道密钥的ID与服务器通道的公钥的ID比较,将它们转换为数值形式 - uint256。这种方法用于确保服务器和客户端都确定哪个密钥用于什么。如果服务器使用第一个密钥进行加密,那么使用这种方法,客户端将始终将其用于解密。

使用条款是:

The server id is smaller than our id:
Encryption: First Key
Decryption: Second Key

The server id is larger than our id:
Encryption: Second Key
Decryption: First Key

If the ids are equal (nearly impossible):
Encryption: First Key
Decryption: First Key

[实现示例]

通道内通信

所有后续的数据包交换将在通道内发生,通道密钥将用于加密。 让我们在新创建的通道内发送相同的dht.getSignedAddressList请求,看看区别。

让我们使用相同的adnl.packetContents结构为通道构建数据包:

89cd42d1                                                               -- TL ID adnl.packetContents
0f c1fbe8c4ab8f8e733de83abac17915 -- rand1, 15 (0f) random bytes
c4000000 -- flags (0x00c4) -> 0b0000000011000100
-- message (because second bit = 1)
7af98bb4 -- TL ID adnl.message.query
fe3c0f39a89917b7f393533d1d06b605b673ffae8bbfab210150fe9d29083c35 -- query_id
04 ed4879a9 000000 -- query (our dht.getSignedAddressList packed in bytes with padding 3)
0200000000000000 -- seqno (because flag's sixth bit = 1), 2 because it is our second message
0100000000000000 -- confirm_seqno (flag's seventh bit = 1), 1 because it is the last seqno received from the server
07 e4092842a8ae18 -- rand2, 7 (07) random bytes

通道内的数据包非常简单,实质上由序列(seqno)和消息本身组成。

序列化后,像上次一样,我们计算数据包的sha256哈希。然后我们使用用于通道传出数据包的密钥加密数据包。计算我们传出消息的加密密钥的pub.aes ID,并构建我们的数据包:

bcd1cf47b9e657200ba21d94b822052cf553a548f51f539423c8139a83162180 -- ID of encryption key of our outgoing messages 
6185385aeee5faae7992eb350f26ba253e8c7c5fa1e3e1879d9a0666b9bd6080 -- sha256 content hash (before encryption)
... -- the encrypted content of the packet

我们通过UDP发送数据包,并等待响应。作为回应,我们将收到与我们发送的同类型的数据包(相同字段),但带有我们请求dht.getSignedAddressList的回复。

其他消息类型

对于基本通信,使用像adnl.message.queryadnl.message.answer这样的消息,我们在上面讨论了,但对于某些情况,也使用其他类型的消息,我们将在本节中讨论。

adnl.message.part

此消息类型是其他可能消息类型的一部分,例如adnl.message.answer。当消息太大而无法通过单个UDP数据报传输时,使用此传输数据的方法。

adnl.message.part 
hash:int256 -- sha256 hash of the original message
total_size:int -- original message size
offset:int -- offset according to the beginning of the original message
data:bytes -- piece of data of the original message
= adnl.Message;

因此,为了组装原始消息,我们需要获取几个部分,并根据偏移量将它们连接成一个字节数组。 然后将其作为消息处理(根据这个字节数组中的ID前缀)。

adnl.message.custom

adnl.message.custom data:bytes = adnl.Message;

当更高级别的逻辑与请求-响应格式不符时,使用此类消息,这种消息类型允许将处理完全转移到更高级别,因为消息只携带一个字节数组,没有query_id和其他字段。例如,RLDP使用此类消息,因为对许多请求只有一个响应,这种逻辑由RLDP本身控制。

结论

此后的通信基于本文描述的逻辑进行,但数据包的内容取决于更高级别的协议,如DHT和RLDP。

参考

这里是原文链接,作者是Oleg Baranov