终于搞定Linux的NAT即时生效问题

引:超长的前言

Linux的NAT不能及时生效,因为它是基于ip_conntrack的,如果在NAT的iptables规则添加之前,此流的数据包已经绑定了一个ip_conntrack,那么该NAT规则就不会生效,直到此ip_conntrack过期,如果一直有数据在鲁莽地尝试传输,那么就会陷入僵持状态。
在Linux系统中,ip_conntrack创建成功是按照一个流的头包是否成功被传输出协议栈这个原则来判定的,因此只要有路由,一个数据包总是能被forward出去,此后,直到ip_conntrack过期,后续的包将都不是头包,虽然它们都是NEW状态(这是因为只有收到对端回复才会变为ESTABLISH状态),但是却不是第一个包,因此就没有机会去匹配后续添加的NAT规则。这件事令我头疼了好久,也实现了很多版本的诸如“NAT的即时匹配”等模块,但是均不完美,不完美之处在于和原生的NAT不兼容,虽然Linux的NAT做得不好,但是保持对它的兼容是对狂妄者最大的尊敬,说白了就是不应该修改既有NAT的代码!
令我奇怪的是,为什么没有人遇到和我一样的问题以及像我一样烦恼,如果有的话,我敢保证一定有现成的东西可用,不幸的是,没有,起码我没有找到这样的志同道合者。即使懒惰的开源社区没有积极的人,为何像Endian Firewall,Firewall Builder等这些重量级UTM,或者OpenWRT这种不伦不类者都没有实现?Why?
其实,这个问题也不是那么难,用户态完全可以封装一个新的iptables nat脚本,在添加了NAT规则之后,先使用conntrack-tools工具中的conntrac命令执行-D,参数是和NAT规则一样的matches,这样就清除了相关的ip_conntrack,NAT得以即时生效。但是这种做法不是那么的“本原主义”,有时候我在想,UNIX的KISS到底是遵循柏拉图呢,还是针对柏拉图的逆袭...我需要的是有一种机制,而不仅仅是一种策略来完成“NAT的即时生效”问题,前提是不能改动现有的代码。于是我彻底否定了上述的用户态封装的方案,也半否定了之前的修改NAT模块的方案,我要一个新的方案,它是另外一个模块,加载之,则成。
Netfilter,Linux平台“蹂躏”网络协议栈的利器,它几乎可以做到任何事情。它可以把数据包摆来摆去,随意放到随便什么地方,只要你愿意,你真的可以把数据包放到随便你想让它去的地方。本文涉及的是一个queue/reinject机制,就是说,在一个地方,特定的地方将数据包排入队列,然后一个处理器处理队列里面的数据包,然后再将其重新注入它排队的地方继续接受协议栈的后续处理,典型的queue机制是将数据包排入用户态的一个程序,然后一个程序处理之,再通过Netlink套接字将其重新注入,这是十分灵活的方法,我们完全可以写一个用户态程序,然后在其中将其ip_conntrack结构删除,再重新注入,这样,下一个数据包到来的时候就可以匹配NAT规则了,但是怎么删除呢?到头来还是要用conntrack的接口-就像conntrack-tools那样。有没有更方便的方法呢?
在内核态,有现成的ip_conntrack的操作接口可用调用,是不是就不用到用户态再处理呢?一般而言,导入用户态是因为用户态处理更加灵活,灵活性才是基本原则,如果在内核态更灵活,那就直接在内核态处理!是的,这就是我的思路。在我的实现中,我并不用等到下一个数据包才能匹配NAT规则,而是本数据包就可以。
十分感谢xtables-addons,它提供了一个编译模板,我可以很方便得实现自己的想法,一般而言就写两个C文件,有时候需要一个H文件即可,这次我又一次受惠于xtables-addons,可以快速开发,验证自己模块的功用。现在该给出代码了。

说明:命名与思路

我不知道该怎么给自己的模块命令,我的英文狠烂,老婆很忙又不肯帮我,又不能起一个中文名字,因此我只能使用XXX这种让人遐想的名字,我不会使用aaa,abc这种,这样会让人觉得我不负责任,有点玩世不恭或者太草率等所有你能想到,并且,真实地,我也因为这种草率埃过领导的批评以及同道人的嘲讽。接受了教训之后,我就使用XXX。
说完了这个奇怪的XXX命名,说一下思路。我的思路很简单,那就是针对所有的不会匹配NAT规则的数据包,将其ip_conntrack结构体删除,然后将其重新注入到PREROUTING的最开始处,这样它就可以绑定一个新的NEW状态的ip_conntrack,因此这样就可以匹配NAT规则了。

第一部分:iptables模块文件libxt_XXX.c

/*
 *      "XXX" target extension for iptables! 其中就是一个幌子,为了使用iptables而已!
 *
 *      This program is free software; you can redistribute it and/or
 *      modify it under the terms of the GNU General Public License; either
 *      version 2 of the License, or any later version, as published by the
 *      Free Software Foundation.
 */
#include 
#include 
#include "compat_user.h"

static void xxx_tg_help(void)
{
        printf("XXX takes no options\n\n");
}

static int xxx_tg_parse(int c, char **argv, int invert, unsigned int *flags,
                         const void *entry, struct xt_entry_target **target)
{
        return 0;
}

static void xxx_tg_check(unsigned int flags)
{
}

static struct xtables_target xxx_tg_reg = {
        .version       = XTABLES_VERSION,
        .name          = "XXX",
        .revision      = 1,
        .family     = NFPROTO_IPV4,
        .help          = xxx_tg_help,
        .parse         = xxx_tg_parse,
};

static __attribute__((constructor)) void xxx_tg_ldr(void)
{
        xtables_register_target(&xxx_tg_reg);
}


