nat規則將資料流的地址資訊進行轉換,轉換了之後需要將轉換後的地址資訊寫入ip_conntrack結構體中,經過nat之後目的地址無非兩個方向,一個是本機(redirect target),一個是其它機器(網關上的nat一般都這樣),於是netfilter需要對這兩個方向的轉換記錄提供支援。
netfilter的ip_conntrack提供了下列兩個HOOK,ip_conntrack_out_ops用於nat到其它機器的支援,ip_conntrack_local_in_ops提供nat到原生支援:
static struct nf_hook_ops ip_conntrack_out_ops = {
.hook = ip_refrag, //這個名字起得有些怪異,不過也很合理,因為ip_conntrack由於可能需要操作4層以上協議頭或資料載荷,因此必須在PREROUTING這個掛載點上對ip資料包進行defrag操作。
.owner = THIS_MODULE,
.pf = PF_INET,
.hooknum = NF_IP_POST_ROUTING,
.priority = NF_IP_PRI_LAST,
};
static struct nf_hook_ops ip_conntrack_local_in_ops = {
.hook = ip_confirm, //這個名字很好,不過也沒有覆蓋其全部功能
.owner = THIS_MODULE,
.pf = PF_INET,
.hooknum = NF_IP_LOCAL_IN,
.priority = NF_IP_PRI_LAST-1,
};
不管是ip_refrag還是ip_confirm,最終都是要調用__ip_conntrack_confirm的,從__ip_conntrack_confirm這個函數的邏輯可以一眼看出ip_conntrack的結構:
int __ip_conntrack_confirm(struct nf_ct_info *nfct)
{
...
hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);
repl_hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_REPLY].tuple);
...
if (!LIST_FIND(...IP_CT_DIR_ORIGINAL...) //如果原始流和nat後(也可以不做nat,此時兩個tuple是一樣的)的流資訊都沒有加入hash
&& !LIST_FIND(...IP_CT_DIR_REPLY...)) {
list_prepend(&ip_conntrack_hash[hash],
&ct->tuplehash[IP_CT_DIR_ORIGINAL]);
list_prepend(&ip_conntrack_hash[repl_hash],
&ct->tuplehash[IP_CT_DIR_REPLY]);
ct->timeout.expires += jiffies;
...
set_bit(IPS_CONFIRMED_BIT, &ct->status); //流已經順利地經過了本機
...
}
...
}
最後,通過ip_conntrack為引子來用一種簡單的方式描述一下ip_conntrack在整個netfilter中的作用以及其實現邏輯:
struct ip_conntrack
{
...
struct ip_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX]; //一共2個方向
...
struct list_head sibling_list; //related的串連
...
struct ip_conntrack_expect *master; //和sibling_list的意義相反
...
struct {
struct ip_nat_info info;
union ip_conntrack_nat_help help;
} nat;
};
repl是replace的意思,是這樣的,因為orig是不會被修改的,所以repl雖然字面是reply,實則replace,不管是SNAT還是DNAT,修改的都是資料包往目的地方向的鏈路資訊,對於SNAT來說,資料流進入網關,然後被snat,此時orig的tuple是不會變的,變化的是reply的tuple,因為資料回應回來以後,目的地址就是轉換後的源地址,而源地址就是當前資料流的目的地址,所以repl作為reply和replace來講都是一樣的。
事實是,tuple只是為了匹配流的,在資料包剛進入時,為了將資料包和一個流相聯絡,tuple就有用了,ip_conntrack_tuple中包含足夠的資訊用於匹配一個流,包括ip地址和連接埠號碼:
struct ip_conntrack_tuple
{
struct ip_conntrack_manip src; //和下面的dst類似
struct {
u_int32_t ip; //ip地址
union {
...//應用程式層協議
} u;
u_int16_t protonum; //傳輸層協議
} dst;
}; //該結構包含兩個ip-port對
下面用一種抽象的方式將ip_contrack以及nat的流程化繁為簡而為一種簡單的形式,從情景角度分析資料包流經nat網關時核心中相關模組的行為,資料流以(x-y)表示,並且忽略連接埠資訊,忽略狀態資訊。其實狀態資訊很重要,netfilter的其它模組可以使用ip_contrack設定的狀態從而做出特殊的決策,同時狀態資訊還可以標識一個流目前的行為以及目前其需要被給與的行為,忽略之是為了事情更簡單,引出一種分析代碼的方式,凡遺漏之事務容日後視心力與心情加之。
資料包(a-b)進入R,發生了snat,地址資訊成為了(m-b),雖然發生了nat,(a-b)和(m-b)應該是屬於同一個資料流CK的,ip_conntrack需要作記錄,以便將兩個流綁定在一起,資料從a到b的方向在R處成了由m到b的方向,屬於一個方向,都是源到目的,發生了snat後,資料就可以出去了,既然資料出去R了,我們也就不關心它了,我們關心的是從b發出的回應a資料到達R後如何將之綁定到流CK,資料回來後由於發生過snat流標示顯然是(b-m),於是ip_conntrack需要將(b-m)也綁定到CK,此時我們可以定義出CK了:
struct Contrack {
Two-Direct[2];
}CK = {
Two-Direct[0] = (a-b);
Two-Direct[1] = (b-m);
};
#define IP_CT_DIR_ORIGINAL 0
#define IP_CT_DIR_REPLY 1
和上面的ip_conntrack對比一下看看少了什嗎?既然上面的例子是在R處發生了snat,那麼nat的指導資訊顯然也應該在CK中,加上後就圓滿了。nat資訊實際上是一個數組,並且是兩個方向上的,比如下列規則:
-A POSTROUTING -d 172.16.0.0/255.255.0.0 -o eth0 -j MASQUERADE
實際上有兩條nat規則,一個是到達172.16.0.0網路的資料來源地址轉化為eth0的地址,另一條是從172.16.0.0回應的資料包的地址還要轉化回來,否則資料就有去無回了,所以針對一個源地址(-s)或者目的地址(-d),一共需要有(兩個方向*一條流最大掛載點)條nat規則,看一下一個流最大可以有幾個掛載點,如果資料只是過路,那麼最大也就兩個,既作snat,又做dnat,可是如果去本機出入的流,那麼很有可能會有三個掛載點,因此nat規則一共需要3對也就是6條,用數組表示就是nat_info[6],於是CK成了:
CK = {
Two-Direct[0] = (a-b);
Two-Direct[1] = (b-m);
NAT-Info[] = {,,,,,};
};
最終NAT-Info中的元素是什麼呢?肯定是地址資訊了,最簡單的方式就是每個元素就是一個ip-port對,然後通過數組下標來索引nat的類型,比如定義:
#define SRC_NAT 0
#define OPPOSITE_SRC 1
#define DST_NAT 2
#define OPPOSITE_DST 3
...
有了上述定義後來初始化nat資訊數組:
NAT-Info[] = {{src_ip_to,port1},{src_ip_from,port2},
{dst_ip_to,port3},{dst_ip_from,port4},
...}; //對應於上述CK的定義,這裡的src_ip_to就是m,而src_ip_from就是a
src_ip_to是需要轉換成的新源ip,src_ip_from是回應資料需要轉換回的原始源ip,dst首碼的ip地址的含義類似,這張表初始化完了之後,對應資料流再有資料包來的時候就可以直接通過查這張表來進行地址轉換了。由於來回兩個方向的流都被映射進了CK結構體,因此不管哪個方向過來資料,(a-b)也好,(b-m)也好,都會對應到同一個CK,這在linux中是通過hash實現的,既然找到了CK,從CK中取出NAT-Info就可以得到如何轉化地址的資訊,很顯然,不可能每次資料包到來時都要查nat表,而是在一個流的第一個包到達時就確定了NAT-Info,也就是一個流(CK)建立的時候,建立一個CK之後,尋找nat表,如果有規則命中,那麼根據nat表的規則來建立NAT-Info資訊,同時還要更新Two-Direct[1]為新的轉換後的流(注意是反向的),該流的第一個資料包流出機器或者流往使用者層的時候將上述流資訊記錄到hash中,兩個方向的都要記錄,如果再有包來臨,不管哪個方向的,請求包還是回應包,通過地址資訊查詢hash都可以找到CK,然後到了nat的時候,直接從CK將NAT-Info取出即可,取出後判斷當前是哪個HOOK,根據當前的HHOK來使用NAT-Info中的資訊實行地址轉換。整個過程中,CK的作用就是追蹤串連,它最大的共用就是一個流的第一個包來的時候建立CK,之後nat會使用這個初始CK查nat表,之後再來資料包CK以及其中的資訊比如nat資訊就可以直接取出來使用了,是conntrack模組取出,後續模組使用。
不要被核心代碼中複雜的細節所蒙蔽,其實每一段代碼每一個機制的思想都是很簡單的,正如ip_conntrack-nat的思想以及資料結構的設計和我上述的說明一樣,如果能通過閱讀代碼將資料結構抽象成最簡單的形式並且剖析出思想,那麼閱讀代碼才算是有了收穫,否則總有一天會迷失於茫茫字元海中而不可自拔,最終只見樹木不見森林,搞得自己也不想再鑽研了。理解了大致的流程之後,再次閱讀代碼的時候就要詳細些了,以下是幾個比較重要的函數:
ip_nat_setup_info:初始化ip_nat_info資訊;
find_best_ips_proto_fast:初始化nat後的新的tuple;
ip_conntrack_alter_reply:配置由於nat而改變的反向tuple;
do_bindings:nat模組實施nat轉換。