P4-编程语言入门

P4 官网
P4 Github 地址
P4 官方 Tutorials Github 地址

介绍

P4 (Programming Protocol-independent Packet Processors, 四个 P 开头的单词) 是一门用于可编程设备的特定语言, 旨在实现网络设备数据平面的可编程性, 允许用于通过编程定义如何处理网络流量, 相比之下, 传统的数据平面限于实现特定的协议, 灵活性差, 比较固化.

通过 P4 配置, 可以无需更换硬件来更改路由器或交换机数据处理的逻辑, 且协议与平台无关.

为什么说 P4 对 data plane 的掌控比较强

因为其遵循 Top-down 设计, 数据平面的交换机在硬件层面就是可编程的, 上层的设计改变, 下层的行为也改变 (也就是 Top 决定 Down). 相比于传统 Fixed-function ASIC 设备, 若想改变上层, 需要先更改下层硬件 (Down 决定 Top).

相关概念

PISA

PISA, Protocol-Independent Switch Architecture, 协议无关交换机架构.

  • 数据包进入时, 由 programmable parser 处理成中间码 (其实叫 individual headers, 一种 parsed representation, 可用于 matching 和 actions)
  • 中间码经 programmable match-action pipline 处理 (比如 modify, add, remove)
  • 中间码在处理后, 经 programmable deparser 又转换为数据包发出

P4 target

P4 target 指运行 P4 程序的具体硬件或软件平台, 这个概念强调的是, P4 程序并不是为某一种特定设备编写的, 而是面向一种通用的数据平面描述.

P4 Architecture

P4 Architecture (P4 架构) 指的是一个标准化的接口和组件集, 这些接口和组件允许开发者在一个具体的 目标设备 (Target) 上使用 P4 编程. 也就是说 P4 Architecture 在 P4 程序和目标设备 (如交换机, 模拟器, 网络处理器) 之间充当桥梁.

P4 Architecture 还定义了哪些部分可以通过 P4 进行编程 (比如定义如何解析, 处理和转发数据包), 以及哪些部分是固定的 (指无法通过 P4 程序修改的部分, 通常是设备固有的功能) 或外部实现的 (externs, 指一些目标设备特定的功能, 不能直接用 P4 代码描述, 但可以通过 P4 API 调用), 从而为 P4 程序提供一个开发框架.

示例:

这里左边是 Architecture, 右边是 target.

Architecture 大致都包含有四个阶段:

  • Parse, 分析并解析数据包头, 将数据包内容分解成不同的字段 (代码里一般是 headers 和 metadata), 以便后续阶段处理
  • Ingress, 将字段放入 Match-Action pipeline, 匹配相应规则并采取对应动作, 读修改, 转发或丢弃数据包
  • Traffic Manager (TM, 流量管理器), 负责数据包的队列 (根据不同流量的优先级放入不同的队列中), 调度以及流量管理 (控制传输速率以满足带宽或 QoS), 确保数据包按优先级顺序或服务质量要求发送
  • Egress, 和 Ingress 类似, 在数据包出站前进行一些操作, 如标记, 封装, 或进一步修改头字段
  • Deparser, 将字段封装为数据包, 以便正确转发

Programming a P4 Target 的流程

  • P4 程序根据 P4 Architecture Model 提供的接口来编写
  • P4 Runtime 是 Control Plane 和 Data plane 间的接口

Behavioral Model

Behavioral Model, 行为模型, 是 P4 语言模拟网络设备行为的一个实现框架.

BMV2 software switch

BMv2, Behavioral Model version2, 是 P4.org 提供的一个软件交换机实现, 旨在支持 P4 语言编写的程序. 它是一个开源项目, 主要用于开发, 测试和验证 P4 程序 (并不用于实际使用). 相关资料如下:

BMV2 由 C++ 编写, 用 P4 compiler 编译 P4 程序产生的 JSON 文件作为输入, 解释得到要对 packet 做的操作.

V1Model architecture

