背景
最近有个需求,需要把公网IP映射到虚拟机上。需求其实很简单,利用Neutron的原生FloatingIP功能就能解决,但是真正解决的时候还是耗了不少时间。大家都是知道公有云和私有云最本质的区别不是虚拟化,也不是存储,而是网络。公有云的网络庞大而复杂,如何把公网IP正确的映射到用户的虚拟机上,背后还是包含了众多的网络技术。对于大部分利用OpenStack搭建私有云的企业,一般采用Flat和Vlan的组网模型,网关都在物理交换机上,很少利用L3-agent来做VRouter。所以网络相对简单,排查起来也比较容易。我的也是基于Vlan的组网架构,在此基础上创建VRouter来绑定FloatingIP。
经过这两天的折腾,终于把公网IP映射到虚拟机上,中途遇到不少坑,有自身原因也有OS本身的坑,总之没有想象中的容易。
Tips:先声明下Neutron的组网架构不一致,最终效果也不一样,本文内容不做任何保证
操作
在操作之前简单介绍下我的环境,1台控制+网络节点,9台计算节点。由于映射公网IP地址需求比较少,L3-agent就只部署到网络节点上。
网络节点网卡分配如下:
网卡 | IP | 类型 | 用途 |
---|---|---|---|
em1 | 10.17.64.1 | 普通 | 管理网,Ceph PublicNet |
em2 | External Net | Flat | 公网 |
em3 | Privite Net | Vlan | 虚拟机网络 |
em4 | 10.17.70.1 | 普通 | Ceph ClusterNet |
Neutron支持的服务如下:
[DEFAULT]
service_plugins=neutron_lbaas.services.loadbalancer.plugin.LoadBalancerPluginv2,router,metering,firewall,neutron.services.qos.qos_plugin.QoSPlugin
[service_providers]
service_provider=LOADBALANCERV2:Haproxy:neutron_lbaas.drivers.haproxy.plugin_driver.HaproxyOnHostPluginDriver:default
service_provider=FIREWALL:Iptables:neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver:default
这里面包括LBAAS,FWAAS,ROUTER和QOS等特性。
ml2_conf.ini
ml2没什么的改的,只需要在加载ml2驱动的时候包含vlan和falt就可以了,在这里配置
[ml2]
type_drivers = vlan,flat
还需要定义一下flat的网络类型
[ml2_type_flat]
flat_networks = external
l3_agent.ini
l3的坑多,最开始我用的默认配置,结果出现一些摸不着头脑的问题,例如下面这样。
1. VRouter绑定的Port网卡qg-xxxx
和qr-xxxx
都桥接在br-int
下
# ovs-vsctl show
···
Bridge br-int
Controller "tcp:127.0.0.1:6633"
is_connected: true
fail_mode: secure
Port br-int
Interface br-int
type: internal
Port "tapc68839ea-5f"
tag: 1
Interface "tapc68839ea-5f"
type: internal
Port "int-br-bo8eb174"
Interface "int-br-bo8eb174"
type: patch
options: {peer="phy-br-bo8eb174"}
Port "qg-c40e9517-79"
Interface "qg-c40e9517-79"
type: internal
Port "qr-2ee3d6b2-2c"
tag: 1
Interface "qr-2ee3d6b2-2c"
type: internal
···
结果发现一个隐藏的坑是external_network_bridge
默认配置项是空的,这导致router绑定都port都放在br-int下。
解决也很简单,如下配置就好了。
[DEFAULT]
external_network_bridge = br-ex
2. Router防火墙
由于开启了防火墙,还要需要在L3的扩展里面加上fwaas,如下:
[AGENT]
extensions=fwaas
虚拟Router
1. 创建NetWork
$ neutron net-create --provider:physical_network external --provider:network_type flat --router:external=True --shared true external
2. 创建Subnet
tips:在公网IP资源稀缺情况下sunbet不要开启dhcp,避免不必要的浪费。
$ neutron subnet-create --allocation-pool start=<公网起始IP>,end=<公网结束IP> --gateway <公网网关> --disable-dhcp PublicNetwork <公网CIDR>
3. 创建Router
$ neutron router-create router
4. 设置Router网关
$ neutron router-gateway-set ROUTER EXTERNAL-NET
这里external网络可以绑定固定ip,只需要引入--fixed-ip subnet_id=SUBNET,ip_address=IP_ADDR
就可以了。
5. Router绑定内网Port
$ neutron router-interface-add ROUTER PRIVETE-NET
完成以上工作就能在网络节点的ovs和namespace中看到vrouter的信息了。
$ ip netns exec qdhcp-ade96f26-fbdd-4c6d-b705-069994609d1b ip a
1: lo: mtu 65536 qdisc noqueue state UNKNOWN qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
52: qg-c40e9517-79: mtu 1500 qdisc noqueue state UNKNOWN qlen 1000
link/ether fa:16:3e:75:64:72 brd ff:ff:ff:ff:ff:ff
inet < External Gateway >/26 brd < External Boardcast > scope global qg-c40e9517-79 # < External Gateway > router绑定的一个公网ip,用于做SNAT
valid_lft forever preferred_lft forever
inet6 fe80::f816:3eff:fe75:6472/64 scope link
valid_lft forever preferred_lft forever
53: qr-2ee3d6b2-2c: mtu 1500 qdisc noqueue state UNKNOWN qlen 1000
link/ether fa:16:3e:72:b8:41 brd ff:ff:ff:ff:ff:ff
inet < Privetnet Gateway >/23 brd < Privetnet Boardcast > scope global qr-2ee3d6b2-2c # < Privite Gateway > 内网物理机网络的一个网关
valid_lft forever preferred_lft forever
inet6 fe80::f816:3eff:fe72:b841/64 scope link
valid_lft forever preferred_lft forever
$ ip netns exec qdhcp-ade96f26-fbdd-4c6d-b705-069994609d1b ip r
default via < 公网网关 > dev qg-c40e9517-79
浮动IP
1. 创建浮动IP
$ neutron floatingip-create EXTERNAL-NET
也可以手动创建固定IP的,加入--fixed-ip-address
参数即可。
2. 绑定浮动IP到虚拟机
$ neutron floatingip-associate FLOATINGIP_ID VM_PORT
绑定成功后在Router的命名空间里面可以看到以下内容:
$ ip netns exec qrouter-7d233b38-fca8-44c0-9752-9350589a0af1 ip a
1: lo: mtu 65536 qdisc noqueue state UNKNOWN qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
52: qg-c40e9517-79: mtu 1500 qdisc noqueue state UNKNOWN qlen 1000
link/ether fa:16:3e:75:64:72 brd ff:ff:ff:ff:ff:ff
inet < External Gateway >/26 brd < External Boradcast > scope global qg-c40e9517-79
valid_lft forever preferred_lft forever
inet < Floating IP >/32 brd < Floating Boradcast > scope global qg-c40e9517-79
valid_lft forever preferred_lft forever
inet6 fe80::f816:3eff:fe75:6472/64 scope link
valid_lft forever preferred_lft forever
53: qr-2ee3d6b2-2c: mtu 1500 qdisc noqueue state UNKNOWN qlen 1000
link/ether fa:16:3e:72:b8:41 brd ff:ff:ff:ff:ff:ff
inet < Privetnet Gateway >/23 brd < Privetnet Boardcast > scope global qr-2ee3d6b2-2c
valid_lft forever preferred_lft forever
inet6 fe80::f816:3eff:fe72:b841/64 scope link
valid_lft forever preferred_lft forever
这里看到其实Floating IP是被绑定到qg-xxxx网卡上。我们都知道,浮动IP是通过DNAT转发到虚拟机实际的IP上。
接下来就看看iptables的规则
$ ip netns exec qrouter-7d233b38-fca8-44c0-9752-9350589a0af1 iptables -t nat -S
# Floating IP 出口流量的DNAT规则,
-A neutron-l3-agent-OUTPUT -d < Floating IP >/32 -j DNAT --to-destination < VM IP >
# 进出qg-c40e9517-79网卡、状态非NDAT的流量都通过
-A neutron-l3-agent-POSTROUTING ! -i qg-c40e9517-79 ! -o qg-c40e9517-79 -m conntrack ! --ctstate DNAT -j ACCEPT
# metadata的转发流量,丢到本地9697端口处理
-A neutron-l3-agent-PREROUTING -d 169.254.169.254/32 -i qr-+ -p tcp -m tcp --dport 80 -j REDIRECT --to-ports 9697
# Floating IP 入口流量的DNAT规则,
-A neutron-l3-agent-PREROUTING -d < Floating IP >/32 -j DNAT --to-destination < VM IP >
# 接管虚拟机SNAT规则
-A neutron-l3-agent-float-snat -s < VM IP >/32 -j SNAT --to-source < External Gateway >
-A neutron-l3-agent-snat -j neutron-l3-agent-float-snat
# Privenetwork的SNAT规则
-A neutron-l3-agent-snat -o qg-c40e9517-79 -j SNAT --to-source < External Gateway >
-A neutron-l3-agent-snat -m mark ! --mark 0x2/0xffff -m conntrack --ctstate DNAT -j SNAT --to-source < External Gateway >
-A neutron-postrouting-bottom -m comment --comment "Perform source NAT on outgoing traffic." -j neutron-l3-agent-snat
折腾的地方
如果这样就解决了,那也太简单,这篇文章也没任何意义。
Floating IP不通
刚开始我以为绑定好Floating IP后,就能通过公网登录虚拟机。不!其实是不通的。
问题的表象是:
1. 本地ping不通公网的< Floating IP >
2. 本地能ping通< External Gateway >
3. 在Router的NS里面也ping不通< Floating IP >
4. 在Router的NS里面能ping通公网的物理网关
1. 刚开始以为是ovs的创建的port问题,于是手动通过ovs创建一个网卡绑定公网ip,发现问题不在于此。
过程如下:
# 创建ovs port
$ ovs-vsctl add-port br-ex test -- set interface test type=internal
# 创建测试namespaces
$ ip netns add teset
$ ip link set test netns test
$ ip netns exec test ip link set test up
# 配置公网ip和路由
$ ip addr add < 公网IP01/cidr > dev test
$ ip router add default via < 公网网关 > dev test
$ ip addr add < 公网IP02>/32 dev test
结果发现配置在test网卡上的公网IP01
和公网IP02
都能在本地ping通,说明问题不在ovs的端口创建上。
2. 防火墙
在防火墙上创建了icmp的规则,仍然不通。
3. Router的iptables
我在这里清空了VRouter NS里的nat表,发现本地能够ping通< Floating IP >了,说明本地到VRouter的ovs端口的链路是正常的,问题应该出在转发上。
于是恢复nat表规则,果然< Floating IP >又ping不通了。
4. 虚拟机抓包
在物理机上抓虚拟机网卡,发现ping < Floating IP >。虚拟机能够响应icmp请求。
18:45:42.098339 fa:16:3e:72:b8:41 > fa:16:3e:40:79:61, ethertype IPv4 (0x0800), length 98: <<本地公网ip>> > << 虚拟机ip >>: ICMP echo request, id 47876, seq 2, length 64
18:45:42.098554 fa:16:3e:40:79:61 > 48:7a:da:f6:ce:27, ethertype IPv4 (0x0800), length 98: << 虚拟机ip >> > <<本地公网ip>>: ICMP echo reply, id 47876, seq 2, length 64
这里看到了虚拟机reply了icmp的请求,但是我本地ping并没有回包,说明问题出在了网关上。
5. 重置网关
问题已经清楚了,虚拟机在回包的时候发到物理交换机的网关了,VRouter没有收到回包,当然ping不通了。解决这个问题比较简单。
在虚拟机里面把默认网关指向router里的< Privetnet Gateway >,这个时候在本地到Floating IP的链路就通了。也可以通过ssh < Floating IP >登录到虚拟机。
虚拟机内网不通
虽然能够通过Floating IP访问虚拟机了,但是之前虚拟机的默认网关在物理机交换机上,是和公司内网打通的。由于虚拟机改了默认网关到虚拟机路由器,而VRouter里面默认网关是< 公网网关 >,所以此时虚拟机网络不通。
刚开始我有两个解决思路是:
1. 通过iptables标识来源报文,通过标识转发流量
在虚拟机iptables的mangle表中INPUT chains里添加一个mark,不是内网的cidr的报文通过是set-mark 10
在虚拟机iptables的nat表中OUTPUT chins里匹配mark 10的报文,如果匹配这forward到< Privetnet Gateway >
这个方法开始我觉得理论上是可行的,但一直没测试成功,便放弃了。
2. 通过snat隐藏公网ip
- 在router命名空间的iptables里,将来源公网地址SNAT成< Privetnet Gateway >
这个方法可行,但是这样对虚拟机来说就隐藏了来源的公网地址,这不符合我的需求,放弃了。
正确的解决思路
上面两个方法我一开始就想多了,其实简单点,直接通过路由方式就能满足需求,但是需要引入人工配置,不太方便。
- 在router的namespace里面创建静态路由,匹配目的地址是内网的,转发到物理机的网关上。
neutron router-update router --routes type=dict list=true destination=< 内网CIDR >,nexthop=< 物理交换机网关 >
这样虚拟机到内网环境就通了,但是内网到虚拟机仍然不通。
- 将静态路由配置在虚拟机里
vm $ ip router add < 内网CIDR > via < 物理交换机网关 > dev eth0
这样虚拟机的内网就打通了,同时公网的访问就通过默认的VRouter出去。
至此,虚拟机内网和公网的网络就打通了。
总结
这么蛋疼的网络结构主要原因是二层的网关引入了物理交换机,造成VRouter绑定虚拟机网络时不能够充当网关角色。
这个问题困扰我两台,其实总结了下,有两点需要人工干预:
- 将虚拟机默认网关指向VRouter
- 将虚拟机内网路由到物理交换机网关
感想
由于在未引入DVR时,Neutron的南北流量是通过网络节点的router出去的,在大流量的情况下会成为瓶颈,所以在开始做OpenStack网络规划的时候就没有考虑采用L3-Agent,这导致后来在做Floating IP的时候给自己挖了坑。
其实说白了,还是要看自己公司的网络需求和网络的运维成本。
如果只是将虚拟机接入公司内部二层网络,采用ml2 vlan + 物理网关的方案是可以行。这种方案不好的地方就是在有上层的访问策略需要人在交换机或路由器上配置,运维成本高,但是优点是简单。
虚拟机网关采用l3 router也是可行的,代价是纯软件的router转发效率没有物理交换机高,同时会引入DVR和HA来解决南北向流量和高可用的问题,配置,架构比较复杂。优点也很明显,运维成本低,而且所有内部网络可控,Neutron提供丰富的上层访问策略来限制网络,解放了人在交换机上的操作。