网络协议栈(8)Bridge设备
一、应用
在很多tap虚拟网卡的使用中,bridge也都是被使用的。至少是在qemu的网络模拟和vpn的bridge实现也依赖于bridge这种虚拟设备,所以在看了tap的使用之后,bridge的使用和原理也不可避免的要弱弱的围观一下。
这里的bridge并不是一个物理的网桥,而是一个虚拟的网络设备。它相当于联合了一部分网卡,然后组成一个小的自治组织,从而对操作系统的IP层来说就是一个独立的网卡,也就是当路由一个报文的时候,从系统中只能看到一个网络设备,所以这样就保证了网络层出口的简单性。
这种机制的另一个最为直接的特点就是可以节省IP地址。
二、设备的MAC
这个是一个首先面临的问题,这么一个虚拟的网络设备有一个IP地址,但是这个IP地址下罩着了很多的MAC地址,那么这个虚拟的网络设备的MAC地址将如何确定?因为这里可能会涉及到之后MAC地址如何选取的问题,毕竟IP只有一个,那么不同的网口收到ARP消息的时候该如何响应自己的MAC地址?当然,本人本着蛋疼的精神,在机缘巧合之下见了一下调用链,
(gdb) bt
#0 __constant_memcpy (n=6, from=0xcff3b118, to=0xcfe69118)
at include/asm/string.h:240
#1 br_stp_change_bridge_id (n=6, from=0xcff3b118, to=0xcfe69118)
at net/bridge/br_stp_if.c:139
#2 0xc0826016 in br_stp_recalculate_bridge_id (br=0xcfe69480)
at net/bridge/br_stp_if.c:175
#3 0xc0820a7b in br_add_if (br=0xcfe69480, dev=0xcff3b000)
at net/bridge/br_if.c:429
#4 0xc082125a in add_del_if (br=0xcfe69480, ifindex=1, isadd=1)
at net/bridge/br_ioctl.c:96
#5 0xc0822b6d in br_dev_ioctl (dev=0xcfe69000, rq=0xcfed3eb4, cmd=35234)
at net/bridge/br_ioctl.c:402
#6 0xc06ec6a7 in dev_ifsioc (ifr=0xcfed3eb4, cmd=35234) at net/core/dev.c:2612
#7 0xc06ec91d in dev_ioctl (cmd=35234, arg=0xbfbe97d8) at net/core/dev.c:2769
#8 0xc06d563a in sock_ioctl (file=0xc1277bc0, cmd=35234, arg=3216938968)
at net/socket.c:874
#9 0xc01d30f6 in do_ioctl (filp=0xc1277bc0, cmd=35234, arg=3216938968)
at fs/ioctl.c:28
但是里面的代码比较奇怪,好像bridge设备对MAC地址数据比较小的地址情有独钟,不知道为什么
list_for_each_entry(p, &br->port_list, list) {
if (addr == br_mac_zero ||
memcmp(p->dev->dev_addr, addr, ETH_ALEN) < 0)这里选择MAC值比较小的MAC地址。
addr = p->dev->dev_addr;
为此,我还验证了一下
[root@Harry openvpnObj]# ifconfig
br0 Link encap:Ethernet HWaddr 00:0C:29:9F:81:76
……
eth2 Link encap:Ethernet HWaddr 00:0C:29:9F:81:6C
……
eth3 Link encap:Ethernet HWaddr 00:0C:29:9F:81:76
……
[root@Harry openvpnObj]# brctl addif br0 eth2
[root@Harry openvpnObj]# ifconfig
br0 Link encap:Ethernet HWaddr 00:0C:29:9F:81:6C see?mac地址变了,如果大家认为是最新的生效的话,大家可以修改一下两个网卡的添加顺序,最终使用的MAC地址同样是不变的。
……
eth2 Link encap:Ethernet HWaddr 00:0C:29:9F:81:6C
……
eth3 Link encap:Ethernet HWaddr 00:0C:29:9F:81:76
……
然后在我的windows主机中ping这个IP,可以看到这个MAC就是某个网卡的IP
I:\Documents and Settings\tsecer>arp -a
……
Interface: 192.168.203.1 --- 0x3
Internet Address Physical Address Type
192.168.203.177 00-0c-29-9f-81-6c dynamic
三、bridge MAC地址管理
整个路由模块的入口位于linux-2.6.21\net\bridge\br.c文件中,这个文件是整个bridge模块可用的入口,如果作为一个模块编译,那么整个模块的入口就在这里,它只是对系统中的一些全局变量进行了赋值。其中最为重要的就是br_handle_frame_hook = br_handle_frame;,这个函数指针将会在网口接收到数据向上穿得开始获得一次处理机会,
netif_receive_skb--->>>handle_bridge--->>>br_handle_frame_hook--->>>br_handle_frame_finish
在该函数中,将会根据报文的链路层源地址来更新自己的记录的转发数据库信息(Forward DataBase)结构,这个结构的键值就是一个MAC地址,还记录了这个MAC地址的报文是从该网桥的哪个bridge接口接收的。这就是网桥的自我学习能力,如果它学习到了网卡的信息,那么它就没必要对自己接收到的以太网报文在自己所有的端口中广播,而是可以向特定端口转发,当然这里也是进行链路层过滤的一个好机会。可以认为,由于从这个端口中接收到了源地址为该MAC的数据帧,那么当向该地址发送报文的时候,报文就应该从这个端口发送出去。
这个成员保存在struct net_bridge结构的
struct hlist_head hash[BR_HASH_SIZE];
成员中,其中的各个成员为一个
struct net_bridge_fdb_entry结构,其中包含了最为重要的
struct net_bridge_port *dst;
mac_addr addr;
unsigned char is_local;
信息,其中的net_bridge_port就是网桥的一个端口,这个端口对应的就连接一个网卡设备,可以认为这是一个网桥和网口之间的中间连接器,知道这个对象,就知道了对应的网桥和网卡设备。mac_addr则记录了一个链路层地址,这个是整个转发数据库的键值。is_local则确定这个地址是否在本机中。
在br_handle_frame_finish函数中将会通过br_fdb_update(br, p, eth_hdr(skb)->h_source);函数来更新这个数据库,可以看到,其中的第三个参数就是这个报文的源MAC地址,所以数据库中就保留了这个MAC地址通过哪个网口进入本机。
当然这个信息不能毫不利己专门利人,它也需要记录自己管理的所有的网口的MAC地址,并且这些地址明显是local地址。这个过程显而易见是在向网桥中添加网口的时候实现的。具体路径为:
br_add_if--->>>br_fdb_insert--->>>fdb_insert--->>>fdb_create(head, source, addr, 1)
由于fdb_create的最后一个参数为1,从而表明这个MAC地址是一个本机地址。
四、bridge 网口的添加和删除
在br_add_if和br_del_if函数中将会完成对于网口设备的添加和删除。这个感觉也是一个比较直观的操作,就是向一个容器中添加或者删除一些设备。但是还是看一下其中的数据结构,从而可以便于以后的定量分析。
对于bridge接入的每个设备,bridge并不是直接通过一个net_device的指针来执行新添加的网口设备,而是在网口和网桥之间添加了一个中间件net_bridge_port,至于为什么这么做,隐隐约约感觉是有好处的,就是解耦网桥和网卡之间的强耦合。因为vmware的hub.c中对于hub设备也是这么实现的,但是如果直接耦合在一起会有什么问题,俺也不太清楚。
没添加一个新的网口(InterFace)到网桥中,就会通过new_nbp(br, dev)创建一个新的net_bridge_port结构。在br_add_if--->>>new_nbp--->>find_portno接口中选择一个网桥中没有使用的槽位。这个查找过程也比较直观,但是看起来却有点不太常见,它
static int find_portno(struct net_bridge *br)
{
int index;
struct net_bridge_port *p;
unsigned long *inuse;
inuse = kcalloc(BITS_TO_LONGS(BR_MAX_PORTS), sizeof(unsigned long),
GFP_KERNEL);这里首先分配一个整数数组,数组中总共的bit数目要能够容纳所有的自己可以添加的最多的网口数(默认是1024个)。
if (!inuse)
return -ENOMEM;
set_bit(0, inuse); /* zero is reserved */
list_for_each_entry(p, &br->port_list, list) {
set_bit(p->port_no, inuse);遍历网桥的所有槽位,将该槽位占用的网口编号对一个的bit置一。这里也就是说,每个net_bridge_port中保存自己在网桥中所使用的槽位号,但是网桥bridge结构本身并没有维护这个槽位位图信息,而是需要使用时动态生成。
}
index = find_first_zero_bit(inuse, BR_MAX_PORTS);这里从槽位bit中选择第一个没有被使用的槽位,并将这个槽位号分配给该网卡。
kfree(inuse);
return (index >= BR_MAX_PORTS) ? -EXFULL : index;
}
在br_add_if--->>>list_add_rcu(&p->list, &br->port_list)中将新创建的结构添加到自己的prot_list链表中,也就是该链表维护了bridge设备中所有挂接的网卡信息。
五、链路层路由
本机发送报文
在br_dev_setup接口中初始化了网桥的发送接口为br_dev_xmit,这也就是说当上层的IP层路由确定某个报文需要通过一个虚拟网桥发送时,它会把以太网报文通过这个接口转交给网桥,此时网桥就可以进行自己内部链路层的路由。其主体代码为下面三个比较直观的判断,
if (dest[0] & 1)如果报文本身就是广播或者多播,那么就在自己所有的管理端口中发次洪水,所以每个网口都会被淹到,从而接收到这个报文。
br_flood_deliver(br, skb, 0);
else if ((dst = __br_fdb_get(br, dest)) != NULL)如果能够从自己维护的链路层数据库中找到这个MAC地址,就可以定点发射了。
br_deliver(dst->dst, skb);这里的dst就是指定的MAC地址的接收网口
else如果网桥对这个MAC地址也一无所知,那就只好广发英雄帖,大家都来围观一下。
br_flood_deliver(br, skb, 0);
网口接收
当一个网口接收到报文的时候,它一般通过netif_receive_skb--->>>handle_bridge--->>>br_handle_frame--->>>br_handle_frame_finish
在该函数中,其中通过br_fdb_update(br, p, eth_hdr(skb)->h_source);根据报文的源地址完成了本地转发数据库的更新,然后是自己的报文的本机向上投递或者是转发,当然转发是要经过防火墙过滤的,不过这一点我们可以暂时忽略。
dst = __br_fdb_get(br, dest);从数据库中查询
if (dst != NULL && dst->is_local) {
if (!passedup)
br_pass_frame_up(br, skb);如果目的MAC地址为本机所有,那么转交给更上层,在br_pass_frame_up中,执行indev = skb->dev;和skb->dev = br->dev;,其中前者让上层看到是bridge(而不是原始网口)网络设备接收到的数据,后者IP层只看到这一小堆网口中那个数值最小的、最为代表的网卡地址,体现在报文结构sk_buff中。
else
kfree_skb(skb);
goto out;
}
if (dst != NULL) {如果MAC地址不在本机,则尝试进行转发,当然转发不是随随便便就可以转发的,因为是否转发是IP层的问题,MAC层越俎代庖可能会高效一点,但是我们不鼓励这种不按套路出牌的做法,这会使系统失控而无法管理。
br_forward(dst->dst, skb);
goto out;
}
br_flood_forward(br, skb, 0);这又是无奈选择,只能发次洪水了。
六、在OpenVPN中的应用
在OpenVPN的bridge模式中,server会建立一个虚拟网桥,将目的局域网中的网口添加在该网桥中。然后再创建一个tap设备也添加入该网桥中。然后VPN server在一个外网网口上侦听客户端发送的以太网数据。由于客户端也是通过tap格式读取到网口数据的,所以它包含了完整的以太网报文格式,这其中就包含了源MAC和目的MAC。Client将这个这以太帧通过WAN的IP数据体内保存(可能会加密,可能会压缩)的数据发送给Server,server接收到这个数据之后(可能做对应的解密和解压),然后通过write系统调用写入bridge下的TAP设备。我们看一下此时的流程:
do_sync_write--->>>tun_chr_aio_write-->>tun_get_user--->>netif_rx_ni--->>netif_rx_schedule--->>>__netif_rx_schedule--->>>__raise_softirq_irqoff(NET_RX_SOFTIRQ);
此时触发软中断
net_rx_action-->>dev->poll--->>>netif_receive_skb--->>handle_bridge
此时已经进入了我们之前分析的情景。然后在handle_bridge--->>>br_handle_frame_hook--->>>br_handle_frame_finish--->>>>br_fdb_update,这里就会记录下源MAC地址是通过本机的TAP端口接收的,而这个源MAC地址是Client写入客户端TAP的源MAC地址,一般也就是客户端TAP设备的MAC地址。
由于Client和Server之间交互的是MAC层数据,所以Client的ARP报文、广播报文都可以到达Server的tap,进而进入server的局域网或者是server本身的协议栈。反过来,由于Server上bridge设备的存在,它会记录下那些客户端发送的以太网报文的源地址都是通过TAP设备发送的。这也就是说,如果bridge接收到一个目的MAC地址是Clent MAC地址的报文(此时真实网卡要设置为杂收模式,从而可以接收到局域网中所有的以太网报文),那么它会根据自己学习得到的信息将这个报文转发给自己的TAP网口(因为bridge从这个网口中收到过源MAC地址为客户端MAC的报文),而VPN Server则可以从这个TAP中执行read读取这些报文,然后经过WAN的IP层返还给Client。
这里的奇妙之处就在于无论有多少个客户端,服务器都只需要一个tap设备。因为之前分析中可以看到,此时的,bridge在自己的数据库中记录的键值是MAC地址,所以同一个设备可以发送出任意源地址的以太网报文。此时的VPN Server就需要从TAP中读取的所有的报文中区分出这些报文是分给那个客户端的,这个事实上并不难,因为MAC地址是唯一的。
这里顺便说一下,其实真实的网卡是很灵活的,它可以发送任意的以太网报文,因为各个设备的hard_start_xmit接口接收的是完整的以太网数据帧,也就是包含了源和目的MAC地址,所以上层可以让一个网卡发送任意报文,所以一个网卡发送的报文的源MAC地址和网卡的MAC地址没有必然联系。另一方面,通过将网卡设置为杂收模式,可以让网卡接收到触及网卡的所有报文,而不管报文的MAC目的地址是否和自己的MAC地址匹配。
七、在qemu中的应用
这个原理和OpenVPN相似,只是此时在本机内部进行。qemu将虚拟机内部对网卡接口的操作(此时必然是完整的以太网报文格式)报文直接写入本机的tap设备,从而相当于host接收到一个以太网报文,这样,虚拟机的网络是和主机的一个并列关系。而qemu本身就可以只和tap设备读写操作就可以完成对网卡的模拟,从而虚拟机本身不需要对虚拟机内部发送的报文做任何转换处理。最后放一个qemu的调试调用链:
(gdb) bt
#0 tap_receive (nc=0xa8750d0, buf=0xa473f3e8 "", size=42) at net/tap.c:157
#1 0x080be252 in qemu_vlan_deliver_packet (sender=0xa8a7780, flags=0, buf=
0xa473f3e8 "", size=42, opaque=0xa874ff8) at net.c:440
#2 0x080c0331 in qemu_net_queue_deliver (queue=0xa875018, sender=0xa8a7780,
flags=0, data=0xa473f3e8 "", size=42) at net/queue.c:154
#3 0x080c0488 in qemu_net_queue_send (queue=0xa875018, sender=0xa8a7780,
flags=0, data=0xa473f3e8 "", size=42, sent_cb=0) at net/queue.c:188
#4 0x080be42f in qemu_send_packet_async_with_flags (sender=0xa8a7780, flags=
0, buf=0xa473f3e8 "", size=42, sent_cb=0) at net.c:508
#5 0x080be493 in qemu_send_packet_async (sender=0xa8a7780, buf=0xa473f3e8 "",
size=42, sent_cb=0) at net.c:515
#6 0x080be4ea in qemu_send_packet (vc=0xa8a7780, buf=0xa473f3e8 "", size=42)
at net.c:521
#7 0x08229ee1 in xmit_seg (s=0xa471f008)
at /home/tsecer/KernelDebug/qemu-0.15.0/hw/e1000.c:408
#8 0x0822a4d7 in process_tx_desc (s=0xa471f008, dp=0xbfb1f178)
at /home/tsecer/KernelDebug/qemu-0.15.0/hw/e1000.c:497
#9 0x0822a765 in start_xmit (s=0xa471f008)
at /home/tsecer/KernelDebug/qemu-0.15.0/hw/e1000.c:549
#10 0x0822b7d2 in set_tctl (s=0xa471f008, index=3590, val=7)
at /home/tsecer/KernelDebug/qemu-0.15.0/hw/e1000.c:847
#11 0x0822b9be in e1000_mmio_writel (opaque=0xa471f008, addr=14360, val=7)
at /home/tsecer/KernelDebug/qemu-0.15.0/hw/e1000.c:914
---Type <return> to continue, or q <return> to quit---
#12 0x081c1cdf in io_writel (physaddr=14360, val=7, addr=3498326040, retaddr=
0x1cc8c1d) at ../softmmu_template.h:213
#13 0x081c1dd2 in __stl_mmu (addr=3498326040, val=7, mmu_idx=0)
at ../softmmu_template.h:245
#14 0x01cc8c1e in ?? ()
#15 0x00000028 in ?? ()
#16 0x00000001 in ?? ()
#17 0x00000000 in ?? ()
(gdb)
(gdb) x /50x 0xa473f3e8
0xa473f3e8: 0x2a290c00 0x54524dfe 0x56341200 0x01000608这里开始有完整的guest操作系统中的MAC地址
0xa473f3f8: 0x04060008 0x54520200 0x56341200 0xdecba8c0 这里是发送方的IP192.168.203.222
0xa473f408: 0x2a290c00 0xa8c04dfe 0x00009bcb 0x00000000这里的一部分内容被截断这个可能是一个ARP协议报文
0xa473f418: 0x00000000 0x00850200 0x0000b571 0x01010000
0xa473f428: 0x12005452 0x00045634 0x02ff0000 0x00000000
0xa473f438: 0x00000000 0x12ff0100 0x00005634 0x00000000
0xa473f448: 0x00000000 0x00000000 0x00000000 0x00000000
0xa473f458: 0x00000000 0x00000000 0x00000000 0x00000000
0xa473f468: 0x00000000 0x00000000 0x00000000 0x00000000
0xa473f478: 0x00000000 0x00000000 0x00000000 0x00000000
0xa473f488: 0x00000000 0x00000000 0x00000000 0x00000000
0xa473f498: 0x00000000 0x00000000 0x00000000 0x00000000
0xa473f4a8: 0x00000000 0x00000000