abstract

本文提出了一种通过使用ovs learn来给指定ICMP Request的Response染色的思路,来提供云计算网络中基于OVS的流量染色和采集方案,以实现主动流量探测、请求路径分析、时延分析等功能。

概述

在虚拟化网络中,稳定性决定了网络的质量所在,对于实现一个可靠、稳定、自愈的虚拟化网络,监控是比不可少的一环。

笔者曾在物理网络部门工作过一段时间,深知被厂商深度绑定的网络设备和环境下,系统的整体运行对于工程师们来说呈现一个难以控制的黑盒环境。数据包在大规模、不同厂商、不同系统、不同型号的设备之间转发时,很难回答出如下问题:

  • 这条流的转发路径是哪些?
  • 转发时延是多少?
  • 时延增大或丢包时,究竟故障在哪条链路或者设备上?

所以最近,INT(In-band Network Telemetry)的概念越来越热,可以看到各大厂商都在致力于提升网络的遥测能力。通过在转发数据包的头部打上每一hop的监控信息(节点信息、堆积时延、处理时延等),最终通过发送给专门的分析节点,可以监控、绘制出流的转发路径、时延信息等。

在云计算虚拟化网络中,我们可以更灵活一点,可以根据自己的需求设计overlay的INT方案。对于租户网络而言,我们无法控制和探测的用户流量,这要求我们需要主动发包去探测用户节点之间的网络状况,以回答上述的几个问题,包括:

question

1. 连通是否正常? 2. 时延是否正常? 3. 若发生异常,包丢在哪里?

设计概述

主动探测系统设计中主要考虑因素为如何构造一个请求包,使得VM产生的回包带有一定的“特征”,这种特征使得请求包和响应包可以被关联。通过镜像每一hop上的请求包和响应包,即可分析出流的转发路径等信息。

我们自然的想到,本质是利用一个有状态的协议来完成这样的交互过程。基于TCP的SYN探测,可以自然而然通过五元组作为会话信息,通过分析回包的SA或者RA,即可判断通信和时延等问题。

但由于某种因素制约而如果无法使用TCP时,目光就很自然的放在了ICMP上,甚至应该上来就考虑ICMP的方案。确实ICMP探测更符合直观,但遗憾的是,openflow协议中无法识别icmp的identifier,这导致我们无法关联icmp_request和icmp_reply。

本文讨论的即为如何构造一个染色的icmp_request,并且主机、OS无关的icmp_reply中可以携带该染色信息。采集时,只要将染色的request、reply镜像至分析系统即可。

通过基于ovs的learn动作,我们可以在收到染色的请求包时,动态生成一条用于匹配响应包的flow,该flow通过交换源目IP、指定hard_timeoutidle_timeout来用于最大化的降低采集到用户ICMP流量的可能性。

染色

对ICMP的染色方案比较有限,我们的眼光最终聚焦在ttldscp上。一般来说,这两个字段是OpenflowERSPAN都可以支持的字段,可以有效识出染色流量。本文以IP Header中的dscp字段作为染色标记。

转发面设计

OVS转发面需要在全局hook点中做一些动作,分别hook icmp的request和response报文,进行染色、镜像至分析系统等步骤。

报文交互

整体数据包交互如下图所示:

packet

hook icmp_request

对于hook icmp_request的flow可以简化为如下逻辑:

1
2
cookie=0x20,table=1,priority=40000,metadata=0x1,icmp,icmp_type=8,icmp_code=0,nw_tos=0x40 
actions=Send(),Learn(),Back()

match主要用于匹配染色的(tos=0x40)icmp_request报文。

action部分主要由三部分组成:

  • Send()为镜像给分析系统;
  • Learn()通过icmp_request报文学习到一条用于icmp_reply匹配的报文用于反向染色;
  • Back() 将该报文送回table_0,进行常规的转发操作。

由于flow比较复杂,分别阐述如下。

Send()

Send()部分的完整flow如下所述:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 1. 保存IN_PORT到寄存器REG0
move:NXM_OF_IN_PORT[]->NXM_NX_REG0[0..15],
// 2. 保存TUN_SRC到寄存器REG1
move:NXM_NX_TUN_IPV4_SRC[]->NXM_NX_REG1[],
// 3. 设置去往分析系统的TUN_ID
set_field:0x100->tun_id,
// 4. 设置当前TUN_SRC为0
set_field:0.0.0.0->tun_src,
// 5. 设置当前IN_PORT为0
load:0->NXM_OF_IN_PORT[],
// 6. 设置TUN_DST为分析系统
set_field:1.2.3.4->tun_dst,
// 7. 从gre口发出
output:512,
// 8. 从REG0还原保存的IN_PORT
move:NXM_NX_REG0[0..15]->NXM_OF_IN_PORT[],
// 9. 从REG1还原保存的TUN_SRC
move:NXM_NX_REG1[]->NXM_NX_TUN_IPV4_SRC[],

