iptables 和 netfilter 學習筆記

作者簡介:張銘軒,西安郵電大學計算機專業研二學生,導師陳莉君教授,熱衷於探索 linux 內核。

iptables 和 netfilter

iptables 是 Linux 上最常用的防火牆工具,iptables 與協議棧內有包過濾功能的 hook 交互來完成工作。這些內核 hook 構成了 netfilter 框架

每個進入網絡系統的包(接收或發送)在經過協議棧時都會觸發這些 hook,程序可以通過註冊 hook 函數的方式在一些關鍵路徑上處理網絡流量。iptables 相關的內核模塊在這些 hook 點註冊了處理函數,因此可以通過配置 iptables 規則來使得網絡流量符合防火牆規則。

netfilter 框架

netfilter 提供了 5 個 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 會執行以下步驟:

  1. 查找鉤子列表:根據傳入的協議和鉤子點,Netfilter 查找已註冊的鉤子函數列表。

  2. 調用鉤子函數:遍歷所有註冊的鉤子函數,依次調用每個鉤子函數,並將數據包(skb)傳遞給它們。

  3. 處理結果:每個鉤子函數可以選擇:

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 也不同

綜合前面討論的 table 順序問題,我們可以看到對於一個收到的、目的是本機的包: 首先依次經過 PRETOUTING chain 上面的 raw、mangle、nat table;然後依次經 過 INPUT chain 的 mangle、filter、security、nat table,然後纔會到達本機的某個 socket。

iptables 規則

規則放置在特定 table 的特定 chain 裏面。當 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, 那這個包會跳過連接跟蹤系統。

連接跟蹤系統中的連接狀態有:

iptable 使用

iptables 可以配置和管理 Netfilter 框架,其是一個用戶態工具,用以定義網絡數據包過濾規則,此處主要以 NAT 爲例作爲講解

NAT

NAT 是一種網絡技術,用於在網絡設備(如路由器、網關)上對數據包的源地址或目的地址進行修改,通常在局域網與互聯網之間進行轉換。NAT 主要用於節約 IPv4 地址,隱藏內部網絡的結構,並在網絡層提供一定程度的安全性。

NAT 在路由器或防火牆處修改通過的數據包的 IP 地址信息。常見的 NAT 類型包括:

  1. SNAT(Source NAT):修改數據包的源 IP 地址,通常用於內網設備訪問外網時,將內網設備的私有 IP 地址替換爲公共 IP 地址。

  2. DNAT(Destination NAT):修改數據包的目的 IP 地址,通常用於將外部請求映射到內網的某臺服務器(如端口轉發)。

NAT 通過維護一個映射表,跟蹤修改前後的 IP 地址和端口號,從而保證數據包能正確地返回給發起請求的內部設備。

NAT 的類型

  1. 靜態 NAT:一個內部 IP 地址映射到一個固定的外部 IP 地址。通常用於讓內部服務器暴露給外部網絡。

  2. 動態 NAT:內部 IP 地址池映射到外部 IP 地址池。適用於內網設備臨時需要與外網通信,使用的外部 IP 地址動態分配。

  3. 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 = (<< NF_INET_PRE_ROUTING) |
     (1 << NF_INET_POST_ROUTING) |
     (<< 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 中,從用戶態添加一個規則到內核態的流程如下:

用戶態調用

準備規則

調用 libiptc

套接字通信

進入內核態

處理規則

返回結果

用戶態接收結果

具體流程再次不進行敘述,主要核心邏輯在於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