走進Linux核心之Netfilter網路框架

語言: CN / TW / HK

theme: smartblue

走進Linux核心之Netfilter網路框架

本文正在參與 “走過Linux三十年”話題徵文活動

筆者此前對Linux核心相關模組稍有研究,實現核心級通訊加密、影片流加密等,涉及:Linux核心網路協議棧、Linux核心通訊模組、Linux核心加密模組、祕鑰生成分發等。
後續考慮開設Linux核心專欄。

話不多說直接上才藝,現在帶你走進Linux核心之Netfilter網路框架。


一、概述:Netfilter是什麼

對於不經常接觸Linux核心的應用層開發者來說,可能對Netfilter瞭解的比較少。但大多數Linux使用者多少都用過或知道iptables,然而,iptables的功能實現就是在Netfilter之上完成的。

Netfilter是 Linux 核心中的一個框架,它為以定製處理器形式實施的各種網路相關操作提供了靈活性。Netfilter提供資料包過濾、網路地址翻譯和埠翻譯的各種選項。

1.Netfilter構成

其詳細組成: image.png

Netfilter是 Linux 核心中進行資料包過濾,連線跟蹤(Connect Track),網路地址轉換(NAT)等功能的主要實現框架;該框架在網路協議棧處理資料包的關鍵流程中定義了一系列鉤子點(Hook 點),並在這些鉤子點中註冊一系列函式對資料包進行處理。這些註冊在鉤子點的函式即為設定在網路協議棧內的資料包通行策略,換句話說就是,這些函式可以決定核心是接受還是丟棄某個資料包,函式的處理結果決定網路資料包的“命運”。

從圖中我們可以看到,Netfilter 框架採用模組化設計理念,並且貫穿了 Linux 系統的核心態和使用者態。

在使用者態層面,根據不同的協議型別,為上層使用者提供了不同的系統呼叫工具,比如我們常用的針對IPv4協議iptables,IPv6 協議的ip6tables,針對ARP協議的arptables,針對網橋控制的ebtables,針對網路連線追蹤的conntrack等。

不同的使用者態工具在核心中有對應的模組進行實現,而底層都需要呼叫 Netfilter hook API 介面進行實現。

同時也發現,之前提到的iptables,Linux防火牆工具其實也是 Netfilter 框架中的一個元件。

image.png

2.Netfilter資料包路徑

正常資料包在Netfilter中的路徑:

image.png


二、Netfilter實現

Netfilter Hooks in the Linux Kernel

1.Netfilter掛載點:Netfilter places

(1)函式定義

從上面網路包傳送接受流程圖中看出,可以在不同的地方註冊Nefilter的hook函式.由如下定義:

```c // include/linux/netfilter.h

enum nf_inet_hooks { NF_INET_PRE_ROUTING, NF_INET_LOCAL_IN, NF_INET_FORWARD, NF_INET_LOCAL_OUT, NF_INET_POST_ROUTING, NF_INET_NUMHOOKS }; ```

  • NF_INET_PRE_ROUTING: incoming packets pass this hook in the () function before they are processed by the routing code. ip_rcv()``linux/net/ipv4/ip_input.c
  • NF_INET_LOCAL_IN: all incoming packets addressed to the local computer pass this hook in the function . ip_local_deliver()
  • NF_INET_FORWARD: incoming packets are passed this hook in the function . ip_forwared()
  • NF_INET_LOCAL_OUT: all outgoing packets created in the local computer pass this hook in the function . ip_build_and_send_pkt()
  • NF_INET_POST_ROUTING: this hook in the ipfinishoutput() function before they leave the computer.

(2)掛載點分析

Netfilter 通過向核心協議棧中不同的位置註冊 鉤子函式(Hooks) 來對資料包進行過濾或者修改操作,這些位置稱為 掛載點,主要有 5 個:PRE_ROUTINGLOCAL_INFORWARDLOCAL_OUTPOST_ROUTINGimage.png

