詳述netfilter 和 iptables!

roc_guo發表於2022-07-24
導讀 Netfilter (配合 iptables)使得使用者空間應用程式可以註冊核心網路棧在處理資料包時應用的處理規則,實現高效的網路轉發和過濾。很多常見的主機防火牆程式以及 Kubernetes 的 Service 轉發都是透過 iptables 來實現的

詳述netfilter 和 iptables!詳述netfilter 和 iptables!

關於 netfilter 的介紹文章大部分只描述了抽象的概念,實際上其核心程式碼的基本實現不算複雜,本文主要參考   核心 2.6 版本程式碼(早期版本較為簡單),與最新的 5.x 版本在實現上可能有較大差異,但基本設計變化不大,不影響理解其原理。

本文假設讀者已對 TCP/IP 協議有基本瞭解。

Netfilter 的設計與實現

netfilter 的定義是一個工作在 Linux 核心的網路資料包處理框架,為了徹底理解 netfilter 的工作方式,我們首先需要對資料包在 Linux 核心中的處理路徑建立基本認識。

資料包的核心之旅

資料包在核心中的處理路徑,也就是處理網路資料包的核心程式碼呼叫鏈,大體上也可按 TCP/IP 模型分為多個層級,以接收一個 IPv4 的 tcp 資料包為例:

1. 在物理-網路裝置層,網路卡透過 DMA 將接收到的資料包寫入記憶體中的 ring buffer ,經過一系列中斷和排程後,作業系統核心呼叫 __skb_dequeue 將資料包加入對應裝置的處理佇列中,並轉換成 sk_buffer 型別(即 socket buffer - 將在整個核心呼叫棧中持續作為引數傳遞的基礎資料結構,下文指稱的資料包都可以認為是 sk_buffer ),最後呼叫 netif_receive_skb 函式按協議型別對資料包進行分類,並跳轉到對應的處理函式。如下圖所示:

詳述netfilter 和 iptables!詳述netfilter 和 iptables!

network-path

1. 假設該資料包為 IP 協議包,對應的接收包處理函式 ip_rcv將被呼叫,資料包處理進入網路(IP)層。ip_rcv檢查資料包的 IP 首部並丟棄出錯的包,必要時還會聚合被分片的 IP 包。然後執行 ip_rcv_finish函式,對資料包進行路由查詢並決定是將資料包交付本機還是轉發其他主機。假設資料包的目的地址是本主機,接著執行的 dst_input函式將呼叫 ip_local_deliver函式。ip_local_deliver函式中將根據 IP 首部中的協議號判斷載荷資料的協議型別,最後呼叫對應型別的包處理函式。本例中將呼叫 TCP 協議對應的 tcp_v4_rcv函式,之後資料包處理進入傳輸層。

2. tcp_v4_rcv函式同樣讀取資料包的 TCP 首部並計算校驗和,然後在資料包對應的 TCP control buffer 中維護一些必要狀態包括 TCP 序列號以及 SACK 號等。該函式下一步將呼叫 __tcp_v4_lookup查詢資料包對應的 socket,如果沒找到或 socket 的連線狀態處於 TCP_TIME_WAIT,資料包將被丟棄。如果 socket 處於未加鎖狀態,資料包將透過呼叫 tcp_prequeue函式進入 prequeue佇列,之後資料包將可被使用者態的使用者程式所處理。傳輸層的處理流程超出本文討論範圍,實際上還要複雜很多。

netfilter hooks
接下來我們正式進入主題。netfilter 的首要組成部分是 netfilter hooks。

hook 觸發點

對於不同的協議(IPv4、IPv6 或 ARP 等),Linux 核心網路棧會在該協議棧資料包處理路徑上的預設位置觸發對應的 hook。在不同協議處理流程中的觸發點位置以及對應的 hook 名稱(藍色矩形外部的黑體字)如下,本文僅重點關注 IPv4 協議:

詳述netfilter 和 iptables!詳述netfilter 和 iptables!

netfilter-flow

所謂的 hook 實質上是程式碼中的列舉物件(值為從 0 開始遞增的整型):

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 };

每個 hook 在核心網路棧中對應特定的觸發點位置,以 IPv4 協議棧為例,有以下 netfilter hooks 定義:

詳述netfilter 和 iptables!詳述netfilter 和 iptables!

netfilter-hooks-stack

