1. 程式人生 > >終於搞定Linux的NAT即時生效問題

終於搞定Linux的NAT即時生效問題

               

引:超長的前言

Linux的NAT不能及時生效,因為它是基於ip_conntrack的,如果在NAT的iptables規則新增之前,此流的資料包已經綁定了一個ip_conntrack,那麼該NAT規則就不會生效,直到此ip_conntrack過期,如果一直有資料在魯莽地嘗試傳輸,那麼就會陷入僵持狀態。
       在Linux系統中,ip_conntrack建立成功是按照一個流的頭包是否成功被傳輸出協議棧這個原則來判定的,因此只要有路由,一個數據包總是能被forward出去,此後,直到ip_conntrack過期,後續的包將都不是頭包,雖然它們都是NEW狀態(這是因為只有收到對端回覆才會變為ESTABLISH狀態),但是卻不是第一個包,因此就沒有機會去匹配後續新增的NAT規則。這件事令我頭疼了好久,也實現了很多版本的諸如“NAT的即時匹配”等模組,但是均不完美,不完美之處在於和原生的NAT不相容,雖然Linux的NAT做得不好,但是保持對它的相容是對狂妄者最大的尊敬,說白了就是不應該修改既有NAT的程式碼!
       令我奇怪的是,為什麼沒有人遇到和我一樣的問題以及像我一樣煩惱,如果有的話,我敢保證一定有現成的東西可用,不幸的是,沒有,起碼我沒有找到這樣的志同道合者。即使懶惰的開源社群沒有積極的人,為何像Endian Firewall,Firewall Builder等這些重量級UTM,或者OpenWRT這種不倫不類者都沒有實現?Why?
       其實,這個問題也不是那麼難,使用者態完全可以封裝一個新的iptables nat指令碼,在添加了NAT規則之後,先使用conntrack-tools工具中的conntrac命令執行-D,引數是和NAT規則一樣的matches,這樣就清除了相關的ip_conntrack,NAT得以即時生效。但是這種做法不是那麼的“本原主義”,有時候我在想,UNIX的KISS到底是遵循柏拉圖呢,還是針對柏拉圖的逆襲...我需要的是有一種機制,而不僅僅是一種策略來完成“NAT的即時生效”問題,前提是不能改動現有的程式碼。於是我徹底否定了上述的使用者態封裝的方案,也半否定了之前的修改NAT模組的方案,我要一個新的方案,它是另外一個模組,載入之,則成。
       Netfilter,Linux平臺“蹂躪”網路協議棧的利器,它幾乎可以做到任何事情。它可以把資料包擺來擺去,隨意放到隨便什麼地方,只要你願意,你真的可以把資料包放到隨便你想讓它去的地方。本文涉及的是一個queue/reinject機制,就是說,在一個地方,特定的地方將資料包排入佇列,然後一個處理器處理佇列裡面的資料包,然後再將其重新注入它排隊的地方繼續接受協議棧的後續處理,典型的queue機制是將資料包排入使用者態的一個程式,然後一個程式處理之,再通過Netlink套接字將其重新注入,這是十分靈活的方法,我們完全可以寫一個使用者態程式,然後在其中將其ip_conntrack結構刪除,再重新注入,這樣,下一個資料包到來的時候就可以匹配NAT規則了,但是怎麼刪除呢?到頭來還是要用conntrack的介面-就像conntrack-tools那樣。有沒有更方便的方法呢?
       在核心態,有現成的ip_conntrack的操作介面可用呼叫,是不是就不用到使用者態再處理呢?一般而言,匯入使用者態是因為使用者態處理更加靈活,靈活性才是基本原則,如果在核心態更靈活,那就直接在核心態處理!是的,這就是我的思路。在我的實現中,我並不用等到下一個資料包才能匹配NAT規則,而是本資料包就可以。
       十分感謝xtables-addons,它提供了一個編譯模板,我可以很方便得實現自己的想法,一般而言就寫兩個C檔案,有時候需要一個H檔案即可,這次我又一次受惠於xtables-addons,可以快速開發,驗證自己模組的功用。現在該給出程式碼了。

