1. 程式人生 > >編寫iptables模塊實現不連續IP地址的DNAT-POOL

編寫iptables模塊實現不連續IP地址的DNAT-POOL

most really inet_addr ice literal cif 執行 comment any

1.背景

《路由應用-使用路由實現負載流量均衡》的第3.3節,並沒有給出如何配置一個pool,那是因為在Linux 2.6.10之上,已經不再支持配置不連續IP地址的pool了,如果看iptables的man手冊,將會得到以下信息:

In Kernels up to 2.6.10 you can add several --to-destination options. For those kernels, if you specify more than one destination address, either via an address range or multiple --to-destination options, a simple round-robin (one after another in cycle) load balancing takes place between these addresses. Later Kernels (>= 2.6.11-rc1) don‘t have the ability to NAT to multiple ranges anymore.

在相對老的內核中,使用iptables時,可以配置多個to-destination選項來支持不連續IP的pool配置,可是到了2.6.11以及之後,內核不再支持不連續IP地址的pool配置了。具體的原因還得從Changelog中尋找,在2.6.11-rc1的Changelog中有如下描述:
[PATCH] Remove NAT to multiple ranges
The NAT code has the concept of multiple ranges: you can say "map this connection onto IP 192.168.1.2 - 192.168.1.4, 192.168.1.7 ports 1024-65535, and 192.168.1.10". I implemented this because we could.
But it‘s not actually *used* by many (any?) people, and you can approximate this by a random match (from patch-o-matic) if you really
want to. It adds complexity to the code.

可見,沒有人用的東西是不必再繼續存在下去的。作者建議使用Netfilter的非正式補丁patch-o-matic的形式進行支持,可是事情遠沒有那麽麻煩,如果有上網尋找現成方案的時間,自己寫一個pool的實現也是很快的。

2.實現一個新功能的步驟

Linux的防火墻和NAT完全是由Netfilter機制實現的,然而具體的策略卻需要一個用戶態程序配置進內核的Netfilter,一個很流行可能也是唯一在使用的用戶態工具就是iptables。iptables負責接收並檢查用戶的配置,然後將其傳到內核,內核接收後會將策略配置在內核的內存中,至此iptables的配置任務完成。因此要實現一個新的功能,必然首先需要編寫一個內核模塊用於支持機制,然後還需要一個iptables模塊呼應之,用於配置策略。
總的來說,Netfilter的工作方式為:在特定的HOOK點執行多條rule,每一條rule對於每一個數據包執行多個match,多個match都匹配後,執行該rule的target。每一條rule都要顯式綁定於一個table和一個HOOK點。
對於實現一個IP地址pool,用於DNAT的時候從中選擇目的IP地址,很明顯是一個target要做的。因此,我們要做的就是實現一個新的target。對於實現一個新的match,本文不討論,它的實現過程和實現一個target幾乎一模一樣。

2.1.編寫一個內核模塊

2.1.1.定義並註冊一個xt_target結構體

這是必須的要求,內核為每一個協議族(family)保留了一個target鏈表,新定義的xt_target根據其family註冊在特定的鏈表上。

2.1.2.實現xt_target的target函數

該回調函數實現了動作邏輯,也就是說,匹配完成後要執行一個target,具體如何執行就要看該回調函數如何定義。內核並沒有對此函數的實現有任何限制,理論上可以在其中實現一切。

2.1.3.定義xt_target的targetsize字段

為了提高target定位的效率,高一些版本的內核要求xt_target強制長度,該長度表示“用戶策略”數據結構的總長度。

2.1.4.定義xt_target的table,hooks,family字段

Netfilter要求每一個match以及每一個target都顯式綁定在一個HOOK上,如果需要用戶態進程比如iptables呼應,還需要顯式綁定於一個table。

2.2.編寫一個iptables模塊

