Neutron 理解 (6): Neutron 是怎么实现虚拟三层网络的 [How Neutron implements virtual L3 network]

学习 Neutron 系列文章:

(1)Neutron 所实现的虚拟化网络

(2)Neutron OpenvSwitch + VLAN 虚拟网络

(3)Neutron OpenvSwitch + GRE/VxLAN 虚拟网络

(4)Neutron OVS OpenFlow 流表 和 L2 Population

(5)Neutron DHCP Agent

(6)Neutron L3 Agent

(7)Neutron LBaas

(8)Neutron Security Group

(9)Neutron FWaas 和 Nova Security Group

(10)Neutron VPNaas

Neutron 对虚拟三层网络的实现是通过其 L3 Agent (neutron-l3-agent)。该 Agent 利用 Linux IP 栈、route 和 iptables 来实现内网内不同网络内的虚机之间的网络流量,以及虚机和外网之间网络流量的路由和转发。为了在同一个Linux 系统上支持可能的 IP 地址空间重叠,它使用了 Linux network namespace 来提供隔离的转发上下文。

1. 基础知识

1.1 Linux network namespace

1.1.1 概念和操作

在二层网络上,VLAN 可以将一个物理交换机分割成几个独立的虚拟交换机。类似地,在三层网络上,Linux network namespace(netns) 可以将一个物理三层网络分割成几个独立的虚拟三层网络。

Network namespace (netns)从 Linux 2.6.24 版本开始添加,直到 2.6.29 添加完成。每个 netns  拥有独立的 (virtual)network devices, IP addresses, IP routing tables, /proc/net directory, ports 等等。新创建的 netns 默认只包含 loopback device。除了这个设备,每个 network device,不管是物理的还是虚拟的网卡还是网桥等,都只能存在于一个 netns。而且,连接物理硬件的物理设备只能存在于 root netns。其它普通的网络设备可以被创建和添加到某个 netns。

使用 ip 命令来操作 netns。

#添加 network namespace
ip netnas add <network namespace name>
#Example:
ip netns add nstest #列表所有 netns
ip netns list #删除某 netns
ip netns delete <network namespace name> #在 network namespace 中运行命令
ip netns exec <network namespace name> <command>
#Example using the namespace from above:
ip netns exec nstest ip addr #添加 virtual interfaces 到 network namespace
ip link add veth-a type veth peer name veth-b #创建一对虚拟网卡veth-a 和 veth-b,两者由一根虚拟网线连接
#将 veth-b 添加到 network namespace
ip link set veth-b netns nstest #设置 VI 的 IP 地址
#defaut namespace 中
ip addr add 10.0.0.1/24 dev veth-a
ip link set dev veth-a up # namespace nstest 中
ip netns exec nstest ip addr add 10.0.0.2/24 dev veth-b
ip netns exec nstest ip link set dev veth-b up
#互通
# ping 10.1.1.1
PING 10.1.1.1 (10.1.1.1) 56(84) bytes of data.
64 bytes from 10.1.1.1: icmp_seq=1 ttl=64 time=0.087 ms # ip netns exec netns1 ping 10.1.1.2
PING 10.1.1.2 (10.1.1.2) 56(84) bytes of data.
64 bytes from 10.1.1.2: icmp_seq=1 ttl=64 time=0.054 ms
#查看路由表和 iptbales
# ip netns exec netns1 route
# ip netns exec netns1 iptables -L

1.1.2 namespace 间的通信

(1)一种简单的方式是使用 Linux veth pair 来实现两个 network namespace 之间的通信:

Neutron 理解 (6): Neutron 是怎么实现虚拟三层网络的  [How Neutron implements virtual L3 network]

(2)当有两个以上的 network namespace 之间需要通信时,需要使用一个虚机交换机,和两个 veth pair。传统的方式是 Linux bridge:

Neutron 理解 (6): Neutron 是怎么实现虚拟三层网络的  [How Neutron implements virtual L3 network]

你也可以使用 Open vSwitch:

Neutron 理解 (6): Neutron 是怎么实现虚拟三层网络的  [How Neutron implements virtual L3 network]

(3)再一种方式是使用 Open vSwitch 和 OVS internal ports:

Neutron 理解 (6): Neutron 是怎么实现虚拟三层网络的  [How Neutron implements virtual L3 network]

来源,可见详细的配置命令。)

veth (irtual Ethernet interfaces) 设备:这是一种成对出现的特殊网络设备,它们象一根管道一样连接在一起。VETH 设备总是成对出现,送到一端请求发送的数据总是从另一端以请求接受的形式出现。该设备不能被用户程序直接操作,但使用起来比较简单。创建并配置正确后,向其一端输入数据,VETH 会改变数据的方向并将其送入内核网络核心,完成数据的注入。在另一端能读到此数据。

关于几种方式的性能比较,这篇文章也给出了它的测试结论:

  • 使用 OVS patch ports:性能更好
  • 不要使用 Linux veth pairs:它会带来明显的性能下降

在 Neutron 中,可以使用配置项 ovs_use_veth 来配置是否使用 veth,默认为 false,表示默认使用 OVS internal port。

1.2 iptables

1.2.1 netfilter/iptables 基本概念

netfilter/iptables(简称为iptables)组成 Linux 平台下的包过滤防火墙。其中,iptables 是一个 linux 用户空间(userspace)模块,位于/sbin/iptables,用户可以使用它来操作防火墙表中的规则。真正实现防火墙功能的是 netfilter,它是一个 linux 内核模块,做实际的包过滤。实际上,除了 iptables 以外,还有很多类似的用户空间工具。

这篇文章 详细介绍了 iptables/netfilter 的概念:

Neutron 理解 (6): Neutron 是怎么实现虚拟三层网络的  [How Neutron implements virtual L3 network]       Neutron 理解 (6): Neutron 是怎么实现虚拟三层网络的  [How Neutron implements virtual L3 network]

  • Netfilter 是一套数据包过滤框架,在处理 IP 数据包时 hook 了5个关键钩子。通过这5个关键点来实现各种功能,比如firewall/ips。
  • ip_tables 是真正的内核防火墙模块,通过把自己的函数注入到 Netfilter 的框架中来实现的防火墙功能.
  • Netfilter 提供了最基本的底层支撑,具体的功能实现只要注册自己的函数就可以了,这样保证了协议栈的纯净与可扩展性.通过上图可以看出 netfilter 与 iptables是分离的.

