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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* include/net/net_namespace.h */
struct net {
refcount_t passive; /* 引用计数,降为0时释放 */
unsigned int dev_base_seq; /* 设备列表版本号,受 rtnl_mutex 保护 */
int ifindex; /* 下一个可用接口索引 */

struct list_head list; /* 全局 net_namespace_list 链表节点 */
struct list_head dev_base_head;/* 本 namespace 的设备链表头 */

struct net_device *loopback_dev; /* 本 namespace 的 lo 设备 */

struct netns_ipv4 ipv4; /* IPv4 子系统上下文(含 fib_main) */
struct netns_ipv6 ipv6; /* IPv6 子系统上下文(可选编译) */

struct hlist_head *dev_name_head;/* 设备名哈希表(按 ifname 索引) */
struct hlist_head *dev_index_head;/* 接口索引哈希表(按 ifindex 索引) */

struct net_generic __rcu *gen; /* per-namespace 扩展数据指针数组 */
u64 net_cookie; /* 不可变的 namespace 唯一标识 */
} __randomize_layout;

__randomize_layout 是内核的结构体布局随机化标注,用于对抗内核结构体偏移泄漏攻击。ipv4.fib_main 就藏在 struct netns_ipv4 里,指向本命名空间的主路由表。每个 namespace 拥有完全独立的路由表,这正是容器网络隔离的关键:两个 namespace 内的相同目的地址可以指向完全不同的下一跳。

全系统初始的 namespace 是静态定义的 init_netnet/core/net_namespace.c 第 48 行):

1
2
struct net init_net;
EXPORT_SYMBOL(init_net);

所有未显式进入其他 namespace 的进程和设备都归属于 init_net,包括系统启动阶段的物理网卡。

1.2 copy_net_ns:创建新命名空间

调用 unshare(CLONE_NEWNET)clone(CLONE_NEWNET) 时,内核最终调用 copy_net_nsnet/core/net_namespace.c,第 462 行):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
struct net *copy_net_ns(unsigned long flags,
struct user_namespace *user_ns, struct net *old_net)
{
struct ucounts *ucounts;
struct net *net;
int rv;

if (!(flags & CLONE_NEWNET))
return get_net(old_net); /* 不需要新 namespace,引用计数加一 */

ucounts = inc_net_namespaces(user_ns); /* 检查 ulimit 限制 */
if (!ucounts)
return ERR_PTR(-ENOSPC);

net = net_alloc(); /* 分配 struct net */
if (!net) { rv = -ENOMEM; goto dec_ucounts; }

preinit_net(net);
refcount_set(&net->passive, 1);
net->ucounts = ucounts;
get_user_ns(user_ns);

rv = down_read_killable(&pernet_ops_rwsem);
if (rv < 0) goto put_userns;

rv = setup_net(net, user_ns); /* 遍历 pernet_list,为每个子系统调用 .init */

up_read(&pernet_ops_rwsem);
if (rv < 0) goto put_userns;
return net;
}

setup_net 的核心工作是遍历全局 pernet_list 链表,依次调用每个子系统通过 register_pernet_subsysregister_pernet_device 注册的 .init 回调。IPv4 子系统在 .init 中创建独立的 FIB 路由表、本地路由表;网络设备子系统创建 lo 回环设备;netfilter 子系统初始化独立的 hook 表。这些工作完成后,新 namespace 拥有一个功能完整但彼此隔离的网络环境。

1.3 按 PID / FD 查找命名空间

内核对外暴露了两个查找接口(net/core/net_namespace.c),分别用于 ip netns execsetns 系统调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/* 通过 /proc/<pid>/ns/net 文件描述符查找 */
struct net *get_net_ns_by_fd(int fd)
{
struct fd f = fdget(fd);
struct net *net = ERR_PTR(-EINVAL);

if (!f.file)
return ERR_PTR(-EBADF);

if (proc_ns_file(f.file)) {
struct ns_common *ns = get_proc_ns(file_inode(f.file));
if (ns->ops == &netns_operations)
net = get_net(container_of(ns, struct net, ns));
}
fdput(f);
return net;
}

