Featured image of post P4示例程序-2 中继器

P4示例程序-2 中继器

在交换机中编写Match-Action匹配表,并通过Thrift、P4Runtime两种方式为匹配表添加表项

例程拓扑

该例程实现了简单的双端口交换机,使得两个主机能够进行通信,即类似于中继器的功能。

静态配置方案

拓扑描述文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
  "p4_src": "repeater.p4",
  "cli": true,
  "pcap_dump": true,
  "enable_log": true,
  "topology": {
    "assignment_strategy": "l2",
    "links": [["h1", "s1"], ["h2", "s1"],
    "hosts": {
      "h1": {},
      "h2": {}
    },
    "switches": {
      "s1": {
      }
    }
  }
}

相较于“P4示例程序-1 数据包反射器”中的拓扑配置文件,只需额外配置主机h1、h2,并添加主机与交换机之间的链路连接。Python脚本的方式同理,在此省略。

交换机程序

由于无需对数据包进行解析、更改,因此无需定义数据包格式、解器器、封装器、校验值的校验与计算,只需对入队列侧的处理逻辑进行编程,从P4的标准元数据中提取数据包的入端口,并填写出端口。

1
2
3
4
5
6
7
8
9
parser MyParser(packet_in packet,
                out headers hdr,
                inout metadata meta,
                inout standard_metadata_t standard_metadata) {
    // 无线对数据包进行解析,因此直接accept结束即可
    state start{
        transition accept;
    }
}
 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 standard_metadata) {
   apply {
       // 如果入端口是1号端口 => 从2号端口转发
       if (standard_metadata.ingress_port == 1){
           standard_metadata.egress_spec = 2;
       }
       // 如果入端口是2号端口 => 从1号端口转发
       else if (standard_metadata.ingress_port == 2){
           standard_metadata.egress_spec = 1;
       }
   }
}

运行测试

在mininet中进行Ping测试与带宽测试:

1
2
mininet > pingall
mininet > iperf

在新窗口使用以下命令进入host2的命令行,并运行接收数据包的Python脚本:

1
2
mx h2
python receive.py

在新窗口使用以下命令进入host1的命令行,并运行发送数据包的Python脚本:

1
2
mx h1
python send.py 10.0.0.2 "HelloWorld"

基于表的配置方案

交换机程序

1
2
3
4
5
6
7
8
9
parser MyParser(packet_in packet,
                out headers hdr,
                inout metadata meta,
                inout standard_metadata_t standard_metadata) {
    // 无线对数据包进行解析,因此直接accept结束即可
    state start{
        transition accept;
    }
}
 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
control MyIngress(inout headers hdr,
                  inout metadata meta,
                  inout standard_metadata_t standard_metadata) {
    // 定义名为forward的动作
    // 注意这里的参数egress_port为从匹配表中查到的值,因此无需表明方向
    action forward(bit<9> egress_port){
        // 将匹配中查找到的值设置出端口号
        standard_metadata.egress_spec = egress_port;
    }

    // 定义名为repeater的匹配表
    table repeater {
        // 定义匹配表的键为P4标准元数据中的ingress_port
        // exact指示为精确匹配,其余的匹配方式包括最长前缀字段匹配lpm、三元匹配ternary
        key = {
            standard_metadata.ingress_port: exact;
        }
        // 设置动作集,可能的动作包括forward与NoAction
        // NoAction是P4内置的动作
        actions = {
            forward;
            NoAction;
        }
        // 匹配表的大小为2
        // 一个是从Host1发向Host2的规则,一个是Host2发向Host1的规则
        size = 2;
        // 设置匹配表的默认动作是NoAction
        default_action = NoAction;
    }

    apply {
        // 查询repeater匹配表
        repeater.apply();
    }
}

基于Thrift的数据平面配置

Thrift客户端

P4-Utils包含了Thrift框架,可以通过Thrift客户端(控制平面)向Thrift服务端(运行BMv2交换机的数据平面)发送指令,进而修改数据平面的表项。在执行p4run命令启动网络时,在输出的信息中可以查看到Thrift服务端的端口号为9090。

因此新建命令行,使用以下命令启动Thrift客户端simple_switch_CLI、并连接到服务端,可以通过?查询支持的命令,并通过help查询命令的具体用法。

1
2
3
simple_switch_CLI --thrift-port 9090 --thrift-ip 127.0.0.1
RuntimeCmd: ?
RuntimeCmd: help table_add

根据提示,使用以下命令:在repeater表中添加表项,其查询键为1、执行的动作为forward、为执行动作forward传的参为2。

1
2
RuntimeCmd: table_add repeater forward 1 => 2
RuntimeCmd: table_add repeater forward 2 => 1

文本输入

创建文件s1-commands.txt,将上述在命令行键入的命令可以直接放入文本文件。

1
2
table_add repeater forward 1 => 2
table_add repeater forward 2 => 1