数据包处理过程:

  1. 数据包从左边进入IP协议栈,进行 IP 校验以后,数据包被第一个钩子函数 PRE_ROUTING 处理。
  2. 然后就进入路由模块,由其决定该数据包是转发出去还是送给本机。
  3. 若该数据包是送给本机的,则要经过钩子函数 LOCAL_IN 处理后传递给本机的上层协议;若该数据包应该被转发,则它将被钩子函数 FORWARD 处理,然后还要经钩子函数 POST_ROUTING 处理后才能传输到网络。
  4. 本机进程产生的数据包要先经过钩子函数 LOCAL_OUT 处理后,再进行路由选择处理,然后经过钩子函数POST_ROUTING处理后再发送到网络。

netfilter 使用表(table)和 链(chain)来组织网络包的处理规则(rule)。它默认定义了以下表和链:

Neutron 理解 (6): Neutron 是怎么实现虚拟三层网络的  [How Neutron implements virtual L3 network]

表功能 链功能
raw  
PREROUTING         
OUTPUT

RAW 拥有最高的优先级,它使用PREROUTING和OUTPUT两个链,因此 RAW 可以覆盖所有包。在raw表中支持一个特殊的目标:TRACE,使内核记录下每条匹配该包的对应iptables规则信息。使用raw表内的TRACE target 即可实现对iptables规则的跟踪调试。比如:

# iptables -t raw -A OUTPUT -p icmp -j TRACE
# ipt ables -t raw -A PREROUTING -p icmp -j TRACE

Filter 包过滤
FORWARD
过滤目的地址和源地址都不是本机的包
INPUT
过滤目的地址是本机的包
OUTPUT
过滤源地址是本机的包
Nat 网络地址转换               
PREROUTING
在路由前做地址转换,使得目的地址能够匹配上防火墙的路由表,常用于转换目的地址。
POSTROUTING
在路由后做地址转换。这意味着不需要在路由前修改目的地址。常用语转换源地址。
OUTPUT

对防火墙产生的包做地址转换(很少量地用于 SOHO 环境中)

Mangle TCP 头修改
PREROUTING
POSTROUTING
OUTPUT
INPUT
FORWARD
在路由器修改 TCP 包的 QoS(很少量地用在 SOHO 环境中)

每个注册的 Hook 函数依次调用各表的链的规则来处理网络包:

Neutron 理解 (6): Neutron 是怎么实现虚拟三层网络的  [How Neutron implements virtual L3 network]

来源:Ethernet bridging hooks )

  • PREROUTING Hook 依次调用 Managle 和 Nat 的 PREOUTING 链中的规则来处理网络包
  • LOCAL_IN Hook 依次调用  MANGLE 和 Filter 的 INPUT 链中的规则来过滤网络包
  • LOCAL_OUT Hook 依次调用 Mangle,Nat,Filter 表的 Output 链中的规则来过滤网络包
  • FORWARD Hook 依次调用 Mangle 和 Filter 表的 FORWARD 链中的规则来过滤网络包
  • POST_ROUTING Hook 依次调用 Managle 和 Nat 表的 POSTROUTING 链中的规则来处理网络包

对 Neutron Virtual Router 所使用的 filter 表来说,它的三个链 INPUT, FORWARD, 和 OUTPUT 是分开的。一个数据包,根据其源和目的地址,只能被其中的某一个处理。

  • 如果数据包的目的地址是本机,那它被 INPUT 处理。
  • 如果数据包的源地址是本机,那它被 OUTPUT 处理。
  • 如果源地址和目的地址都是别的机器,那它被 FORWARD 链处理。

图中的 ”路由判断“ 即判断包的目的地址。如果目的地址不是本机的地址,那它就是需要被路由的包;如果目的地址是本机的,那么被filter 的 INPUT 处理后,被主机的某个程序处理。该程序如果需要发回响应包的话,其源地址肯定是本机的,所有它会被 filter 的 OUTPUT 链处理。该包不一定会出网卡,因为可能会走 loopback,它又会回到本机,重新走封包进入的过程。

1.2.2 iptables

iptables 是一个 CLI 类型的 Linux 用户空间工具,它使得系统管理员能够配置netfile 表(tables)中的链和规则。Linux 使用不同的内核模块和应用来管理不同的网络协议iptable 适用于 ipv4,ip6tables 适用于 ipv6,arptables 适用于 ARP,ebtables 适用于网络帧。iptales 需要管理员权限。

规则(rules)其实就是网络管理员预定义的条件,规则一般的定义为“如果数据包头符合这样的条件,就这样处理这个数据包”。规则存储在内核空间的信息包过滤表中,这些规则分别指定了源地址、目的地址、传输协议(如TCP、UDP、ICMP)和服务类型(如HTTP、FTP和SMTP)等。当数据包与规则匹配时,iptables就根据规则所定义的方法来处理这些数据包,如放行(accept)、拒绝(reject)和丢弃(drop)等。配置防火墙的主要工作就是添加、修改和删除这些规则。

操作 iptables 服务:

# /etc/init.d/iptables start/stop/restart

iptables 各命令选项:

Neutron 理解 (6): Neutron 是怎么实现虚拟三层网络的  [How Neutron implements virtual L3 network]

引用自 http://fishcried.com/2014-08-29/iptable/

  • -p, --protocol protocol: tcp, udp, udplite, icmp, esp, ah, sctp 之一
  • -s, --source address[/mask] a network name, a hostname, a network IP address (with /mask), or a plain IP address.
  • -d, --destination address[/mask][,...]:同 -s
  • -j, --jump target:match 后的 target。
  • -g, --goto chain
  • [!] -i, --in-interface name:连接进来的 interface 名称。!表示否。
  • [!] -o, --out-interface name

