Docker網路基礎 - Linux網橋工作原理與實現

語言: CN / TW / HK

Linux 的 網橋 是一種虛擬裝置(使用軟體實現),可以將 Linux 內部多個網路介面連線起來,如下圖所示:

而將網路介面連線起來的結果就是,一個網路介面接收到網路資料包後,會複製到其他網路介面中,如下圖所示:

如上圖所示,當網路介面A接收到資料包後, 網橋 會將資料包複製並且傳送給連線到  網橋 的其他網路介面(如上圖中的網絡卡B和網絡卡C)。

Docker 就是使用 網橋 來進行容器間通訊的,我們來看看 Docker 是怎麼利用  網橋 來進行容器間通訊的,原理如下圖:

Docker 在啟動時,會建立一個名為 docker0 的  網橋 ,並且把其 IP 地址設定為  172.17.0.1/16 (私有 IP 地址)。然後使用虛擬裝置對  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() 函式將資料包轉發給裝置對應的埠。