以上主要需要考虑如下几个问题。

1. 设置OUTPUT

其中,关键逻辑为第4和第5步,在向GRE口发出采集的icmp_request之前,需要置0。这是因为在从gre口收到icmp_request时,镜像时,output也是512,对于openflow来说output和inport若相同则不允许转发。

解决方法有两个,其一是在output前clear IN_PORT寄存器,另一种是使用特殊的Action IN_PORT(一个取值特殊的保留port)来作为output到in_port的动作。

因此这里通过先保存IN_PORT到reg0,然后output到gre口,最后恢复IN_PORT寄存器的方法。

2. 设置TUN_SRC

此外,从GRE口收到的icmp_request镜像给分析系统前,需要把GRE的TUN_SRC改写为自己,否则分析系统无法区分该流量是从src还是dst采集到的icmp_request。

因此这里也是先保存TUN_SRC,然后将当前TUN_SRC寄存器置0,由ovs自己填充源IP,最后还原TUN_SRC寄存器。

至此,src和dst的icmp_request已经可以正确镜像到分析系统。

Learn()

tip

learn是一种ovs action,在flow中提供了根据数据包自动生成flow的能力。可以这么比喻,learn(flow_str)中的flow_str定义了一个flow的class,每一个匹配并执行learn action的数据包header field提供了class的入参,通过learn(flow_str)实例化为一条flow,该flow会被加入flow_str中指定的table中。

learn()的常见使用场景包括OpenStack中实现的基于learn()来学习mac、port、vlan的映射关系。

因此,这里我们的思路是,通过icmp_request数据包生成一条用于匹配icmp_reply的flow,主要动作包括:染色、镜像至分析系统。

learn action的简化版本为:

1
2
3
4
5
6
7
// 1. 设置REG3为512
//    在此处(global hook) reg3还未使用,因此这里不会先保存再还原
load:512->NXM_NX_REG3[],
// 2. learn action 简化版
learn(table=31,idle_timeout=2,hard_timeout=4,priority=30000,dl_type=0x0800,ip_proto=1,icmp_type=0,icmp_code=0,NXM_OF_IP_SRC[]=NXM_OF_IP_DST[],NXM_OF_IP_DST[]=NXM_OF_IP_SRC[],Stain(),Send()),
// 3. 还原REG3为0
load:0->NXM_NX_REG3[]

在第一步先设置REG3为512,因为learn action中的output动作的参数只支持寄存器格式的值,不支持字面量,因此这里设置寄存器REG3给learn action使用,第三步再还原REG3。

其中,当执行learn action时,会将上述指定的flow加入table_31,match中包括属性idle_timeout, hard_timeout, priority 等,match中指定匹配icmp_reply包。这里,在learn action中无法使用常规flow中类似于ip,tcp这样的语义简化版本,必须指定key=value这样的明细版本,因此通过dl_type=0x0800ip_proto=1指定协议为ICMP。

match中还指定了匹配的源IP,和目的IP。这里需要注意的是,ovs去实例化该flow时,是基于当前处理的数据包寄存器去解析learn中指定的flow_str的。因此这里最后实例化后的flow match中源目IP条件会类似于如下:

1
table=31,idle_timeout=2, hard_timeout=4, priority=30000,icmp,nw_src=10.2.1.1,nw_dst=10.2.6.1,icmp_type=0,icmp_code=0

Stain()

Stain() 进行染色操作,比较简单,就是设置tos:

1
load:0x40->NXM_OF_IP_TOS[]

Send()

Send() 会和遇到前面一样的问题,这里的Send() 如下:

1
2
3
4
5
load:0->NXM_NX_TUN_IPV4_SRC[],
load:0->NXM_OF_IN_PORT[],
load:0x100->NXM_NX_TUN_ID[],
load:0x1020408->NXM_NX_TUN_IPV4_DST[],
output:NXM_NX_REG3[]

可以看到这里直接overwrite了TUN_SRC[]和IN_PORT[],而没有先保存后还原,这里也是OVS对于learn action有所限制。

这是因为,在learn action中,目前支持几个动作,包括如下:

1
2
3
4
5
field=value
field[start..end]=src[start..end]
load:value->dst[start..end]
load:src[start..end]->dst[start..end]
output:field[start..end]

可以看到并不支持move,因此无法保存寄存器A到寄存器B中。

其中虽然支持load,但是load:a->bmove:a->b在此处的区别在于:

  1. move是将a寄存器的值保存到b中,a寄存器的值由具体的包匹配过程中去确定,相当于是runtime;
  2. 而在learn中,load:a->b会通过当前处理的数据包的寄存器去取a寄存器的值,然后赋值到寄存器b,相当于在learn()去编译时就确定了a寄存器的值,使用的是匹配该learn flow的数据包对应的namespace,而非后续匹配learn学习到的flow的数据包对应的namespace。

