Linux網橋工作原理與實現
Linux網橋工作原理與實現
Linux 的 網橋
是一種虛擬裝置(使用軟體實現),可以將 Linux 內部多個網路介面連線起來,如下圖所示:
而將網路介面連線起來的結果就是,一個網路介面接收到網路資料包後,會複製到其他網路介面中,如下圖所示:
如上圖所示,當網路介面A接收到資料包後,網橋
會將資料包複製並且傳送給連線到 網橋
的其他網路介面(如上圖中的網絡卡B和網絡卡C)。
Docker 就是使用 網橋
來進行容器間通訊的,我們來看看 Docker 是怎麼利用 網橋
來進行容器間通訊的,原理如下圖:
Docker 在啟動時,會建立一個名為 docker0
的 網橋
,並且把其 IP 地址設定為 172.17.0.1/16
veth-pair
來將容器與 網橋
連線起來,如上圖所示。而對於 172.17.0.0/16
網段的資料包,Docker 會定義一條 iptables NAT
的規則來將這些資料包的 IP 地址轉換成公網 IP 地址,然後通過真實網路介面(如上圖的 ens160
介面)傳送出去。
接下來,我們主要通過程式碼來分析 網橋
的實現。
網橋的實現
1. 網橋的建立
我們可以通過下面命令來新增一個名為 br0
的 網橋
裝置物件:
[root@vagrant]# brctl addbr br0
然後,我們可以通過命令 brctl show
來檢視系統中所有的 網橋
裝置列表,如下:
[root@vagrant]# brctl show
bridge name bridge id STP enabled interfaces
br0 8000.000000000000 no
docker0 8000.000000000000 no
當使用命令建立一個新的 網橋
裝置時,會觸發核心呼叫 br_add_bridge()
函式,其實現如下:
int br_add_bridge(char *name) { struct net_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->dev); // 把網橋註冊到網路裝置中 return 0; }
br_add_bridge()
函式主要完成以下幾個工作:
- 呼叫
new_nb()
函式建立一個網橋
裝置物件。 - 呼叫
__dev_get_by_name()
函式檢查裝置名是否已經被註冊過,如果註冊過返回錯誤資訊。 - 將
網橋
裝置物件新增到bridge_list
連結串列中,核心使用bridge_list
連結串列來儲存所有網橋
裝置。 - 呼叫
register_netdev()
將網橋設備註冊到網路裝置中。
從上面的程式碼可知,網橋
裝置使用了 net_bridge
結構來描述,其定義如下:
struct net_bridge
{
struct net_bridge *next; // 連線核心中所有的網橋物件
rwlock_t lock; // 鎖
struct net_bridge_port *port_list; // 網橋埠列表
struct net_device dev; // 網橋裝置資訊
struct net_device_stats statistics; // 資訊統計
rwlock_t hash_lock; // 用於鎖定CAM表
struct net_bridge_fdb_entry *hash[BR_HASH_SIZE]; // CAM表
struct timer_list tick;
/* STP */
...
};
在 net_bridge
結構中,比較重要的欄位為 port_list
和 hash
:
port_list
:網橋埠列表,儲存著繫結到網橋
的網路介面列表。hash
:儲存著以網路介面MAC地址
為鍵值,以網橋埠為值的雜湊表。
網橋埠
使用結構體 net_bridge_port
來描述,其定義如下:
struct net_bridge_port
{
struct net_bridge_port *next; // 指向下一個埠
struct net_bridge *br; // 所屬網橋裝置物件
struct net_device *dev; // 網路介面裝置物件
int port_no; // 埠號
/* STP */
...
};
而 net_bridge_fdb_entry
結構用於描述網路介面裝置 MAC地址
與 網橋埠
的對應關係,其定義如下:
struct net_bridge_fdb_entry
{
struct net_bridge_fdb_entry *next_hash;
struct net_bridge_fdb_entry **pprev_hash;
atomic_t use_count;
mac_addr addr; // 網路介面裝置MAC地址
struct net_bridge_port *dst; // 網橋埠
...
};
這三個結構的對應關係如下圖所示:
可見,要將 網路介面裝置
繫結到一個 網橋
上,需要使用 net_bridge_port
結構來關聯的,下面我們來分析怎麼將一個 網路介面裝置
繫結到一個 網橋
中。
網橋是工作在 TCP/IP 協議棧的第二層,也就是說,網橋能夠根據目標 MAC 地址對資料包進行廣播或者單播。當目標 MAC 地址能夠從網橋的 hash 表中找到對應的網橋埠,說明此資料包是單播的資料包,否則就是廣播的資料包。
2. 將網路介面繫結到網橋
要將一個 網路介面裝置
繫結到一個 網橋
上,可以使用以下命令:
[root@vagrant]# brctl addif br0 eth0
上面的命令讓網路介面 eth0
繫結到網橋 br0
上。
當呼叫命令將網路介面裝置繫結到網橋上時,核心會觸發呼叫 br_add_if()
函式來實現,其程式碼如下:
int br_add_if(struct net_bridge *br, struct net_device *dev)
{
struct net_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);
return 0;
}
br_add_if()
函式主要完成以下工作:
- 呼叫
new_nbp()
函式建立一個新的網橋埠
並且新增到網橋
的port_list
連結串列中。 - 將網路介面裝置設定為
混雜模式
。 - 呼叫
br_fdb_insert()
函式將新建的網橋埠
插入到網路介面MAC地址
對應的雜湊表中。
也就是說,br_add_if()
函式主要建立 網路介面裝置
與 網橋
的關係。
3. 網橋中的網路介面接收資料
當某個 網路介面
接收到資料包時,會判斷這個 網路介面
是否繫結到某個 網橋
上,如果綁定了,那麼就呼叫 handle_bridge()
函式處理這個資料包。handle_bridge()
函式實現如下:
static int __inline__
handle_bridge(struct sk_buff *skb, struct packet_type *pt_prev)
{
int ret = NET_RX_DROP;
...
br_handle_frame_hook(skb);
return ret;
}
br_handle_frame_hook
是一個函式指標,其指向 br_handle_frame()
函式,我們來分析 br_handle_frame()
函式的實現:
void br_handle_frame(struct sk_buff *skb)
{
struct net_bridge *br;
br = skb->dev->br_port->br; // 獲取裝置連線的網橋物件
read_lock(&br->lock); // 對網橋上鎖
__br_handle_frame(skb); // 呼叫__br_handle_frame()函式處理資料包
read_unlock(&br->lock);
}
br_handle_frame()
函式的實現比較簡單,首先對 網橋
進行上鎖操作,然後呼叫 __br_handle_frame()
處理資料包,我們來分析 __br_handle_frame()
函式的實現:
static void __br_handle_frame(struct sk_buff *skb)
{
struct net_bridge *br;
unsigned char *dest;
struct net_bridge_fdb_entry *dst;
struct net_bridge_port *p;
int passedup;
dest = skb->mac.ethernet->h_dest; // 目標MAC地址
p = skb->dev->br_port; // 網路介面繫結的埠
br = p->br;
passedup = 0;
...
// 將學習到的MAC地址插入到網橋的hash表中
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);
else
kfree_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地址插入到網橋的hash表中。
- 如果資料包是一個廣播包(目標MAC地址的第一位為1),那麼呼叫
br_flood()
函式把資料包傳送給連線到網橋上的所有網路介面。 - 呼叫
br_fdb_get()
獲取目標MAC地址對應的網橋埠,如果目標MAC地址對應的網橋埠存在,那麼呼叫br_forward()
函式把資料包轉發給此埠。 - 否則呼叫 呼叫
br_flood()
函式把資料包傳送給連線到網橋上的所有網路介面。
函式 br_forward()
用於把資料包傳送給指定的網橋埠,其實現如下:
static void __br_forward(struct net_bridge_port *to, struct sk_buff *skb)
{
skb->dev = to->dev;
dev_queue_xmit(skb);
}
void br_forward(struct net_bridge_port *to, struct sk_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()
函式用於將資料包傳送給繫結到 網橋
上的所有網路介面裝置,其實現如下:
void br_flood(struct net_bridge *br, struct sk_buff *skb, int clone)
{
struct net_bridge_port *p;
struct net_bridge_port *prev;
...
prev = NULL;
p = br->port_list;
while (p != NULL) { // 遍歷繫結到網橋的所有網路介面裝置
if (should_forward(p, skb)) { // 埠是否能夠接收資料包?
if (prev != NULL) {
struct sk_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()
函式將資料包轉發給裝置對應的埠。