ZeroTier 源码解析 (5) 交换机 (Switch)

在上一章 对等节点 (Peer) 中,我们学习了 ZeroTier 是如何通过 Peer 对象来管理与网络中其他每一个节点的"一对一"连接的。我们知道,Peer 对象就像一张详细的联系人名片,记录了如何找到并安全地与另一个节点通信。

但是,在一个由许多节点组成的虚拟网络 (Network)中,事情变得更加复杂。当你的电脑(一个节点 (Node))发送一个数据包时,它可能想发给某个特定的 Peer,也可能想发给网络中的所有人(广播),或者发给一个物理上连接在远程 Peer 后面的设备(桥接)。同样,当一个加密的数据包从互联网上到达时,它可能是给你的,也可能只是路过,需要你帮忙转发给别人。

谁来扮演这个网络交通总指挥的角色,检查每一个数据包的"信封",并决定它的下一站是哪里呢?

这就是 交换机 (Switch) 的职责所在。它是 ZeroTier 节点内部的核心数据调度中心。

什么是交换机 (Switch)?

Switch 对象是 ZeroTier 节点 (Node) 的数据处理中枢,其功能非常类似于现实世界中的物理网络交换机。你可以把它想象成一个高效的**"邮件分拣中心"**。

所有进出你节点的数据包,无论它们是什么类型,都会先被送到这个"分拣中心"进行处理:

  • 来自本地的包裹 :当你在电脑上运行的某个程序(比如浏览器或游戏)发送数据时,这些数据包就像是你写好地址准备寄出的包裹。Switch 会接收它们,查看收件人地址(目标 MAC 地址)。
    • 如果收件人是网络里的另一个 ZeroTier 成员,Switch 就把它打包成 ZeroTier 的加密格式,交给对应的 对等节点 (Peer) 对象去发送。
    • 如果收件人是"所有人"(广播),Switch 就会复印这份包裹,发给网络里所有相关的成员。
  • 来自远方的包裹 :当一个加密的数据包从互联网抵达时,它就像一个从远方寄来的国际包裹。Switch 会先负责"清关"和"拆包"(解密和验证)。
    • 如果包裹是给你的,Switch 就会把里面的内容(原始的以太网帧)取出来,交给你的操作系统。
    • 如果包裹只是需要你中继转发,Switch 就会把它交给正确的下一个 Peer,让它继续旅程。
    • 如果这是一个特殊的"控制信件"(例如,一个 WHOIS 请求,询问某个节点的身份信息),Switch 会直接处理这个请求,而不会把它当作普通数据。

简而言之,Switch 是所有数据流动的必经之路。它通过检查每个数据包的目的地,来智能地决定是应该将它转发给另一个 Peer,还是交给本地的虚拟网络,或是作为控制协议消息来处理。

Switch 的核心职责

Switch 类位于 node/Switch.hppnode/Switch.cpp 中,它的主要职责可以分为两大类,对应两个核心的入口函数:

  1. 处理本地发出的流量 (onLocalEthernet) : 当你的应用程序通过 ZeroTier 的虚拟网卡发送数据时,Switch 会被调用。它负责:

    • 分析目标 MAC 地址,判断是单播、多播还是广播。
    • 根据网络规则对数据包进行过滤。
    • 将以太网帧封装进 ZeroTier 的数据包 (Packet) 格式。
    • 将封装好的 Packet 交给正确的 Peer 或多播模块进行发送。
  2. 处理远端收到的流量 (onRemotePacket) : 当从互联网上收到一个 ZeroTier 数据包时,Switch 会被调用。它负责:

    • (在 Packet 对象内部的帮助下)解密和验证数据包。
    • 检查数据包的类型(verb),判断是普通数据帧还是控制消息。
    • 如果是数据帧,就解开封装,取出以太网帧,并注入到本地操作系统的虚拟网卡。
    • 如果是需要中继的数据包,就将其转发给目标 Peer
    • 如果是控制消息,就直接处理它。

Switch 的工作流程

让我们通过两个最常见的场景,来看看 Switch 是如何工作的。

场景一:发送数据(从本地到远端)

假设你的电脑(10.0.0.1)想要 ping 同一个 ZeroTier 网络里的服务器(10.0.0.2)。