說明:命名與思路

我不知道該怎麼給自己的模組命令,我的英文狠爛,老婆很忙又不肯幫我,又不能起一箇中文名字,因此我只能使用XXX這種讓人遐想的名字,我不會使用aaa,abc這種,這樣會讓人覺得我不負責任,有點玩世不恭或者太草率等所有你能想到,並且,真實地,我也因為這種草率埃過領導的批評以及同道人的嘲諷。接受了教訓之後,我就使用XXX。
       說完了這個奇怪的XXX命名,說一下思路。我的思路很簡單,那就是針對所有的不會匹配NAT規則的資料包,將其ip_conntrack結構體刪除,然後將其重新注入到PREROUTING的最開始處,這樣它就可以繫結一個新的NEW狀態的ip_conntrack,因此這樣就可以匹配NAT規則了。

第一部分:iptables模組檔案libxt_XXX.c

/* *      "XXX" target extension for iptables! 其中就是一個幌子,為了使用iptables而已! * *      This program is free software; you can redistribute it and/or *      modify it under the terms of the GNU General Public License; either *      version 2 of the License, or any later version, as published by the *      Free Software Foundation. */#include <stdio.h>#include <xtables.h>#include "compat_user.h"static void xxx_tg_help(void){        printf("XXX takes no options\n\n");}static int xxx_tg_parse(int c, char **argv, int invert, unsigned int *flags,                         const void *entry, struct xt_entry_target **target){        return 0;}static void xxx_tg_check(unsigned int flags){}static struct xtables_target xxx_tg_reg = {        .version       = XTABLES_VERSION,        .name          = "XXX",        .revision      = 1,        .family     = NFPROTO_IPV4,        .help          = xxx_tg_help,        .parse         = xxx_tg_parse,};static __attribute__((constructor)) void xxx_tg_ldr(void){        xtables_register_target(&xxx_tg_reg);}


可以看到,這個檔案簡直就是一場騙局,裡面什麼都沒有!是的,什麼也沒有,之所以寫這個檔案,只是為了我可以寫下:
iptables -t mangle -A PREROUTING ... -j XXX
的時候而不報錯。確實,XXX就是一個target,既然你想用iptables,起碼也要例行一下公事,哪怕僅僅是敷衍一下而已,確實也只是敷衍一下。否則,你依然有辦法設定,那就是使用procfs,sysfs,或者Netlink等...

第二部分:核心模組xt_XXX.c