V1Model 是 P4 程序语言中使用的一个标准架构模型,专门为基于软件的 BMv2 (Behavioral Model v2) 交换机而设计。

Mininet

Mininet 官网

Mininet 是一个网络模拟工具, 常用于创建, 测试和研究软件定义网络 (SDN). 它允许用户在一台物理计算机上模拟大规模的网络拓扑, 包括交换机, 主机, 链路和控制器, 且能够运行标准的网络协议和应用程序.

P4Runtime

P4Runtime 是 P4 语言生态中的一个重要组件, 它提供了一种控制平面与数据平面之间的通信接口, 用于在和控制器之间进行实时的控制和管理. 通过 P4Runtime, 控制器可以动态下发, 更新或删除 P4 数据平面中的匹配表项和状态, 实现灵活的网络管理.

1
2
3
4
5
6
+-----------------+       P4Runtime API       +-----------------+
| 控制器 | <-----------------------> | P4 交换机 (BMv2)|
+-----------------+ +-----------------+
| |
| 数据包流 |
+----------------------------------------------+

流程:

  • 编译阶段: P4 程序首先使用 P4 编译器 (如 p4c) 编译, 生成数据平面的二进制文件 (如 BMv2 程序), 并生成 P4Runtime API 文件 (如 .p4info 文件). P4Info 文件描述了数据平面中的表结构, 动作, 元数据 等信息
  • 运行阶段: 控制器通过 P4Runtime 读取 P4Info 文件, 了解设备支持的表项和元数据. 然后控制器可以动态与交换机交互, 如下发表项, 添加流规则, 或接收数据包事件

Ethernet 和 IPv4

Ethernet 是链路层协议, 位于 OSI 模型的第二层, 负责在同一局域网内的设备之间传输数据帧, 处理物理地址 (MAC 地址).

IPv4 是网络层协议, 位于 OSI 模型的第三层, 负责在不同网络之间路由数据包, 处理逻辑地址 (IP地址).

P4 程序基本结构

P4 的组成元素如下:

对于不同的 Model, 似乎程序的结构也有所不同.

V1Model 示例

这里分开写, 完整的程序合在一起就好

1
2
3
4
5
6
7
8
9
#include <core.p4>
#include <v1model.p4>

/* HEADERS */
struct metadata { ... }
struct headers {
ethernet_t ethernet;
ipv4_t ipv4;
}
  • core.p4 是 P4 语言的核心库, 通常包含基础的类型定义, 数据结构, 操作符和功能
  • v1model.p4 是 P4 语言的标准模型, 通常用于定义一个标准的 P4 数据平面模型, 包含了一些典型的网络设备行为和数据结构
1
2
3
4
5
6
7
/* PARSER */
parser Myparser(packet_in packet,
out headers hdr,
input metadata meta,
inout standard_metadata_t smeta) {
...
}
  • metadata (一般存储一些标志位或控制信息) 和 headers 两个结构体都是自定义名称的, 但一般还是遵循这两个命名规范
  • parser 关键字表明 “解析器定义”, 用于描述如何解析数据包中的各种协议头
    • packet_in packet, 指代流入的数据包, packet_in 是 P4 中的特殊类型, 用于表示输入的数据包流, 不需要额外的方向修饰符 (毕竟语义已经很明确了)
    • out headers hdr, out 表明这时一个 输出参数, 在解析的过程中, headers 类型的结构体 hdr 会被填充数据
    • input metadata meta, input 表明其为一个 只读输入参数, 即该参数在解析器中不会被修改
    • inout standard_metadata_t smeta, inout 表示该参数既可以作为输入, 也可以作为输出, 即它的值可以在解析器中被修改; standard_metadata_t, 是一个标准元数据结构, 存储与交换机端口, 流量控制等相关的信息
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
/* CHECKSUM VERIFICATION */
control MyVerifyChecksum(in headers hdr,
inout metadata meta) {
...
}
/* INGRESS PROCESSING */
control MyIngress(inout header hdr,
inout metadata meta,
inout standard_metadata_t std_mate) {
...
}
/* EGRESS PROCESSING */
control MyEgress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t std_meta) {
...
}
/* CHECKSUM UPDATE */
control MyComputerChecksum(inout headers hdr,
inout metadata meta) {
...
}
/* DEPARSER */
control MyDeparser(inout headers hdr,
inout metadata meta) {
...
}
  • control 定义一个 控制块, 描述数据包处理的具体逻辑