因此这里无法在learn action中保留原生(raw)的寄存器字面量(literal value),也就无法先 push 后 pop IN_PORT和TUN_SRC。

不过虽然这里overwrite了这两个寄存器,但是可以在进入table_31之前先保存这两个变量,匹配完table_31之后,再还原这两个变量,即可以workaround这个问题。

Back()

Back()对应的逻辑为,在镜像到分析系统、学习等操作之后,转回原先的转发table进行后续的转发处理。

flow为:

1
resubmit(,0)

完整hook flow

因此完整的、hook icmp_request的、十分复杂的flow为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// match
cookie=0x20,table=1,priority=40000,metadata=0x1,icmp,icmp_type=8,icmp_code=0,nw_tos=0x40 actions=

// Send()
move:NXM_OF_IN_PORT[]->NXM_NX_REG0[0..15],
move:NXM_NX_TUN_IPV4_SRC[]->NXM_NX_REG1[],
set_field:0x100->tun_id,
set_field:0.0.0.0->tun_src,
load:0->NXM_OF_IN_PORT[],
set_field:1.2.4.8->tun_dst,
output:512,
move:NXM_NX_REG0[0..15]->NXM_OF_IN_PORT[],
move:NXM_NX_REG1[]->NXM_NX_TUN_IPV4_SRC[],
load:512->NXM_NX_REG3[],

// Learn()
learn(
table=31,idle_timeout=2,hard_timeout=4,priority=30000,dl_type=0x0800,ip_proto=1,icmp_type=0,icmp_code=0,
NXM_OF_IP_SRC[]=NXM_OF_IP_DST[],
NXM_OF_IP_DST[]=NXM_OF_IP_SRC[],
load:0->NXM_NX_TUN_IPV4_SRC[],
load:0->NXM_OF_IN_PORT[],
load:0x40->NXM_OF_IP_TOS[],
load:0x100->NXM_NX_TUN_ID[],
load:0x1020408->NXM_NX_TUN_IPV4_DST[],
output:NXM_NX_REG3[]
),

// Back()
load:0->NXM_NX_REG3[],
resubmit(,0)

hook icmp_reply

有了前面的基础,hook icmp_reply就十分简单了。基本思路如下:

1
2
cookie=0x20,table=1,priority=40000,metadata=0x1,icmp,icmp_type=0,icmp_code=0,nw_tos=0x40 
actions=Save(in_port,tun_src),Resubmit(table=31),Restore(in_port,tun_src),Back()

match部分指定hook 染色的icmp_reply,action中先保存IN_PORT和TUN_SRC寄存器的值,再跳转到table_31,之后还原IN_PORT和TUN_SRC寄存器,最后跳回table_0进行后续的转发逻辑。

Save和Restore

save() flow为:

1
2
3
4
// 保存IN_PORT至REG0
move:NXM_OF_IN_PORT[]->NXM_NX_REG0[0..15],
// 保存TUN_SRC至REG1
move:NXM_NX_TUN_IPV4_SRC[]->NXM_NX_REG1[]

restore() flow为:

1
2
3
4
// 还原IN_PORT
move:NXM_NX_REG0[0..15]->NXM_OF_IN_PORT[],
// 还原TUN_SRC
move:NXM_NX_REG1[]->NXM_NX_TUN_IPV4_SRC[]

Resubmit()

resubmit到table_31的flow为:

1
set_field:0x2->metadata,resubmit(,31)

Back()

跳回table的逻辑为:

1
set_field:0x3->metadata,resubmit(,0)

完整flow

所以最后完整的flow为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# match
cookie=0x20,table=1,priority=40000,metadata=0x1,icmp,icmp_type=0,icmp_code=0,nw_tos=0x40 actions=

# Save()
move:NXM_OF_IN_PORT[]->NXM_NX_REG0[0..15],
move:NXM_NX_TUN_IPV4_SRC[]->NXM_NX_REG1[],

# Resubmit()
set_field:0x31->metadata,
resubmit(,31),

# Restore()
move:NXM_NX_REG0[0..15]->NXM_OF_IN_PORT[],
move:NXM_NX_REG1[]->NXM_NX_TUN_IPV4_SRC[],

# Back()
set_field:0x32->metadata,
resubmit(,0)

前置依赖

对于ToS染色的依赖,要求GRE外部IP Header中的ToS需要继承overlay IP Header标记的ToS,因此需要GRE隧道配置中需要继承内层ToS的隧道属性,可以执行如下命令:

1
ovs-vsctl set in <gre_iface_name> options:tos=inherit

关于构造ICMP Reply

本文提出的方案中依赖vm回复icmp_reply,如果vm禁ping则无法响应,因此准确性需要前后对比验证来排除禁ping的情况。

另一种思路是,在flow中可以做到构造icmp reply,实现类似于arp代答的逻辑,以此可以做到vm无关。