可以看到,这个文件简直就是一场骗局,里面什么都没有!是的,什么也没有,之所以写这个文件,只是为了我可以写下:
iptables -t mangle -A PREROUTING ... -j XXX
的时候而不报错。确实,XXX就是一个target,既然你想用iptables,起码也要例行一下公事,哪怕仅仅是敷衍一下而已,确实也只是敷衍一下。否则,你依然有办法设置,那就是使用procfs,sysfs,或者Netlink等...

第二部分:内核模块xt_XXX.c

/*
 *      xt_xxx - kernel module to drop and re-NEW CONNTRACK to
 *              fit NAT
 *
 *      Original author: Wangran 
 */

#include 
#include 
#include 
#include "compat_xtables.h"


MODULE_AUTHOR("Wanagran ");
MODULE_DESCRIPTION("Xtables: xxx match module");
MODULE_LICENSE("GPL");
MODULE_ALIAS("ipt_xxx");

/*
 * queue handler捕获数据包,然后重新注入,区别在于:
 * 1:如果本身是NOTRACK的数据包,直接注回去;
 * 2:如果本身没有绑定任何conntrack,直接注回去;
 * 3:如果本身有conntrack,删掉该conntrack后,注回去
 * 3.1.不是注回原来的位置,而是注回PREROUTING最开始的位置。
 * 注意:虽然TAGEGET本身已经阻止了1,2的情况,还是判断了一下,
 *       因为虽然我知道这一点,但是resetct_queue并不清楚...
 */

static int resetct_queue(struct nf_queue_entry *entry, unsigned queue_num)
{
        struct sk_buff *skb = entry->skb;
        struct nf_conn *ct = NULL;

        enum ip_conntrack_info ctinfo;
        if (nf_ct_is_untracked(skb))
                goto reinject;
        else if (!(ct = nf_ct_get(skb, &ctinfo)))
                goto reinject;
        else {
                // 为了重新初始化conntrack,使之状态变为可做NAT的NEW!
                struct list_head *elem = &nf_hooks[entry->pf][entry->hook];
                nf_reset(skb);
                nf_ct_kill(ct);
                entry->elem = list_entry(elem, struct nf_hook_ops, list);
        }
reinject:
        nf_reinject(entry, NF_ACCEPT);
        return 0;

}

/*
 * XXX的执行TARGET,旨在针对以下的一类数据包进行queue处理:
 * 本身是NEW状态,且已经被confirm了,这种数据包在其conntrack
 * 过期之前,无疑已经不会再去匹配任何NAT规则了!
 */

static unsigned int
xxx_tg4(struct sk_buff **skb, const struct xt_action_param *par)
{
        struct nf_conn *ct;
        enum ip_conntrack_info ctinfo;
        ct = nf_ct_get(*skb, &ctinfo);
        if (!ct || ct == &nf_conntrack_untracked) {
                return XT_CONTINUE;
        }
        // 仅仅处理正向数据包,否则...
        if (CTINFO2DIR(ctinfo) == IP_CT_DIR_REPLY) {
                return XT_CONTINUE;
        }
        if (ctinfo == IP_CT_NEW && !nf_ct_is_confirmed(ct)) {
                return XT_CONTINUE;
        }
        return NF_QUEUE;
}


static struct nf_queue_handler xxxqh = {
        .name  = "resetct",
        .outfn = resetct_queue,
};

static struct xt_target xxx_tg_reg[] __read_mostly = {
        {
                .name           = "XXX",
                .revision       = 1,
                .family         = NFPROTO_IPV4,
                .table          = "mangle",
                .hooks          = 1 << NF_INET_PRE_ROUTING,
                .target         = xxx_tg4,
                .me             = THIS_MODULE,
        },
};
static int __init xt_xxx_target_init(void)
{
        int status = 0;
        status = nf_register_queue_handler(NFPROTO_IPV4, &xxxqh);
        if (status < 0) {
                printk("XXX: register queue handler error\n");
                goto err;
        }
        status = xt_register_targets(xxx_tg_reg, ARRAY_SIZE(xxx_tg_reg));
        if (status < 0) {
                printk("XXX: register target error\n");
                goto err;
        }

err:
        return status;
}

static void __exit xt_xxx_target_exit(void)
{
        nf_unregister_queue_handlers(&xxxqh);
        return xt_unregister_targets(xxx_tg_reg, ARRAY_SIZE(xxx_tg_reg));
}

module_init(xt_xxx_target_init);
module_exit(xt_xxx_target_exit);


内核模块也很简单,我不喜欢复杂,并且对Netfilter理解得足够多,多到可以做到不复杂,这并不意味着我搞不定复杂,毕竟简单的基础就是复杂,理解Netfilter本身就是一件很复杂的事。不想多说,如果你懂Netfilter,自然可以一眼看明白上述代码,如果不懂,要么去学,要么放弃,如果仅仅想用一下这个模块,那就编译加载。

第三部分:关于使用

一般而言,你可以使用下面的命令:
iptables -t mangle -A PREROUTING -j XXX

这样的话,所有进来的数据包都会执行下面的逻辑:




如果这样,相当于架空了整个ip_conntrack的优化,这种鲁莽的做法并不是我的目的,我希望它和其它的match比如mark,condition一起使用,这样就可以把不相关的数据包过滤掉而不触及,依旧执行往常的逻辑,这就是我为何一直坚持使用iptables的原因而不是使用其它的用户态/内核态通信的方式。就想之前我提到的基于ip_conntrack的快速/慢速匹配方式那样,这个NAT及时匹配也可以使用类似的逻辑:

iptables -t mangle -A PREROUTING -m condition --condition slow ... -j XXX
slow变量维持的时间T,T等于所有协议中ip_conntrack过期的最长时间。

你可能感兴趣的:(终于搞定Linux的NAT即时生效问题)