P4 05-L2_Learning
P4示例程序-05 L2自学习交换机
功能
在L2交换机的基础上,引入控制平面使得交换机能够自学习到转发表、而无需预先配置,类似于实现经典SDN中的Packet-In消息与Packet-Out消息
拓扑结构
代码
Clone
p4app_clone.json1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24"topology": {
"assignment_strategy": "l2",
"default":{
"auto_arp_tables": false
},
"links": [["h1", "s1"], ["h2", "s1"], ["h3", "s1"], ["h4","s1"]],
"hosts": {
"h1": {
},
"h2": {
}
,
"h3": {
}
,
"h4": {
}
},
"switches": {
"s1": {
"cpu_port" : true
}
}
}- Headers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24typedef bit<9> egressSpec_t; // 定义出端口类型
typedef bit<48> macAddr_t; // 定义 MAC 地址类型
typedef bit<32> ip4Addr_t; // 定义 IPv4 地址类型
header ethernet_t {
macAddr_t dstAddr; // 目的 MAC 地址
macAddr_t srcAddr; // 源 MAC 地址
bit<16> etherType; // 以太网类型字段
}
header cpu_t {
bit<48> srcAddr; // 记录源 MAC 地址
bit<16> ingress_port; // 记录入端口号
}
struct metadata {
@field_list(0)
bit<9> ingress_port; // 用户自定义元数据,记录入端口
}
struct headers {
ethernet_t ethernet; // 以太网头部
cpu_t cpu; // 自定义 CPU 学习头部
} - Parser
1
2
3
4
5
6
7
8
9
10parser MyParser(packet_in packet,
out headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
state start {
packet.extract(hdr.ethernet); // 提取以太网头部
transition accept; // 状态转移到 accept
}
} - Checksum Verification
1
2
3
4control MyVerifyChecksum(inout headers hdr, inout metadata meta) {
apply {
}
} - Ingress Processing
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
71control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
action drop() {
mark_to_drop(standard_metadata); // 标记该数据包丢弃
}
action mac_learn() {
meta.ingress_port = standard_metadata.ingress_port; // 记录入端口
clone_preserving_field_list(CloneType.I2E, 100, 0); // 克隆数据包用于学习
}
table smac {
key = {
hdr.ethernet.srcAddr: exact; // 使用源 MAC 地址匹配
}
actions = {
mac_learn; // 执行学习动作
NoAction; // 不执行动作
}
size = 256; // 表项大小为 256
default_action = mac_learn; // 默认执行学习动作
}
action forward(bit<9> egress_port) {
standard_metadata.egress_spec = egress_port; // 设置出端口
}
table dmac {
key = {
hdr.ethernet.dstAddr: exact; // 使用目的 MAC 地址匹配
}
actions = {
forward; // 执行转发动作
NoAction; // 不执行动作
}
size = 256; // 表项大小为 256
default_action = NoAction; // 默认不做动作
}
action set_mcast_grp(bit<16> mcast_grp) {
standard_metadata.mcast_grp = mcast_grp; // 设置多播组
}
table broadcast {
key = {
standard_metadata.ingress_port: exact; // 根据入端口匹配
}
actions = {
set_mcast_grp; // 设置多播组动作
NoAction; // 不执行动作
}
size = 256; // 表项大小为 256
default_action = NoAction; // 默认不设置多播组
}
apply {
smac.apply(); // 应用源地址学习表
if (dmac.apply().hit){
// # 命中目的地址表,不需要额外处理
}
else {
broadcast.apply(); // 未命中则应用广播表
}
}
} - Egress Processing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16control 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(); // 设置 CPU 头部为有效
hdr.cpu.srcAddr = hdr.ethernet.srcAddr; // 将源 MAC 写入 CPU 头部
hdr.cpu.ingress_port = (bit<16>)meta.ingress_port; // 写入入口端口
hdr.ethernet.etherType = L2_LEARN_ETHER_TYPE; // 更改以太类型为学习类型
truncate((bit<32>)22); //ether+cpu header # 截断包长为 22 字节
}
}
} - Checksum Computation
1
2
3
4control MyComputeChecksum(inout headers hdr, inout metadata meta) {
apply {
}
} - DE parser
1
2
3
4
5
6
7control 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); // 打包 CPU 学习头部
}
} - Switch
1
2
3
4
5
6
7
8V1Switch(
MyParser(),
MyVerifyChecksum(),
MyIngress(),
MyEgress(),
MyComputeChecksum(),
MyDeparser()
) main; controller_base.py1
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# controller_base.py
from p4utils.utils.helper import load_topo
from p4utils.utils.sswitch_p4runtime_API import SimpleSwitchP4RuntimeAPI
from p4utils.utils.sswitch_thrift_API import SimpleSwitchThriftAPI
class BaseController:
def __init__(self, sw_name):
# 加载交换机拓扑文件 topology.json
self.topo = load_topo('topology.json')
self.sw_name = sw_name
# 获取连接 CPU 端口的 port index;可能为 None
self.cpu_port = self.topo.get_cpu_port_index(self.sw_name)
# 获取交换机设备 ID(用于 gRPC)
device_id = self.topo.get_p4switch_id(sw_name)
grpc_port = self.topo.get_grpc_port(sw_name)
sw_data = self.topo.get_p4rtswitches()[sw_name]
# 初始化 gRPC 控制面 API 客户端
self.controller = SimpleSwitchP4RuntimeAPI(
device_id,
grpc_port,
p4rt_path=sw_data['p4rt_path'],
json_path=sw_data['json_path']
)
# 调用初始化逻辑
self.init()
def reset(self):
# gRPC reset 控制面状态
self.controller.reset_state()
# 使用 Thrift 完整重置转发面状态
thrift_port = self.topo.get_thrift_port(self.sw_name)
controller_thrift = SimpleSwitchThriftAPI(thrift_port)
controller_thrift.reset_state()
def init(self):
# 执行重置
self.reset()
# 配置广播组
self.add_broadcast_groups()
# 配置 clone session 用于 CPU 收包
self.add_clone_session()
def add_clone_session(self):
# 如果有 CPU 端口配置,则创建 clone session,将报文复制到 CPU
if self.cpu_port is not None:
self.controller.cs_create(100, [self.cpu_port])
def add_broadcast_groups(self):
# 获取所有接口名对应端口号
interfaces_to_port = self.topo.get_node_intfs(fields=['port'])[self.sw_name].copy()
# 排除 lo 接口
interfaces_to_port.pop('lo', None)
# 排除 CPU 端口接口
interfaces_to_port.pop(self.topo.get_cpu_port_intf(self.sw_name), None)
mc_grp_id = 1 # 多播组 ID 号
for ingress_port in interfaces_to_port.values():
# 除去当前 ingress_port,生成目标端口列表
port_list = list(interfaces_to_port.values())
port_list.remove(ingress_port)
# 创建多播组,将所有其它端口加入组
self.controller.mc_mgrp_create(mc_grp_id, port_list)
# 添加广播表项:当来自 ingress_port 时,使用 mc_grp_id 多播
self.controller.table_add(
"broadcast",
"set_mcast_grp",
[str(ingress_port)],
[str(mc_grp_id)]
)
mc_grp_id += 1
def learn(self, learning_data):
# 根据新学到的 (MAC, port),添加转发表项
for mac_addr, ingress_port in learning_data:
print(f"mac: {mac_addr:012X} ingress_port: {ingress_port}")
# 添加 smac 表项(标记为已学习)
self.controller.table_add("smac", "NoAction", [str(mac_addr)])
# 添加 dmac 表项:目的 MAC 转发到 ingress_port
self.controller.table_add("dmac", "forward", [str(mac_addr)], [str(ingress_port)])controller_main.py1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17# main.py
import sys
from digest_learning import DigestLearningController
from cpu_learning import CpuLearningController
if __name__ == "__main__":
sw_name = sys.argv[1] # 交换机名称,例如 's1'
receive_from = sys.argv[2] # 参数 "digest" 或 "cpu"
if receive_from == "digest":
# 启动 digest 模式控制器
DigestLearningController(sw_name).run()
elif receive_from == "cpu":
# 启动 cpu 学习模式控制器
CpuLearningController(sw_name).run()
else:
print("Usage: python main.py <sw_name> <digest|cpu>")controller_clone.py1
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# cpu_learning.py
from controller_base import BaseController
from scapy.all import Ether, sniff, Packet, BitField, raw
# 定义一个自定义报文格式,用于解析 clone 到 CPU 的包负载
class CpuHeader(Packet):
name = 'CpuPacket'
fields_desc = [
BitField('macAddr', 0, 48), # 源 MAC 地址字段
BitField('ingress_port', 0, 16) # ingress port 字段
]
class CpuLearningController(BaseController):
def recv_msg_cpu(self, pkt):
# 使用 scapy 解包原始数据包
packet = Ether(raw(pkt))
# 判断以太类型是否匹配自定义 CPU 协议
if packet.type == 0x1234:
cpu_header = CpuHeader(bytes(packet.load))
# 从 header 中提取 mac 和 ingress_port,进行学习
self.learn([(cpu_header.macAddr, cpu_header.ingress_port)])
def run(self):
# 获取 CPU 接口名称(从拓扑中,并将 eth0→eth1)
cpu_port_intf = str(self.topo.get_cpu_port_intf(self.sw_name).replace("eth0", "eth1"))
# 使用 scapy 抓包监听 CPU 接口,调用 recv_msg_cpu 处理每个包
sniff(iface=cpu_port_intf, prn=self.recv_msg_cpu)network_clone.py1
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
30from p4utils.mininetlib.network_API import NetworkAPI
net = NetworkAPI()
# Network general options
net.setLogLevel('info')
net.setCompiler(p4rt=True)
net.disableArpTables()
# Network definition
net.addP4RuntimeSwitch('s1')
net.setP4Source('s1','./p4src/l2_learning_copy_to_cpu.p4')
net.addHost('h1')
net.addHost('h2')
net.addHost('h3')
net.addHost('h4')
net.addLink('s1', 'h1')
net.addLink('s1', 'h2')
net.addLink('s1', 'h3')
net.addLink('s1', 'h4')
# Assignment strategy
net.l2()
# Nodes general options
net.enableCpuPortAll()
net.enablePcapDumpAll()
net.enableLogAll()
net.enableCli()
net.startNetwork()
Digest
p4app_digest.json1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23"topology": {
"assignment_strategy": "l2",
"default":{
"auto_arp_tables": false
},
"links": [["h1", "s1"], ["h2", "s1"], ["h3", "s1"], ["h4","s1"]],
"hosts": {
"h1": {
},
"h2": {
}
,
"h3": {
}
,
"h4": {
}
},
"switches": {
"s1": {
}
}
}- Headers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23ypedef bit<9> egressSpec_t; // 定义egress端口字段类型
typedef bit<48> macAddr_t; // 定义MAC地址类型
typedef bit<32> ip4Addr_t; // 定义IPv4地址类型
header ethernet_t { // 定义以太网头部结构
macAddr_t dstAddr; // 目标MAC地址
macAddr_t srcAddr; // 源MAC地址
bit<16> etherType; // 以太网类型字段
}
struct learn_t { // 定义学习信息结构
bit<48> srcAddr; // 源MAC地址
bit<9> ingress_port; // 入端口号
}
struct metadata { // 定义元数据结构
/* empty */
learn_t learn; // 添加学习字段
}
struct headers { // 定义报文头部集合
ethernet_t ethernet; // 以太网头部
} - Parser
1
2
3
4
5
6
7
8
9
10parser MyParser(packet_in packet,
out headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
state start { // 起始状态
packet.extract(hdr.ethernet); // 提取以太网头部
transition accept; // 状态转移至accept
}
} - Checksum Verification
1
2
3
4control MyVerifyChecksum(inout headers hdr, inout metadata meta) {
apply {
}
} - Ingress Processing
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
68control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
action drop() { // 丢弃动作
mark_to_drop(standard_metadata); // 设置为丢弃
}
action mac_learn(){ // 学习源MAC地址
meta.learn.srcAddr = hdr.ethernet.srcAddr; // 存储源MAC
meta.learn.ingress_port = standard_metadata.ingress_port; // 存储入端口
digest<learn_t>(1, meta.learn); // 发送digest到控制器
}
table smac { // 源MAC学习表
key = {
hdr.ethernet.srcAddr: exact; // 匹配源MAC
}
actions = {
mac_learn; // 调用学习动作
NoAction; // 或不执行
}
size = 256; // 表项大小
default_action = mac_learn; // 默认进行学习
}
action forward(bit<9> egress_port) { // 转发动作
standard_metadata.egress_spec = egress_port; // 设置出端口
}
table dmac { // 目标MAC转发表
key = {
hdr.ethernet.dstAddr: exact; // 匹配目标MAC
}
actions = {
forward; // 调用转发动作
NoAction; // 或不执行
}
size = 256; // 表项大小
default_action = NoAction; // 默认不转发
}
action set_mcast_grp(bit<16> mcast_grp) { // 设置多播组
standard_metadata.mcast_grp = mcast_grp; // 设置多播字段
}
table broadcast { // 广播表
key = {
standard_metadata.ingress_port: exact; // 匹配入端口
}
actions = {
set_mcast_grp; // 设置多播组
NoAction; // 或不执行
}
size = 256; // 表项大小
default_action = NoAction; // 默认不设置
}
apply { // 应用阶段
smac.apply(); // 应用源MAC表
if (dmac.apply().hit){ // 若目标MAC命中
// 什么都不做(继续转发)
}
else {
broadcast.apply(); // 否则执行广播表
}
}
} - Egress Processing
1
2
3
4
5
6
7control MyEgress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
apply { } // 不做egress处理
} - Checksum Computation
1
2
3
4control MyComputeChecksum(inout headers hdr, inout metadata meta) {
apply {
}
} - DE parser
1
2
3
4
5
6
7control MyDeparser(packet_out packet, in headers hdr) {
apply {
//parsed headers have to be added again into the packet.
packet.emit(hdr.ethernet); // 重新打包以太网头部
}
} - Switch
1
2
3
4
5
6
7
8
9V1Switch(
MyParser(),
MyVerifyChecksum(),
MyIngress(),
MyEgress(),
MyComputeChecksum(),
MyDeparser()
) main; controller_digest.py1
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# digest_learning.py
from controller_base import BaseController
class DigestLearningController(BaseController):
def config_digest(self):
# 启用 digest 报文:从表 learn_t 中上报最多 10 条条目,每隔最久 1 ms
self.controller.digest_enable('learn_t', 1000000, 10, 1000000)
def unpack_digest(self, dig_list):
learning_data = []
# 解析 digest message 列表
for dig in dig_list.data:
# 第一个字段是 MAC 地址(48 位)
mac_addr = int.from_bytes(dig.struct.members[0].bitstring, byteorder='big')
# 第二个字段是 ingress_port(16 位)
ingress_port = int.from_bytes(dig.struct.members[1].bitstring, byteorder='big')
learning_data.append((mac_addr, ingress_port))
return learning_data
def recv_msg_digest(self, dig_list):
# 解包 digest 并调用学习函数
learning_data = self.unpack_digest(dig_list)
self.learn(learning_data)
def run(self):
# 启用 digest 上报
self.config_digest()
# 不断获取 digest 列表并处理
while True:
dig_list = self.controller.get_digest_list()
self.recv_msg_digest(dig_list)network_digest.py1
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
29from p4utils.mininetlib.network_API import NetworkAPI
net = NetworkAPI()
# Network general options
net.setLogLevel('info')
net.setCompiler(p4rt=True)
net.disableArpTables()
# Network definition
net.addP4RuntimeSwitch('s1')
net.setP4Source('s1','./p4src/l2_learning_digest.p4')
net.addHost('h1')
net.addHost('h2')
net.addHost('h3')
net.addHost('h4')
net.addLink('s1', 'h1')
net.addLink('s1', 'h2')
net.addLink('s1', 'h3')
net.addLink('s1', 'h4')
# Assignment strategy
net.l2()
# Nodes general options
net.enablePcapDumpAll()
net.enableLogAll()
net.enableCli()
net.startNetwork()
P4仿真(以clone模式为例)
- 启动网络拓扑
sudo p4run
- 以clone模式启动S1
sudo python l2_learning_controller.py s1 cpu
- 启动
pingall命令,发现主机间成功通信
P4 05-L2_Learning
http://example.com/2025/07/30/P4 05-L2_Learning/