欢迎来到 ZeroTier 的核心代码探索之旅!在本系列教程中,我们将逐一揭开构成 ZeroTier 强大功能的核心概念。这是我们的第一站。
想象一下,你和你的团队成员分布在全球各地——有些人在家办公,有些人在咖啡馆,还有些人在办公室。你们需要共享文件、访问内部服务器,并像在同一个办公室里一样无缝协作。在传统世界里,这通常需要复杂的 VPN 设置和网络配置。
那么,有没有一种更简单的方法,能让这些分散的设备感觉就像用一根网线连接在同一个房间里一样呢?
这就是 虚拟网络 (Network) 概念要解决的问题。
Network 对象代表一个节点(你的设备)加入的虚拟局域网。它就像一个私密的、安全的线上会议室,只有被邀请的成员才能进入和交流。
什么是虚拟网络?
让我们用一个简单的比喻来理解:
把一个虚拟网络想象成一个专为您和您的团队开设的“线上办公室”。
- 独一无二的办公室门牌号: 每个虚拟网络都有一个唯一的 网络ID (Network ID),就像办公室的门牌号一样。只有知道这个门牌号的人才能找到并申请加入。
- 成员准入控制: 并不是任何人都能随意进入。网络管理员会维护一份“成员列表”。只有被批准的成员才能进入这个线上办公室。这确保了网络的私密性和安全性。
- 内部专线电话: 一旦进入,每个成员都会被分配一个内部 IP 地址(例如
10.147.17.23)。这就像一个内部电话分机号,成员之间可以通过这个地址直接通信,无论他们身处何处。 - 安保和规章制度: 这个线上办公室有自己的规则 (Rules)。例如,“只有开发部门的成员才能访问代码服务器”,“禁止访问外部的视频网站”等。这些规则由网络管理员设定,所有成员都必须遵守。
在 ZeroTier 的代码世界里,Network 类就是这个“线上办公室”的数字体现。当你的设备(一个 节点 (Node))加入一个 ZeroTier 网络时,它就会在内部创建一个 Network 对象来管理所有与该网络相关的事务。
Network 对象的核心职责
一个 Network 对象包含了关于这个虚拟网络的所有关键信息和功能:
- 配置管理: 它持有从网络控制器(例如 my.zerotier.com)获取的所有配置信息。这包括网络名称、IP 地址分配规则、路由规则以及最重要的——访问控制规则。
- 数据包过滤: 这是
Network最核心的职责。每当有数据包想要离开或进入这个虚拟网络时,Network都会像一个严格的保安一样,根据配置好的规则集(我们称之为NetworkConfig)来检查这个数据包。只有符合规则的数据包才会被放行。 - 状态维护: 它跟踪网络的当前状态,比如是否成功连接、配置是否最新等。
一个节点 (Node)可以同时加入多个不同的虚拟网络,并且每个网络都由一个独立的 Network 对象管理,它们之间是完全隔离的,就像你同时在多个不同的线上办公室里一样,一个办公室里的谈话不会被另一个办公室听到。
代码中的 Network
让我们看看 Network 在代码中是如何定义的。它是一个 C++ 类,你可以在 node/Network.hpp 文件中找到它的声明。
// 文件: node/Network.hpp
class Network
{
// ... (为简化起见,省略了部分成员)
private:
const uint64_t _id; // 网络的唯一ID (例如: 8056c2e21c000001)
NetworkConfig _config; // 网络的所有配置信息,包括规则
MAC _mac; // 这个节点在此网络中的虚拟MAC地址
// ...
public:
// 构造函数:当一个节点加入网络时调用
Network(const RuntimeEnvironment *renv, void *tPtr, uint64_t nwid, void *uptr, const NetworkConfig *nconf);
// 过滤发出的数据包
bool filterOutgoingPacket(
void *tPtr,
const bool noTee,
// ... (其他参数)
);
// 过滤收到的数据包
int filterIncomingPacket(
void *tPtr,
const SharedPtr<Peer> &sourcePeer,
// ... (其他参数)
);
};
_id: 一个64位的整数,是网络的唯一标识符。_config: 一个NetworkConfig对象,它就是我们前面提到的“规章制度”,包含了所有路由、成员、和访问控制规则。_mac: 每个节点在虚拟网络中都有一个虚拟的网卡,这个网卡需要一个 MAC 地址。ZeroTier 会根据节点的身份 (Identity)和网络ID为它生成一个独一无二的虚拟 MAC 地址。filterOutgoingPacket和filterIncomingPacket: 这两个函数是Network的核心,负责执行数据包的过滤和检查。
Network 的工作流程:数据包过滤
想象一下,你的电脑(IP为 10.0.0.1)想要 ping 同一个虚拟网络里的另一台服务器(IP为 10.0.0.2)。这个过程在 ZeroTier 内部是如何运作的呢?
下面是一个简化的流程图:
sequenceDiagram
participant App as 你的应用程序 (如 ping)
participant Node as ZeroTier 节点
participant Net as Network 对象
participant Rules as 网络规则 (NetworkConfig)
participant Internet as 物理网络
App->>Node: 发送数据包 (目标: 10.0.0.2)
Node->>Net: 调用 filterOutgoingPacket(数据包)
Net->>Rules: 这个数据包是否符合规则?
Rules-->>Net: 允许 (ACCEPT)
Net-->>Node: 数据包已允许
Node->>Internet: 将数据包加密并发送给目标节点
- 你的应用程序(如
ping命令)创建了一个数据包,希望发送到10.0.0.2。 - 操作系统将这个数据包交给 ZeroTier 的虚拟网卡。
- 节点 (Node) 捕获到这个数据包,并找到它所属的
Network对象。 - 节点调用该
Network对象的filterOutgoingPacket()方法。 filterOutgoingPacket()方法会调用一个内部的辅助函数_doZtFilter(),这个函数是规则引擎的核心。它会逐一检查_config中定义的规则。_doZtFilter()发现一条规则说“允许所有ICMP(ping命令使用的协议)流量”,于是返回“允许”的结果。filterOutgoingPacket()接收到“允许”的结果,并告知节点。- 节点随后将这个数据包加密,通过真实的互联网发送给
10.0.0.2所在的对等节点 (Peer)。
深入代码:规则匹配
_doZtFilter 函数是整个规则系统的核心。它位于 node/Network.cpp 文件中,通过一个大型的 switch 语句来匹配各种规则类型。
让我们看一个最简单的规则匹配示例:ZT_NETWORK_RULE_MATCH_ETHERTYPE。这个规则用来匹配数据包的以太网类型(例如,IPv4、IPv6、ARP)。
// 文件: node/Network.cpp
// 在 _doZtFilter 函数内部
// ... (此处省略了循环和上下文代码)
// rt 是当前规则的类型
switch(rt) {
// ...
case ZT_NETWORK_RULE_MATCH_ETHERTYPE:
// 检查数据包的 etherType 是否与规则中定义的值完全相同
thisRuleMatches = (uint8_t)(rules[rn].v.etherType == (uint16_t)etherType);
break;
// ... 其他规则匹配
}
这段代码非常直观。它只是简单地比较数据包的 etherType 和规则中定义的 etherType 是否相等。如果相等,thisRuleMatches 就被设为 true。
规则集是一系列“匹配(MATCH)”和“动作(ACTION)”的组合。例如,一个典型的规则组合可能是:
- MATCH
etherTypeis IPv4. - MATCH destination IP is
10.0.0.5. - ACTION
DROP(丢弃数据包).
只有当所有的 MATCH 条件都满足时,紧随其后的 ACTION 才会被执行。如果任何一个 MATCH 不满足,这个动作就会被跳过,继续处理下一组规则。如果所有规则都处理完仍没有明确的 ACCEPT 或 DROP,默认行为通常是丢弃数据包。
总结
在本章中,我们了解了 ZeroTier 中最基础也是最重要的概念之一:虚拟网络 (Network)。
Network就像一个安全的、私有的线上办公室,它让分散在各地的设备能够像在同一个局域网中一样通信。- 每个
Network都有一个唯一的 ID,并包含一套配置 (NetworkConfig),其中定义了成员资格和通信规则。 Network对象的核心职责是过滤数据包,确保只有符合规则的流量才能在网络中传输。
Network 为我们构建了一个安全、隔离的通信环境。但是,是谁来加入这些网络呢?在下一章中,我们将深入探讨代表着你设备本身的实体——节点 (Node)。