1
2
3
4
5
6
7
8
9
/* SWITCH */
V1Switch(
MyParser(),
MyVerifyChecksum(),
MyIngress(),
MyEgress(),
MyComputerChecksum(),
MyDeparser()
) main;
  • V1Switch 模型创建一个名为 main 的交换机实例
  • V1Switch 的参数是传递的各组件 (注意加上 () 并不是执行函数, 而是表明其为一个组件), 需要按顺序

v1switch.p4 文件中, 可以看到 V1Switch 的定义为:

1
2
3
4
5
6
7
package V1Switch<H, M>(Parser<H, M> p,
VerifyChecksum<H, M> vr,
Ingress<H, M> ig,
Egress<H, M> eg,
ComputeChecksum<H, M> ck,
Deparser<H> dep
);

(暂不解释)

Hello World

1
2
3
4
5
6
7
8
9
10
11
12
#include <core.p4>
#include <v1model.p4>
struct headers {}
struct metadata {}

parser MyParser(packet_in packet,
out header hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {

state start { transition accept; }
}

P4 的解析器是由多个状态组成的, start 通常是解析器的初始状态.

  • state start, 定义了 start 状态的行为
  • transition accept, 表示切换到 accept 状态, 而 accept 状态是一个内置的状态, 表示解析器完成了数据包的解析过程, 并且将数据包传递给后续处理
1
control MyVerifyChecksum(inout headers hdr, inout metadata meta) { apply { } }
  • apply {} 块是控制块中需要执行的部分, 这里表示不执行任何操作
1
2
3
4
5
6
7
8
9
10
11
control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
apply {
if (standard_metadata.ingress_port == 1) {
standard_metadata.egress_spec = 2;
} else if (standard_metadata.ingress_port == 2) {
standard_metadata.egress_spec = 1;
}
}
}
  • 这里通过数据包进入的端口来设置流出的端口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
control MyEgress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
apply { }
}

control MyComputerChecksum(inout headers hdr, inout metadata meta) {
apply { }
}

control MyDeparser(packet_out packet, in headers hdr) {
apply { }
}

V1Switch(
MyParser(),
MyVerifyChecksum(),
MyIngress(),
MyEgress(),
MyComputerChecksum(),
MyDeparser()
) main;
  • 这里在 Parser 中没有对数据包做处理, 因此 Deparser 也为空

Basic Forwarding

这里仅修改了 MyIngress control 块, 其他部分同上面 hello world:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
action set_egress_spec(bit<9> port) {
standard_metadata.egress_spec = port;
}

table forward {
key = { standard_metadata.ingress_port: exact; }
actions = {
set_egress_spec;
NoAction;
}
size = 1024;
default_action = NoAction()
}

apply { forward.apply(); }
}
  • action 关键词用于定义一个动作, 感觉类似一个待调用的函数, 接受一个 bit<9> 的参数
  • table 关键词用于创建一个流表, 存储匹配条件以及相应动作, 而 forward 是该流表的名称 (就是变量名啦),
    • key 定义流表的键, 查找哪个字段, 以及如何查找 key = { standard_metadata.ingress_port: exact; }, 表示查找 standard_metadata.ingress_port 字段, 采用 exact 的匹配原则 (可能就是完全一致的以配, 这里的意思就是只处理 standard_metadata.ingress_port 字段)
    • action 表示对匹配项执行的操作, 包含多条 action (前面用 action 关键字定义的)
    • size 指定流表的容量, 即可以存储的条目数量
    • default_action 定义当没有找到匹配条目时的默认行为
    • NoAction 是一个内置的特殊 action, 表示不对数据包做处理
  • forward.apply(), 这里的 apply() 是流表的内置方法, 表示启用该流表的查找和动作处理

