问题描述与解决方案
还是老问题,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么?