TCP原理之:linux网桥
0. 前言
Linux内核提供了对网桥的支持,通过建立网桥设备能够将系统中的多个网络接口相连通,实现网络接口之前的通信。为了便于理解,我们可以将网桥想象成现实中的交换机,事实上网桥基本上实现了交换机的所有功能。
1. 原理解析
现实中的交换机我们可能比较清楚,而网桥这种抽象的、处于Linux系统内部的设备,它的机制和工作原理究竟是怎样的呢?简单来说,我们可以通过下面的一张示意图来进行解析:
与你平时对网桥的理解是不是有些不一样呢?对于内核以及协议栈而言,网桥也是一种网络接口,可以对网桥设置IP。我们可以简单地将网桥理解为“交换机+网卡”,即里面内置了一个网络接口“ethx”的虚拟交换机,而且这个“ethx”与内核协议栈相联通,如下图所示:
从上面的图中我们可以看得出来,当将eth0
、eth1
等物理设备添加到网桥后,这些网口就变得仿佛是外部独立的PC一样,而网桥变得像是与这些PC相连接的交换机。发往eth0
、eth1
的报文不再能够进入内核协议栈,反而只有发往ethx
的报文才能到达协议栈。看起来是不是有点乱?我们来理一下。
如果一个报文的目的地址是eth0
,那么eth0
收到报文后,会将其传递给br0
,br0
收到报文后会发广播查找报文的MAC地址,广播会通过eth0
发送出去,但并不会得到响应,所以最终报文会丢失。
从上面的分析我们可以看出,当一个物理网口连接到网桥,那么它将退化成一根”网线“,它上面所设置的IP等参数均无效,因为已经没有协议栈来处理其收到的报文。
2. 应用场景
网桥的应用范围很广泛,它不仅可以将多个物理网卡桥接在一起,还可以将系统中创建的虚拟网口连接在一起,使得各个网口之间可以通信。我们以桥接两个网络命名空间(虚拟机)为例来讲解一下网桥的使用方法。
下面我们以VETH-Pair类型的虚拟网口来模拟一下网桥是如何将内部的虚拟网口连接在一起的,对VETH-Pair不太清楚的可以参考一下我的另一篇文章:TCP原理之:虚拟网络。
首先我们先创建一个虚拟网桥:
# brctl addbr br0
可以看到我们的网桥已经创建成功了:
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: br0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether ce:b0:31:4c:77:08 brd ff:ff:ff:ff:ff:ff
下面我们来创建两对VETH-Pair网口,并分别为他们创建两个网络命名空间:
# ip link add left type veth peer name veth0 //创建虚拟网卡对left-veth0
# ip netns add left //创建命名空间left
# ip link set veth0 netns left //将veth0添加到left命名空间
#
# ip link add right type veth peer name veth0 //创建虚拟网卡对right-veth0
# ip netns add right //创建命名空间right
# ip link set veth0 netns right //将veth0添加到right命名空间
将left
和right
网口分别添加到网桥br0
:
# brctl addif br0 left
# brctl addif br0 right
# brctl show
bridge name bridge id STP enabled interfaces
br0 8000.0a8f2925c2d9 no left
right
此时系统内部的网络拓扑结构如下图所示:
此时命名空间left
已经能够和right
命名空间进行通信。
# ip netns exec right ping 192.168.100.100
PING 192.168.100.100 (192.168.100.100) 56(84) bytes of data.
64 bytes from 192.168.100.100: icmp_seq=1 ttl=64 time=0.079 ms
64 bytes from 192.168.100.100: icmp_seq=2 ttl=64 time=0.104 ms
64 bytes from 192.168.100.100: icmp_seq=3 ttl=64 time=0.095 ms
64 bytes from 192.168.100.100: icmp_seq=4 ttl=64 time=0.100 ms
64 bytes from 192.168.100.100: icmp_seq=5 ttl=64 time=0.064 ms
3. 内核实现
这里简单讲一下在内核中网桥是如何实现报文的转发的。网桥的收报处理函数为br_handle_frame
,这个函数是我们通过brctl addif br0 eth0
命令将eth0
网口加到br0
网桥时,通过函数netdev_rx_handler_register
将其注册为eth0
网口的handler
。因此当eth0
收到报文后,会将报文交给它的handler
处理,也就是br_handle_frame
函数。
br_handle_frame
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;
br_should_route_hook_t *rhook;
/* 不处理回环设备的报文 */
if (unlikely(skb->pkt_type == PACKET_LOOPBACK))
return RX_HANDLER_PASS;
/* 检查mac有效性 */
if (!is_valid_ether_addr(eth_hdr(skb)->h_source))
goto drop;
skb = skb_share_check(skb, GFP_ATOMIC);
if (!skb)
return RX_HANDLER_CONSUMED;
p = br_port_get_rcu(skb->dev);
/* 检查端口是否开启了VLAN,如果开启了,则交给网桥的VLAN模块处理 */
if (p->flags & BR_VLAN_TUNNEL) {
if (br_handle_ingress_vlan_tunnel(skb, p,
nbp_vlan_group_rcu(p)))
goto drop;
}
/* 处理BPDU(网桥协议数据单元)报文 */
if (unlikely(is_link_local_ether_addr(dest))) {
......
}
forward:
/* 根据端口的状态对报文进行处理。 */
switch (p->state) {
/* ebtables获取路由的hook点 */
case BR_STATE_FORWARDING:
rhook = rcu_dereference(br_should_route_hook);
if (rhook) {
if ((*rhook)(skb)) {
*pskb = skb;
return RX_HANDLER_PASS;
}
dest = eth_hdr(skb)->h_dest;
}
/* fall through */
case BR_STATE_LEARNING:
if (ether_addr_equal(p->br->dev->dev_addr, dest))
skb->pkt_type = PACKET_HOST;
/* 调用NF_BR_PRE_ROUTING处所注册的钩子函数。这里要执行两处处理:
* 首先调用ebtables在此处添加的规则,通过后,如果设置了
* /proc/sys/net/bridge/bridge-nf-call-iptables,则调用iptables
* 在NF_PRE_ROUTING注册的规则。
*/
NF_HOOK(NFPROTO_BRIDGE, NF_BR_PRE_ROUTING,
dev_net(skb->dev), NULL, skb, skb->dev, NULL,
br_handle_frame_finish);
break;
default:
drop:
kfree_skb(skb);
}
return RX_HANDLER_CONSUMED;
br_handle_frame_finish
int br_handle_frame_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
/* 通过skb的网络设备获取其绑定的网桥设备 */
struct net_bridge_port *p = br_port_get_rcu(skb->dev);
enum br_pkt_type pkt_type = BR_PKT_UNICAST;
struct net_bridge_fdb_entry *dst = NULL;
struct net_bridge_mdb_entry *mdst;
bool local_rcv, mcast_hit = false;
const unsigned char *dest;
struct net_bridge *br;
u16 vid = 0;
/* 检查网桥是否启用 */
if (!p || p->state == BR_STATE_DISABLED)
goto drop;
/* 检查是否配置了CONFIG_BRIDGE_VLAN_FILTERING,配置了的话会根据报文的
* vlan对其进行过滤
*/
if (!br_allowed_ingress(p->br, nbp_vlan_group_rcu(p), skb, &vid))
goto out;
nbp_switchdev_frame_mark(p, skb);
/* insert into forwarding database after filtering to avoid spoofing */
br = p->br;
if (p->flags & BR_LEARNING)
/* 将报文的mac更新到网桥的fdb表中。fdb表是每个网桥维护的网口与mac地址对应关系的表。 */
br_fdb_update(br, p, eth_hdr(skb)->h_source, vid, false);
/* 检查网桥设备是否开启了混杂模式 */
local_rcv = !!(br->dev->flags & IFF_PROMISC);
dest = eth_hdr(skb)->h_dest;
/* 检查是否是广播报文,如果是广播(或者组播)报文 */
if (is_multicast_ether_addr(dest)) {
/* by definition the broadcast is also a multicast address */
if (is_broadcast_ether_addr(dest)) {
pkt_type = BR_PKT_BROADCAST;
local_rcv = true;
} else {
pkt_type = BR_PKT_MULTICAST;
/* 检查是否配置了IGMP协议支持,如果配置了,则将组播交给IGMP协议处理 */
if (br_multicast_rcv(br, p, skb, vid))
goto drop;
}
}
if (p->state == BR_STATE_LEARNING)
goto drop;
BR_INPUT_SKB_CB(skb)->brdev = br->dev;
BR_INPUT_SKB_CB(skb)->src_port_isolated = !!(p->flags & BR_ISOLATED);
/* 处理IPv4的arp报文和IPv6的子邻居协议 */
if (IS_ENABLED(CONFIG_INET) &&
(skb->protocol == htons(ETH_P_ARP) ||
skb->protocol == htons(ETH_P_RARP))) {
br_do_proxy_suppress_arp(skb, br, vid, p);
} else if (IS_ENABLED(CONFIG_IPV6) &&
skb->protocol == htons(ETH_P_IPV6) &&
br->neigh_suppress_enabled &&
pskb_may_pull(skb, sizeof(struct ipv6hdr) +
sizeof(struct nd_msg)) &&
ipv6_hdr(skb)->nexthdr == IPPROTO_ICMPV6) {
struct nd_msg *msg, _msg;
msg = br_is_nd_neigh_msg(skb, &_msg);
if (msg)
br_do_suppress_nd(skb, br, vid, p, msg);
}
/* 获取报文的目的地址。组播报文的话,获取所有的目的端口;单播报文的话,获取目的mac地址。 */
switch (pkt_type) {
case BR_PKT_MULTICAST:
mdst = br_mdb_get(br, skb, vid);
if ((mdst || BR_INPUT_SKB_CB_MROUTERS_ONLY(skb)) &&
br_multicast_querier_exists(br, eth_hdr(skb))) {
if ((mdst && mdst->host_joined) ||
br_multicast_is_router(br)) {
local_rcv = true;
br->dev->stats.multicast++;
}
mcast_hit = true;
} else {
local_rcv = true;
br->dev->stats.multicast++;
}
break;
case BR_PKT_UNICAST:
dst = br_fdb_find_rcu(br, dest, vid);
default:
break;
}
if (dst) {
unsigned long now = jiffies;
/* 如果是单播,且目的地址是网桥上的端口,调用br_pass_frame_up传给内核协议栈 */
if (dst->is_local)
return br_pass_frame_up(skb);
if (now != dst->used)
dst->used = now;
/* 说明报文是发给别的主机的,调用br_forward将报文转发出去。 */
br_forward(dst->dst, skb, local_rcv, false);
} else {
if (!mcast_hit)
/* 广播报文,采用flood方式发送给所有的端口。 */
br_flood(br, skb, pkt_type, local_rcv, false);
else
/* 组播报文,发送给所有的目的端口。 */
br_multicast_flood(mdst, skb, local_rcv, false);
}
/* 开启了混杂模式,发送给网桥的那个端口 */
if (local_rcv)
return br_pass_frame_up(skb);
out:
return 0;
drop:
kfree_skb(skb);
goto out;
}