支持nat-t的tunnel要么是标准的C/S模型,能够从墙内发起建立tunnel隧道,且有保活机制使防火墙上的ct持久生效。要么像IPSec一样,能够在协议层面支持,知道tunnel是经过nat的。
linux gre 和 vxlan 作为常用的tunnel口是无法过nat的,但其性能和复杂度比ssl,ipsec这些能够过nat的tunnel要好很多。
做了些gre/vxlan过nat的测试,做了些记录:
GRE过nat
如果网关设备为ovs,直接通过流表学习到nat过的gre头即可,如果网关设备为linux bridge需要改动内核gre模块,修改量不多。
网关设备为ovs:
1. ovs上的操作
* 创建br0
ovs-vsctl add-br br0
ip link set up dev br0
ip addr add 211.1.1.1/24 dev br0
* 创建gre口
ovs-vsctl add-port br0 gre1 -- set interface gre1 type=gre options:local_ip=192.168.121.177 options:remote_ip=flow options:key=flow
* 创建流表,in_port=1是gre1的端口编号,流表主要用于学习反向nat过的流表。流表需要配置老化时间防止对端不可用后流表残留,这里只是测试没配置。
ovs-ofctl add-flow br0 "priority=1,in_port=1,actions=learn(priority=1,NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[] load:NXM_NX_TUN_ID[]->NXM_NX_TUN_ID[],load:NXM_NX_TUN_IPV4_SRC[]->NXM_NX_TUN_IPV4_DST[],output:NXM_OF_IN_PORT[]),NORMAL"
* 配置arp表项,可以省掉,能够自动学习,工作上组网远比试验复杂,所有节点mac和ip都是分配的,arp也是静态配置的,所以这里用静态配置。
#ip neigh
211.1.1.2 dev br0 lladdr de:22:5f:f3:71:a6 PERMANENT
2. bridge上操作
* 创建bridge
brctl addbr br0
ip link set up dev br0
ip addr add 211.1.1.2/24 dev br0
* 创建gre口,加入bridge
key一定要加上,即使是0
ip link add gretap0 type gretap local 15.1.1.2 remote 192.168.121.177 key 0
brctl addif br0 gretap0
ip link set up dev gretap0
* 配置arp表项
211.1.1.1 dev br0 lladdr f2:f9:24:d1:a1:40 PERMANENT
3. nat 设备上操作
* 配置出口nat
iptables -t nat -A POSTROUTING -i eth0 -o eth1 -j MASQUERADE
* 安装gre的nat支持
modprobe nf_conntrack_pptp
modprobe nf_nat_pptp
modprobe nf_conntrack_proto_gre
modprobe nf_nat_proto_gre
4. 测试
bridge上 ping 211.1.1.1
#ping 211.1.1.1
PING 211.1.1.1 (211.1.1.1) 56(84) bytes of data.
64 bytes from 211.1.1.1: icmp_seq=1 ttl=64 time=1.10 ms
64 bytes from 211.1.1.1: icmp_seq=2 ttl=64 time=0.634 ms
64 bytes from 211.1.1.1: icmp_seq=3 ttl=64 time=0.661 ms
ovs上可以看到学习到一个流表,NXM_NX_TUN_IPV4_DST 是经过nat的nat设备的出口ip地址,而不是bridge上ip地址,这样即使nat过,回程流量的gre头也能正确封装,当然有个前提,nat内部的bridge设备需要做gre的keepalive,定时发些报文到ovs上,使nat设备能够保持连接跟踪表,ovs上能刷新回程流表:
cookie=0x0, duration=2449.653s, table=0, n_packets=635, n_bytes=62230, idle_age=0, hard_age=0, priority=1,dl_dst=de:22:5f:f3:71:a6 actions=load:0->NXM_NX_TUN_ID[],load:0xc0a87945->NXM_NX_TUN_IPV4_DST[],output:6
网关设备为bridge
正在测试中。。。。
二. vxlan过nat
vxlan封装在udp中,但是两端是独立建socket的,而不是c/s模型,需要做些修改支持nat。vxlan在bridge实现较为简单,linux原生的vxlan实现直接使用vxlan接口上配置的dstport作为udp的目的端口号,实际上如果过nat-t,nat内部的设备需要先主动从vxlan上发一些数据包,在nat设备上建立连接跟踪表,nat外部的设备用转换过的src ip和src port作为返向流量的dst ip和dst port,linux的fdb表项学习已基本支持这个功能,唯一需要修改的地方是src port的学习,修改量10行之内就有很好的效果;
ovs类似,基本原理也是nat外部设备通过入方向的报文学习到出方向的vxlan的封装信息,通过流表的learn功能实现添加流表,同样缺少tun_port的支持,修改量稍大,过了一下ovs的源码,感觉修改量在可控范围内,300行左右的修改;
大概看了一下vxlan的协议栈,感觉可以通过修改vxlan模块达成目的,特别如果在dpdk上实现更简单一点。今天在原生的linux上试了一下,只修改了vxlan.ko 模块不到10行代码,搞定了,效果非常好。
bridge协议栈,每次收到报文的时候都会学习 fdb表项,就是mac---ip地址映射,回来的报文走这个表项封装vxlan报文以及转发,vxlan的表项特殊之处全在下面这个结构里了,有remote_ip,remoteport,remote_vni,内核只根据接收报文对remote_ip做了赋值,其它两个都取的接口上的配置,所以只需要对这两个成员赋下值就ok了,这样即使nat过的ip和port,也能学到fdb表中,回程报文使用这两个值封装vxlan,到了防火墙也能通过。
注意需要在cpe设备上做保活,防止防火墙的连接跟踪表项老化。
struct vxlan_rdst {
union vxlan_addr remote_ip;
__be16 remote_port;
__be32 remote_vni;
u32 remote_ifindex;
struct list_head list;
struct rcu_head rcu;
struct dst_cache dst_cache;
};
vxlan_xmit_one:
。。。。
if (rdst) {
dst_port = rdst->remote_port ? rdst->remote_port : vxlan->cfg.dst_port;
vni = rdst->remote_vni;
dst = &rdst->remote_ip;
local_ip = vxlan->cfg.saddr;
dst_cache = &rdst->dst_cache;
}
。。。。
udp_tunnel_xmit_skb(rt, sk, skb, local_ip.sin.sin_addr.s_addr,
dst->sin.sin_addr.s_addr, tos, ttl, df,
src_port, dst_port, xnet, !udp_sum);
。。。。
修改点:
static bool vxlan_snoop(struct net_device *dev,
union vxlan_addr *src_ip, __be16 src_port, __be32 vni,const u8 *src_mac) // 增加port和vni赋值
{
struct vxlan_dev *vxlan = netdev_priv(dev);
struct vxlan_fdb *f;
f = vxlan_find_mac(vxlan, src_mac);
if (likely(f)) {
struct vxlan_rdst *rdst = first_remote_rcu(f);
if (likely(vxlan_addr_equal(&rdst->remote_ip, src_ip)))
return false;
/* Don't migrate static entries, drop packets */
if (f->state & NUD_NOARP)
return true;
if (net_ratelimit())
netdev_info(dev,
"%pM migrated from %pIS to %pIS\n",
src_mac, &rdst->remote_ip, &src_ip);
rdst->remote_ip = *src_ip;
rdst->remote_port = src_port; // 增加port和vni赋值
rdst->remote_vni = vni;
f->updated = jiffies;
vxlan_fdb_notify(vxlan, f, RTM_NEWNEIGH);
} else {
/* learned new entry */
spin_lock(&vxlan->hash_lock);
/* close off race between vxlan_flush and incoming packets */
if (netif_running(dev))
vxlan_fdb_create(vxlan, src_mac, src_ip,
NUD_REACHABLE,
NLM_F_EXCL|NLM_F_CREATE,
//vxlan->dst_port,
//vxlan->default_dst.remote_vni,
src_port, vni, // 增加port和vni赋值
0, NTF_SELF);
spin_unlock(&vxlan->hash_lock);
}
return false;
}
调用这个函数的地方赋下值即可。