天天看點

netfilter 概要

扼要地介紹Linux核心中netfilter,iptable,連接配接跟蹤,NAT功能。這個分析基于核心版本2.6.28。

       請不要奢望通讀本文檔就能融會貫通這四個功能實作,因為連作者也沒有達到那個程度~ 這隻是我給自己複習代碼時做些路标之用。網上列出netfilter代碼的文檔已經很多,是以,我隻以文字說明為主。

       行文難免會有錯,請不吝賜教。

一、netfilter。

       Netfilter本身并不複雜,它隻是在Linux協定棧上的功能點上一種hook注入機制。舉個例子,當Linux核心檢測到接收到的資料包是到達本機的,就會調用核心函數ip_local_deliver(),這個函數不會直接處理相應的事務,而是主動給Netfilter一次執行hook的機會:

int ip_local_deliver(struct sk_buff *skb) {

              return NF_HOOK(PF_INET, NF_INET_LOCAL_IN, skb, skb->dev, NULL,    ip_local_deliver_finish); }

       這裡NF_HOOK宏就是netfilter的核心入口了。它的主要功能實作是nf_hook_slow(),這個函數的邏輯不算複雜,處理普通包的代碼非常直覺,隻要留意一下NF_REPEAT/NF_QUEUE/NF_STOLEN的情況即可。

 Netfilter在IPv4協定棧上的Hook點如下:

Chain 函數名
LOCAL_IN Ip_local_deliver()
LOCAL_OUT

IP_VS_XMIT()

__ip_local_out()

__ip_local_out()内會進一步調用dst_output()
PRE_ROUTING

Xfrm4_transport_finish()

ip_rcv()

POST_ROUTING

Ip_output()

Ip_mc_output()

dst_output()可能調用它們。
FORWARD Ip_forward() dst_input()可能調用它。

        不同的Hook間是有優先級差別的,高優先級的Hook會先調用,這不是個可有可無的特性。例如,連接配接跟蹤代碼要求輸入IPv4分組的所有分片都得到齊了才行,再例如,NAT代碼靠一個連接配接是否已經confirm了判斷這個資料包是不是做進一步處理。

        Netfilter 在IPv4協定棧上的預設hooks有(其中FIRST的優先級最高,按從高到底排序):

Netfilter hook priority Hooks Chains
FIRST ip_sabotage_in() PRE_ROUTING
CONNTRACK_DEFRAG ipv4_conntrack_defrag()

LOCAL_OUT

PRE_ROUTING

RAW ipt_do_table() wrappers

LOCAL_OUT

PRE_ROUTING

SELINUX_FIRST selinux_ipv4_forward() FORWARD
selinux_ipv4_local() LOCAL_OUT
CONNTRACK ipv4_conntrack_in() PRE_ROUTING
ipv4_conntrack_local() LOCAL_OUT
MANGLE ipt_do_tables() wrappers All chains
NAT_DST nf_nat_in() PRE_ROUTING
nf_nat_local_fn() LOCAL_OUT
FILTER ipt_do_table() wrappers

LOCAL_IN

LOCAL_OUT

FORWARD

SECURITY ipt_do_table() wrappers

LOCAL_IN

LOCAL_OUT

FORWARD

NAT_SRC nf_nat_out() POST_ROUTING
nf_nat_fn() LOCAL_IN
SELINUX_LAST selinux_ipv4_postroute() POST_ROUTING
CONNTRACK_CONFIRM ipv4_confirm()

LOCAL_IN

POST_ROUTING

LAST

二、iptable。

       Iptable通過ip_tables_init()初始化,它調用nf_register_sockopt()為iptables注冊一個socket option,這個option用于讀或寫iptable的配置:Linux的防火牆規則、NAT轉換映射最終都是通過這個接口通知核心的。注意,這裡隻有讀和寫兩種操作,沒有改操作。是以,任何寫配置的操作都會之前的所有舊配置都替換掉。

       通過這個socket option寫iptable配置,最終都會調用核心函數do_replace()。這個函數的大緻過程是:

<!--[if !supportLists]-->1、  <!--[endif]-->調用translate_table()函數,将用ipt_replace結構描述的輸入資料轉換為用xt_table_info結構表示。在轉換過程中,會要必要的資料完整性檢查,同時還會加載所需的核心子產品,例如相應iptable table子產品,match子產品,target子產品,nat協定子產品等等。

<!--[if !supportLists]-->2、  <!--[endif]-->調用__do_replace()進行實際替換核心的資料結構。

       translate_table()涉及到的資料結構衆多,可以參考唐文俠士的大作“Linux netfilter機制分析”。這裡,我隻會該文做些補充。

        translate_table()處理時做得一個值得留意的檢查是每個規則的有效chain,由此我們可以得到不同table的有效chain:

table Valid chain
Filter

LOCAL_IN

LOCAL_OUT

FORWARD

NAT

PRE_ROUTING

POST_ROUTING

LOCAL_OUT

Mangle All chains
Security

LOCAL_IN

LOCAL_OUT

FORWARD

        Ipt_replace,和xt_table_info的entries成員儲存的是一個ipt_entry數組,而ipt_entry則到iptable規則本身,包括包模式(ip成員),比對要求(ipt_match結構),目标處理等資訊(ipt_target結構):

        “包模式”儲存于ipt_entry的ip成員内;

       “比對要求”和“目标處理”儲存于ipt_entry的elems成員内,這又是一個結構數組。這個數組以ipt_match序列開始,之後是ipt_target序列。Ipt_target序列以位元組ipt_entry->target_offset開始。

        Ipt_replace和xt_table_info的成員hook_entry[NF_INET_NUMHOOKS]儲存的是一系列entries的偏移。例如,hook_entry[NF_INET_LOCAL_IN]儲存着LOCAL_IN鍊上需要處理的第一個iptable規則的偏移。Iptables的核心函數ipt_do_table()會從這個偏移上找到的iptable規則開始處理。請注意,預設hooks表中有許多hook其實隻是ipt_do_table()的包裝函數,它們使用不同的iptable table調用它。

        Ipt_replace和xt_table_info的成員underflow[NF_INET_NUMHOOKS]儲存的是也一系列entries的偏移。有些iptable target可能傳回IPT_RETURN,這表明這要求核心傳回到上一個處理的規則上,這個回溯關系事實上是一條“鍊棧”。而每個chain都可以有這樣一個鍊棧,underflow[]記錄的就是這個棧的棧底偏移。

        Iptable的核心實作内有一個經典的空間換時間的例子。

        結合以上介紹,再讀ipt_do_table()函數應該就不再那麼困難了。

 三、連接配接跟蹤。

        在預設hooks表内,CONNTRACK優先級上的hook最終都會調用nf_conntrack_in()。

        這個函數的核心邏輯如下:

<!--[if !supportLists]--> 1、  <!--[endif]-->調用l4proto->error(),對輸入包作L4協定的合法性基本檢查。因為conntrack的hook點可能在協定棧的輸入路徑上,此時L4協定事先還沒有機會檢查。

<!--[if !supportLists]-->2、  <!--[endif]-->調用resolve_normal_ct(),這是連接配接跟蹤的核心函數;

<!--[if !supportLists]-->3、  <!--[endif]-->調用l4proto->packet(),根據L4協定的設計更新輸入skb連接配接跟蹤狀态,這個狀态資訊儲存于一個nf_conn資料結構中,一般其變量名為ct。

<!--[if !supportLists]-->4、  <!--[endif]-->若發現是一個REPLY方向的資料包,設定ct->status |= IPS_SEEN_REPLY_BIT,标記這個連接配接上已經發現了REPLY資料。

        Resolve_normal_ct()主要邏輯如下:

<!--[if !supportLists]--> 1、  <!--[endif]-->調用l3proto和l4proto->get_tuple(),獲得資料包的連接配接資訊,主要是L3位址,L4端口等;

