P4-16 Specifications(v1.2.2)学习笔记

本文是学习2021年5月最新的P416语言规范(P416 Language Specification)时所做的学习笔记,以下将P416语言规范简称为规范。同时,为了避免因翻译而导致的问题,文中用加粗的原英文来表达部分术语,翻译成中文的术语也将加粗标记。

目录

术语

  • 体系结构(Architecture):描述一组P4可编程组件以及他们之间的数据平面接口,由厂家提供。可以认为是P4程序跟Target之间的一种“约定”,P4程序员需要针对该“约定”编写程序。
  • 数据平面(Data Plane):数据平面实现分组处理和转发逻辑。
  • 控制平面(Control Plane):控制平面提供了数据平面处理和转发分组前所必须的各种网络信息和转发查询表项。
  • 元数据(Metadata):P4程序的执行过程中生成的中间数据。
  • 分组(Packet):分组数据平面要处理和转发的对象。
  • 分组首部(Packet Header): 分组最开始的一段格式化数据。一个给定的分组可能包含一系列代表不同网络协议的首部
  • 分组有效载荷(Packet Payload): 跟在分组首部后面的分组数据。
  • 分组处理系统(Packet-processing system):为处理网络分组而设计的一种数据处理系统,分组处理系统实现控制平面数据平面算法。
  • 目标(Target):一种可以执行P4程序的分组处理系统Target可以是一个交换机、路由器或者其它可编程转发元件,Target是一个总称。

P4简介

P4是一种语言,全称是Programming Protocol-independent Packet Processors编程协议无关的分组处理器),用于表示可编程转发元件(如硬件或软件交换机、网络接口卡、路由器或网络设备)的数据平面如何处理Packet。P4仅用于指定Target数据平面功能以及部分控制平面数据平面通信的接口,但P4不能用来描述Target控制平面功能。

P4可编程交换机与传统交换机的区别

  • 协议无关性:P4交换机是协议无关的,交换机不与任何特定的网络协议绑定,程序员可以通过编程自定义各种数据平面协议和分组的处理转发逻辑。
  • 目标无关性:用户不需要关心底层硬件的细节就可对分组的处理方式进行编程。这一特性通过P4前后端编译器实现,前端编译器将P4高级语言程序转换成中间表示,后端编译器将中间表示编译成设备配置,自动配置目标设备。
  • 可重构性:在不更换硬件的前提下,允许用户随时改变分组的处理和转发方式 ,并在编译后重新配置交换机。
P4-16 Specifications(v1.2.2)学习笔记

核心抽象

  • Header:描述在一个分组内的各个协议首部的格式。

  • Parser:描述接收到的分组中允许的首部序列、如何解析这些首部序列、以及要从分组中提取的首部和字段。

  • Tables:将用户定义的keyAction相关联。 Table泛化了传统的交换机表,可用于实现路由表、流表、访问控制列表和其它用户自定义的表。

  • Actions: 描述如何处理首部字段和MetadataActions可以包含控制平面在运行时提供的数据。

  • Match-Action Units:执行以下操作序列:首先,根据分组字段或所计算出的Metadata来构造key;然后,使用构造的keyTable中查找,选择要执行的Action;最后,执行所选的Action

  • Control Flow:描述分组在Target上的处理流程,包括Match-Action Units调用的数据依赖序列。也可以用来实现Deparser

  • Extern Object:特定于Architecture的第三方库,可以由P4程序通过定义良好的API进行调用。

  • User-defined Metadata:用户自定义的数据结构,与分组相关联。

  • Intrinsic Metadata:Architecture提供的元数据,例如,所接收到的分组的输入端口。

用P4编程Target时的一个典型工具工作流如下图所示

P4-16 Specifications(v1.2.2)学习笔记

Target制造商为Target提供硬件或软件实现框架、Architecture定义和P4编译器。P4程序员为特定的Architecture编写程序(图中绿色区块),该Architecture定义了Target上的一组P4可编程组件及其外部数据平面接口。

编译一组P4程序会产生两个工件:

  • 数据平面配置,用于在数据平面实现由 P4 程序指定的转发逻辑
  • API,用于管理数据平面对象状态

BNF范式(巴科斯范式)

规范中使用BNF范式来给出P4的语法,因此,在介绍具体语法之前,先简单描述一下BNF范式。本部分内容不是规范中介绍的内容。

巴科斯范式是以美国人巴科斯(Backus)和丹麦人诺尔(Naur)的名字命名的一种形式化的语法表示方法,用来描述语法的一种形式体系,是一种典型的元语言,又称巴科斯-诺尔形式(Backus-Naur Form,BNF)。它不仅能严格地表示语法规则,而且所描述的语法是与上下文无关的。

BNF表示语法规则的方式为:

  • 非终结符用尖括号括起
  • 每条规则的左部是一个非终结符,右部是由非终结符和终结符组成的一个符号串,中间一般以“: :=”分开
  • 具有相同左部的规则可以共用一个左部,各右部之间以直竖“|”隔开
  • 双引号(" ")内包含的字符代表着这些字符本身,而双引号本身用double_quote来代表
  • 在双引号外的字(有可能有下划线)代表着语法部分
  • 尖括号(< >)内包含的为必选项
  • 方括号([ ])内包含的为可选项
  • 花括号( { } )内包含的为可重复0至无数次的项
  • 竖线( | )表示在其左右两边任选一项,相当于"OR"的意思。
  • ::= 是“被定义为”的意思

规范中,所使用的BNF表示与上述略有不同:

  • 字母大写的符号表示终结符,小写字母表示的符号为非终结符

  • “被定义为”用冒号(:)表示,而不是“: : =”

    nonTypeName
    : IDENTIFIER
    | APPLY
    | KEY
    | ACTIONS
    | STATE
    | ENTRIES
    | TYPE
    ;
    name
    : nonTypeName
    | TYPE_IDENTIFIER
    ;
    

如上述例子表示的是,一个name被定义为一个nonTypeName或者一个TYPE_IDENTIFIER,一个nonTypeName被定义为一个IDENTIFIER或者APPLY...或者一个TYPE。全部大写的符号是终结符,无须再定义。

very_simple_switch(VSS)

接下来将介绍规范中给出的very_simple_switch例子,以下称该例子为VSS。

VSS Architecture 概览

P4-16 Specifications(v1.2.2)学习笔记

