OVS可与内核的连接跟踪系统(Connection tracking system)一同使用,借助Conntrack的功能,OpenFlow流可用于匹配TCP、UDP、ICMP等连接的状态。(连接跟踪系统支持跟踪有状态和无状态协议)。
本教程演示OVS如何使用连接跟踪系统。以匹配从连接建立到连接拆除的TCP报文段。将OVS与Linux内核模块一同作为此演示的数据路径。(在Linux内核中利用openvswitch模块执行的数据处理)。
此演示在Open vSwitch的“master”分支进行了测试。
conntrack:连接跟踪模块。用于有状态报文的检查。
pipeline:报文处理管道。它是当报文遍历表时所经过的路径,报文需要与表中的某个流的匹配字段一致,并且执行此流中规定的动作。
网络命名空间:是在单个Linux内核中,创建多个虚拟路由域的方法。每个网络命名空间都有自己的网络表实例(arp、路由),以及连接的特定接口。
流:本教程中使用的是OpenFlow流,它可以使用OpenFlow控制器或OVS命令行工具编程,如这里使用的ovs-ofctl工具。流具有匹配字段和动作字段。
OVS支持与Conntrack相关的以下匹配字段:
匹配报文的连接状态. 可能的值有:
- *new*
- *est*
- *rel*
- *rpl*
- *inv*
- *trk*
- *snat*
- *dnat*
每个以"+“为前缀的标志,表示必须设置,或者以”-"为前缀的标志表示不能设置。还可以指定多个标志,例如ct_state=+trk+new。下面我们将看到其中一些标志的用法。详细的说明,请参阅OVS字段文档.
由最近的ct action(通过位于Conntrack条目上的OpenFlow 流)设置的16位ct_zone
值可以用作另一个流量条目的匹配字段。
ct_mark:由ct action的 exec参数中的action设置到当前报文所属的连接中的32位元数据。
ct_label:
由exec参数内的操作提交到的128位标签CT动作,到当前数据包的连接属于。
ct_label:
由ct action的 exec参数中的action设置到当前报文所属的连接中的128位标签.
ct_nw_src / ct_ipv6_src:
匹配IPv4/IPv6连接跟踪原始方向元组的源地址。
ct_nw_dst / ct_ipv6_dst:
匹配IPv4/IPv6连接跟踪原始方向元组的目标地址。
ct_nw_proto:
匹配连接跟踪原始方向元组的IP协议类型。
ct_tp_src:
匹配连接跟踪原始方向的元组传输层源端口。
ct_tp_dst:
匹配连接跟踪原始方向的元组传输层目的端口。
OVS支持与conntrack连接跟踪相关的"ct"动作。
*ct([argument][,argument...])*
ct action动作将报文送入连接跟踪器.
支持以下的参数:
commit:
将连接提交到连接跟踪模块,该模块对此连接的存储超出报文在管道中的生命周期。
force:
除以上的commit标志外,还可以使用force标志来有效地终止现有连接并在当前方向开始新连接。
table=number:
管道处理一分为二。原始报文将继续以未跟踪数据包的形式处理当前动作action列表。报文的另一个实例将发送到连接跟踪程序,之后它将重新注入OpenFlow管道并继续在表number
中处理,此时其已设置了ct_state状态和其它ct match匹配字段。
zone=value 或 zone=src[start…end]:
一个16位的上下文ID,可以将连接隔离在单独的域,允许在不同区域使用重叠的网络地址。如果未提供区域值,则默认为使用区域0。
exec([action][,action…]):
在连接跟踪上下文中执行受限制的动作集。在exec
的动作列表中只接受修改ct_mark 或 ct_label字段的动作。
alg=
指定alg(Application Layer Gateway 应用层网关)以跟踪特定连接类型。
nat:
指定所跟踪的连接的NAT翻译地址和端口。
本教程使用以下拓扑来执行测试。
+ +
| |
| +-----+ |
| | | |
| | | |
| +----------+ | OVS | +----------+ |
| | left | | | | right | |
| | namespace| | | |namespace | |
+-----+ A +------+ +-----+ B +--------+
| | | A'| | B' | | |
| | | | | | | |
| +----------+ | | +----------+ |
| | | |
| | | |
| | | |
| +-----+ |
| |
| |
+ +
192.168.0.X n/w 10.0.0.X n/w
A = veth_l1
A' = veth_l0
B = veth_r1
B' = veth_r0
创建以上拓扑的步骤如下所述。
创建 “left” 网络命名空间:
$ ip netns add left
创建 “right” 命名空间:
$ ip netns add right
创建第一对veth接口:
$ ip link add veth_l0 type veth peer name veth_l1
将 veth_l1 添加到 “left” 网络命名空间:
$ ip link set veth_l1 netns left
创建第二对veth接口:
$ ip link add veth_r0 type veth peer name veth_r1
将 veth_r1 添加到 “right” 网络命名空间:
$ ip link set veth_r1 netns right
创建网桥 br0:
$ ovs-vsctl add-br br0
将 veth_l0 和 veth_r0 接口添加到网桥 br0::
$ ovs-vsctl add-port br0 veth_l0
$ ovs-vsctl add-port br0 veth_r0
在"left"网络命名空间生成的源/目的 IP地址分别为192.168.0.x / 10.0.0.x的数据包,以及在"right"网络命名空间生成的相反方向的数据包都将出现在OVS交换机中,就好像两个网络(192.168.0.X 和 10.0.0.X)中的主机在通信。
这在基本上模拟了两个网络或子网中的主机之间通过中间的OVS进行通信的情况。
注意:
一对veth接口即可实现两个网络命名空间之间的通信,本例仅是演示。
可以使用scapy生成TCP报文。我们在Ubuntu 16.04上使用了scapy用于本测试中执行的步骤。(scapy的安装,不在本文讨论范围内)。
你可以在每个名称空间上保持两个活动的scapy会话:
$ sudo ip netns exec left sudo `which scapy`
$ sudo ip netns exec right sudo `which scapy`
注意: 如果你碰到以下的错误:
ifreq = ioctl(s, SIOCGIFADDR,struct.pack("16s16x",LOOPBACK_NAME))
IOError: [Errno 99] Cannot assign requested address
执行此命令:
$ sudo ip netns exec sudo ip link set lo up
在OVS中可以添加两个简单的流,这两个流将转发从"left"命名空间到"right"命名空间,以及从"right"到"left"的数据包:
$ ovs-ofctl add-flow br0 \
"table=0, priority=10, in_port=veth_l0, actions=veth_r0"
$ ovs-ofctl add-flow br0 \
"table=0, priority=10, in_port=veth_r0, actions=veth_l0"
除了添加这两个流之外,我们还将添加与TCP报文的状态匹配的流。
我们将发送TCP连接建立报文,即:位于"left"网络命名空间的主机192.168.0.2与位于"right"命名空间的主机10.0.0.2之间的syn, syn-ack 和 ack报文。
首先,我们添加一个流来启动"tracking"跟踪OVS中接收到的报文。
如何开始跟踪报文呢?
要开始跟踪报文,首先需要匹配动作为"ct"的流。此动作发送报文到连接跟踪器。要确定报文是一个"untracked"未跟踪的报文,流匹配字段的ct_state
必须设置为“-trk”,即其不是跟踪的报文。一旦报文被发送到连接跟踪器,那么我们唯一可知的就是其连接跟踪状态。(即,此报文是否代表一个新的连接或报文属于现有连接或格式不正确的报文等等。)
我们添加下面的流:
(flow #1)
$ ovs-ofctl add-flow br0 \
"table=0, priority=50, ct_state=-trk, tcp, in_port=veth_l0, actions=ct(table=0)"
从"left"命名空间发送的TCP SYN报文将与流#1匹配,因为数据包从veth_l0端口进入OVS,并且还没有被跟踪。(因为报文刚刚进入OVS。所有首次进入OVS的报文都是"untracked")。
由于配置的"ct"动作,流将把报包发送到连接跟踪器。"ct"动作中的参数"table=0"将管道处理分为两部分。原始的报文实例将作为"untracked"报文继续处理当前动作列表。(由于在此之后没有任何动作,原始报文将被丢弃。)。
另一个分叉的报文实例将发送到连接跟踪器,之后将重新注入到OpenFlow管道继续在指定的流表中处理,此时已设置了ct_state状态值和其它ct匹配字段。在以上的情况下,带有ct_state状态和其它ct匹配字段的报文将返回到表0。
接下来,我们添加一个流来匹配从conntrack返回的报包:
(flow #2)
$ ovs-ofctl add-flow br0 \
"table=0, priority=50, ct_state=+trk+new, tcp, in_port=veth_l0, actions=ct(commit),veth_r0"
既然报文是从连接跟踪返回,ct_state状态值应设置了"trk"。
此外,如果这是TCP连接的第一个报文,则ct_state状态也应设置了"new"标志。(正是当前的情况,因为不存在任何192.168.0.2和10.0.0.2之间的TCP连接)ct
参数"commit"将把连接提交到连接跟踪模块。这一动作的意义在于连接信息将被存储在连接跟踪模块,并且将超出数据包在管道中的生存期限。
我们使用scapy发送TCP SYN报文(位于"left"命名空间的scapy会话)(flags=0x02 is syn):
$ >>> sendp(Ether()/IP(src="192.168.0.2", dst="10.0.0.2")/TCP(sport=1024, dport=2048, flags=0x02, seq=100), iface="veth_l1")
此报文将匹配 flow #1 和 flow #2.
连接跟踪模块conntrack将有此连接的项:
$ ovs-appctl dpctl/dump-conntrack | grep "192.168.0.2"
tcp,orig=(src=192.168.0.2,dst=10.0.0.2,sport=1024,dport=2048),reply=(src=10.0.0.2,dst=192.168.0.2,sport=2048,dport=1024),protoinfo=(state=SYN_SENT)
注意:在这个阶段,如果重新传输TCP SYN报文,它将再次匹配流 #1(因为新报文总是未跟踪的),并且它也将匹配流 #2。它与流 #2匹配的原因是,尽管conntrack有关于此连接的信息,但它不处于"ESTABLISHED"状态,因此再次匹配"new"状态。
接下来,对于来自相反/服务器方向的TCP SYN+ACK报文,我们需要以下的OVS流:
(flow #3)
$ ovs-ofctl add-flow br0 \
"table=0, priority=50, ct_state=-trk, tcp, in_port=veth_r0, actions=ct(table=0)"
(flow #4)
$ ovs-ofctl add-flow br0 \
"table=0, priority=50, ct_state=+trk+est, tcp, in_port=veth_r0, actions=veth_l0"
flow #3 匹配未跟踪的由服务器(10.0.0.2)发回的报文,并且将此报文发送到conntrack. (另外, 我们可以将flow #1与flow #3合并,方法是去掉"in_port"匹配字段)
TCP SYN+ACK报文经过conntrack处理后,其ct_state设置了"est"连接建立标志。
注意:当conntrack看到双向流量后,将连接的ct_state设置为"est"状态,但它还没有看到客户端的第三个ACK报文,它在conntrack的此条目上配置一个短时的清除计时器。
使用scapy发送TCP SYN+ACK报文(在"right"命名空间的scapy会话中)(标志=0x12为ACK和SYN):
$ >>> sendp(Ether()/IP(src="10.0.0.2", dst="192.168.0.2")/TCP(sport=2048, dport=1024, flags=0x12, seq=200, ack=101), iface="veth_r1")
此报文将匹配 flow #3 和 flow #4.
conntrack 条目:
$ ovs-appctl dpctl/dump-conntrack | grep "192.168.0.2"
tcp,orig=(src=192.168.0.2,dst=10.0.0.2,sport=1024,dport=2048),reply=(src=10.0.0.2,dst=192.168.0.2,sport=2048,dport=1024),protoinfo=(state=ESTABLISHED)
在只接收到SYN和SYN ACK报文后,conntrack的状态成为"ESTABLISHED"。但此时,如果它没有收到第三个ACK报文(来自客户端),此连接很快会从conntrack中清除。
接下来,对于来自客户端方向的TCP ACK报文,我们可以添加以下流匹配此报文:
(flow #5)
$ ovs-ofctl add-flow br0 \
"table=0, priority=50, ct_state=+trk+est, tcp, in_port=veth_l0, actions=veth_r0"
使用scapy发送第三个TCP ACK报文(位于"left"命名空间的scapy会话)(flags=0x10 为 ACK):
$ >>> sendp(Ether()/IP(src="192.168.0.2", dst="10.0.0.2")/TCP(sport=1024, dport=2048, flags=0x10, seq=101, ack=201), iface="veth_l1")
此报文将匹配 flow #1 和 flow #5.
conntrack 条目:
$ ovs-appctl dpctl/dump-conntrack | grep "192.168.0.2"
tcp,orig=(src=192.168.0.2,dst=10.0.0.2,sport=1024,dport=2048), \
reply=(src=10.0.0.2,dst=192.168.0.2,sport=2048,dport=1024), \
protoinfo=(state=ESTABLISHED)
conntrack状态保持在"ESTABLISHED"状态,但现在它已经收到来自客户端的ACK,即使没有接收此连接上的任何数据,其也将保持此状态很长时间。
当携带一个字节载荷的TCP数据段从192.168.0.2发送到10.0.0.2时,携带该段数据的报文将匹配流 #1,和之后的流 #5。
使用scapy发送一个字节的TCP报文段(位于"left"命名空间的scapy会话)(flags=0x10 is ack)::
$ >>> sendp(Ether()/IP(src="192.168.0.2", dst="10.0.0.2")/TCP(sport=1024, dport=2048, flags=0x10, seq=101, ack=201)/"X", iface="veth_l1")
使用scapy发送以上报文段的ACK回复(位于"right"命名空间scapy会话)(flags=0x10 is ack)::
$ >>> sendp(Ether()/IP(src="10.0.0.2", dst="192.168.0.2")/TCP(sport=2048, dport=1024, flags=0X10, seq=201, ack=102), iface="veth_r1")
数据报文的ACK回复应匹配 flow #3 and flow #4.
有不同的方法可以拆除TCP连接。我们通过从客户端发送"FIN"报文,服务器回复"FIN+ACK"报文,接着客户端发送最后的"ACK"报文的方式拆除连接。
从客户端到服务器的所有报文都会匹配Flow #1和Flow #5。从服务器到客户端的所有报文都会匹配Flow #3和Flow #4。值得注意的一点是,即使TCP连接正在进行
拆除,所有报文(实际上正在拆除连接)仍然匹配"+est"状态。一个报文,如果其conntrack条目是或者曾经是"ESTABLISHED"状态,应继续匹配OVS的ct_state的"+est"标志。
注意:实际上,当conntrack连接状态为"TIME_WAIT"状态时(交换了所有TCP连接拆除所需的FIN和ACK报文后),一个重新传输的报文(从192.168.0.2->10.0.0.2),仍然命中流量 #1和 #5。
使用scapy发送TCP FIN报文("left"命名空间的scapy会话)(flags=0x11 为 ACK 和 FIN):
$ >>> sendp(Ether()/IP(src="192.168.0.2", dst="10.0.0.2")/TCP(sport=1024, dport=2048, flags=0x11, seq=102, ack=201), iface="veth_l1")
此报文匹配 flow #1 和 flow #5.
conntrack 条目:
$ sudo ovs-appctl dpctl/dump-conntrack | grep "192.168.0.2"
tcp,orig=(src=192.168.0.2,dst=10.0.0.2,sport=1024,dport=2048),reply=(src=10.0.0.2,dst=192.168.0.2,sport=2048,dport=1024),protoinfo=(state=FIN_WAIT_1)
使用scapy发送TCP FIN+ACK报文("right"命名空间scapy会话)(flags=0x11 为 ACK 和 FIN):
$ >>> sendp(Ether()/IP(src="10.0.0.2", dst="192.168.0.2")/TCP(sport=2048, dport=1024, flags=0X11, seq=201, ack=103), iface="veth_r1")
此报文命中 flow #3 和 flow #4.
conntrack 条目:
$ sudo ovs-appctl dpctl/dump-conntrack | grep "192.168.0.2"
tcp,orig=(src=192.168.0.2,dst=10.0.0.2,sport=1024,dport=2048),reply=(src=10.0.0.2,dst=192.168.0.2,sport=2048,dport=1024),protoinfo=(state=LAST_ACK)
使用scapy发送TCP ACK报文("left"命名空间scapy会话)(flags=0x10 为 ACK):
$ >>> sendp(Ether()/IP(src="192.168.0.2", dst="10.0.0.2")/TCP(sport=1024, dport=2048, flags=0x10, seq=103, ack=202), iface="veth_l1")
此报文命中 flow #1 和 flow #5.
conntrack 条目:
$ sudo ovs-appctl dpctl/dump-conntrack | grep "192.168.0.2"
tcp,orig=(src=192.168.0.2,dst=10.0.0.2,sport=1024,dport=2048),reply=(src=10.0.0.2,dst=192.168.0.2,sport=2048,dport=1024),protoinfo=(state=TIME_WAIT)
下表总结了TCP报文与流匹配字段的关系
+-------------------------------------------------------+-------------------+
| TCP Segment |ct_state(flow#) |
+=======================================================+===================+
| **Connection Setup** | |
+-------------------------------------------------------+-------------------+
|192.168.0.2 → 10.0.0.2 [SYN] Seq=0 | -trk(#1) then |
| | +trk+new(#2) |
+-------------------------------------------------------+-------------------+
|10.0.0.2 → 192.168.0.2 [SYN, ACK] Seq=0 Ack=1 | -trk(#3) then |
| | +trk+est(#4) |
+-------------------------------------------------------+-------------------+
|192.168.0.2 → 10.0.0.2 [ACK] Seq=1 Ack=1 | -trk(#1) then |
| | +trk+est(#5) |
+-------------------------------------------------------+-------------------+
| **Data Transfer** | |
+-------------------------------------------------------+-------------------+
|192.168.0.2 → 10.0.0.2 [ACK] Seq=1 Ack=1 | -trk(#1) then |
| | +trk+est(#5) |
+-------------------------------------------------------+-------------------+
|10.0.0.2 → 192.168.0.2 [ACK] Seq=1 Ack=2 | -trk(#3) then |
| | +trk+est(#4) |
+-------------------------------------------------------+-------------------+
| **Connection Teardown** | |
+-------------------------------------------------------+-------------------+
|192.168.0.2 → 10.0.0.2 [FIN, ACK] Seq=2 Ack=1 | -trk(#1) then |
| | +trk+est(#5) |
+-------------------------------------------------------+-------------------+
|10.0.0.2 → 192.168.0.2 [FIN, ACK] Seq=1 Ack=3 | -trk(#3) then |
| | +trk+est(#4) |
+-------------------------------------------------------+-------------------+
|192.168.0.2 → 10.0.0.2 [ACK] Seq=3 Ack=2 | -trk(#1) then |
| | +trk+est(#5) |
+-------------------------------------------------------+-------------------+
注意:tshark抓包显示的序号和确认序号都是相对的。
(flow #1)
$ ovs-ofctl add-flow br0 \
"table=0, priority=50, ct_state=-trk, tcp, in_port=veth_l0, actions=ct(table=0)"
(flow #2)
$ ovs-ofctl add-flow br0 \
"table=0, priority=50, ct_state=+trk+new, tcp, in_port=veth_l0, actions=ct(commit),veth_r0"
(flow #3)
$ ovs-ofctl add-flow br0 \
"table=0, priority=50, ct_state=-trk, tcp, in_port=veth_r0, actions=ct(table=0)"
(flow #4)
$ ovs-ofctl add-flow br0 \
"table=0, priority=50, ct_state=+trk+est, tcp, in_port=veth_r0, actions=veth_l0"
(flow #5)
$ ovs-ofctl add-flow br0 \
"table=0, priority=50, ct_state=+trk+est, tcp, in_port=veth_l0, actions=veth_r0"