Parser

解析器的作用是: 把 packets 映射到 headers 和 metadata 中 (也就是主要负责提取信息). 其基于状态机运行, 每个 parser 都有 3 个预定义的状态:

  • start
  • accept
  • reject

(其他的状态也可以自定义)

在 state 中, 可以执行 action, 或者过渡 (transition) 到下一个 state.

比如:

1
2
3
4
5
6
7
8
9
10
parser MyParser(packet_in packet,
out headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {

state start(
packet.extract(hdr.ethernet);
transition accept;
)
}
  • packet.extract(hdr.ethernet)packet 中提取数据, 将其解析为 ethernet 头部, 并将其赋值给 hdr.ethernet

Controls

Controls 就可以理解为执行一次的函数. 具体的行为在 apply { } 块中执行. 主要包括:

  • Match-Action Pipelines
  • Deparsers (也就是说没有专门的 deparser 关键字, 其也定义在 control 里)
  • 额外的处理, 如 checksums

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t std_meta) {
/* Declarations region */
bit<48> tmp;

apply {
/* Control Flow */
tmp = hdr.ethernet.dstAddr;
hdr.ethernet.dstAddr = hdr.ethernet.srcAddr;
hdr.ethernet.srcAddr = tmp;
std_meta.egress_spec = std_meta.ingress_port;
}
}
  • 这里吧 src 和 dst 的 MAC 地址交换, 然后从接收包的 port 再发送出去
    (也就是哪里出来就哪里回去)

Tables

Tables 是 Match-Action pipeline 的基本单位 (都是操作 table 中的数据), 定义 table 时需要指定:

  • 要 match 什么
  • 怎么 match
  • 要执行的 actions 列表 (都列在 table 定义里)
  • table 的基本属性, 如 size, default action, static entries

定义好 table 后, 其需要包含多条 entries (也就是 rules), 每一条 entry 需要包含:

  • 用于 match 的 key
  • 当 match 时的 action
  • Action data (可以没有, 是与指定动作相关的参数, 通常用于执行该动作所需的额外信息)

Match-Action 的过程可以描述如下:

  1. packets 被 parser 处理后, 得到 headers and metadata
  2. Headers 和 metadata 经 control plane 处理时, 用 table 进行 match
  3. 如果 match hit, 则执行对应的 action code, 不然执行 default action
  4. 输出最终的 headers and metadata

一个 P4 Table 的示例:

1
2
3
4
5
6
7
8
9
10
11
12
table ipv4_lpm {
key = {
hdr.ipv4.dstAddr: 1pm;
}
actions = {
ipv4_forward;
drop;
NoAction;
}
size = 1024;
default_action = NoAction();
}

(都用 = 赋值, 用 ; 结尾, 块后头不用加 ;)

  • 1pm 指 “最长前缀匹配” (Longest Prefix Match)

1pm, exact 这些都称做 match_kind, 不同的 model (architecture) 都有定义不同的 match_kind, 可以在头文件中找到如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* core.p4 */
match_kind {
exact,
ternary,
1pm
}

/* v1model.p4 */
match_kind {
range,
selector
}

/* core.p4 */
match_kind {
regexp,
fuzzy
}

这里, 在 P4 程序中, 定义 table 的格式, 要执行的 match-action 操作, 处于 Data Plane, 而填充 table entries 属于 Control Plane 的工作.

启用 table 需要调用 apply() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t std_meta) {
table ipv4_lpm {
...
}

apply {
...
ipv4_lpm.apply();
...
}
}

Action

Action 和 C 的函数类似, 可以在一个 control 块中定义, 也可以全局定义. 传入 action 的参数也有方向和类型.