如图所示,这是一个最简单的P4可编程Architecture。VSS可以从8个以太网入端口(最左边的三个蓝色箭头)、再循环通道(最下面的从右到左的蓝色长箭头)或者与CPU(控制平面)直接相连的端口(左上角的蓝色箭头)接收分组。VSS只有唯一的一个Parser,其输出到唯一的一个Match-Action PipelineMatch-Action Pipeline输出到唯一的一个Deparser,在退出Deparser后,分组被传送到11个出端口(8个以太网出端口+3个特殊端口)中的某一个端口。3个特殊端口分别为:

  • 被送到“CPU Port”的分组将被传送到控制平面

  • 被送到“Drop Port”的分组将被丢弃

  • 被传送到“Recirculate Port”的分组将通过再循环管道回到入端口

图中白色的模块是可编程的部分,用户必须提供对应的P4程序来指定每个白色模块的行为。红色箭头表示用户自定义数据。青色区块是功能固定的组件。绿色箭头是数据平面接口,用来在功能固定的模块和可编程模块之间传递信息——在P4程序中作为Intrinsic Metadata

VSS Architecture 固定功能模块

  • Arbiter Block(仲裁模块)

    • 从11个端口(8个以太网入端口+3个特殊端口)中的某一个端口接收分组
    • 对于从以太网入端口收到的分组,仲裁模块计算以太网尾部校验和并且进行验证。如果校验和不匹配,该分组将被丢弃。如果校验和匹配,将尾部校验和字段从分组有效荷载中移除。
    • 如果有多个分组可用,仲裁模块需要运行仲裁算法来决定哪个分组先处理,其余的分组在等待队列中排队。
    • 如果仲裁模块正在处理前一个分组,并且等待队列满了,则输入端口直接丢弃该分组
    • 在收到一个分组后,仲裁模块设置inCtrl.inputPort的值,该值将作为Match-Action Pipeline的一个输入,以标识该分组的源输入端口。物理以太网端口编号为0~7,再循环通道端口编号为13,CPU端口编号为14。
  • Parser Runtime Block(解析器运行时模块)

    解析器运行时模块和Parser协同工作,它基于解析操作给Match-Action Pipeline提供一个错误代码,并且给解复用模块提供分组有效荷载的信息(比如剩余有效荷载的大小等)。从解析器运行时模块到解复用模块的箭头表示从Parser到解复用器的附加信息流:正在处理的分组以及解析结束的分组内的偏移量(即分组有效负载的开始位置)。一旦Parser处理完一个分组,Match-Action Pipeline就会被调用。Match-Action Pipeline以相关的MetadataPacket HeadersUser-defined Metadata)作为输入。

  • Demux Block(解复用模块)

    解复用模块的核心功能是接收来自Deparser的新首部和来自Parser有效荷载,将它们重组成一个新的分组后发送到正确的出端口。出端口由Match-Action Pipeline设置的outCtrl.outPort值来指定:

    • 送到Drop端口的分组将被丢弃
    • 送到以太网端口(编号为0~7)的分组将会被发送到对应的物理接口。如果该输出接口正在发送另一个分组,则该分组会被放在等待队列里。当要发送该分组时,物理接口会计算一个正确的以太网校验和尾部并追加到分组上。
    • 送到CPU端口的分组将会传送到控制平面。如果是这种情况,被传送到CPU的将是原始分组而不是重组后的新分组,重组后的新分组将会被丢弃。
    • 送到再循环通道端口的分组将会从再循环通道返回到入端口。当一个分组无法通过单一通道完成处理时,再循环通道就非常有用。
    • 如果outCtrl.outPort制定的出端口号是非法的值(比如端口号为9,前面设置的端口号只有0~7, 13和14),该分组将被丢弃。
    • 最后,如果解复用模块正在处理前一个分组并且等待队列已经满了,那么该分组将会被直接丢弃。

VSS程序解析

在此部分将对VSS程序代码进行解析,同时介绍所涉及到的P4语法,完整代码将附在文末。

1. include、typedef 和 type

# include <core.p4>
# include "very_simple_switch_model.p4"
typedef bit<48> EthernetAddress;
typedef bit<32> IPv4Address;

与一个C++程序类似,P4通过# include预处理命令包含一些外部文件。

core.p4是P4的一个核心库,包含对大多数程序有用的声明,例如它包含预定义的packet_inpacket_out外部对象的声明,这些对象在ParserDeparser中用于访问分组数据。同时,核心库还定义了一些标准数据类型和错误代码。

very_simple_switch_model.p4是VSS Architecture的声明文件,包含了对ParserDeparserPackage等不可缺少的重要组件的声明。very_simple_switch_model.p4的部分声明(VSS涉及到的)将附在文末。

与C++类似,关键字typedef可以给类型取一个别名:

typedef typeName newName;

上述语句给typeName类型起了一个别名newName。在使用上,newNametypeName完全一样,两者只是名字不一样,实际上是同一个东西。

typedef有明显区别的是关键字type,它用以引入一个全新的类型。

type typeName newType;

上述语句引入了一种全新的类型,注意newTypetypeName是两种不同的类型。如果这两种类型要互相赋值,则需要强制类型转换:

type bit<32> U32;
U32 x = (U32)0;

在描述需要通过通信信道(比如控制平面API或者要被送到控制平面的网络分组)与控制平面进行交换的P4值时,通常使用type关键字。例如:

type bit<9> PortId_t;

上述语句定义了一个全新的类型PortId_t,用以表示长度为 9 比特的端口号。这样可以避免端口号被进行算数运算,因为新引入的类型PortId_t不支持算术运算(尽管bit<9>类型支持算数运算,但他们是不同的两种类型)。

注意,并不是所有类型都支持typedeftype关键字:

P4-16 Specifications(v1.2.2)学习笔记

2. header、基础数据类型和操作

header Ethernet_h {
    EthernetAddress dstAddr; // 目的地址
    EthernetAddress srcAddr; // 源地址
    bit<16> etherType;       // 上层协议类型
}

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;     // 目的地址
}

分组首部用关键字header定义。header是P4的派生类型,跟c++中的struct关键字类似。header定义了一个特定首部的所有字段。注意,header类型除了显示定义的字段外,还有一个bool类型的隐藏字段validity,表示该首部是否有效,初始值为false可以使用以下 3 个方法来对validity字段进行操作:

  • isValid()方法: 返回validity字段的值.
  • setValid() 方法:将validity字段值设为true
  • setInvalid() 方法:将validity字段值设为false

如上面的代码所示,VSS定义了两个首部的格式:以太帧首部IPv4协议首部bit<n>是基本数据类型中的Integer类型。Integer是整数类型的统称,不是一个标准的基本数据类型,根据有符号和无符号,定长和不定长,分为以下几种形式:

基础类型符号 说明
bit<w> 表示长度为 w 比特的无符号整数,也叫bit-string
int<w> 表示长度为 w 比特的有符号整数
varbit<n> 长度最多w 比特的不定长无符号整数
int 无限精度有符号整数
Integer的字面值

一个整数值可能包含某些表示整数类型的前缀:

  • 表示数值进制的前缀:

    • 0x 或者 0X 表示 16 进制
    • 0o 或 0O 表示 8 进制,注意与C++区分,必须是数字0+字母o/O,而不是单独一个数字0。C++中 8 进制用单一数字 0 作为前缀
    • 0d 或 0D 表示 10 进制
    • 0b 或 0B 表示 2 进制
    • 不加以上任何前缀,默认为 10 进制
  • 表示长度和有符号无符号属性的前缀:

    • 长度用一个数字表示,后面紧跟着字母s/w,表示有/无符号属性

    • nw 表示长度为 n 比特的无符号整数,等价于bit<n>类型

    • ns 表示长度为 n 比特的有符号整数,等价于int<n>类型

    • 不带以上任何前缀的等价于int类型

  • 在数值中间可能出现下划线( _ ),这在P4中也是合法的,计算具体数值时直接忽略下划线

  • 长度为n比特的数字,每个比特从0~n-1编号。比特位0是最低有效位(最右边),比特位n-1是最高有效位(最左边)

Integer的操作符

P4中的所有二元操作符(除了移位操作)都要求两个操作数的类型和长度一样。通常来说,对于w比特的整数,若其值超出可表示范围,P4仅保留其低w位。此外,P4支持可选的饱和算术,与传统的取模算术不同。具体体现在溢出时采取的方式不同:假设一个长度为8比特的有符号运算结果的真实值为130,。我们都知道8比特长度能表示的有符号数值范围为-128~127,显然130不在范围内。对于饱和算术,该结果将会尽可能的接近真实数值,即该结果将被设为127;而对于取模算术,将130对128取模后得到-126(不同的取模运算实现方式得到的值不一样,这里是采用向上取整的方式,采用截断方式和向下取整结果是2)。同样的,对于8比特无符号数258,P4的结果为255,而不是2。

无符号整数bit<w>类型

操作符 举例 功能描述
负号(-) -X 与C++一样,其结果为2W-X
正号(+) +X 等价于X
减号(-) a-b 与C++一样,结果为无符号数,等价于a+(-b)
加号(+)、乘号(*) a+b、a*b 与C++一样,溢出则只取低w位
饱和算术减 |-| 采用饱和算术进行减法
饱和算术加 |+| 采用饱和算术进行加法
按位与、或、异或、取反 &、|、^、~ 与C++一样
比较运算符 ==、!=、>、<、>=、<= 与C++一样
逻辑左移、右移 X<<n、X>>n 左操作数是无符号整数,右操作数必须是bit<w>类型或者一个非负整数,如果 n 大于 X 的长度,则 X 所有位都变为 0
提取 X=E[L:R]、e[L:R]=x w>L>=R>=0,将 E 的R~L(包括R和L)比特位提取出来赋值给X。X 结果是一个长度为L-R+1的无符号整数;做左值时表示将e的R~L(包括R和L)比特位设置为x,其他位不变。若x是有符号整数,则视为无符号整数。注意P4比特位编号是从右往左,从0到w-1。
拼接 a++b 将b拼接在a的后面,结果的总长度为a的长度和b的长度和,类型和符号取决于a

有符号整数int<w>类型

操作符 举例 功能描述
负号(-) -X 与C++一样
正号(+) +X 等价于X
加号(+)、减号(-)、乘号(*) a+b、a-b、a*b 与C++一样,溢出则只取低w位
饱和算术减 |-| 采用饱和算术进行减法
饱和算术加 |+| 采用饱和算术进行加法
按位与、或、异或、取反 &、|、^、~ 与C++一样
比较运算符 ==、!=、>、<、>=、<= 与C++一样
左移、右移 X<<n、X>>n 左操作数是有符号整数,右操作数必须是bit<w>类型或者非负整数,左移操作,与无符号移位操作一样;右移操作,高位补符号位
提取 X=E[L:R]、e[L:R]=x w>L>=R>=0,将 E 的R~L(包括R和L)比特位提取出来赋值给X。X 结果是一个长度为L-R+1的无符号整数;做左值时表示将e的R~L(包括R和L)比特位设置为x,其他位不变。若x是有符号整数,则视为无符号整数。注意P4比特位编号是从右往左,从0到w-1。
拼接 a++b 将b拼接在a的后面,结果的总长度为a的长度和b的长度和,类型和符号取决于a

任意精度整数int类型

操作符 举例 功能描述
负号(-) -X 与C++一样
正号(+) +X 等价于X
加号(+)、减号(-)、乘号(*) a+b、a-b、a*b 与C++一样,因为是无限精度,不会溢出,所以也不存在饱和算术
除号 a/b 正整数间的截断除法
取模 a%b 正整数间的取模运算
饱和运算操作和按位操作 |+|、|-|、&、|、^、~ 未定义
比较运算符 ==、!=、>、<、>=、<= 与C++一样
左移、右移 X<<n、X>>n 左操作数是有符号整数,右操作数必须是正整数,左移等价于X*2n,右移等价于X/2n
提取 X=E[L:R]、e[L:R]=x w>L>=R>=0,将 E 的R~L(包括R和L)比特位提取出来赋值给X。X 结果是一个长度为L-R+1的无符号整数;做左值时表示将e的R~L(包括R和L)比特位设置为x,其他位不变。若x是有符号整数,则视为无符号整数。注意P4比特位编号是从右往左,从0到w-1。
拼接 a++b 将b拼接在a的后面,结果的总长度为a的长度和b的长度和,类型和符号取决于a

不定长整数varbit<n>类型

varbit只支持以下操作:

  • 赋值操作。a=b,a和b必须有一样的静态长度。在执行赋值的时候,a的动态长度会被设置为b的动态宽度。
  • 比较是否相等。当且仅当a和b的长度和每一位的值都一样时,两者才相等。
