在上一章 交换机 (Switch) 中,我们将 Switch
比作一个高效的"邮件分拣中心",它负责指挥所有数据流的走向。我们知道,Switch
处理的每一个项目都是一个独立的、封装好的信息单元。
那么,这些在 ZeroTier 网络中穿梭的"邮件"本身到底是什么样子的呢?它们如何既能携带我们要发送的数据,又能包含路由、加密和控制所需的所有元数据?
这就是 数据包 (Packet)
的用武之地。如果 Switch
是邮政分拣中心,那么 Packet
就是那个带有地址、邮票和内容的标准信封。
什么是数据包 (Packet)?
Packet
是 ZeroTier 网络中通信的基本单位。它就像一个精心设计的数字信封 ,无论是发送文件、网页请求,还是简单的 ping
命令,所有数据最终都必须被装入这种标准格式的信封中才能在网络上传输。
让我们用一个实体信封来比喻 Packet
的结构:
数字信封"] --> Header["头部 (Header)
信封外部"]; Packet --> Payload["载荷 (Payload)
信封内部的信件"]; Header --> ToFrom["收/发件人地址
(源/目标 ZT 地址)"]; Header --> TrackingID["追踪号码
(唯一的 Packet ID)"]; Header --> Instructions["特殊指令 (Verb)
例如:'账单'、'邀请函'"]; Header --> SecuritySeal["防伪封条 (MAC)
(用于验证信息未被篡改)"]; Payload --> Content["数据内容
(如以太网帧、控制信息等)"];
-
信封外部(头部 Header) :
Packet
的头部包含了所有必要的元数据,用于正确、安全地投递。- 收/发件人地址: 明确的源和目标 ZeroTier 地址。
- 追踪号码 : 一个唯一的
Packet ID
,也用作加密的初始向量(IV),确保每次通信都是独一无二的。 - 特殊指令 (Verb) : 这是
Packet
最具特色的部分。它是一个"动词",明确地告诉接收方这个数据包的意图 是什么。例如,是VERB_FRAME
(发送一个普通的数据帧),VERB_HELLO
(与一个新节点打招呼并交换身份),还是VERB_ERROR
(告知对方发生了错误)。 - 防伪封条 (MAC): 一个消息认证码,用于确保数据包在传输过程中没有被篡改。
-
信封内部(载荷 Payload) : 这是
Packet
真正携带的信息。它的内容完全取决于头部的"动词 (Verb)"。如果动词是VERB_FRAME
,载荷就是实际的以太网帧;如果动词是VERB_ERROR
,载荷就是具体的错误代码。
最关键的是,Packet
类自身就封装了处理安全性的所有逻辑。它负责对载荷进行加密、压缩,并生成那个防伪封条。这确保了 ZeroTier 通信的机密性、完整性和效率。
Packet
的核心职责
Packet
作为一个数据结构类,其核心职责是封装和保护数据。
- 数据封装: 它提供了一个标准的格式,可以将上层应用的数据(如以太网帧)与 ZeroTier 协议所需的元数据(地址、动词等)打包在一起。
- 加密与解密 : 当一个数据包准备发送时,
Packet
的armor()
方法会使用发送方和接收方对等节点 (Peer)之间共享的密钥来加密载荷。相应地,dearmor()
方法在接收端负责解密。 - 认证与验证 :
armor()
方法在加密的同时,还会计算出一个消息认证码(MAC),就像给信封打上蜡封。dearmor()
方法在收到包后,会首先校验这个"蜡封"是否完好,如果被篡改,则立即丢弃该包。 - 压缩与解压 : 为了节省带宽,
Packet
还内置了compress()
和uncompress()
方法,使用 LZ4 算法对载荷进行可选的压缩。
Packet
的生命周期:一次安全的投递
想象一下,节点A 要发送一条消息给节点B。这个过程在 Packet
层面是如何运作的?
- 打包 : 节点 (Node) A 创建一个
Packet
对象,填上B的地址和自己的地址,并将"动词"设为VERB_FRAME
。然后,它将原始数据(例如一个以太网帧)附加到Packet
的载荷区。 - 压缩 :
Node
A 调用Packet
的compress()
方法。如果压缩能显著减小体积,Packet
内部就会替换掉原始载荷,并设置一个"已压缩"的标志。 - 封装与加密 (Armor) :
Node
A 调用armor()
方法。这个方法会使用与B共享的密钥:- 加密整个载荷区。
- 计算整个包(包括头部和加密后的载荷)的 MAC 值,并填入头部。
- 发送 : 此时,
Packet
已经是一个准备就绪、无法被窃听和篡改的二进制数据块,可以通过互联网发送给节点B。 - 解封与验证 (Dearmor) : 节点B收到数据后,用它创建
Packet
对象,并立即调用dearmor()
方法。这个方法会:- 使用与A共享的密钥,重新计算一遍收到的包的 MAC 值。
- 如果计算出的 MAC 与包头部的 MAC 不符,说明数据在途中被篡改,
dearmor()
返回false
,包被丢弃。 - 如果 MAC 相符,
dearmor()
才会继续用密钥解密载荷。
- 解压与处理 : 如果
dearmor()
成功,节点B接着调用uncompress()
(如果需要),最后从载荷区读出原始数据进行处理。
代码中的 Packet
Packet
的定义可以在 node/Packet.hpp
中找到。它继承自一个名为 Buffer
的类,本质上是一个可操作的字节缓冲区。
cpp
// 文件: node/Packet.hpp
class Packet : public Buffer<ZT_PROTO_MAX_PACKET_LENGTH>
{
public:
// 动词列表,定义了数据包的意图
enum Verb {
VERB_NOP = 0x00,
VERB_HELLO = 0x01,
VERB_ERROR = 0x02,
VERB_OK = 0x03,
VERB_FRAME = 0x06,
// ... 其他动词
};
// 构造函数:创建一个新的数据包
Packet(const Address &dest, const Address &source, const Verb v);
// 获取和设置动词
inline Verb verb() const;
inline void setVerb(Verb v);
// 加密并认证数据包
void armor(const void *key, bool encryptPayload, const AES aesKeys[2]);
// 验证并解密数据包
bool dearmor(const void *key, const AES aesKeys[2]);
// 压缩和解压缩载荷
bool compress();
bool uncompress();
// ... 其他获取和设置头部字段的方法
};
enum Verb
: 这个枚举定义了所有可能的"动词",是Packet
的核心。- 构造函数
Packet(...)
: 创建一个数据包时,必须指定收件人、发件人和意图(动词)。 armor()
/dearmor()
: 这是实现安全通信的两个关键方法。compress()
/uncompress()
: 提供可选的压缩功能。
深入代码:IncomingPacket
和 tryDecode
当一个数据包从网络到达时,它首先被封装成一个 IncomingPacket
对象,这是 Packet
的一个特殊子类。然后,交换机 (Switch) 会调用 IncomingPacket
的 tryDecode()
方法来处理它。
tryDecode()
的核心是一个巨大的 switch
语句,它完美地诠释了"动词"驱动的设计思想。
cpp
// 文件: IncomingPacket.cpp (tryDecode 函数的简化逻辑)
bool IncomingPacket::tryDecode(const RuntimeEnvironment *RR, void *tPtr, int32_t flowId)
{
// ... 此处省略了解密(dearmor)和解压(uncompress)的过程 ...
// 假设数据包已经通过验证并解密
_authenticated = true;
const Packet::Verb v = verb(); // 获取数据包的"动词"
bool r = true;
switch(v) { // 根据动词的类型,执行不同的处理逻辑
case Packet::VERB_HELLO:
// 如果是 HELLO,执行处理 HELLO 的逻辑
r = _doHELLO(RR, tPtr, true);
break;
case Packet::VERB_FRAME:
// 如果是数据帧,执行处理数据帧的逻辑
r = _doFRAME(RR, tPtr, peer, flowId);
break;
case Packet::VERB_ERROR:
// 如果是错误报告,执行处理错误的逻辑
r = _doERROR(RR, tPtr, peer);
break;
// ... 处理其他所有类型的动词 ...
default: // 忽略未知的动词
peer->received(...); // 但仍然记录收到了一个包
break;
}
// ...
return r;
}
这段代码清晰地展示了 Switch
是如何委托 IncomingPacket
进行自我处理的:
- 在
tryDecode()
内部,首先调用dearmor()
和uncompress()
,完成解封和准备工作。 - 然后,它获取数据包的
verb()
。 switch
语句根据verb()
的值,将任务派发给对应的私有方法(例如_doHELLO
,_doFRAME
等)。- 每个
_do...
方法都包含了处理特定类型数据包的所有逻辑,例如,_doFRAME
会将解出的以太网帧注入到操作系统的网络栈中。
这种设计使得代码结构非常清晰,每种数据包的处理逻辑都被整齐地封装在各自的函数里。
深入幕后:armor()
的魔法
armor()
方法是数据包安全的保障。虽然它的实现涉及复杂的密码学操作,但其核心思想可以简化为以下伪代码:
pseudocode
// Packet::armor() 的概念性伪代码
function armor(共享密钥, 是否加密内容):
// 1. 根据共享密钥和数据包的唯一信息(如Packet ID)
// 生成一个一次性的加密密钥和 MAC 密钥。
(一次性加密密钥, MAC密钥) = 从(共享密钥, PacketID)派生得出
// 2. 如果需要,用"一次性加密密钥"加密"信件内容"(Payload)。
if (是否加密内容):
this.载荷 = encrypt(this.载荷, with: 一次性加密密钥)
// 3. 为整个包(包括头部和可能已加密的载荷)
// 用"MAC密钥"计算一个"防伪封条"(MAC)。
mac_value = compute_mac(整个数据包的数据, with: MAC密钥)
// 4. 将"防伪封条"贴在"信封"上(写入MAC字段)。
this.mac_field = mac_value
这个过程确保了:
- 机密性: 即使攻击者截获了数据包,没有共享密钥也无法解密载荷。
- 完整性: 任何对数据包(哪怕一个比特)的修改都会导致 MAC 验证失败,从而被接收方识破和丢弃。
- 防重放 : 因为
Packet ID
每次都不同,所以派生出的一次性密钥也不同。攻击者无法把旧的数据包重新发一遍来欺骗系统。
总结
在本章中,我们详细了解了 ZeroTier 通信的基本单元------数据包 (Packet)
。
Packet
就像一个数字信封,它将要传输的数据(载荷)和所有必要的元数据(头部)封装在一起。- 其头部的核心是一个动词 (Verb) ,它明确定义了这个数据包的意图,驱动着整个节点的处理逻辑。
Packet
类自身负责加密、认证和压缩 ,通过armor()
和dearmor()
方法,为 ZeroTier 的通信提供了端到端的安全保障。- 当一个
IncomingPacket
到达时,它的tryDecode()
方法会根据"动词"来调度执行相应的处理代码,实现了清晰的逻辑分离。
我们现在知道了节点之间如何通过安全的 Packet
来交换信息。但是,一个节点是如何知道网络中存在哪些其他节点,以及如何联系它们的呢?换句话说,节点是如何构建和维护它对整个虚拟网络的"地图"的?