天天看點

linux核心netfilter之ip_conntrack子產品的作用舉例--ftp為例

很多協定的控制資訊在應用層資料中被包含,這些資訊直接影響到了鍊路的建立,比如ftp協定就是這樣,ftp分為port模式和pass模式,port模式中,起初client連接配接server的21端口,然後當需要傳輸data的時候,client發送一個控制包給server,包中包含client端開啟的端口和自己的ip位址,server收到之後用自己的20端口去連接配接client控制包中建議的ip和端口,在這種情況下,如果client在nat後面使用私網位址,那麼server将無法連接配接client,是以nat網關必須要處理這種情況,處理方式就是修改client發給server的控制包(如果加密将不可能修改,還好ftp是不加密的);在pass模式下,client連接配接server的21端口後,如果要傳輸data,client還要連接配接server的另一個随機端口,該端口是由server發送的控制包傳給client的,如果client或者server端所在的防火牆禁止了任意非熟知端口,那麼資料将被防火牆攔截;不管是port模式還是pass模式,防火牆都要處理“第二個”資料連接配接通路的放行問題,在linux中是通過RELATED狀态來放行的,正如前文所述,隻需配置一條--state RELATED -j ACCEPT規則即可,但是具體這個規則如何實作,linux的連接配接追蹤子產品又是怎樣處理ftp的nat問題的,本文詳述之。

     首先從ip_conntrack的HOOK函數說起:

unsigned int ip_conntrack_in(...)

{

    ...

    proto = ip_ct_find_proto((*pskb)->nh.iph->protocol); //從資料包中取出協定号

    ...//resolve_normal_ct會試圖在已建立的連接配接中尋找剛進入的包屬于的連接配接,如果找不到則建立立一個狀态為NEW的連接配接,同時還要初始化該連接配接相關的資料,比如helper

    ct = resolve_normal_ct(*pskb, proto,&set_reply,hooknum,&ctinfo);//此中調用的init_conntrack函數是一個做了很多事的函數

    if (ret != NF_DROP && ct->helper) {  //如果有helper則調用其help函數

        ret = ct->helper->help(*pskb, ct, ctinfo);

        ...

    }

}

init_conntrack中有如下邏輯:

...//從連結清單中查找該連接配接,如果找到說明這是一個“預測”的連接配接

expected = LIST_FIND(&ip_conntrack_expect_list, expect_cmp,

                 struct ip_conntrack_expect *, tuple);

...

