iptables 和 netfilter 學習筆記
作者簡介:張銘軒,西安郵電大學計算機專業研二學生,導師陳莉君教授,熱衷於探索 linux 內核。
iptables 和 netfilter
iptables 是 Linux 上最常用的防火牆工具,iptables 與協議棧內有包過濾功能的 hook 交互來完成工作。這些內核 hook 構成了 netfilter 框架
每個進入網絡系統的包(接收或發送)在經過協議棧時都會觸發這些 hook,程序可以通過註冊 hook 函數的方式在一些關鍵路徑上處理網絡流量。iptables 相關的內核模塊在這些 hook 點註冊了處理函數,因此可以通過配置 iptables 規則來使得網絡流量符合防火牆規則。
netfilter 框架
netfilter 提供了 5 個 hook 點。包經過協議棧時會觸發內核模塊註冊在這裏的處理函數 。
其中主要包括:
-
NF_IP_PRE_ROUTING
: 接收到的包進入協議棧後立即觸發此 hook,在進行任何路由判斷 (將包發往哪裏)之前 -
NF_IP_LOCAL_IN
: 接收到的包經過路由判斷,如果目的是本機,將觸發此 hook -
NF_IP_FORWARD
: 接收到的包經過路由判斷,如果目的是其他機器,將觸發此 hook -
NF_IP_LOCAL_OUT
: 本機產生的準備發送的包,在進入協議棧後立即觸發此 hook -
NF_IP_POST_ROUTING
: 本機產生的準備發送的包或者轉發的包,在經過路由判斷之後, 將觸發此 hook
以 ip_rcv
爲例,它主要負責接收和處理 IPv4 數據包,在處理完核心邏輯後,會進入內核預義的鉤子進行後續處理。
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,struct net_device *orig_dev)
{
struct net *net = dev_net(dev);//返回與給定網絡設備關聯的net結構
//net結構用以表示內核中的網絡命名空間
skb = ip_rcv_core(skb, net);
if (skb == NULL)
return NET_RX_DROP;
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
net, NULL, skb, dev, NULL,
ip_rcv_finish);
}
具體進入函數層面的分析:
#define NF_HOOK(proto, hooknum, net, skb, dev, in, out) \
nf_hook(proto, hooknum, net, skb, dev, in, out)
static inline int nf_hook(u_int8_t pf, unsigned int hook, struct net *net,struct sock *sk, struct sk_buff *skb,struct net_device *indev, struct net_device *outdev, int (*okfn)(struct net *, struct sock *, struct sk_buff *))
{
struct nf_hook_entries *hook_head = NULL; // 存儲鉤子鏈表的頭指針
int ret = 1; // 默認返回值
...
// 根據協議族選擇鉤子鏈表
switch (pf) {
case NFPROTO_IPV4:
hook_head = rcu_dereference(net->nf.hooks_ipv4[hook]);
// 獲取與當前網絡命名空間相關的 IPv4 鉤子鏈表
break;
...
}
// 執行鉤子處理
if (hook_head) {
struct nf_hook_state state;
// 初始化鉤子狀態
nf_hook_state_init(&state, hook, pf, indev, outdev, sk, net, okfn);
// 調用鉤子處理函數
ret = nf_hook_slow(skb, &state, hook_head, 0);
}
...
return ret; // 返回處理結果
}
//進入具體的處理函數
int nf_hook_slow(struct sk_buff *skb, struct nf_hook_state *state,
const struct nf_hook_entries *e, unsigned int s)
{
unsigned int verdict;
int ret;
// 遍歷所有鉤子函數
for (; s < e->num_hook_entries; s++) {
// 調用當前鉤子函數
verdict = nf_hook_entry_hookfn(&e->hooks[s], skb, state);
switch (verdict & NF_VERDICT_MASK) {
case NF_ACCEPT:
// 如果鉤子返回接受,則繼續下一個鉤子
break;
case NF_DROP:
// 如果鉤子返回丟棄
kfree_skb_reason(skb, SKB_DROP_REASON_NETFILTER_DROP);
ret = NF_DROP_GETERR(verdict);
if (ret == 0)
ret = -EPERM; // 返回權限錯誤
return ret; // 終止處理並返回丟棄結果
case NF_QUEUE:
// 如果鉤子返回隊列
ret = nf_queue(skb, state, s, verdict);
if (ret == 1)
continue; // 繼續下一個鉤子
return ret; // 返回處理結果
default:
// 對於 NF_STOLEN 或其他非標準結果的隱式處理
return 0;
}
}
return 1; // 所有鉤子處理完畢,返回接受結果
}
上述代碼總結爲,當 NF_HOOK
被調用時,Netfilter 會執行以下步驟:
-
查找鉤子列表:根據傳入的協議和鉤子點,Netfilter 查找已註冊的鉤子函數列表。
-
調用鉤子函數:遍歷所有註冊的鉤子函數,依次調用每個鉤子函數,並將數據包(
skb
)傳遞給它們。 -
處理結果:每個鉤子函數可以選擇:
-
繼續處理數據包,返回
NF_ACCEPT
。 -
丟棄數據包,返回
NF_DROP
。 -
修改數據包並繼續處理,返回
NF_QUEUE
或其他值。
iptables 表和鏈
iptables 使用 table 來組織規則,根據用來做什麼類型的判斷,將規則分爲不同 table。在每個 table 內部,規則被進一步組織成 chain,內置的 chain 是由內置的 hook 觸發 的。chain 基本上能決定規則何時被匹配。內置的 chain 名字和 netfilter hook 名字是一一對應的:如 PREROUTING 是 由 NF_IP_PRE_ROUTING hook 觸發的 chain
因此不同 table 的 chain 最終都是註冊到 netfilter hook 。例如,有三個 table 有 PRETOUTING
chain。當這些 chain 註冊到對應的 NF_IP_PRE_ROUTING
hook 點時,它們需要指定優先級,應該依次調用哪個 table 的 PRETOUTING
chain,優先級從高到低。
table 種類
filter table:
filter table 是最常用的 table 之一,用於判斷是否允許一個包通過。
nat table:
nat table 用於實現網絡地址轉換規則。當包進入協議棧的時候,這些規則決定是否以及如何修改包的源 / 目的地址,以改變包被路由時的行爲。nat
table 通常用於將包路由到無法直接訪問的網絡。
mangle table:
mangle table 用於修改包的 IP 頭。例如,可以修改包的 TTL,增加或減少包可以經過的跳數。這個 table 還可以對包打只在內核內有效的 “標記”,後續的 table 或工具處理的時候可以用到這些標記。標記不會修改包本身,只是在包的內核表示上做標記。
raw table:
raw table 定義目的是使一個讓包繞過連接跟蹤。建立在 netfilter 之上的連接跟蹤特性使得 iptables 將包 看作已有的連接或會話的一部分,而不是一個由獨立、不相關的包組成的流。 數據包到達網絡接口之後很快就會有連接跟蹤邏輯判斷。
security table:
security table 的作用是給包打上 SELinux 標記,以此影響 SELinux 或其他可以解讀 SELinux 安全上下文的系統處理包的行爲。這些標記可以基於單個包,也可以基於連接。
每種 table 實現的 chain
當一個包觸發 netfilter hook 時,處理過程將沿着列從上向下執行。其有內置的優先級,數值越小越優先。
enum nf_ip_hook_priorities {
NF_IP_PRI_FIRST = INT_MIN,
NF_IP_PRI_RAW_BEFORE_DEFRAG = -450,
NF_IP_PRI_CONNTRACK_DEFRAG = -400,
NF_IP_PRI_RAW = -300, //raw
NF_IP_PRI_SELINUX_FIRST = -225,
NF_IP_PRI_CONNTRACK = -200, //conntrack
NF_IP_PRI_MANGLE = -150, //manage
NF_IP_PRI_NAT_DST = -100, //dnat
NF_IP_PRI_FILTER = 0, //filter
NF_IP_PRI_SECURITY = 50, //security
NF_IP_PRI_NAT_SRC = 100, //snat
NF_IP_PRI_SELINUX_LAST = 225,
NF_IP_PRI_CONNTRACK_HELPER = 300,
NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX,
NF_IP_PRI_LAST = INT_MAX,
};
特定事件會導致 table 的 chain 被跳過。例如,只有每個連接的第一個包會去匹配 NAT 規則,對這個包的動作會應用於此連接後面的所有包。到這個連接的應答包會被自動應用反方向的 NAT 規則。
對於不同的包,由於 netfilter 掛載函數的不同,導致其對應的 chain 也不同
-
收到的、目的是本機的包:
PRETOUTING
->INPUT
-
收到的、目的是其他主機的包:
PRETOUTING
->FORWARD
->POSTROUTING
-
本地產生的包:
OUTPUT
->POSTROUTING
綜合前面討論的 table 順序問題,我們可以看到對於一個收到的、目的是本機的包: 首先依次經過 PRETOUTING chain 上面的 raw、mangle、nat table;然後依次經 過 INPUT chain 的 mangle、filter、security、nat table,然後纔會到達本機的某個 socket。
iptables 規則
規則放置在特定 table 的特定 chain 裏面。當 chain 被調用的時候,包會依次匹配 chain 裏面的規則。每條規則都有一個匹配部分和一個目標部分
規則的匹配部分指定了一些條件,包必須滿足這些條件纔會和相應的將要執行的動作進行關聯。規則可以匹配協議類型、目的或源地址、目的或源端口、目的或源網段、接收或發送的接口(網卡)、協議頭、連接狀態等等條件。這些綜合起來,能夠組合成非常複雜的規則來區分不同的網絡流量。
包符合某種規則的條件而觸發的動作叫做目標。目標分爲兩種類型:
-
終止目標:這種 target 會終止 chain 的匹配,將控制權轉移回 netfilter hook。根據返回值的不同,hook 或者將包丟棄,或者允許包進行下一 階段的處理
-
非終止目標:非終止目標執行動作,然後繼續 chain 的執行。雖然每個 chain 最終都會回到一個終止目標,但是在這之前,可以執行任意多個非終止目標
每個規則可以跳轉到哪個 target 依上下文而定,例如,table 和 chain 可能會設置 target 可用或不可用。規則裏激活的 extensions 和匹配條件也影響 target 的可用性。
還有一種特殊的非終止目標:跳轉目標。jump target 是跳轉到其他 chain 繼續處理的動作。向用戶自定義 chain 添加規則和向內置的 chain 添加規則的方式是相同的。不同的地方在於, 用戶定義的 chain 只能通過從另一個規則跳轉(jump)到它,因爲它們沒有註冊到 netfilter hook。用戶定義的 chain 可以看作是對調用它的 chain 的擴展。
iptables 和 conntrack
在討論 raw table 和 匹配連接狀態的時候,我們介紹了構建在 netfilter 之上的連接跟蹤系統。連接跟蹤系統使得 iptables 基於連接上下文而不是單個包來做出規則判斷, 給 iptables 提供了有狀態操作的功能。
跟蹤系統將包和已有的連接進行比較,如果包所屬的連接已經存在就更新連接狀態, 否則就創建一個新連接。如果 raw table 的某個 chain 對包標記爲目標是 NOTRACK, 那這個包會跳過連接跟蹤系統。
連接跟蹤系統中的連接狀態有:
-
NEW
:如果到達的包關連不到任何已有的連接,但包是合法的,就爲這個包創建一個新連接。對 面向連接的(connection-aware)的協議例如 TCP 以及非面向連接的(connectionless )的協議例如 UDP 都適用 -
ESTABLISHED
:當一個連接收到應答方向的合法包時,狀態從NEW
變成ESTABLISHED
。對 TCP 這個合法包其實就是SYN/ACK
包;對 UDP 和 ICMP 是源和目 的 IP 與原包相反的包 -
RELATED
:包不屬於已有的連接,但是和已有的連接有一定關係。這可能是輔助連接( helper connection),例如 FTP 數據傳輸連接,或者是其他協議試圖建立連接時的 ICMP 應答包 -
INVALID
:包不屬於已有連接,並且因爲某些原因不能用來創建一個新連接,例如無法 識別、無法路由等等 -
UNTRACKED
:如果在raw
table 中標記爲目標是UNTRACKED
,這個包將不會進入連 接跟蹤系統 -
SNAT
:包的源地址被 NAT 修改之後會進入的虛擬狀態。連接跟蹤系統據此在收到反向包時對地址做反向轉換 -
DNAT
:包的目的地址被 NAT 修改之後會進入的虛擬狀態。連接跟蹤系統據此在收到反向包時對地址做反向轉換
iptable 使用
iptables 可以配置和管理 Netfilter 框架,其是一個用戶態工具,用以定義網絡數據包過濾規則,此處主要以 NAT 爲例作爲講解
NAT
NAT 是一種網絡技術,用於在網絡設備(如路由器、網關)上對數據包的源地址或目的地址進行修改,通常在局域網與互聯網之間進行轉換。NAT 主要用於節約 IPv4 地址,隱藏內部網絡的結構,並在網絡層提供一定程度的安全性。
NAT 在路由器或防火牆處修改通過的數據包的 IP 地址信息。常見的 NAT 類型包括:
SNAT(Source NAT):修改數據包的源 IP 地址,通常用於內網設備訪問外網時,將內網設備的私有 IP 地址替換爲公共 IP 地址。
DNAT(Destination NAT):修改數據包的目的 IP 地址,通常用於將外部請求映射到內網的某臺服務器(如端口轉發)。
NAT 通過維護一個映射表,跟蹤修改前後的 IP 地址和端口號,從而保證數據包能正確地返回給發起請求的內部設備。
NAT 的類型
靜態 NAT:一個內部 IP 地址映射到一個固定的外部 IP 地址。通常用於讓內部服務器暴露給外部網絡。
動態 NAT:內部 IP 地址池映射到外部 IP 地址池。適用於內網設備臨時需要與外網通信,使用的外部 IP 地址動態分配。
PAT(端口地址轉換),也叫端口多路複用:多個內部 IP 地址可以共享一個或多個外部 IP 地址,但通過不同的端口區分不同的連接。它是 NAT 中最常見的類型,也稱爲 NAT 重載。
和 NAT 相關的最重要的規則,都在 nat table 裏。在相應 chain 中配置所需的規則,即可實現 NAT 的功能
iptables 可以配置和管理 Netfilter 框架,定義網絡數據包過濾規則
iptables [-t table] command [match pattern] [action]
在指定好 table 和 chain 之後,需要對匹配模式進行指定
如
iptables -t nat -A POSTROUTING -p tcp -s 192.168.1.2 [...]
iptables -t nat -A POSTROUTING -p udp -d 192.168.1.2 [...]
iptables -t nat -A PREROUTING -s 192.168.0.0/16 -i eth0 [...]
至此,我們已經可以指定匹配模式來過濾包了,接下來就是選擇合適的動作,對於 nat table,有如下幾種動作 SNAT, MASQUERADE, DNAT, REDIRECT,都需要通過 -j 指定
iptables [...] -j SNAT --to-source 123.123.123.123
iptables [...] -j MASQUERADE
iptables [...] -j DNAT --to-destination 123.123.123.123:22
iptables [...] -j REDIRECT --to-ports 8080
SNAT: 修改源 IP 爲固定新 IP,SNAT 只對離開路由器的包有意義,因此它只用在 POSTROUTING chain 中
MASQUERADE: 修改源 IP 爲動態新 IP, 和 SNAT
類似,但是對每個包都會動態獲取指定輸出接口(網卡)的 IP
DNAT : 修改目的 IP
REDIRECT : 將包重定向到本機另一個端口
我們希望實現的是:從本地網絡發出的、目的是公網的包,將發送方地址修改爲路由器 的地址。
接下來假設路由器的本地網絡走 eth0
端口,到公網的網絡走 eth1
端口。那麼如下 iptables 命令就能完成我們期望的功能:
iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE
註冊鉤子函數
之後進入源碼查看,一個鉤子函數是怎麼被註冊的:
nf_hook_ops 是 netfilter 框架中用於定義鉤子操作的結構體
struct nf_hook_ops {
nf_hookfn *hook;//指向鉤子函數的指針,鉤子函數將在數據包通過特定鉤子時被調用
struct net_device *dev;//指向特定網絡設備的指針,表示該鉤子操作適用的網絡接口。
void *priv;//指向用戶自定義私有數據的指針,可以在鉤子函數中使用,以便傳遞額外信息
u8 pf;//協議族,用於指定該鉤子操作支持的協議
enum nf_hook_ops_type hook_ops_type:8;//鉤子操作的類型,通常用於區分不同用途的鉤子操作
unsigned int hooknum;//鉤子的位置,指示該鉤子操作在 Netfilter 鉤子鏈中的位置
int priority;//鉤子的優先級,值越小優先級越高
};
鉤子函數的註冊通常隨着模塊的加載,在模塊初始化過程中通過直接或間接調用 nf_register_net_hooks 或 nf_register_net_hook
依舊以 nat 爲例,其 hook 點和相應信息被定義如下
static const struct xt_table nf_nat_ipv4_table = {
.name = "nat",
.valid_hooks = (1 << NF_INET_PRE_ROUTING) |
(1 << NF_INET_POST_ROUTING) |
(1 << NF_INET_LOCAL_OUT) |
(1 << NF_INET_LOCAL_IN),
.me = THIS_MODULE,
.af = NFPROTO_IPV4,
};
初始化 NAT 表時,調用 iptable_nat_init 進行註冊
module_init(iptable_nat_init);
static int __init iptable_nat_init(void)
{
int ret = xt_register_template(&nf_nat_ipv4_table,
iptable_nat_table_init);
...
}
其關聯到 iptable_nat_table_init 函數進行初始化
static int iptable_nat_table_init(struct net *net)
{
...
repl = ipt_alloc_initial_table(&nf_nat_ipv4_table);//分配一個初始的NAT表結構
...
ret = ipt_register_table(net, &nf_nat_ipv4_table, repl, NULL);//將表註冊到netfilter框架中
...
ret = ipt_nat_register_lookups(net);//註冊NAT查找函數
...
}
使用 ipt_register_table 進行註冊
int ipt_register_table(struct net *net, const struct xt_table *table,
const struct ipt_replace *repl,
const struct nf_hook_ops *template_ops)
{
...
ret = translate_table(net, newinfo, loc_cpu_entry, repl);//參數檢查
...
new_table = xt_register_table(net, table, &bootstrap, newinfo);//註冊到netfilter框架中。
...
ret = nf_register_net_hooks(net, ops, num_ops);//註冊鉤子操作
...
}
nf_register_net_hooks 調用 nf_register_net_hook 函數來創建鉤子函數
int nf_register_net_hook(struct net *net, const struct nf_hook_ops *reg)
{
int err;
if (reg->pf == NFPROTO_INET) {//NFPROTO_INET協議族
if (reg->hooknum == NF_INET_INGRESS) {//chain爲INGRESS
err = __nf_register_net_hook(net, NFPROTO_INET, reg);
if (err < 0)
return err;
} else {
err = __nf_register_net_hook(net, NFPROTO_IPV4, reg);//註冊ipv4鉤子
if (err < 0)
return err;
err = __nf_register_net_hook(net, NFPROTO_IPV6, reg);//註冊ipv6鉤子
...
}
} else {
err = __nf_register_net_hook(net, reg->pf, reg);//註冊鉤子
if (err < 0)
return err;
}
return 0;
}
以 ipv4 鉤子爲例,進入__nf_register_net_hook 函數
static int __nf_register_net_hook(struct net *net, int pf,
const struct nf_hook_ops *reg)
{
struct nf_hook_entries *p, *new_hooks;
struct nf_hook_entries __rcu **pp;
int err;
switch (pf) {
case NFPROTO_NETDEV:
...
case NFPROTO_INET:
if (reg->hooknum != NF_INET_INGRESS)
break;
err = nf_ingress_check(net, reg, NF_INET_INGRESS);//檢查
if (err < 0)
return err;
break;
}
pp = nf_hook_entry_head(net, pf, reg->hooknum, reg->dev);//獲取指定鉤子的頭部指針
...
p = nf_entry_dereference(*pp);
new_hooks = nf_hook_entries_grow(p, reg);//擴展鉤子條目,以容納新註冊的鉤子。
...
nf_static_key_inc(reg, pf);//增加鉤子的計數
...
return 0;
}
其核心在 nf_hook_entries_grow 函數
static struct nf_hook_entries *
nf_hook_entries_grow(const struct nf_hook_entries *old,
const struct nf_hook_ops *reg)
{
...
new = allocate_hook_entries_size(alloc_entries);//分配新的鉤子條目空間
new_ops = nf_hook_entries_get_hook_ops(new);// 獲取新分配的鉤子的操作指針
//插入新鉤子
while (i < old_entries) {//遍歷舊鉤子
//比較優先級
//如果新的鉤子優先級低,將原始鉤子保留在新列表
if (inserted || reg->priority > orig_ops[i]->priority) {
new_ops[nhooks] = (void *)orig_ops[i];
new->hooks[nhooks] = old->hooks[i];
i++;
} else {//如果新的鉤子優先級高,插入新鉤子
new_ops[nhooks] = (void *)reg;
new->hooks[nhooks].hook = reg->hook;
new->hooks[nhooks].priv = reg->priv;
inserted = true;
}
nhooks++;
}
//未找到 即新插入的鉤子優先級最低
if (!inserted) {//在末尾插入
new_ops[nhooks] = (void *)reg;
new->hooks[nhooks].hook = reg->hook;
new->hooks[nhooks].priv = reg->priv;
}
return new;
}
至此,將相應的的鉤子插入到 netfilter 框架之中。
iptables 調用邏輯
iptables 的具體實現邏輯如下圖所示
在 iptables
中,從用戶態添加一個規則到內核態的流程如下:
用戶態調用:
-
用戶通過 iptables 命令行工具調用添加規則的命令:
-
該命令被解析,並調用相應的庫函數(通常是
libiptc
中的函數)。
準備規則:
- 在用戶空間,命令行工具會構造規則結構體,並將規則信息填充到這個結構體中。
調用 libiptc:
-
用戶態工具調用
libiptc
中的函數,例如iptc_append_entry
,來將規則添加到指定的鏈中。 -
libiptc
會使用套接字(通常是原始套接字)與內核通信。
套接字通信:
-
libiptc
使用socket
系統調用創建一個原始套接字,並使用getsockopt
或setsockopt
來發送規則。 -
具體而言,會調用
TC_INIT
來初始化句柄,並通過getsockopt
獲取當前規則的信息。
進入內核態:
- 當調用
setsockopt
發送規則時,控制權轉移到內核態。內核中的netfilter
處理邏輯會處理這個請求。
處理規則:
-
內核中的
netfilter
接收這個請求,並將規則添加到相應的鏈中。具體實現位於net/netfilter
目錄下。 -
內核會更新相應的數據結構(如
ip_tables
)。
返回結果:
- 添加規則後,內核會將結果返回給用戶態程序,通常通過返回值或設置
errno
的方式。
用戶態接收結果:
- 用戶態工具接收內核的返回結果,並打印成功或錯誤信息給用戶。
具體流程再次不進行敘述,主要核心邏輯在於xt_replace_table
struct xt_table_info *xt_replace_table(struct xt_table *table,
unsigned int num_counters,
struct xt_table_info *newinfo,
int *error)
{
struct xt_table_info *private;
...
//將舊錶的初始條目複製到新信息中
newinfo->initial_entries = private->initial_entries;
table->private = newinfo;
...
}
參考博文:A Deep Dive into Iptables and Netfilter Architecture | DigitalOcean
對應譯文:[譯] 深入理解 iptables 和 netfilter 架構 (arthurchiao.art)
參考博文:NAT with Linux and iptables - Tutorial (Introduction) (karlrupp.net)
對應譯文:[譯] NAT - 網絡地址轉換(2016) (arthurchiao.art)
參考博文:來,今天飛哥帶你理解 iptables 原理!- 騰訊雲開發者社區 - 騰訊雲 (tencent.com)
源碼環境爲 6.2.0
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/xDFEpEGnonl9RUUwQMkxOw