其中 -j:

    当数据包进入后,会依次比照 iptables 中的每条规则,直到有一条规则可以对该报文进行匹配,这时该报文将被执行"ACCEPT","DORP","REJECT" 或者其它动作,除 REDIRECT 外,执行完后停止跟余下的 iptables 规则匹配。

  • -ACCEPT: 将封包放行,进行完此处理动作后,将不再比对其它规则,直接跳往下一个规则链。
  • -REJECT:   拦阻该封包,并传送封包通知对方。
  • -DROP: 丢弃封包不予处理,进行完此处理动作后,将不再比对其它规则,直接中断过滤程序。
  • -DNAT:DNAT 改写封包目的地 IP 为某特定 IP 或 IP 范围,可以指定 port 对应的范围,进行完此处理动作后,将会直接跳往下一个规则链。
  • -REDIRECT:    将封包重新导向到另一个端口(PNAT),进行完此处理动作后,将会继续比对其它规则。
  • -SNAT: 改写封包来源 IP 为某特定 IP 或 IP 范围,可以指定 port 对应的范围,进行完此处理动作后,将直接跳往下一个规则链。
  • -RETURN:中断当前链,返回调用链或者默认的policy。

一些例子:

  • iptables -A INPUT -s 10.10.10.10 -j DROP #丢弃从 10.10.10.10 主机来的所有包
  • iptables -A INPUT -s 10.10.10.0/24 -j DROP #丢弃从 10.10.10.0/24 网段进来所有包
  • iptables -A INPUT -p tcp --dport ssh -s 10.10.10.10 -j DROP # 如果协议是 tcp,目标端口是 ssh 端口,源IP 为 10.10.10.10,那么丢弃它
  • iptables -A INPUT -i virbr0 -p udp -m udp --dport 53 -j ACCEPT #接受从 virbr0 进来的所有目标端口 53 的 udp 包
  • iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT #接受 RELEASED 和 ESTABLISHED  状态的连接。Linux 3.7 以后,--state 被替换成了 --conntrack
  • iptables -A FORWARD -d 192.168.122.0/24 -o virbr0 -m state --state RELATED,ESTABLISHED -j ACCEPT #转发时接受这些包
  • iptables -A FORWARD -p icmp -j ACCEPT #转发时接受所有 ICMP 路由包。
  • iptables -A INPUT -i lo -j ACCEPT #使用 -i 过滤从 lo 设备进来的包
  • iptables -A INPUT -i eth0 -j ACCEPT #使用 -i  过滤从网卡 eth0 进来的包。不指定网卡的话表示所有网卡。

封包过滤实现的是针对安全方面的策略,通常我们遵循“凡是没有明确允许的都是禁止的”这样的原则来设计安全策略:首先禁止所有的东西,然后根据需要再开启必要的部分。

关于 iptables 的详细的说明,可以参考 这里,以及 这里 和 这里,以及 这里

Neutron 主要用到 filter 表和 nat 表,其中, filter 用来实现安全组(Security Group)和 防火墙(FWaas);nat 主要用来实现 router。

1.2.3 NAT 的实现

可以使用 iptables nat 表来实现网络地址转换(NAT)。NAT 包括 SNAT (源地址转换)和 DNAT (目的地址转换)。两者的区别在于做地址转换是在路由前还是路由后:

(1)SNAT:路由 - 转换 - 发出

数据经过时, 源地址发生改变,目的地址不变。SNAT 的具体数据流向:

  1. 封包先经过 PREROUTING,检查目的 IP 是不是本网段的地址。是的话,走路径A。
  2. 如果不是,则开始查询路由表,查找到相应路由条目后(查找路由的过程在 PREROUTING 和 FORWARD 之间),经过 FORWARD 链进行转发,再通过 postrouting 时进行NAT转换。

从这里可以看出,SNAT转换的步骤在 POSTROUTING 链上实现, PREROUTING 只是用来做路由选择。因此,要做 SNAT 的话,需要添加 POSTROUTING 规则,使用 “-j SNAT -to-source”。比如:

iptables -t nat -A POSTROUTING -s 192.168.252.0/ -j SNAT -to-source 100.100.100.1

(2)DNAT:转换 - 路由- 发出

DNAT 的功能正好和 SNAT 相反,源地址不变,目的地址发生改变。DNAT 可以用作 PNAT,可以将一个 IP 的端口转换成另一个IP的另外一个端口号,经常用于内网服务器映射到公网,用来隐藏服务器的真实地址。DNAT 的具体数据流向:

  1. 在 DNAT 中,NAT 是在 PREROUTING 上做的。在数据进入主机后,路由选择过程是在 PREROUTING 和 FORWARD 之间的,所以应该先做地址转换之后,再进行路由选择,而后经过 FORWARD,最后从 POSTROUTING 出去。
  2. 因此,要做 DNAT,需要添加 PREROUTING 规则,使用 “-j DNAT --to-destination”。比如:
iptables -t nat -A PREROUTING -d 100.100.100.1 -p tcp --dport  -j DNAT --to-destination 192.168.252.1

Neutron 理解 (6): Neutron 是怎么实现虚拟三层网络的  [How Neutron implements virtual L3 network]

有一类比较特殊的 DNAT 是使用 “-j REDIRECT” 做端口号转换:

## Send incoming port- web traffic to our squid (transparent) proxy
# iptables -t nat -A PREROUTING -i eth1 -p tcp --dport -j REDIRECT --to-port

关于 SNAT 和 DNAT 的更多解释可以参考 这里 和 这里

1.3 route (Linux 路由表)

Linux 系统的 route 命令用于显示和操作 IP 路由表,它的主要作用是创建一个静态路由来指定一个主机或者一个网络通过一个网络接口,如eth0。

route [-CFvnee]

route [-v] [-A family] add [-net|-host] target [netmask Nm] [gw Gw] [metric N] [mss M] [window W] [irtt I] [reject] [mod] [dyn] [rein-
state] [[dev] If] route [-v] [-A family] del [-net|-host] target [gw Gw] [netmask Nm] [metric N] [[dev] If]

例子:

  • route add -net 224.0.0.0 netmask 240.0.0.0 dev eth0 #增加一条经过 eth0 到达 244.0.0.0 的路由
  • route add -net 224.0.0.0 netmask 240.0.0.0 reject #增加一条屏蔽的路由,目的地址为224.x.x.x将被拒绝。
  • route del -net 224.0.0.0 netmask 240.0.0.0
  • route del -net 224.0.0.0 netmask 240.0.0.0 reject
  • route del default gw 192.168.120.240
  • route add default gw 192.168.120.240

这个命令比较简单,可以参考 这个

