- 操作系统
- 云原生
- 虚拟化
- 网络
【免费下载链接】IncludeOS
A minimal, resource efficient unikernel for cloud services
本篇文章围绕 IncludeOS 集成测试套件中router6(IPv6 路由测试)的网络拓扑图 diagram.md 展开,完整剖析该测试如何用两个 Linux 网桥、一对 veth 与一个网络命名空间,将一台运行 IncludeOS unikernel 的双网口"路由器"接入两端 IPv6 网络。读完本文,你将掌握该测试拓扑中每个网元与地址的职责、setup.sh 的建网/清理脚本逻辑、Router<IP6>路由表与转发回调的源码级原理,以及如何基于 NDP 邻居发现完成跨网段 UDP 连通性验证。
一、测试目标与整体拓扑
router6是 test/integration/net/router6 目录下的 IPv6 路由集成测试,其目标与 IPv4 版 router 测试 对称:让一台拥有两个网口的 IncludeOS 虚拟机扮演路由器,把包从一侧网络转发到另一侧网络,再借助 test.py 中的 iperf3 流量验证转发路径。
拓扑图 diagram.md 用 ASCII 勾勒了全部关键网元(为便于阅读,本文将其整理为结构化表格):
| 网元 | 角色 | 地址 / 关键属性 |
|---|---|---|
| IncludeOS router | 被测试的双网口 unikernel 路由器 | eth0/eth1两个 virtio 网卡 |
| Bridge43 (Linux) | eth0 侧接入网桥(TAP 设备) | fe80::e823:fcff:fef4:83e7 |
| Bridge44 (Linux) | eth1 侧接入网桥(TAP 设备) | 无 IP(纯二层转发) |
| veth0 | 连接 Bridge44 的 veth 一端 | 无 IP |
| veth1 | 位于命名空间 server1 内的 veth 另一端 | fe80::abcd:abcd:1234:8367(测试终点) |
| Network Namespace 0 | 根网络命名空间(发起端) | 通过 bridge43 可达路由器 |
| Network Namespace 1 | 隔离的server1命名空间(终点) | 承载 veth1 |
测试连通性的两条命令写在拓扑图底部:
- 发起端(根命名空间,经 bridge43):
nc fe80:0:0:0:abcd:abcd:1234:8367 -u 4242 - 终点(命名空间 server1 内):
nc -u -l fe80:0:0:0:abcd:abcd:1234:8367 4242
也就是说,UDP 数据报从根命名空间经 bridge43 进入 IncludeOS 路由器的 eth0,被路由表命中后从 eth1 转出,经 bridge44、veth0 抵达命名空间内的 veth1 上的目标地址。整条链路中,链路本地地址(fe80::/64前缀 + 112 位掩码)承担了全部寻址职责。
二、setup.sh:一步步搭建两桥一命名空间的 IPv6 环境
setup.sh 是一个无参数即执行setup()的脚本,入口逻辑如下:
if [ "$1" == "--clean" ] then undo elif [ "$1" == "--vmsetup" ] then vmsetup else setup fi脚本顶部定义了本次拓扑的核心常量:
source_net=fe80:0:0:0:e823:fcff:fef4:0/112 # 发起端网段(路由器 eth0 侧) source_bridge=bridge43 # 发起端网桥(须预先存在) dest_net=fe80:0:0:0:abcd:abcd:1234:0/112 # 终点网段(路由器 eth1 侧) dest_bridge=bridge44 # 终点侧新建网桥 dest_gateway=fe80:0:0:0:abcd:abcd:1234:8367 # 终点主机地址(veth1) if1=tap0 # VM 网卡 1 的 TAP 名 if2=tap1 # VM 网卡 2 的 TAP 名 export NSNAME="server1" # 网络命名空间名setup()的执行步骤与拓扑图的对应关系:
- 创建 veth 对:
sudo ip link add veth_src type veth peer name veth_dest,对应图中 veth0/veth1; - 拉起源端:
sudo ip link set veth_src up; - 创建命名空间:
sudo ip netns add server1,并将veth_dest移入其中(ip link set veth_dest netns $NSNAME),这就把终点主机隔离到了 Network Namespace 1; - 配置终点地址:在命名空间内
ip -6 addr add $dest_gateway dev veth_dest,即把fe80::abcd:abcd:1234:8367赋给 veth1,同时拉起veth_dest与lo; - 新建 bridge44 并加入 veth_src:
ip link add name $dest_bridge type bridge,随后ip link set dev veth_src master $dest_bridge,对应图中 Bridge44 ↔ veth0 的连线; - 双向路由注入:在根命名空间写入
ip -6 route add $dest_net dev $source_bridge(发往终点网段的流量一律走 bridge43 进入路由器),在命名空间内写入server1 ip -6 route add $source_net dev veth_dest(回程流量走 veth_dest 返回根命名空间)。
undo()提供了对称的清理逻辑,且刻意set +e保证"即使某条命令失败也继续执行全部清理命令",依次删除 veth、bridge44、命名空间与注入的路由。
vmsetup()则服务于测试运行时序:当 VM 启动输出标志后(见下文 test.py),把 VM 的第二个网卡 TAP(tap1)从默认网桥摘除(nomaster)再挂到 bridge44(master $dest_bridge),从而将路由器 eth1 动态接入终点侧网络。
三、service.cpp:Router 的路由表与转发回调
service.cpp 是路由器固件的核心。它先为两个网口配置 IPv6 地址:
auto& inet = Interfaces::get(0); inet.add_addr({"fe80::e823:fcff:fef4:85cd"}, 112); // eth0 侧地址 auto& inet2 = Interfaces::get(1); inet2.add_addr({"fe80::abcd:abcd:1234:5678"}, 112); // eth1 侧地址注意两处被注释的 NDP 配置inet.ndp().add_router({"fe80::e823:fcff:fef4:83e7"}, 0xffff),它们的注释状态意味着本测试不通过 NDP 路由器通告(RA)下发默认路由,而是完全依赖显式路由表——这正是拓扑图中两台 Linux 网桥地址只承担链路本地角色、不充当"路由通告者"的原因。
3.1 手工转发回调 ip_forward
与直接复用Router::forward_delg()的通用做法不同,该测试自己实现了一个转发回调ip_forward,签名与 IP6.hpp 中的 Forward_delg 一致:
static void ip_forward (IP6::IP_packet_ptr pckt, Inet& stack, Conntrack::Entry_ptr) { Inet* route = router->get_first_interface(pckt->ip_dst()); if (not route) { /* 无路由则丢弃 */ return; } if (route == &stack) { /* 目标就是自己,拒绝再转发 */ return; } route->ip6_obj().ship(std::move(pckt)); }它调用Router::get_first_interface()依据目的地址选出出口网口,然后直接ship到对应接口的 IP6 对象。两个网口都通过set_packet_forwarding(ip_forward)注册了该回调。
3.2 路由表定义
Router<IP6>::Routing_table routing_table{ {{0xfe80, 0, 0, 0, 0xabcd, 0xabcd, 0x1234, 0 }, 112, {0xfe80, 0, 0, 0, 0xabcd, 0xabcd, 0x1234, 0x8367}, inet2, 1 }, {{ 0xfe80, 0, 0, 0, 0xe823, 0xfcff, 0xfef4, 0}, 112, {0xfe80, 0, 0, 0, 0xe823, 0xfcff, 0xfef4, 0x83e7}, inet, 1 } }; router = std::make_unique<Router<IP6>>(routing_table);每条Route由五元组构成(对应 router.hpp 中 Route 的构造函数):目标网段 net_、前缀长度 netmask_、下一跳 nexthop_、出口接口 iface_、开销 cost_。两条路由分别把fe80::abcd:abcd:1234:0/112指向inet2、fe80::e823:fcff:fef4:0/112指向inet,cost 均为 1。启动日志会遍历路由表打印:
INFO2("* %s/%i -> %s / %s, cost %i", r.net().str().c_str(), __builtin_popcount(r.netmask()), // 由前缀长度换算掩码中的置位位数 r.interface()->ifname().c_str(), r.nexthop().to_string().c_str(), r.cost());3.3 NDP 路由检查器
set_route_checker6(route_checker)注册的回调在源码中实际落到了 NDP 层:
// api/net/inet.hpp#L213 void set_route_checker6(Route_checker6 delg); // src/net/inet.cpp#L405-L406 void Inet::set_route_checker6(Route_checker6 delg) { ndp_.set_proxy_policy(delg); }其语义是:当收到针对本接口可达网段中某个地址的邻居请求(Neighbor Solicitation)时,询问路由器"我是否有到该 IP 的路由",有则代答(NDP 代理/Proxy NDP),无则拒绝。测试中的route_checker回调实现为:
bool have_route = router->route_check(addr); // 查路由表是否存在出口 INFO2(have_route ? "* Responding YES" : "* Responding NO");这保证终点侧(fe80::abcd:abcd:1234:8367)即使与路由器 eth1 不在同一前缀(veth1 处于独立命名空间),也能通过代理应答完成邻居发现。
四、Router 源码级转发原理
api/net/router.hpp 中的Router<IP6>模板类提供了完整的最长前缀匹配(LPM)路由能力,测试中用到的几个关键接口:
route.match(dest):(dest & netmask_) == net_即命中(router.hpp#L61-L62);get_first_interface(dest):遍历路由表返回第一个命中路由的出口接口(router.hpp#L139-L143);route_check(dest):get_first_interface(dest) != nullptr,判断是否存在路由(router.hpp#L146-L148);get_most_specific_route(dest):遍历全部命中路由,选前缀最长者——这正是标准路由器的最长前缀匹配策略(router.hpp#L192-L206)。
模板还内置了Router<IP6>::forward()完整转发流程(router.hpp#L329-L370),与测试中手工实现的ip_forward思路一致但更完整:校验hop_limit为零则按 RFC 1812 发送 ICMPv6 Time Exceeded 并丢弃;非本机源地址的包递减 hop limit;过 forward 过滤链(forward_chain,用于防火墙/NAT 挂载);最后get_most_specific_route选路并route->ship()。
Route<IP6>::ship与nexthop的特化(router.hpp#L253-L276)体现了关键优化:若目的地址与出口接口同网段,则下一跳直接取目的地址本身,否则才走路由表里的 nexthop:
template<> inline IP6::addr Route<IP6>::nexthop(IP6::addr ip) const noexcept { if ((ip & iface_->netmask6()) == (iface_->ip6_addr() & iface_->netmask6())) return ip; // 同链直接投递 return nexthop_; // 否则经下一跳 }在数据面,IP6 层的 receive() 在确认"目的地址不是本机"(is_for_me失败)后,若注册了forward_packet_就把包交给转发回调,否则直接以Bad_destination理由丢弃;transmit() 对出方向包同样先经forward_packet_再决定是否直接ship。这与测试中"eth0 收包 → ip_forward → eth1 发包"的路径一一对应。
另外,Router构造函数通过 Statman 注册了三个计数器:router.packets_fwd、router.packets_dropped、router.bytes_fwd(router.hpp#L212-L214),便于在运行时观测转发/丢弃统计。
五、test.py 与 vm.json:自动化验证链路
test.py 用 vmrunner 驱动整个测试:
iperf_cmd = "iperf3" transmit_size = "100M" nsname = "server1" def move_tap1(o): # 检测到 "Routing test" 输出后 subprocess.call(["./setup.sh", "--vmsetup"]) # 把 tap1 挂到 bridge44 def iperf_server(): # 命名空间内立即启动 iperf3 服务端 subprocess.Popen(["sudo","ip","netns","exec", nsname, iperf_cmd, "-s"], ...) def iperf_client(o): # 检测到 "Service ready" 后发起客户端 subprocess.check_output([iperf_cmd,"-c","fe80:0:0:0:abcd:abcd:1234:8367%bridge43", "-n", transmit_size]) vmrunner.vms[0].exit(0, "Test completed without errors")执行顺序是:先setup.sh --clean清理残留 →setup.sh建网 → 启动 VM(net_router6.elf.bin)→ 在 VM 打印 "Routing test ipv6" 时执行--vmsetup接入第二网桥 → 命名空间内 iperf3 服务端就绪后,客户端经%bridge43的 zone 限定语法以链路本地地址fe80::abcd:abcd:1234:8367发起 100M 传输 → 成功则退出码 0,退出时统一执行clean()(pkill iperf3 +setup.sh --clean)。
vm.json 声明了虚拟机规格:
{ "image": "test_router.img", "mem" : 320, "net" : [{"device" : "virtio"}, {"device" : "virtio"}], "intrusive": "True", "time_sensitive" : "True" }两个 virtio 网卡即拓扑图中的 eth0/eth1,320MB 内存足以运行带完整 TCP/IP 协议栈的 unikernel 路由器。构建侧,CMakeLists.txt 通过os_add_executable(net_router6 "Routing test ipv6" service.cpp)生成固件,并os_add_drivers(net_router6 virtionet)挂载 virtio 网卡驱动、configure_file把 test.py/setup.sh 复制到构建目录供 vmrunner 调用。
六、运行方式、已知限制与 IPv4 对照
运行该测试前需满足前置条件:
- 已按 README.md 完成 IncludeOS 构建,具备 QEMU/vmrunner 环境;
- 已存在名为
bridge43的网桥(发起端侧,脚本假定其预先就绪); - 宿主机装有
iperf3(测试流量工具)、支持sudo ip netns的 Linux 网络命名空间能力; - 需要 root 权限执行 setup 脚本。
测试入口与 IPv4 版一致,可在构建目录中运行:
python3 test.py # 或显式指定固件镜像 python3 test.py path/to/net_router6.elf.bin值得注意的已知限制记录在 README.md:
Depends on iperf3. TODO: It currently seems to time out.
即该测试依赖 iperf3,且目前存在 iperf 传输超时的问题(TODO 状态)。因此本文所述拓扑与代码逻辑是完整可用的参考实现,但实际执行时应以当前仓库状态为准,超时现象可能与宿主机网桥/命名空间时序或 unikernel 侧转发吞吐有关,可先对照 router(IPv4) 的等价实现排查——两者结构完全对称:
| 对比项 | router(IPv4) | router6(IPv6) |
|---|---|---|
| 地址配置 | network_config配置 IPv4 + 网关 | add_addr配置链路本地 IPv6 |
| 路由检查器 | set_route_checker→ ARP 代理 | set_route_checker6→ NDP 代理 |
| 终点地址 | 10.42.42.2 | fe80::abcd:abcd:1234:8367 |
| 测试流量 | iperf3 +nc -u命令示例 | 同为 iperf3 +nc -u命令示例 |
七、小结
diagram.md 用一张拓扑图浓缩了 IncludeOS IPv6 路由器测试的全部要素:双 virtio 网口的 unikernel、两个 Linux 网桥(bridge43/bridge44)、一对 veth 和一个server1命名空间,配合 setup.sh 的地址注入与 service.cpp 的Router<IP6>路由表/NDP 代理,实现了从nc fe80::abcd:abcd:1234:8367 -u 4242到nc -u -l的跨网段 UDP 连通。理解这张图,也就掌握了 IncludeOS 上"双接口路由"的标准测试范式:数据面靠set_packet_forwarding+ 路由表选路,控制面靠 NDP 代理完成跨链路的邻居解析,这两条线索在 router.hpp、ip6.cpp 与 inet.cpp 中都有清晰的源码落点。
- 操作系统
- 云原生
- 虚拟化
- 网络
【免费下载链接】IncludeOS
A minimal, resource efficient unikernel for cloud services
相关推荐
IncludeOS 路由器集成测试:双桥接 + 网络命名空间拓扑搭建与 IP 转发实现解析
IncludeOS 路由器集成测试:双桥接 + 网络命名空间拓扑搭建与 IP 转发实现解析 本篇以 IncludeOS 仓库中的 test/integratio
操作系统云原生虚拟化网络ProxySQL mysql_router 插件:以 MySQL Router 兼容实例身份接管 InnoDB Cluster 拓扑路由
ProxySQL mysql_router 插件:以 MySQL Router 兼容实例身份接管 InnoDB Cluster 拓扑路由 本指南围绕仓库 doc
后端数据库负载均衡IncludeOS 网关集成测试指南:路由、NAT 防火墙与 VLAN 转发的实战验证
IncludeOS 网关集成测试指南:路由、NAT 防火墙与 VLAN 转发的实战验证 导读 test/integration/net/gateway 是 In
操作系统云原生虚拟化网络
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考