深入理解 netfilter 和 iptables
在公眾號后臺回復:JGNB,可獲取杰哥原創(chuàng)的 PDF 手冊。
Netfilter (配合 iptables)使得用戶空間應用程序可以注冊內(nèi)核網(wǎng)絡棧在處理數(shù)據(jù)包時應用的處理規(guī)則,實現(xiàn)高效的網(wǎng)絡轉發(fā)和過濾。很多常見的主機防火墻程序以及 Kubernetes 的 Service 轉發(fā)都是通過 iptables 來實現(xiàn)的。
關于 netfilter 的介紹文章大部分只描述了抽象的概念,實際上其內(nèi)核代碼的基本實現(xiàn)不算復雜,本文主要參考 Linux 內(nèi)核 2.6 版本代碼(早期版本較為簡單),與最新的 5.x 版本在實現(xiàn)上可能有較大差異,但基本設計變化不大,不影響理解其原理。
Netfilter 的設計與實現(xiàn)
netfilter 的定義是一個工作在 Linux 內(nèi)核的網(wǎng)絡數(shù)據(jù)包處理框架,為了徹底理解 netfilter 的工作方式,我們首先需要對數(shù)據(jù)包在 Linux 內(nèi)核中的處理路徑建立基本認識。
數(shù)據(jù)包的內(nèi)核之旅
數(shù)據(jù)包在內(nèi)核中的處理路徑,也就是處理網(wǎng)絡數(shù)據(jù)包的內(nèi)核代碼調(diào)用鏈,大體上也可按 TCP/IP 模型分為多個層級,以接收一個 IPv4 的 tcp 數(shù)據(jù)包為例:
在物理-網(wǎng)絡設備層,網(wǎng)卡通過 DMA 將接收到的數(shù)據(jù)包寫入內(nèi)存中的 ring buffer,經(jīng)過一系列中斷和調(diào)度后,操作系統(tǒng)內(nèi)核調(diào)用 __skb_dequeue 將數(shù)據(jù)包加入對應設備的處理隊列中,并轉換成 sk_buffer 類型(即 socket buffer - 將在整個內(nèi)核調(diào)用棧中持續(xù)作為參數(shù)傳遞的基礎數(shù)據(jù)結構,下文指稱的數(shù)據(jù)包都可以認為是 sk_buffer),最后調(diào)用 netif_receive_skb 函數(shù)按協(xié)議類型對數(shù)據(jù)包進行分類,并跳轉到對應的處理函數(shù)。如下圖所示:

network-path
假設該數(shù)據(jù)包為 IP 協(xié)議包,對應的接收包處理函數(shù) ip_rcv 將被調(diào)用,數(shù)據(jù)包處理進入網(wǎng)絡(IP)層。ip_rcv 檢查數(shù)據(jù)包的 IP 首部并丟棄出錯的包,必要時還會聚合被分片的 IP 包。然后執(zhí)行 ip_rcv_finish 函數(shù),對數(shù)據(jù)包進行路由查詢并決定是將數(shù)據(jù)包交付本機還是轉發(fā)其他主機。假設數(shù)據(jù)包的目的地址是本主機,接著執(zhí)行的 dst_input 函數(shù)將調(diào)用 ip_local_deliver 函數(shù)。ip_local_deliver 函數(shù)中將根據(jù) IP 首部中的協(xié)議號判斷載荷數(shù)據(jù)的協(xié)議類型,最后調(diào)用對應類型的包處理函數(shù)。本例中將調(diào)用 TCP 協(xié)議對應的 tcp_v4_rcv 函數(shù),之后數(shù)據(jù)包處理進入傳輸層。
tcp_v4_rcv 函數(shù)同樣讀取數(shù)據(jù)包的 TCP 首部并計算校驗和,然后在數(shù)據(jù)包對應的 TCP control buffer 中維護一些必要狀態(tài)包括 TCP 序列號以及 SACK 號等。該函數(shù)下一步將調(diào)用 __tcp_v4_lookup 查詢數(shù)據(jù)包對應的 socket,如果沒找到或 socket 的連接狀態(tài)處于TCP_TIME_WAIT,數(shù)據(jù)包將被丟棄。如果 socket 處于未加鎖狀態(tài),數(shù)據(jù)包將通過調(diào)用 tcp_prequeue 函數(shù)進入 prequeue 隊列,之后數(shù)據(jù)包將可被用戶態(tài)的用戶程序所處理。傳輸層的處理流程超出本文討論范圍,實際上還要復雜很多。
netfilter hooks
接下來我們正式進入主題。netfilter 的首要組成部分是 netfilter hooks。
hook 觸發(fā)點
對于不同的協(xié)議(IPv4、IPv6 或 ARP 等),Linux 內(nèi)核網(wǎng)絡棧會在該協(xié)議棧數(shù)據(jù)包處理路徑上的預設位置觸發(fā)對應的 hook。在不同協(xié)議處理流程中的觸發(fā)點位置以及對應的 hook 名稱(藍色矩形外部的黑體字)如下,本文僅重點關注 IPv4 協(xié)議:

netfilter-flow
所謂的 hook 實質(zhì)上是代碼中的枚舉對象(值為從 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 在內(nèi)核網(wǎng)絡棧中對應特定的觸發(fā)點位置,以 IPv4 協(xié)議棧為例,有以下 netfilter hooks 定義:

netfilter-hooks-stack
NF_INET_PRE_ROUTING: 這個 hook 在 IPv4 協(xié)議棧的 ip_rcv 函數(shù)或 IPv6 協(xié)議棧的 ipv6_rcv 函數(shù)中執(zhí)行。所有接收數(shù)據(jù)包到達的第一個 hook 觸發(fā)點(實際上新版本 Linux 增加了 INGRESS hook 作為最早觸發(fā)點),在進行路由判斷之前執(zhí)行。
NF_INET_LOCAL_IN: 這個 hook 在 IPv4 協(xié)議棧的 ip_local_deliver() 函數(shù)或 IPv6 協(xié)議棧的 ip6_input() 函數(shù)中執(zhí)行。經(jīng)過路由判斷后,所有目標地址是本機的接收數(shù)據(jù)包到達此 hook 觸發(fā)點。
NF_INET_FORWARD: 這個 hook 在 IPv4 協(xié)議棧的 ip_forward() 函數(shù)或 IPv6 協(xié)議棧的 ip6_forward() 函數(shù)中執(zhí)行。經(jīng)過路由判斷后,所有目標地址不是本機的接收數(shù)據(jù)包到達此 hook 觸發(fā)點。
NF_INET_LOCAL_OUT: 這個 hook 在 IPv4 協(xié)議棧的 __ip_local_out() 函數(shù)或 IPv6 協(xié)議棧的 __ip6_local_out() 函數(shù)中執(zhí)行。所有本機產(chǎn)生的準備發(fā)出的數(shù)據(jù)包,在進入網(wǎng)絡棧后首先到達此 hook 觸發(fā)點。
NF_INET_POST_ROUTING: 這個 hook 在 IPv4 協(xié)議棧的 ip_output() 函數(shù)或 IPv6 協(xié)議棧的 ip6_finish_output2() 函數(shù)中執(zhí)行。本機產(chǎn)生的準備發(fā)出的數(shù)據(jù)包或者轉發(fā)的數(shù)據(jù)包,在經(jīng)過路由判斷之后, 將到達此 hook 觸發(fā)點。
NF_HOOK 宏和 netfilter 向量
所有的觸發(fā)點位置統(tǒng)一調(diào)用 NF_HOOK 這個宏來觸發(fā) 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?*))?{?return?NF_HOOK_THRESH(pf,?hook,?skb,?in,?out,?okfn,?INT_MIN);?}
NF-HOOK 接收的參數(shù)如下:
pf: 數(shù)據(jù)包的協(xié)議族,對 IPv4 來說是 NFPROTO_IPV4。
hook: 上圖中所示的 netfilter hook 枚舉對象,如 NF_INET_PRE_ROUTING 或 NF_INET_LOCAL_OUT。
skb: SKB 對象,表示正在被處理的數(shù)據(jù)包。
in: 數(shù)據(jù)包的輸入網(wǎng)絡設備。
out: 數(shù)據(jù)包的輸出網(wǎng)絡設備。
okfn: 一個指向函數(shù)的指針,該函數(shù)將在該 hook 即將終止時調(diào)用,通常傳入數(shù)據(jù)包處理路徑上的下一個處理函數(shù)。
NF-HOOK 的返回值是以下具有特定含義的 netfilter 向量之一:
NF_ACCEPT: 在處理路徑上正常繼續(xù)(實際上是在 NF-HOOK 中最后執(zhí)行傳入的 okfn)。
NF_DROP: 丟棄數(shù)據(jù)包,終止處理。
NF_STOLEN: 數(shù)據(jù)包已轉交,終止處理。
NF_QUEUE: 將數(shù)據(jù)包入隊后供其他處理。
NF_REPEAT: 重新調(diào)用當前 hook。
回歸到源碼,IPv4 內(nèi)核網(wǎng)絡棧會在以下代碼模塊中調(diào)用 NF_HOOK():