if (expected) {

    __set_bit(IPS_EXPECTED_BIT, &conntrack->status); //預測的連接配接到了,設定一個标志,在resolve_normal_ct得到已有連接配接的情況下會判斷如果有了這個标志,則設定IP_CT_RELATED狀态,該狀态可用于filter的判斷

    expected->sibling = conntrack; //預測的連接配接已經到來并且初始化了。expected->sibling在預測的時候是NULL,因為那時僅僅是預測,連接配接還沒有真的到來,後面可以看到,ip_conntrak預測之後,ip_nat會使用預測結果,然後調用helper的help修改應用層的和連接配接相關的控制資料,比如ip位址和端口資訊,在周遊一個已有連接配接的所有預測到的連接配接進而決定是否調用ip_nat的helper時,如果一個預測即一個ip_conntrack_expect的sibling字段非NULL,ip_nat将跳過此預測結果,因為它已經是真實的連接配接了,說明已經在它還是預測的連接配接的時候就已經被help過了。

     每一個ip_conntrack都可以擁有多個helper,用于幫助處理連接配接相關的資訊,比如ftp協定穿越防火牆就需要處理nat和副連接配接(data連接配接)問題,是以就有必要用一個helper子產品來處理這一類情況,處理ftp nat的helper和處理副連接配接的helper其實不是一類helper,前者是ip_nat_ftp結構體,後者是ip_conntrack_ftp結構體,雖然不同,但是它們的處理邏輯和注冊邏輯都是一樣的,是以到後面說ftp nat的時候再統一說明。下面是ip_conntrack_ftp注冊的help函數的實作邏輯

static int help(...)

    ...//操作skb,取出我們需要的一切資訊

    skb_copy_bits(skb, dataoff, ftp_buffer, skb->len - dataoff);

    array[0] = (ntohl(ct->tuplehash[dir].tuple.src.ip) >> 24) & 0xFF;

    array[1] = (ntohl(ct->tuplehash[dir].tuple.src.ip) >> 16) & 0xFF;

    array[2] = (ntohl(ct->tuplehash[dir].tuple.src.ip) >> 8) & 0xFF;

    array[3] = ntohl(ct->tuplehash[dir].tuple.src.ip) & 0xFF;

    //以上的這個array就是server需要連接配接的ip位址

    for (i = 0; i < ARRAY_SIZE(search); i++) {

        if (search[i].dir != dir) continue;

        found = find_pattern(...);//在ftp_buffer中尋找search字元,如果找到了,則說明本次資料包需要help,其中有個參數是個數組,數組的每一個元素都是一個比對鍵,此謂search,是一個ftp_search結構體類型的數組

        if (found) break;

    ...//如果找不到則傳回,說明本次到來的資料不需要help

    exp = ip_conntrack_expect_alloc();

    ... //初始化一個ip_conntrack_expect,可以用于描述一個将要建立的連接配接

    exp->expectfn = NULL;

    ip_conntrack_expect_related(exp, ct); //準備添加一個RELATED的連接配接,如果使用者在iptables規則中配置RELATED連接配接可以通過,那麼ftp的port模式資料連接配接就可以暢行無阻了。iptables的RELATED連接配接就是在這裡被“預料”到的,然後加入進已有的連接配接。

    ret = NF_ACCEPT;

 out:

    UNLOCK_BH(&ip_ftp_lock);

    return ret;

最終會在ip_conntrack_expect_insert函數中将“預料”到的連接配接加入與此“預料”的連接配接相關聯的已有連接配接的連結清單中,同時還将這個預料到的連接配接加入一個系統全局的連結清單中,并且如果已有的連接配接需要限制“預料”連接配接的建立連接配接時間,則需要啟動一個定時器,定時器逾時連接配接還不到的話,就會删除該預料的連接配接。這個related連接配接會被netfilter的state子產品使用,比如你使用--state NEW/ESTABLISHED/...的話,在state子產品中的match回調函數中,系統會取出該資料包屬于的連接配接,然後取出該連接配接的state,将之與參數的state比較,然後傳回進入target抉擇。

     以上是資料在ip_conntrack子產品中的流程,出了ip_conntrack就該進入ip_nat了,還是從其HOOK說起:

static unsigned int ip_nat_fn(...)

    ct = ip_conntrack_get(*pskb, &ctinfo); //得到連接配接,如果沒有得到則傳回NULL

    ...  //如果沒有得到既有連接配接則傳回ACCEPT(注意有ICMP重定向的特殊情況),由後續的鍊來抉擇,不管怎樣nat總在conntrack之後起作用,是以隻要有連接配接,conntrack就會将之加入hash

    switch (ctinfo) {

    case IP_CT_NEW:  //如果是一個連接配接的第一個包,那麼就要初始化一系列結構體,包括兩個方向的nat轉換表,ftp等等需要help的協定的相關結構體等等

        info = &ct->nat.info;

        WRITE_LOCK(&ip_nat_lock);

        if (!(info->initialized & (1 << maniptype))) {

            ...

            ret = ip_nat_rule_find(pskb, hooknum, in, out, ct, info);

    return do_bindings(ct, ctinfo, info, hooknum, pskb);

unsigned int do_bindings(...)

    int proto = (*pskb)->nh.iph->protocol;

    ...//實施位址/端口轉換,省略。就是在兩個方向的轉換表中根據方向和位址/端口資訊來修改資料包的協定頭

    helper = info->helper;  //info在ip_nat_setup_info也就是初始化連接配接的時候就會被建立,這裡隻是取出來

    if (helper) {

        ...//一個主連接配接可以有多個與之RELATED的副連接配接,是以下面就周遊這些副連接配接

        list_for_each_prev(cur_item, &ct->sibling_list) { 

            ...//如果已經是established的連接配接了,則說明下面将要做的工作已經作過了,就不再做了。

            if (exp_for_packet(exp, *pskb)) {  //包合理則調用help函數,在help函數中處理特殊的nat轉換,比如ftp的port模式相關的nat轉換

                ret = helper->help(ct, exp, info, ctinfo, hooknum, pskb);

對于ip_nat_ftp幫助子產品而言,其help函數的執行邏輯如下:

static unsigned int help(...)

    ct_ftp_info = &exp->help.exp_ftp_info;

        ftp_data_fixup(ct_ftp_info, ct, pskb, ctinfo, exp);

最終ftp_data_fixup調用了mangle[ct_ftp_info->ftptype](...)函數,顯然最後的函數完成了對資料包的修改,對資料包進行修改就是為了ftp伺服器可以成功連接配接到用戶端。由于用戶端很多時候在具有nat功能的防火牆後,并且都是用私網位址,而在ftp的port模式下,如果用戶端将一個私有位址建議給了ftp伺服器用于連接配接,伺服器是連接配接不到的,這個建議的ip位址在ftp的資料包中,是以必須修改資料包,将建議的位址和端口修改為nat後的位址和端口,同時再鋪設一條nat,用于伺服器連接配接用戶端時将請求真正轉到内網的用戶端。ip_nat_mangle_tcp_packet是ip_nat_helper.c中的一個很重要的函數,就是它完成了對應用層資料包的修改。

     ip_conntrack和ip_nat處理ftp總的過程就是,ip_conntrack子產品得到了連接配接的資訊,然後根據連接配接資訊可以得到一個helper,需要說明,一個連接配接完全可以沒有helper,而且大多數的都沒有helper,是否需要helper是根據連接配接的類型決定的,一般觸及應用層控制資料的修改時才會使用helper,比如ftp的控制指令是在應用層資料中被傳輸的,連接配接的類型是可以從資料包以及協定頭中得到的,是以ip_conntrack子產品需要資料包不能分段,也就是說需要完整的ip資料包。得到helper之後開始調用其help函數,然後判斷目前資料包是否需要help,比如判斷是否是ftp的特殊指令,該指令可以建立一條新的連接配接,如果是這樣的話,那麼help函數則“預測”到一條即将建立的連接配接并将之和目前連接配接關聯,然後ip_conntrack基本就沒有什麼做的了,資料包繼續在netfilter中流動,進入nat,同樣的,nat也如ip_conntrack判斷是否需要help,如果是則調用helper的help函數,判斷是否需要help的依據一般就是是否在ip_conntrack子產品中“預測”到了即将建立的連接配接,如果預測到了,那麼就調用nat的helper的help函數,并且将預測到的連接配接參數傳入,在ip_nat_ftp的help函數中根據預測連接配接的資訊對應用層控制資料進行修改。

     兩類helper的注冊都是在子產品初始化的時候進行的,而helper與連接配接或者nat的綁定則是在連接配接初始化的時候進行的。ip_nat_fn是nat的HOOK,其中對于IP_CT_NEW包來講需要調用call_expect,而後者最終調用下面的函數實作ftp相關的ip_nat_helper結構體的指定,該結構體在子產品初始化時被注冊:

unsigned int ip_nat_setup_info(...)

    info->helper = LIST_FIND(&helpers, helper_cmp, struct ip_nat_helper *, &reply); 

    //尋找ip_nat_ftp的helper,在ip_nat_ftp的init中,會調用ip_conntrack_helper_register将ftp相關的資訊注冊進核心,這些資訊包含在ip_nat_helper結構體中,其中有很多靜态資料是用于比對helper的,比如建立一個連接配接,當資料越過conntrack而進入nat時會調用ip_nat_setup_info,在該函數中,如上述調用LIST_FIND,其實就是使用目前的addr,port等資訊和注冊的helper逐個進行比較,一旦有命中的則将此helper取出留作後用,ip_nat_helper中最重要的就是help函數了。

ip_nat_ftp子產品的初始化函數如下:

static int __init init(void)

    for (i = 0; (i < MAX_PORTS) && ports[i]; i++) {

        ftp[i].tuple.src.u.tcp.port = htons(ports[i]);

        ftp[i].tuple.dst.protonum = IPPROTO_TCP;

        ftp[i].mask.src.u.tcp.port = 0xFFFF;

        ftp[i].mask.dst.protonum = 0xFFFF;

        ftp[i].max_expected = 1;

        ftp[i].timeout = 0;

        ftp[i].flags = IP_CT_HELPER_F_REUSE_EXPECT;

        ftp[i].me = ip_conntrack_ftp;

        ftp[i].help = help;

        ret = ip_conntrack_helper_register(&ftp[i]);

    return 0;

類似的ip_conntrack的helper也是在子產品初始化時注冊,在連接配接初始化時被指定特定的連接配接的,道理和nat是一樣的。

     總之,helper子產品一般是對需要在應用層資料中傳輸控制資料的協定進行幫助的,因為OS實作的協定棧并不包含應用層,但是有的時候必須對應用層控制資料進行修改,這時就不得不需要一個額外的幫助子產品了,注意,一般helper修改的都是控制資料,而不是業務資料,所謂控制資料就是和業務無關的,僅僅影響到連接配接本身的資料。

 本文轉自 dog250 51CTO部落格,原文連結:http://blog.51cto.com/dog250/1271821

繼續閱讀