上一篇 P4 学习笔记(四)- 实战 Reflector & Repeater 里面我们实战练习了最基础的两个例子,实现了网络包的镜像和转发,简单理解了实际开发过程中要配置的文件和使用的工具。这篇文章我们用三个例子循序渐进的学习链路层的交换机用 P4 应该如何实现。
目录
- 简单的链路层转发
- 组播
- 自学习交换机
- P4->NetFPGA
简单的链路层转发
这个练习在 https://github.com/nsg-ethz/p4-learning/tree/master/exercises/03-L2_Basic_forwarding,记得提前 git clone 下来!所有的操作都在这个文件夹里进行。有一些之前已经讲过的操作,就不会再过多的说明了。
链路层转发练习的拓扑结构
这个练习里,我们会用到四个 host 和一个交换机,按照上图连接,它们对应的拓扑结构被定义在 p4app.json
里,原理和上一篇类似,这里就不多说了,如果不确定的话可以回去看一下上一篇文章。
第一个练习要学习的,就是链路层的交换机是如何实现转发机制的,也就是根据一个已知的 MAC 地址,交换机要知道对应的端口是哪一个。真正的交换机都是自动学习这样一个映射关系的,我们在最后一个练习就会看到。在第一个练习里,我们把复杂的过程拆解,先实现一个静态的转发映射表练练手。
在我们要完成的 p4src/l2_basic_forwarding.p4
这个文件中,我们有一系列的 TODOs 要完成,我们一个一个说。
第一步,我们要定义一些数据类型,比如我们链路层的 header,还有常规的 metadata。
/*************************************************************************
*********************** H E A D E R S ***********************************
*************************************************************************/
typedef bit<9> egressSpec_t; // 定义端口类型,占 9 个 bit
typedef bit<48> macAddr_t; // 链路层地址,占 48 个 bit
header ethernet_t { // 链路层的 header
macAddr_t dstAddr;
macAddr_t srcAddr;
bit<16> etherType;
}
struct metadata { // metadata 这里虽然没有用到,但作为标准的参数,我们要给出定义
/* empty */
}
struct headers { // 这里列出所有需要的 header,这个练习里只需要链路层的部分
ethernet_t ethernet;
}
第二步,我们要定义一个 Match-Action 的表,把 MAC 地址映射到对应端口上。其实这一步类似于上一个 Repeater 的练习。定义表的时候,我们通常要定义四个部分,(i) 键值,(ii) 动作,(iii) 表的大小,和 (iv) 默认的动作。
/*************************************************************************
************** I N G R E S S P R O C E S S I N G *******************
*************************************************************************/
control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
action drop() {
mark_to_drop(standard_metadata);
}
action forward(bit<9> egress_port) { // 转发的动作,就是把在表中查到的 port 赋值给 egress_spec
standard_metadata.egress_spec = egress_port;
}
table dmac { // 定义一个表叫 dmac
key = {
hdr.ethernet.dstAddr: exact; // Match 的时候就看目标 MAC 地址是否完全一样
}
actions = { // 列出所有的需要的动作
forward;
NoAction;
}
size = 256;
default_action = NoAction; // 如果表里没有查到,先不进行任何动作
}
apply {
dmac.apply(); // 记得要 apply 才会用上这个表
}
}
定义好了我们的表之后,还需要在表里面填写好转发的规则,否则现在我们的 dmac
表还是空的,全部 packet 都会 miss,然后会默认选择 NoAction。所以我们要像上次 Repeater 里面一样,用 CLI 语言写好我们的规则。
在 p4app.json
里,我们有写:
...
"switches": {
"s1": {
"cli_input": "s1-commands.txt"
}
}
...
所以只要在这个练习的根路径下创建一个叫 s1-commands.txt
的新文件,然后在里面写上:
# table_add <表的名字> <动作的名字> <动作的参数——MAC地址> => <目标端口数>
table_add dmac forward 00:00:0a:00:00:01 => 1
table_add dmac forward 00:00:0a:00:00:02 => 2
table_add dmac forward 00:00:0a:00:00:03 => 3
table_add dmac forward 00:00:0a:00:00:04 => 4
其实和上一篇文章中 Repeater 练习里面的语法结构是一模一样的,只不过这次我们把键值从 input_port 换成了 MAC 地址。但为什么要这么映射呢?每一个 host 的 MAC 地址其实都是按照一个模式去分配的,也就是 00:00:<16进制的IP地址>
。比如 h1 的 IP 地址是 10.0.1.5
,那么它的 MAC 地址就是 00:00:0a:00:01:05
。
在我们运行 sudo p4run
之后,终端里会自动显示各个节点的网络配置,包括交换机 s1 的每个端口的信息:
Switch port mapping:
s1: 1:h1 2:h2 3:h3 4:h4
这样我们就知道应该如何在 s1-commands.txt
中创建规则了。当然,我们也可以之后在 mininet
里的 CLI 用比如 h1 ifconfig -a
去查看比如 h1 的网络配置,在上一篇文章中也已经介绍过,这里就不再赘述。
等到所有的任务都完成,就可以 sudo p4run
运行起我们的环境,然后用 PING 来测试一下交换机 s1 能不能成功的转发网络报文了。
mininet> pingall
*** Ping: testing ping reachability
h1 -> h2 h3 h4
h2 -> h1 h3 h4
h3 -> h1 h2 h4
h4 -> h1 h2 h3
*** Results: 0% dropped (12/12 received)
如果看到上面这样的结果,那么说明我们成功的从各个 host 节点上都可以成功和其他节点通信啦!
小结:这个练习,我们主要是复习一下表的设计和操作,如何使用表来完成网络报文在链路层的转发。难点可能就是要知道
s1-commands.txt
里面的每一个规则应该如何添加,怎么确定哪个 MAC 地址对应着哪个端口。
组播
这个练习在 https://github.com/nsg-ethz/p4-learning/tree/master/exercises/03-L2_Flooding,记得提前 git clone 下来!所有的操作都在这个文件夹里进行。有一些之前已经讲过的操作,就不会再过多的说明了。
现在我们已经学会了如何利用一个静态配置好的表来按照 MAC 地址对应到端口实现网络报文的转发。但其实在这之前,还有一个问题就是,给我们一个 IP 地址我们怎么知道对应的 MAC 地址是什么呢?如果不能解析出来一个 IP 包的 MAC 地址,那我们又怎么知道在链路层该发给谁呢?这个问题的答案就是 ARP 协议,关于 ARP 大家可以看这个回答。
我们之前练习的时候好像都没有遇到这样的问题,所有的信息好像就很 magically 的都配置好了,交换机清楚的知道每个 host 的 IP 地址对应的 MAC 地址。这是因为我们 p4app.json
里面默认了 "auto_arp_tables": true
,也就是自动填充好 ARP table,但在这个练习里,我们要回归原始,把它设置成 "auto_arp_tables": false
。这样的话,就要我们自己协助交换机完成 ARP table 的学习过程。其中最重要的,就是要先学会使用 multicast,也就是组播。
在一个交换机不知道收到的网络报文改转发给谁的时候,它会把这个网络报文发给除了接受端口之外的所有其他的端口,这个过程也叫 L2 Flooding。比如在交换机收到了一个来自 h1(1 号端口)的网络报文,这个 packet 要发给 h2,但交换机不知道 h2 是谁,于是要发 ARP request 问 h2 是谁,request 要发给除了 1 号端口的所有其他端口,也就是 2, 3, 4 号端口,然后 2 号端口会发回来一个 ARP reply,告诉交换机 h2 在 00:00:0a:00:00:02
。
为了实现这个机制,我们就要用到 P4 里面的 standard_metadata.mcast_grp
了。类似于把网络报文从一个特定的端口发出去的 standard_metadata.egress_spec
,当定义了 standard_metadata.mcast_grp
的时候,网络报文会从一个特定的组播的小组发出去,而这个小组可能包含多个端口。
像我们所使用的一个交换机 s1 连接四个 host 的情况,组播就可以在 CLI 中(接着上面的 s1-commands.txt
)按照如下的方式配置:
mc_mgrp_create 1 # 创建组播的小组 1
mc_node_create 0 2 3 4 # 创建一个节点 0,包含了 2 3 4 三个端口
mc_node_associate 1 0 # 把节点 0 和组播小组 1 联系起来
mc_mgrp_create 2 # 创建组播的小组 2
mc_node_create 1 1 3 4 # 创建一个节点 1,包含了 1 3 4 三个端口
mc_node_associate 2 1 # 把节点 1 和组播小组 2 联系起来
mc_mgrp_create 3 # 创建组播的小组 1
mc_node_create 2 1 2 4 # 创建一个节点 2,包含了 1 2 4 三个端口
mc_node_associate 3 2 # 把节点 2 和组播小组 3 联系起来
mc_mgrp_create 4 # 创建组播的小组 1
mc_node_create 3 1 2 3 # 创建一个节点 2,包含了 1 2 3 三个端口
mc_node_associate 4 3 # 把节点 3 和组播小组 4 联系起来
更多具体的配置和语法文档在这里。
定义了这样的组,我们就可以说,当收到了一个 1 号端口发来的未知网络报文的时候,我们就从组播的 1 小组发给 2, 3, 4 号端口连接的 host。当然,为了实现这样的过程,我们需要建立一个新的表,能够把一个网络报文的 ingress_port
映射到一个组播的小组。所以在 p4 里的 Ingress 部分,我们要在上一个练习的基础上,加上下面这些:
action set_mcast_grp(bit<16> mcast_grp) {
standard_metadata.mcast_grp = mcast_grp; // 通过查表,设置发出的组播组号
}
table broadcast {
key = {
standard_metadata.ingress_port : exact; // 根据 ingress_port 选择映射的组播号
}
actions = {
set_mcast_grp;
NoAction;
}
size = 32;
default_action = NoAction;
}
有了这第二个表,我们就可以继续补充上 CLI 里面(接着上面的 s1-commands.txt
)需要的规则:
table_add broadcast set_mcast_grp 1 => 1 # 来自 ingress_port 1 的 packet 从组播的 1 组发出去
table_add broadcast set_mcast_grp 2 => 2
table_add broadcast set_mcast_grp 3 => 3
table_add broadcast set_mcast_grp 4 => 4
我们已经定义好,也填好了两个 dmac
和 broadcast
两个表了,下面就该实现逻辑部分了。我们想要实现的就是,在知道了一个网络报文的目标 MAC 地址的时候,也就是 dmac
表 hit 的情况下,按照 dmac
的表实现转发,如果不知道 MAC 地址的时候,用组播的方式发送 ARP request,获取 MAC 地址。所以这里我们就需要用一个判断语句。P4 里的判断语句有两种写法,一种是 if
:
apply {
if (!dmac.apply().hit) { // 如果没有 hit
broadcast.apply(); // 就采用 multicast 的表
}
}
另一种就是 switch
:
apply {
switch (dmac.apply().action_run) { // 检查 match 到的 action
NoAction: { // 如果是 miss,也就是采用默认的 NoAction
broadcast.apply(); // 就采用 multicast
}
}
}
写完这些,我们就可以再一次跑上 sudo p4run
,测试一下能不能成功 multicast 到各个端口。为了测试这一点,我们可以先打开四个终端,用 tcpdump
分别监听四个 interfaces,然后再用 h1 ping h2
测试。
ARP Table
如上图所示,每个 interface 都抓到了 ARP request 的包,说明我们的交换机在不知道 MAC 地址的时候,能够成功组播 ARP request 到各个其他端口。值得说明的是,在 s1-eth1
的 interface 上,我们也看到了 ARP request,s1-eth1
对应的是 1 号端口连接的 h1。按理说从 1 号端口来的网络报文应该不会再在 1 号端口发 ARP request 了,其实这个地方我们看到的 ARP request 是接收到的网络报文,而没有进行转发。如果是广播而不是组播的话,我们其实会在 s1-eth1
上看到两个 ARP request。
再次测 pingall
的话,我们也会得到刚才一样的结果。
小结:做完这个练习,我们就已经基本熟悉了 P4 里对表的操作,而且探索了逻辑控制的部分,把两个表的功能融合在了一起,让它们各司其职。
自学习交换机
这个练习在 https://github.com/nsg-ethz/p4-learning/tree/master/exercises/04-L2_Learning,记得提前 git clone 下来!所有的操作都在这个文件夹里进行。有一些之前已经讲过的操作,就不会再过多的说明了。
到了这一步,我们已经可以不需要 mininet 提供的 ARP 自动填充了,但好像这个交换机还不是很聪明的样子。回头看一下我们的 s1-commands.txt
里面,还需要我们手动告诉交换机下面这些信息:
table_add dmac forward 00:00:0a:00:00:01 => 1
table_add dmac forward 00:00:0a:00:00:02 => 2
table_add dmac forward 00:00:0a:00:00:03 => 3
table_add dmac forward 00:00:0a:00:00:04 => 4
如果每一个交换机都要我们手动配置这些信息,那可是要累死了,所以这也是自学习交换机存在的意义了。如果我们每收到一个网络报文,我们记住它的源 MAC 地址,和 ingress port,不就能得到上面这些信息了吗?
我们来整理一下思路…
- 学习过程:对于每个网络报文,先看看它的源 MAC 地址有没有见过,如果没见过,那我们就把这个地址和它对应的 ingress port 的映射记录在表(之前已经实现的
dmac
)里,然后要想办法告诉交换机,这个 MAC 地址我们见过了,不用再做更多操作了,继续下一步转发就可以了。 - 转发过程:这一步也是我们之前实现过的,如果网络报文的目标 MAC 地址是已知的,那就正常转发,否则我们就把它从除了接收端口外的其他所有端口上转发出去。
为了能取代我们人为填表的过程,我们在这里需要引入一个控制层,也就是 controller。让 controller 帮我们把我们在 s1-commands.txt
里的事情都自动完成。P4 里和 controller 通讯的方式有两种,一种是 Clone Packets,一种是 Digest,我们分别来看。
Clone
Clone,顾名思义,就是把 packet 克隆一个一模一样的,发给 controller。而原来的那个本体,会继续按正常的 pipeline 的顺序被处理和转发。当我们收到一个没见过源 MAC 地址的网络报文的时候,我们就可以克隆一个 packet,发给 controller,然后让 controller 帮我们在 dmac
的表里注册好 MAC 地址和 ingress port 的映射,然后告诉交换机这个源地址已经见过了。
Clone 作为一个 extern
函数,有两种使用方式:
clone(in CloneType type, in bit<32> session)
clone3(in CloneType type, in bit<32> session, in T data)
这两个方式的唯一区别就是,是否带有第三个用于存储一些额外参数用的自定义的 metadata struct
,因为 clone
之后对应的 standard_metadata
都会被重制(通常都是 0),所以如果还有要用到的信息,比如我们这里要用到 standard_metadata.ingress_port
,就要用第三个参数传递一下。
然后说第一个参数,我们会用到的,暂时只有两种,CloneType.I2E
和 CloneType.E2E
。I2E
是从 ingress 的过程克隆一份,直接发送到 egress 的部分进行处理,跳过 ingress 的时候调用 clone
后面的代码。E2E
是从 egress 的过程克隆一份,再一次发回到 egress 的 pipeline,有点像一个在 egress 里二次加工的过程。
这里有一个潜在的问题,就是怎么判断正在处理的 packet 是本体还是被克隆的那一个呢?如果分不清的话,比如 E2E
的时候会不会就是一个无限循环了?P4 在这里用 standard_metadata.instance_type
来区分,普通的 packet 的 instance_type == 0
,I2E
的 instance_type == 1
,E2E
的 instance_type == 2
,于是我们就可以用 if
语句来区分本体和克隆体了。具体的应用实例我们会在稍后看到。
第二个参数,就是一个标准的 session ID 了,P4 里也叫 mirror ID,这个 ID 是用来告诉交换机该把克隆的 packet 发送到哪一个端口的。在 CLI 里,我们会用
mirroring_add <session> <output_port>
来绑定把 session 和 controller 连接的端口绑定在一起。比如如果 controller 连在交换机的端口 7 的话,我们就可以用 mirroring_add 100 7
把它和 session 100 绑定在一起,然后在 clone
的时候把第二个参数定义为 100,这样克隆的 packet 都会被发送到连接在端口 7 的 controller 了。
更多关于 Clone 的用法,可以在这个官方文档查到。
了解了 Clone 的机制之后,我们就可以开始思考如何实现学习的过程了。
第一步,为了让 controller 能够得到 MAC 地址和 ingress port 的映射,我们需要把这两个数据发给 controller,所以我们可以定义一个 header cpu_t
作为给 controller 发送的消息的 payload:
header cpu_t {
bit<48> srcAddr; // 源 MAC 地址
bit<16> ingress_port; // 交换机的接收端口
}
struct headers {
ethernet_t ethernet;
cpu_t cpu; // 在 Ethernet 之后加上给 controller 看的 header
}
同时,我们提到了,Clone 的 packet 会失去 metadata 里 ingress_port
的信息,所以我们需要用自己定义的 metadata
暂时存储这个信息,然后之后 copy 到 cpu_t.ingress_port
:
struct metadata {
bit<9> ingress_port;
}
定义了这两个数据结构之后,我们会发现,我们在 copy ingress_port
的时候需要做数据类型转换,这是因为在 controller 上我们要用 python 的 struct.unpack
来解析 cpu_t
,所以需要加上 padding,所以要从 9 bit 变成 16 bit。
基于上一个练习,我们已经有了 dmac
和 broadcast
两个表了。只需要再加一个表来告诉交换机一个源 MAC 地址有没有见过就好了。我们不能用 dmac
来完成这个过程,是因为 dmac
match 的是 destination MAC 地址,而这里我们需要 match source MAC 地址。所以我们就叫这个新的表 smac
。如果一个 packet 的源 MAC 地址已经见过了(hit),那我们就不用处理(NoAction
),否则,我们要克隆一个 packet 发给 controller,让 controller 登记一下 srcAddr->ingress_port
的映射。
control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
...
action mac_learn() {
meta.ingress_port = standard_metadata.ingress_port; // 把接收端口存在我们自己定义的 metadata 里
// 因为 clone 的时候 standard_metadata.ingress_port 就会被重制为 0
clone3(CloneType.I2E, 100, meta); // 克隆的 packet 会被从 Ingress 直接发送到 Egress 部分,并从 session 100 对应的端口发给 controller
}
table smac {
key = {
hdr.ethernet.srcAddr: exact; // Match 的是源 MAC 地址
}
actions = {
mac_learn;
NoAction;
}
size = 256;
default_action = mac_learn; // 默认动作是 mac_learn
}
...
}
有了这个 smac
表之后,我们在原来的逻辑基础上,只需要在前面加上 smac.apply()
就可以了:
control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
...
apply {
smac.apply(); // 先检查一下源 MAC 地址有没有见过
if (!dmac.apply().hit) { // 如果没有 hit
broadcast.apply(); // 就采用 multicast 的表
}
}
...
}
这样每个 packet 在 Ingress 的过程,都要先经由 smac
处理,再被转发。被 smac
处理的 packet 会被 clone3
那一行代码送到 Egress 的过程:
control MyEgress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
apply {
// If ingress clone
if (standard_metadata.instance_type == 1){
hdr.cpu.setValid(); // 把克隆的 packet 的 cpu header 设置成 valid,这样在 Deparser 的时候才会被加进 headers 里
hdr.cpu.srcAddr = hdr.ethernet.srcAddr;
hdr.cpu.ingress_port = (bit<16>)meta.ingress_port; // 把 ingress_port 拷贝过来,记得转换类型
hdr.ethernet.etherType = L2_LEARN_ETHER_TYPE; // EtherType 换成 0x1234,controller 用这个来过滤
truncate((bit<32>)22); // 截断 ether(48+48+16 bits)+cpu(48+16 bits) header = 22 bytes 之后的部分,因为我们只需要 cpu header 里面的信息作为 payload
}
}
}
control MyDeparser(packet_out packet, in headers hdr) {
apply {
//parsed headers have to be added again into the packet.
packet.emit(hdr.ethernet);
packet.emit(hdr.cpu); // 普通的 packet 的 cpu header 不是 valid 的,所以只有 ethernet header 没有 cpu header
}
}
到此为止,P4 的代码就已经写好了。下面我们就要写一下 controller 的代码了。
Controller 的代码会用到 scapy
和 ETHz 网络团队写的 p4utils 与 Mininet 环境中的各个节点(主要是交换机)进行交互。
比如初始化的过程,是下面这段代码:
from p4utils.utils.topology import Topology # 用来解析当前 mininet 配置的网络拓扑结构
from p4utils.utils.sswitch_API import SimpleSwitchAPI # 用来代替 s1-commands.txt 里的 CLI,和 mininet 的 control plane 交互
from scapy.all import Ether, sniff, Packet, BitField
class CpuHeader(Packet):
'''
定义我们 CPU header 的构成
'''
name = 'CpuPacket'
fields_desc = [BitField('macAddr',0,48), BitField('ingress_port', 0, 16)]
...
class L2Controller(object):
def __init__(self, sw_name):
# 初始化网络拓扑结构
self.topo = Topology(db="topology.db")
# 从拓扑结构中获取 controller 在交换机上的端口
self.sw_name = sw_name
self.cpu_port = self.topo.get_cpu_port_index(self.sw_name)
# 初始化 controller 的交互接口 API
self.thrift_port = self.topo.get_thrift_port(sw_name)
self.controller = SimpleSwitchAPI(self.thrift_port)
self.init()
def init(self):
self.controller.reset_state() # 重制交换机上的状态
self.add_boadcast_groups()
self.add_mirror()
def add_mirror(self):
if self.cpu_port:
self.controller.mirroring_add(100, self.cpu_port) # 也就是 CLI 中的 mirroring_add 100 7
def add_boadcast_groups(self):
# CLI 中配置组播的部分
interfaces_to_port = self.topo[self.sw_name]["interfaces_to_port"].copy()
# 把 lo 和 cpu 的端口去掉
interfaces_to_port.pop('lo', None)
interfaces_to_port.pop(self.topo.get_cpu_port_intf(self.sw_name), None)
mc_grp_id = 1 # 组 ID
rid = 0 # 节点 ID
for ingress_port in interfaces_to_port.values():
port_list = interfaces_to_port.values()[:]
del(port_list[port_list.index(ingress_port)])
# 增加一个组播的小组
self.controller.mc_mgrp_create(mc_grp_id)
# 增加一个组播的节点
handle = self.controller.mc_node_create(rid, port_list)
# 把组播的小组和节点关联起来
self.controller.mc_node_associate(mc_grp_id, handle)
# 把 ingress_port 和组播的组映射关系加进表里
self.controller.table_add("broadcast", "set_mcast_grp", [str(ingress_port)], [str(mc_grp_id)])
mc_grp_id +=1
rid +=1
主循环的部分,要一直监听来自 cpu 端口的 packet,我们就选择用 scapy 的 sniff
。
...
def recv_msg_cpu(self, pkt):
packet = Ether(str(pkt))
if packet.type == 0x1234: # 只查看 EtherType 是 0x1234 的 packet
cpu_header = CpuHeader(bytes(packet.payload)) # 解析 ethernet header 后面的 cpu header
self.learn([(cpu_header.macAddr, cpu_header.ingress_port)]) # 我们需要完成的 learn 函数
def run_cpu_port_loop(self): # 主循环函数
cpu_port_intf = str(self.topo.get_cpu_port_intf(self.sw_name).replace("eth0", "eth1"))
sniff(iface=cpu_port_intf, prn=self.recv_msg_cpu)
if __name__ == "__main__":
import sys
sw_name = sys.argv[1]
receive_from = sys.argv[2]
if receive_from == "cpu":
controller = L2Controller(sw_name).run_cpu_port_loop()
最后,我们需要完成的就是 learn
这个函数,有了 self.controller
,其实我们需要做的就和 CLI 里面的是一样的,但要注意 match 的 key 和对应的 value 分别是什么。
...
def learn(self, learning_data):
for mac_addr, ingress_port in learning_data:
print "mac: %012X ingress_port: %s " % (mac_addr, ingress_port)
# 和 CLI 中的 table_add <table_name> <action_name> <key> <value> 一样
self.controller.table_add("smac", "NoAction", [str(mac_addr)]) # 在 smac 中加入我们已经见过的 source MAC addr
self.controller.table_add("dmac", "forward", [str(mac_addr)], [str(ingress_port)]) # 在 dmac 中注册 MAC addr -> ingress_port 的映射
到这里,我们就终于可以测试了!跑上 sudo p4run
,然后再在另一个终端跑上 sudo python l2_learning_controller.py s1 cpu
,就可以试试 pingall
了:
mininet> pingall
*** Ping: testing ping reachability
h1 -> h2 h3 h4
h2 -> h1 h3 h4
h3 -> h1 h2 h4
h4 -> h1 h2 h3
*** Results: 0% dropped (12/12 received)
如果要看看 dmac
的表有没有被自动填充好,可以用下面这个命令:
$ simple_switch_CLI --thrift-port 9090
Obtaining JSON from switch...
Done
Control utility for runtime P4 table manipulation
RuntimeCmd: table_dump dmac
小结:做完这个练习,我们在复习了表的操作和之外,学会了如何使用 clone 的方式和 controller 通讯来自动填好交换机上的表的实现,同时,我们也看了如何使用 p4utils 来自动化之前 s1-commands.txt 的部分。
Digest
和 controller 的通讯过程,还可以用 digest
来实现。就像 digest
的名字一样,我们不会克隆整个 packet,而是取 packet 的一部分作为信息发给 controller。比如我们现在其实只需要 packet 的 srcAddr
和 ingress_port
,别的部分都不需要,有的时候能节约很多空间。Digest 是基于 nanomsg 实现的一个 extern
函数,但其实用 digest 并不一定比 clone 快,因为使用 nanomsg 要在 payload 前面加上一个 control header,而且 controller 每收到一个 message,都要回复一个 ACK,证明自己收到了,防止收到重复的 message。
P4 部分的实现方式如下(和 clone 部分相同的就省略了):
struct learn_t { // digest 的 payload 部分
bit<48> srcAddr;
bit<16> ingress_port; // 记得加上 padding 变成 16 bits
}
struct metadata {
learn_t learn; // 加入进我们的自定义 metadata 部分
}
struct headers {
ethernet_t ethernet; // headers 里不再需要 cpu header 了
}
...
control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
...
action mac_learn(){ // 这个函数中,我们把 digest 的信息填充好,发给 controller
meta.learn.srcAddr = hdr.ethernet.srcAddr;
meta.learn.ingress_port = (bit<16>)standard_metadata.ingress_port; // 记得转换数据类型
digest(1, meta.learn); // digest 的第一个参数永远是 1,第二个参数是我们的 digest payload
}
...
control MyEgress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
apply { } // Egress 部分不需要再做任何处理了
}
...
controller 部分,只要加下面这几个函数和主循环,最底层还是用上面写好的 learn
函数:
class L2Controller(object):
...
def unpack_digest(self, msg, num_samples):
'''
msg = | control header | sample0 | sample1 | ... | sample#{num_samples-1} |
@参数:
msg - 原本的 message
num_samples - control header 后面的 sample 数
'''
digest = []
print len(msg), num_samples
starting_index = 32 # 跳过 control header 的部分
for sample in range(num_samples): # 每一个 sample 都解析到 digest 的列表中
mac0, mac1, ingress_port = struct.unpack(">LHH", msg[starting_index:starting_index+8]) # srcAddr (48 bits) + ingress_port (16 bits) = 64 bits = 8 bytes
starting_index +=8
mac_addr = (mac0 << 16) + mac1 # 把 mac0 (32 bits) 和 mac1 (16 bits) 叠加在一起
digest.append((mac_addr, ingress_port))
return digest
def recv_msg_digest(self, msg):
topic, device_id, ctx_id, list_id, buffer_id, num = struct.unpack("<iQiiQi", msg[:32]) # nanomsg 的 control header
digest = self.unpack_digest(msg, num) # 解析 digest 的 payload
self.learn(digest)
# 发送 ACK 告诉交换机已经收到了信息
self.controller.client.bm_learning_ack_buffer(ctx_id, list_id, buffer_id)
def run_digest_loop(self): # digest 的主循环
# 建立 nanomsg 的 socket connection
sub = nnpy.Socket(nnpy.AF_SP, nnpy.SUB)
notifications_socket = self.controller.client.bm_mgmt_get_info().notifications_socket
sub.connect(notifications_socket)
sub.setsockopt(nnpy.SUB, nnpy.SUB_SUBSCRIBE, '')
while True: # 持续接收 nanomsg 的信息
msg = sub.recv()
self.recv_msg_digest(msg)
if __name__ == "__main__":
import sys
sw_name = sys.argv[1]
receive_from = sys.argv[2]
if receive_from == "digest":
controller = L2Controller(sw_name).run_digest_loop()
elif receive_from == "cpu":
controller = L2Controller(sw_name).run_cpu_port_loop()
完成了这两个部分,我们就可以按照 clone 的测试方式检验结果了。
P4->NetFPGA
偶然在这里看到 NetFPGA 平台上 Learning Switch 的实现方式,也在这个地方记录一下。
对于基于 P4->NetFPGA 做高速网卡的设计和研究感兴趣的同学可以移步去这个 github repo 学习一下,欢迎一起讨论!
SUME 的 Pipeline
#include <core.p4>
#include "simple_sume_switch.p4" // 这个 tutorial 里面自己定义了更加简单的结构,正常的话要 #include <sume_switch.p4>
/* standard sume switch metadata */
struct sume_metadata_t {
bit<16> pkt_len; // unsigned int
port_t src_port; // one-hot encoded
port_t dst_port; // one-hot encoded
bit<8> drop;
bit<8> send_dig_to_cpu; // send digest_data to CPU
digest_metadata_t digest_data;
}
// digest metadata to send to CPU
struct digest_metadata_t {
bit<8> src_port;
bit<48> eth_src_addr;
bit<24> unused;
}
我们之前使用的,都是 NetFPGA 是一个不同于我们之前使用的 v1model 的另一个目标架构(target),他们之间最主要的区别,可能就是 P4->NetFPGA 的架构更简单粗暴,比如上图中间的 Match Action Pipeline 把 Ingress 和 Egress 都合并了,然后自带的 metadata 不太一样,但可以实现在 FPGA 硬件的板子上。关于各种不同的目标架构的异同,之后我会找机会单独记录一下。
在看 NetFPGA 的实现之前,我们先简单回顾并且总结一下 Learning Switch 要做的事情:
- 解析头文件
- 根据
dst_addr
转发 - 如果
dst_addr
不在转发的表里的话,广播给除了ingress_port
之外的所有其他的端口 - 根据
src_addr
学习端口和地址的对应关系 - 如果
src_addr
还没有见过的话,就发给 control plane,然后把这个src_addr
加进表里
主要的代码部分如下:
/* 定义可以解析的 Packet 数据结构 */
// standard Ethernet header
header Ethernet_h {
EthernetAddress dstAddr;
EthernetAddress srcAddr;
bit<16> etherType;
}
// IPv4 header without options
header IPv4_h {
bit<4> version;
bit<4> ihl;
bit<8> diffserv;
bit<16> totalLen;
bit<16> identification;
bit<3> flags;
bit<13> fragOffset;
bit<8> ttl;
bit<8> protocol;
bit<16> hdrChecksum;
IPv4Address srcAddr;
IPv4Address dstAddr;
}
// List of all recognized headers
struct Parsed_packet {
Ethernet_h ethernet;
IPv4_h ip;
}
/* 定义 Parser */
parser TopParser(packet_in b,
out Parsed_packet p,
out user_metadata_t user_metadata,
inout sume_metadata_t sume_metadata) {
state start {
b.extract(p.ethernet);
transition select(p.ethernet.etherType) {
IPV4_TYPE: parse_ipv4;
default: reject;
}
}
state parse_ipv4 {
b.extract(p.ip);
transition: accept;
}
}
/* 定义 Control Flow */
control ... {
// 第一个 forward 表
action set_output_port(port_t port) {
sume_metadata.dst_port = port;
}
table forward() {
key = {
headers.ethernet.dstAddr: exact;
}
actions = {
set_output_port;
}
size = 64;
default_action = nop;
}
// 第二个 broadcast 表
action set_broadcast(port_t port) {
sume_metadata.dst_port = port;
}
table broadcast() {
key = {
sume_metadata.src_port: exact;
}
actions = {
set_broadcast;
nop;
}
size = 64;
default_action = nop;
}
// 第三个 smac 表
table smac() {
key = {
headers.ethernet.srcAddr: exact;
}
actions = {
nop;
}
size = 64;
default_action = nop;
}
// 定义发送给 CPU 的操作
action send_to_control() {
sume_metadata.digest_data.src_port = sume_metadata.src_port;
sume_metadata.digest_data.eth_src_addr = headers.ethernet.srcAddr;
sume_metadata.send_dig_to_cpu = 1;
}
apply {
// 尝试 forward
if (!forward.apply().hit) {
// dst_addr 未知的话,就广播
broadcast.apply();
}
// 判断是否需要学习 src_addr
if (!smac.apply().hit) {
// src_addr 未知的话,发给 CPU
send_to_control();
}
}
}
/* 定义 Deparser */
control TopDeparser(packet_out b,
in Parsed_packet p,
in user_metadata_t user_metadata,
inout sume_metadata_t sume_metadata) {
apply {
b.emit(p.ethernet);
b.emit(p.ip);
}
}
然后 control plane 也是用 python 来处理 digest:
...
def learn_digest(pkt):
dig_pkt = Digest_data(str(pkt))
add_to_tables(dig_pkt)
def add_to_tables(dig_pkt):
src_port = dig_pkt.src_port
src_addr = dig_pkt.eth_src_addr
(found, val) = table_cam_read_entry('forward', [src_addr]) # 内置的查表函数
if (!found):
table_cam_add_entry('forward', [src_addr], 'set_output_port', [src_port]) # 内置的在表里添加 entry 的函数
table_cam_add_entry('smac', [src_addr], 'nop', [])
def main():
sniff(iface=DMA_IFACE, prn=learn_digest, count=0)
小结
这篇文章,我们用 P4 分三步实现了链路层的自学习交换机。复习了表的使用,学习了多个表的协同、p4utils
的自动化过程、还有通过 clone
和 digest
两种方式和 controller 通讯的模式。