如何擴展Linux的ip_conntrack multi zone conntrack機制

inux中有一個基於Netfilter的連接跟蹤機制,即ip_conntrack,每一個conntrack表示的就是一個流,該流裏面保存了大量的信息字段,這些字段本地有效,指導着數據包的轉發策略,但是我覺得這些字段信息還不夠詳細,試想,一個nfmark字段好像就可以做到一切了,但是我如果想爲一個數據流綁定一個字符串怎麼辦呢?也許你會說使用iptables+ipset+nfmark可以完成一切,這也是UNIX/Linux哲學的風格,一種後現代主義的風格,但是最近我上了不歸路,非要在ip_conntrack裏面擴充一個字段,爲我們產品加入一個基於用戶名字符串的訪問控制和審計功能,於是我有了以下看似可以的方案,順便鄙視一下紙上談兵的人:
1.完全學ipmark的樣子,在sk_buff和nf_conn裏面均加一個mark字段,分別代表數據包的mark和數據流的mark
作罷的原因:
需要重新編譯內核,而我不希望爲了一個小小的功能重新編譯內核,背後的思想是我比較崇尚熱插拔。
2.不動sk_buff,只在nf_conn裏面加一個字段,skb僅僅作爲一箇中轉,在iptables的target通過skb找到nf_conn,設置nf_conn的info字段
作罷的原因:
Linux嚴格控制內核模塊的版本,模塊依賴的頭文件一點都不能動,如果我改變了net/netfilter/nf_conntrack.h,那麼新編譯的所有的依賴nf_conntrack.ko的模塊中的符號CRC碼都會變化從而無法通過內核的驗證,我不得不學Netfilter的一個項目xtables-addons中compat-xtables的樣子,把所有的會改變CRC碼的導出函數全部再重新實現一遍,然而,天啊,我起初的想法太天真了,沒完美了的循環依賴,以至於我想罵兩句:
第一:
ip_conntrack爲何不讓人擴展?雖然它有一個extend機制,但是MD簡直就是自說自話,全部都是預定義好的,就下面的枚舉裏面的幾類:

enum nf_ct_ext_id
{
    NF_CT_EXT_HELPER,
    NF_CT_EXT_NAT,
    NF_CT_EXT_ACCT,
    NF_CT_EXT_ECACHE,
    NF_CT_EXT_NUM,
};

你加一個新的類型,就會改變內核頭文件,既然不讓擴展,爲何還叫extend呢?你乾脆直接放進nf_conn就可以了,搞成extend感覺上好像多麼的模塊化,多麼的可插拔,實際上你能擴展的東西只能是邏輯,而不能是數據結構!

第二:

Linux爲何把extend寫的那麼死呢?當我突然感到這是合理的時候,我就三緘其口了,後面我會說到,數據結構需要可以自解釋,即自己解釋自己。雖然人可以看到一個結構體馬上說出它的含義,但是程序卻很難將一堆數據對應到一個結構體!自解釋,如果不知道自解釋,那就說明你根本就TM就不懂計算機!雖然你可能很精通編程...

思路

既然不能擴展nf_conn的extend,也不能在nf_conn本身加新的字段,那麼只能重新編譯內核了,在重新編譯內核的時候,加入且僅僅加入一個extend類型,作爲一箇中間層,在這個extend中實現一個可插拔的註冊機制,以後再想加入新的擴展就可以直接在這個extend的機制上進行了。然而,我還是不想編譯內核,這是一個思想!我希望做最小的改動。萬事都難不倒偏執的人,我採用了一個常規卻不常用的方法,那就是默默地擴展結構體的大小,這也正是在《JAVA編程思想》裏面學到的一個思想。

思想

這其實是一種OO的思想,找到一個基類,然後擴展它,在擴展繼承的過程中實現你自己的邏輯,我擴展的是內核的nf_conn_counter結構體:

struct nf_conn_counter {
    u_int64_t packets;
    u_int64_t bytes;
};

我希望它成爲下面的樣子:

struct nf_conn_counter {
    u_int64_t packets;
    u_int64_t bytes;
    unsigned char *info;
};

但是我又不能改變結構體的定義,所以我採用下面等價的辦法:

struct conn_info_extends_nf_conn_counter {
      struct nf_conn_counter base;
      char *info;
}

info是最關鍵的。我需要做的僅僅就是在爲nf_conn_counter分配空間的時候爲其多加一個指針的空間即可,至於這個指針指向什麼,自有調用者解釋。在我的需求中,它可能就是一個字符串,存在info信息。acct_extend原始定義爲(之所以選擇對acct開刀,是因爲它足夠簡單,在字面上裏面,其表示統計信息,加入一個info也無可厚非):

static struct nf_ct_ext_type acct_extend __read_mostly = {
    .len    = sizeof(struct nf_conn_counter[IP_CT_DIR_MAX]),
    .align    = __alignof__(struct nf_conn_counter[IP_CT_DIR_MAX]),
    .id    = NF_CT_EXT_ACCT,
};

將其修改爲:

struct info_compat {
        struct nf_conn_counter nc[IP_CT_DIR_MAX];
        unsigned char * info;
};

static struct nf_ct_ext_type acct_extend __read_mostly = {
        .len    = sizeof(struct info_compat),
        .align  = __alignof__(struct info_compat),
        .id    = NF_CT_EXT_ACCT,
};