iptables是一個用戶態防火墻應用程序,其全部實現由extension來完成,這個機制類似於插件機制,你可以很方便的擴展iptables的功能。
iptables是高度模塊化的,它的幾乎所有的功能都在模塊中被實現,比如一個很簡單的配置項:-p tcp,-p說明它是一個match,而tcp則是該match的一個模塊,對於-p這個match,其它可選的模塊還有udp,icmp等等,在iptables結構中,模塊和模塊名稱是高度相關的,iptables的所有模塊都安裝於/usr/local/lib/xtables/(具體機器的iptables模塊安裝位置可能與此不同),比如對於-p tcp來講,需要的模塊就是/usr/local/lib/xtables/libxt_tcp.so,同名的源代碼位於iptables-$version/extensions目錄的libxt_tcp.c。
如果想實現一個新的iptables模塊,無疑需要比葫蘆畫瓢地編寫一個類似的文件

2.2.1.定義並註冊一個xtables_target結構體

這件事情和內核態的註冊xt_target是對應的,然而這個用戶態的xtables_target只是為了通知內核配置策略,而不像內核的xt_target那樣永存。一旦iptables命令返回,這個用戶態的target結構體即被釋放。

2.2.2.實現xtables_target的x6_parse回調函數

這個回調函數很核心,它將iptables的配置參數轉化為要傳給內核的策略配置。

2.2.3.定義x6_options數組

該數組的每一項表示一個配置選項,比如--to-destination中的to-destination就是x6_options中的一個成員字段。

2.2.4.實現save回調函數用於iptables-save時的顯示

如果不希望被備份,則不需要實現此函數。

2.2.5.用戶態size和內核態targetsize的對應

內核在接收用戶態iptables等程序配置的target的時候,要進行size匹配驗證,如果size不一致將會拒絕這個配置,因此iptables的xtables_target結構體的size一定要和內核態xt_target結構體的targetsize一致。這個驗證在2.6.18以後的內核中是強制的,之所以如此是因為在內核中netfilter的match以及target是在一塊連續內核排列的,正是有這些size才能斷定哪裏是match以及target的邊界。

3.POOL的實現

理解了原理之後,我們發現實現POOL是多麽簡單的一件事。

3.1.實現POOL target的內核態模塊

以下是對應的內核模塊ipt_POOL.ko的源代碼ipt_POOL.c
/* POOL.  將目的地址隨機映射到不連續的IP地址池 */
/* (C) 2011/06/28 By ZhaoYa
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */

#include <linux/types.h>
#include <linux/module.h>
#include <linux/netfilter.h>
#include <net/netfilter/nf_nat_rule.h>
#include <linux/netfilter_ipv4.h>
#include <linux/jiffies.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ZhaoYa <[email protected]>");
MODULE_DESCRIPTION("Xtables: DNAT from ip-pool");

#define MAX 100
struct ip_addr_pool {
        unsigned int size;
        __be32 ips[MAX];
};

static bool pool_check(const struct xt_tgchk_param *par)
{
        //TODO
        return true;
}

static unsigned int
pool_target(struct sk_buff *skb, const struct xt_target_param *par)
{
        struct nf_conn *ct;
        enum ip_conntrack_info ctinfo;
        const struct ip_addr_pool *mr = par->targinfo;
        //以時鐘嘀嗒作為隨機源,理由是你不知道何時數據包會過來
        unsigned int indx = jiffies%(mr->size);
        struct nf_nat_range newrange;
        newrange.min_ip = mr->ips[indx];
        newrange.max_ip = mr->ips[indx];
        newrange.flags = IP_NAT_RANGE_MAP_IPS;
        NF_CT_ASSERT(par->hooknum == NF_INET_PRE_ROUTING ||
                     par->hooknum == NF_INET_LOCAL_OUT);
        ct = nf_ct_get(skb, &ctinfo);
        NF_CT_ASSERT(ct && (ctinfo == IP_CT_NEW || ctinfo == IP_CT_RELATED));
        return nf_nat_setup_info(ct, &newrange, IP_NAT_MANIP_DST);
}

static struct xt_target pool_reg __read_mostly = {
        .name           = "POOL",
        .family         = NFPROTO_IPV4,
        .target         = pool_target,
        .targetsize     = sizeof(struct ip_addr_pool),
        .table          = "nat",
        .hooks          = (1 << NF_INET_PRE_ROUTING) | (1 << NF_INET_LOCAL_OUT),
        .checkentry     = pool_check,
        .me             = THIS_MODULE,
};