掛載點解析:

  • PRE_ROUTING:路由前。資料包進入IP層後,但還沒有對資料包進行路由判定前。
  • LOCAL_IN:進入本地。對資料包進行路由判定後,如果資料包是傳送給本地的,在上送資料包給上層協議前。
  • FORWARD:轉發。對資料包進行路由判定後,如果資料包不是傳送給本地的,在轉發資料包出去前。
  • LOCAL_OUT:本地輸出。對於輸出的資料包,在沒有對資料包進行路由判定前。
  • POST_ROUTING:路由後。對於輸出的資料包,在對資料包進行路由判定後。

路由判定:

從上圖可以看出,路由判定是資料流向的關鍵點。

  • 第一個路由判定通過查詢輸入資料包 IP頭部 的目的 IP地址 是否為本機的 IP地址,如果是本機的 IP地址,說明資料是傳送給本機的。否則說明資料包是傳送給其他主機,經過本機只是進行中轉。
  • 第二個路由判定根據輸出資料包 IP頭部 的目的 IP地址 從路由表中查詢對應的路由資訊,然後根據路由資訊獲取下一跳主機(或閘道器)的 IP地址,然後進行資料傳輸。

資料包流向 從圖中可以看到,三個方向的資料包需要經過的鉤子節點不完全相同:

  • 發往本地:NF_INET_PRE_ROUTING-->NF_INET_LOCAL_IN
  • 轉發:NF_INET_PRE_ROUTING-->NF_INET_FORWARD-->NF_INET_POST_ROUTING
  • 本地發出:NF_INET_LOCAL_OUT-->NF_INET_POST_ROUTING

(3)掛載連結串列

通過向這些 掛載點 註冊鉤子函式,就能夠對處於不同階段的資料包進行過濾或者修改操作。由於鉤子函式能夠註冊多個,所以核心使用連結串列來儲存這些鉤子函式。當資料包進入本地(LOCAL_IN 掛載點)時,就會相繼呼叫ipt_hookfw_confirm 鉤子函式來處理資料包。另外,鉤子函式還有優先順序,優先順序越小越先執行。正因為掛載點是通過連結串列來儲存鉤子函式,所以掛載點又被稱為 ,掛載點對應的鏈名稱如下所示:

  • LOCAL_IN 掛載點:又稱為 INPUT鏈
  • LOCAL_OUT 掛載點:又稱為 OUTPUT鏈
  • FORWARD 掛載點:又稱為 PORWARD鏈
  • PRE_ROUTING 掛載點:又稱為 PREROUTING鏈
  • POST_ROUTING 掛載點:又稱為 POSTOUTING鏈

Netfilter 定義了 5 個常量來表示這 5 個位置,如下程式碼:

```c // 檔案: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

```

2.註冊鉤子函式:Register the hooks

註冊和解註冊鉤子函式:Register the hooks

(1)註冊和解註冊鉤子函式

kernel 提供如下函式來註冊和解除hook函式.

```c // include/linux/netfilter.h / Function to register/unregister hook points. /

int nf_register_hook(struct nf_hook_ops reg); void nf_unregister_hook(struct nf_hook_ops reg); int nf_register_hooks(struct nf_hook_ops reg, unsigned int n); void nf_unregister_hooks(struct nf_hook_ops reg, unsigned int n); ```

這些函式用於將自定義的鉤子操作(struct nf_hook_ops)註冊到指定的鉤子節點中。

(2)鉤子操作資料結構

其中結構如下: nf_hook_ops

```c struct nf_hook_ops { struct list_head list;

    /* User fills in from here down. */
    nf_hookfn *hook;
    struct module *owner;
    u_int8_t pf;
    unsigned int hooknum;
    /* Hooks are ordered in ascending priority. */
    int priority;

}; ``` 這個結構體中儲存了自定義的鉤子函式(nf_hookfn),函式優先順序(priority),處理協議型別(pf),鉤子函式生效的鉤子節點(hooknum)等資訊。

(3)註冊鉤子函式

當定義好一個鉤子函式結構後,需要呼叫 nf_register_hook 函式來將其註冊到 nf_hooks 陣列中,nf_register_hook 函式的實現如下:

```c // 檔案: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.宣告鉤子函式:hook functions

其中hook函式由 指定,其函式宣告如下: nf_hookfn *hook

```c // 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 *)); ```

它返回如下結果之一:

```c //

define NF_DROP 0

define NF_ACCEPT 1

define NF_STOLEN 2

define NF_QUEUE 3

define NF_REPEAT 4

define NF_STOP 5

define NF_MAX_VERDICT NF_STOP

```

4.處理協議型別:pf

pf (protocol family) 是協議系列的識別符號.

enum { NFPROTO_UNSPEC = 0, NFPROTO_IPV4 = 2, NFPROTO_ARP = 3, NFPROTO_BRIDGE = 7, NFPROTO_IPV6 = 10, NFPROTO_DECNET = 12, NFPROTO_NUMPROTO, };

5.鉤子標識:hooknum

鉤子識別符號,每個協議系列的所有有效識別符號都在標頭檔案中定義。

例如:
<linux/netfilter_ipv4.h>

```c / IP Hooks / / After promisc drops, checksum checks. /

define NF_IP_PRE_ROUTING 0

/ If the packet is destined for this box. /

define NF_IP_LOCAL_IN 1

/ If the packet is destined for another interface. /

define NF_IP_FORWARD 2

/ Packets coming from a local process. /

define NF_IP_LOCAL_OUT 3

/ Packets about to hit the wire. /

define NF_IP_POST_ROUTING 4

define NF_IP_NUMHOOKS 5

```

6.鉤子優先順序:priority

鉤子的優先順序,每個協議系列的所有有效識別符號都在標頭檔案中定義。

例如:
<linux/netfilter_ipv4.h>

c enum nf_ip_hook_priorities { NF_IP_PRI_FIRST = INT_MIN, NF_IP_PRI_CONNTRACK_DEFRAG = -400, NF_IP_PRI_RAW = -300, NF_IP_PRI_SELINUX_FIRST = -225, NF_IP_PRI_CONNTRACK = -200, NF_IP_PRI_MANGLE = -150, NF_IP_PRI_NAT_DST = -100, NF_IP_PRI_FILTER = 0, NF_IP_PRI_SECURITY = 50, NF_IP_PRI_NAT_SRC = 100, NF_IP_PRI_SELINUX_LAST = 225, NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX, NF_IP_PRI_LAST = INT_MAX, };

c enum { NFPROTO_UNSPEC = 0, NFPROTO_IPV4 = 2, NFPROTO_ARP = 3, NFPROTO_BRIDGE = 7, NFPROTO_IPV6 = 10, NFPROTO_DECNET = 12, NFPROTO_NUMPROTO, };

7.觸發呼叫鉤子函式

鉤子函式已經被儲存到不同的鏈上,什麼時候才會觸發呼叫這些鉤子函式來處理資料包?要觸發呼叫某個掛載點上(鏈)的所有鉤子函式,需要使用 NF_HOOK 巨集來實現,其定義如下: ```c // 檔案: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 函式的實現:

```c // 檔案: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 巨集來處理資料包,程式碼如下:

```c // 檔案: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應用案例

如下為在網路上找到的一個核心模組 Demo,該模組的基本功能是將經過 IPv4 網路層 NF_INET_LOCAL_IN 節點的資料包的源 Mac 地址,目的 Mac 地址以及源 IP,目的 IP 打印出來,原始碼包下載.NF_INET_LOCAL_IN

程式碼如下所示: ```c

include

include

include

include

include

include

include

include

include

MODULE_LICENSE("GPLv3"); MODULE_AUTHOR("SHI"); MODULE_DESCRIPTION("Netfliter test");

static unsigned int nf_test_in_hook(unsigned int hook, struct sk_buff skb, const struct net_device in, const struct net_device out, int (okfn)(struct sk_buff*));

static struct nf_hook_ops nf_test_ops[] __read_mostly = { { .hook = nf_test_in_hook, .owner = THIS_MODULE, .pf = NFPROTO_IPV4, .hooknum = NF_INET_LOCAL_IN, .priority = NF_IP_PRI_FIRST, }, };

void hdr_dump(struct ethhdr *ehdr) { printk("[MAC_DES:%x,%x,%x,%x,%x,%x" "MAC_SRC: %x,%x,%x,%x,%x,%x Prot:%x]\n", ehdr->h_dest[0],ehdr->h_dest[1],ehdr->h_dest[2],ehdr->h_dest[3], ehdr->h_dest[4],ehdr->h_dest[5],ehdr->h_source[0],ehdr->h_source[1], ehdr->h_source[2],ehdr->h_source[3],ehdr->h_source[4], ehdr->h_source[5],ehdr->h_proto); }