一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t std_meta) {

action swap_mac(inout bit<48> src,
inout bit<48> dst) {
bit<48> tmp = src;
src = dst;
dst = tmp;
}

apply {
swap_mac(hdr.ethernet.srcAddr,
hdr.ethernet.dstAddr);
std_meta.egress_spec = std_meta.ingress_port;
}
}
  • 可以看出, action 会直接对传入的参数修改 (就像 C 中传入指针修改一样)

NoActioncore.p4 文件中的定义如下:

1
2
action NoAction() {
}

(可以看到确实是空)

Action 接受两种类型的参数:

  • 直接调用 (在 Data Plane 直接传参)
  • 非直接调用 (在 Control Plane table match 时调用时自动传参)

Deparsing

Deparsing 负责把 headers 重新转换为 packet, 其具体逻辑编写在 control 块中.

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* From core.p4 */
extern packet_out {
void emit<T>(in T hdr);
}

/* User Program */
control DeparserImpl(packet_out packet,
in headers hdr) {
apply {
...
packet.emit(hdr.ethernet)
...
}
}
  • core.p4 中定义的 packet_out 类型, 有 emit 方法, 其能够将 valid header 序列化并保存到指定变量中
  • extern 表明导出该类型外部文件可用

语法

结构体声明

1
2
3
4
5
struct struct_name {
type name1;
type name2;
...
}

和 C/C++ 一样, 用 struct 关键字.

类型声明

1
2
3
4
5
6
struct standard_metadata_t {
bit<9> ingress_port;
bit<9> egress_spec;
bit<9> egress_port;
...
}
  • bit<9> 是一种 “位宽类型声明”, 用于精确控制字段的位宽, 这与 C/C++ 中的 int 类型名指代长度不同. 这里是声明 9 位无符号整数字段 (P4 不允许符号位, 因此所有的 bit<N> 类型都是无符号的)
  • ingress_port, 指数据包进入的端口
  • egress_spec, 指预期数据包流出端口, 是由 Ingress Pipeline 决定和设置
  • egress_port, 指实际数据包流出的端口, 通常和 egress_spec 的值相同, 但可能由于重定向, 多播等原因不同

header 类型, 其成员是有序的 (在内存中顺序排列, 读取的时候有顺序, 这里也是字节对齐的), 可以包含 bit<n>, int<n>, varbit<n> 的成员, 如:

1
2
3
4
5
6
7
8
9
10
11
header ethernet_t {
macAddr_t dstAddr;
macAddr_t srcAddr;
bit<16> etherType;
}
header ipv4_t {
bit<4> version;
bit<4> ihl;
bit<8> diffserv;
...
}

header 类型的一个好处在于, 其可以被标记为有效或无效 (用 setValid()setInvalid() 函数), 可以用 isValid() 来检测, 从而方便处理 (有些 invalid 就跳过).

typedef

同 C/C++:

1
2
typedef bit<48> macAddr_t;
typedef bit<32> ipAddr_t;

返回值?

P4 中没有传统意义的函数返回值, 解析器利用状态机来解析数据包, 并将解析结果存储在传入的参数中 (比如 headers 结构体), 而非通过返回值传递.

Select 语句

select 类似 C 中的 switch, 但是不会自动 fall-through (也就是说不需要 break 也能跳出), 其匹配成功后返回键值, 如:

1
2
3
4
5
6
7
8
9
10
11
state start {
transition parse_ethernet;
}

state parse_ethernet {
packet.extract(hdr.ethernet);
transition select(hdr.ethernet.etherType) {
0x800: parse_ipv4;
default; accept;
}
}
  • hdr.ethernet.etherType 指示了以太网帧中承载的上层协议, 例如, 如果 etherType0x0800, 则表示帧承载的是 IPv4 数据包; 如果为 0x0806, 则表示 ARP 协议, 这里的话如果值是 0x800 则跳转到 parse_ipv4, 不然到 accept.

运算符

