GRE/VXLAN 过nat的一些尝试(一)
gre 和 vxlan tunnel是无法过nat的,这两种tunnel使用起来很方便,有时候需要tunnel需要过nat,如果使用ssl,ipsec这些能够过nat的tunnel,会麻烦一些,且性能偏低。
最近工作上需要使用ipsec over 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;
}
调用这个函数的地方赋下值即可。
=============================================
ovs的实现撸了一遍代码,也可以实现,先说下方法,后面有时间测试过了再粘些代码:
每个接口需要增加一条流表,如下,主要是学习一条流表,根据目的mac设置vxlan tunnel头,其中set_field:NXM_NX_TUN_DPORT[]->tun_dport 不支持,需要增加支持。
in_port=1 actions=learn(NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[] action=set_field:NXM_OF_IP_SRC[]->tun_dst,set_field:NXM_NX_TUN_ID[]->tun_id,set_field:NXM_NX_TUN_DPORT[]->tun_dport,IN_PORT)
ovs_flow_cmd_new === 增加对 set_field:NXM_NX_TUN_DPORT[]->tun_dport 的支持
ovs tunnel key中增加 port 字段,vxlan_tnl_send 发送时,dport 和 sport从tunnel key中取而不是接口配置中取。
struct ovs_key_ipv4_tunnel {
__be64 tun_id;
__be32 ipv4_src;
__be32 ipv4_dst;
__be16 tun_flags;
__be16 tun_sport;
__be16 tun_dport;
u8 ipv4_tos;
u8 ipv4_ttl;
} __packed __aligned(4); /* Minimize padding. */
重要函数: key_extract/ovs_dp_process_received_packet/ovs_vport_receive/ovs_flow_cmd_set/vxlan_tnl_send