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 的實現。