大多 C 中的运算符都能使用, 但没有 division 和 modulo, 如下:

  • +, -, *
  • ~, &, |, ^, >>, <<
  • ==, !=, >, >=, <, <=
  • bit-slicing: [m:l]
  • 拼接: ++

Tutorial

Tutorial 环境准备

首先确保安装的 virtualbox 以及其内核版本正确, 不然会报错:

Archlinux 上, 可以用 downgrade 命令降低软件版本:

1
sudo downgrade virtualbox

选择 7.0.20 版本即可, 同理对内核降版本:

1
sudo downgrade virtualbox-host-dkms

选择和 virtualbox 相同的版本.

之后:

1
2
3
4
5
cd
mkdir p4-dev
git clone https://github.com/p4lang/tutorials.git
cd tutorials/vm-ubuntu-24.04
vagrant up --provider=virtualbox

等待虚拟机启动, 配置脚本会将主机名改为 p4, 有两个用户可登录:

  • vagrant, 密码: vagrant
  • p4, 密码: p4

环境构建所花的时间较长, 其取决于网速的电脑速度. (大概1个多小时吧)

构建结束如:

之后可用:

1
vagrant ssh

连接到虚拟机器. 这里默认登录到 vagrant 用户, 但我们实际需要 p4 用户, 可以选择进入虚拟机后切换用户, 也可以在 Vagrantfile 中指定 ssh 默认连接的用户:

1
2
3
4
5
6
7
8
Vagrant.configure("2") do |config|
config.vm.box = "bento/ubuntu-24.04"

# 指定登录用户
config.ssh.username = "p4"
...
...
end

此时需要密码登录. 若要在 vagrant ssh 时免密, 可以先用 vagrant ssh 登录到 vagrant 用户, 复制 ~/.ssh/authorized_keys 文件里的内容到 p4 用户的 ~/.ssh/authorized_keys 中即可.

若想在 vagrant share 将其暴露出来后免密, 配置如下:

1
2
3
vagrant ssh # 先用密码登录进去
ssh-keygen -t rsa # 之后一直回车就行
vim .ssh/authorized_keys # 把自己主机的 public key 粘贴进去

若启用时不想打开 gui, 则可以修改 Vagrantfile, 把 vb.gui 设为 false:

之后:

1
2
vagrant up dev
vagrant ssh

Tutorial 使用指南

下面都以 basic 为例.

要做的内容是补全 basic.p4TODO 注释的部分.

每个题其实都有示例答案可以参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
p4@p4:~/tutorials/exercises/basic$ tree -L 1
.
├── basic.p4
├── build
├── logs
├── Makefile
├── pcaps
├── pod-topo
├── README.md
├── receive.py
├── send.py
├── solution
└── triangle-topo

这里的 solution 目录下有示例答案.

Basic Forwarding

要实现 IPv4 forwarding, 交换机需要对每个 packet 执行下面操作:

  1. 更新 source (当前 switch 吧) 和 destination (下一跳 switch 吧) MAC 地址
  2. IP 报头中的 TTL (Time-To-Live) 值减一
  3. 从合适的 port 将 packet 发出

在补全代码前的现象为:

此时 h1 ping h2 没有返回值, 网络不通.

完整代码如下:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
/* -*- P4_16 -*- */
#include <core.p4>
#include <v1model.p4>

const bit<16> TYPE_IPV4 = 0x800;

/*************************************************************************
*********************** H E A D E R S ***********************************
*************************************************************************/

typedef bit<9> egressSpec_t;
typedef bit<48> macAddr_t;
typedef bit<32> ip4Addr_t;

header ethernet_t {
macAddr_t dstAddr;
macAddr_t srcAddr;
bit<16> etherType;
}

header ipv4_t {
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;
ip4Addr_t srcAddr;
ip4Addr_t dstAddr;
}

struct metadata {
/* empty */
}

struct headers {
ethernet_t ethernet;
ipv4_t ipv4;
}

/*************************************************************************
*********************** P A R S E R ***********************************
*************************************************************************/