1.4 路由器的辅助(Secondary) IP

先来看一个 Virutal Router 的 interface 的 ip 配置: 
: qg-3c8d6a68-: <BROADCAST,UP,LOWER_UP> mtu  qdisc noqueue state UNKNOWN group default
link/ether fa::3e:2e:5b: brd ff:ff:ff:ff:ff:ff
inet 192.168.1.110/ brd 192.168.1.255 scope global qg-3c8d6a68-
valid_lft forever preferred_lft forever
inet 192.168.1.104/ brd 192.168.1.104 scope global qg-3c8d6a68-
valid_lft forever preferred_lft forever
inet6 fe80::f816:3eff:fe2e:5b23/ scope link
valid_lft forever preferred_lft forever

<BROADCAST,UP,LOWER_UP>:端口的各种状态

  • UP: device is functioning (enabled 状态,可通过 ip * up/down 设置。)
  • BROADCAST: device can send traffic to all hosts on the link (能够发广播)
  • MULTICAST: device can perform and receive multicast packets (能够发多播)
  • ALLMULTI: device receives all multicast packets on the link (能够接收多播)
  • PROMISC: device receives all traffic on the link (接收所有的traffic)
  • LOWER_UP:the state of the Ethernet link(表示线已接上)

inet/brd/scope:IP 地址及子网掩码,广播地址,作用域

scope:

  • global:valid everywhere
  • site:valid only within this site (IPv6)
  • link:valid only on this device
  • host:valid only inside this host (machine)

注意到这个interface有两个静态 IP 地址。第一个是主要的(primary)IP,第二个是辅助的( secondary) 的 IP。当一个网卡配置了静态IP后,你可以添加secondary  IP 给它。这样它就拥有了多个 IP 地址了。Secondary IP 不可以通过 DHCP 分配。它所有的IP 地址都关联到它唯一的一个 MAC 地址上。那为什么需要 secondary IP 地址呢? 路由器有个 Secondary IP 的概念,这个特性可以创建逻辑子网,也就是说在一个物理网口上连接两个子网,比如这个网口接到一台交换机上,如 果这个网口没有配置Secondary IP的话,那么这台交换机只能连接一个网段的主机,比如 192.168.1.1/24,但是,如果它配置了Secondary IP,那么就可以连接两个网段的主机,比如 192.168.1.1/24 和 10.0.0.1/24。更详细的解释可以看这里 和 这里

命令:

#增加 secondary IP
ip netns exec qrouter-e438bebe--4b68-a613-ec0df38d3064 ip a add dev qg-3c8d6a68- 192.168.1.105/ brd 192.168.1.105 #删除 secondar IP
ip netns exec qrouter-e438bebe-6795-4b68-a613-ec0df38d3064 ip addr del 192.168.1.104/32 dev qg-3c8d6a68-97

1.5 Gratuitous ARP

Gratuitous ARP也称为 免费ARP,无故ARP。Gratuitous ARP不同于一般的ARP请求,它并非期待得到IP对应的MAC地址,而是当主机启动的时候,将发送一个Gratuitous arp请求,即请求自己的IP地址的MAC地址。它常用于三个用途:

  • Change of L2 address:通告自己改变了 MAC 地址。以 ARP Response 的形式发送广播,它通常只是为了把自己的ARP信息通告/更新给局域网全体,这种Response不需要别人请求,是自己主动发送的通告。报文结构如下当一个网络设备的 MAC 地址发生改变时,发送该设备的 Gratuitous ARP,通知所在广播域内的已经包含该 IP 地址在其 ARP 表中的机器去更新它的 ARP 条目。

Neutron 理解 (6): Neutron 是怎么实现虚拟三层网络的  [How Neutron implements virtual L3 network]

  • Duplicate address detection:重复 MAC 地址检测。以 ARP Request的形式发送广播,请求自己的MAC地址,目的是探测局域网中是否有跟自己IP地址相同的主机,也就是常说的IP冲突。发送主机并不需要一定收到此请求的回答。如果收到一个回答,表示网络中存在与自身IP相同的主机。如果没有收到应答,则表示本机所使用的IP与网络中其它主机并不冲突。

Neutron 理解 (6): Neutron 是怎么实现虚拟三层网络的  [How Neutron implements virtual L3 network]

(注意:这两种 ARP 帧虽然都是广播发送的,但目的不同,从帧结构上来说,前者注重的是Target Internet Address,而后者注重的是Sender Hardware Address和Sender Inteernet Address。)

  • Virtual IP:用于一组服务器做 failover 时通知周围的机器新生效的 IP 地址的 MAC。

以上内容引用自 谈谈arp欺骗的那点破事,更多信息,还可以参见 这篇文章 和 这篇文章

2. Neutron L3 Agent 的实现原理

每个 L3 Agent 运行在一个 network namespace 中,每个 namespace 由 qrouter-<router-UUID>命名,比如 qrouter-e506f8fe-3260-4880-bd06-32246225aeae。网络节点如果不支持 Linux namespace 的话,只能运行一个 Virtual Router。也可以通过设置配置项 use_namespaces = True/False 来使用或者不使用 namespace。

Neutron L3 Agent 负责路由(routing)、浮动 IP 分配(floatingip allocation), 地址转换(SNAT/DNAT)和 Security Group 管理(Blueprint 在这里。在后面的文章中打算和 Nova 中的 Security Group 一起分析)。

2.1 Router 作为浮动 IP 地址的ARP Proxy

虚机的浮动 IP 其实不是真实网卡的 IP 地址,而是一个虚拟地址。那么,使用浮动 IP 和虚机通信的机器怎么获得 MAC 地址呢?Router 在这个过程中作为一个 ARP Proxy,其 IP 协议栈会向 ARP 广播请求回应浮动 IP 对应所在的外部端口的 MAC 地址。下面的例子中,该 router 挂接的子网内有两个浮动 IP,L3 Agent 都将它们添加到 Router 的外部端口 qg-3c8d6a68-97 上:

: qg-3c8d6a68-: <BROADCAST,UP,LOWER_UP> mtu  qdisc noqueue state UNKNOWN group default
link/ether fa::3e:2e:5b: brd ff:ff:ff:ff:ff:ff
inet 192.168.1.110/ brd 192.168.1.255 scope global qg-3c8d6a68-
valid_lft forever preferred_lft forever
inet 192.168.1.104/ brd 192.168.1.104 scope global qg-3c8d6a68-
valid_lft forever preferred_lft forever
inet 192.168.1.111/ brd 192.168.1.111 scope global qg-3c8d6a68-
valid_lft forever preferred_lft forever
inet6 fe80::f816:3eff:fe2e:5b23/ scope link
valid_lft forever preferred_lft forever

先插播一句。上图可以看到 qg 下面有个3个 ip 地址,第一个是 VR 自己的 fip,后面两个是后面虚机的 fip。也就是说,要给虚机分配 fip,必须先给 VR 分配 fip,因为这样才能有 qg。

这么做的目的,由于外网中的机器和虚机浮动 IP 是同一个网段的,外网中的机器通过浮动 IP 访问虚机之前,需要通过 ARP 获取该浮动 IP 的 MAC 地址。浮动IP 其实是个虚机IP,没有对应一个网络设备,因此,Neutron 将它们添加到 external port 上,共享 external port 的 MAC 地址。这样,在 router network namespace IP 协议栈收到 ARP 广播后,就可以该 IP 对应 的 MAC 地址,然后外网中的虚机就会使用该 MAC 作为目的 MAC 地址直接向 router 的 external port 端口发送网络帧。查询外网机器的 arp table,即可看到 192.168.1.104 的 MAC 为 qg-3c8d6a68-97 的  MAC 地址。也就是说,外部端口上的所有 IP 的 MAC 地址都相同。这时候,其实 router 充当了一个 ARP Proxy 的角色。

s1@controller:~$ arp
Address HWtype HWaddress Flags Mask Iface
192.168.1.104 ether fa:16:3e:2e:5b:23 C eth0
192.168.1.110 ether fa:16:3e:2e:5b:23 C eth0
192.168.1.111 ether fa:16:3e:2e:5b:23 C eth0

2.2 路由 (Routing)

一个 Virtual Router 连接几个 subnet 就会有几个 virtual interface。每个 interface 的地址是该 subnet 的 gateway 地址。比如:

root@network:/home/s1# ip netns exec qrouter-e438bebe-6795-4b68-a613-ec0df38d3064 ip addr

33: qr-2aa928c9-e8: <BROADCAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default #IP 设为它连接的子网的 Gateway IP
link/ether fa:16:3e:90:e5:50 brd ff:ff:ff:ff:ff:ff
inet 91.1.180.1/24 brd 91.1.180.255 scope global qr-2aa928c9-e8
37: qr-a5c6ed86-c1: <BROADCAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default
link/ether fa:16:3e:87:40:f3 brd ff:ff:ff:ff:ff:ff
inet 81.1.180.1/24 brd 81.1.180.255 scope global qr-a5c6ed86-c1
48: qg-3c8d6a68-97: <BROADCAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default
link/ether fa:16:3e:2e:5b:23 brd ff:ff:ff:ff:ff:ff
inet 192.168.1.110/24 brd 192.168.1.255 scope global qg-3c8d6a68-97

L3 Agent 在启动时设置如下的路由规则:

root@network:/home/s1# ip netns exec qrouter-e438bebe-6795-4b68-a613-ec0df38d3064 route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.1.1 0.0.0.0 UG 0 0 0 qg-3c8d6a68-97
81.1.180.0 0.0.0.0 255.255.255.0 U 0 0 0 qr-a5c6ed86-c1 #到哪个网段的traffic发到相应的 interface
91.1.180.0 0.0.0.0 255.255.255.0 U 0 0 0 qr-2aa928c9-e8
192.168.1.0 0.0.0.0 255.255.255.0 U 0 0 0 qg-3c8d6a68-97

虚机的 IP 栈在发现数据包的目的虚机的 IP 地址不在自己网段的话,会将其发到 Router 上对应其 subnet 的 Virtual Interface。然后,Virtual Router 根据配置的路由规则和目的IP地址,将包转发到目的端口发出。

2.3 源地址转换 SNAT

2.3.1 Neutron 的 SNAT iptables chains

在没有设置浮动 IP 时,当主机访问外网时,需要将主机的固定 IP 转换成外网网段的 gateway 的 IP 地址,以免暴露内部 IP 地址。其做法是 Neutron 在 iptables 表中增加了 POSTROUTING 链。

root@network:/home/s1# ip netns exec qrouter-e438bebe-6795-4b68-a613-ec0df38d3064 iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N neutron-l3-agent-OUTPUT      #Neutorn 增加的 chain
-N neutron-l3-agent-POSTROUTING #Neutorn 增加的 SNAT chain
-N neutron-l3-agent-PREROUTING
-N neutron-l3-agent-float-snat  #Neutorn 增加的 SNAT chain
-N neutron-l3-agent-snat        #Neutorn 增加的 SNAT chain
-N neutron-postrouting-bottom   #Neutorn 增加的 SNAT chain
-A PREROUTING -j neutron-l3-agent-PREROUTING
-A OUTPUT -j neutron-l3-agent-OUTPUT
-A POSTROUTING -j neutron-l3-agent-POSTROUTING #(1)将 SNAT chain 转到自定义的 neutron-l3-agent-POSTROUTING
-A POSTROUTING -j neutron-postrouting-bottom   #(3)将 SNAT chain 转到自定义的 neutron-postrouting-bottom
-A neutron-l3-agent-POSTROUTING ! -i qg-3c8d6a68-97 ! -o qg-3c8d6a68-97 -m conntrack ! --ctstate DNAT -j ACCEPT #(2)如果出口或者入口不是 qg-3c8d6a68-97 并且状态不是 DNAT 的都接受
-A neutron-l3-agent-snat -j neutron-l3-agent-float-snat
-A neutron-l3-agent-snat -s 91.1.180.0/24 -j SNAT --to-source 192.168.1.110 #(5)将 91.1.180.0/24 网段的包的目的 IP 转为 192.168.1.110
-A neutron-l3-agent-snat -s 81.1.180.0/24 -j SNAT --to-source 192.168.1.110 #(5)将 91.1.180.0/24 网段的包的目的 IP 转为 192.168.1.110
-A neutron-postrouting-bottom -j neutron-l3-agent-snat                      #(4)再转到 neutron-l3-agent-snat

