Netfileter & iptables 實現(一)— Netfilter實現

語言: CN / TW / HK

在《 Netfilter & iptables 原理 》一文中,我們介紹了 Netfilter iptables 的原理,而本文主要通過源碼分析來介紹一下 Netfilter 與 iptables 的實現過程。

一、Netfilter 掛載點

我們先來回顧一下 Netfilter 的原理,Netfilter 是通過在網絡協議棧的不同階段註冊鈎子函數來實現對數據包的處理與過濾,如 圖1 所示:

(圖1 Netfilter掛載點)

在 圖1 中,藍色部分就是 Netfilter 掛載鈎子函數的位置,所以 Netfilter 定義了 5 個常量來表示這 5 個位置,如下代碼:

// 文件:include/linux/netfilter_ipv4.h


#define NF_IP_PRE_ROUTING 0
#define NF_IP_LOCAL_IN 1
#define NF_IP_FORWARD 2
#define NF_IP_LOCAL_OUT 3
#define NF_IP_POST_ROUTING 4

上面代碼中的常量與 圖1 中掛載鈎子函數的位置一一對應,如常量 NF_IP_PRE_ROUTING 對應着 圖1 的 PRE_ROUTING 處。

二、Netfilter 鈎子函數鏈

前面説過,Netfilter 是通過在網絡協議中的不同位置掛載鈎子函數來對數據包進行過濾和處理,而且每個掛載點能夠掛載多個鈎子函數,所以 Netfilter 使用鏈表結構來存儲這些鈎子函數,如 圖2 所示:

(圖2 Netfilter鈎子函數鏈)

如 圖2 所示,Netfilter 的每個掛載點都使用一個鏈表來存儲鈎子函數列表。在內核中,定義了一個名為 nf_hooks 的數組來存儲這些鏈表,如下代碼:

// 文件:net/core/netfilter.c


struct list_head nf_hooks[32][5];

struct list_head 結構是內核的通用鏈表結構。

nf_hooks 變量定義為一個二維數組,第一維是用來表示不同的協議(如 IPv4 或者 IPv6,本文只討論 IPv4,所以可以把 nf_hooks 當成是一維數組),而第二維用於表示不同的掛載點,如 圖2 中的 5 個掛載點。

三、鈎子函數

接下來我們介紹一下鈎子函數在 Netfilter 中的存儲方式。

前面我們介紹過,Netfilter 通過鏈表來存儲鈎子函數,而鈎子函數是通過結構 nf_hook_ops 來描述的,其定義如下:

// 文件:include/linux/netfilter.h


struct nf_hook_ops
{
struct list_head list; // 連接相同掛載點的鈎子函數
nf_hookfn *hook; // 鈎子函數指針
int pf; // 協議類型
int hooknum; // 鈎子函數所在鏈
int priority; // 優先級
};

下面我們對 nf_hook_ops 結構的各個字段進行説明:

  • list :用於把處於相同掛載點的鈎子函數鏈接起來。

  • hook :鈎子函數指針,就是用於處理或者過濾數據包的函數。

  • pf :協議類型,用於指定鈎子函數掛載在 nf_hooks 數組第一維的位置,如 IPv4 協議設置為 PF_INET

  • hooknum :鈎子函數所在鏈(掛載點),如 NF_IP_PRE_ROUTING

  • priority :鈎子函數的優先級,用於管理鈎子函數的調用順序。

其中 hook 字段的類型為 nf_hookfn nf_hookfn 類型的定義如下:

// 文件:include/linux/netfilter.h


typedef unsigned int nf_hookfn(unsigned int hooknum,
struct sk_buff **skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *));

我們也介紹一下 nf_hookfn 函數的各個參數的作用:

  • hooknum :鈎子函數所在鏈(掛載點),如 NF_IP_PRE_ROUTING

  • skb :數據包對象,就是要處理或者過濾的數據包。

  • in :接收數據包的設備對象。

  • out :發送數據包的設備對象。

  • okfn :當掛載點上所有的鈎子函數都處理過數據包後,將會調用這個函數來對數據包進行下一步處理。

四、註冊鈎子函數

當定義好一個鈎子函數結構後,需要調用 nf_register_hook 函數來將其註冊到 nf_hooks 數組中, nf_register_hook 函數的實現如下:

// 文件:net/core/netfilter.c


int nf_register_hook(struct nf_hook_ops *reg)
{
struct list_head *i;


br_write_lock_bh(BR_NETPROTO_LOCK); // 對 nf_hooks 進行上鎖


// priority 字段表示鈎子函數的優先級
// 所以通過 priority 字段來找到鈎子函數的合適位置
for (i = nf_hooks[reg->pf][reg->hooknum].next;
i != &nf_hooks[reg->pf][reg->hooknum];
i = i->next)
{
if (reg->priority < ((struct nf_hook_ops *)i)->priority)
break;
}


list_add(&reg->list, i->prev); // 把鈎子函數添加到鏈表中


br_write_unlock_bh(BR_NETPROTO_LOCK); // 對 nf_hooks 進行解鎖


return 0;
}