P4的其他基础类型
基础类型 类型值(core.p4中定义的)
void ——
error ParseError,PacketTooShort等
string 仅允许用于表示编译时常量字符串
match_kind exact,ternary,lpm
bool true,false
类型转化
显式转换
类型转换 说明
bit<1> <-> bool 0是false,1为true,反之亦然
int -> bool 只有当int的值为 0 或 1 时,对应的bool值为falsetrue
int<w> -> bit<w> 所有比特位不变,把负数当做正数
bit<w> -> int<w> 所有比特位不变,如果最高位为1,则视为负数
bit<w> -> bit<x> 如果 w>x,保留低 x 位;如果 w<x,高位补 0
int<w> -> int<x> 如果 w>x,保留低 x 位;如果 w<x,高位补符号位
bit<w> -> int 所有比特位不变,结果永远为正
int<w> -> int 所有比特位不变,结果可能为负
int -> bit<w> 转为补码后,保留低 w 位
int -> int<w> 转为补码后,保留低 w 位
隐式转化

为了保持语言简单并避免引入隐藏代价,P4只隐式地将int类型转换为固定宽度类型,并将具有基础类型的enum转换为基础类型。特别是,对int类型的表达式和具有固定宽度类型的表达式使用二目操作,会将int类型的操作数隐式转换为另一个操作数的类型。

Mask(&&&) 操作和 Range(..) 操作

&&&Mask操作,接受两个bit<w>类型的操作数,得到一个集合,集合中的元素类型为bit<w>

a &&& b={c | bit<w> c, c 满足a&b==c&b}

..Range操作,接受两个类型为int<w>bit<w>的操作数,得到一个集合,集合中的元素包括两个操作数之间的所有连续整数。

4w5..4w8   // 得到集合{4w5, 4w6, 4w7, 4w8}

3. struct、header stack 和 header union

struct Parsed_packet {
    Ethernet_h ethernet;
    IPv4_h ip;
}
struct

header类似,struct也是一个派生类型,具体用法跟C++的结构体一样。没有任何字段的空结构体也是合法的。这里主要介绍另外两个跟header密切相关的派生类型header stackheader_unions

header stack

header stack类似C++中的数组,该数组元素是header类型的,例如:

header Mpls_h {
	bit<20> label;
	bit<3> tc;
	bit bos;
	bit<8> ttl;
}
Mpls_h[10] mpls;

上例中,定义了一个名为mplsheader stack,它包含10个元素,每个元素都是Mpls_h类型的。

header stack的操作

假设有一个名为 hs,大小为 n 的header stack,则 hs 有如下操作

  • hs[index]:下标为index的元素的引用,0<=index<n
  • hs.size:返回hs的大小
  • hs.nextIndex:计数器,hs.next初始值为 0,每当成功调用一次extract,计数器会自动加 1。
  • hs.next:返回下标为hs.nextIndex的元素的引用,只能在Parser中使用。hs.next初始指向 hs 的第一个元素,每当成功调用一次extract,指针会自动往前推进。当hs.nextIndex大于等于 n 时,访问hs.next会出现error.StackOutOfBounds的错误
  • hs.lastIndex:返回hs.nextIndex-1,只能在Parser中使用。
  • hs.last:返回下标为hs.nextIndex-1的元素的引用,只能在Parser中使用。当hs.nextIndex等于 0 时,访问hs.last会出现error.StackOutOfBounds的错误
  • hs.push_front(int count):将hs的元素依次右移count个位置,原hs的前count个元素无效,后count个元素被消除
  • hs.pop_front(int count):将hs的元素依次左移count个位置,原hs的前count个元素被消除,后count个元素无效
header union

header_union是多个header类型元素的共同体,所有元素共用存储资源,并且最多只能选择其中的一个元素。例如:

header_union IP_h {
	IPv4_h v4;
	IPv6_h v6;
}

上例定义了一个名为IP_h的共同体,包含两个协议首部IPv4和IPv6,但是在任何时刻,只有其中的一个协议生效。即,任何时刻,header_union里面的Header,只有一个Headervalidity字段是true,其余所有Headervalidity字段都是false

4. error

error {
    IPv4OptionsNotSupported, 
    IPv4IncorrectVersion,
    IPv4ChecksumError
}

自定义了三个错误代码,在core.p4中也预定义了一些错误代码:

error {
    NoError,           // No error.
    PacketTooShort,    // Not enough bits in packet for 'extract'.
    NoMatch,           // 'select' expression has no matches.
    StackOutOfBounds,  // Reference to invalid element of a header stack.
    HeaderTooShort,    // Extracting too many bits into a varbit field.
    ParserTimeout,     // Parser execution time limit exceeded.
    ParserInvalidArgument  // Parser operation was called with a value
    					   // not supported by the implementation.
}

除了上述用户自定义的数据结构外,通常一个P4程序还需要实现ParserMatch-Action管道和Deparser三个关键模块。在VSS中,这三个模块的功能如下:

  • Parser:需要识别的Header为Ethernet Header,其后跟着IPv4 Header。如果这两个Header有一个丢失,解析终止同时记录一个错误代码。否则,它会将这些Header中的信息提取到数据结构Parsed_packet中。

  • Match-Action管道:它包含四个Match-Action单元(即有四个Table),如下图所示:

    • 如果有任何解析错误出现,该分组将被丢弃。实现方式为将outCtrl.outPort的值设置为DROP_PORT

    • 第一个Table使用IPv4协议目的地址来决定outCtrl.outPort和下一跳的IPv4地址。如果查表失败,丢弃分组。此外,该表还减少IPv4的ttl字段值。

    • 第二个Table检查ttl的值:如果ttl值为0,从CPU端口将分组发送到控制平面

    • 第三个Table使用下一跳的IPV4地址(第一个表计算的)来决定下一跳的以太网地址。

    • 最后一个Table使用outCtrl.outPort来标识当前交换机的源以太网地址,该地址在输出分组中设置。

      P4-16 Specifications(v1.2.2)学习笔记

  • Deparser:通过重新组装Match-Action管道计算的以太帧Header和IPv4协议Header来构造输出分组

基于上述功能描述,下面将介绍ParserMatch-Action管道和Deparser三个关键模块的代码实现。

5. Parser

// Parser section
parser TopParser(packet_in b, out Parsed_packet p) {
    Checksum16() ck; // instantiate checksum unit
    state start {
        b.extract(p.ethernet);
        transition select(p.ethernet.etherType) {
        0x0800: parse_ipv4;
        // no default rule: all other packets rejected
        } 
    }
    state parse_ipv4 {
        b.extract(p.ip);
        verify(p.ip.version == 4w4, error.IPv4IncorrectVersion);
        verify(p.ip.ihl == 4w5, error.IPv4OptionsNotSupported);
        ck.clear();
        ck.update(p.ip);
        // Verify that packet checksum is zero
        verify(ck.get() == 16w0, error.IPv4ChecksumError);
        transition accept;
    } 
}

