走進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實現