NF_INET_PRE_ROUTING: 這個 hook 在 IPv4 協議棧的 ip_rcv 函式或 IPv6 協議棧的 ipv6_rcv 函式中執行。所有接收資料包到達的第一個 hook 觸發點(實際上新版本 Linux 增加了 INGRESS hook 作為最早觸發點),在進行路由判斷之前執行。
NF_INET_LOCAL_IN: 這個 hook 在 IPv4 協議棧的 ip_local_deliver 函式或 IPv6 協議棧的 ip6_input 函式中執行。經過路由判斷後,所有目標地址是本機的接收資料包到達此 hook 觸發點。
NF_INET_FORWARD: 這個 hook 在 IPv4 協議棧的 ip_forward 函式或 IPv6 協議棧的 ip6_forward 函式中執行。經過路由判斷後,所有目標地址不是本機的接收資料包到達此 hook 觸發點。
NF_INET_LOCAL_OUT: 這個 hook 在 IPv4 協議棧的 __ip_local_out 函式或 IPv6 協議棧的 __ip6_local_out 函式中執行。所有本機產生的準備發出的資料包,在進入網路棧後首先到達此 hook 觸發點。
NF_INET_POST_ROUTING: 這個 hook 在 IPv4 協議棧的 ip_output 函式或 IPv6 協議棧的 ip6_finish_output2 函式中執行。本機產生的準備發出的資料包或者轉發的資料包,在經過路由判斷之後, 將到達此 hook 觸發點。

NF_HOOK 宏和 netfilter 向量
所有的觸發點位置統一呼叫 NF_HOOK這個宏來觸發 hook:

static inline int NF_HOOK(uint8_t pf, unsigned int hook, struct sk_buff *skb, struct net_device * in, struct net_device *out, int (*okfn)(struct sk_buff *)) { returnNF_HOOK_THRESH(pf, hook, skb, in, out, okfn, INT_MIN); }

NF-HOOK接收的引數如下:

pf: 資料包的協議族,對 IPv4 來說是 NFPROTO_IPV4 。
hook: 上圖中所示的 netfilter hook 列舉物件,如 NF_INET_PRE_ROUTING 或 NF_INET_LOCAL_OUT。
skb: SKB 物件,表示正在被處理的資料包。
in: 資料包的輸入網路裝置。
out: 資料包的輸出網路裝置。
okfn: 一個指向函式的指標,該函式將在該 hook 即將終止時呼叫,通常傳入資料包處理路徑上的下一個處理函式。

NF-HOOK的返回值是以下具有特定含義的 netfilter 向量之一:

1. NF_ACCEPT: 在處理路徑上正常繼續(實際上是在 NF-HOOK 中最後執行傳入的 okfn )。
2. NF_DROP: 丟棄資料包,終止處理。
3. NF_STOLEN: 資料包已轉交,終止處理。
4. NF_QUEUE: 將資料包入隊後供其他處理。
5. NF_REPEAT: 重新呼叫當前 hook。
迴歸到原始碼,IPv4 核心網路棧會在以下程式碼模組中呼叫 NF_HOOK:

詳述netfilter 和 iptables!詳述netfilter 和 iptables!

NF_HOOK

實際呼叫方式以 `net/ipv4/ip_forward.c`[1] 對資料包進行轉發的原始碼為例,在 ip_forward函式結尾部分的第 115 行以 NF_INET_FORWARDhook 作為入參呼叫了 NF_HOOK 宏,並將網路棧接下來的處理函式 ip_forward_finish作為 okfn引數傳入 :

int ip_forward(struct sk_buff *skb) { .....(省略部分程式碼) if(rt->rt_flags&RTCF_DOREDIRECT && !opt->srr && !skb_sec_path(skb)) ip_rt_send_redirect(skb); skb->priority = rt_tos2priority(iph->tos); returnNF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD, skb, skb->dev, rt->dst.dev, ip_forward_finish); .....(省略部分程式碼) }

回撥函式與優先順序

netfilter 的另一組成部分是 hook 的回撥函式。核心網路棧既使用 hook 來代表特定觸發位置,也使用 hook (的整數值)作為資料索引來訪問觸發點對應的回撥函式。

核心的其他模組可以透過 netfilter 提供的 api 向指定的 hook 註冊回撥函式,同一 hook 可以註冊多個回撥函式,透過註冊時指定的 priority引數可指定回撥函式在執行時的優先順序。

註冊 hook 的回撥函式時,首先需要定義一個 nf_hook_ops結構(或由多個該結構組成的陣列),其定義如下:

struct nf_hook_ops { struct list_head list; /* User fills infrom here down. */ nf_hookfn *hook; struct module *owner; u_int8_t pf; unsigned int hooknum; /* Hooks are ordered inascending priority. */ int priority; };

在定義中有 3 個重要成員:

hook: 將要註冊的回撥函式,函式引數定義與 NF_HOOK 類似,可透過 okfn 引數巢狀其他函式。
hooknum: 註冊的目標 hook 列舉值。
priority: 回撥函式的優先順序, 較小的值優先執行 。
定義結構體後可透過 int nf_register_hook(struct nf_hook_ops *reg)或 int nf_register_hooks(struct nf_hook_ops *reg, unsigned int n);分別註冊一個或多個回撥函式。同一 netfilter hook 下所有的 nf_hook_ops註冊後以 priority 為順序組成一個連結串列結構,註冊過程會根據 priority 從連結串列中找到合適的位置,然後執行連結串列插入操作。

在執行 NF-HOOK宏觸發指定的 hook 時,將呼叫 nf_iterate函式迭代這個 hook 對應的 nf_hook_ops連結串列,並依次呼叫每一個 nf_hook_ops的註冊函式成員 hookfn。示意圖如下:

netfilter-hookfn1

這種鏈式呼叫回撥函式的工作方式,也讓 netfilter hook 被稱為 Chain,下文的 iptables 介紹中尤其體現了這一關聯。

每個回撥函式也必須返回一個 netfilter 向量;如果該向量為 NF_ACCEPT,nf_iterate將會繼續呼叫下一個 nf_hook_ops的回撥函式,直到所有回撥函式呼叫完畢後返回 NF_ACCEPT;如果該向量為 NF_DROP,將中斷遍歷並直接返回 NF_DROP;**如果該向量為 **NF_REPEAT**,將重新執行該回撥函式**。nf_iterate的返回值也將作為 NF-HOOK的返回值,網路棧將根據該向量值判斷是否繼續執行處理函式。示意圖如下:

詳述netfilter 和 iptables!詳述netfilter 和 iptables!

netfilter-hookfn2

netfilter hook 的回撥函式機制具有以下特性:

回撥函式按優先順序依次執行,只有上一回撥函式返回 NF_ACCEPT 才會繼續執行下一回撥函式。
任一回撥函式都可以中斷該 hook 的回撥函式執行鏈,同時要求整個網路棧中止對資料包的處理。

iptables

基於核心 netfilter 提供的 hook 回撥函式機制,netfilter 作者 Rusty Russell 還開發了 iptables,實現在使用者空間管理應用於資料包的自定義規則。

iptbles 分為兩部分:

使用者空間的 iptables  向使用者提供訪問核心 iptables 模組的管理介面。
核心空間的 iptables 模組在記憶體中維護規則表,實現表的建立及註冊。
核心空間模組 xt_table 的初始化
在核心網路棧中,iptables 透過 xt_table結構對眾多的資料包處理規則進行有序管理,一個 xt_table對應一個規則表,對應的使用者空間概念為 table。不同的規則表有以下特徵:

對不同的 netfilter hooks 生效。
在同一 hook 中檢查不同規則表的優先順序不同。
基於規則的最終目的,iptables 預設初始化了 4 個不同的規則表,分別是 raw、 filter、nat 和 mangle。下文以 filter 為例介紹 xt_table的初始化和呼叫過程。

filter table 的定義如下:

#define FILTER_VALID_HOOKS ((1 << NF_INET_LOCAL_IN) | \ (1 << NF_INET_FORWARD) | \ (1 << NF_INET_LOCAL_OUT)) static const struct xt_table packet_filter = { .name = "filter", .valid_hooks = FILTER_VALID_HOOKS, .me = THIS_MODULE, .af = NFPROTO_IPV4, .priority = NF_IP_PRI_FILTER, }; (net/ipv4/netfilter/iptable_filter.c)

在 iptable_filter.c[2] 模組的初始化函式 [iptable_filter_init]****中,呼叫xt_hook_link對 xt_table結構 packet_filter 執行如下初始化過程:

透過 .valid_hooks 屬性迭代 xt_table 將生效的每一個 hook,對於 filter 來說是 NF_INET_LOCAL_IN , NF_INET_FORWARD 和 NF_INET_LOCAL_OUT 這 3 個 hook。
對每一個 hook,使用 xt_table 的 priority 屬性向 hook 註冊一個回撥函式。
不同 table 的 priority 值如下:

enum nf_ip_hook_priorities { NF_IP_PRI_RAW = -300, 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, };

當資料包到達某一 hook 觸發點時,會依次執行不同 table 在該 hook 上註冊的所有回撥函式,這些回撥函式總是根據上文的 priority 值以固定的相對順序執行:

詳述netfilter 和 iptables!詳述netfilter 和 iptables!

tables-priorityipt_do_table