/* 通过 PID 查找进程所属的 namespace */
struct net *get_net_ns_by_pid(pid_t pid)
{
struct task_struct *tsk;
struct net *net;

net = ERR_PTR(-ESRCH);
rcu_read_lock();
tsk = find_task_by_vpid(pid);
if (tsk) {
task_lock(tsk);
struct nsproxy *nsproxy = tsk->nsproxy;
if (nsproxy)
net = get_net(nsproxy->net_ns);
task_unlock(tsk);
}
rcu_read_unlock();
return net;
}

ip netns exec 命令在底层先 open("/run/netns/<name>") 获得 fd,再调用 setns(fd, CLONE_NEWNET) 切换命名空间,内核路径即为 get_net_ns_by_fd。命名空间的 fd 本质上是 procfs 下的 inode,内核把 struct netns_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
2
3
4
5
6
7
struct veth_priv {
struct net_device __rcu *peer; /* RCU 保护的对端指针 */
atomic64_t dropped; /* 丢包计数(对端不存在时) */
struct bpf_prog *_xdp_prog; /* 挂载的 XDP 程序 */
struct veth_rq *rq; /* 接收队列数组(支持多队列) */
unsigned int requested_headroom;
};

每条接收队列 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev)
{
struct veth_priv *rcv_priv, *priv = netdev_priv(dev);
struct veth_rq *rq = NULL;
struct net_device *rcv;
int length = skb->len;
bool use_napi = false;
int rxq;

rcu_read_lock();
rcv = rcu_dereference(priv->peer); /* 取出对端 net_device */
if (unlikely(!rcv) || !pskb_may_pull(skb, ETH_HLEN)) {
kfree_skb(skb);
goto drop;
}

rcv_priv = netdev_priv(rcv);
rxq = skb_get_queue_mapping(skb);
if (rxq < rcv->real_num_rx_queues) {
rq = &rcv_priv->rq[rxq];
/* 若对端挂载了 XDP 程序或开启了 GRO,则走 NAPI 路径 */
use_napi = rcu_access_pointer(rq->napi) &&
veth_skb_is_eligible_for_gro(dev, rcv, skb);
}

skb_tx_timestamp(skb);
if (likely(veth_forward_skb(rcv, skb, rq, use_napi) == NET_RX_SUCCESS)) {
if (!use_napi)
dev_lstats_add(dev, length);
} else {
drop:
atomic64_inc(&priv->dropped);
}

if (use_napi)
__veth_xdp_flush(rq);

rcu_read_unlock();
return NETDEV_TX_OK;
}

veth_forward_skb(第 306 行)是最终把 skb 推给对端的函数:

1
2
3
4
5
6
7
static int veth_forward_skb(struct net_device *dev, struct sk_buff *skb,
struct veth_rq *rq, bool xdp)
{
return __dev_forward_skb(dev, skb) ?: xdp ?
veth_xdp_rx(rq, skb) :
__netif_rx(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 场景下表现尤为突出。

ip link add veth0 type veth peer name veth1 触发的内核路径是 RTM_NEWLINKrtnl_newlinkveth_newlink(第 1832 行)。该函数的工作流程如下:

  1. 解析 VETH_INFO_PEER 嵌套属性,获取 peer 端的名称和 namespace;
  2. 调用 rtnl_create_link 先注册 peer 设备(veth1);
  3. 调用 eth_hw_addr_random 为两端分别生成随机 MAC 地址;
  4. 调用 register_netdevice(peer)register_netdevice(dev) 注册两端;
  5. 最后通过 rcu_assign_pointer(priv->peer, peer) 建立互指关系:
1
2
3
4
5
priv = netdev_priv(dev);
rcu_assign_pointer(priv->peer, peer);

priv = netdev_priv(peer);
rcu_assign_pointer(priv->peer, dev);

两端设备可以分属不同的 namespace(通过 IFLA_NET_NS_PIDIFLA_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_FRAGLISTNETIF_F_GRO_UDP_FWD,则通过 NAPI 聚合小包,显著提升大流量场景的吞吐。

veth_netdev_ops 的完整声明(第 1725 行)可以看出 veth 支持的全部操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static const struct net_device_ops veth_netdev_ops = {
.ndo_init = veth_dev_init,
.ndo_open = veth_open,
.ndo_stop = veth_close,
.ndo_start_xmit = veth_xmit,
.ndo_get_stats64 = veth_get_stats64,
.ndo_set_rx_mode = veth_set_multicast_list,
.ndo_set_mac_address = eth_mac_addr,
.ndo_get_iflink = veth_get_iflink,
.ndo_fix_features = veth_fix_features,
.ndo_set_features = veth_set_features,
.ndo_set_rx_headroom = veth_set_rx_headroom,
.ndo_bpf = veth_xdp, /* 挂载/卸载 XDP 程序 */
.ndo_xdp_xmit = veth_ndo_xdp_xmit,
.ndo_get_peer_dev = veth_peer_dev,
};

三、Linux Bridge(二层交换)

3.1 收包入口:br_handle_frame

Linux Bridge 通过在每个成员端口的 net_device 上注册 rx_handler 来截获二层帧。当报文从 veth/物理网卡到达时,__netif_receive_skb_core 检测到已注册的 rx_handler,调用 br_handle_framenet/bridge/br_input.c,第 320 行):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
static rx_handler_result_t br_handle_frame(struct sk_buff **pskb)
{
struct net_bridge_port *p;
struct sk_buff *skb = *pskb;
const unsigned char *dest = eth_hdr(skb)->h_dest;

if (unlikely(skb->pkt_type == PACKET_LOOPBACK))
return RX_HANDLER_PASS;

if (!is_valid_ether_addr(eth_hdr(skb)->h_source))
goto drop;

skb = skb_share_check(skb, GFP_ATOMIC);

p = br_port_get_rcu(skb->dev);
if (p->flags & BR_VLAN_TUNNEL)
br_handle_ingress_vlan_tunnel(skb, p, nbp_vlan_group_rcu(p));

if (unlikely(is_link_local_ether_addr(dest))) {
/* 处理 IEEE 802.1D 保留地址 */
switch (dest[5]) {
case 0x00: /* 01:80:C2:00:00:00 Bridge Group Address(STP BPDU) */
if (p->br->stp_enabled == BR_NO_STP || ...)
goto forward;
/* 上送本地协议栈处理 STP */
*pskb = skb;
__br_handle_local_finish(skb);
return RX_HANDLER_PASS;
case 0x0E: /* 01:80:C2:00:00:0E 802.1AB LLDP */
/* ... */
}
}

forward:
/* STP 状态机:只有 FORWARDING 和 LEARNING 状态才允许继续 */
switch (p->state) {
case BR_STATE_FORWARDING:
case BR_STATE_LEARNING:
return nf_hook_bridge_pre(skb, pskb);
default:
drop:
kfree_skb(skb);
}
return RX_HANDLER_CONSUMED;
}

STP 的核心逻辑体现在 p->state 的判断:处于 FORWARDINGLEARNING 状态的端口允许帧继续处理;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
2
if (p->flags & BR_LEARNING)
br_fdb_update(br, p, eth_hdr(skb)->h_source, vid, 0);

目的 MAC 查找与转发:

1
2
3
4
5
6
7
8
9
10
11
12
case BR_PKT_UNICAST:
dst = br_fdb_find_rcu(br, eth_hdr(skb)->h_dest, vid);
break;

if (dst) {
if (test_bit(BR_FDB_LOCAL, &dst->flags))
return br_pass_frame_up(skb); /* 目的是 bridge 本身,上送协议栈 */
br_forward(dst->dst, skb, local_rcv, false);
} else {
/* FDB miss:泛洪到所有端口 */
br_flood(br, skb, pkt_type, local_rcv, false, vid);
}

这个逻辑精确映射了物理以太网交换机的行为:已知单播直接转发,未知单播泛洪,广播和组播依各自规则处理。

3.3 br_forward:报文转发到出端口

br_forwardnet/bridge/br_forward.c,第 144 行)负责把 skb 转发到指定端口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void br_forward(const struct net_bridge_port *to,
struct sk_buff *skb, bool local_rcv, bool local_orig)
{
if (unlikely(!to))
goto out;

/* 若目标端口 link down,尝试切换到备用端口(bond failover) */
if (rcu_access_pointer(to->backup_port) && !netif_carrier_ok(to->dev)) {
struct net_bridge_port *backup_port;
backup_port = rcu_dereference(to->backup_port);
if (unlikely(!backup_port))
goto out;
to = backup_port;
}

if (should_deliver(to, skb)) {
if (local_rcv)
deliver_clone(to, skb, local_orig); /* 本地也需要收,先克隆 */
else
__br_forward(to, skb, local_orig);
return;
}
/* ... */
}

__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_keynet/bridge/br_fdb.c,第 215 行):

