最近看了一些 Linux Bridge 的资料. 以及看看了 Linux kernel 的代码实现.

the very begining

因为最近使用 k8s CNI 中的 main plugin 和自制的 IPAM 插件来实现 CNI, 了解了许多 main plugin 下的知识. 其中又比较仔细地看了下 bridge 的实现.

Linux Bridge 的组成

  1. 一些网络端口 ( a set of network ports)
  2. 一个控制配置逻辑 (a control plane)
  3. 一个路由逻辑 (a forwarding plane)
  4. 一个 MAC 端口映射表 (a MAC learning database)

Linux Bridge 数据结构

以下代码来自 torvalds/linux/net/bridge

Linux Bridge 主要的数据结构

net_bridge

1
2
3
4
5
6
7
8
9
10
11
12
13
struct net_bridge {
spinlock_t lock;
spinlock_t hash_lock;
struct list_head port_list;
struct net_device *dev;
struct hlist_head hash[BR_HASH_SIZE];
#if IS_ENABLED(CONFIG_BRIDGE_NETFILTER)
bool nf_call_iptables;
bool nf_call_ip6tables;
bool nf_call_arptables;
#endif
......
};
  1. 是最主要的数据结构, 包含了该 Bridge device 的所有配置.
  2. 一个 net_bridge_port 组成的双向链表 port_list
  3. *dev 指向当前设备
  4. hash 指向 MAC 端口映射表

net_bridge_port

1
2
3
4
5
6
7
8
9
10
11
struct net_bridge_port {
struct net_bridge *br;
struct net_device *dev;
struct list_head list;

port_id port_id;

struct timer_list forward_delay_timer;
struct timer_list hold_timer;
struct timer_list message_age_timer;
};

net_bridge_fdb_entry

1
2
3
4
5
6
7
8
9
10
struct net_bridge_fdb_entry {
struct hlist_node hlist;
struct net_bridge_port *dst;

mac_addr addr;
__u16 vlan_id;

unsigned long updated ____cacheline_aligned_in_smp;
unsigned long used;
};

Bridge 的创建和添加 dev

用到了 ioctl 的 SIOCBRADDBR 和 SIOCBRADDIF. 添加 dev 到 Bridge 的过程, 会将目标 dev 添加到 MAC 端口映射表中去.

package 处理

Linux bridge module I/O

主要是分为两块, 首先是 get 或者 update MAC 端口印射表, 然后根据是否找到对应的 MAC-port 以及 dest-MAC 的类型来决定是如何将进入 Bridge 的包转发出去. 分别由 br_forward/br_multicast_forward/br_flood_forward 来发起单播/组播/广播.

VEB and VEPA

  1. VEB 即 Virtual Ethernet Bridges
  2. VEPA 即 Virtual Ethernet Port Aggregator 也即发卡弯模式.
  3. brctl hairpin <bridge> <port> {on|off} turn hairpin on/off

该部分对 bridge 上端口的设置, 影响的主要是同一个 bridge 下 device 包的走向. 具体差距可以参考以下.
VEB
VEPA

Docker 下的 bridge

docker network create -d bridge br-0 会创建一个 subnet: 172.23.0.0/16, gateway: 172.23.0.1 的 Bridge 网络. 然后使用 docker run -p PORT IMAGE 运行的容器会被分配一个该子网下的 IP, 并在主机上通过 iptables DNAT 主机的 port 到该 IP.

k8s 下的 Bridge (CNI main plugin)

会根据 CNI 配置文件 /etc/cni/net.d/*.conf[list] 来对本地进行配置 Linux Bridge. 而 IPAM 一般可以使用 host-local. 但仅仅使用 bridge + host-local 的 CNI plugin 甚至不能满足 k8s 下对 CNI 的基本要求–即 pod 和 pod 之间可以不经过 NAT 直接进行连通. 因为该 bridge 创建并分配的子网网络并没有一个路由的出口(如主机在 192.168.0.0/16 网段, 而 host-local 在 10.0.0.0/8 网段), 外部也没有一个可以访问 pod 的手段(192.168.0.0/16 的主机无法访问某台主机上的 10.x.y.z 的 pod).

Trick in bridge

  1. k8s 下的 CNI plugin 都是单独的代码进行打包, 实现的非常简单(甚至例如 bridge/macvlan/ipvlan 等简单的网络虚拟手段, 都可以用 brctl/ip 等命令来实现, 除了解析 json 需要用到 jq 略麻烦). 而 bridge 在实现的时候, 有一个步骤会确认主机上所需的 br0 是存在的. 也就是说如果提前创建好了的 br0, bridge 插件并不会重新去创建.
  2. 前面没有提到的是, 任何设备使用 brctl addif <bridge> <device> 之后, 则流经该设备的流量都会由该 <bridge> 来处理.
  3. 所以, 在容器之前, 那个虚拟机管理下的 桥接模式 是将本机的 eth0 add 到了一个网桥上, 并通过和 eth0 所在的网络直接进行网络 IP 的分配, 也即虚拟机可以获取和主机同网段的 IP 地址, 或者是自己手动指定 static 的 IP 地址.
  4. 同样的, 该方式在 k8s 的 bridge CNI plugin 也能实现, 甚至还能进一步地实现指定 pod 指定 IP 的功能. 即给 192.168.0.0/16 网段主机上的容器分配同网段的 IP 给 pod.
  5. 该方式的利弊端都很明显, 弊端即需要预先配置一个网桥, 还有会占用掉主机端的 IP, 以及网络隔离可能会引入复杂 vlan 划分方式, 利端则是该方式无需外部路由配置即可实现 k8s 对 CNI 的基本要求(需要上游交换机打开混杂模式), IP 管理的问题在一个提前规划的生产环境内可以降低到可接受的范围.

Reference

torvalds/linux
Anatomy of a Linux bridge
Edge Virtual Bridging with VEB and VEPA
A Hacker’s Guide to Kubernetes Networking