2.3.2 实验:从虚机  81.1.180.12 ping 外网 192.168.1.15

1. 在 router 的连接 81.1.180.12 网段 interface 上:

root@network:/home/s1# ip netns exec qrouter-e438bebe--4b68-a613-ec0df38d3064 tcpdump -envi qr-a5c6ed86-c1 -vvv
tcpdump: listening on qr-a5c6ed86-c1, link-type EN10MB (Ethernet), capture size bytes
^C17::48.904820 fa::3e:2b:3e:2a > fa::3e:::f3, ethertype IPv4 (0x0800), length : (tos 0x0, ttl , id , offset , flags [DF], proto ICMP (), length )
81.1.180.12 > 192.168.1.15: ICMP echo request, id , seq , length
::48.906601 fa::3e:::f3 > fa::3e:2b:3e:2a, ethertype IPv4 (0x0800), length : (tos 0x0, ttl , id , offset , flags [none], proto ICMP (), length )
192.168.1.15 > 81.1.180.12: ICMP echo reply, id , seq , length
::49.906238 fa::3e:2b:3e:2a > fa::3e:::f3, ethertype IPv4 (0x0800), length : (tos 0x0, ttl , id , offset , flags [DF], proto ICMP (), length )

2. 在 route 的连接 192.168.1.15 网段 interface 上

root@network:/home/s1# ip netns exec qrouter-e438bebe--4b68-a613-ec0df38d3064 tcpdump -envi qg-3c8d6a68- -vvv
tcpdump: listening on qg-3c8d6a68-, link-type EN10MB (Ethernet), capture size bytes
^C17::47.661916 fa::3e:2e:5b: > :::c7:cf:ca, ethertype IPv4 (0x0800), length : (tos 0x0, ttl , id , offset , flags [DF], proto ICMP (), length )
192.168.1.110 > 192.168.1.15: ICMP echo request, id , seq , length
::47.663300 :::c7:cf:ca > fa::3e:2e:5b:, ethertype IPv4 (0x0800), length : (tos 0x0, ttl , id , offset , flags [none], proto ICMP (), length )
192.168.1.15 > 192.168.1.110: ICMP echo reply, id , seq , length

可见在外网网段的 interface 收到数据包之前,SRC IP 已经被替换成了外网网段的 Gateway IP 了。

2.3.3 关于 SNAT 的一点细节

SNAT 只能用于从内部网络发起的目的是外部网络的连接,而不能相反。

前面分析了内网网络包如何出去,但因为外网主机回复的网络包的目的地址为SNAT 地址,不是真正内网主机的地址,因此回复包如何回到源主机呢?SNAT 的实现会保存一个连接映射表(connection table/mapping table),里面的每条记录保存着该连接的内部(源)address:port和外面(目标)address:port 之间的映射关系。在做 SNAT 时,会往该表中插入一条记录;当回复的包回来以后,会查询该表,找出之前的源也就是现在的目标地址,然后重新构建网络包发给真正的源主机。因为这是地址和端口的组合作为映射的键,如果重复了会怎么办呢?当表中有重复记录时,SNAT 在插入记录时把源端口修改为一个未使用的端口,来保证记录的唯一性。具体可以参考 http://www.commercialventvac.com/finao/DNATs-and-SNATs.html

2.4 目的地址转换 DNAT

要使外网内的机器能访问虚机,需要设置虚机的浮动IP。浮动 IP 在 Virtual Router 连接的 external network 的 subnet 内分配。注意浮动 IP 只有在 Virtual Router 上配置了 External network subnet gateway 才有意义。

2.4.1 浮动IP分配

创建浮动IP:

root@sun:~# neutron floatingip-create Extnet
Created a new floatingip:
+---------------------+--------------------------------------+
| Field | Value |
+---------------------+--------------------------------------+
| fixed_ip_address | |
| floating_ip_address | 10.8.127.11 |
| floating_network_id | 9c9436d4-2b7c-4787-8535-9835e6d9ac8e |
| id | 7b4cee72-ffcd-4484-a5d8-371b23bb3cc3 |

关联到一个 port:

root@sun:~# neutron port-list | grep 192.168.10.26
| d74c703e-824a-41b1-b4b3-3cd4edfa22b3 | | fa:16:3e:14:ff:6d | {"subnet_id": "ccc80588-2b0d-459b-82e9-972ff4291b79", "ip_address": "192.168.10.26"} |
root@sun:~# neutron floatingip-associate 7b4cee72-ffcd-4484-a5d8-371b23bb3cc3 d74c703e-824a-41b1-b4b3-3cd4edfa22b3
+---------------------+--------------------------------------+
| Field | Value |
+---------------------+--------------------------------------+
| fixed_ip_address | 192.168.10.26 |
| floating_ip_address | 10.8.127.11 |

每个浮动 IP 唯一对应一个 Router:浮动IP -> 关联的 Port -> 所在的 Subnet -> 包含该 subnet 以及 external subnet 的 Router。创建浮动 IP 时,在 Neutron 完成数据库操作来分配浮动IP后,它通过 RPC 来通知该浮动IP对应的 router 去设置该浮动IP对应的 iptables 规则。上面的例子中,固定IP 为 ‘192.168.10.26’ 的虚机可以在外网中使用浮动 IP  ‘10.8.127.11’ 来访问了。

<更新 2015/11/27> Kilo 版本中,创建浮动 IP 的时候允许指定浮动IP 地址,这么做就能够对同一个虚机使用同一个浮动IP。

blueprint:Allow specific Floating IP Address

效果:Neutorn floatingip-create cli 上增加了一个参数 --floating-ip-address 用于指定创建的浮动IP地址:

root@controller:~/s1# neutron floatingip-create --port-id 97f3ed61-7f9e-4f0a-91af-e95a572acd9c --floating-ip-address 9.115.251.105 5b4daf62-d992-453f-b74d-8c585365a604
Created a new floatingip:
+---------------------+--------------------------------------+
| Field | Value |
+---------------------+--------------------------------------+
| fixed_ip_address | 70.0.0.105 |
| floating_ip_address | 9.115.251.105 |
| floating_network_id | 5b4daf62-d992-453f-b74d-8c585365a604 |
| id | -e2cf-4dc6-85a3-26a58a493f70 |
| port_id | 97f3ed61-7f9e-4f0a-91af-e95a572acd9c |
| router_id | b94a203d--4d0b--5c65e01bd76f |
| status | DOWN |
| tenant_id | dea8b51d28bf41599e63464828102759 |
+---------------------+--------------------------------------+