1
2
3
4
5
6
7
8
9
10
11
12
static struct net_bridge_fdb_entry *fdb_find_rcu(struct rhashtable *tbl,
const unsigned char *addr, __u16 vid)
{
struct net_bridge_fdb_key key;

WARN_ON_ONCE(!rcu_read_lock_held());

key.vlan_id = vid;
memcpy(key.addr.addr, addr, sizeof(key.addr.addr));

return rhashtable_lookup(tbl, &key, br_fdb_rht_params);
}

哈希参数在模块初始化时固定:

1
2
3
4
5
6
static const struct rhashtable_params br_fdb_rht_params = {
.head_offset = offsetof(struct net_bridge_fdb_entry, rhnode),
.key_offset = offsetof(struct net_bridge_fdb_entry, key),
.key_len = sizeof(struct net_bridge_fdb_key),
.automatic_shrinking = true,
};

automatic_shrinking = true 保证在 MAC 条目大量删除后哈希表会自动收缩,避免内存浪费。

3.5 br_fdb_update:动态 MAC 学习

每收到一帧,br_fdb_update 都会以源 MAC 为键更新 FDB(第 853 行):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void br_fdb_update(struct net_bridge *br, struct net_bridge_port *source,
const unsigned char *addr, u16 vid, unsigned long flags)
{
if (hold_time(br) == 0) /* hold_time=0 表示禁用动态学习 */
return;

fdb = fdb_find_rcu(&br->fdb_hash_tbl, addr, vid);
if (likely(fdb)) {
/* 已有条目:若源端口变化(设备漫游),更新 fdb->dst */
if (unlikely(source != READ_ONCE(fdb->dst) &&
!test_bit(BR_FDB_STICKY, &fdb->flags))) {
br_switchdev_fdb_notify(br, fdb, RTM_DELNEIGH);
WRITE_ONCE(fdb->dst, source);
fdb_modified = true;
}
fdb->updated = jiffies; /* 刷新老化时间戳 */
} else {
spin_lock(&br->hash_lock);
fdb = fdb_create(br, source, addr, vid, flags);
if (fdb) {
trace_br_fdb_update(br, source, addr, vid, flags);
fdb_notify(br, fdb, RTM_NEWNEIGH, true);
}
spin_unlock(&br->hash_lock);
}
}