P4中的Parser实际上是一个有穷状态机(Finite State Machine,FSM),包含一个起始状态(start)和两个终结状态(accept,表示解析成功,和reject,表示解析失败)。注意,start状态是Parser的一部分,是程序员提供的状态,而acceptreject逻辑上实在Parser之外的,与程序员提供的状态不同,如下图所示。一个Parser从一个start状态开始,直到到达一个accept或者reject状态为止。Architecture必须指定到达accept或者reject状态时的具体行为。

P4-16 Specifications(v1.2.2)学习笔记

VSS的Parser声明如下:

parser Parser<H>(packet_in b, 
                out H parsedHeaders);

可以看到,该Parser接收两个参数,一个是packet_in类型的,表示输入分组。另一个是H类型的,表示一个已解析的HeaderH是一个泛型,不是一个具体的类型,程序员需要自己用自定义的Header类型替代。注意到在第二个参数的最前面有一个out关键字。P4中有三个方向关键字,用以指定参数的方向属性:

in:输入参数,该参数是只读的,不能作为左值

out:输出参数,该参数通常是未初始化的,并且必须是左值。在执行函数调用后,参数的值被复制到该左值的相应存储位置

inout:既可以是in也可以是out,必须是左值

从代码上可以看到,VSS自定义了两个状态startparse_ipv4。每个状态以state关键字标识,其后跟着状态名,然后是具体的状态体(用花括号包含)。不难发现,两个状态体*有的关键字有extracttransitionselect,我们将逐一介绍。

extract

extract:前面提到在core.p4中声明了一个内置的外部类型packet_in,表示输入分组

extern packet_in {
    void extract<T>(out T headerLvalue);
    void extract<T>(out T variableSizeHeader, in bit<32> varFieldSizeBits);
    T lookahead<T>();
    bit<32> length(); // This method may be unavailable in some architectures
    void advance(bit<32> bits);
}

extractpacket_in中的一个方法。Parser通过调用extract方法来抽取分组数据。可以看到extract有两个变体:

void extract<T>(out T headerLeftValue);

该方法只有一个参数headerLeftValue,用以提取长度固定的header类型首部。如果该方法成功执行,headerLeftValue的值将被从对应分组中提取的数据字段填充,并且validity字段被设置为true。例如,在VSS中的start状态中,如果extract方法执行成功,那么p.ethernet的各个字段将被从分组b中抽取的数据填充,并且p.ethernet的隐含字段validity将被置为true

此外,还有一种包含两个参数的变体:

void extract<T>(out T headerLvalue, in bit<32> variableFieldSize);

该方法中,headerLeftValue必须是恰好包含一个varbit类型的字段。第二个参数variableFieldSize是一个bit<32>类型的整数,表示该首部中唯一的varbit字段的长度。下面介绍完lookahead方法后,会举例说明如何使用具有两个参数的变体。

packet_in 中的其他方法介绍
lookahead
T lookahead<T>();

lookahead方法与extract类似,用以抽取数据,但不同的地方在于:

  • extract抽取的数据填充在headerLeftValue参数中,而lookahead是返回所抽取的数据
  • extract抽取完数据后,nextBitIndex指针会往前推进,而使用lookahead方法,nextBitIndex指针不推进
  • T 必须是固定长度的类型,即,T 不能包含varbit字段

穿插一点补充内容:

关于如何使用extract的第二个变体:

前面提到,“extract第二个变体需要两个参数,headerLeftValue必须是恰好包含一个varbit类型的字段。第二个参数variableFieldSize是一个bit<32>类型的整数,表示该首部中唯一的varbit字段的长度”,那么varbit字段的长度的长度该如何确定呢?换句话说,第二个参数如何确定?事实上,我们可以结合lookahead方法,计算出该varbit字段的长度,考虑一下例子:

header Tcp_option_sack_h {
    bit<8> kind;
    bit<8> length;
    varbit<256> sack;
}

struct Tcp_option_sack_top {
    bit<8> kind;
    bit<8> length;
}

parser Tcp_option_parser(packet_in b, out Tcp_option_stack vec) {
    state start {
        . . . . . . .
    }
    . . . . . . .  // 其他的state定义,不具体列出
    state parse_tcp_option_sack {
        bit<8> n = b.lookahead<Tcp_option_sack_top>().length;
        b.extract(vec.next.sack, (bit<32>) (8 * n - 16));
        transition start;
    } 
}

在上面的例子中,我们定义一个包含一个varbit<256> sack字段的首部header Tcp_option_sack_h,该首部还包含另外两个字段:kind(表示类型)和length(表示该首部的总长度,单位是字节)。现在如果能够确定首部Tcp_option_sack_h的总长度 n字节,那么就可以计算出sack的长度是n-2(字节)了。于是,我们可以按照以下步骤来做:

  • 先定义一个struct Tcp_option_sack_top,仅包含Tcp_option_sack_h的两个固定字段,然后用lookahead方法提取分组中的Tcp_option_sack_top数据。Tcp_option_sack_top是固定长度,因此可以使用lookahead方法。从而,我们可以得到length字段的值,这样就得到了首部的总长度:

    bit<8> n = b.lookahead<Tcp_option_sack_top>().length;
    
  • 基于此,再进一步调用extract方法,就可以提取出整个首部了。由于lookahead方法不会使nextBitIndex指针往前推进,所以对extract方法不会产生影响。

    b.extract(vec.next.sack, (bit<32>) (8 * n - 16));   // n的单位是字节,第二个参数的单位是比特,因此需要进行转化
    

本例子讲述了如何通过lookahead方法和extract方法相结合的方式,提取包含varbit字段的首部

advance

advance方法用以跳过bits个比特的数据。由于上面提到extract抽取完数据后,nextBitIndex指针会往后推进,因此也可以使用extract方法将数据抽取到下划线标识符上使指针往前推进,从而跳过接下来的一些比特:

b.extract<T>(_)
Parser中的 transition 和 select

transition语句用控制状态转移,类似goto语句。select语句类似switch,但是与之不同的是,在P4中,default_标签后面的case是不可到达的。这意味着,select中的标签可能是可以重复的,如果重复的标签是在default_标签之后,则后面的重复标签不可到达。

Parser中的 checksum 和 verify

checksum是一个外部函数,用来计算检验和:

