P4 05-L2_Learning

P4示例程序-05 L2自学习交换机

  • 功能

    在L2交换机的基础上,引入控制平面使得交换机能够自学习到转发表、而无需预先配置,类似于实现经典SDN中的Packet-In消息与Packet-Out消息

  • 拓扑结构

拓扑结构
  • 代码

  • Clone

  1. p4app_clone.json
    1
    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
    }
    }
    }
  2. Headers
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    typedef 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 学习头部
    }
  3. Parser
    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; // 状态转移到 accept
    }
    }
  4. Checksum Verification
    1
    2
    3
    4
    control MyVerifyChecksum(inout headers hdr, inout metadata meta) {
    apply {
    }
    }
  5. 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
    71
    control 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(); // 未命中则应用广播表
    }
    }
    }
  6. Egress Processing
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    control 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 字节
    }
    }
    }
  7. Checksum Computation
    1
    2
    3
    4
    control MyComputeChecksum(inout headers hdr, inout metadata meta) {
    apply {
    }
    }
  8. DE parser
    1
    2
    3
    4
    5
    6
    7
    control 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 学习头部
    }
    }
  9. Switch
    1
    2
    3
    4
    5
    6
    7
    8
    V1Switch(
    MyParser(),
    MyVerifyChecksum(),
    MyIngress(),
    MyEgress(),
    MyComputeChecksum(),
    MyDeparser()
    ) main;
  10. controller_base.py
    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
    # 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)])
  11. controller_main.py
    1
    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>")
  12. controller_clone.py
    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
    # 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)

  13. network_clone.py
    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
    from 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

  1. p4app_digest.json
    1
    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": {
    }
    }
    }
  2. Headers
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    ypedef 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; // 以太网头部
    }
  3. Parser
    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; // 状态转移至accept
    }
    }
  4. Checksum Verification
    1
    2
    3
    4
    control MyVerifyChecksum(inout headers hdr, inout metadata meta) {
    apply {
    }
    }
  5. 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
    control 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(); // 否则执行广播表
    }
    }
    }
  6. Egress Processing
    1
    2
    3
    4
    5
    6
    7
    control MyEgress(inout headers hdr,
    inout metadata meta,
    inout standard_metadata_t standard_metadata) {

    apply { } // 不做egress处理
    }

  7. Checksum Computation
    1
    2
    3
    4
    control MyComputeChecksum(inout headers hdr, inout metadata meta) {
    apply {
    }
    }
  8. DE parser
    1
    2
    3
    4
    5
    6
    7
    control MyDeparser(packet_out packet, in headers hdr) {
    apply {
    //parsed headers have to be added again into the packet.
    packet.emit(hdr.ethernet); // 重新打包以太网头部
    }
    }

  9. Switch
    1
    2
    3
    4
    5
    6
    7
    8
    9
    V1Switch(
    MyParser(),
    MyVerifyChecksum(),
    MyIngress(),
    MyEgress(),
    MyComputeChecksum(),
    MyDeparser()
    ) main;

  10. controller_digest.py
    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
    # 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)

  11. network_digest.py
    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
    from 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模式为例)

  1. 启动网络拓扑sudo p4run
网络拓扑
  1. 以clone模式启动S1sudo python l2_learning_controller.py s1 cpu
启动s1
  1. 启动pingall命令,发现主机间成功通信
pingall命令

P4 05-L2_Learning
http://example.com/2025/07/30/P4 05-L2_Learning/
作者
Wsdbybyd
发布于
2025年7月30日
许可协议