NF_HOOK
實際調(diào)用方式以 net/ipv4/ip_forward.c[1] 對數(shù)據(jù)包進行轉發(fā)的源碼為例,在 ip_forward 函數(shù)結尾部分的第 115 行以 NF_INET_FORWARDhook 作為入?yún)⒄{(diào)用了 NF_HOOK 宏,并將網(wǎng)絡棧接下來的處理函數(shù) ip_forward_finish 作為 okfn 參數(shù)傳入:
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);??return?NF_HOOK(NFPROTO_IPV4,?NF_INET_FORWARD,?skb,?skb->dev,?????????rt->dst.dev,?ip_forward_finish);?.....(省略部分代碼)?}
回調(diào)函數(shù)與優(yōu)先級
netfilter 的另一組成部分是 hook 的回調(diào)函數(shù)。內(nèi)核網(wǎng)絡棧既使用 hook 來代表特定觸發(fā)位置,也使用 hook (的整數(shù)值)作為數(shù)據(jù)索引來訪問觸發(fā)點對應的回調(diào)函數(shù)。
內(nèi)核的其他模塊可以通過 netfilter 提供的 api 向指定的 hook 注冊回調(diào)函數(shù),同一 hook 可以注冊多個回調(diào)函數(shù),通過注冊時指定的 priority 參數(shù)可指定回調(diào)函數(shù)在執(zhí)行時的優(yōu)先級。
注冊 hook 的回調(diào)函數(shù)時,首先需要定義一個 nf_hook_ops 結構(或由多個該結構組成的數(shù)組),其定義如下:
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;?};
在定義中有 3 個重要成員:
hook: 將要注冊的回調(diào)函數(shù),函數(shù)參數(shù)定義與 NF_HOOK 類似,可通過 okfn參數(shù)嵌套其他函數(shù)。
hooknum: 注冊的目標 hook 枚舉值。
priority: 回調(diào)函數(shù)的優(yōu)先級,較小的值優(yōu)先執(zhí)行。
定義結構體后可通過 int nf_register_hook(struct nf_hook_ops *reg) 或 int nf_register_hooks(struct nf_hook_ops *reg, unsigned int n); 分別注冊一個或多個回調(diào)函數(shù)。同一 netfilter hook 下所有的nf_hook_ops 注冊后以 priority 為順序組成一個鏈表結構,注冊過程會根據(jù) priority 從鏈表中找到合適的位置,然后執(zhí)行鏈表插入操作。
在執(zhí)行 NF-HOOK 宏觸發(fā)指定的 hook 時,將調(diào)用 nf_iterate 函數(shù)迭代這個 hook 對應的 nf_hook_ops 鏈表,并依次調(diào)用每一個 nf_hook_ops 的注冊函數(shù)成員 hookfn。示意圖如下:

netfilter-hookfn1
這種鏈式調(diào)用回調(diào)函數(shù)的工作方式,也讓 netfilter hook 被稱為 Chain,下文的 iptables 介紹中尤其體現(xiàn)了這一關聯(lián)。
每個回調(diào)函數(shù)也必須返回一個 netfilter 向量;如果該向量為 NF_ACCEPT,nf_iterate 將會繼續(xù)調(diào)用下一個 nf_hook_ops 的回調(diào)函數(shù),直到所有回調(diào)函數(shù)調(diào)用完畢后返回 NF_ACCEPT;如果該向量為 NF_DROP,將中斷遍歷并直接返回 NF_DROP;**如果該向量為 **NF_REPEAT,將重新執(zhí)行該回調(diào)函數(shù)。nf_iterate 的返回值也將作為 NF-HOOK 的返回值,網(wǎng)絡棧將根據(jù)該向量值判斷是否繼續(xù)執(zhí)行處理函數(shù)。示意圖如下:

netfilter-hookfn2
netfilter hook 的回調(diào)函數(shù)機制具有以下特性:
回調(diào)函數(shù)按優(yōu)先級依次執(zhí)行,只有上一回調(diào)函數(shù)返回 NF_ACCEPT 才會繼續(xù)執(zhí)行下一回調(diào)函數(shù)。
任一回調(diào)函數(shù)都可以中斷該 hook 的回調(diào)函數(shù)執(zhí)行鏈,同時要求整個網(wǎng)絡棧中止對數(shù)據(jù)包的處理。
iptables
基于內(nèi)核 netfilter 提供的 hook 回調(diào)函數(shù)機制,netfilter 作者 Rusty Russell 還開發(fā)了 iptables,實現(xiàn)在用戶空間管理應用于數(shù)據(jù)包的自定義規(guī)則。
iptbles 分為兩部分:
用戶空間的 iptables 命令向用戶提供訪問內(nèi)核 iptables 模塊的管理界面。
內(nèi)核空間的 iptables 模塊在內(nèi)存中維護規(guī)則表,實現(xiàn)表的創(chuàng)建及注冊。
內(nèi)核空間模塊
xt_table 的初始化
在內(nèi)核網(wǎng)絡棧中,iptables 通過 xt_table 結構對眾多的數(shù)據(jù)包處理規(guī)則進行有序管理,一個 xt_table 對應一個規(guī)則表,對應的用戶空間概念為 table。不同的規(guī)則表有以下特征:
對不同的 netfilter hooks 生效。
在同一 hook 中檢查不同規(guī)則表的優(yōu)先級不同。
基于規(guī)則的最終目的,iptables 默認初始化了 4 個不同的規(guī)則表,分別是 raw、 filter、nat 和 mangle。下文以 filter 為例介紹 xt_table的初始化和調(diào)用過程。
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] 模塊的初始化函數(shù) [iptable_filter_init](https://elixir.bootlin.com/linux/v2.6.39.4/C/ident/iptable_filter_init "iptable_filter_init") ****中,調(diào)用xt_hook_link 對 xt_table 結構 packet_filter 執(zhí)行如下初始化過程:
通過 .valid_hooks 屬性迭代 xt_table 將生效的每一個 hook,對于 filter 來說是 NF_INET_LOCAL_IN,NF_INET_FORWARD 和 NF_INET_LOCAL_OUT這 3 個 hook。
對每一個 hook,使用 xt_table 的 priority 屬性向 hook 注冊一個回調(diào)函數(shù)。
不同 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,?};
當數(shù)據(jù)包到達某一 hook 觸發(fā)點時,會依次執(zhí)行不同 table 在該 hook 上注冊的所有回調(diào)函數(shù),這些回調(diào)函數(shù)總是根據(jù)上文的 priority 值以固定的相對順序執(zhí)行:

tables-priority
ipt_do_table()
filter 注冊的 hook 回調(diào)函數(shù) iptable_filter_hook[3] 將對 xt_table 結構執(zhí)行公共的規(guī)則檢查函數(shù) ipt_do_table[4]。ipt_do_table 接收 skb、hook 和 xt_table作為參數(shù),對 skb 執(zhí)行后兩個參數(shù)所確定的規(guī)則集,返回 netfilter 向量作為回調(diào)函數(shù)的返回值。
在深入規(guī)則執(zhí)行過程前,需要先了解規(guī)則集如何在內(nèi)存中表示。每一條規(guī)則由 3 部分組成:
一個 ipt_entry 結構體。通過 .next_offset 指向下一個 ipt_entry 的內(nèi)存偏移地址。
0 個或多個 ipt_entry_match 結構體,每個結構體可以動態(tài)的添加額外數(shù)據(jù)。
1 個 ipt_entry_target 結構體, 結構體可以動態(tài)的添加額外數(shù)據(jù)。
ipt_entry 結構體定義如下:
struct?ipt_entry?{?struct?ipt_ip?ip;?unsigned?int?nfcache;??/*?ipt_entry?+?matches?在內(nèi)存中的大小*/?u_int16_t?target_offset;?/*?ipt_entry?+?matches?+?target?在內(nèi)存中的大小?*/?u_int16_t?next_offset;??/*?跳轉后指向前一規(guī)則?*/?unsigned?int?comefrom;?/*?數(shù)據(jù)包計數(shù)器?*/?struct?xt_counters?counters;?/*?長度為0數(shù)組的特殊用法,作為?match?的內(nèi)存地址?*/?unsigned?char?elems[0];?};
ipt_do_table 首先根據(jù) hook 類型以及 xt_table.private.entries屬性跳轉到對應的規(guī)則集內(nèi)存區(qū)域,執(zhí)行如下過程:

ipt_do_table
首先檢查數(shù)據(jù)包的 IP 首部與第一條規(guī)則 ipt_entry 的 .ipt_ip 屬性是否一致,如不匹配根據(jù) next_offset 屬性跳轉到下一條規(guī)則。
若 IP 首部匹配 ,則開始依次檢查該規(guī)則所定義的所有 ipt_entry_match 對象,與對象關聯(lián)的匹配函數(shù)將被調(diào)用,根據(jù)調(diào)用返回值有返回到回調(diào)函數(shù)(以及是否丟棄數(shù)據(jù)包)、跳轉到下一規(guī)則或繼續(xù)檢查等結果。
所有檢查通過后讀取 ipt_entry_target,根據(jù)其屬性返回 netfilter 向量到回調(diào)函數(shù)、繼續(xù)下一規(guī)則或跳轉到指定內(nèi)存地址的其他規(guī)則,非標準 ipt_entry_target 還會調(diào)用被綁定的函數(shù),但只能返回向量值不能跳轉其他規(guī)則。
靈活性和更新時延
以上數(shù)據(jù)結構與執(zhí)行方式為 iptables 提供了強大的擴展能力,我們可以靈活地自定義每條規(guī)則的匹配條件并根據(jù)結果執(zhí)行不同行為,甚至還能在額外的規(guī)則集之間棧式跳轉。
由于每條規(guī)則長度不等、內(nèi)部結構復雜,且同一規(guī)則集位于連續(xù)的內(nèi)存空間,iptables 使用全量替換的方式來更新規(guī)則,這使得我們能夠從用戶空間以原子操作來添加/刪除規(guī)則,但非增量式的規(guī)則更新會在規(guī)則數(shù)量級較大時帶來嚴重的性能問題:假如在一個大規(guī)模 Kubernetes 集群中使用 iptables 方式實現(xiàn) Service,當 service 數(shù)量較多時,哪怕更新一個 service 也會整體修改 iptables 規(guī)則表。全量提交的過程會 kernel lock 進行保護,因此會有很大的更新時延。
用戶空間的 tables、chains 和 rules
用戶空間的 iptables 命令行可以讀取指定表的數(shù)據(jù)并渲染到終端,添加新的規(guī)則(實際上是替換整個 table 的規(guī)則表)等。
iptables 主要操作以下幾種對象:
table:對應內(nèi)核空間的 xt_table 結構,iptable 的所有操作都對指定的 table 執(zhí)行,默認為 filter。
chain:對應指定 table 通過特定 netfilter hook 調(diào)用的規(guī)則集,此外還可以自定義規(guī)則集,然后從 hook 規(guī)則集中跳轉過去。
rule:對應上文中 ipt_entry、ipt_entry_match 和ipt_entry_target,定義了對數(shù)據(jù)包的匹配規(guī)則以及匹配后執(zhí)行的行為。
match:具有很強擴展性的自定義匹配規(guī)則。
target:具有很強擴展性的自定義匹配后行為。
基于上文介紹的代碼調(diào)用過程流程,chain 和 rule 按如下示意圖執(zhí)行:

iptables-chains
對于 iptables 具體的用法和指令本文不做詳細介紹。
conntrack
僅僅通過 3、4 層的首部信息對數(shù)據(jù)包進行過濾是不夠的,有時候還需要進一步考慮連接的狀態(tài)。netfilter 通過另一內(nèi)置模塊 conntrack 進行連接跟蹤(connection tracking),以提供根據(jù)連接過濾、地址轉換(NAT)等更進階的網(wǎng)絡過濾功能。由于需要對連接狀態(tài)進行判斷,conntrack 在整體機制相同的基礎上,又針對協(xié)議特點有單獨的實現(xiàn)。
推薦閱讀
iptables 防火墻(三)- 規(guī)則的導出 / 導入、使用防火墻腳本程序 |(附體系思維導圖)
iptables 防火墻(二)- SNAT / DNAT 策略及應用 |(附體系思維導圖)
iptables 防火墻(一)- 四表/五鏈、數(shù)據(jù)包匹配流程、編寫 iptables 規(guī)則

