在上一章 对等节点 (Peer) 中,我们学习了 ZeroTier 是如何通过 Peer
对象来管理与网络中其他每一个节点的"一对一"连接的。我们知道,Peer
对象就像一张详细的联系人名片,记录了如何找到并安全地与另一个节点通信。
但是,在一个由许多节点组成的虚拟网络 (Network)中,事情变得更加复杂。当你的电脑(一个节点 (Node))发送一个数据包时,它可能想发给某个特定的 Peer
,也可能想发给网络中的所有人(广播),或者发给一个物理上连接在远程 Peer
后面的设备(桥接)。同样,当一个加密的数据包从互联网上到达时,它可能是给你的,也可能只是路过,需要你帮忙转发给别人。
谁来扮演这个网络交通总指挥的角色,检查每一个数据包的"信封",并决定它的下一站是哪里呢?
这就是 交换机 (Switch)
的职责所在。它是 ZeroTier 节点内部的核心数据调度中心。
什么是交换机 (Switch)?
Switch
对象是 ZeroTier 节点 (Node) 的数据处理中枢,其功能非常类似于现实世界中的物理网络交换机。你可以把它想象成一个高效的**"邮件分拣中心"**。
所有进出你节点的数据包,无论它们是什么类型,都会先被送到这个"分拣中心"进行处理:
- 来自本地的包裹 :当你在电脑上运行的某个程序(比如浏览器或游戏)发送数据时,这些数据包就像是你写好地址准备寄出的包裹。
Switch
会接收它们,查看收件人地址(目标 MAC 地址)。- 如果收件人是网络里的另一个 ZeroTier 成员,
Switch
就把它打包成 ZeroTier 的加密格式,交给对应的 对等节点 (Peer) 对象去发送。 - 如果收件人是"所有人"(广播),
Switch
就会复印这份包裹,发给网络里所有相关的成员。
- 如果收件人是网络里的另一个 ZeroTier 成员,
- 来自远方的包裹 :当一个加密的数据包从互联网抵达时,它就像一个从远方寄来的国际包裹。
Switch
会先负责"清关"和"拆包"(解密和验证)。- 如果包裹是给你的,
Switch
就会把里面的内容(原始的以太网帧)取出来,交给你的操作系统。 - 如果包裹只是需要你中继转发,
Switch
就会把它交给正确的下一个Peer
,让它继续旅程。 - 如果这是一个特殊的"控制信件"(例如,一个
WHOIS
请求,询问某个节点的身份信息),Switch
会直接处理这个请求,而不会把它当作普通数据。
- 如果包裹是给你的,
简而言之,Switch
是所有数据流动的必经之路。它通过检查每个数据包的目的地,来智能地决定是应该将它转发给另一个 Peer
,还是交给本地的虚拟网络,或是作为控制协议消息来处理。
Switch
的核心职责
Switch
类位于 node/Switch.hpp
和 node/Switch.cpp
中,它的主要职责可以分为两大类,对应两个核心的入口函数:
-
处理本地发出的流量 (
onLocalEthernet
) : 当你的应用程序通过 ZeroTier 的虚拟网卡发送数据时,Switch
会被调用。它负责:- 分析目标 MAC 地址,判断是单播、多播还是广播。
- 根据网络规则对数据包进行过滤。
- 将以太网帧封装进 ZeroTier 的数据包 (Packet) 格式。
- 将封装好的
Packet
交给正确的Peer
或多播模块进行发送。
-
处理远端收到的流量 (
onRemotePacket
) : 当从互联网上收到一个 ZeroTier 数据包时,Switch
会被调用。它负责:- (在
Packet
对象内部的帮助下)解密和验证数据包。 - 检查数据包的类型(
verb
),判断是普通数据帧还是控制消息。 - 如果是数据帧,就解开封装,取出以太网帧,并注入到本地操作系统的虚拟网卡。
- 如果是需要中继的数据包,就将其转发给目标
Peer
。 - 如果是控制消息,就直接处理它。
- (在
Switch
的工作流程
让我们通过两个最常见的场景,来看看 Switch
是如何工作的。
场景一:发送数据(从本地到远端)
假设你的电脑(10.0.0.1
)想要 ping
同一个 ZeroTier 网络里的服务器(10.0.0.2
)。
- 数据产生 :
ping
命令生成一个 ICMP 数据包,操作系统将其封装成一个以太网帧,目标 MAC 地址是服务器10.0.0.2
对应的虚拟 MAC 地址。 - 进入交换机 : 节点 (Node) 捕获这个帧,并立即调用
Switch
的onLocalEthernet()
方法。 - 决策与封装 :
Switch
查看目标 MAC 地址。ZeroTier 的虚拟 MAC 地址是根据节点的 ZeroTier 地址和网络 ID 生成的,所以Switch
可以从中反解出目标的 ZeroTier 地址(比如a1b2c3d4e5
)。 - 它创建一个新的 ZeroTier 数据包 (Packet),将
verb
(类型)设置为VERB_FRAME
,然后把原始的以太网帧作为"货物"装进去。 - 转发 :
Switch
在节点的拓扑 (Topology)管理器中查找目标地址a1b2c3d4e5
对应的 对等节点 (Peer) 对象。 - 找到后,
Switch
调用send()
方法,最终将这个任务委托给Peer
对象,由它负责加密并通过最佳物理路径 (Path)发送出去。
深入代码:onLocalEthernet
onLocalEthernet
的逻辑核心是一个 if-else
结构,用来判断数据包的目的地类型。下面是它极度简化的逻辑:
cpp
// 文件: node/Switch.cpp
void Switch::onLocalEthernet(
void *tPtr,
const SharedPtr<Network> &network,
const MAC &from,
const MAC &to, // 目标 MAC 地址
// ... 其他参数
) {
if (to.isMulticast()) {
// 1. 如果是多播或广播地址
// 将其发送到多播组
RR->mc->send(...);
} else if (to[0] == MAC::firstOctetForNetwork(network->id())) {
// 2. 如果是网络内另一个 ZeroTier 节点的 MAC 地址
Address toZT(to.toAddress(network->id())); // 从 MAC 反解出 ZT 地址
// 封装成 VERB_FRAME 数据包
Packet outp(toZT, RR->identity.address(), Packet::VERB_FRAME);
outp.append(network->id());
outp.append(...); // 添加以太网帧内容
// 将数据包放入发送队列
send(tPtr, outp, true, ...);
} else {
// 3. 如果是其他 MAC 地址 (例如,桥接设备)
// 查找哪个 Peer 桥接了这个 MAC,然后发给他
// ... 桥接逻辑 ...
}
}
这段代码清晰地展示了 Switch
的分拣逻辑:
- 检查是否是广播/多播地址。
- 检查是否是本 ZeroTier 网络内的另一个成员。
to[0] == MAC::firstOctetForNetwork(...)
是一个巧妙的优化,因为 ZeroTier 为每个网络内的 MAC 地址分配了特定的第一个字节,可以快速判断。 - 如果都不是,就假定它是一个通过桥接模式连接到网络中的外部设备,并尝试找到负责这个设备的"网桥"节点。
场景二:接收数据(从远端到本地)
现在,服务器 10.0.0.2
收到了 ping
请求,并发送了一个回复。这个回复的数据包会加密后通过互联网传回你的电脑。
这个流程图展示了 Switch
处理入站数据包的决策过程:
- 接收与解密 :
onRemotePacket
方法被调用。它会(借助Packet
类)使用与发送方Peer
共享的密钥来解密数据包并验证其完整性。 - 分类 :
Switch
检查包的verb
。 - 处理数据帧 : 如果是
VERB_FRAME
,Switch
会解开封装,取出里面的以太网帧。然后,它会把这个帧通过回调函数RR->node->putFrame()
交给操作系统,就像有一个真实的网卡收到了数据一样。你的ping
程序最终会收到这个回复。 - 处理中继 : 如果
Packet
的最终目的地不是当前节点,Switch
会查找真正的目的地Peer
,并将数据包转发过去。 - 处理控制消息 : 如果是
VERB_WHOIS
、VERB_HELLO
等控制消息,Switch
会直接在内部处理它们,例如更新Peer
的状态或回复一个OK
消息。
深入代码:onRemotePacket
onRemotePacket
的代码比较复杂,因为它要处理数据包的分片重组、中继逻辑等。但其核心思想可以简化如下:
cpp
// 文件: node/Switch.cpp (概念性简化)
void Switch::onRemotePacket(..., const void *data, unsigned int len) {
// ... 此处省略了大量的分片重组和错误检查 ...
// 假设我们已经收到了一个完整的、未分片的 Packet
const Address destination( ... 从 data 中解析出目标地址 ... );
const Address source( ... 从 data 中解析出源地址 ... );
if (destination != RR->identity.address()) {
// 1. 如果包不是给我的,执行中继逻辑
Packet packet(data, len);
if (packet.hops() < ZT_RELAY_MAX_HOPS) { // 防止无限转发
packet.incrementHops();
SharedPtr<Peer> relayTo = RR->topology->getPeer(tPtr, destination);
if (relayTo) {
relayTo->sendDirect(tPtr, packet.data(), ...); // 转发给下一个 Peer
}
}
} else {
// 2. 如果包是给我的
IncomingPacket packet(data, len, path, now);
// tryDecode 会解密并根据 verb 执行相应操作
// 如果是 VERB_FRAME,它会调用 Node::putFrame 将数据注入本地
if (!packet.tryDecode(RR, tPtr, ...)) {
// 解码/处理失败或需要等待(例如,等待 WHOIS 回复)
// 将其放入接收队列,稍后重试
}
}
}
这段代码展示了 Switch
的两个关键角色:
- 中继站: 当数据包的目的地不是自己时,它会扮演一个路由器或中继站的角色,将数据包转发到正确的下一跳。
- 终点站 : 当数据包是发给自己的,它会调用
tryDecode()
进行最终处理。tryDecode()
是一个大型的switch
语句,根据不同的verb
执行不同的动作,比如将数据帧交给操作系统,或者更新网络配置等。
总结
在本章中,我们认识了 ZeroTier 节点内部的"网络交通总指挥"------交换机 (Switch)
。
Switch
就像一个邮件分拣中心,负责处理所有进出节点的数据包 (Packet)。- 它有两个主要入口:
onLocalEthernet
用于处理从本地发出 的数据,onRemotePacket
用于处理从远端收到的数据。 - 通过检查数据包的目的地和类型,
Switch
智能地决定是注入本地 、转发给对等节点 ,还是作为控制消息直接处理。 Switch
是连接虚拟世界(ZeroTier 网络)和物理世界(你电脑上的应用和互联网)的关键桥梁,确保了数据在正确的时间被送到正确的地点。
我们已经多次提到了 Switch
处理的核心对象------Packet
。这个 Packet
到底是什么样的结构?它如何携带我们的数据,又如何包含各种控制信息呢?