P4 Tutorial 快速上手 (2) Basic IPv4 Forwarding
提示:本系列仅适用于软件交换机BMv2
P4 Tutorial 快速上手系列 (1)
前言
代码库链接:https://github.com/p4lang/tutorials/tree/master/exercises/basic
在对P4 Tutorial库的粗略介绍后,便正式开始上手之旅。
本文将对最基础的练习进行讲解:basic练习的目标是编写一个具备基于ipv4地址的转发程序。因此,待编写的P4程序需要完成与传统SDN架构中交换机类似的转发操作,即控制平面将路由信息下发至交换机,交换机基于接收的信息填充路由表。具体的规则通常是指示目的 IP 地址映射到下一跃点的 MAC 地址和输出端口。在本练习中,控制平面操作已经被预先定义,仅需基于 P4 实现数据平面逻辑。
在本练习中,将使用以下拓扑。它可以视作Fat-tree的两个 pod,因此可以称为 pod-topo:
转发原理
在传统交换机中实现对数据包转发,实际上仅需明确数据包携带的目的MAC地址,并根据ARP表直接进行转发。但在本练习中,是需要由BMv2充当路由器的功能,即根据数据包的目的IP地址查找路由表,具体过程如下:
1.由于充当三层交换机,在接收的数据包后,需要对IP包头中的首部校验和Header Checksum进行校验,若出错将直接丢弃掉数据包。
2.根据IP Header中目的IP地址字段,通过最长前缀匹配(LPM)的方法获取到数据包的下一跳MAC地址与转发端口。
3.根据IPv4协议,在进行转发前,需要对数据包的MAC帧头中源MAC、目的MAC字段,IP包头的生存时间TTL按协议规定进行更新。
4.重新计算Header Checksum,将数据包转发至查询到的交换机端口。
为基于P4实现上述流程,本练习将在P4.org的BMv2软件交换机中实现的V1Model架构下编写,因此程序中还需要实现包头定义、解析、封装、入/出口定义等固定流程。V1Model 的架构文件可以在以下位置找到:https://github.com/p4lang/p4c/blob/main/p4include/v1model.p4。此文件描述了架构中 P4 可编程元素的接口、支持的外部以及架构的标准元数据字段,建议阅读。
#代码解析
源代码过长,因此不全部复制粘贴到此处。练习中首先提供了一份非完整的代码basic.p4,结构如下:
01-43行----头文件引用、包头部定义,值得注意的是header是一种独特的数据结构定义,多个header最终需要被封装到一个结构体headers中(40-43行)。
45-58行----包头部解析器定义
61-67行----校验和计算
70-104行----入口动作
106-114行----出口动作
116-138行----校验和更新
141-149行----包头填充(逆解析)
151-162行----V1model架构下程序入口以及各函数执行顺序
该不完整的代码还缺少4个部分,分别在包头部解析器定义、校验和验证、查表转发以及包头填充部分。(相较于教程中对校验和验证的忽略,本文进行了补全,因此与Solution中代码有微小不同)
1.包头部解析
parser MyParser(packet_in packet,
out headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
state start {
/* TODO: add parser logic */
transition accept;
}
}
该部分代码的指示BMv2如何提取数据包的头部字段,本质上是一个有限状态机。程序的入口在“state start”。由于本练习旨在以太网的基础上转发IPv4数据包,涉及到以太网帧头以及IPv4包头的修改,因此需要提取这两个部分的字段。包头字段在15-34行已经定义,与以太网帧和IPv4包头格式完全对应。
首先需要提取以太网帧头,并判断上层协议是否为IPv4协议,因此定义新的状态“parse_ethernet”。由于不考虑其他二层协议,因此直接在start状态将直接转换到parse_ethernet状态,即,将上述代码中“transition accept;”转换为改为“transition parse_ethernet;”。
parse_ethernet状态需要完成的操作包括提取帧头部,判断上层协议是否为IPv4协议,若是,则转换到parse_ipv4状态,否则将直接接收。parse_ethernet的定义如下:
state parse_ethernet {
packet.extract(hdr.ethernet); // packet.extract将依据header定义按位提取比特并存在结构体中
transition select(hdr.ethernet.etherType) { // select将根据选取的字段匹配下一状态
TYPE_IPV4: parse_ipv4; // 当hdr.ethernet.etherType与TYPE_IPV4相同,即等于0x800跳转状态
default: accept; // 当无法识别网络层协议号时,默认动作为接收
}
}
由于无需再解析传输层协议,因此parse_ipv4状态下仅需提取IPv4包头,具体定义如下:
state parse_ipv4 {
packet.extract(hdr.ipv4); // 依据header ipv4_t的定义提取各字段
transition accept;
}
以上为basic.p4中包头部解析部分的程序段,需要重点掌握的是几个关键字的用法,包括状态的声明state,状态转移transiton,按header定义提取包头packet.extract()。
2.校验与验证
原教程中未要求此部分内容,但在其问题与思考中提到basic.p4的solution版本能否取代路由器,对比实际的路由器功能,basic.p4缺少了对IP校验和的验证,同时显然并不具备路由功能(因为路由功能由控制器完成)。为了使程序更加完整,应当在MyVerifyChecksum中增加校验和验证,v1model提供了两种校验函数,分别是verify_checksum与verify_checksum_with_payload,在此采用前者完成对IP包头的完整性进行验证。其使用方法与update_checksum类似,具体如下:
control MyVerifyChecksum(inout headers hdr, inout metadata meta) {
apply {
verify_checksum(hdr.ipv4.isValid(), // 仅当IPv4包头存在时启用,{}中为用于hash的数据
{ hdr.ipv4.version, hdr.ipv4.ihl, hdr.ipv4.diffserv, hdr.ipv4.totalLen, dr.ipv4.identification,
hdr.ipv4.flags, hdr.ipv4.fragOffset, hdr.ipv4.ttl, hdr.ipv4.protocol, hdr.ipv4.srcAddr,
hdr.ipv4.dstAddr }, hdr.ipv4.hdrChecksum, HashAlgorithm.csum16); // 用于对比的校验和和采用的hash算法
}
}
当verify_checksum计算所得的结果与hdr.ipv4.hdrChecksum不匹配时,交换机会将standard_metadata中的checksum_error置1,以便在Ingress中处理。因此在Ingress中需增加相应代码进行判断:
if (hdr.ipv4.isValid() && standard_metadata.checksum_error == 0) {
…… // 其他应用
}
3.查表转发
该部分代码位于control MyIngress中,与转发相关的操作均应当位于此阶段。提供的初始代码如下:
action ipv4_forward(macAddr_t dstAddr, egressSpec_t port) {
/* TODO: fill out code in action body */
}
table ipv4_lpm {
key = {
hdr.ipv4.dstAddr: lpm; // 使用IPv4目的地址字段进行匹配
} // 匹配方法为最长前缀匹配(Longest prefix match)
actions = {
ipv4_forward; // 采取上面定义的ipv4_forward
drop;
NoAction; // 无操作,不采取任何action
}
size = 1024; // 匹配表大小(条数)
default_action = NoAction();
}
apply {
/* TODO: fix ingress control logic
* - ipv4_lpm should be applied only when IPv4 header is valid
*/
ipv4_lpm.apply(); // ipv4_lpm为定义的匹配表,apply方法将进行查表操作
}
该段程序在执行时由apply{}开始,目前缺少了对数据包是否为IPv4数据包的判断。在v1model中,在数据包解析阶段(PARSER),交换机根据状态转移结果确定数据包携带了哪些包头,在本练习中共提取了hdr.ethernet和hdr.ipv4两种头部字段。在程序中可以用hdr.xx.isValid()确定数据包是否具有xx的封装,当xx存在与数据包且被解析时,hdr.xx.isValid()会返回布尔型的1用于判断。因此,为判断数据包是否为IPv4数据包,可以使用hdr.ipv4.isValid(),对应代码应修改为:
if (hdr.ipv4.isValid()) {
ipv4_lpm.apply();
}
在应用ipv4_lpm后,交换机根据数据包的目的地址匹配适宜的动作。在练习中,正常数据包都会被执行ipv4_forward动作,该动作会引入两个形参dstAddr和port,分别代表下一跳MAC地址和需要发往的端口号,两个形参的具体数值由控制层面下发的JSON文件决定,各交换机对应的转发规则在pod-topo/sX-runtime.json中,X为交换机的id号。以交换机1为例,处理发往10.0.1.1的数据包时,依据的表项为:
"table": "MyIngress.ipv4_lpm", // 对应的表的位置(MyIngress)与名称(ipv4_lpm)
"match": {
"hdr.ipv4.dstAddr": ["10.0.1.1", 32] // 匹配ipv4的目的地址,值为10.0.1.1,子网掩码32位
},
"action_name": "MyIngress.ipv4_forward", // 对应的动作的位置(MyIngress)与名称(ipv4_forward)
"action_params": { // 相关参数
"dstAddr": "08:00:00:00:01:11", // 下一跳MAC地址:"08:00:00:00:01:11"
"port": 1 // 转发端口:1
}
对于发往10.0.1.1的数据包,交换机1将转发到1端口。在ipv4_forward中确定数据包发往的egress_port,完成对数据包头部的更改,包括替源/目的换MAC地址、生存时间TTL减1。对应代码更改如下:
action ipv4_forward(macAddr_t dstAddr, egressSpec_t port) {
standard_metadata.egress_spec = port; // 令交换机识别数据包需要发往的端口
hdr.ethernet.srcAddr = hdr.ethernet.dstAddr; // MAC地址调整(不懂的温习计算机网络)
hdr.ethernet.dstAddr = dstAddr;
hdr.ipv4.ttl = hdr.ipv4.ttl - 1; // TTL调整(不懂的温习计算机网络)
}
##包头填充
在包头解析阶段将数据包包头对应的比特提取后,需要在逆解析(封装阶段)进行重新填充,代码如下:
control MyDeparser(packet_out packet, in headers hdr) {
apply {
packet.emit(hdr.ethernet); // 与packet.extract相反,填充以太帧头
packet.emit(hdr.ipv4); // 填充IPv4包头
}
}
在经过以上步骤后,一个完整的basic.p4程序就改好啦,可以与solution中文件进行对照以发现自己的代码是否存在错误,接下来就可以编译运行测试一条龙了。
运行结果
连通性实验
在对应文件夹下运行终端,输入make run进行程序编译、拓扑构建以及策略下发:
命令执行结果如下所示:
接下来就可以测试转发程序是否工作啦,使用mininet中常用命令pingall进行测试:
Tutorial库中所有实验都会贴心的进行自动抓包,实验中产生的数据包将以PCAP的文件格式存储在pcaps文件夹下。利用wireshark打开s1-eth1-in.pcap文件可以看到如下所示的数据包。(pcap文件的明明规则为 交换机ID-端口号-入/出方向)
校验和
为了测试校验和检验程序是否生效,可以发送携带错误checksum的数据包进行判断。首先需要确定Checksum数据包能正常转发。利用xterm命令打开 h1 和 h2 终端,先在h2运行python receive.py, 再由h1运行python send.py 10.0.2.2 Test:
对send.py和receive.py的具体说明暂且略过,上述程序将一个携带“Test”内容的TCP报文由h1发送至h2,通过验算可知在发送阶段chksum字段没有出错,因此转发正常进行。值得注意的是,在转发过程中,由于TTL发生了改变,chksum也会逐跳变化。下面需要修改发送程序以插入错误的chksum。打开send.py,找到:
pkt = pkt /IP(dst=addr) / TCP(dport=1234, sport=random.randint(49152,65535)) / sys.argv[2]
将其改为
pkt = pkt /IP(dst=addr, chksum=0) / TCP(dport=1234, sport=random.randint(49152,65535)) / sys.argv[2]
保存后在h1终端执行python send.py 10.0.2.2 Hello? :
可以发现数据包在h1发出时,IP Header中chksum字段被置0,h2未能收到该数据包,说明交换机在检测到校验和出错时直接丢弃了数据包。
总结
以上就是本系列(2)的主要内容,基于basic.p4对P4中常用的函数进行解释说明,对于新手必须熟练掌握常用的关键字,才能高效的进行后续学习和开发。
下一步,将对basic_tunnel进行讲解。