为将配置匹配表的文本送入,需要在拓扑描述文件中为S1交换机使用cli_input字段引用文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
{
  "p4_src": "repeater_with_table.p4",
  "cli": true,
  "pcap_dump": true,
  "enable_log": true,
  "topology": {
    "assignment_strategy": "l2",
    "links": [["h1", "s1"], ["h2", "s1"],
    "hosts": {
      "h1": {},
      "h2": {}
    },
    "switches": {
      "s1": {
        "cli_input": "s1-commands.txt"
      }
    }
  }
}

类似的若使用Python脚本作为拓扑描述文件,需要使用setP4CliInput函数为交换机S1指定文件:

 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
from p4utils.mininetlib.network_API import NetworkAPI

net = NetworkAPI()

# Network general options
net.setLogLevel('info')
net.enableCli()

# Network definition
net.addP4Switch('s1')
net.setP4Source('s1','repeater_with_table.p4')
net.setP4CliInput('s1', 's1-commands.txt')
net.addHost('h1')
net.addHost('h2')
net.addLink('s1', 'h1')
net.addLink('s1', 'h2')

# Assignment strategy
net.l2()

# Nodes general options
net.enablePcapDumpAll()
net.enableLogAll()
# Start network
net.startNetwork()

基于P4Runtime

P4Runtime是P4社区定义的一个控制平面与数据平面之间的标准化接口。

控制平面

可以通过Python脚本连接P4Runtime服务端,并配置匹配表。

 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
from p4utils.utils.helper import load_topo
from p4utils.utils.sswitch_p4runtime_API import SimpleSwitchP4RuntimeAPI

# 加载网络拓扑配置文件,获取拓扑
# topology.json配置文件在网络启动时自动生成
topo = load_topo('topology.json')
# 此处个人认为命名为switches更易于理解
controllers = {}
# 遍历拓扑中所有支持P4Runtime的交换机
for switch, data in topo.get_p4rtswitches().items():
    # 为每一个支持P4Runtime的交换机实例化一个SimpleSwitchP4RuntimeAPI对象,用于建立连接
    controllers[switch] = SimpleSwitchP4RuntimeAPI(
            data['device_id'],  # 交换机的设备ID,从0开始编号
            data['grpc_port']  # 交换机监听的gRPC端口
            p4rt_path=data['p4rt_path'],  # P4Runtime协议文件路径,描述P4程序结构
            json_path=data['json_path']  # BMv2的JSON配置文件路径,数据平面行为定义
    )

# 获取名为s1的交换机控制对象
controller = controllers['s1']
# 清空该交换机repeater表中的全部表项
controller.table_clear('repeater')
# 向repeater表添加一条规则:匹配ingress_port=1时,执行forward动作,参数egress_port=2
controller.table_add('repeater', 'forward', ['1'], ['2'])
# 向repeater表添加另一条规则:匹配ingress_port=2时,执行forward动作,参数egress_port=1
controller.table_add('repeater', 'forward', ['2'], ['1'])

拓扑描述文件:Python脚本

使用Python脚本是一种简单的方式,主要变动在于:

  1. setCompiler(p4rt=True):指示编译P4时生成P4Runtime所需的JSON与P4Info;
  2. execScript(‘python controller.py’, reboot=True):指示在网络启动后自动运行controller.py脚本(即控制平面配置匹配表的脚本),reboot为True指示网络重启后也重新运行脚本;
  3. addP4RuntimeSwitch(‘s1’):添加一个支持P4Runtime的交换机,命名为S1。
 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
from p4utils.mininetlib.network_API import NetworkAPI

net = NetworkAPI()

# Network general options
net.setLogLevel('info')
net.setCompiler(p4rt=True)
net.execScript('python controller.py', reboot=True)
net.enableCli()

# Network definition
net.addP4RuntimeSwitch('s1')
net.setP4Source('s1','repeater.p4')
net.addHost('h1')
net.addHost('h2')
net.addLink('s1', 'h1')
net.addLink('s1', 'h2')

# Assignment strategy
net.l2()

# Nodes general options
net.enablePcapDumpAll()
net.enableLogAll()

# Start network
net.startNetwork()

拓扑描述文件:JSON格式

使用JSON格式定义拓扑文件稍微复杂些:

  1. switch_node:指示创建交换机时,使用p4utils.mininetlib.node模块(Python模块)中的P4RuntimeSwitch类,该类支持P4Runtime;
  2. compiler_module:指示编译P4时生成P4Runtime所需的JSON与P4Info;
  3. exec_scripts:指示在网络启动后自动运行controller.py脚本(即控制平面配置匹配表的脚本),reboot为True指示网络重启后也重新运行脚本。
 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
{
  "p4_src": "repeater.p4",
  "cli": true,
  "pcap_dump": true,
  "enable_log": true,
  "switch_node":
  {
    "module_name": "p4utils.mininetlib.node",
    "object_name": "P4RuntimeSwitch"
  },
  "compiler_module":
  {
    "options":
    {
      "p4rt": true
    }
  },
  "exec_scripts": 
  [
    {
        "cmd": "python controller.py",
        "reboot_run": true
    }
  ],
  "topology": {
    "assignment_strategy": "l2",
    "links": [["h1", "s1"], ["h2", "s1"]],
    "hosts": {
      "h1": {
      },
      "h2": {
      }
    },
    "switches": {
      "s1": {
      }
    }
  }
}

补充:多键多动作参数的匹配表

匹配表可以具有多个键,并为动作提供多个参数,例如:

 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
control MyIngress(inout headers hdr,
                  inout metadata meta,
                  inout standard_metadata_t standard_metadata) {
    // 定义动作,该动作包含两个参数
    // egress_port用于确定数据包的出端口
    // priority用来打标签,标签打在数据包的etherType字段
    action forward_and_tag(bit<9> egress_port, bit<3> priority) {
        standard_metadata.egress_spec = egress_port;
        hdr.ethernet.etherType = priority;
    }

    // 匹配表
    table mac_table {
        key = {
            standard_metadata.ingress_port : exact;     // 第一个键:数据包的入端口
            hdr.ethernet.dstAddr           : exact;     // 第二个键:目的MAC地址
        }
        actions = {
            forward_and_tag;
            NoAction;
        }
        size = 1024;
        default_action = NoAction;
    }

    apply {
        mac_table.apply();
    }
}

此时配置匹配表的文本文件可以写入:

1
table_add mac_table forward_and_tag 1 00:aa:bb:cc:dd:ee => 2 5

附录 端侧收发函数

数据包发送脚本

 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
#!/usr/bin/env python
import sys
import socket
import random
from subprocess import Popen, PIPE
import re

from scapy.all import sendp, get_if_list, get_if_hwaddr
from scapy.all import Ether, IP, UDP, TCP

def get_if():
    ifs=get_if_list()
    iface=None # "h1-eth0"
    for i in get_if_list():
        if "eth0" in i:
            iface=i
            break
    if not iface:
        print("Cannot find eth0 interface")
        exit(1)
    return iface

def get_dst_mac(ip):
    try:
        pid = Popen(["arp", "-n", ip], stdout=PIPE)
        s = str(pid.communicate()[0])
        mac = re.search(r"(([a-f\d]{1,2}\:){5}[a-f\d]{1,2})", s).groups()[0]
        return mac
    except:
        return None

def main():
    if len(sys.argv)<3:
        print('pass 2 arguments: <destination> "<message>"')
        exit(1)

    addr = socket.gethostbyname(sys.argv[1])
    iface = get_if()

    if len(sys.argv) > 3:
        tos = int(sys.argv[3]) % 256
    else:
        tos = 0

    ether_dst = get_dst_mac(addr)

    if not ether_dst:
        print("Mac address for %s was not found in the ARP table" % addr)
        exit(1)

    print("Sending on interface %s to %s" % (iface, str(addr)))
    pkt =  Ether(src=get_if_hwaddr(iface), dst=ether_dst)
    pkt = pkt /IP(dst=addr,tos=tos) / sys.argv[2]
    sendp(pkt, iface=iface, verbose=False)

if __name__ == '__main__':
    main()

数据包接收脚本

 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
#!/usr/bin/env python
import sys
import os

from scapy.all import sniff, get_if_list, Ether, get_if_hwaddr, IP, Raw

def get_if():
    iface=None
    for i in get_if_list():
        if "eth0" in i:
            iface=i
            break
    if not iface:
        print("Cannot find eth0 interface")
        exit(1)
    return iface

def isNotOutgoing(my_mac):
    my_mac = my_mac
    def _isNotOutgoing(pkt):
        return pkt[Ether].src != my_mac
    return _isNotOutgoing

def handle_pkt(pkt):
    print("Packet Received:")
    ether = pkt.getlayer(Ether)
    ip = pkt.getlayer(IP)
    msg = ip.payload

    print("###[ Ethernet ]###")
    print("  src: {}".format(ether.src))
    print("  dst: {}".format(ether.dst))
    print("###[ IP ]###")
    print("  src: {}".format(ip.src))
    print("  dst: {}".format(ip.dst))
    print("###[ MESSAGE ]###")
    print("  msg: {}".format(str(msg)))
    print()

def main():
    ifaces = [i for i in os.listdir('/sys/class/net/') if 'eth' in i]
    iface = ifaces[0]
    print("sniffing on %s" % iface)
    sys.stdout.flush()

    my_filter = isNotOutgoing(get_if_hwaddr(get_if()))
    sniff(filter="ip", iface = iface,
          prn = lambda x: handle_pkt(x), lfilter=my_filter)

if __name__ == '__main__':
    main()
Licensed under CC BY-NC-SA 4.0
皖ICP备2025083746号-1
公安备案 陕公网安备61019002003315号



使用 Hugo 构建
主题 StackJimmy 设计