Linux 网络内核协议栈深度剖析(八):网络虚拟化与容器网络实现
容器网络是现代云原生基础设施的底座。Docker、Kubernetes 依赖 Linux 内核提供的四大网络虚拟化原语——Network Namespace、veth pair、Linux Bridge、VXLAN——把跑在同一台物理机或跨越多台主机的容器连接成一张逻辑网络。本文基于 Linux 6.4-rc1 源码,逐层拆解这四个模块的内核实现,并在最后串联出一条完整的容器间数据包路径。
一、网络命名空间(Network Namespace)
1.1 struct net:命名空间的核心数据结构
每一个网络命名空间都对应内核中的一个 struct net 实例。它持有独立的设备列表、路由表、协议族上下文,是整个网络子系统多租户隔离的根基。来自 include/net/net_namespace.h(Linux 6.4-rc1)的关键字段:
1 | /* include/net/net_namespace.h */ |
__randomize_layout 是内核的结构体布局随机化标注,用于对抗内核结构体偏移泄漏攻击。ipv4.fib_main 就藏在 struct netns_ipv4 里,指向本命名空间的主路由表。每个 namespace 拥有完全独立的路由表,这正是容器网络隔离的关键:两个 namespace 内的相同目的地址可以指向完全不同的下一跳。
全系统初始的 namespace 是静态定义的 init_net(net/core/net_namespace.c 第 48 行):
1 | struct net init_net; |
所有未显式进入其他 namespace 的进程和设备都归属于 init_net,包括系统启动阶段的物理网卡。
1.2 copy_net_ns:创建新命名空间
调用 unshare(CLONE_NEWNET) 或 clone(CLONE_NEWNET) 时,内核最终调用 copy_net_ns(net/core/net_namespace.c,第 462 行):
1 | struct net *copy_net_ns(unsigned long flags, |
setup_net 的核心工作是遍历全局 pernet_list 链表,依次调用每个子系统通过 register_pernet_subsys 或 register_pernet_device 注册的 .init 回调。IPv4 子系统在 .init 中创建独立的 FIB 路由表、本地路由表;网络设备子系统创建 lo 回环设备;netfilter 子系统初始化独立的 hook 表。这些工作完成后,新 namespace 拥有一个功能完整但彼此隔离的网络环境。
1.3 按 PID / FD 查找命名空间
内核对外暴露了两个查找接口(net/core/net_namespace.c),分别用于 ip netns exec 和 setns 系统调用:
1 | /* 通过 /proc/<pid>/ns/net 文件描述符查找 */ |
ip netns exec 命令在底层先 open("/run/netns/<name>") 获得 fd,再调用 setns(fd, CLONE_NEWNET) 切换命名空间,内核路径即为 get_net_ns_by_fd。命名空间的 fd 本质上是 procfs 下的 inode,内核把 struct net 的 ns_common 嵌入其中,形成 fd -> inode -> ns_common -> struct net 的完整引用链。
1.4 协议栈如何感知 namespace
每个 socket 都通过 sk->sk_net(实际上是 struct sock 中嵌入的 struct net 引用,通过 sock_net(sk) 访问)绑定到特定的 struct net。发包时,路由查找调用 sock_net(sk) 取出 namespace 指针,再在该 namespace 的 ipv4.fib_main 路由表里查找下一跳;收包时,netif_receive_skb 通过 dev->nd_net(即 dev_net(dev))定位到设备所属的 namespace,把报文递给正确的协议族上下文。这条绑定关系贯穿整个网络路径,实现了完全的隔离。
容器网络中,veth pair 的两端可以分别归属于不同的 namespace——宿主机侧的 veth 端口在 init_net 中,而容器侧的端口在容器的 namespace 中。内核在 veth_newlink 创建设备对时,通过 rtnl_link_get_net 解析 IFLA_NET_NS_PID 或 IFLA_NET_NS_FD 属性,将 peer 设备注册到目标 namespace,实现跨 namespace 的虚拟网线。
二、veth 虚拟以太网对
2.1 数据结构
veth 设备以一对的形式存在。每个 veth 设备的私有数据 struct veth_priv 只有一个最核心的字段——对端设备指针(drivers/net/veth.c,第 72 行):
1 | struct veth_priv { |
每条接收队列 struct veth_rq 含有一个 NAPI 调度器 xdp_napi、一个 ptr_ring(XDP 的环形队列)、per-CPU 统计 veth_rq_stats,以及 XDP 内存信息 xdp_mem。这套结构为 XDP offload 和 GRO 聚合提供了完整的基础。
2.2 veth_xmit:发送路径的精髓
veth 的发送函数极其简洁——它不进入真正的硬件队列,而是直接把 skb 递交给对端设备(drivers/net/veth.c,第 333 行):
1 | static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev) |
veth_forward_skb(第 306 行)是最终把 skb 推给对端的函数:
1 | static int veth_forward_skb(struct net_device *dev, struct sk_buff *skb, |
__dev_forward_skb 负责修正 skb->dev、处理 pkt_type(如跨命名空间时重置为 PACKET_HOST)。随后:
- 若不使用 NAPI:调用
__netif_rx(skb),走软中断路径上送协议栈; - 若使用 NAPI/XDP:调用
veth_xdp_rx(rq, skb),把 skb 放入ptr_ring,由 NAPI poll 批量处理。
整个发送过程没有内存拷贝,没有 DMA,没有中断——就是一次函数调用链。这是 veth 吞吐量接近回环设备的根本原因,在 100Gbps 场景下表现尤为突出。
2.3 veth_newlink:通过 rtnetlink 创建 veth 对
ip link add veth0 type veth peer name veth1 触发的内核路径是 RTM_NEWLINK → rtnl_newlink → veth_newlink(第 1832 行)。该函数的工作流程如下:
- 解析
VETH_INFO_PEER嵌套属性,获取 peer 端的名称和 namespace; - 调用
rtnl_create_link先注册 peer 设备(veth1); - 调用
eth_hw_addr_random为两端分别生成随机 MAC 地址; - 调用
register_netdevice(peer)和register_netdevice(dev)注册两端; - 最后通过
rcu_assign_pointer(priv->peer, peer)建立互指关系:
1 | priv = netdev_priv(dev); |
两端设备可以分属不同的 namespace(通过 IFLA_NET_NS_PID 或 IFLA_NET_NS_FD 指定),实现跨 namespace 的虚拟以太网线。
2.4 GRO 与 XDP 支持
veth_xdp_xmit(第 470 行)是 XDP redirect 场景下的发送路径,将 xdp_frame 数组批量写入对端的 ptr_ring,一次调用可发送多帧,大幅降低 per-packet 开销。GRO 聚合的判断逻辑在 veth_skb_is_eligible_for_gro:若发送方没有 TSO 且接收方开启了 NETIF_F_GRO_FRAGLIST 或 NETIF_F_GRO_UDP_FWD,则通过 NAPI 聚合小包,显著提升大流量场景的吞吐。
对 veth_netdev_ops 的完整声明(第 1725 行)可以看出 veth 支持的全部操作:
1 | static const struct net_device_ops veth_netdev_ops = { |
三、Linux Bridge(二层交换)
3.1 收包入口:br_handle_frame
Linux Bridge 通过在每个成员端口的 net_device 上注册 rx_handler 来截获二层帧。当报文从 veth/物理网卡到达时,__netif_receive_skb_core 检测到已注册的 rx_handler,调用 br_handle_frame(net/bridge/br_input.c,第 320 行):
1 | static rx_handler_result_t br_handle_frame(struct sk_buff **pskb) |
STP 的核心逻辑体现在 p->state 的判断:处于 FORWARDING 或 LEARNING 状态的端口允许帧继续处理;BLOCKING 状态的端口直接丢包,阻断二层环路。STP BPDU(目的 MAC 01:80:C2:00:00:00)需要上送到生成树协议守护进程处理,不能直接转发,因此走 RX_HANDLER_PASS 路径绕过 bridge 层。
3.2 br_handle_frame_finish:MAC 学习与转发决策
通过 netfilter 桥前向钩子(NF_BR_PRE_ROUTING)后,报文进入 br_handle_frame_finish(第 74 行)。此函数完成两件核心工作:
源 MAC 学习:
1 | if (p->flags & BR_LEARNING) |
目的 MAC 查找与转发:
1 | case BR_PKT_UNICAST: |
这个逻辑精确映射了物理以太网交换机的行为:已知单播直接转发,未知单播泛洪,广播和组播依各自规则处理。
3.3 br_forward:报文转发到出端口
br_forward(net/bridge/br_forward.c,第 144 行)负责把 skb 转发到指定端口:
1 | void br_forward(const struct net_bridge_port *to, |
__br_forward 处理 VLAN 出方向标签(br_handle_vlan),将 skb->dev 设置为目标端口设备,经 NF_BR_FORWARD 钩子后调用 br_dev_queue_push_xmit,最终走 dev_queue_xmit 发出。对于 veth 对端,这就意味着报文重新进入 veth_xmit,穿越 veth 进入容器的网络命名空间。
3.4 FDB:rhashtable 实现的 MAC 地址表
Bridge FDB 使用内核的 rhashtable(可自动扩缩容的无锁哈希表)。查找键由 (MAC地址, VLAN ID) 组成,对应 struct net_bridge_fdb_key(net/bridge/br_fdb.c,第 215 行):
1 | static struct net_bridge_fdb_entry *fdb_find_rcu(struct rhashtable *tbl, |
哈希参数在模块初始化时固定:
1 | static const struct rhashtable_params br_fdb_rht_params = { |
automatic_shrinking = true 保证在 MAC 条目大量删除后哈希表会自动收缩,避免内存浪费。
3.5 br_fdb_update:动态 MAC 学习
每收到一帧,br_fdb_update 都会以源 MAC 为键更新 FDB(第 853 行):
1 | void br_fdb_update(struct net_bridge *br, struct net_bridge_port *source, |
读路径(fdb_find_rcu)全程在 RCU 读锁保护下进行,不需要加锁;写路径(新建条目)需要 br->hash_lock 自旋锁。这种读写锁分离的设计保证了在高吞吐收包场景下 FDB 查找的零竞争。
老化定时器(gc_work)周期性地清理 updated + hold_time < jiffies 的动态条目,默认老化时间 300 秒(STP topology change 期间降为 15 秒)。
四、VXLAN 隧道
4.1 核心数据结构
VXLAN 在内核中以虚拟网络设备的形态存在。关键结构定义于 include/net/vxlan.h:
1 | /* per UDP socket information */ |
vxlan_sock 是多个 vxlan_dev 共享的 UDP socket 抽象——当多个 VXLAN 设备使用相同的本地端口时,内核只创建一个 vxlan_sock,通过 VNI 区分不同的 overlay 网络。VXLAN FDB(struct vxlan_fdb,定义于 drivers/net/vxlan/vxlan_private.h)以内层 MAC 地址为键,映射到一组 struct vxlan_rdst(远端 VTEP 列表),支持多路径和组播。
4.2 vxlan_xmit:封装发送
vxlan_xmit(vxlan_core.c,第 2680 行)是 vxlan 设备的 ndo_start_xmit。它在 VXLAN FDB 中查找内层目的 MAC,找到对应的远端 VTEP 后,调用 vxlan_xmit_one 完成封装:
1 | static netdev_tx_t vxlan_xmit(struct sk_buff *skb, struct net_device *dev) |
vxlan_xmit_one(第 2377 行)负责真正的封装:查路由获取 rtable,调用 vxlan_build_skb 在 skb 头部预留空间并填充 VXLAN 头(8 字节),最终通过:
1 | udp_tunnel_xmit_skb(rt, sock4->sock->sk, skb, |
把内层以太帧包裹在 UDP/IP 外层头中,通过 VTEP 的物理网卡发送出去。外层 UDP 源端口由 udp_flow_src_port 基于内层报文的流哈希(5 元组)计算,保证同一条流的所有报文走相同的 ECMP 路径,实现负载均衡。
4.3 vxlan_rcv:解封装接收
对端 VTEP 上的 vxlan_rcv(第 1615 行)作为 UDP socket 的 encap_rcv 回调被调用:
1 | static int vxlan_rcv(struct sock *sk, struct sk_buff *skb) |
解封后,gro_cells_receive 将内层报文送回软中断的 GRO 处理路径,最终进入 netif_receive_skb,被对端节点上的 Bridge/veth 继续处理。vxlan_set_mac 在解封时同步学习了源 VTEP IP,使得后续反向流量可以直接走 VXLAN FDB 查找而无需泛洪,这是 VXLAN 数据面自学习(data-plane learning)的关键机制。
五、容器网络全链路:从容器 A 到容器 B
以下以 Kubernetes CNI Flannel(VXLAN 模式)的典型部署为例,追踪一个 TCP 数据包的完整路径。集群节点上有两条 veth pair,容器侧端口分属各自的 namespace,宿主机侧端口接入 cni0 网桥;节点间通过 VXLAN 设备 flannel.1 互联。
5.1 同节点通信
1 | Pod A eth0 (10.244.1.2/24, ns: podA-netns) |
关键步骤说明:
- Pod A 调用
write(),TCP 发包,查本地路由:目的10.244.1.3在同子网10.244.1.0/24,走eth0(即veth_a_c)。 - ARP 解析后,skb 经
veth_xmit由容器 namespace 穿越到宿主机 namespace,到达veth_a_h。 veth_a_h注册了rx_handler = br_handle_frame,网桥收包,br_fdb_update学习 Pod A 的 MAC。br_fdb_find_rcu以 Pod B 的 MAC + VLAN 0 为键查哈希表,找到veth_b_h端口。br_forward→__br_forward→dev_queue_xmit(skb)→veth_xmit(veth_b_h)。- veth 再次穿越 namespace,报文进入 Pod B 的网络命名空间,
__netif_rx上送 TCP 栈。
整个过程全部在内核态完成,两次 veth 穿越无内存拷贝,一次 bridge FDB 查找 O(1),延迟极低。
5.2 跨节点通信(VXLAN 路径)
1 | 节点1: |
跨节点路径的核心在于:
- 封装侧(节点 1):
vxlan_xmit_one查路由、填充外层 IP(192.168.1.1 → 192.168.1.2)、VXLAN 头(VNI=1)、UDP 头(src_port 基于内层流哈希,dst_port=8472),调用udp_tunnel_xmit_skb发出。 - 解封装侧(节点 2):
vxlan_rcv验证 VNI 有效性、调用__iptunnel_pull_header剥离外层,恢复内层以太帧,通过gro_cells_receive重新入栈,后续路径与同节点通信完全相同。
值得注意的是,VXLAN FDB 的学习发生在解封装时(vxlan_set_mac),而非发包时。这意味着第一个跨节点数据包需要触发 FDB miss 泛洪(发往默认多播组或配置的默认 VTEP),后续反向流量才能利用已学习的 FDB 条目精确转发,避免泛洪。
六、macvlan 与 ipvlan
6.1 macvlan 原理
macvlan 允许在一块物理网卡上创建多个虚拟接口,每个接口拥有独立的 MAC 地址,但共享同一个物理端口。macvlan_port 维护了一张以 MAC 地址为键的哈希表(drivers/net/macvlan.c,第 43 行):
1 | struct macvlan_port { |
收包路径同样通过 rx_handler 截获:macvlan_handle_frame 按目的 MAC 在 vlan_hash 中查找,将帧递交给对应的 macvlan 虚拟设备,分发到不同的 namespace 或容器。发包时,macvlan 设备直接将 skb->dev 替换为下层物理设备(lowerdev),调用 dev_queue_xmit_accel,报文不经 bridge 直接走物理网卡,减少了一次 FDB 查找的开销。
macvlan 有四种工作模式:
- bridge 模式:同一物理接口下的多个 macvlan 接口可以直接二层互通,内核软件桥接;
- vepa 模式(Virtual Ethernet Port Aggregator):所有报文都发往上联交换机,依赖外部交换机的 hairpin 转发实现 macvlan 间通信;
- private 模式:macvlan 接口间完全隔离,即使目的 MAC 匹配也不互转;
- passthru 模式:只允许创建一个 macvlan 接口,并将物理网卡的所有特性透传。
6.2 macvlan vs Linux Bridge
| 维度 | macvlan | Linux Bridge |
|---|---|---|
| 二层隔离方式 | 依赖物理交换机(vepa/private)或软件桥接(bridge 模式) | 纯内核软件交换 |
| 本机容器互通 | bridge 模式可以;vepa/private 模式需外部交换机 | 直接走 bridge 转发,无需外部 |
| 发包路径 | 直接下层 dev,无 FDB 查找 | bridge FDB 查找后出端口 |
| 支持 STP | 不支持 | 支持(防环路) |
| 适用场景 | 容器直接暴露在物理网络,需要与物理主机同子网 | 典型容器网络(docker0、cni0) |
6.3 ipvlan
ipvlan 与 macvlan 的区别在于:所有虚拟接口共享同一个 MAC 地址,用不同的 IP 地址区分流量。内核在二层(L2 模式)或三层(L3 模式)进行分发:
- L2 模式:类似 macvlan,在链路层按目的 IP 查找目标虚拟接口,帧直接递交;支持广播和组播,ARP 正常工作。
- L3 模式:在网络层转发,虚拟接口不参与 ARP,只处理单播 IP;内核直接路由决策,不需要 ARP 和 MAC 学习,更简洁。L3s 模式(L3 strict)进一步隔离了广播域。
ipvlan L3 是 Kubernetes 高密度 Pod 场景的优化选项,因为它避免了 bridge 的泛洪开销,同时规避了 macvlan bridge 模式下本机容器需要通过外部交换机绕行的限制。Cilium 的 eBPF 数据面在不使用 kube-proxy 时,也可以选择 ipvlan 作为底层连接机制,进一步降低延迟。
七、诊断方法
7.1 网络命名空间操作
1 | # 列出所有命名空间(/run/netns 下的命名 namespace) |
7.2 Bridge 诊断
1 | # 查看 FDB 表(MAC 地址学习结果,包含 VLAN) |
7.3 VXLAN 诊断
1 | # 查看 VXLAN 设备详情(VNI、dstport、local VTEP IP、remote) |
7.4 容器网络丢包排查流程
1 | # 第一步:确认接口 UP 状态和物理链路 |
7.5 bpftrace 追踪内核路径
1 | # 追踪 bridge 收包,打印接收端口名 |
总结
Linux 网络虚拟化的架构极为精巧,四个原语各司其职、层次分明:
Network Namespace 通过
struct net把整个网络协议栈完整”克隆”了一份,copy_net_ns触发各子系统的.init回调,保证每个 namespace 拥有独立的设备、路由表和协议族上下文;sock->sk_net则把每个 socket 绑定到具体的 namespace,确保数据路径不出界。veth pair 用最简洁的
rcu_dereference(priv->peer)加一次__netif_rx实现了零拷贝的虚拟以太网线,veth_newlink在 rtnetlink 路径中原子地注册两端设备并建立互指关系,支持跨 namespace 部署。Linux Bridge 以
rx_handler钩子切入二层收包流程,br_handle_frame完成 STP 状态检查和 link-local 地址过滤,br_fdb_update借助rhashtable实现 O(1) 的 MAC 学习与查找,br_forward经 netfilter 钩子发往目标端口,整套实现不超过 2000 行代码却功能完整。VXLAN 以 UDP 封装跨越三层物理网络,
vxlan_xmit_one和vxlan_rcv构成封解装闭环,外层 UDP 源端口的流哈希计算保证了 ECMP 负载均衡,gro_cells_receive使解封装后的内层帧无缝重入 GRO 路径,吞吐量接近直连。
四者叠加,构成了 Docker/Kubernetes 容器网络模型的完整内核实现基础。理解这套机制,不仅能高效排查各类容器网络问题(丢包、延迟、ARP 异常),也是深入理解 eBPF/XDP 容器网络加速(Cilium、Calico eBPF 模式)的必要前提——这些加速方案的本质,是在上述收发包路径的关键节点注入 BPF 程序,绕过部分内核网络层,以换取更低的延迟和更高的吞吐。