Linux系统NAT实现机制的升级改进_linux 系统nat

一点牢骚和希望

一直以来,一直对Linux的NAT很不满,也写过《Linux系统如何平滑生效NAT》系列文章中的patch进行修补,还写过一些类Cisco实现的patch,然而都效果不大好,暴雨的夜晚,长假的倒数第二晚,虽然没有10月7日晚雨量大,可是10月6日晚上到7日凌晨,上海嘉定那边的雨也可以堪称暴雨了。一直想看却一直没有时间看的《斯巴达克斯 第三季》终于看完了,雨越大越兴奋,可是巴拉巴西的《链接》也看完了,《罗马人的故事》最后一卷也看完了,《黑天鹅》还没有到货,剩下的只有写点代码了...于是半瓶竹叶青陪我到天蒙蒙亮,修改了几个内核代码文件,debug了几小时,小睡了两小时后,起床去买新鲜的肉类和蔬菜以及海鲜,因为长假最后一天要一起在家吃火锅。火锅很爽,外面大雨如注,屋里热气腾腾...就这样,雨一直下到第二天早上。

        10月7日第一天上班,我正常出门,可是到了公司已经12点多,在上地铁的时候,涉水到膝盖,转弯,突然发现暗黄色漂浮物,臭气迎面而来,素不相识的一行人为了安全走在了一起,我在头阵...听说是附近的厕所出问题了,污水粪便就从地下涌了上来...继续前行,停下,还是回头?如果只有我一人,我肯定回头了,然而后面有俩MM,还挺时尚漂亮,都说要过去,然后再洗,另外一位本来应西装笔挺的正装哥们儿由于裤子太合身无法挽起,执意也要先走过去再说...我如此邋遢的该如何,可想而知...真心不想趟这浑水啊!!...

        这么多和工作无关的琐事,我太罗嗦了。步入正题了!

alloc_null_binding的作用

Linux的NAT实现是基于ip_conntrack的,这句话已经不知道说了多少遍。一切均实现在Netflter的HOOK函数里面,其逻辑一点也不复杂,然而有意个小小的要点,那就是:即使没有匹配到任何的NAT规则的和NAT无关的数据流,也要针对其执行一个null_binding,所谓的null_binding就是用其原有的源IP地址和目标IP地址构造一个range,然后基于这个range做转换,这看似是一个无用的东西,其实还真的有用。

        用处在哪里呢?注意null_binding只是不改变IP地址,其端口可能要发生改变。为何要改变和NAT无关的数据流的端口呢?因为和NAT有关的数据流可能为了五元组的唯一性已经将和NAT无关的数据流的某个端口给占用了,这就影响了和NAT无关的数据流五元组的唯一性。由于ip_conntrack是不区分是否和NAT有关的,而NAT操作要改变五元组,为了整个conntrack的五元组都是唯一的,哪怕只有一个数据流执行了NAT,也可能占用了某个其它数据流的五元组要素,进而引发连锁反应,所以全部要执行唯一性检测和更新,alloc_null_binding就是为了做这个操作。

彻底消除流头匹配NAT的概念

要是没有深入研究过Linux的NAT,只是仅仅会配置它的话,也许你还真的不知道NAT规则只对一个流的第一个起作用,确切的说,是只针对一个流的ip_conntrack结构体刚刚建立还没有confirm的时候起作用,因为有时ip_conntrack结构体会过期。只要这样的包离开了协议栈,流就被confirm了,接下来的属于同一个流的其它数据包就直接使用上述那个包的保存在ip_conntrack结构体中的NAT结果了。

        正是由于这个特点,使得你无法中途添加NAT规则使之立马生效或者修改已有的NAT结果。这种有状态的特性带来了很多的问题。之前写过《Linux系统如何平滑生效NAT》系列文章,做过一些修正补丁。然而那些补丁的问题在于:它们还是基于流头匹配NAT规则的小修小补。我们知道,这种小修小补最终的结果就是不可维护,那么何不来一个颠覆,即,不再采用流头匹配NAT的原则,改为想什么时候匹配就什么匹配的原则。这其实是一种更高层次的颠覆,即流头匹配原则是新的匹配原则的一种特例。

       废除了流头匹配原则后,我决定把何时执行NAT的决定权留给应用程序,因此我决定注册一个sysctl变量,当其非0时执行NAT,不管是不是已经confirm了。