nf_register_hook 函數的實現比較簡單,步驟如下:

  • nf_hooks 進行上鎖操作,用於保護 nf_hooks 變量不受併發競爭。

  • 通過鈎子函數的優先級來找到其在鈎子函數鏈表中的正確位置。

  • 把鈎子函數插入到鏈表中。

  • nf_hooks 進行解鎖操作。

插入過程如 圖3 所示:

(圖3 鈎子函數插入過程)

如 圖3 所示,我們要把優先級為 20 的鈎子函數插入到 PRE_ROUTING 這個鏈中,而 PRE_ROUTING 鏈已經存在兩個鈎子函數,一個優先級為 10, 另外一個優先級為 30。

通過與鏈表中的鈎子函數的優先級進行對比,發現新的鈎子函數應該插入到優先級為 10 的鈎子函數後面,所以就 如圖3 所示就把新的鈎子函數插入到優先級為 10 的鈎子函數後面。

五、觸發調用鈎子函數

鈎子函數已經被保存到不同的鏈上,那麼什麼時候才會觸發調用這些鈎子函數來處理數據包呢?

要觸發調用某個掛載點上(鏈)的所有鈎子函數,需要使用 NF_HOOK 宏來實現,其定義如下:

// 文件:include/linux/netfilter.h


#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) \
(list_empty(&nf_hooks[(pf)][(hook)]) \
? (okfn)(skb) \
: nf_hook_slow((pf), (hook), (skb), (indev), (outdev), (okfn)))

首先介紹一下 NF_HOOK 宏的各個參數的作用:

  • pf :協議類型,就是 nf_hooks 數組的第一個維度,如 IPv4 協議就是 PF_INET

  • hook :要調用哪一條鏈(掛載點)上的鈎子函數,如 NF_IP_PRE_ROUTING

  • indev :接收數據包的設備對象。

  • outdev :發送數據包的設備對象。

  • okfn :當鏈上的所有鈎子函數都處理完成,將會調用此函數繼續對數據包進行處理。

NF_HOOK 宏的實現也比較簡單,首先判斷一下鈎子函數鏈表是否為空,如果是空的話,就直接調用 okfn 函數來處理數據包,否則就調用 nf_hook_slow 函數來處理數據包。我們來看看 nf_hook_slow 函數的實現:

// 文件:net/core/netfilter.c


int nf_hook_slow(int pf, unsigned int hook, struct sk_buff *skb,
struct net_device *indev, struct net_device *outdev,
int (*okfn)(struct sk_buff *))
{
struct list_head *elem;
unsigned int verdict;
int ret = 0;


elem = &nf_hooks[pf][hook]; // 獲取要調用的鈎子函數鏈表


// 遍歷鈎子函數鏈表,並且調用鈎子函數對數據包進行處理
verdict = nf_iterate(&nf_hooks[pf][hook], &skb, hook, indev, outdev, &elem, okfn);
...
// 如果處理結果為 NF_ACCEPT, 表示數據包通過所有鈎子函數的處理, 那麼就調用 okfn 函數繼續處理數據包
// 如果處理結果為 NF_DROP, 表示數據包被拒絕, 應該丟棄此數據包
switch (verdict) {
case NF_ACCEPT:
ret = okfn(skb);
break;
case NF_DROP:
kfree_skb(skb);
ret = -EPERM;
break;
}


return ret;
}

nf_hook_slow 函數的實現也比較簡單,過程如下:

  • 首先調用 nf_iterate 函數來遍歷鈎子函數鏈表,並調用鏈表上的鈎子函數來處理數據包。

  • 如果處理結果為 NF_ACCEPT ,表示數據包通過所有鈎子函數的處理, 那麼就調用 okfn 函數繼續處理數據包。

  • 如果處理結果為 NF_DROP ,表示數據包沒有通過鈎子函數的處理,應該丟棄此數據包。

既然 Netfilter 是通過調用 NF_HOOK 宏來調用鈎子函數鏈表上的鈎子函數,那麼內核在什麼地方調用這個宏呢?

比如數據包進入 IPv4 協議層的處理函數 ip_rcv 函數中就調用了 NF_HOOK 宏來處理數據包,代碼如下:

// 文件:net/ipv4/ip_input.c


int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt)
{
...
return NF_HOOK(PF_INET, NF_IP_PRE_ROUTING, skb, dev, NULL, ip_rcv_finish);
}

如上代碼所示,在 ip_rcv 函數中調用了 NF_HOOK 宏來處理輸入的數據包,其調用的鈎子函數鏈(掛載點)為 NF_IP_PRE_ROUTING 。而 okfn 設置為 ip_rcv_finish ,也就是説,當 NF_IP_PRE_ROUTING 鏈上的所有鈎子函數都成功對數據包進行處理後,將會調用 ip_rcv_finish 函數來繼續對數據包進行處理。

六、總結

本文主要介紹了 Netfilter 的實現,因為 Netfilter 是 Linux 網絡數據包過濾的框架,而 iptables 就是建立在 Netfilter 之上的。所以,先了解 Netfilter 的實現對分析 iptables 的實現有非常大的幫助。

而在下一章中,我們將會繼續分析 iptables 的實現。