static int __init pool_target_init(void)
{
        return xt_register_target(&pool_reg);
}

static void __exit pool_target_exit(void)
{
        xt_unregister_target(&pool_reg);
}

module_init(pool_target_init);
module_exit(pool_target_exit);


3.2.實現POOL target的用戶態iptables模塊

以下是iptables模塊libipt_POOL的源代碼libipt_POOL.c
/* POOL.  將目的地址隨機映射到不連續的IP地址池 -用戶態iptables模塊*/
/* (C) 2011/06/28 By ZhaoYa
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */
#include <stdio.h>
#include <netdb.h>
#include <string.h>
#include <stdlib.h>
#include <xtables.h>
#include <linux/netfilter_ipv4/ip_tables.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

enum {
        //目前只支持一個配置
        FROM_POOL = 0,
};

//最多一個池中有100個地址
#define MAX 100
struct ip_addr_pool {
        unsigned int size;
        u_int32_t ips[MAX];
};
struct ipt_natinfo
{
        struct xt_entry_target t;
        struct ip_addr_pool mr;
};

static void POOL_help(void)
{
        printf(
                "POOL target options:\n"
                " --pool [<ipaddr>[,<ipaddr[,<...>]>]]\n");
}

static const struct xt_option_entry POOL_opts[] = {
        {
                .name = "pool",
                .id = FROM_POOL,
                .type = XTTYPE_STRING,
                .flags = XTOPT_MAND | XTOPT_MULTI
        },
        XTOPT_TABLEEND,
};

static struct ipt_natinfo *
set_contents(struct ipt_natinfo *info, const char *arg)
{
        unsigned int size;
        char *tok;
        unsigned int i = 0;
        size = XT_ALIGN(sizeof(struct ipt_natinfo));
        info = realloc(info, size);
        if (!info)
                xtables_error(OTHER_PROBLEM, "Out of memory\n");

        tok = strtok(arg, ",");
        if (tok){
                while (tok && i < MAX) {
                        info->mr.ips[i] = (u_int32_t)inet_addr(tok);
                        info->mr.size++;
                        tok = strtok(NULL, ",");
                        i ++;
                }
        } else {
                info->mr.ips[i] = (u_int32_t)inet_addr(arg);
                info->mr.size++;
        }
        return info;
}

static void POOL_parse(struct xt_option_call *cb)
{
        const struct ipt_entry *entry = cb->xt_entry;
        struct ipt_natinfo *info = (void *)(*cb->target);
        int portok;

        if (entry->ip.proto == IPPROTO_TCP
            || entry->ip.proto == IPPROTO_UDP
            || entry->ip.proto == IPPROTO_SCTP
            || entry->ip.proto == IPPROTO_DCCP
            || entry->ip.proto == IPPROTO_ICMP)
                portok = 1;
        else
                portok = 0;

        xtables_option_parse(cb);
        switch (cb->entry->id) {
        case FROM_POOL:
        {
                char *arg ;
                arg = strdup(cb->arg);
                if (arg == NULL)
                        xtables_error(RESOURCE_PROBLEM, "strdup");
                info = set_contents(info, arg);
                free(arg);
                *cb->target = &(info->t);
                break;
        }
        }
}

static void POOL_save(const void *ip, const struct xt_entry_target *target)
{
        const struct ipt_natinfo *info = (const void *)target;
        unsigned int i = 0;

        printf(" --pool ");
        for (i = 0; i < info->mr.size; i++) {
                struct in_addr ia;
                char *addr;
                ia.s_addr = info->mr.ips[i];
                addr = inet_ntoa(ia);
                if (i == info->mr.size-1)
                        printf("%s", addr);
                else
                        printf("%s,", addr);
        }
}

static struct xtables_target pool_tg_reg = {
        .name           = "POOL",
        .version        = XTABLES_VERSION,
        .family         = NFPROTO_IPV4,
        .size           = XT_ALIGN(sizeof(struct ip_addr_pool)),
        .userspacesize  = XT_ALIGN(sizeof(struct ip_addr_pool)),
        .help           = POOL_help,
        .x6_parse       = POOL_parse,
        .save           = POOL_save,
        .x6_options     = POOL_opts,
};