什么时候需要匹配NAT规则

既然说流头匹配原则不好,会带来问题(比如confirm的连接由于没有NAT而僵持在那里的问题),那么肯定要指出何时执行NAT匹配是必要的,这叫有破有立。在以下的情况下,执行NAT是必要的:

1.数据流连接时,由于还没有做NAT而导致久久连不上的情形。此时数据流的CT状态依然是NEW;
2.数据流已经成功连接,但是需要改变一下源地址(改变目标地址意味着重新连接一个新的服务)。此时的数据流的TC状态是ESTABLISHED;
3.数据流已经经过NAT连接,但NAT规则改变了。此时的数据流的TC状态是ESTABLISHED;

哪些情况不能执行NAT

并不是所有的以上情况都适合执行NAT匹配进而执行NAT,我们不光要考虑双向五元组标示的ip_conntrack本身,还要考虑协议本身的语义。我们看一下TCP协议,由于TCP严格根据五元组维持一个既有的连接,修改任何因子都意味着连接不复存在。因此:

1.对于TCP之类的有连接4层协议而言,只有NEW状态的数据流才能执行NAT,非NEW状态意味着已经收到目标的反馈,执行NAT没有意义;
2.一个流的其中一个数据包已经做好了NAT,并且NAT规则没有改变的情况,此时反向五元组已经被改了,没有必要每次都去匹配一遍NAT规则表;

能做和不能做

对于能做的事情,一般而言你不做也可以,就是你可以做也可以不做,但是对于不能做的事情,基本就是严禁了,如果你做了,就会带来严重的后果或者即使没有严重的后果也完全是无用功,世界就是这么的不对称,有时点到为止,总是功不抵过!因此对于以上两个小节,‘什么时候需要匹配NAT规则’中的一些点,我把控制权交给了应用程序,因此导出了一个sysctl接口,而对于‘哪些情况不能执行NAT’中的情形,则由内核来控制。

代码实现

以上的所有落实下来的话就是代码了,我没有将标准的patch贴到文章,因为那是打patch的时候给程序看的,如果让人看,一大堆的+++---的肯定很扰乱视线,因此我换了一种方式,即
//包围的为我添加的代码段,
/########包围的为我修改的代码段。本小节的结构为:

{{文件名\n代码段\n总体说明},...}:

include/net/netfilter/nf_nat.h

//避开ip_conntrack_status枚举成员即可,然而13可谓一个重量级的数字
#define NF_FORCE_NAT_BIT 13

说明:增加了一个新的CT状态,用来指示是否要做NAT匹配。

include/net/netfilter/nf_conntrack_l4proto.h

struct nf_conntrack_l4proto
{
...
    int (*can_force_nat)(struct nf_conn *ct, struct sk_buff *skb);
...
}

说明:nf_conntrack_l4proto结构体增加了一个can_force_nat回调函数,将判断是否能重新执行NAT的决定权交给4层协议自己而不是在ip_conntrack以及nat逻辑中为之代劳。

net/netfilter/nf_conntrack_proto_tcp.c

//
static int nf_ct_can_force_nat(struct nf_conn *ct, struct sk_buff *skb)
{
        //没什么好说的...
        return 1;
}
//
...
struct nf_conntrack_l4proto nf_conntrack_l4proto_tcp4 __read_mostly =
{
...
//
        .can_force_nat          = nf_ct_can_force_nat,
//
...
};

说明:添加了nf_ct_can_force_nat回调函数,指示在ESTABLISH状态不能重新执行NAT。

net/ipv4/netfilter/nf_nat_standalone.c