sequenceDiagram participant App as 你的应用程序(ping) participant Node as 你的节点 participant Switch as Switch 对象 participant ServerPeer as 服务器的 Peer 对象 participant Internet as 物理互联网 App->>Node: 发送以太网帧 (目标: 10.0.0.2 的 MAC 地址) Node->>Switch: 调用 onLocalEthernet(以太网帧) Switch->>Switch: 分析目标 MAC,确定目标 ZT 地址 Switch->>Switch: 封装成 VERB_FRAME 数据包 Switch->>ServerPeer: 找到服务器的 Peer 对象,请求发送 ServerPeer->>Internet: 加密并通过最优路径发送数据包
  1. 数据产生 : ping 命令生成一个 ICMP 数据包,操作系统将其封装成一个以太网帧,目标 MAC 地址是服务器 10.0.0.2 对应的虚拟 MAC 地址。
  2. 进入交换机 : 节点 (Node) 捕获这个帧,并立即调用 SwitchonLocalEthernet() 方法。
  3. 决策与封装 : Switch 查看目标 MAC 地址。ZeroTier 的虚拟 MAC 地址是根据节点的 ZeroTier 地址和网络 ID 生成的,所以 Switch 可以从中反解出目标的 ZeroTier 地址(比如 a1b2c3d4e5)。
  4. 它创建一个新的 ZeroTier 数据包 (Packet),将 verb(类型)设置为 VERB_FRAME,然后把原始的以太网帧作为"货物"装进去。
  5. 转发 : Switch 在节点的拓扑 (Topology)管理器中查找目标地址 a1b2c3d4e5 对应的 对等节点 (Peer) 对象。
  6. 找到后,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 的分拣逻辑:

  1. 检查是否是广播/多播地址。
  2. 检查是否是本 ZeroTier 网络内的另一个成员。to[0] == MAC::firstOctetForNetwork(...) 是一个巧妙的优化,因为 ZeroTier 为每个网络内的 MAC 地址分配了特定的第一个字节,可以快速判断。
  3. 如果都不是,就假定它是一个通过桥接模式连接到网络中的外部设备,并尝试找到负责这个设备的"网桥"节点。

场景二:接收数据(从远端到本地)

现在,服务器 10.0.0.2 收到了 ping 请求,并发送了一个回复。这个回复的数据包会加密后通过互联网传回你的电脑。

graph TD A[加密的数据包从互联网到达] --> B{Switch::onRemotePacket}; B --> C{解密并检查 Packet 类型}; C -- VERB_FRAME (数据帧) --> D{解开封装,取出以太网帧}; D --> E{目标 MAC 是我吗?}; E -- 是 --> F[注入本地虚拟网卡,App 收到回复]; E -- 否 (中继) --> G[转发给正确的 Peer]; C -- VERB_WHOIS (控制消息) --> H[直接处理请求,例如回复身份信息];

这个流程图展示了 Switch 处理入站数据包的决策过程:

  1. 接收与解密 : onRemotePacket 方法被调用。它会(借助 Packet 类)使用与发送方 Peer 共享的密钥来解密数据包并验证其完整性。
  2. 分类 : Switch 检查包的 verb
  3. 处理数据帧 : 如果是 VERB_FRAMESwitch 会解开封装,取出里面的以太网帧。然后,它会把这个帧通过回调函数 RR->node->putFrame() 交给操作系统,就像有一个真实的网卡收到了数据一样。你的 ping 程序最终会收到这个回复。
  4. 处理中继 : 如果 Packet 的最终目的地不是当前节点,Switch 会查找真正的目的地 Peer,并将数据包转发过去。
  5. 处理控制消息 : 如果是 VERB_WHOISVERB_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 的两个关键角色:

  1. 中继站: 当数据包的目的地不是自己时,它会扮演一个路由器或中继站的角色,将数据包转发到正确的下一跳。
  2. 终点站 : 当数据包是发给自己的,它会调用 tryDecode() 进行最终处理。tryDecode() 是一个大型的 switch 语句,根据不同的 verb 执行不同的动作,比如将数据帧交给操作系统,或者更新网络配置等。

总结

在本章中,我们认识了 ZeroTier 节点内部的"网络交通总指挥"------交换机 (Switch)

  • Switch 就像一个邮件分拣中心,负责处理所有进出节点的数据包 (Packet)。
  • 它有两个主要入口:onLocalEthernet 用于处理从本地发出 的数据,onRemotePacket 用于处理从远端收到的数据。
  • 通过检查数据包的目的地和类型,Switch 智能地决定是注入本地转发给对等节点 ,还是作为控制消息直接处理。
  • Switch 是连接虚拟世界(ZeroTier 网络)和物理世界(你电脑上的应用和互联网)的关键桥梁,确保了数据在正确的时间被送到正确的地点。

我们已经多次提到了 Switch 处理的核心对象------Packet。这个 Packet 到底是什么样的结构?它如何携带我们的数据,又如何包含各种控制信息呢?

相关推荐
御承扬30 分钟前
HarmonyOS NEXT系列之编译三方C/C++库
c语言·c++·harmonyos
许怀楠36 分钟前
【C++】类和对象(下)
c++
danzongd1 小时前
浅谈C++ const
c++·内存·优化·汇编语言·计算机系统·寄存器
岁忧2 小时前
(LeetCode 面试经典 150 题) 104. 二叉树的最大深度 (深度优先搜索dfs)
java·c++·leetcode·面试·go·深度优先
范特西_3 小时前
矩阵中的最长递增路径-记忆化搜索
c++·算法
重启的码农4 小时前
ZeroTier 源码解析 (4) 对等节点 (Peer)
c++·网络协议
汤永红5 小时前
第4章 程序段的反复执行4 多重循环练习(题及答案)
数据结构·c++·算法·信睡奥赛
网硕互联的小客服6 小时前
IIS7.5下的https无法绑定主机头,显示灰色如何处理?
网络协议·http·https
R-G-B7 小时前
【04】OpenCV C++实战篇——实战:发票精准定位,提取指定单元格数据。(倾角计算、旋转矫正、产品定位、目标定位、OCR文字提取)
c++·opencv·ocr·发票精准定位·提取指定单元格数据·倾角计算·旋转矫正