读路径(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* per UDP socket information */
struct vxlan_sock {
struct hlist_node hlist;
struct socket *sock; /* 绑定在 UDP 4789/8472 端口的 socket */
struct hlist_head vni_list[VNI_HASH_SIZE]; /* VNI -> vxlan_dev 哈希表 */
refcount_t refcnt;
u32 flags; /* VXLAN_F_* 特性标志 */
};

/* Pseudo network device */
struct vxlan_dev {
struct vxlan_dev_node hlist4; /* 在 IPv4 socket 的 VNI 哈希表中的节点 */
struct list_head next; /* per-namespace vxlan 设备链表 */
struct vxlan_sock __rcu *vn4_sock;/* 监听 IPv4 UDP 的 socket */
struct vxlan_sock __rcu *vn6_sock;/* 监听 IPv6 UDP 的 socket(可选) */
struct net_device *dev; /* 对应的 net_device */
struct net *net; /* 报文收发所在的 namespace */
struct vxlan_rdst default_dst; /* 默认远端 VTEP 地址(泛洪用) */
struct timer_list age_timer; /* FDB 老化定时器 */
spinlock_t hash_lock[FDB_HASH_SIZE];
struct hlist_head fdb_head[FDB_HASH_SIZE]; /* VXLAN FDB 哈希桶 */
struct vxlan_config cfg; /* VNI、dst_port、TTL、DF 等配置 */
};

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_xmitvxlan_core.c,第 2680 行)是 vxlan 设备的 ndo_start_xmit。它在 VXLAN FDB 中查找内层目的 MAC,找到对应的远端 VTEP 后,调用 vxlan_xmit_one 完成封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static netdev_tx_t vxlan_xmit(struct sk_buff *skb, struct net_device *dev)
{
struct vxlan_dev *vxlan = netdev_priv(dev);
struct vxlan_fdb *f;
struct ethhdr *eth;
__be32 vni = 0;

skb_reset_mac_header(skb);

eth = eth_hdr(skb);
f = vxlan_find_mac(vxlan, eth->h_dest, vni); /* 查 VXLAN FDB */

if (f == NULL) {
/* FDB miss:查全零 MAC 条目作为默认泛洪目标 */
f = vxlan_find_mac(vxlan, all_zeros_mac, vni);
if (f == NULL) {
if (vxlan->cfg.flags & VXLAN_F_L2MISS)
vxlan_fdb_miss(vxlan, eth->h_dest); /* 通知用户态控制面 */
dev->stats.tx_dropped++;
kfree_skb(skb);
return NETDEV_TX_OK;
}
}

/* 遍历 rdst 列表(多播/多路径场景),对除最后一个 rdst 外克隆发送 */
list_for_each_entry_rcu(rdst, &f->remotes, list) {
if (!fdst) { fdst = rdst; continue; }
skb1 = skb_clone(skb, GFP_ATOMIC);
if (skb1)
vxlan_xmit_one(skb1, dev, vni, rdst, did_rsc);
}
if (fdst)
vxlan_xmit_one(skb, dev, vni, fdst, did_rsc);
else
kfree_skb(skb);

return NETDEV_TX_OK;
}

vxlan_xmit_one(第 2377 行)负责真正的封装:查路由获取 rtable,调用 vxlan_build_skb 在 skb 头部预留空间并填充 VXLAN 头(8 字节),最终通过:

1
2
3
4
5
udp_tunnel_xmit_skb(rt, sock4->sock->sk, skb,
local_ip.sin.sin_addr.s_addr,
dst->sin.sin_addr.s_addr,
tos, ttl, df,
src_port, dst_port, xnet, !udp_sum);

把内层以太帧包裹在 UDP/IP 外层头中,通过 VTEP 的物理网卡发送出去。外层 UDP 源端口由 udp_flow_src_port 基于内层报文的流哈希(5 元组)计算,保证同一条流的所有报文走相同的 ECMP 路径,实现负载均衡。

4.3 vxlan_rcv:解封装接收

对端 VTEP 上的 vxlan_rcv(第 1615 行)作为 UDP socket 的 encap_rcv 回调被调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static int vxlan_rcv(struct sock *sk, struct sk_buff *skb)
{
/* 1. 检查 VXLAN 最小头长度(8 字节) */
if (!pskb_may_pull(skb, VXLAN_HLEN))
goto drop;

unparsed = *vxlan_hdr(skb);

/* 2. VNI 标志位(bit 27)必须置位,否则丢弃 */
if (!(unparsed.vx_flags & VXLAN_HF_VNI))
goto drop;

/* 3. 从 sk 的 user_data 取出 vxlan_sock,再按 VNI 找到 vxlan_dev */
vs = rcu_dereference_sk_user_data(sk);
vni = vxlan_vni(vxlan_hdr(skb)->vx_vni);
vxlan = vxlan_vs_find_vni(vs, skb->dev->ifindex, vni, &vninode);
if (!vxlan)
goto drop;

/* 4. 剥离 VXLAN/UDP/IP 外层头,恢复内层以太帧 */
if (__iptunnel_pull_header(skb, VXLAN_HLEN, protocol, raw_proto, ...))
goto drop;

/* 5. 更新 VXLAN FDB(源 MAC -> 源 VTEP IP 映射,自动学习) */
if (!raw_proto)
vxlan_set_mac(vxlan, vs, skb, vni);

/* 6. 重置网络层头,通过 GRO cells 重入协议栈 */
skb_reset_network_header(skb);
gro_cells_receive(&vxlan->gro_cells, skb);
return 0;
drop:
kfree_skb(skb);
return 0;
}

