实现一个做双向NAT的虚拟网卡

问题描述与解决方案

还是老问题,Linux系统中通过iptables配置的NAT无法在双向通信环境中使用,你无法配置一条NAT规则实现对两个方向主动发起的流量做NAT,解决这个问题的方案有好几种:

1.配置两条NAT规则

iptables的NAT配置本身就是先match再执行一个target,因此一条规则只能表示一种转换策略,要想实现“来自x的数据包的源地址转换为y,去往y的数据包的目标地址转为x”这样的逻辑,必须使用两条规则。那么为何不使用两条规则呢?因为iptables的nat配置是基于数据流的,它只对一个创建ip_conntrack结构体的那个数据包进行规则查找,因此在一个流已经创建并在数据传输的时候,加入一条nat配置是无效的。
       xtables-addons中有一个RAWNAT,不再基于ip_conntrack了,也就是它是基于数据包而不是数据流的NAT,即时生效问题解决了,但是由于它还是一个match-target规则,因此要想实现双向的NAT,还是要配置两条规则。

2.编写一个Netfilter HOOK

编写一个Netfilter HOOK模块不是什么难事,我自己写过好几个,但是,Netfilter框架是在协议栈的处理路径上拦截数据包进行检查-匹配/动作的,它对每一个经过协议栈的数据包都要进行检查,也就是说每一个数据包都要经过HOOK函数的过滤,在Netfilter HOOK过多的时候,大大降低了效率。

3.使用专门的虚拟设备

这是一种全新的理念,实现一个虚拟网卡,其xmit函数是这样的:
static netdev_tx_t sdnat_net_xmit(struct sk_buff *skb, struct net_device *dev)
{
    struct sdnat_struct *sdnat = netdev_priv(dev);
    unsigned int flags = sdnat->flags;
    struct nat_entry *entry;

    entry = find_sdnat_policy(skb, flags);
    if (unlikely(!entry)) {
        goto xmit;
    }
    if (flags & SNAT) {
        do_trans_src(entry, skb);
    } else if (flags & DNAT) {
        do_trans_dst(entry, skb);
    }
    // 此时skb的dst为将数据包导入NAT设备的dst_entry,
    // 为了防止循环路由,将其drop,NAT已经完成,已经没有用了
    skb_dst_drop(skb);
    // 清除mark,因为一般通过mark策略路由将数据包导入NAT设备
    // 这也是为了防止循环路由
    skb->mark = 0;
xmit:
    netif_rx_ni(skb);
drop:
    kfree_skb(skb);
    return NETDEV_TX_OK;
}

do_trans_src/dst完全可以通过一个函数实现,此处是为了使接口更加清晰。具体的转换就不多说了,很简单,修改掉IP报头的源地址或者目标地址,然后重新计算L3,L4的校验码。
       关键是如何组织nat规则。我使用一个nat_entry来保存每一条规则:
struct nat_entry {
    struct hlist_node hash_list;
    __be32 key1;  //对于SNAT即原始IP地址,对于DNAT即要转换到的IP地址
    __be32 key2;  //对于SNAT即要转换到的IP地址,对于DNAT即原始IP地址
    __be32 hash;  /数据包源IP或者目标IP的jhash_2words值
    int flags;
};

hash的计算如下:
static u32 keys_get_hash(__be32 key)
{
    return jhash_2words(key, 0x01, 0x0);
}

模块加载的时候,会创建两个虚拟网卡,一个负责SNAT,一个负责DNAT,同时系统中也会有两个sdnat_struct结构体,一个负责SNAT,一个负责DNAT:
struct sdnat_struct {
    int flags;
    struct net_device   *dev;
    struct hlist_head entrys[1024];
};

Linux上要配置就是两条策略路由:
a.从内网口进入往外发的数据包导入到SNAT网卡设备进行SANT;
b.从外网口进入到内网口的数据包导入到DNAT网卡设备进行DNAT。
这样就可以双向自动转换了,不管数据是从哪个首先发起的,实现了“来自x的数据包的源地址转换为y,去往y的数据包的目标地址转为x”。是不是和Cisco的static NAT有些类似呢?定义出入设备而不是靠iptables的match来过滤数据包。我比较喜欢使用procfs作为用户接口,因为它方便shell操作:
echo +192.168.1.1 9.24.100.1 >/proc/net/nat
上面的命令执行后,将会在两块网卡共享的hash表中添加一个nat_entry,key1为192.168.1.1,key2为9.24.100.1,在SNAT网卡设备中,将会用skb的iph->saddr做hash后查表匹配其key1,取出key2作为要转换的IP地址,在DNAT网卡设备中,将会用skb的iph->daddr做hash后查表匹配key2,取出key1作为要转换到的IP地址。如果想删除一条规则,那么就执行:
echo -192.168.1.1 9.24.100.1 >/proc/net/nat
策略路由规则如下:
ip rule add iif $内网口 table snat
ip rule add iif $外网口 table dnat
ip route add 0.0.0.0/0 dev snat0 table snat
ip route add 0.0.0.0/0 dev dnat0 table dnat
依靠路由来做是否要进行NAT的判断,是不是更加高效些呢?而不再需要通过Netfilter模块去匹配每一个数据包了,也不需要折腾低效率的ip_conntrack了!值得注意的是,sdnat设备的xmit函数最终执行了一个netif_rx_ni这相当于将数据包重新注入其本身,此时数据包的iif将不再是内网口或者外网口了,而是实实在在的sdant虚拟网卡设备,因此数据包再次到达路由模块的时候将不会再次进入sdnat设备。

引申出来的思想和含义

除了Netfilter框架之外,我们也可以使用Linux的网卡设备模型来构建另一套数据包过滤系统,是的,其思想就是上面展示的。我曾经写过几篇关于在路由项中保存信息,然后通过查路由表的方式获取信息的技巧,其中使用了自己定义的“路由表”,查询方式依然是最长前缀匹配法,只是路由项中保存的东西变了。在本文中,我给出的是使用Linux原生的路由表(不是自己定义的)+自定义的虚拟网卡设备实现数据包过滤的思想,按照这种思想,iptables的每一个target就是一个虚拟网卡设备,每一系列的matches就是一条路由,该路由的路由项就是将数据包导入对应的虚拟网卡设备,路由的方式来匹配数据包将比Netfilter的方式高效,因为它使用了hash/trie这类高效的数据结构,而不是像Netfilter那样遍历好几层的链表。
       事实上,这种思想很新吗?不!路由项不是有unreachable或者blackhole吗?它们不正是iptables的REJECT和DROP么?

你可能感兴趣的:(实现一个做双向NAT的虚拟网卡)