extern Checksum16 {
    Checksum16(); // constructor
    void clear(); // prepare unit for computation
    void update<T>(in T data); // add data to checksum
    void remove<T>(in T data); // remove data from existing checksum
    bit<16> get(); // get the checksum for the data added since last clear
}

verify也是一个外部函数,只能在Parser中调用:

extern void verify(in bool condition, in error err);

如果conditiontrue,该方法没有任何作用。如果conditionfalse,则会立即transitionreject状态,设置解析错误代码为err(第二个参数)

6. Match-Action Pipeline

// Match-action pipeline section
control TopPipe(inout Parsed_packet headers,
                in error parseError, // parser error
                in InControl inCtrl, // input port
                out OutControl outCtrl) {
    IPv4Address nextHop; // local variable
    /***************table 1***************/
    action Drop_action() {
        . . . . . .
    }
    action Set_nhop(IPv4Address ipv4_dest, PortId port) {
        . . . . . .
    }
    table ipv4_match {
        . . . . . .
    }
    /***************table 2***************/
    action Send_to_cpu() {
        . . . . . . 
    }
    table check_ttl {
        . . . . . . 
    }
    /***************table 3***************/
    action Set_dmac(EthernetAddress dmac) {
        . . . . . . 
    }
    table dmac {
       . . . . . . 
    }
    /***************table 4***************/
    action Set_smac(EthernetAddress smac) {
        . . . . . .
    }
    table smac {
        . . . . . .
    }
    apply {
        . . . . . . 
    } 
}

Parser负责从分组中提取首部数据。这些首部(和其他的Metadata)可以在控制模块中进行操作和转换。实现控制模块功能的主体是Match-Action Units,其核心组件是tableaction

action控制平面可以动态影响数据平面行为的主要结构:

action actionName(parameterList){动作主体}

参数列表中,没有方向属性的参数称为“action data”,这些参数需要放在参数列表的末尾,并且这些参数值来自表项(table entries)(例如,由控制平面指定、默认的default_action属性或者const entries属性)。动作主体用花括号包含,是一些列的语句或者申明,但是不能有switch语句。

table描述了一个Match-Action Unit,执行以下步骤(如下图所示):

  • 构造Key

  • 查表。表中通过key来查找表项(match的过程),查找的结果是一个action。lookup table是一个有限映射,其内容由控制平面通过单独的控制平面API进行异步操作(读/写)。

  • 执行对应的action

    P4-16 Specifications(v1.2.2)学习笔记

一个标准的table需要包含keyaction属性,可选的包含default_actionsize属性:

  • key是形如(expression:match_kind)的一对二元组。expression指定key的内容,match_kind指定key的匹配方式。P4核心库定义了三种匹配方式:exact(完全匹配)、lpm(最长前缀匹配)和ternary(三元匹配,即匹配某些指定的比特位,例如用mask操作指定一些比特位,可参见下面例子中的注释)。

  • default_action,当查表失败时,执行default_action指定的动作。如果没有显示的指定default_action,编译器会默认为每个table指定default_actionNoAction

  • size是一个整数,指定该table所需的大小。

  • const entries。前面提到,table entries可以由const entries指定。我们可以事先定义一些表项,用以在编译时初始化表项,下面这个例子分别使用exactternary方式进行两个key匹配,同时用const entries指定了一些常量表项。同时,在常量表项中用注释解释了三元匹配:

    header hdr {
        bit<8> e;
        bit<16> t;
        bit<8> l;
        bit<8> r;
        bit<1> v;
    }
    struct Header_t {
    	hdr h;
    }
    struct Meta_t {}
    control ingress(inout Header_t h, inout Meta_t m,
        inout standard_metadata_t standard_meta) {
        action a() { standard_meta.egress_spec = 0; }
        action a_with_control_params(bit<9> x) { standard_meta.egress_spec = x; }
        
        table t_exact_ternary {
            key = {
                h.h.e : exact;
                h.h.t : ternary;
        	}
            actions = {
                a;
                a_with_control_params;
            }
            
        	default_action = a;
            
            const entries = {
                /** 以0x1111 &&& 0xF为例,解释一下三元匹配
                 *  0001 0001 0001 0001 & 0000 0000 0000 1111 = 0000 0000 0000 0001
                 *  xxxx xxxx xxxx 0001 & 0000 0000 0000 1111 = 0000 0000 0000 0001
                 *  这意味着,只要低4位是0001,高12位不管是什么都可以匹配成功
                 *  这么匹配有可能会匹配到多个结果,用户需要为每个表项定一个优先级,
                 *  当存在多个匹配时,根据用户定义好的优先级进行选择,最终只能匹配一个结果*/
                (0x01, 0x1111 &&& 0xF ) : a_with_control_params(1);
                (0x02, 0x1181 ) : a_with_control_params(2);
                (0x03, 0x1111 &&& 0xF000) : a_with_control_params(3);
                (0x04, 0x1211 &&& 0x02F0) : a_with_control_params(4);
                (0x04, 0x1311 &&& 0x02F0) : a_with_control_params(5);
                (0x06, _ ) : a_with_control_params(6);
                _ : a;
            } 
        } 
    }
    
  • 当定义好 Match-action Units后,还需要使用apply来调用

下面将分别介绍每个table以及所涉及到的action实现

7. the first table in VSS

/***************table 1***************/ 
action Drop_action() {
    outCtrl.outputPort = DROP_PORT;
}
action Set_nhop(IPv4Address ipv4_dest, PortId port) {
    nextHop = ipv4_dest;
    headers.ip.ttl = headers.ip.ttl - 1;
    outCtrl.outputPort = port;
}
table ipv4_match {
    key = { headers.ip.dstAddr: lpm; } // longest-prefix match
    actions = {
       Drop_action;
       Set_nhop;
    }
    size = 1024;
    default_action = Drop_action;
}

第一个table使用IPv4协议目的地址来决定outCtrl.outPort和下一跳的IPv4地址。如果查表失败,丢弃分组。此外,该表还减少IPv4的ttl字段值。

为了实现上述功能,首先定义了两个actionDrop_action通过把outCtrl.outPort设为DROP_PORT来指示解复用模块应该丢弃该分组Set_nhop接收两个参数,通过参数ipv4_dest来获取IPv4协议目的地址,通过参数port来指定输出端口号outCtrl.outPortSet_nhop用自定义的nexthop局部变量来表示下一跳的IPv4地址。同时,还应该将首部ttl字段值减 1。ipv4_destport没有方向属性,都是“action data”,由表项提供。