<!--[if !supportLists]-->2、  <!--[endif]-->在net->ct.hash表中查找tuple,如果沒有找到,就調用init_conntrack()傳回一個“新的查找結果”;net對應的是一個名字空間的概念,用于實作類似于Solaris中的domain的功能。Net->ct.hash記錄了所有已經被跟蹤了的連接配接的資訊;

<!--[if !supportLists]-->3、  <!--[endif]-->将查找結果轉換為nf_conn結構形式,這個結構是記錄連接配接跟蹤狀态的主要結構,結果變量名為ct;

<!--[if !supportLists]-->4、  <!--[endif]-->Ctinfo變量記錄了目前連接配接的狀态。如果ct在REPLY方向上,ct_info = ESTAB+IS_REPLY,否則:

              如果本連接配接上已經出現了REPLY資料,就

                     ctinfo = ESTAB

              如果本連接配接是一個期待連接配接(expected connection),則

                     Ctinfo = RELATED

              否則

                     Ctinfo = NEW

<!--[if !supportLists]-->5、  <!--[endif]-->用ct和ctinfo更新輸入skb。

這裡需要一點解釋:

        1、連接配接跟蹤中的ESTAB狀态,不等同于TCP連接配接中的對應術語;

       2、舉一個期待連接配接的例子。FTP的資料連接配接和控制連接配接是兩個相關的L4連接配接。其中資料連接配接後于控制連接配接建立。在處理控制連接配接時,核心可以預見資料連接配接會在什麼端口上建立,這些資訊就記錄在核心中了。之後真正建立資料連接配接時,核心會先查找之前記錄的資訊,如果驗證本連接配接的确是一個期待連接配接,那麼就修改本連接配接狀态為RELATED。類似的處理還見于TFTP、ICMP等。

       3、粉色文字所描述的代碼是互相互聯的。

        再來看看init_conntrack():

<!--[if !supportLists]-->1、  <!--[endif]-->調用l3proto和l4proto->invert_tuple()獲得REPLY資料包的tuple資訊;

<!--[if !supportLists]-->2、  <!--[endif]-->調用l4proto->new();

<!--[if !supportLists]-->3、  <!--[endif]-->在之前的期待連接配接資訊中查找本連接配接的資訊,如果找到說明這是一個我們期待之中的連接配接,設定相應的标志位;

<!--[if !supportLists]-->4、  <!--[endif]-->初始化需要的conntrack extension;

<!--[if !supportLists]-->5、  <!--[endif]-->将新配置設定的nf_conn添加到net->ct.unconfirmed哈希表;

<!--[if !supportLists]-->6、  <!--[endif]-->如果可能,調用exp->expectfn();

 這裡也需要一些解釋:

<!--[if !supportLists]--> 1、  <!--[endif]-->關于conntrack externsion。有些資料結構不是所有nf_conn結構都需要的,比如期待連接配接資訊,NAT資訊等;如果為每個nf_conn都留出儲存這些資訊的位置是非常浪費空間,為此,核心設計conntrack extension機制。隻在需要時,才配置設定需要的空間,目前隻有三種extension。

<!--[if !supportLists]-->2、  <!--[endif]-->注意,新增加的nf_conn沒有直接增加到net->ct.hash中。因為CONNTRACK之後的包過濾hook可能會扔掉這個資料包,這個ct會在CONNTRACK_CONFIRM的hook内移動到net->ct.hash中。CONNTRACK_CONFIRM的hook實作比較簡單,本文不再多言,直接看代碼就行了。

 四,NAT

        NAT實作需要儲存轉換前後的資訊,這些資訊儲存于連接配接跟蹤狀态表中,也即nf_conn結構中,其中ORIG方向為原始位址資訊,REPLY方向被修改為轉換後位址資訊。

       在NAT_DST/NAT_SRC上的hooks,最後都會調用nf_nat_fn()函數,這是NAT功能的入口。

        Nf_nat_fn()的核心邏輯如下:

<!--[if !supportLists]--> 1、  <!--[endif]-->檢查目前skb,是否被本函數處理過,如果沒有,就檢查目前資料包的conn是否已經confirm過。如果已經confirm了,說明這個連接配接在NAT子產品加載之前就已經存在了,此時NAT不對之再作進一步,直接放行;

<!--[if !supportLists]-->2、  <!--[endif]-->若目前ctinfo為RELATED或者RELATED+IS_REPLY,且目前協定為ICMP,就調用nf_nat_icmp_reply_translation(),對ICMP包做特殊NAT處理,本函數傳回;

<!--[if !supportLists]-->3、  <!--[endif]-->若目前ctinfo為RELATED或者RELATED+IS_REPLY或者NEW,判斷該資料包是否已經作過NAT預處理了,如果沒有就調用nf_nat_rule_find()查找nat表作位址修改前的準備工作。但是如果目前chain為LOCAL_IN,就隻配置設定一個alloc_null_binding(),即構造一個不做任何位址映射的NAT配置;

<!--[if !supportLists]-->4、  <!--[endif]-->剩下一種情況是ctinfo為ESTAB,此時不作特别的NAT預處理;

<!--[if !supportLists]-->5、  <!--[endif]-->調用nf_nat_packet()實際修改資料包。

        一些解釋:

<!--[if !supportLists]-->1、  <!--[endif]-->關于alloc_null_binding(),将nf_nat_rage.min_ip和max_ip設定為與原IP位址相同的IP位址,即不需轉換,然後調用nf_nat_setup_info()。

<!--[if !supportLists]-->2、  <!--[endif]-->Nf_nat_rule_find()的核心功能是通過ipt_do_table()完成,額外再處理一些邊界條件。而nat表上的兩個重要target:SNAT和DNAT的函數最終都會調用nf_nat_setup_info()進實際的NAT預處理操作;

        Nf_nat_setup_info()的核心邏輯:

<!--[if !supportLists]--> 1、  <!--[endif]-->首先将ct->tuplehash[REPLY]反轉一下。因為REPLY方向的ct資訊可能儲存了NAT轉換之後的位址資訊,這樣其實就是在得到可能的NAT轉換結果;

<!--[if !supportLists]-->2、  <!--[endif]-->因為以上的結果還有可能是沒有NAT轉換過的位址,是以這裡再用上面的結果調用get_unique_tuple(),擷取一個真正可用的NAT轉換後位址;

<!--[if !supportLists]-->3、  <!--[endif]-->若新得到的位址資訊與前不同,則:

<!--[if !supportLists]-->a)        <!--[endif]-->求這個新位址資訊“反轉”,即轉換後的REPLY方向資訊;

<!--[if !supportLists]-->b)        <!--[endif]-->使用上面的“反轉”結果初始化ct->tuplehash[REPLY]。

<!--[if !supportLists]-->4、  <!--[endif]-->将ct->tuplehash[ORIG]加入到net->ipv4.nat_bysource哈希表中。

        Get_unique_tuple()核心邏輯:

<!--[if !supportLists]--> 1、  <!--[endif]-->如果該位址資訊已經是SNAT過的,且該位址資訊就是為本資料包服務的就直接傳回之,沒有必要再繼續處理了。這個判定過程是通過find_appropriate_src()完成的,在這個函數内部會先查找剛才提到的net->ipv4.nat_bysource哈希表,然後判斷是否這個位址資訊是否就是“自己人”;

<!--[if !supportLists]-->2、  <!--[endif]-->調用find_best_ips_proto(),通過hash“揉”出可用的NAT轉換資訊,一個新tuple;

<!--[if !supportLists]-->3、  <!--[endif]-->使用nat proto相關的函數,以确定這個新tuple滿足它們的要求,如果有必要nat proto也可修改之。

nf_nat_packet()的代碼很少,但核心邏輯有些繞,可以結合以下表格了解它:

NAT類型 LAN->WAN WAN->LAN 注解
SNAT 根據reply tuple改SIP 根據orig tuple改DIP 一般由LAN側發起
DNAT 根據orig tuple改SIP 根據reply tuple改DIP 一般由WAN側發起

繼續閱讀