CPU 上送¶
一台主机、一台交换机,每个数据平面包都被通过 CPU 端口(510)
上送控制器。控制器也可以通过带有显式出口端口元数据的
PacketOut 把数据包注入回数据平面。
你将看到什么¶
主机产生 ARP / IPv6 ND / ICMP 流量时,s1 packet listen 会实时
显示上送上来的数据包。s1 packet send 把控制器构造的帧反向
注入数据平面。
拓扑¶
examples/cpu_punt/topology.py:
"""One host, one switch, all dataplane traffic punted to CPU.
Run with:
sudo p4net examples/cpu_punt/topology.py
Then in the shell:
h1 cmd ping -c 3 -W 1 10.0.0.99 # generates ARP traffic
s1 packet listen count=3 timeout=5 # observe punted packets
Or send a packet from controller to host 1:
s1 packet send ffffffffffff000000000001880b48656c6c6f \\
metadata: egress_port=1
"""
from __future__ import annotations
from pathlib import Path
from p4net.topo import Topology
HERE = Path(__file__).resolve().parent
topology = Topology()
h1 = topology.add_host("h1", ip="10.0.0.1/24", mac="00:00:00:00:00:01")
s1 = topology.add_switch(
"s1",
p4_src=HERE / "cpu_punt.p4",
cpu_port=510,
)
topology.add_link(h1, s1, port_b=1)
if __name__ == "__main__":
from p4net.cli.main import main
raise SystemExit(main([__file__]))
交换机上的 cpu_port=510 是把 CPU 端口接入 BMv2 的关键参数。
P4 程序¶
examples/cpu_punt/cpu_punt.p4:
/* CPU-punt demo pipeline.
*
* Every dataplane packet is punted to the controller (via the CPU port).
* Packets injected from the controller carry a `packet_out` header that
* names the desired egress port; the ingress control copies that into
* `std.egress_spec` and invalidates the header before the packet is
* deparsed onto the wire.
*
* Pairs with `examples/cpu_punt/topology.py`, which sets `cpu_port=510`
* on the BMv2 switch.
*/
#include <core.p4>
#include <v1model.p4>
const bit<9> CPU_PORT = 510;
@controller_header("packet_in")
header packet_in_t {
bit<9> ingress_port;
bit<7> _pad0;
}
@controller_header("packet_out")
header packet_out_t {
bit<9> egress_port;
bit<7> _pad0;
}
header ethernet_t {
bit<48> dstAddr;
bit<48> srcAddr;
bit<16> etherType;
}
struct headers {
packet_in_t packet_in;
packet_out_t packet_out;
ethernet_t ethernet;
}
struct metadata {}
parser MyParser(packet_in pkt, out headers hdr, inout metadata meta,
inout standard_metadata_t std) {
state start {
transition select(std.ingress_port) {
CPU_PORT: parse_packet_out;
default: parse_ethernet;
}
}
state parse_packet_out {
pkt.extract(hdr.packet_out);
transition parse_ethernet;
}
state parse_ethernet {
pkt.extract(hdr.ethernet);
transition accept;
}
}
control MyVerifyChecksum(inout headers hdr, inout metadata meta) { apply {} }
control MyIngress(inout headers hdr, inout metadata meta,
inout standard_metadata_t std) {
apply {
if (std.ingress_port == CPU_PORT) {
// Controller-injected packet: forward as instructed and strip
// the controller header before deparsing.
std.egress_spec = hdr.packet_out.egress_port;
hdr.packet_out.setInvalid();
} else {
// Dataplane packet: punt to controller; stamp ingress_port.
std.egress_spec = CPU_PORT;
hdr.packet_in.setValid();
hdr.packet_in.ingress_port = std.ingress_port;
hdr.packet_in._pad0 = 0;
}
}
}
control MyEgress(inout headers hdr, inout metadata meta,
inout standard_metadata_t std) { apply {} }
control MyComputeChecksum(inout headers hdr, inout metadata meta) { apply {} }
control MyDeparser(packet_out pkt, in headers hdr) {
apply {
pkt.emit(hdr.packet_in);
pkt.emit(hdr.ethernet);
}
}
V1Switch(MyParser(), MyVerifyChecksum(), MyIngress(), MyEgress(),
MyComputeChecksum(), MyDeparser()) main;
两个 @controller_header 声明分别定义 PacketIn(上送控制器)与
PacketOut(控制器注入)的元数据布局。parser 根据
std.ingress_port == CPU_PORT 判断是否需要在 ethernet 之前先解析
packet_out 报头。入口控制:
- 控制器注入的包:把
egress_port复制进std.egress_spec, 并使控制器报头无效。 - 数据平面包:把
std.egress_spec = CPU_PORT,使packet_in报头有效,并写入ingress_port。
运行¶
Shell 中:
p4net> s1 packet listen count=3 timeout=5
[ingress_port=1] 333300000016000000000001...
[ingress_port=1] 333300000016000000000001...
[ingress_port=1] ff02000000000000000000010002...
[ingress_port=1] 这个前缀是被解码出来的 packet_in 控制器报头。
hex 负载在 CLI 中截断到 64 字符;如需完整 payload,在 Python 端
使用 client.expect_packet_in()。
从控制器注入一个朝向 h1 的帧:
关键设计点¶
- BPF 过滤器小技巧。集成测试要验证控制器注入的帧确实到达
h1时,使用的是tcpdump -i h1-eth0 -c 1 ether proto 0x88B5, 而非裸tcpdump -c 1。无过滤的话,IPv6 ND 噪声会先把 count-1 的位置占掉,测试帧到来时 tcpdump 已经退出。本示例使用0x88B(local-experimental 以太网类型)就是同样的考虑。 - 缺失的元数据自动补零。
encode_packet_out_metadata遍历 P4Info 中声明的每一个 metadata 字段,对未给出的键回退到metadata.get(name, 0),因此_pad0调用方无需显式指定。
可尝试的变体¶
- 用
s1.client.on_packet_in(handler)注册一个 Python 处理器, 解析上送的以太网帧,做学习交换机式的逻辑。 - 在 setup 脚本里用
s1.client.send_packet_out(payload, {"egress_port": 1})注入一串探测帧。 - 加一张
default_action = punt()的ipv4_lpm表,把已编程的 转发与控制器兜底逻辑混合起来。