iptables深入解析:nat篇

jopen 9年前發布 | 64K 次閱讀 iptables 路由器/防火墻

原文出處: linuxDOS 

關于nat,在實際應用中還是很廣泛的,snat/dnat/dmz/等等.下面我們就結合代碼深入分析下nat的運作.
參考:iptables.1.4.21 kernel 3.8.13
NAT英文全稱是“Network Address Translation”

顧名思義,它是一種把內部私有網絡地址(IP地址)翻譯成合法網絡IP地址的技術。因此我們可以認為,NAT在一定程度上,能夠有效的解決公網地址不足的問題

分類:
NAT有三種類型:靜態NAT(Static NAT)、動態地址NAT(Pooled NAT)、網絡地址端口轉換NAPT(Port-Level NAT)
其中,網絡地址端口轉換NAPT(Network Address Port Translation)則是把內部地址映射到外部網絡的一個IP地址的不同端口上。它可以將中小型的網絡隱藏在一個合法的IP地址后面。NAPT與 動態地址NAT不同,它將內部連接映射到外部網絡中的一個單獨的IP地址上,同時在該地址上加上一個由NAT設備選定的端口號.NAPT是使用最普遍的一種轉換方式
,它又細分為snat和dnat.

(1)源NAT(Source NAT,SNAT):修改數據包的源地址。源NAT改變第一個數據包的來源地址,它永遠會在數據包發送到網絡之前完成,數據包偽裝就是一具SNAT的例子。
(2)目的NAT(Destination NAT,DNAT):修改數據包的目的地址。Destination NAT剛好與SNAT相反,它是改變第一個數據懈的目的地地址,如平衡負載、端口轉發和透明代理就是屬于DNAT

應用:
NAT主要可以實現以下幾個功能:數據包偽裝、平衡負載、端口轉發和透明代理。

數據偽裝: 可以將內網數據包中的地址信息更改成統一的對外地址信息,不讓內網主機直接暴露在因特網上,保證內網主機的安全。同時,該功能也常用來實現共享上網。

端口轉發: 當內網主機對外提供服務時,由于使用的是內部私有IP地址,外網無法直接訪問。因此,需要在網關上進行端口轉發,將特定服務的數據包轉發給內網主機。

負載平衡: 目的地址轉換NAT可以重定向一些服務器的連接到其他隨機選定的服務器。

失效終結: 目的地址轉換NAT可以用來提供高可靠性的服務。如果一個系統有一臺通過路由器訪問的關鍵服務器,一旦路由器檢測到該服務器當機,它可以使用目的地址轉換NAT透明的把連接轉移到一個備份服務器上。

透明代理: NAT可以把連接到因特網的HTTP連接重定向到一個指定的HTTP代理服務器以緩存數據和過濾請求。一些因特網服務提供商就使用這種技術來減少帶寬的使用而不用讓他們的客戶配置他們的瀏覽器支持代理連接

原理

地址轉換
NAT的基本工作原理是,當私有網主機和公共網主機通信的IP包經過NAT網關時,將IP包中的源IP或目的IP在私有IP和NAT的公共IP之間進行轉換

要做SNAT的信息包被添加到POSTROUTING鏈中。要做DNAT的信息包被添加到PREROUTING鏈中。直接從本地出站的信息包的規則被添加到OUTPUT 鏈中。

DNAT:若包是被送往PREROUTING鏈的,并且匹配了規則,則執行DNAT或REDIRECT目標。為了使數據包得到正確路由,必須在路由之前進行DNAT。

路由:內核檢查信息包的頭信息,尤其是信息包的目的地。

處理本地進程產生的包:對nat表OUTPUT鏈中的規則實施規則檢查,對匹配的包執行目標動作。

SNAT:若包是被送往POSTROUTING鏈的,并且匹配了規則,則執行SNAT或MASQUERADE目標。系統在決定了數據包的路由之后才執行該鏈中的規則

但是nat也不是萬能的,它也是有缺陷的,解決辦法就是nat穿透技術:

其實NAT穿越技術依賴于UPnP協議的支持,也就是說NAT設備必須支持UPnP,支持NAT穿越技術;而網絡應用程序一樣也需要支持UPnP,支持NAT穿越技術,只不過,這通常都是通過調用相關的NAT Traversal API實現的,window XP默認已經安裝了NAT Traversal API,當然網絡應用程序要調用它仍然需要進行一些修改,現在的MSN Messenger就支持調用NAT Traversal API. 這里不再詳細說明,感興趣的可以查找資料.

下面看看實際代碼部分:
Nat的初始化工作和之前分析的filter幾乎一樣。Nat的ipv4部分在Iptables_nat.c 、Core部分在nf_nat_core.c,不同的就是表不一樣.
這里我們拿snat一個實際例子分析,應用環境如圖:

 iptables深入解析:nat篇

很明顯,直接lan內pc無法與外網通信,因為保留的ip地址即使外網能收到,但是回復的時候路由也會丟棄.所以需要snat:
#iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -j SNAT –to-source 202.20.65.5
或#iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -j MASQUERADE // 默認會獲取wan口地址進行映射.
鉤子點POSTROUTING 對應SNAT PREROUTING 對應DNAt(因為會影響以后的路由);還需要說明的是不論prerouting的dnat or postrouting snat都在基本ct的后邊,helper和confirm的前面.(DNAT的優先級高于SNAT)
參見:

enum nf_ip_hook_priorities {
    NF_IP_PRI_FIRST = INT_MIN,
    NF_IP_PRI_CONNTRACK_DEFRAG = -400,
    NF_IP_PRI_RAW = -300,
    NF_IP_PRI_SELINUX_FIRST = -225,
    NF_IP_PRI_CONNTRACK = -200,
    NF_IP_PRI_MANGLE = -150,
    NF_IP_PRI_NAT_DST = -100,
    NF_IP_PRI_FILTER = 0,
    NF_IP_PRI_SECURITY = 50,
    NF_IP_PRI_NAT_SRC = 100,
    NF_IP_PRI_SELINUX_LAST = 225,
    NF_IP_PRI_CONNTRACK_HELPER = 300,
    NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX,
    NF_IP_PRI_LAST = INT_MAX,
};

對于圖中lan–>wan(外網通信)我們梳理下報文的處理流程:
第一個報文:192.168.1.3—>202.20.65.4(協議端口先忽略)
–>nf_conntrack_in(查詢ct,沒有則建立ct,prerouting)—>先snat處理,然后ipv4_helper處理,最后ipv4_confirm處理.(postrouing)
回復的報文:202.20.65.4—>202.20.65.5
—>nf_conntrack_in,然后de-snat處理(查詢到之前建立的ct, prerouting)—->snat處理(postrouting)
下面代碼分析,先看snat的處理:
hook函數為nf_nat_ipv4_out它調用了核心處理函數nf_nat_ipv4_fn:

static unsigned int
nf_nat_ipv4_fn(unsigned int hooknum,
     struct sk_buff *skb,
     const struct net_device *in,
     const struct net_device *out,
     int (*okfn)(struct sk_buff *))
{
    struct nf_conn *ct;
    enum ip_conntrack_info ctinfo;
    struct nf_conn_nat *nat;
    /* maniptype == SRC for postrouting. */
    enum nf_nat_manip_type maniptype = HOOK2MANIP(hooknum);

    /* We never see fragments: conntrack defrags on pre-routing
     * and local-out, and nf_nat_out protects post-routing.
     */
    NF_CT_ASSERT(!ip_is_fragment(ip_hdr(skb)));

    ct = nf_ct_get(skb, &ctinfo);
    /* Can't track? It's not due to stress, or conntrack would
     * have dropped it. Hence it's the user's responsibilty to
     * packet filter it out, or implement conntrack/NAT for that
     * protocol. 8) --RR
     */
    if (!ct)
        return NF_ACCEPT;

    /* Don't try to NAT if this packet is not conntracked */
    if (nf_ct_is_untracked(ct))
        return NF_ACCEPT;

    nat = nfct_nat(ct);
    if (!nat) {
        /* NAT module was loaded late. */
        if (nf_ct_is_confirmed(ct))
            return NF_ACCEPT;
        nat = nf_ct_ext_add(ct, NF_CT_EXT_NAT, GFP_ATOMIC);
        if (nat == NULL) {
            pr_debug("failed to add NAT extension\n");
            return NF_ACCEPT;
        }
    }

    switch (ctinfo) {
    case IP_CT_RELATED:
    case IP_CT_RELATED_REPLY:
        if (ip_hdr(skb)->protocol == IPPROTO_ICMP) {
            if (!nf_nat_icmp_reply_translation(skb, ct, ctinfo,
                             hooknum))
                return NF_DROP;
            else
                return NF_ACCEPT;
        }
        /* Fall thru... (Only ICMPs can be IP_CT_IS_REPLY) */
    case IP_CT_NEW:
        /* Seen it before? This can happen for loopback, retrans,
         * or local packets.
         */
        if (!nf_nat_initialized(ct, maniptype)) {
            unsigned int ret;

            ret = nf_nat_rule_find(skb, hooknum, in, out, ct);
            if (ret != NF_ACCEPT)
                return ret;
        } else {
            pr_debug("Already setup manip %s for ct %p\n",
                 maniptype == NF_NAT_MANIP_SRC ? "SRC" : "DST",
                 ct);
            if (nf_nat_oif_changed(hooknum, ctinfo, nat, out))
                goto oif_changed;
        }
        break;

    default:
        /* ESTABLISHED */
        NF_CT_ASSERT(ctinfo == IP_CT_ESTABLISHED ||
             ctinfo == IP_CT_ESTABLISHED_REPLY);
        if (nf_nat_oif_changed(hooknum, ctinfo, nat, out))
            goto oif_changed;
    }

    return nf_nat_packet(ct, ctinfo, hooknum, skb);

oif_changed:
    nf_ct_kill_acct(ct, ctinfo, skb);
    return NF_DROP;
}

它首先根據hooknum獲取nat類型:enum nf_nat_manip_type maniptype // postrouting 為snat
獲取ct信息.,并創建nat ext信息,由于之前已經建立了ct,所以這里狀態為IP_CT_NEW. ct->status未設置.
接著調用nf_nat_rule_find–>ipt_do_tables處理snat rules. 對于rules的處理機制流程我們已經很熟悉了.和filter不一樣的就是target的處理,
簡單看看iptables對命令的解析的ipt_ip信息:
Ipt_do_table
先是查找五元組的匹配ip_packet_match ipt_entry->ip(struct ipt_ip)
那么上面的命令規則下發的ip信息是什么呢
In_dev :不限
Out_dev:不限
Src_ip:192.168.1.0/24
Dst_ip:不限
Protonum:不限
Sport:不限
Dport:不限
五元組匹配后,找到nat的target,解析 –to-source 202.20.65.5.

static struct xtables_target snat_tg_reg = {
    .name        = "SNAT",
    .version    = XTABLES_VERSION,
    .family        = NFPROTO_IPV4,
    .size        = XT_ALIGN(sizeof(struct nf_nat_ipv4_multi_range_compat)),
    .userspacesize    = XT_ALIGN(sizeof(struct nf_nat_ipv4_multi_range_compat)),
    .help        = SNAT_help,
    .x6_parse    = SNAT_parse,
    .x6_fcheck    = SNAT_fcheck,
    .print        = SNAT_print,
    .save        = SNAT_save,
    .x6_options    = SNAT_opts,
};

先把ip地址信息放在struct nf_nat_ipv4_range range中,然后和struct ipt_natinfo *info = (void *)(*cb->target)關聯其實就是填充.

/* Dest NAT data consists of a multi-range, indicating where to map
   to. */
struct ipt_natinfo
{
    struct xt_entry_target t;
    struct nf_nat_ipv4_multi_range_compat mr;
};

我們看看內核的nat target是如何處理的:由于是單ip參數,所以是v1

{
        .name        = "SNAT",
        .revision    = 1,
        .target        = xt_snat_target_v1,
        .targetsize    = sizeof(struct nf_nat_range),
        .table        = "nat",
        .hooks        = (1 << NF_INET_POST_ROUTING) |
                 (1 << NF_INET_LOCAL_IN),
        .me        = THIS_MODULE,
    },
