Linux網橋模型:
Linux核心通過一個虛拟的網橋裝置來實作橋接的,這個裝置可以綁定若幹個以太網接口裝置,進而将它們橋接起來。如下圖所示:
網橋裝置br0綁定了eth0和eth1。對于網絡協定棧的上層來說,隻看得到br0,因為橋接是在資料鍊路層實作的,上層不需要關心橋接的細節。于是協定棧上層需要發送的封包被送到br0,網橋裝置的處理代碼再來判斷封包該被轉發到eth0或是eth1,或者兩者皆是;反過來,從eth0或從eth1接收到的封包被送出給網橋的處理代碼,在這裡會判斷封包該轉發、丢棄、或送出到協定棧上層。
而有時候eth0、eth1也可能會作為封包的源位址或目的位址,直接參與封包的發送與接收(進而繞過網橋)。
相關資料結構:
其中最左邊的net_device是一個代表網橋的虛拟裝置結構,它關聯了一個net_bridge結構,這是網橋裝置所特有的資料結構。
在net_bridge結構中,port_list成員下挂一個連結清單,連結清單中的每一個節點(net_bridge_port結構)關聯到一個真實的網口裝置的net_device。網口裝置也通過其br_port指針做反向的關聯(那麼顯然,一個網口最多隻能同時被綁定到一個網橋)。
net_bridge結構中還維護了一個hash表,是用來處理位址學習的。當網橋準備轉發一個封包時,以封包的目的Mac位址為key,如果可以在hash表中索引到一個net_bridge_fdb_entry結構,通過這個結構能找到一個網口裝置的net_device,于是封包就應該從這個網口轉發出去;否則,封包将從所有網口轉發。
網橋資料包的處理流程:
接收過程:
對于資料包的處理流程并沒有明顯的主線,主要就是根據核心代碼中網橋部分的源碼進行分析。
網口裝置接收到的封包最終通過net_receive_skb函數被網絡協定棧所接收。這個函數主要做三件事情:1、如果有抓包程式需要skb,将skb複制給它們;2、處理橋接;3、将skb送出給網絡層。
點選(此處)折疊或打開
int netif_receive_skb(struct sk_buff *skb)
{
......
if (handle_bridge(&skb, &pt_prev, &ret, orig_dev))
goto out;
}
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;
//對于回環裝置以及skb->dev->br_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);
br_handle_frame_hook在網橋初始化子產品br_init(void)函數中被指派.
br_handle_frame_hook = br_handle_frame;
是以網橋對于資料包的處理過程是從br_handle_frame開始的。
struct sk_buff *br_handle_frame(struct net_bridge_port *p, struct sk_buff *skb)
const unsigned char *dest = eth_hdr(skb)->h_dest;
int (*rhook)(struct sk_buff *skb);
//判斷是否為有效的實體位址,非全0位址以及非廣播位址
if (!is_valid_ether_addr(eth_hdr(skb)->h_source))
goto drop;
//判斷skb包是否被共享skb->users != 1,若是,則複制一份,否則直接傳回
skb = skb_share_check(skb, GFP_ATOMIC);
if (!skb)
return NULL;
//這個函數并非像想象的那樣,判斷是否為本地位址
//而是在判斷是否為鍊路本地多點傳播位址,01:80:c2:00:00:0x
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;
/* If STP is turned off, then forward */
if (p->br->stp_enabled == BR_NO_STP && dest[5] == 0)
goto forward;
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 */
forward:
switch (p->state) {
case BR_STATE_FORWARDING:
//如果網橋處于forwarding狀态,并且該封包必須要走L3層進行轉發,則直接傳回
//br_should_route_hook鈎子函數在ebtable裡面設定為ebt_broute函數,它根據使用者的規
//決定該封包是否要通過L3層來轉發;一般rhook為空
rhook = rcu_dereference(br_should_route_hook);
if (rhook != NULL) {
if (rhook(skb))
return skb;
dest = eth_hdr(skb)->h_dest;
}
/* fall through */
case BR_STATE_LEARNING:
//如果資料包的目的mac位址為虛拟網橋裝置的mac位址,則标記為host
if (!compare_ether_addr(p->br->dev->dev_addr, dest))
skb->pkt_type = PACKET_HOST;
//調用網橋在NF_BR_PREROUTING處挂載的鈎子函數,因為網橋在其鈎子函數過//程中嵌套調用了INET層BR_PREROUTING的鈎子函數,過程有些曲折,故最後//再分析
NF_HOOK(PF_BRIDGE, NF_BR_PRE_ROUTING, skb, skb->dev, NULL,
br_handle_frame_finish);
break;
default:
drop:
kfree_skb(skb);
return NULL;
FORWARDING以及LEARNING為網橋的狀态,網橋端口一般有5種狀态:
1) disable 被管理者禁用
2) blcok 休息,不參與資料包轉發
3) listening 監聽
4) learning 學習ARP資訊,準備向工作狀态改變
5) forwarding 正常工作,轉發資料包
/* note: already called with rcu_read_lock (preempt_disabled) */
int br_handle_frame_finish(struct sk_buff *skb)
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;
//判斷網橋狀态
if (!p || p->state == BR_STATE_DISABLED)
/* insert into forwarding database after filtering to avoid spoofing */
//br為虛拟網橋結構
br = p->br;
//根據資料包的源實體位址,更新網橋的轉發表
br_fdb_update(br, p, eth_hdr(skb)->h_source);
if (p->state == BR_STATE_LEARNING)
/* The packet skb2 goes to the local host (NULL to skip). */
//skb2資料包用于傳遞本機,skb資料包則用于forward
skb2 = NULL;
//如果網口處于混雜模式,複制一份傳遞主機
if (br->dev->flags & IFF_PROMISC)
skb2 = skb;
dst = NULL;
//如果為廣播資料包,增加計數,同樣需要發一份給主機
if (is_multicast_ether_addr(dest)) {
br->dev->stats.multicast++;
/*根據網橋口以及目标位址判斷是否為本機資料包*/
else if ((dst = __br_fdb_get(br, dest)) && dst->is_local) {
/* Do not forward the packet since it's local. */
skb = NULL;
if (skb2 == skb)
skb2 = skb_clone(skb, GFP_ATOMIC);
if (skb2)
/*完成将資料包傳遞給本機的工作*/
br_pass_frame_up(br, skb2);
if (skb) {
if (dst)
//如果存在目的位址則将其轉發
br_forward(dst->dst, skb);
//否則,flood資料包,向除接收網口外的其餘網口發送該資料包
br_flood_forward(br, skb);
out:
return 0;
kfree_skb(skb);
goto out;
//此函數主要實作通過網橋接收發往本機的資料包
static void br_pass_frame_up(struct net_bridge *br, struct sk_buff *skb)
struct net_device *indev, *brdev = br->dev;
//完成資料包的統計計數
brdev->stats.rx_packets++;
brdev->stats.rx_bytes += skb->len;
//将skb的dev改變為網橋結構的brdev
//此時skb的dev選項由實際網絡裝置eth0等改變為虛拟網橋裝置br0
indev = skb->dev;
skb->dev = brdev;
//重新走資料包接收流程,netif_receive_skb
//但因為dev的改變,dev的br_port字段不再為空,不會重走網橋流程,直接傳遞
NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, indev, NULL,
netif_receive_skb);
下面看一下轉發資料包的流程,對于flood_forward的流程,同樣通過br_forward來實作,隻不過改為循環周遊hash表中的裝置,對于每一個裝置都調用一次br_forward流程。
/* called with rcu_read_lock */
void br_forward(const struct net_bridge_port *to, struct sk_buff *skb)
/*如果skb->dev 不等于網橋的dev,同時網橋狀态為forwarding,則進行轉發*/
if (should_deliver(to, skb)) {
__br_forward(to, skb);
return;
static void __br_forward(const struct net_bridge_port *to, struct sk_buff *skb)
struct net_device *indev;
if (skb_warn_if_lro(skb)) {
//将skb的dev字段改為查找到的出口dev字段
skb->dev = to->dev;
skb_forward_csum(skb);
//周遊執行NF_BR_FORWARD鈎子函數
NF_HOOK(PF_BRIDGE, NF_BR_FORWARD, skb, indev, skb->dev,
br_forward_finish);
int br_forward_finish(struct sk_buff *skb)
//繼續跑NF_BR_POST_ROUTING處的鈎子函數
return NF_HOOK(PF_BRIDGE, NF_BR_POST_ROUTING, skb, NULL, skb->dev,
br_dev_queue_push_xmit);
int br_dev_queue_push_xmit(struct sk_buff *skb)
/* drop mtu oversized packets except gso */
/*如果skb資料包的長度大于MTU值,則丢棄*/
if (packet_length(skb) > skb->dev->mtu && !skb_is_gso(skb))
else {
/* ip_refrag calls ip_fragment, doesn't copy the MAC header. */
if (nf_bridge_maybe_copy_header(skb))
kfree_skb(skb);
else {
skb_push(skb, ETH_HLEN);
// 此時skb的dev已經替換成進行轉發的dev了,dev_queue_xmit将使
//用該網口裝置的發送函數完成資料包的發送
dev_queue_xmit(skb);
發送過程:
協定棧上層需要發送封包時,調用dev_queue_xmit(skb)函數。如果這個封包需要通過網橋裝置來發送,則skb->dev指向一個網橋裝置。網橋裝置沒有使用發送隊列(dev->qdisc為空),是以dev_queue_xmit将直接調用dev->hard_start_xmit函數,而網橋裝置的hard_start_xmit等于函數br_dev_xmit;
/* net device transmit always called with no BH (preempt_disabled) */
/*br_dev_xmit為網橋裝置的資料包發送函數*/
int br_dev_xmit(struct sk_buff *skb, struct net_device *dev)
struct net_bridge *br = netdev_priv(dev);
const unsigned char *dest = skb->data;
dev->stats.tx_packets++;
dev->stats.tx_bytes += skb->len;
skb_reset_mac_header(skb);
skb_pull(skb, ETH_HLEN);
/*如果為廣播位址,則flood該資料包
* 如果能夠根據skb中的目的mac位址查找到對應的網口,則通過br_deliver發送該資料包
* 如果查找不到,同樣flood該資料包
*/
if (dest[0] & 1)
br_flood_deliver(br, skb);
else if ((dst = __br_fdb_get(br, dest)) != NULL)
br_deliver(dst->dst, skb);
else
br_flood_deliver函數的實作過程,同樣是周遊hash表,對于每一個網口裝置都調用一次__br_deliver,是以下面就主要看一下br_deliver函數的流程
void br_deliver(const struct net_bridge_port *to, struct sk_buff *skb)
__br_deliver(to, skb);
static void __br_deliver(const struct net_bridge_port *to, struct sk_buff *skb)
/*将skb中的dev改成出口裝置所對應的dev*/
NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_OUT, skb, NULL, skb->dev,
/*最終仍然通過br_dev_queue_push_xmit完成資料包的發送過程*/
至此,整個網橋中資料的處理流程已經完全結束了。
Netfilter:
對于網橋中的netfilter的鈎子函數的排程過程有些曲折,對于INET層的鈎子函數全部被嵌套進BRIDGE層鈎子函數的運作流程中。
下面首先來看一下網橋一共挂載了哪些鈎子函數
static struct nf_hook_ops br_nf_ops[] __read_mostly = {
{ .hook = br_nf_pre_routing,
.owner = THIS_MODULE,
.pf = PF_BRIDGE,
.hooknum = NF_BR_PRE_ROUTING,
.priority = NF_BR_PRI_BRNF, },
{ .hook = br_nf_local_in,
.hooknum = NF_BR_LOCAL_IN,
{ .hook = br_nf_forward_ip,
.hooknum = NF_BR_FORWARD,
.priority = NF_BR_PRI_BRNF - 1, },
{ .hook = br_nf_forward_arp,
{ .hook = br_nf_local_out,
.hooknum = NF_BR_LOCAL_OUT,
.priority = NF_BR_PRI_FIRST, },
{ .hook = br_nf_post_routing,
.hooknum = NF_BR_POST_ROUTING,
.priority = NF_BR_PRI_LAST, },
//以上為BRIDGE層挂載的鈎子函數,一下為INET層挂載的鈎子函數
{ .hook = ip_sabotage_in,
.pf = PF_INET,
.hooknum = NF_INET_PRE_ROUTING,
.priority = NF_IP_PRI_FIRST, },
.pf = PF_INET6,
.priority = NF_IP6_PRI_FIRST, },
};
其實在這些鈎子函數内部并沒有涉及資料包操作的具體流程,大多是一些條件判斷和驗證的工作,現在就拿發往本機的資料包在作為一個例子,看一下這個資料包在netfilter中的流經過程(自認為還算有代表意義)。
對于發往本機網橋口的skb包,其skb->dev->br_port不為空,是以會進入網橋的處理流程,br_handler_frame,在此函數的退出處會首先運作NF_BR_PRE_ROUTING的鈎子函數
NF_HOOK(PF_BRIDGE, NF_BR_PRE_ROUTING, skb, skb->dev, NULL,
網橋挂載在BR_PRE_ROUTING的函數為br_nf_pre_routing
在此函數内部首先完成ipv4及ipv6資料的分流,這裡隻考慮ipv4資料,接着進行ipv4資料正确性的驗證,以及網橋标記nf_bridge->mask |= BRNF_NF_BRIDGE_PREROUTING;(下面還要用到此标記)在函數的最後通過NF_HOOK(PF_INET, NF_INET_PRE_ROUTING, skb, skb->dev, NULL,br_nf_pre_routing_finish)完成對于INET層PRE_ROUTING鈎子函數的周遊。
周遊結束後進入br_nf_pre_routing_finish函數,這個函數首先對資料包打标記,nf_bridge->mask ^= BRNF_NF_BRIDGE_PREROUTING;(跟上一個标記對應)主要處理skb包需要進行dnat的情形(貌似,沒有細看),在函數的最後
NF_HOOK_THRESH(PF_BRIDGE, NF_BR_PRE_ROUTING, skb, skb->dev, NULL,
br_handle_frame_finish, 1);
在這裡通過NF_HOOK_THRESH而非NF_HOOK的意思是因為,NF_HOOK_THRESH是從目前優先級的下一個函數開始周遊所有的鈎子函數,在這裡也就是說從br_nf_pre_routing函數的下一個優先級函數開始周遊BR_PRE_ROUTING的後續函數,雖然目前網橋隻挂載了這一個函數,但是這屬于Netfilter的機制問題。
接着,在跑完BR_PRE_ROUTING的剩餘函數後就進入br_handle_frame_finish函數,對于本機資料包則繼續調用br_pass_frame_up函數,在br_pass_frame_up函數中将skb的dev字段修改為虛拟網橋的dev,最後NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, indev, NULL, netif_receive_skb);将資料包重新跑skb的接收流程,由于網橋裝置的dev->br_port不為空,是以不會再進入網橋處理流程了。
那網橋是怎樣防止同一個skb包跑兩次INET_PRE_ROUTING的流程呢,主要就是通過挂在在INET_PRE_ROUTING出優先級最高的NF_IP_PRI_FIRST處的函數ip_sabotage_in以及上文特殊提到的兩個打标記的代碼來完成的。
static unsigned int ip_sabotage_in(unsigned int hook, struct sk_buff *skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
/*此鈎子函數用于確定發往本機的skb包隻跑一次INET_PRE_ROUTING的鈎子函數
* 在第一次進入BR_PRE_ROUTING時打上标記mask,在BR_PRE_ROUTING的鈎子函數中周遊INET_PRE_ROUTING的鈎子函數
* 在br_pre_routing結束的時候,異或該mask值,這樣就使得發往本機的skb包在第二次傳遞的時候可以不再跑鈎子
if (skb->nf_bridge &&
!(skb->nf_bridge->mask & BRNF_NF_BRIDGE_PREROUTING)) {
return NF_STOP;
return NF_ACCEPT;
NF_STOP的意思就是說該skb包就立即結束檢查而接受,不再進傳入連結表中後續的hook節點。
整個網橋子產品大體就是這個樣子,剩下的還有網橋的生成樹協定,這周打算把這個搞懂。。。
第一次寫分析,自己給自己加油,繼續努力~