默认情况下,只有 admin 才能使用,不过,可以修改 policy.json 文件将其向普通租户开放。

2.4.2 Neturon DNAT Chains

外网访问虚机时,目的 IP 地址为虚机的浮动 IP 地址,因此,必须由 iptables 将其转化为固定 IP 地址,然后再将它路由到虚机。我们需要关注的是 iptables 的 nat 表的 PREOUTING chain:

root@network:/home/s1# ip netns exec qrouter-e438bebe--4b68-a613-ec0df38d3064 iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N neutron-l3-agent-OUTPUT
-N neutron-l3-agent-PREROUTING #neutron 增加的 DNAT chain
-A PREROUTING -j neutron-l3-agent-PREROUTING # DNAT 由 neutron 新增的 chain 负责处理
-A OUTPUT -j neutron-l3-agent-OUTPUT
-A neutron-l3-agent-OUTPUT -d 192.168.1.104/ -j DNAT --to-destination #本机访问浮动IP 修改为固定 IP
-A neutron-l3-agent-PREROUTING -d 169.254.169.254/ -p tcp -m tcp --dport -j REDIRECT --to-ports 9697 #将虚机访问 metadata server 的 traffic 端口由 80 改到 9697(由配置项 metadata_port 设置,默认为 9697),那里有 application 在监听。具体内容很深,可以参考这篇文章
-A neutron-l3-agent-PREROUTING -d 192.168.1.104/ -j DNAT --to-destination 91.1.180.14 #到浮动IP的traffic的目的IP 换成虚机的固定 IP

每个浮动 IP,增加三个规则:

-A neutron-l3-agent-PREROUTING -d <floatingip> -j DNAT --to-destination <fixedip> #从本机访问虚机,Dst IP 由浮动IP该为访问固定IP
-A neutron-l3-agent-OUTPUT -d <floatingip> -j DNAT --to <fixedip> #从别的机器*问虚机,DST IP 由浮动IP改为固定IP
-A neutron-l3-agent-float-snat -s <fixedip> -j SNAT --to <floatingip> #虚机访问外网,将Src IP 由固定IP改为浮动IP

这里可以看到当设置了浮动 IP 以后,SNAT 不在使用 External Gateway 的 IP 了,而是使用浮动 IP 。虽然 entires 依然存在,但是因为 neutron-l3-agent-float-snat 比 neutron-l3-agent-snat 靠前而得到执行。

-A neutron-l3-agent-float-snat -s 91.1.180.14/ -j SNAT --to-source 192.168.1.104
-A neutron-l3-agent-snat -j neutron-l3-agent-float-snat
-A neutron-l3-agent-snat -s 91.1.180.0/ -j SNAT --to-source 192.168.1.110
-A neutron-l3-agent-snat -s 81.1.180.0/ -j SNAT --to-source 192.168.1.110
-A neutron-postrouting-bottom -j neutron-l3-agent-snat

2.4.3 实验:从外网192.168.1.15 ping虚机 81.1.180.14 的浮动IP 192.168.1.104

1. 在route 的连接外网网段的interace 上:

17:58:46.116944 08:00:27:c7:cf:ca > fa:16:3e:2e:5b:23, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 25176, offset 0, flags [DF], proto ICMP (1), length 84)
192.168.1.15 > 192.168.1.104: ICMP echo request, id 24530, seq 4, length 64
17:58:46.117910 fa:16:3e:2e:5b:23 > 08:00:27:c7:cf:ca, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 63, id 23128, offset 0, flags [none], proto ICMP (1), length 84)
192.168.1.104 > 192.168.1.15: ICMP echo reply, id 24530, seq 4, length 64

2. 在 router 的连接内网网段的interface上:

root@network:/home/s1# ip netns exec qrouter-e438bebe-6795-4b68-a613-ec0df38d3064 tcpdump -envi qr-2aa928c9-e8 -vvv
tcpdump: listening on qr-2aa928c9-e8, link-type EN10MB (Ethernet), capture size 65535 bytes
^C19:46:12.266739 fa:16:3e:90:e5:50 > fa:16:3e:f3:1e:c0, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 63, id 53299, offset 0, flags [DF], proto ICMP (1), length 84)
192.168.1.15 > 91.1.180.14: ICMP echo request, id 2831, seq 1, length 64
19:46:12.269143 fa:16:3e:f3:1e:c0 > fa:16:3e:90:e5:50, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 23157, offset 0, flags [none], proto ICMP (1), length 84)
91.1.180.14 > 192.168.1.15: ICMP echo reply, id 2831, seq 1, length 64

2.5 L3 Agent iptables 完整流程实验

该实验中使用 “ iptables -t nat  -L -nv” 命令来查看每个链上匹配到的数据包数目。

2.4.1 虚机 91.1.180.14 ping 另一个虚机 81.1.180.12

Neutron 理解 (6): Neutron 是怎么实现虚拟三层网络的  [How Neutron implements virtual L3 network]

可见:

(1)DNAT 匹配到的是默认的 Policy

(2)SNAT 匹配到 “-A neutron-l3-agent-POSTROUTING ! -i qg-3c8d6a68-97 ! -o qg-3c8d6a68-97 -m conntrack ! --ctstate DNAT -j ACCEPT” 规则后就被 Accept 了。

2.5.1 虚机 91.1.180.14 ping 外网 192.168.1.4

Neutron 理解 (6): Neutron 是怎么实现虚拟三层网络的  [How Neutron implements virtual L3 network]

可见:

(1)DNAT 匹配到的是默认的 Policy ACCEPT

(2)SNAT 匹配到 “-A neutron-l3-agent-float-snat -s 91.1.180.14/32 -j SNAT --to-source 192.168.1.104” 规则后做 SNAT。

2.5.2 外网 192.168.1.4 ping 虚机 192.168.1.104(91.1.180.14)

Neutron 理解 (6): Neutron 是怎么实现虚拟三层网络的  [How Neutron implements virtual L3 network]