void _init(void)
{
        xtables_register_target(&pool_tg_reg);
}


3.3.編寫一個Makefile

為了方便安裝,編寫一個Makefile文件
CC=gcc

IPTABLES_SRC=/root/iptables/iptables-1.4.12
INCLUDE=-I$(IPTABLES_SRC)/include
KERNEL_SRC=/lib/modules/`uname -r`/build
MOD=ipt_POOL.ko

all: modules libipt_POOL.so

modules: $(MOD)

ipt_POOL.ko:  ipt_POOL.c
        $(MAKE) -C $(KERNEL_SRC) SUBDIRS=$(PWD) modules

libipt_POOL.so: libipt_POOL.c
        $(CC)  $(INCLUDE) -fPIC -c libipt_POOL.c
        ld -shared -o libipt_POOL.so libipt_POOL.o

clean:
        -rm -f *.o *.so *.ko .*.cmd *.mod.c *.symvers *.order

install: all
        cp -rf libipt_POOL.so /usr/local/lib/xtables/
        cp -rf $(MOD) /lib/modules/`uname -r`/kernel/net/ipv4/netfilter/
    @depmod -a


3.4.試一下

# iptables -t nat -A OUTPUT -p icmp -j POOL --pool 192.168.188.226,192.168.188.222,192.168.188.191,192.168.188.82
# iptables-save
*nat
:PREROUTING ACCEPT [15991:1906518]
:POSTROUTING ACCEPT [1548:237918]
:OUTPUT ACCEPT [1541:237330]
-A OUTPUT -p icmp -j POOL --pool 192.168.188.226,192.168.188.222,192.168.188.191,192.168.188.82
COMMIT

4.進一步的工作

以上僅僅使用了一個一體化的基於iptables的POOL方案,這種方案配置起來很不靈活,如果想往pool中添加一個地址,就不得不先刪除對應的rule,然後再將rule加上新添加的IP後重新配置入內核,如果你想往已經有了80個IP地址的pool中添加一個IP地址,...扁粉...因此必然需要將pool的配置和pool的target設置分離開來,類似ipset所作的那樣,實現一個poolset。

4.1.能和ipset聯動嗎?

答案無疑是肯定的,然而需要修改ipset的內核態源碼,但是有這個必要嗎?ipset的優勢在查詢方面,而poolset的需求只是在一個特定的pool中的諸多IP地址中取出一個。因此它要簡單得多,無非實現兩點:第一,根據一個鍵值在poolset中定位一個特定的pool;第二,在該pool中定位一個特定的IP地址用於DNAT。

4.2.學ipset的樣子做個工具

4.2.1.實現內核模塊

內核中首先要實現一個poolset鏈表,內容為一系列的pool,每一個poolset表項都有一個字符串類型的name字段,可以根據該name定位特定的pool。因此用戶態iptables程序和內核態對應結構可以定義為如下很簡單的結構體:
struct pool-info {
    unsigned char pool_name[MAXNAMELEN];
}


該結構體用於iptables和內核態的target的互動。

4.2.1.1.註冊一個xt_target。

該xt_target的target回調函數很簡單,就是在內核的poolset表中定位配置的pool,然後在該pool中找到一個IP地址用於DNAT

4.2.1.2.實現管理內核態的poolset表的機制。

該機制主要接收用戶態程序的配置,然後將地址加入到特定的POOL中,主要實現增加,刪除,修改等操作。為了簡便,本文使用procfs來管理poolset,標準的做法是開啟一個netlink接收線程。

4.2.1.3.實現思路

ipt_POOLSET.c
#include <linux/types.h>
#include <linux/module.h>
#include <linux/netfilter.h>
#include <net/netfilter/nf_nat_rule.h>
#include <linux/netfilter_ipv4.h>
#include <linux/proc_fs.h>
#include <linux/fs.h>
#include <linux/jiffies.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ZhaoYa <[email protected]>");
MODULE_DESCRIPTION("Xtables: DNAT from ip-pool");