#define NIPQUAD(addr) \ ((unsigned char )&addr)[0], \ ((unsigned char )&addr)[1], \ ((unsigned char )&addr)[2], \ ((unsigned char )&addr)[3]

define NIPQUAD_FMT "%u.%u.%u.%u"

static unsigned int nf_test_in_hook(unsigned int hook, struct sk_buff skb, const struct net_device in, const struct net_device out, int (okfn)(struct sk_buff)) { struct ethhdr eth_header; struct iphdr ip_header; eth_header = (struct ethhdr )(skb_mac_header(skb)); ip_header = (struct iphdr *)(skb_network_header(skb)); hdr_dump(eth_header); printk("src IP:'"NIPQUAD_FMT"', dst IP:'"NIPQUAD_FMT"' \n", NIPQUAD(ip_header->saddr), NIPQUAD(ip_header->daddr)); return NF_ACCEPT; }

static int __init init_nf_test(void) { int ret; ret = nf_register_hooks(nf_test_ops, ARRAY_SIZE(nf_test_ops)); if (ret < 0) { printk("register nf hook fail\n"); return ret; } printk(KERN_NOTICE "register nf test hook\n"); return 0; }

static void __exit exit_nf_test(void) { nf_unregister_hooks(nf_test_ops, ARRAY_SIZE(nf_test_ops)); }

module_init(init_nf_test); module_exit(exit_nf_test); ```

dmesg | tail 後的結果:

[452013.507230] [MAC_DES:70,f3,95,e,42,faMAC_SRC: 0,f,fe,f6,7c,13 Prot:8] [452013.507237] src IP:'10.6.124.55', dst IP:'10.6.124.54' [452013.944960] [MAC_DES:70,f3,95,e,42,faMAC_SRC: 0,f,fe,f6,7c,13 Prot:8] [452013.944968] src IP:'10.6.124.55', dst IP:'10.6.124.54' [452014.960934] [MAC_DES:70,f3,95,e,42,faMAC_SRC: 0,f,fe,f6,7c,13 Prot:8] [452014.960941] src IP:'10.6.124.55', dst IP:'10.6.124.54' [452015.476335] [MAC_DES:70,f3,95,e,42,faMAC_SRC: 0,f,fe,f6,7c,13 Prot:8] [452015.476342] src IP:'10.6.124.55', dst IP:'10.6.124.54' [452016.023311] [MAC_DES:70,f3,95,e,42,faMAC_SRC: 0,f,fe,f6,7c,13 Prot:8] [452016.023318] src IP:'10.6.124.55', dst IP:'10.6.124.54'

這個 Demo 程式是個核心模組,模組入口為module_init傳入的init_nf_test函式。

init_nf_test函式中,其通過 Netfilter 提供的 nf_register_hooks 介面將自定義的nf_test_opt註冊到鉤子節點中。nf_test_optstruct nf_hook_ops型別的結構體陣列,其內部包含了所有關鍵元素,比如鉤子函式的註冊節點(此處為NF_INET_LOCAL_IN)以及鉤子函式(nf_test_in_hook)。

nf_test_in_hook函式內部,其檢查每一個傳遞過來的資料包,並將其源 Mac 地址,目的 Mac 地址,源 IP 地址以及目的 IP 地址打印出來。最後返回NF_ACCEPT,將資料包交給下一個鉤子函式處理。


四、Linux流量控制

Traffic Control HOWTO:大多利用Netfilter來實現流的控制.
比較詳細的文件是 Linux Advanced Routing & Traffic Control HOWTO 和縮簡版的 Traffic Control HOWTO.


五、擴充套件閱讀

Monitoring and Tuning the Linux Networking Stack: Sending Data

Linux Netfilter and Traffic Control

Netfilter and iptables homepage

圖解 Linux 網路包傳送過程

網路基礎--七層模型

OSI七層模型與TCP/IP五層模型

Linux 網路層收發包流程及 Netfilter 框架淺析

Netfilter & iptables 原理

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