static unsigned int
xt_snat_target_v1(struct sk_buff *skb, const struct xt_action_param *par)
{
    const struct nf_nat_range *range = par->targinfo;
    enum ip_conntrack_info ctinfo;
    struct nf_conn *ct;

    ct = nf_ct_get(skb, &ctinfo);
    NF_CT_ASSERT(ct != NULL &&
         (ctinfo == IP_CT_NEW || ctinfo == IP_CT_RELATED ||
         ctinfo == IP_CT_RELATED_REPLY));

    return nf_nat_setup_info(ct, range, NF_NAT_MANIP_SRC);
}

其實nat的處理就是nf_nat_setup_info包括dnat也是.
先是獲取curr_tuple,調用get_unique_tuple根據curr_tuple和range信息建立新的tuple即new_tuple

get_unique_tuple(&new_tuple, &curr_tuple, range, ct, maniptype);

 

/* Manipulate the tuple into the range given. For NF_INET_POST_ROUTING,
 * we change the source to map into the range. For NF_INET_PRE_ROUTING
 * and NF_INET_LOCAL_OUT, we change the destination to map into the
 * range. It might not be possible to get a unique tuple, but we try.
 * At worst (or if we race), we will end up with a final duplicate in
 * __ip_conntrack_confirm and drop the packet. */
static void
get_unique_tuple(struct nf_conntrack_tuple *tuple,
         const struct nf_conntrack_tuple *orig_tuple,
         const struct nf_nat_range *range,
         struct nf_conn *ct,
         enum nf_nat_manip_type maniptype)
{
    const struct nf_nat_l3proto *l3proto;
    const struct nf_nat_l4proto *l4proto;
    struct net *net = nf_ct_net(ct);
    u16 zone = nf_ct_zone(ct);

    rcu_read_lock();
    l3proto = __nf_nat_l3proto_find(orig_tuple->src.l3num);
    l4proto = __nf_nat_l4proto_find(orig_tuple->src.l3num,
                    orig_tuple->dst.protonum);

    /* 1) If this srcip/proto/src-proto-part is currently mapped,
     * and that same mapping gives a unique tuple within the given
     * range, use that.
     *
     * This is only required for source (ie. NAT/masq) mappings.
     * So far, we don't do local source mappings, so multiple
     * manips not an issue.
     */
    if (maniptype == NF_NAT_MANIP_SRC &&
     !(range->flags & NF_NAT_RANGE_PROTO_RANDOM)) {
        /* try the original tuple first */
        if (in_range(l3proto, l4proto, orig_tuple, range)) {
            if (!nf_nat_used_tuple(orig_tuple, ct)) {
                *tuple = *orig_tuple;
                goto out;
            }
        } else if (find_appropriate_src(net, zone, l3proto, l4proto,
                        orig_tuple, tuple, range)) {
            pr_debug("get_unique_tuple: Found current src map\n");
            if (!nf_nat_used_tuple(tuple, ct))
                goto out;
        }
    }

    /* 2) Select the least-used IP/proto combination in the given range */
    *tuple = *orig_tuple;
    find_best_ips_proto(zone, tuple, range, ct, maniptype);

    /* 3) The per-protocol part of the manip is made to map into
     * the range to make a unique tuple.
     */

    /* Only bother mapping if it's not already in range and unique */
    if (!(range->flags & NF_NAT_RANGE_PROTO_RANDOM)) {
        if (range->flags & NF_NAT_RANGE_PROTO_SPECIFIED) {
            if (l4proto->in_range(tuple, maniptype,
                     &range->min_proto,
                     &range->max_proto) &&
             (range->min_proto.all == range->max_proto.all ||
             !nf_nat_used_tuple(tuple, ct)))
                goto out;
        } else if (!nf_nat_used_tuple(tuple, ct)) {
            goto out;
        }
    }

    /* Last change: get protocol to try to obtain unique tuple. */
    l4proto->unique_tuple(l3proto, tuple, range, maniptype, ct);
out:
    rcu_read_unlock();
}