//
#ifdef CONFIG_SYSCTL
//增加用户态的sysctl接口,位于/proc/sys/net/ipv4/netfilter/nf_force_nat
static struct ctl_table_header *nat_sysctl_header;
static unsigned int nf_force_nat __read_mostly = 0;
static struct ctl_table nf_nat_sysctl_table[] = {
        {
                .procname       = "nf_force_nat",
                .data           = &nf_force_nat,
                .maxlen         = sizeof(unsigned int),
                .mode           = 0644,
                .proc_handler   = proc_dointvec_jiffies,
        },
        {
                .ctl_name       = 0
        }
};
#endif
//
...
static unsigned int
nf_nat_fn(unsigned int hooknum,
          struct sk_buff *skb,
          const struct net_device *in,
          const struct net_device *out,
          int (*okfn)(struct sk_buff *))
{
...
//
#ifdef CONFIG_SYSCTL
        if (nf_force_nat !=0) {
                set_bit(NF_FORCE_NAT_BIT, &ct->status);
        } else {
                clear_bit(NF_FORCE_NAT_BIT, &ct->status);
        }
#else
        clear_bit(13, &ct->status);
#endif
//
        switch (ctinfo) {
        case IP_CT_RELATED:
...
        case IP_CT_NEW:
/########
//增加一个标签
renat:
                /* Seen it before?  This can happen for loopback, retrans,
                   or local packets.. */
                //增加一个允许NAT的可能性
                if (!nf_nat_initialized(ct, maniptype) || test_bit(NF_FORCE_NAT_BIT, &ct->status))
/########
                {
                        unsigned int ret;
...
        default:
                /* ESTABLISHED */
                NF_CT_ASSERT(ctinfo == IP_CT_ESTABLISHED ||
                             ctinfo == (IP_CT_ESTABLISHED+IP_CT_IS_REPLY));
//
                if (test_bit(NF_FORCE_NAT_BIT, &ct->status)) {
                        struct nf_conntrack_l3proto *l3proto;
                        struct nf_conntrack_l4proto *l4proto;
                        unsigned int dataoff;
                        u_int8_t protonum;
                        int ret;
                        l3proto = __nf_ct_l3proto_find(NFPROTO_IPV4);
                        ret = l3proto->get_l4proto(skb, skb_network_offset(skb),
                                        &dataoff, &protonum);
                        l4proto = __nf_ct_l4proto_find(NFPROTO_IPV4, protonum);
                        /**
                         *      实际上本来就应该由四层协议本身来决定是否可以强制NAT,
                         *      但是那样就要修改conn层的回调
                         */
                        if (l4proto->can_force_nat == NULL ||
                                !l4proto->can_force_nat(ct, skb)){
                                goto renat;
                        }
                }
//
        }
...
}
...
static int __init nf_nat_standalone_init(void)
{
...
//
#ifdef CONFIG_SYSCTL
        nat_sysctl_header = register_sysctl_paths(nf_net_ipv4_netfilter_sysctl_path, nf_nat_sysctl_table);
        if (nat_sysctl_header == NULL) {
                printk("nf_nat_init: can't register nat_sysctl");
                goto cleanup_rule_init;
        }
#endif
//
        return ret;

 cleanup_rule_init:
...
}
static void __exit nf_nat_standalone_fini(void)
{
//
#ifdef CONFIG_SYSCTL
        unregister_sysctl_table(nat_sysctl_header);
        nat_sysctl_header = NULL;
#endif
//
...
}

说明:为NAT的Netflter的HOOK函数添加何时执行NAT的判断逻辑。

net/ipv4/netfilter/nf_nat_rule.c

int nf_nat_rule_find(struct sk_buff *skb,
                     unsigned int hooknum,
                     const struct net_device *in,
                     const struct net_device *out,
                     struct nf_conn *ct)
{
        struct net *net = nf_ct_net(ct);
        int ret;
        ret = ipt_do_table(skb, hooknum, in, out, net->ipv4.nat_table);

        if (ret == NF_ACCEPT) {
/########
                if (!nf_nat_initialized(ct, HOOK2MANIP(hooknum)) || test_bit(NF_FORCE_NAT_BIT, &ct->status)) {
/########
//
                        //如果在ipt_do_table中没有匹配到NAT规则,并且此时允许重新NAT,则说明要把反向五元组还原成原始的反向五元组
                        //本来想在这里做一个优化的,即如果还原了之后,在新的NAT配置上来之前,不再执行还原操作,然而这样会有问题,
                        //注意本文第一节,由于不能保证其它的数据流是否做了NAT从而占据了不该占据的五元组,为了保证唯一性,这里的
                        //alloc_null_binding必须持续调用,唯一可以优化的地方在于可以不用每次都调用nf_ct_invert_tuplepr以及
                        //nf_conntrack_alter_reply,而这只需要一个flag位即可。
                        /* NUL mapping */
                        if (nf_ct_is_confirmed(ct)) {
                                struct nf_conntrack_tuple reply;
                                nf_ct_invert_tuplepr(&reply, &ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);
                                nf_conntrack_alter_reply(ct, &reply);
                        }
//
                        ret = alloc_null_binding(ct, hooknum);
                }
        }
        return ret;
}

说明:nf_nat_rule_find中如果没有找到规则,则判断是否是将已有规则删除了,进而恢复原始状态。

net/ipv4/netfilter/nf_nat_core.c

unsigned int
nf_nat_setup_info(struct nf_conn *ct,
                  const struct nf_nat_range *range,
                  enum nf_nat_manip_type maniptype)
{
...
/########
        //bug仅仅on
        BUG_ON(!test_bit(NF_FORCE_NAT_BIT, &ct->status) && nf_nat_initialized(ct, maniptype));
/########
...
                nf_ct_invert_tuplepr(&reply, &new_tuple);
//
                if (nf_ct_is_confirmed(ct)) {
                        spin_lock_bh(&nf_conntrack_lock);
                        hlist_nulls_del_rcu(&ct->tuplehash[IP_CT_DIR_ORIGINAL].hnnode);
                        hlist_nulls_del_rcu(&ct->tuplehash[IP_CT_DIR_REPLY].hnnode);
                }
//
                nf_conntrack_alter_reply(ct, &reply);
//
                if (nf_ct_is_confirmed(ct)) {
                        nf_conntrack_hash_insert(ct);
                        spin_unlock_bh(&nf_conntrack_lock);
                }
//
...
//
        clear_bit(NF_FORCE_NAT_BIT, &ct->status);
//

        return NF_ACCEPT;
}

说明:在nf_nat_setup_info中判断如果是强制重新执行confirm状态的流的NAT,则重新将其修改过的反向五元组入哈希表。

测试方法

1.以上的代码修改过以后,make,insmod...;

2.ping或者telnet一个不存在的地址;

3.加载iptables规则实现DNAT,将不存在的地址转换为一个存在的地址;

4.echo 1 >/proc/sys/net/ipv4/netfilter/nf_force_nat

5.通了吗?

6.删除那条iptables NAT规则,icmp不通了,telnet仍然通。

同样的方法测试SNAT。

问题

即时这个patch已经朝着perfect前进,它依然无法解决在Linux上简单配置双向静态NAT的问题,它解决的只是随时NAT的问题。那么怎么去支持双向静态NAT呢?目前有一种办法(除了之前写过的那个办法之外)。

        即完全启用nat extension,在添加静态NAT规则的时候,用nat后的已经修改的反向二元组(源/目标IP地址)和正向二元组构造两个个虚拟的nat_conntrack,并将两个二元组插入一个专门的NAT哈希表,这样不管数据从哪个方向发起,在静态NAT的HOOK逻辑(即nf_nat_fn)中,直接去根据自己的源地址去查NAT哈希表,如果找到则取出其反向二元组使用其中的非any地址覆盖nf_conntrack反向五元组的对应位置即可。

        以上设计的本质在于,既然基于matches无法实现双向静态NAT,那么为何不扫除match呢?我们需要的仅仅是下面的推导:

SNAT:    源:A==>源:C
正向:    源A->目标X
反向:    源X->目标C
               ||
               \/
DNAT:    目标C==>目标A
正向:    源X->目标C

反向:    源B->目标X

数据结构如下:

ENUM dir {
    orig,
    reply,
}
tuple {
    address[dir] addrs
}
nat_conntrack {
    tuple[dir] tuples;

}

两个方向的tuple均加入哈希表,永远用正方向的IP二元组去查找,然后取出反向二元组使用。如果以上两个tuple都能在配置NAT规则的时候加入系统,则数据包在nf_nat_fn中就可不用Ipt_do_table调用去匹配NAT规则了,只需要:

1.如果是PREROUTING,则用自己目标IP地址去查询nat_hash,找到tuple后获取对应的nat_conntrack,进而得到反向tuple,然后用反向tuple的源IP地址覆盖掉ip_conntrack的反向五元组的源IP,然后alert reply tuple即可;
2.如果是POSTROUTING,则用自己的源IP地址去查询nat_hash,找到tuple后获取对应的nat_conntrack,进而得到反向tuple,然后用反向tuple的目标IP地址覆盖掉ip_conntrack的反向五元组的目标IP,然后alert reply tuple即可。

原文链接: https://blog.csdn.net/dog250/article/details/12675693

欢迎关注

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

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

    Linux系统NAT实现机制的升级改进_linux 系统nat

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

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

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

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

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

相关推荐