#define MAX 100
#define MAXNAMELEN 100
#define MAXPOOL 5
struct ip_addr_pool {
        unsigned char pool_name[MAXNAMELEN]; //該字段用於target中的具體POOL的查找
        unsigned int size;
        __be32 ips[MAX];
};

struct pool_info {
        unsigned char pool_name[MAXNAMELEN];
};

//定義一個只有MAXPOOL個POOL的靜態數組替代復雜的鏈表操作,標準的做法是用鏈表實現
struct ip_addr_pool gpoolset[MAXPOOL];

static bool poolset_check(const struct xt_tgchk_param *par)
{
        //TODO
        return true;
}

//poolset_target很簡單,就是根據iptables配置的pool名稱,定位一個要使用的pool,也就是一個ip_addr_pool結構體。
static unsigned int
poolset_target(struct sk_buff *skb, const struct xt_target_param *par)
{
        struct nf_conn *ct;
        enum ip_conntrack_info ctinfo;
        struct nf_nat_range newrange;
        const struct pool_info *mr = par->targinfo;
        struct ip_addr_pool res = {};
        unsigned int i = 0;
        unsigned int indx;
        //以下的循環找到根據name索引的ip_addr_pool,也就是一個POOL
        for (i = 0; i < MAXPOOL; i++) {
                if (!strcmp(mr->pool_name, gpoolset[i].pool_name))
                        res = gpoolset[i];
        }
        //找到POOL後,在此POOL中隨機取出一個IP地址用於DNAT
        indx = jiffies%(res.size);
        newrange.min_ip = res.ips[indx];
        newrange.max_ip = res.ips[indx];
        newrange.flags = IP_NAT_RANGE_MAP_IPS;
        NF_CT_ASSERT(par->hooknum == NF_INET_PRE_ROUTING ||
                     par->hooknum == NF_INET_LOCAL_OUT);
        ct = nf_ct_get(skb, &ctinfo);
        NF_CT_ASSERT(ct && (ctinfo == IP_CT_NEW || ctinfo == IP_CT_RELATED));
        return nf_nat_setup_info(ct, &newrange, IP_NAT_MANIP_DST);
}

static struct xt_target poolset_reg __read_mostly = {
        .name           = "POOLSET",
        .family         = NFPROTO_IPV4,
        .target         = poolset_target,
        .targetsize     = sizeof(struct pool_info),
        .table          = "nat",
        .hooks          = (1 << NF_INET_PRE_ROUTING) | (1 << NF_INET_LOCAL_OUT),
        .checkentry     = poolset_check,
        .me             = THIS_MODULE,
};

struct proc_dir_entry *poolset_entry;

static ssize_t write_poolset(struct file *file, const char __user *buf,
                                                   size_t count, loff_t *ppos)
{
        //TODO
        //在這裏實現將buf轉換為特定pool中的IP地址
        return 0;
}

static struct file_operations proc_poolset_operations = {
                .write          = write_poolset,
};

static int __init poolset_target_init(void)
{
        unsigned int i = 0;
        memset(gpoolset, 0, sizeof(gpoolset));
        poolset_entry = proc_mkdir("poolset", NULL);
        for (i = 0; i < MAXPOOL; i++) {
                struct proc_dir_entry *entry;
                char buf[100] = {0};
                sprintf(buf, "%d", i);
                strcpy(gpoolset[i].pool_name, buf);
                entry = create_proc_entry(buf, S_IWUSR, poolset_entry);
                poolset_entry->proc_fops = &proc_poolset_operations;
        }
        return xt_register_target(&poolset_reg);
}

static void __exit poolset_target_exit(void)
{
        unsigned int i = 0;
        for (i = 0; i < MAXPOOL; i++) {
                remove_proc_entry(gpoolset[i].pool_name, poolset_entry);
        }
        xt_unregister_target(&poolset_reg);
}

module_init(poolset_target_init);
module_exit(poolset_target_exit);


4.2.2.實現用戶態配置程序

4.2.2.1.實現管理內核態poolset的用戶態程序