解封后,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
2
3
4
5
6
7
8
9
10
Pod A eth0 (10.244.1.2/24, ns: podA-netns)
└─ veth_a_c (容器侧 veth)
↕ veth_xmit → __netif_rx (穿越 namespace)
└─ veth_a_h (宿主机侧 veth, 成员 cni0)
│ br_handle_frame → br_fdb_update(src_MAC)
│ br_fdb_find_rcu(dst_MAC) → veth_b_h 端口
↓ br_forward → dev_queue_xmit → veth_xmit
└─ veth_b_h (宿主机侧 veth)
↕ 穿越 namespace
└─ veth_b_c → Pod B eth0 (10.244.1.3/24)

关键步骤说明:

  1. Pod A 调用 write(),TCP 发包,查本地路由:目的 10.244.1.3 在同子网 10.244.1.0/24,走 eth0(即 veth_a_c)。
  2. ARP 解析后,skb 经 veth_xmit 由容器 namespace 穿越到宿主机 namespace,到达 veth_a_h
  3. veth_a_h 注册了 rx_handler = br_handle_frame,网桥收包,br_fdb_update 学习 Pod A 的 MAC。
  4. br_fdb_find_rcu 以 Pod B 的 MAC + VLAN 0 为键查哈希表,找到 veth_b_h 端口。
  5. br_forward__br_forwarddev_queue_xmit(skb)veth_xmit(veth_b_h)
  6. veth 再次穿越 namespace,报文进入 Pod B 的网络命名空间,__netif_rx 上送 TCP 栈。

整个过程全部在内核态完成,两次 veth 穿越无内存拷贝,一次 bridge FDB 查找 O(1),延迟极低。

5.2 跨节点通信(VXLAN 路径)

1
2
3
4
5
6
7
8
9
10
11
12
节点1:
Pod A eth0 → veth_a_c ↕ veth_a_h → cni0
→ 路由:10.244.2.0/24 via flannel.1
→ vxlan_xmit (VNI=1): 封装 UDP/IP
→ 节点1 eth0 (物理网卡) → 物理网络

节点2:
→ 节点2 eth0 收到 UDP 8472
→ vxlan_rcv: 解封装,vxlan_set_mac 学习远端 VTEP
→ gro_cells_receive → netif_receive_skb
→ flannel.1 (vxlan dev) → cni0 (bridge)
→ br_fdb_find_rcu → veth_b_h → Pod B eth0