/* *      xt_xxx - kernel module to drop and re-NEW CONNTRACK to *              fit NAT * *      Original author: Wangran <[email protected]> */#include <linux/module.h>#include <linux/netfilter/x_tables.h>#include <net/netfilter/nf_queue.h>#include "compat_xtables.h"MODULE_AUTHOR("Wanagran <[email protected]>");MODULE_DESCRIPTION("Xtables: xxx match module");MODULE_LICENSE("GPL");MODULE_ALIAS("ipt_xxx");/* * queue handler捕獲資料包,然後重新注入,區別在於: * 1:如果本身是NOTRACK的資料包,直接注回去; * 2:如果本身沒有繫結任何conntrack,直接注回去; * 3:如果本身有conntrack,刪掉該conntrack後,注回去 * 3.1.不是注回原來的位置,而是注回PREROUTING最開始的位置。 * 注意:雖然TAGEGET本身已經阻止了1,2的情況,還是判斷了一下, *       因為雖然我知道這一點,但是resetct_queue並不清楚... */static int resetct_queue(struct nf_queue_entry *entry, unsigned queue_num){        struct sk_buff *skb = entry->skb;        struct nf_conn *ct = NULL;        enum ip_conntrack_info ctinfo;        if (nf_ct_is_untracked(skb))                goto reinject;        else if (!(ct = nf_ct_get(skb, &ctinfo)))                goto reinject;        else {                // 為了重新初始化conntrack,使之狀態變為可做NAT的NEW!                struct list_head *elem = &nf_hooks[entry->pf][entry->hook];                nf_reset(skb);                nf_ct_kill(ct);                entry->elem = list_entry(elem, struct nf_hook_ops, list);        }reinject:        nf_reinject(entry, NF_ACCEPT);        return 0;}/* * XXX的執行TARGET,旨在針對以下的一類資料包進行queue處理: * 本身是NEW狀態,且已經被confirm了,這種資料包在其conntrack * 過期之前,無疑已經不會再去匹配任何NAT規則了! */static unsigned intxxx_tg4(struct sk_buff **skb, const struct xt_action_param *par){        struct nf_conn *ct;        enum ip_conntrack_info ctinfo;        ct = nf_ct_get(*skb, &ctinfo);        if (!ct || ct == &nf_conntrack_untracked) {                return XT_CONTINUE;        }        // 僅僅處理正向資料包,否則...        if (CTINFO2DIR(ctinfo) == IP_CT_DIR_REPLY) {                return XT_CONTINUE;        }        if (ctinfo == IP_CT_NEW && !nf_ct_is_confirmed(ct)) {                return XT_CONTINUE;        }        return NF_QUEUE;}static struct nf_queue_handler xxxqh = {        .name  = "resetct",        .outfn = resetct_queue,};static struct xt_target xxx_tg_reg[] __read_mostly = {        {                .name           = "XXX",                .revision       = 1,                .family         = NFPROTO_IPV4,                .table          = "mangle",                .hooks          = 1 << NF_INET_PRE_ROUTING,                .target         = xxx_tg4,                .me             = THIS_MODULE,        },};static int __init xt_xxx_target_init(void){        int status = 0;        status = nf_register_queue_handler(NFPROTO_IPV4, &xxxqh);        if (status < 0) {                printk("XXX: register queue handler error\n");                goto err;        }        status = xt_register_targets(xxx_tg_reg, ARRAY_SIZE(xxx_tg_reg));        if (status < 0) {                printk("XXX: register target error\n");                goto err;        }err:        return status;}static void __exit xt_xxx_target_exit(void){        nf_unregister_queue_handlers(&xxxqh);        return xt_unregister_targets(xxx_tg_reg, ARRAY_SIZE(xxx_tg_reg));}module_init(xt_xxx_target_init);module_exit(xt_xxx_target_exit);


核心模組也很簡單,我不喜歡複雜,並且對Netfilter理解得足夠多,多到可以做到不復雜,這並不意味著我搞不定複雜,畢竟簡單的基礎就是複雜,理解Netfilter本身就是一件很複雜的事。不想多說,如果你懂Netfilter,自然可以一眼看明白上述程式碼,如果不懂,要麼去學,要麼放棄,如果僅僅想用一下這個模組,那就編譯載入。

第三部分:關於使用

一般而言,你可以使用下面的命令:
iptables -t mangle -A PREROUTING -j XXX

這樣的話,所有進來的資料包都會執行下面的邏輯:


如果這樣,相當於架空了整個ip_conntrack的優化,這種魯莽的做法並不是我的目的,我希望它和其它的match比如mark,condition一起使用,這樣就可以把不相關的資料包過濾掉而不觸及,依舊執行往常的邏輯,這就是我為何一直堅持使用iptables的原因而不是使用其它的使用者態/核心態通訊的方式。就想之前我提到的基於ip_conntrack的快速/慢速匹配方式那樣,這個NAT及時匹配也可以使用類似的邏輯:

iptables -t mangle -A PREROUTING -m condition --condition slow ... -j XXX
slow變數維持的時間T,T等於所有協議中ip_conntrack過期的最長時間。