table的主体包含了keyactionssizedefault_action属性。key指定根据首部dstAddr字段来查表,匹配方式是lpm(最长前缀匹配)。actions指定匹配的结果有Drop_actionSet_nhop两种。size指定table的大小为1024字节。default_action指定默认动作为Drop_action

8. the second table in VSS

action Send_to_cpu() {
    outCtrl.outputPort = CPU_OUT_PORT;
}
   
table check_ttl {
    key = { headers.ip.ttl: exact; }
    actions = { 
       Send_to_cpu; 
        NoAction; 
    }
    const default_action = NoAction; // defined in core.p4
}

第二个Table检查ttl字段的值:如果ttl值为0,从CPU端口将分组发送到控制平面

为了实现上述功能,首先定义了一个Send_to_cpu动作,把outCtrl.outPort设为CPU_OUT_PORT来指示解复用模块应该将该分组发送到控制平面

table的主体包含了keyactionsdefault_action属性。key指定根据首部的ttl字段来查表,匹配方式是exact(完全匹配)。actions指定匹配的结果有Send_to_cpuNoAction两种。NoAction是核心库中定义的一个动作,没有做任何具体的操作。default_action指定默认动作为NoAction

9. the third table in VSS

action Set_dmac(EthernetAddress dmac) {
    headers.ethernet.dstAddr = dmac;
}

table dmac {
    key = { nextHop: exact; }
    actions = {
        Drop_action;
        Set_dmac;
    }
    size = 1024;
    default_action = Drop_action;
}

第三个Table使用下一跳的IPV4地址(第一个表计算的)来决定下一跳的以太网地址。

为了实现上述功能,首先定义了一个Set_dmac动作。Set_dmac接收一个参数,通过参数dmac来获取下一跳的以太网地址(ethernet首部dstAddr字段)。dmac没有方向属性,是“action data”,由表项提供。

table的主体包含了keyactionssizedefault_action属性。key指定根据nexthop来查表,匹配方式是exact(完全匹配)。actions指定匹配的结果有Drop_actionSet_dmac两种。size指定table的大小为1024字节。default_action指定默认动作为Drop_action

10. the last table in VSS

action Set_smac(EthernetAddress smac) {
    headers.ethernet.srcAddr = smac;
}
table smac {
    key = { outCtrl.outputPort: exact; }
    actions = {
        Drop_action;
        Set_smac;
    }
    size = 16;
    default_action = Drop_action;
}

最后一个Table使用outCtrl.outPort来标识当前交换机的源以太网地址,该地址在输出分组中设置。

为了实现上述功能,首先定义了一个Set_smac动作。Set_smac接收一个参数,通过参数smac来获取源以太网地址(ethernet首部srcAddr字段)。smac没有方向属性,是“action data”,由表项提供。

table的主体包含了keyactionssizedefault_action属性。key指定根据outCtrl.outPort来查表,匹配方式是exact(完全匹配)。actions指定匹配的结果有Drop_actionSet_smac两种。size指定table的大小为16字节。default_action指定默认动作为Drop_action

11. apply

apply {
    if (parseError != error.NoError) {
        Drop_action(); // invoke drop directly
        return; 
    }
    ipv4_match.apply(); // Match result will go into nextHop
    if (outCtrl.outputPort == DROP_PORT) 
        return;
    check_ttl.apply();
    if (outCtrl.outputPort == CPU_OUT_PORT) 
        return;
    dmac.apply();
    if (outCtrl.outputPort == DROP_PORT) 
        return;
    smac.apply();
}

在实现了上述Match-Action Units后,还需要使用apply来调用。实现方式是通过apply关键字和一个大括号包含的主体。在主体中,每个table各自调用apply方法。

在VSS的apply主体中,首先,如果有任何解析错误出现,该分组将被丢弃。实现方式为将outCtrl.outPort的值设置为DROP_PORT。然后,如果没有出现解析错误,则依次调用上述四个table。通过if语句和return语句来控制是否继续调用接下来的table

12. Deparser

// deparser section
control TopDeparser(inout Parsed_packet p, packet_out b) {
    Checksum16() ck;
    apply {
        b.emit(p.ethernet);
        if (p.ip.isValid()) {
            ck.clear(); // prepare checksum unit
            p.ip.hdrChecksum = 16w0; // clear checksum
            ck.update(p.ip); // compute new checksum.
            p.ip.hdrChecksum = ck.get();
        }
        b.emit(p.ip);
    } 
}

DeparserParser的逆过程,Deparser也是一个control块,至少包含一个packet_out参数,表示输出分组Deparser主体包含一个apply块,在apply中调用emit方法来构造分组emitpacket_out的一个方法:

  • emit应用于一个首部时,如果首部是有效的,则把首部数据加到分组里。否则,不进行任何操作。
  • emit应用于一个header stack时,emitheader stack内的每个元素递归地调用自身。
  • 当应用于一个struct或者header union时,emit对每个字段递归调用自身。注意struct不能包含errorenum字段。

13. package

// Instantiate the top-level VSS package
VSS(TopParser(),
    TopPipe(),
    TopDeparser()) main;

最后,实例化一个package。VSS的package声明如下:

package VSS<H> (Parser<H> p,
                Pipe<H> map,
                Deparser<H> d);

代码附录

1. VSS Architecture的声明

very_simple_switch_model.p4

// File "very_simple_switch_model.p4"
// Very Simple Switch P4 declaration
// core library needed for packet_in and packet_out definitions
# include <core.p4>
/* Various constants and structure declarations */
/* ports are represented using 4-bit values */
typedef bit<4> PortId;
/* only 8 ports are "real" */
const PortId REAL_PORT_COUNT = 4w8; // 4w8 is the number 8 in 4 bits
/* metadata accompanying an input packet */
struct InControl {
	PortId inputPort;
}
/* special input port values */
const PortId RECIRCULATE_IN_PORT = 0xD;
const PortId CPU_IN_PORT = 0xE;
/* metadata that must be computed for outgoing packets */
struct OutControl {
    PortId outputPort;
}
/* special output port values for outgoing packet */
const PortId DROP_PORT = 0xF;
const PortId CPU_OUT_PORT = 0xE;
const PortId RECIRCULATE_OUT_PORT = 0xD;
/* Prototypes for all programmable blocks */
/*** Programmable parser.
* @param <H> type of headers; defined by user
* @param b input packet
* @param parsedHeaders headers constructed by parser
*/
parser Parser<H>(packet_in b, 
                out H parsedHeaders);