該程序標準地應該使用netlink實現用戶配置策略到內核的通知,就像iptables以及ipset一樣,但是也可以更簡單地用任何用戶態/內核態通信的方式實現,比如用procfs來實現

4.2.2.2.根據4.2.1節的結構體實現一個iptables的target實現一個新的target

4.2.2.3.實現思路

對於4.2.2.1的實現,使用echo XX:192.168.Y.Z > /proc/poolset/$name即可實現往特定的pool中添加IP地址。對於iptables模塊的實現思路如下(未測試):
libipt_POOLSET.c
#include <stdio.h>
#include <netdb.h>
#include <string.h>
#include <stdlib.h>
#include <xtables.h>
#include <linux/netfilter_ipv4/ip_tables.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

enum {
        //目前只支持一個配置
        TO_POOLSET_SET = 0,
};

#define MAXNAMELEN 100
struct pool_info {
        unsigned char pool_name[MAXNAMELEN];
};
struct ipt_poolsetinfo
{
        struct xt_entry_target t;
        struct pool_info mr;
};

static const struct xt_option_entry POOLSET_opts[] = {
        {
                .name = "poolset",
                .id = TO_POOLSET_SET,
                .type = XTTYPE_STRING,
                .flags = XTOPT_MAND | XTOPT_MULTI
        },
        XTOPT_TABLEEND,
};

static struct ipt_poolsetinfo *
set_contents(struct ipt_poolsetinfo *info, const char *arg)
{
        unsigned int size;
        size = XT_ALIGN(sizeof(struct ipt_poolsetinfo));
        info = realloc(info, size);
        if (!info)
                xtables_error(OTHER_PROBLEM, "Out of memory\n");
    //內核將只需要一個名稱即可,根據pool名稱,內核會在poolset中定位到具體的pool
        strcpy(info->mr.pool_name, arg);
        return info;
}
static void POOLSET_parse(struct xt_option_call *cb)
{
        const struct ipt_entry *entry = cb->xt_entry;
        struct ipt_poolsetinfo *info = (void *)(*cb->target);
        int portok;

        if (entry->ip.proto == IPPROTO_TCP
            || entry->ip.proto == IPPROTO_UDP
            || entry->ip.proto == IPPROTO_SCTP
            || entry->ip.proto == IPPROTO_DCCP
            || entry->ip.proto == IPPROTO_ICMP)
                portok = 1;
        else
                portok = 0;

        xtables_option_parse(cb);
        switch (cb->entry->id) {
        case TO_POOLSET_SET:
        {
                char *arg ;
                arg = strdup(cb->arg);
                if (arg == NULL)
                        xtables_error(RESOURCE_PROBLEM, "strdup");
                info = set_contents(info, arg);
                free(arg);
                *cb->target = &(info->t);
                break;
        }
        }
}

static struct xtables_target poolset_tg_reg = {
        .name           = "POOLSET",
        .version        = XTABLES_VERSION,
        .family         = NFPROTO_IPV4,
        .size           = XT_ALIGN(sizeof(struct pool_info)),
        .userspacesize  = XT_ALIGN(sizeof(struct pool_info)),
        .x6_parse       = POOLSET_parse,
        .x6_options     = POOLSET_opts,
};

void _init(void)
{
        xtables_register_target(&poolset_tg_reg);
}


5.總結

Linux的很多特性都是機制和策略分離的,機制在內核中實現而策略靠用戶態程序配置。Netfilter和iptables就是這方面的最佳體現之一,它們互相配合,聯合的很默契。因此如果想實現一個自定義的iptables策略,那麽就必須首先實現一個內核模塊,然後再實現一個iptables模塊,幸運的是,這件事是很簡單的,因為netfilter也好,iptables也罷,其核心框架邏輯都已經成型了,你需要做的僅僅是註冊一些數據結構,實現一些回調函數而已,正如本文所體現的,一切很非常簡單。

再分享一下我老師大神的人工智能教程吧。零基礎!通俗易懂!風趣幽默!還帶黃段子!希望你也加入到我們人工智能的隊伍中來!https://blog.csdn.net/jiangjunshow

編寫iptables模塊實現不連續IP地址的DNAT-POOL