parser MyParser(packet_in packet,
out headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {

state start {
/* TODO: add parser logic */
transition parse_ethernet;
}

state parse_ethernet {
packet.extract(hdr.ethernet);
transition select(hdr.ethernet.etherType) {
TYPE_IPV4: parse_ipv4;
default: accept;
}
}

state parse_ipv4 {
packet.extract(hdr.ipv4);
transition accept;
}


}


/*************************************************************************
************ C H E C K S U M V E R I F I C A T I O N *************
*************************************************************************/

control MyVerifyChecksum(inout headers hdr, inout metadata meta) {
apply { }
}


/*************************************************************************
************** 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 ipv4_forward(macAddr_t dstAddr, egressSpec_t port) {
/*
Action function for forwarding IPv4 packets.

This function is responsible for forwarding IPv4 packets to the specified
destination MAC address and egress port.

Parameters:
- dstAddr: Destination MAC address of the packet.
- port: Egress port where the packet should be forwarded.

TODO: Implement the logic for forwarding the IPv4 packet based on the
destination MAC address and egress port.
*/
standard_metadata.egress_spec = port;
hdr.ethernet.srcAddr = hdr.ethernet.dstAddr;
hdr.ethernet.dstAddr = dstAddr;
hdr.ipv4.ttl = hdr.ipv4.ttl - 1;
}

table ipv4_lpm {
key = {
hdr.ipv4.dstAddr: lpm;
}
actions = {
ipv4_forward;
drop;
NoAction;
}
size = 1024;
default_action = NoAction();
}

apply {
/* TODO: fix ingress control logic
* - ipv4_lpm should be applied only when IPv4 header is valid
*/
if (hdr.ipv4.isValid()) {
ipv4_lpm.apply();
}
}
}

/*************************************************************************
**************** E G R E S S P R O C E S S I N G *******************
*************************************************************************/

control MyEgress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
apply { }
}

/*************************************************************************
************* C H E C K S U M C O M P U T A T I O N **************
*************************************************************************/

control MyComputeChecksum(inout headers hdr, inout metadata meta) {
apply {
update_checksum(
hdr.ipv4.isValid(),
{ hdr.ipv4.version,
hdr.ipv4.ihl,
hdr.ipv4.diffserv,
hdr.ipv4.totalLen,
hdr.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);
}
}


/*************************************************************************
*********************** D E P A R S E R *******************************
*************************************************************************/

control MyDeparser(packet_out packet, in headers hdr) {
apply {
/*
Control function for deparser.

This function is responsible for constructing the output packet by appending
headers to it based on the input headers.

Parameters:
- packet: Output packet to be constructed.
- hdr: Input headers to be added to the output packet.

TODO: Implement the logic for constructing the output packet by appending
headers based on the input headers.
*/
packet.emit(hdr.ethernet);
packet.emit(hdr.ipv4);
}
}

/*************************************************************************
*********************** S W I T C H *******************************
*************************************************************************/

V1Switch(
MyParser(),
MyVerifyChecksum(),
MyIngress(),
MyEgress(),
MyComputeChecksum(),
MyDeparser()
) main;

之后:

1
2
3
make 
...
mininet> h1 ping h2

输出:

为什么要修改 standard_metadata 的值

因为该值存储 packet 流入流出的端口信息.

emit 的顺序为什么先是 ethernet 后是 ipv4

数据包的构建是从底层到顶层进行的, 这里的 emit 是用 appending 的方式构建数据包, 因此先封装底层的 Ethernet 帧, 然后是 IPv4 头部信息.

Basic Tunneling

Debugging

可以用 debug table 来输出调试信息到 log 文件:

1
2
3
4
5
6
7
8
9
10
11
12
control MyIngress(...) {
table debug {
key = {
std_meta.egress_spec: exact;
}
actions = { }
}
apply {
...
debug.apply();
}
}

P4-编程语言入门
http://example.com/2024/10/18/P4-编程语言入门/
作者
Jie
发布于
2024年10月18日
许可协议