Docker网络基础——Linux网桥工作原理与实现转载此文请联系Linux内核那些事儿公众号。Linux网桥是Linux内部可以连接多个网络接口的虚拟设备(通过软件实现),如下图所示:networkinterfaces,如下图所示:如上图所示,当networkinterfaceA收到数据包时,bridge会将数据包复制并发送给连接到bridge的其他networkinterface(如图以上网卡B和网卡C在网络中)。Docker使用网桥在容器之间进行通信。下面我们就来看看Docker是如何使用桥接器来实现容器间通信的。原理如下:Docker在启动时,会创建一个名为docker0的网桥,并将其IP地址设置为172.17.0.1/16(私有IP地址)。然后使用虚拟设备对veth-pair连接容器和网桥,如上图所示。对于172.17.0.0/16网段的数据包,Docker会定义一条iptablesNAT规则,将这些数据包的IP地址转换为公网IP地址,然后通过真实的网络接口(如上图中的ens160接口)。接下来我们主要通过代码来分析桥接器的实现。Bridge实现1.Bridge的创建我们可以使用如下命令添加一个名为br0的bridge设备对象:[root@vagrant]#brctladdbrbr0然后,我们可以使用命令brctlshow查看系统中所有的bridge设备列表如下如下:[root@vagrant]#brctlshowbridgenamebridgeidSTPenabledinterfacesbr08000.000000000000nodocker08000.000000000000no命令创建新桥接设备时,会触发内核调用br_add_bridge()函数,实现如下:intbr_add_bridge(char*name){structnet_bridge*br;if((br=new_nb(name))==NULL)//创建桥接设备对象return-ENOMEM;if(__dev_get_by_name(name)!=NULL){//设备名是否已经注册?kfree(br);return-EEXIST;//返回错误,同名设备不能重复注册}//加入网桥列表br->next=bridge_list;bridge_list=br;...register_netdev(&br->开发);//向网络设备注册网桥return0;}br_add_bridge()函数主要完成以下任务:调用new_nb()函数创建网桥设备对象。调用__dev_get_by_name()函数检查设备名是否已经注册,如果已经注册则返回错误信息。将桥设备对象添加到bridge_list链表中,内核使用bridge_list链表保存所有的桥设备。调用register_netdev()向网络设备注册桥接设备。从上面的代码可以看出,桥接设备是使用net_bridge结构体来描述的,其定义如下:structnet_bridge{structnet_bridge*next;//连接内核中所有的桥接对象rwlock_tlock;//lockstructnet_bridge_port*port_list;//bridgeportListstructnet_devicedev;//桥接设备信息structnet_device_statsstatistics;//信息统计rwlock_thash_lock;//用于锁定CAM表structnet_bridge_fdb_entry*hash[BR_HASH_SIZE];//CAM表structtimer_listtick;/*STP*/...};在net_bridge结构体中,比较重要的字段是port_list和hash:port_list:网桥端口列表,存放的是网桥绑定的网络接口列表。hash:存储一个哈希表,以网络接口的MAC地址为键,桥接端口为值。网桥端口用结构体net_bridge_port描述,定义如下:structnet_bridge_port{structnet_bridge_port*next;//指向下一个端口structnet_bridge*br;//属于网桥设备对象structnet_device*dev;//网络接口设备objectintport_no;//端口号/*STP*/...};net_bridge_fdb_entry结构体用于描述网络接口设备的MAC地址与桥接端口的对应关系。;//网络接口设备的MAC地址structnet_bridge_port*dst;//桥接端口...};这三个结构体的对应关系如下图所示:可见,要将网口设备绑定到网桥上,需要使用net_bridge_port结构体进行关联,下面我们来分析一下如何将网口设备绑定到网桥上.网桥工作在TCP/IP协议栈的第二层,也就是说网桥可以根据目的MAC地址广播或单播数据包。当目标MAC地址能从网桥的哈希表中找到对应的网桥端口时,说明该数据包为单播数据包,否则为广播数据包。2.将网络接口绑定到网桥要将网络接口设备绑定到网桥,可以使用以下命令:[root@vagrant]#brctladdifbr0eth0上面的命令将网络接口eth0绑定到网桥br0。当调用命令将网络接口设备绑定到网桥时,内核会触发br_add_if()函数的调用来实现,代码如下:intbr_add_if(structnet_bridge*br,structnet_device*dev){structnet_bridge_port*p;...write_lock_bh(&br->lock);//创建一个新的网桥端口对象,并将其添加到网桥的port_list列表中if((p=new_nbp(br,dev))==NULL){write_unlock_bh(&br->lock);dev_put(dev);return-EXFULL;}//设置网络接口设备为混杂模式dev_set_promiscuity(dev,1);...//添加到网络MAC地址对应的哈希表中接口和桥接端口br_fdb_insert(br,p,dev->dev_addr,1);...write_unlock_bh(&br->lock);return0;}br_add_if()函数主要完成以下任务:调用new_nbp()函数创建一个新的网桥端口并将其添加到网桥的port_list链表中。将网络接口设备设置为混杂模式。调用br_fdb_insert()函数将新建的桥接端口插入到网络接口MAC地址对应的哈希表中。也就是说,br_add_if()函数主要是建立网络接口设备和网桥之间的关系。3.网桥中的网络接口接收数据。当一个网络接口收到数据包时,会判断该网络接口是否绑定到某个网桥上。如果是绑定的,则调用handle_bridge()函数处理这个数据包。handle_bridge()函数实现如下:staticint__inline__handle_bridge(structsk_buff*skb,structpacket_type*pt_prev){intret=NET_RX_DROP;...br_handle_frame_hook(skb);returnret;}br_handle_frame_hook为函数指针,指向br_handle_frame_frame()函数,我们分析一下()函数实现:voidbr_handle_frame(structsk_buff*skb){structnet_bridge*br;br=skb->dev->br_port->br;//获取连接设备的桥对象read_lock(&br->lock);//YesBridgelock__br_handle_frame(skb);//调用__br_handle_frame()函数处理数据包read_unlock(&br->lock);}br_handle_frame()函数的实现比较简单。首先锁定网桥,然后调用__br_handle_frame()处理数据包,我们分析一下__br_handle_frame()函数的实现:;intpassedup;dest=skb->.ethernet->h_dest;//目的MAC地址p=skb->dev->br_port;//网络接口绑定的端口br=p->br;passedup=0;...//将学习到的MAC地址插入到网桥的哈希表中if(p->state==BR_STATE_LEARNING||p->state==BR_STATE_FORWARDING)br_fdb_insert(br,p,skb->mac.ethernet->h_source,0);...if(dest[0]&1){//如果是广播包br_flood(br,skb,1);//将包发送到所有连接到网桥的网络接口if(!passedup)br_pass_frame_up(br,skb);elsekfree_skb(skb);return;}dst=br_fdb_get(br,dest);//获取目标MAC地址对应的网桥端口...if(dst!=NULL){//如果目标MAC地址对应的桥端口存在br_forward(dst->dst,skb);//则只将数据包转发到该端口br_fdb_put(dst);return;}br_flood(br,skb,0);//否则发送给连接到这个网桥的所有网络接口return;...}__br_handle_frame()函数主要完成以下任务:首先,将从数据包中学习到的MAC地址插入到网桥中如果数据包哈希表中是一个广播数据包(目的MAC地址的第一位为1),然后调用br_flood()函数将数据包发送到连接到网桥的所有网络接口。调用br_fdb_get()获取目标MAC地址对应的桥接端口。如果目标MAC地址对应的桥接端口存在,则调用br_forward()函数将报文转发到该端口。否则调用br_flood()函数将数据包发送到连接到网桥的所有网络接口。br_forward()函数用于将数据包发送到指定的桥接端口,其实现如下:);}voidbr_forward(structnet_bridge_port*to,structsk_buff*skb){if(should_forward(to,skb)){//端口是否可以接收数据?__br_forward(to,skb);return;}kfree_skb(skb);}br_forward()函数调用_br_forward()函数用于向指定的桥接端口发送数据。__br_forward()函数首先将数据包的输出接口设备设置为桥接端口绑定的设备,然后调用dev_queue_xmit()函数将数据包发送出去。br_flood()函数用于向绑定到网桥的所有网络接口设备发送数据包,其实现如下:..prev=NULL;p=br->port_list;while(p!=NULL){//遍历桥上绑定的所有网络接口设备if(should_forward(p,skb)){//端口是否可以接收数据packet?if(prev!=NULL){structsk_buff*skb2;//克隆一个数据包if((skb2=skb_clone(skb,GFP_ATOMIC))==NULL){br->statistics.tx_dropped++;kfree_skb(skb);return;}__br_forward(prev,skb2);//发送数据包给设备}prev=p;}p=p->next;}if(prev!=NULL){__br_forward(prev,skb);return;kfree_skb(skb);}br_flood()函数的实现也比较简单,主要是遍历所有绑定到网桥的网络接口设备,然后调用__br_forward()函数将数据包转发到网桥对应的端口设备。