/*** Match-action pipeline
* @param <H> type of input and output headers
* @param headers headers received from the parser and sent to the deparser
* @param parseError error that may have surfaced during parsing
* @param inCtrl information from architecture, accompanying input packet
* @param outCtrl information for architecture, accompanying output packet
*/
control Pipe<H>(inout H headers,
                in error parseError,// parser error
                in InControl inCtrl,// input port
                out OutControl outCtrl); // output port
/*** VSS deparser.
* @param <H> type of headers; defined by user
* @param b output packet
* @param outputHeaders headers for output packet
*/
control Deparser<H>(inout H outputHeaders,
                    packet_out b);
/*** Top-level package declaration - must be instantiated by user.
* The arguments to the package indicate blocks that
* must be instantiated by the user.
* @param <H> user-defined type of the headers processed.
*/
package VSS<H>(Parser<H> p,
                Pipe<H> map,
                Deparser<H> d);
// Architecture-specific objects that can be instantiated
// Checksum unit
extern Checksum16 {
    Checksum16(); // constructor
    void clear(); // prepare unit for computation
    void update<T>(in T data); // add data to checksum
    void remove<T>(in T data); // remove data from existing checksum
    bit<16> get(); // get the checksum for the data added since last clear
}

2. 完整的 VSS 代码

complete VSS program

// Include P4 core library
# include <core.p4>
// Include very simple switch architecture declarations
# include "very_simple_switch_model.p4"
// This program processes packets comprising an Ethernet and an IPv4
// header, and it forwards packets using the destination IP address
typedef bit<48> EthernetAddress;
typedef bit<32> IPv4Address;

// 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;
}

// Structure of parsed headers
struct Parsed_packet {
    Ethernet_h ethernet;
    IPv4_h ip;
}

// User-defined errors that may be signaled during parsing
error {
    IPv4OptionsNotSupported,
    IPv4IncorrectVersion,
    IPv4ChecksumError
}

// Parser section
parser TopParser(packet_in b, out Parsed_packet p) {
    Checksum16() ck; // instantiate checksum unit
    state start {
        b.extract(p.ethernet);
        transition select(p.ethernet.etherType) {
        0x0800: parse_ipv4;
        // no default rule: all other packets rejected
        } 
    }
    state parse_ipv4 {
        b.extract(p.ip);
        verify(p.ip.version == 4w4, error.IPv4IncorrectVersion);
        verify(p.ip.ihl == 4w5, error.IPv4OptionsNotSupported);
        ck.clear();
        ck.update(p.ip);
        // Verify that packet checksum is zero
        verify(ck.get() == 16w0, error.IPv4ChecksumError);
        transition accept;
    } 
}

// Match-action pipeline section
control TopPipe(inout Parsed_packet headers,
                in error parseError, // parser error
                in InControl inCtrl, // input port
                out OutControl outCtrl) {
    IPv4Address nextHop; // local variable
    /*** Indicates that a packet is dropped by setting the
    * output port to the DROP_PORT
    */
    action Drop_action() {
        outCtrl.outputPort = DROP_PORT;
    }
    /*** Set the next hop and the output port.
    * Decrements ipv4 ttl field.
    * @param ivp4_dest ipv4 address of next hop
    * @param port output port
    */
    action Set_nhop(IPv4Address ipv4_dest, PortId port) {
        nextHop = ipv4_dest;
        headers.ip.ttl = headers.ip.ttl - 1;
        outCtrl.outputPort = port;
    }
    /*** Computes address of next IPv4 hop and output port
    * based on the IPv4 destination of the current packet.
    * Decrements packet IPv4 TTL.
    * @param nextHop IPv4 address of next hop
    */
    table ipv4_match {
        key = { headers.ip.dstAddr: lpm; } // longest-prefix match
        actions = {
            Drop_action;
            Set_nhop;
        }
        size = 1024;
        default_action = Drop_action;
    }
    /*** Send the packet to the CPU port
    */
    action Send_to_cpu() {
        outCtrl.outputPort = CPU_OUT_PORT;
    }
    /*** Check packet TTL and send to CPU if expired.
    */
    table check_ttl {
        key = { headers.ip.ttl: exact; }
        actions = { 
            Send_to_cpu; 
            NoAction; 
        }
        const default_action = NoAction; // defined in core.p4
    }
    /*** Set the destination MAC address of the packet
    * @param dmac destination MAC address.
    */
    action Set_dmac(EthernetAddress dmac) {
        headers.ethernet.dstAddr = dmac;
    }
    /*** Set the destination Ethernet address of the packet
    * based on the next hop IP address.
    * @param nextHop IPv4 address of next hop.
    */
    table dmac {
        key = { nextHop: exact; }
        actions = {
            Drop_action;
            Set_dmac;
        }
        size = 1024;
        default_action = Drop_action;
    }
    /*** Set the source MAC address.
    * @param smac: source MAC address to use
    */
    action Set_smac(EthernetAddress smac) {
        headers.ethernet.srcAddr = smac;
    }
    /*** Set the source mac address based on the output port.
    */
    table smac {
        key = { outCtrl.outputPort: exact; }
        actions = {
            Drop_action;
            Set_smac;
        }
        size = 16;
        default_action = Drop_action;
    }
    apply {
        if (parseError != error.NoError) {
            Drop_action(); // invoke drop directly
            return; 
        }
        ipv4_match.apply(); // Match result will go into nextHop
        if (outCtrl.outputPort == DROP_PORT) 
            return;
        check_ttl.apply();
        if (outCtrl.outputPort == CPU_OUT_PORT) 
            return;
        dmac.apply();
        if (outCtrl.outputPort == DROP_PORT) 
            return;
        smac.apply();
    } 
}

// deparser section
control TopDeparser(inout Parsed_packet p, packet_out b) {
    Checksum16() ck;
    apply {
        b.emit(p.ethernet);
        if (p.ip.isValid()) {
            ck.clear(); // prepare checksum unit
            p.ip.hdrChecksum = 16w0; // clear checksum
            ck.update(p.ip); // compute new checksum.
            p.ip.hdrChecksum = ck.get();
        }
        b.emit(p.ip);
    } 
}

// Instantiate the top-level VSS package
VSS(TopParser(),
TopPipe(),
TopDeparser()) main;

References

1、P416 Language Specification

2、P4语言的特性、P4语言和P4交换机的工作原理和流程简介

3、Hello World in P4

4、巴科斯范式-百度百科

上一篇:内网渗透系列:内网隧道之ICMP隧道


下一篇:UDP实现消息发送