编写iptables模块实现不连续IP地址的DNAT-POOL

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 <marywangran@126.com>");
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 <marywangran@126.com>");
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/dog250/article/details/6704814

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    编写iptables模块实现不连续IP地址的DNAT-POOL

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/409332

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月26日 上午11:20
下一篇 2023年4月26日 上午11:20

相关推荐