在這個函數里我們發現了類似ct的l3/l4協議注冊處理時的結構體,

const struct nf_nat_l3proto *l3proto;
    const struct nf_nat_l4proto *l4proto;

關于它們的注冊:

static int __init nf_nat_l3proto_ipv4_init(void)
{
    int err;

    err = nf_nat_l4proto_register(NFPROTO_IPV4, &nf_nat_l4proto_icmp);
    if (err < 0)
        goto err1;
    err = nf_nat_l3proto_register(&nf_nat_l3proto_ipv4);
    if (err < 0)
        goto err2;
    return err;

err2:
    nf_nat_l4proto_unregister(NFPROTO_IPV4, &nf_nat_l4proto_icmp);
err1:
    return err;
}
static const struct nf_nat_l3proto nf_nat_l3proto_ipv4 = {
    .l3proto        = NFPROTO_IPV4,
    .in_range        = nf_nat_ipv4_in_range,
    .secure_port        = nf_nat_ipv4_secure_port,
    .manip_pkt        = nf_nat_ipv4_manip_pkt,
    .csum_update        = nf_nat_ipv4_csum_update,
    .csum_recalc        = nf_nat_ipv4_csum_recalc,
    .nlattr_to_range    = nf_nat_ipv4_nlattr_to_range,
#ifdef CONFIG_XFRM
    .decode_session        = nf_nat_ipv4_decode_session,
#endif
};

l4例如udp的:

const struct nf_nat_l4proto nf_nat_l4proto_udp = {
    .l4proto        = IPPROTO_UDP,
    .manip_pkt        = udp_manip_pkt,
    .in_range        = nf_nat_l4proto_in_range,
    .unique_tuple        = udp_unique_tuple,
#if defined(CONFIG_NF_CT_NETLINK) || defined(CONFIG_NF_CT_NETLINK_MODULE)
    .nlattr_to_range    = nf_nat_l4proto_nlattr_to_range,
#endif
};

繼續后面的處理,判斷mainiptype 和range->flags. miainptype很明顯是src,而range->flags的值來自哪里呢?
首先我們需要明白的是range來自用戶空間的傳遞.

void xtables_option_tfcall(struct xtables_target *t)
{
    if (t->x6_fcheck != NULL) {
        struct xt_fcheck_call cb;

        cb.ext_name = t->name;
        cb.data = t->t->data;
        cb.xflags = t->tflags;
        cb.udata = t->udata;
        t->x6_fcheck(&cb);
    } else if (t->final_check != NULL) {
        t->final_check(t->tflags);
    }
    if (t->x6_options != NULL)
        xtables_options_fcheck(t->name, t->tflags, t->x6_options);
}

而x6_fcheck:

static void SNAT_fcheck(struct xt_fcheck_call *cb)
{
    static const unsigned int f = F_TO_SRC | F_RANDOM;
    struct nf_nat_ipv4_multi_range_compat *mr = cb->data;

    if ((cb->xflags & f) == f)
        mr->range[0].flags |= NF_NAT_RANGE_PROTO_RANDOM;
}

沒有指定端口信息則為range.flags |= NF_NAT_RANGE_MAP_IPS;指定端口信息則:range.flags |= NF_NAT_RANGE_PROTO_SPECIFIED;
同理dnat。在iptables命令解析的時候是先處理target然后才去check的.而range的值:
Parse_to里先解析端口信息,如果有的話會賦值給:
只說下單端口的情況:

range.min.tcp.port
                = range.max.tcp.port
                = htons(port);

ip字符串 ip->s_addr
range.min_ip = ip->s_addr;
range.max_ip = range.min_ip;
然后填充到entry_target的data里
繼續回到get_unique_tuple,接著in_range返回值為0(因為ip地址唯一),我們看看find_appropriate_src做了什么
很明顯一開始net->ct.nat_bysource為null,所以這個函數也返回0.
關鍵在find_best_ips_proto
它根據源tuple信息和range參數,生產新的tuple(映射后的),然后nf_nat_used_tuple查詢是否已有回應報文在hash鏈表上. 最后用l4proto->unique_tuple保證tuple 的唯一性. 總之get_unique_tuple函數主要工作就是生產新的映射后的tuple。
由于ip信息已經改變,所以new_tuple和curr_tuple肯定不一樣. 之后新映射的tuple再逆轉為repl_tuple(映射后)
調用nf_conntrack_alter_reply改變ct信息:

/* Alter reply tuple (maybe alter helper). This is for NAT, and is
   implicitly racy: see __nf_conntrack_confirm */
void nf_conntrack_alter_reply(struct nf_conn *ct,
             const struct nf_conntrack_tuple *newreply)
{
    struct nf_conn_help *help = nfct_help(ct);

    /* Should be unconfirmed, so not in hash table yet */
    NF_CT_ASSERT(!nf_ct_is_confirmed(ct));

    pr_debug("Altering reply tuple of %p to ", ct);
    nf_ct_dump_tuple(newreply);

    ct->tuplehash[IP_CT_DIR_REPLY].tuple = *newreply;
    if (ct->master || (help && !hlist_empty(&help->expectations)))
        return;

    rcu_read_lock();
    __nf_ct_try_assign_helper(ct, NULL, GFP_ATOMIC);
    rcu_read_unlock();
}

即它改變了 ct->tuplehash[IP_CT_DIR_REPLY].tuple,其他不變.
最后處理nat ext,把nat信息和ct關聯起來通過hlist:net->ct.nat_bysource
然后設置ct->status:

ct->status |= IPS_SRC_NAT_DONE

既然nat規則處理完畢,剩下的工作就是處理skb里ip報文信息了。
根據ct里的信息通過nf_nat_packet修改skb指向的ip頭.
后續的鉤子函數,如果有helper處理helper;然后就是ipv4_confirmed

/* reuse the hash saved before */
    hash = *(unsigned long *)&ct->tuplehash[IP_CT_DIR_REPLY].hnnode.pprev;
    hash = hash_bucket(hash, net);
    repl_hash = hash_conntrack(net, zone,
                 &ct->tuplehash[IP_CT_DIR_REPLY].tuple);

首先看第一個hash值,它在__nf_conntrack_alloc的時候被賦值,
Hash計算來自源五元組通過hash_conntrack_raw計算得來.那么snat的時候,基本ct處理后就是nat了,把ct里改變的是ct->tuplehash[IP_CT_DIR_REPLY].tuple信息,其他不變
這樣的話 ipv4_confirm的時候,源hash不變,repl_hash重新計算(因為tuple的ip已經重新映射),然后加入inert ct的全局鏈表net->ct.hash[hash]
既然第一報文已經順利發送出去,那么響應報文又是如何發給lan側的呢?
首先202.20.65.5收到報文到prerouting鉤子,nf_conntrack_in它會查詢到ct信息
設置ctinfo 為IP_CT_ESTABLISHED_REPLY
然后看看ct timeout是否過期,然后調用l4proto->packet更新狀態
Ct->status : confirmed + ips_src_nat
我們知道prerouting只能進行dnat。然后會進入dnat的鉤子函數nf_nat_ipv4_in
根據ctinfo的信息,直接進入nf_nat_packet處理.
它里面有個關鍵部分:

/* Invert if this is reply dir. */
    if (dir == IP_CT_DIR_REPLY)
        statusbit ^= IPS_NAT_MASK;

然后進行dnat處理根據ct信息修改skb指向的ip頭信息.即所謂的de-snat.同理dnat
簡單看下五元組信息的變化:
我們可以看看數據伍元整的流程:
Lan-wan:
Ct(snat)即ct->tuplehash信息
orig:192.168.1.x—-202.20.65.4
reply:202.20.65.4—-202.20.65.5
回復的報文:
查詢到ct(根據202.20.65.4—-202.20.65.5)
Prerouting上nat處理. 由于這個時候只能處理dnat,
找到ct里源五元組即192.168.1.x—-202.20.65.4反轉為202.20.65.4—192.168.1.x
Skb根據這個信息進行dnat映射。即完成了正常的通信.

當然nat還有其他很多復雜的應用,這里僅僅分析一個實例應用的流程,作為深入理解nat的開始.

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!