到此爲止,我沒有修改任何內核頭文件,接下來我來寫一個測試模塊來進行測試:

#include <linux/ip.h>
#include <linux/module.h>
#include <linux/skbuff.h>
#include <linux/version.h>
#include <net/netfilter/nf_conntrack.h>
#include <net/netfilter/nf_conntrack_acct.h>


MODULE_AUTHOR("xtt");
MODULE_DESCRIPTION("gll");
MODULE_LICENSE("GPL");
MODULE_ALIAS("XTT and GLL");

struct nf_info {
        struct nf_conn_counter nc[IP_CT_DIR_MAX];
        char *info;
};

static unsigned int ipv4_conntrack_info (unsigned int hooknum,
                                      struct sk_buff *skb,
                                      const struct net_device *in,
                                      const struct net_device *out,
                                      int (*okfn)(struct sk_buff *))
{
        u32 addr = ip_hdr(skb)->daddr;
    // 測試我家的路由器的地址192.168.1.1
        if (addr == 0x0101a8c0) {
                struct nf_conn *ct;
                enum ip_conntrack_info ctinfo;
                struct nf_conn_counter *acct;
                struct nf_info *info;
                unsigned char *cn = NULL;
                ct = nf_ct_get(skb, &ctinfo);
                if (!ct || ct == &nf_conntrack_untracked)
                        return NF_ACCEPT;

                acct = nf_conn_acct_find(ct);
                if (acct) {
                        info = (struct nf_info *)acct;
                        info->info = (unsigned char*) kzalloc(32, GFP_ATOMIC);
                        if (!info->info) {
                                return NF_ACCEPT;
                        } 
            // 測試將1234567890作爲字符串設置到conntrack
                        memcpy(info->info, "1234567890", min(32, strlen("1234567890")));

                }
        }
        return NF_ACCEPT;
}

static struct nf_hook_ops ipv4_conn_info __read_mostly = {
                .hook          = ipv4_conntrack_info,
                .owner          = THIS_MODULE,
                .pf            = NFPROTO_IPV4,
                .hooknum        = NF_INET_LOCAL_OUT,
                .priority      = NF_IP_PRI_CONNTRACK + 1,
};

static int __init test_info_init(void)
{
        int err;
        err = nf_register_hook(&ipv4_conn_info);
        if (err) {
                return err;
        }
        return err;
}

static void __exit test_info_exit(void)
{
        nf_unregister_hook(&ipv4_conn_info);
}

module_init(test_info_init);
module_exit(test_info_exit);



到底成功了沒有呢?我需要將這一切展示在/proc/net/nf_conntrack裏面,但是由於我使用了acct機制,所以我需要打開內核的acct選項:
sysctl -w net.netfilter.nf_conntrack_acct=1
真正修改的地方在/net/netfilter/nf_conntrack_standalone.c的ct_seq_show函數:

if (seq_printf(s, "use=%u ", atomic_read(&ct->ct_general.use)))
    goto release;

{
    struct nf_info {
        struct nf_conn_counter acct[2];
        char *info;
    };
    struct nf_conn_counter *acct;
    struct nf_info *info;
    acct = nf_conn_acct_find(ct);
    if (acct) {
        info = (struct nf_info *)acct;
        if (info->info) {
            if (seq_printf(s, "info=%s\n", info->info)) {
                goto release;
            }
        }
    }
}

在測試的時候,/proc/net/nf_conntrack中擁有了一個信息:
ipv4 2 tcp 6 431985 ESTABLISHED src=192.168.1.109 dst=192.168.1.1 sport=33591 dport=50 packets=2 bytes=112 src=192.168.1.1dst=192.168.1.109 sport=50 dport=33591 packets=1 bytes=60 [ASSURED] mark=0 zone=0 use=2 info=1234567890
這就說明這種方法是可行的,改動的地方並不多,關鍵是你要找到一個危險性最少的開刀部位,然後按照OO的思想擴展它,給它它所有沒有的行爲。曾經,我對nat的extend進行了偷樑換柱,但那是不對的,正確的做法是在原有結構體的地址後面緊跟着進行擴展,類似0長度數組那種。
我需要解釋一下程序的自解釋了。Linux在實現nf_conntrack的extend的時候,爲何將類型數值以及定義的順序用一個枚舉寫死呢?換句話說那就是爲何不允許用戶隨意定義extend呢?答案是:那很難!Why?試想,如果我把一個結構體給了一個extend type。請問我把這個type存在哪?除了事先自定義一個type序列,僅存的辦法就是把這個type序列存在extend本身了,這就遇到了一個循環定義的問題,我們對此是沒有辦法的,一個程序很難看到一對數字後,然後就可以把它們看作一個結構體,起碼的字段分界就無法解決。雖然可以用OO的思想之外,剩下的解決方案就是尋求一種自解釋的數據格式。我能想到的就是ASN.1了。實際上,ASN.1也是一種事先定義的類型格式序列,只不過該序列是經過標準化的而已,一個ASN.1序列是不需要解釋的,它可以自己解釋自己,需要的僅僅是文檔。一個ASN.1序列可以將一段數據解釋爲一個結構體,或者反過來也可以。OpenSSL裏面的d2i/i2d就是做這個的。難道不是嗎?
清明時節雨紛紛,就這樣在沒有雨的一整天過去了,沒有從《JAVA編程思想》中看到什麼思想,依然沒有感悟到JAVA的思想,依然沒有。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章