桥接Linux协议栈实现——《TCP/IP协议栈源码阅读笔记》
2014-10-08 17:12
1646 查看
1. 网桥设备处理的基本流程
2. 重要数据结构
mac_addr:MAC地址。bridge_id:网桥ID。
net_bridge_fdb_entry:转发数据库的记录项。
net_bridge_port:网桥端口。
br_config_bpdu:入口配置BPDU的一些关键字段会复制到该数据结构中,此结构会将这些字段传给处理配置BPDU的函数,而不是由原BPDU来传递。
3. handle_bridge函数
3.1主要工作
捕获入口封包。3.2源码分析
#if defined(CONFIG_BRIDGE) || defined (CONFIG_BRIDGE_MODULE) ... ... static inline struct sk_buff *handle_bridge( struct sk_buff *skb,struct packet_type **pt_prev, int *ret, struct net_device *orig_dev){ struct net_bridge_port *port; if (skb->pkt_type == PACKET_LOOPBACK||(port = rcu_dereference(skb->dev->br_port)) == NULL) return skb; if (*pt_prev) { *ret = deliver_skb(skb, *pt_prev, orig_dev); *pt_prev = NULL; } return br_handle_frame_hook(port, skb); } #else #define handle_bridge(skb, pt_prev, ret, orig_dev)(skb) #endif
如上,如果内核被配置为支持桥接,handle_bridge函数就会被做如上定义,并且最后调用br_handle_frame_hook(port, skb)函数。其中钩子函数br_handle_frame_hook()在网桥模块初始化br_init( )函数中初始化:
br_handle_frame_hook = br_handle_frame;
若内核不支持桥接功能,则:
#define handle_bridge(skb, pt_prev, ret, orig_dev) (skb)
此时:if (!skb) 条件不满足,所以接着将帧交给协议栈。
4. br_handle_frame ( br_handle_frame_hook )函数
4.1概述:
通过br_handle_frame_hook函数调用的;正确处理帧,则返回NULL;
调用前,已经正确设置RCU锁了;
4.2br_handle_frame函数分析如下:
从skb取出以太网帧头,判读源MAC地址是否为多播地址或者0地址,实则丢弃该封包:const unsigned char *dest = eth_hdr(skb)->h_dest; int (*rhook)(struct sk_buff *skb); if (!is_valid_ether_addr(eth_hdr(skb)->h_source)) goto drop;
拷贝一个skb结构体,因为防止协议探嗅器正在使用skb结构体时,如果不复制一个skb结构体,就无法改变其中的值:
skb = skb_share_check(skb, GFP_ATOMIC); if (!skb) return NULL;
当收到的帧是BPDU帧时,且NF_BR_LOCAL_IN宏已正确定义,会经过此通道调用br_handle_local_finish函数。
if (unlikely(is_link_local(dest))) { /* Pause frames shouldn't be passed up by driver anyway */ if (skb->protocol == htons(ETH_P_PAUSE)) goto drop;
(1)没开启STP则跳转到FORWARD抓发:
/* If STP is turned off, then forward */ if (p->br->stp_enabled == BR_NO_STP && dest[5] == 0) goto forward;
(2)定义NF_BR_LOCAL_IN宏,则调用br_handle_local_finish函数处理BPDU:
if (NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, skb->dev, NULL, br_handle_local_finish)) return NULL; /* frame consumed by filter */ else return skb; /* continue processing */ }
根据参数port的端口状态state值做转发处理:
(3)如果端口处于转发态,则判断ebtalbes是否启用:br_should_route_hook在ebtables启用时才定义,ebtables可以用于对二层的数据报文进行规则匹配和过滤:
forward: switch (p->state) { case BR_STATE_FORWARDING: rhook = rcu_dereference(br_should_route_hook); if (rhook != NULL) { if (rhook(skb)) return skb; dest = eth_hdr(skb)->h_dest; }
(4) 端口的FORWARD状态(未开启ebtalbes)和LEARNING状态都会执行此段:如果数据包的目的地址和网桥的虚拟设备地址相同,则将数据包类型设为PACKET_HOST,也就是发往本地的数据。
case BR_STATE_LEARNING: if (!compare_ether_addr(p->br->dev->dev_addr, dest)) skb->pkt_type = PACKET_HOST;
(5)通过钩子函数调用br_handle_frame_finish函数
NF_HOOK(PF_BRIDGE, NF_BR_PRE_ROUTING, skb, skb->dev, NULL, br_handle_frame_finish); break; default:
(6)丢弃处理:
drop: kfree_skb(skb); } return NULL; }
5. br_handle_frame_finish函数真正开始处理入口帧
5.1概述
调用前已经正确设置RCU锁了;5.2函数解析
定义局部变量:const unsigned char *dest = eth_hdr(skb)->h_dest; struct net_bridge_port *p = rcu_dereference(skb->dev->br_port); struct net_bridge *br; struct net_bridge_fdb_entry *dst; struct sk_buff *skb2;
如果端口处于关闭状态或者设备未和该端口绑定,则丢弃该包(处理DISABLED状态,其他所有状态都会更新转发数据库):
if (!p || p->state == BR_STATE_DISABLED) goto drop;
根据源MAC地址更新转发database表项(又可以新增一个项目,也有可能只更新最近使用时间):
br = p->br; br_fdb_update(br, p, eth_hdr(skb)->h_source);
(4)如果端口状态是LEARNING状态,则丢弃该包:
if (p->state == BR_STATE_LEARNING) goto drop;
检查网卡NIC是否处于混杂模式,是则:
if (br->dev->flags & IFF_PROMISC){ skb2 = skb; dst = NULL;
判断目的地址是否为多播或广播:
(1)是,则更新多播统计信息,并且skb2=skb;
if (is_multicast_ether_addr(dest)) { br->dev->stats.multicast++; skb2 = skb;
(2)否则,根据目的MAC地址查找forward database,放入dst表项中,并判断是否为本地地址,是本地地址,则不转发;
} else if ((dst = __br_fdb_get(br, dest)) && dst->is_local) { skb2 = skb; /* Do not forward the packet since it's local. */ skb = NULL; } }
如果满足上述三者之一:NIC处于混杂模式、多播、本地地址,则克隆一个skb数据结构:
if (skb2 == skb) skb2 = skb_clone(skb, GFP_ATOMIC);
满足以上三种情况之一则表明帧需要传至本地:br_pass_frame_up:
if (skb2) br_pass_frame_up(br, skb2);
多播调用br_flood_forward函数扩散帧,否则调用br_forward转发帧;
6. br_pass_frame_up流程
6.1概述
br_pass_frame_up函数会更新一些统计信息,并且把skb->dev结构体改为网桥结构体:br->dev,然后通过钩子函数调用netif_receive_skb函数。6.2函数解析
更新统计数据和设备结构体指针:struct net_device *indev, *brdev = br->dev; brdev->stats.rx_packets++; brdev->stats.rx_bytes += skb->len; afde skb->dev = brdev;
通过NF_HOOK返回netif_receive_skb函数:
NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, indev, NULL, netif_receive_skb);
7. br_forward转发帧
7.1函数很简单,实现如下:
判断是否该转发,是则调用_ _br_forward转发,然后返回,否则,丢弃:是否skb缓冲区:7.2should_deliver(to, skb)函数
to:该转发的端口;
skb:是缓冲区数据;
该函数主要是测试是否应该讲报文发送出去,它由出口端的状态和报文的入口信息决定的。
入口设备不等于出口设备(skb->dev代表入口设备,p->dev代表出口端口设备),且出口端口处于转发态:
return (skb->dev != p->dev && p->state == BR_STATE_FORWARDING);
8. br_flood_forward函数
当br_handle_frame_finish函数发现L2层目的地址为多播或广播地址时,则会调用此函数br_flood_forward扩散帧。8.1函数解析
void br_flood_forward(struct net_bridge *br, struct sk_buff *skb) { br_flood(br, skb, __br_forward); }
9. br_flood函数
该函数主要用于扩散帧,其基本原理就是:遍历网桥设备除入口外的所有端口,并调用转发帧函数_ _br_forward函数,也就下面的_ _br_forward函数。9.1函数原型
static void br_flood(struct net_bridge *br, struct sk_buff *skb, void (*__packet_hook)(const struct net_bridge_port *p, struct sk_buff *skb))
br: 网桥设备结构体;
skb:要转发的帧;
_ _paclet_hook:函数指针,指转发帧函数(__br_forward)。
9.2函数解析
定义局部变量struct net_bridge_port *p; struct net_bridge_port *prev; prev = NULL;
变量br端口链表,并调用should_deliver函数判断是否该转发帧,是则转发:
list_for_each_entry_rcu(p, &br->port_list, list) { if (should_deliver(p, skb)) {//should_deliver函数可以保证入口端口不等于出口端口,且出口端口p处于FORWARDING状态。 if (prev != NULL) { struct sk_buff *skb2; if ((skb2 = skb_clone(skb, GFP_ATOMIC)) == NULL) { br->dev->stats.tx_dropped++; kfree_skb(skb); return; }// End: if ((skb2=skb_clone(skb,GFP_ATOMIC))==NULL) { __packet_hook(prev, skb2); }// End: if (prev != NULL) { prev = p; }// End: if (should_deliver(p, skb)) { } //End: list_for_each_entry_rcu(p, &br->port_list, list) { if (prev != NULL) { __packet_hook(prev, skb); return; } kfree_skb(skb);
10. _ _br_forward函数
gso处理struct net_device *indev; if (skb_warn_if_lro(skb)) { kfree_skb(skb); return; }
改变skb->dev为出口端口dev:
indev = skb->dev; skb->dev = to->dev;
校验和处理:调用skb_forward_csum(skb)函数。
skb_forward_csum(skb);
该函数实现如下:校验和已处理,不需要再计算!
if (skb->ip_summed == CHECKSUM_COMPLETE) skb->ip_summed = CHECKSUM_NONE;
通过NF_HOOK调用br_forward_finish函数
NF_HOOK(PF_BRIDGE, NF_BR_FORWARD, skb, indev, skb->dev, br_forward_finish);
11. br_forward_finish函数
直接调用钩子函数:int br_forward_finish(struct sk_buff *skb) { return NF_HOOK(PF_BRIDGE, NF_BR_POST_ROUTING, skb, NULL, skb->dev, br_dev_queue_push_xmit); }
12. br_dev_queue_push_xmit函数
12.1函数解析
判断skb->len是否大于PMTU,是则丢弃(GSO: Generic Segmentation Offload除外):if (packet_length(skb) > skb->dev->mtu && !skb_is_gso(skb)) kfree_skb(skb);
利用skb_push函数预留以太网帧头大小的缓冲区:
else { if (nf_bridge_maybe_copy_header(skb)) kfree_skb(skb); else { skb_push(skb, ETH_HLEN); }
调用dev_queue_xmit函数:
dev_queue_xmit(skb);
13. dev_queue_xmit函数(帧的传输一节会说明)
协议栈上层需要发送报文时,调用dev_queue_xmit(skb)函数。如果这个报文需要通过网桥设备来发送,则skb->dev指向一个网桥设备。网桥设备没有使用发送队列(dev->qdisc为空),所以dev_queue_xmit将直接调用dev->hard_start_xmit()函数,而网桥设备的hard_start_xmit等于函数br_dev_xmit;13.1函数解析
定义局部变量:struct net_device *dev = skb->dev; struct netdev_queue *txq; struct Qdisc *q; int rc = -ENOMEM;
gso处理:
/* GSO will handle the following emulations directly. */ if (netif_needs_gso(dev, skb)) goto gso;
首先判断skb是否被分段,如果分了段并且网卡不支持分散读的话,需要将所有段重新组合成一个段。这里_ _skb_linearize其实就是__pskb_pull_tail(skb, skb->data_len),这个函数基本上等同于pskb_may_pull。 pskb_may_pull的作用:检测skb对应的主buf中是否有足够的空间来pull出len长度,如果不够就重新分配skb并将frags中的数据拷贝入新分配的主buff中,而这里将参数len设置为skb->datalen,也就是会将所有的数据全部拷贝到主buff中,以这种方式完成skb的线性化 (其中skb_shinfo(skb)和_
_skb_linearize(skb)函数参见最后)。
if (skb_shinfo(skb)->frag_list && !(dev->features & NETIF_F_FRAGLIST) && _ _skb_linearize(skb)) goto out_kfree_skb;
如果上面已经线性化了一次,这里的__skb_linearize就会直接返回。注意区别frags和frag_list,前者是将多的数据放到单独分配的页面中,sk_buff只有一个。而后者则是连接多个sk_buff :
if (skb_shinfo(skb)->nr_frags &&(!(dev->features & NETIF_F_SG) || illegal_highdma(dev, skb)) && __skb_linearize(skb)) goto out_kfree_skb;
校验和处理:CHECKSUM_PARTIAL指的是校验和还未计算,并且设备不支持校验和计算,所以此次要计算校验和:
if (skb->ip_summed == CHECKSUM_PARTIAL) { skb_set_transport_header(skb, skb->csum_start - skb_headroom(skb)); if (!dev_can_checksum(dev, skb) && skb_checksum_help(skb)) goto out_kfree_skb; }
14. br_dev_xmit函数
dev_queue_xmit函数最终会调用dev->ops->ndo_start_xmit(skb,dev)设备发送函数;然而此时如果dev设备结构体实例是一个网桥设备,那么dev->ops->ndo_start_xmit(skb,dev)也就是hard_start_xmit函数实际上就是网桥设备发送函数br_dev_xmit。该函数在调用br_add_bridge创建一个网桥时调用。这样就会按照网桥设备大蓝图(图16-11)里右上角所示的流程执行。14.1哪种情况会出现调用br_dev_xmit执行
前面也讲过了,Linux下的bridge设备,对下层而言是一个桥设备,进行数据的转发(实际上对下也有接收能力,下一节讲)。而对上层而言,它就像普通的ethernet设备一样,有自己的IP和MAC地址,那么上层当然可以把它加入路由系统,并利用它发送数据啦,并且很容易想到,它的发射函数最终肯定是利用某个从设备的驱动去完成实际的发送的。上层根据目的IP地址,路由选择了该br_dev设备发送,并且由ARP缓存中得到了对应的目的MAC,填写在了skb中,然后启动了发送流程dev_queue_xmit(skb)。因为此时的skb->dev为br_dev,无queue,直接去调用br设备的发送函数,该函数就是br_netdev_ops中定义的br_dev_xmit(skb,br_dev)。如下图(b)所示就是这样一个情况:14.2函数解析
定义一些局部变量,并初始化:对于一个网桥设备的net_devcie结构体dev的私有数据priv指向一个网桥结构体struct net_bridge,所以可以通过下面方法获取:struct net_bridge *br = netdev_priv(dev); const unsigned char *dest = skb->data; struct net_bridge_fdb_entry *dst;
转发:
else if ((dst = __br_fdb_get(br, dest)) != NULL) br_deliver(dst->dst, skb); else br_flood_deliver(br, skb); return 0;
相关文章推荐
- 在linux上实现一个协议栈
- Linux 2.4.x 网络协议栈QoS模块(TC)的设计与实现
- vmware配置linux虚拟机之桥接模式 (实现宿主机和虚拟机互ping 虚拟机可上网)
- 了解linux网络协议栈(二)——协议栈实现
- Linux 2.4.x 网络协议栈QoS模块(TC)的设计与实现
- Linux 2.4.x 网络协议栈QoS模块(TC)的设计与实现
- 由PPPOE看Linux网络协议栈的实现
- Linux 2.4.x 网络协议栈QoS模块(TC)的设计与实现
- 由PPPOE看Linux网络协议栈的实现
- 了解linux网络协议栈(四)——协议栈实现
- Linux 2.4.x 网络协议栈QoS模块(TC)的设计与实现
- 由PPPOE看Linux网络协议栈的实现
- 由PPPOE看Linux网络协议栈的实现
- linux协议栈学习 第七节 GRO的实现
- 由PPPOE看Linux网络协议栈的实现
- Linux 2.4.x 网络协议栈QoS模块(TC)的设计与实现(转)
- Linux 2.4.x内核中网络协议栈QoS模块(TC)的设计与实现
- Linux 2.4.x 网络协议栈QoS模块(TC)的设计与实现
- Linux:宿主机通过桥接方式连接的VMware内部Linux14.04虚拟机(静态IP)实现上网方案
- 了解linux网络协议栈(五)——协议栈实现