跨节点路径的核心在于:

  • 封装侧(节点 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
2
3
4
5
6
7
struct macvlan_port {
struct net_device *dev; /* 下层物理设备 */
struct hlist_head vlan_hash[MACVLAN_HASH_SIZE]; /* MAC -> macvlan_dev */
struct list_head vlans;
u32 flags;
int count; /* macvlan 子设备数量 */
};

收包路径同样通过 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
2
3
4
5
6
7
8
9
10
11
12
13
14
# 列出所有命名空间(/run/netns 下的命名 namespace)
ip netns list

# 在指定 namespace 中执行命令
ip netns exec <ns_name> ip addr show
ip netns exec <ns_name> ip route show
ip netns exec <ns_name> ss -tuln

# 进入容器的网络命名空间(需要容器 PID)
# docker inspect 获取 PID,nsenter 切换 namespace
nsenter -t $(docker inspect --format '{{.State.Pid}}' <cid>) -n ip route show

# 查看 namespace 的 inode(可用于 setns 的 fd)
ls -lai /proc/<pid>/ns/net

7.2 Bridge 诊断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看 FDB 表(MAC 地址学习结果,包含 VLAN)
bridge fdb show dev docker0

# 查看 bridge 成员端口及 STP 状态(port state、role)
bridge link show

# 查看 bridge 详细配置(ageing time、STP、VLAN filtering)
ip -d link show docker0

# 实时监控 FDB 变化(添加/删除事件)
bridge monitor fdb

# 查看 bridge 统计
ip -s link show cni0

7.3 VXLAN 诊断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看 VXLAN 设备详情(VNI、dstport、local VTEP IP、remote)
ip -d link show flannel.1

# 查看 VXLAN FDB(内层 MAC -> 远端 VTEP IP 映射)
bridge fdb show dev flannel.1

# 抓取 VXLAN 封装后的 UDP 流量(外层)
tcpdump -i eth0 -n 'udp port 8472' -vv

# 抓取解封装后的内层流量(在 flannel.1 上)
tcpdump -i flannel.1 -n -e

# 验证 VXLAN 通道是否正常(ping 对端 VTEP IP)
ping -I flannel.1 <remote_vtep_ip>

7.4 容器网络丢包排查流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 第一步:确认接口 UP 状态和物理链路
ip link show veth_a_h
ip link show cni0

# 第二步:查看 per-interface 统计(TX errors、RX drops)
ip -s link show veth_a_h

# 第三步:检查 bridge FDB,确认 Pod MAC 是否已学习
bridge fdb show dev cni0

# 第四步:检查 ARP 表,排查 ARP 解析失败
ip neigh show dev cni0
arp -n

# 第五步:在 veth 宿主机侧抓包,确认报文是否到达 bridge
tcpdump -i veth_a_h -n -e 'not arp'

# 第六步:在 bridge 虚拟接口上抓包,确认转发方向
tcpdump -i cni0 -n

# 第七步:如果是跨节点问题,检查 VXLAN FDB 和外层路由
bridge fdb show dev flannel.1
ip route show table main | grep flannel

7.5 bpftrace 追踪内核路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 追踪 bridge 收包,打印接收端口名
bpftrace -e '
kprobe:br_handle_frame_finish {
$skb = (struct sk_buff *)arg2;
$dev = (struct net_device *)$skb->dev;
printf("br_rx port=%s\n", $dev->name);
}'

# 追踪 vxlan_xmit,统计每个 VXLAN 设备的发包量
bpftrace -e '
kprobe:vxlan_xmit {
$dev = (struct net_device *)arg1;
@[str($dev->name)] = count();
}
interval:s:5 { print(@); clear(@); }'

# 追踪 veth_xmit,测量 veth 转发延迟(纳秒)
bpftrace -e '
kprobe:veth_xmit { @start[tid] = nsecs; }
kretprobe:veth_xmit /@start[tid]/ {
@latency_ns = hist(nsecs - @start[tid]);
delete(@start[tid]);
}'

# 追踪 br_fdb_update,监控 MAC 学习事件
bpftrace -e '
kprobe:br_fdb_update {
$br = (struct net_bridge *)arg0;
$src = (struct net_bridge_port *)arg1;
printf("fdb_learn br=%s port=%s\n",
((struct net_device *)$br->dev)->name,
((struct net_device *)$src->dev)->name);
}'

总结

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 Bridgerx_handler 钩子切入二层收包流程,br_handle_frame 完成 STP 状态检查和 link-local 地址过滤,br_fdb_update 借助 rhashtable 实现 O(1) 的 MAC 学习与查找,br_forward 经 netfilter 钩子发往目标端口,整套实现不超过 2000 行代码却功能完整。

  • VXLAN 以 UDP 封装跨越三层物理网络,vxlan_xmit_onevxlan_rcv 构成封解装闭环,外层 UDP 源端口的流哈希计算保证了 ECMP 负载均衡,gro_cells_receive 使解封装后的内层帧无缝重入 GRO 路径,吞吐量接近直连。

四者叠加,构成了 Docker/Kubernetes 容器网络模型的完整内核实现基础。理解这套机制,不仅能高效排查各类容器网络问题(丢包、延迟、ARP 异常),也是深入理解 eBPF/XDP 容器网络加速(Cilium、Calico eBPF 模式)的必要前提——这些加速方案的本质,是在上述收发包路径的关键节点注入 BPF 程序,绕过部分内核网络层,以换取更低的延迟和更高的吞吐。