可见:

(1)DNAT 匹配到 “-A neutron-l3-agent-PREROUTING -d 192.168.1.104/32 -j DNAT --to-destination 91.1.180.14” 然后做 DNAT。

(2)SNAT 匹配到的是默认的 Policy ACCEPT。

为什么结果中显示的 pacakge 数目只是1呢?参考网上文章,对于 SNAT 和 DNAT target 来说,如果一个包被匹配了,那么和它属于同一个流的所有的包都会被自动转换,然后就可以被路由到正确的主机或网络,这么说来,一个流中被匹配的包的数目就是1了。

总结:

Neutron 理解 (6): Neutron 是怎么实现虚拟三层网络的  [How Neutron implements virtual L3 network]

(图片来源。Neutron 代码分析也可以参考这篇文章。)

3. Neutron L3 Agent 主要代码结构

L3 Agent 启动后,它有若干个 Workers 去 MQ 中拿数据,然后将数据放进一个内部的 queue 中。它还会启动一个循环线程去queue 中取数据。当发现有 router 相关的操作发生后,即调用 _process_routers_loop 方法去处理获取的数据。

3.1 L3 Agent 启动

Neutorn L3 Agent 的 binary 是 /usr/bin/neutron-l3-agent,其main 会初始化一个 class Service(n_rpc.Service) 类的实例。在该实例的 start 函数中,它会启动两个周期性任务:

(1).启动 一个线程来执行循环函数 _process_routers_loop 来处理第二个周期性任务添加到  _queue 中的每一个 router 的 action,包括删除、添加和更新。对每一个待处理的 router,最终会调用到 RouterInfo.process 方法。注意 router 的操作都是在其对应的 namespace 中进行的。在 namespace 创建的时候,执行 'sysctl -w net.ipv4.ip_forward=1' 来使得它能够做 IP 路由转发。

def process(self, agent):
"""Process updates to this router
This method is the point where the agent requests that updates be applied to this router.
:param agent: Passes the agent in order to send RPC messages.
"""
self._process_internal_ports()
self.process_external(agent)
# Process static routes for router
self.routes_updated() # Update ex_gw_port and enable_snat on the router info cache
self.ex_gw_port = self.get_ex_gw_port()
self.snat_ports = self.router.get(
l3_constants.SNAT_ROUTER_INTF_KEY, [])
self.enable_snat = self.router.get('enable_snat')

(2)启动一个周期性函数 periodic_sync_routers_task。它负责通过 RPC 获取到 router 列表,然后将需要增加和删除的 router 加入到 _queue 中。

3.2 Router 处理

L3 Agent 的核心是 Router 的处理。

(1)处理 external gateway (比如,external_gateway_added 增加 external gateway:获取该 router 的所有浮动 IP,在agent_conf.external_network_bridge 所指定的外网物理 OVS bridge 上增加一个 tap 设备,名称为 “gq-*”,然后设置其 MAC 地址,MTU 等)

(2)修改路由表 (routes_updated)

(3)在有 external gateway 时,设置 SNAT iptables (_handle_router_snat_rules):先删除当前所有 POSTROUTING 和 snat chains,然后再增加:

#增加 SNAT chain
-N neutron-l3-agent-float-snat
#为 external gateway
-A neutron-l3-agent-POSTROUTING ! -i qg-3c8d6a68- ! -o qg-3c8d6a68- -m conntrack ! --ctstate DNAT -j ACCEPT #为每一个子网创建一条 SNAT 规则
-A neutron-l3-agent-snat -s 91.1.180.0/ -j SNAT --to-source 192.168.1.110 #()将 91.1.180.0/ 网段的包的目的 IP 转为 192.168.1.110
-A neutron-l3-agent-snat -s 81.1.180.0/ -j SNAT --to-source 192.168.1.110 #()将 91.1.180.0/ 网段的包的目的 IP 转为 192.168.1.110

(4)如果有 external gateway 的话,处理浮动 IP 的 SNAT/DNAT iptables (process_snat_dnat_for_fip)

#为每一个浮动 IP,以 192.168.1.104 为例
-A neutron-l3-agent-PREROUTING -d 192.168.1.104/ -j DNAT --to-destination 91.1.180.14 #DNAT
-A neutron-l3-agent-OUTPUT -d 192.168.1.104/ -j DNAT --to-destination 91.1.180.14 #本机访问
-A neutron-l3-agent-float-snat -s 91.1.180.14/ -j SNAT --to-source 192.168.1.104 #SNAT

(5)将浮动IP 配置到 external gateway port (process_floating_ip_addresses -> add_floating_ip -> _add_fip_addr_to_device)

42: qg-3c8d6a68-97: <BROADCAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default
link/ether fa:16:3e:2e:5b:23 brd ff:ff:ff:ff:ff:ff
inet 192.168.1.110/24 brd 192.168.1.255 scope global qg-3c8d6a68-97
valid_lft forever preferred_lft forever
inet 192.168.1.104/32 brd 192.168.1.104 scope global qg-3c8d6a68-97
valid_lft forever preferred_lft forever

(6)除了上述操作,L3 Agent 还在创建/添加 router 的每一个端口(包括internal 的和 external 的)时发出 Gratuitous  ARP,通知其所在广播域内的其它机器去更新它的 ARP 表中该 IP 条目。比如:

'ip netns exec qrouter-b49a8032-a676-4ef9-aade-355592949aaa arping -A -I qg-e83aae8d-d2 -c 3 192.168.1.100'

其中,“-A” 表示不需要 ARP 返回包;“-I” 表示通过指定的 port 发出包,这样就算只对有需要的subnet 发出包了;“-c 3” 表示尝试3次。

3.3 浮动 IP 处理

在创建、绑定、去绑定或者删除浮动IP时,neutron server 首先执行 DB 操作,然后调用 RPC (notify_routers_updated) 去通知指定的 Router:”create_floatingip“,”update_floatingip“,”delete_floatingip“ 等。

参考链接:

Iptables & Netifilter哪个才是防火墙?

欢迎大家关注我的个人公众号:

Neutron 理解 (6): Neutron 是怎么实现虚拟三层网络的  [How Neutron implements virtual L3 network]

上一篇:MFC开发(一)简单同步时间应用程序


下一篇:jdk_1_8_1