filter 註冊的 hook 回撥函式 iptable_filter_hook[3] 將對 xt_table結構執行公共的規則檢查函式 ipt_do_table[4] 。 ipt_do_table接收 skb、hook和 xt_table作為引數,對 skb執行後兩個引數所確定的規則集,返回 netfilter 向量作為回撥函式的返回值。

在深入規則執行過程前,需要先了解規則集如何在記憶體中表示。每一條規則由 3 部分組成:

一個 ipt_entry 結構體。透過 .next_offset 指向下一個 ipt_entry 的記憶體偏移地址。
0 個或多個 ipt_entry_match 結構體,每個結構體可以動態的新增額外資料。
1 個 ipt_entry_target 結構體, 結構體可以動態的新增額外資料。
ipt_entry結構體定義如下:

struct ipt_entry { struct ipt_ip ip; unsigned int nfcache; /* ipt_entry + matches 在記憶體中的大小*/ u_int16_t target_offset; /* ipt_entry + matches + target 在記憶體中的大小 */ u_int16_t next_offset; /* 跳轉後指向前一規則 */ unsigned int comefrom; /* 資料包計數器 */ struct xt_counters counters; /* 長度為0陣列的特殊用法,作為 match 的記憶體地址 */ unsigned char elems[0]; };

ipt_do_table首先根據 hook 型別以及 xt_table.private.entries屬性跳轉到對應的規則集記憶體區域,執行如下過程:

詳述netfilter 和 iptables!詳述netfilter 和 iptables!

ipt_do_table

首先檢查資料包的 IP 首部與第一條規則 ipt_entry 的 .ipt_ip 屬性是否一致,如不匹配根據 next_offset 屬性跳轉到下一條規則。
若 IP 首部匹配 ,則開始依次檢查該規則所定義的所有 ipt_entry_match 物件,與物件關聯的匹配函式將被呼叫,根據呼叫返回值有返回到回撥函式(以及是否丟棄資料包)、跳轉到下一規則或繼續檢查等結果。
所有檢查透過後讀取 ipt_entry_target ,根據其屬性返回 netfilter 向量到回撥函式、繼續下一規則或跳轉到指定記憶體地址的其他規則,非標準 ipt_entry_target 還會呼叫被繫結的函式,但只能返回向量值不能跳轉其他規則。

靈活性和更新時延
以上資料結構與執行方式為 iptables 提供了強大的擴充套件能力,我們可以靈活地自定義每條規則的匹配條件並根據結果執行不同行為,甚至還能在額外的規則集之間棧式跳轉。

由於每條規則長度不等、內部結構複雜,且同一規則集位於連續的記憶體空間,iptables 使用全量替換的方式來更新規則,這使得我們能夠從使用者空間以原子操作來新增/刪除規則,但非增量式的規則更新會在規則數量級較大時帶來嚴重的效能問題:假如在一個大規模 Kubernetes 叢集中使用 iptables 方式實現 Service,當 service 數量較多時,哪怕更新一個 service 也會整體修改 iptables 規則表。全量提交的過程會 kernel lock 進行保護,因此會有很大的更新時延。

使用者空間的 tables、chains 和 rules

使用者空間的 iptables  行可以讀取指定表的資料並渲染到終端,新增新的規則(實際上是替換整個 table 的規則表)等。

iptables 主要操作以下幾種物件:

table:對應核心空間的 xt_table 結構,iptable 的所有操作都對指定的 table 執行,預設為 filter。
chain:對應指定 table 透過特定 netfilter hook 呼叫的規則集,此外還可以自定義規則集,然後從 hook 規則集中跳轉過去。
rule:對應上文中 ipt_entry 、 ipt_entry_match 和 ipt_entry_target ,定義了對資料包的匹配規則以及匹配後執行的行為。
match:具有很強擴充套件性的自定義匹配規則。
target:具有很強擴充套件性的自定義匹配後行為。
基於上文介紹的程式碼呼叫過程流程,chain 和 rule 按如下示意圖執行:

詳述netfilter 和 iptables!詳述netfilter 和 iptables!

iptables-chains

對於 iptables 具體的用法和指令本文不做詳細介紹,可參考 Iptables Essentials: Common Firewall Rules and Commands | DigitalOcean[5] 。

conntrack

僅僅透過 3、4 層的首部資訊對資料包進行過濾是不夠的,有時候還需要進一步考慮連線的狀態。netfilter 透過另一內建模組 conntrack進行連線跟蹤(connection tracking),以提供根據連線過濾、地址轉換(NAT)等更進階的網路過濾功能。由於需要對連線狀態進行判斷,conntrack在整體機制相同的基礎上,又針對協議特點有單獨的實現。

原文來自:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69955379/viewspace-2907322/,如需轉載,請註明出處,否則將追究法律責任。

相關文章