Golang编程语言知识介绍


  • 首页

  • todo

  • 思考

  • life

  • food

  • OS

  • lua

  • redis

  • Golang

  • C

  • TCP/IP

  • ebpf

  • p4

  • OpenVPN

  • IPSec

  • L2TP

  • DNS

  • distributed

  • web

  • OpenWRT

  • 运维

  • Git

  • 鸟哥的私房菜

  • IT杂谈

  • 投资

  • About Me

  • 友情链接

  • FTP

  • 搜索
close

P4 学习笔记(四)- 实战 Reflector & Repeater

时间: 2023-09-15   |   分类: p4     |   阅读: 3648 字 ~8分钟

为了能够更好的练习 P4 这门语言,这一篇文章我们一起配置一下环境,实战两个简单的练习项目。

目录

  • 环境配置

  • Packet Reflector

    • 文件描述
      • 开发步骤
      • 解决方案
  • Packet Repeater

环境配置

一个完整的 P4 开发环境里,我们需要编译 P4 的代码(用 p4c 编译[1]),把编译好的文件跑在软件交换机上(我们会用 bmv2[2]),在创建的虚拟网络拓扑结构中验证我们实现的逻辑是否是正确的。

所以我们需要的安装的工具依次有:

  • 编译器 p4c
  • 软件交换机 BEHAVIORAL MODEL (bmv2)
  • 用于构建虚拟网络的 Mininet[3]
  • 用来抓包的 Wireshark
  • ETHz NSG 网络团队开发的用于高效开发的 P4 utils[4]

当然,安装这一堆文件还是会很痛苦的,所以 ETHz 的大佬们给大家提供了安装好所有需要的工具的虚拟机 ova 文件,可以在这里科学下载。下载好了就可以直接用 VirtualBox 打开了!友情提醒,VirtualBox 记得安装 guest additions,这样会获得正确的分辨率,保护眼睛,从我做起。

环境配置好,我们就可以下载一下实战需要的源代码啦!我们用到的练习文件在这个 P4-learning 的 repository 里,在虚拟机里下载一份:

$ git clone https://github.com/nsg-ethz/p4-learning.git

然后我们的练习就在其中的 exercises 文件夹里。

以下的练习都在虚拟机里实现的,如果大家决定自己安装所有的工具,请按照自己的配置进行调整。

Packet Reflector

第一个练习很简单,我们在前一篇文章已经见过了它的代码实现,本质上其实就是把源地址和目标地址调换一下,再把 packet 从哪里来的就送回哪里去。但这个练习的主要目的,是为了让我们熟悉开发环境,包括如何建立一个虚拟的网络。

文件描述

这个练习用到的文件有三个:

  • p4app.json:这个文件用来描述我们要建立的网络拓扑结构;
  • send_receive.py:这段 python 脚本用于发送和接收 packets;
  • reflector.p4:这个就是我们要完成的 P4 代码。

首先我们看一下第一个 json 文件:

{
  "program": "reflector.p4",
  "switch": "simple_switch",
  "compiler": "p4c",
  "options": "--target bmv2 --arch v1model --std p4-16",
  "switch_cli": "simple_switch_CLI",
  "cli": true,
  "pcap_dump": true,
  "enable_log": true,
  ...
  "topology": {
    "assignment_strategy": "l2",
    "links": [["h1", "s1"]],
    "hosts": {
      "h1": {
      }
    },
    "switches": {
      "s1": {
      }
    }
  }
}

里面规定了使用 p4c 作为编译器,用 bmv2 的 v1model 作为目标架构,使用 P4 16 的语言标准等等,具体的用法可以参考 p4 utils 的官方文档。但其中拓扑结构的部分,也就是 topology 那一段,直接定义了我们的目标结构。

其中 l2 的 assignment_strategy 表示我们假设所有的交换机都在链路层(layer 2)工作,所有的 hosts 都会被放在同一个子网中。每个 host 的 ARP table 也会被自动填好它的所有邻居的 MAC 地址。除了 l2,还有 mixed 和 l3 两种选项。

使用 mixed 的时候,每个 host 只能和一个交换机相连,连接在同一个交换机上的 host 属于同一个 /24 子网中,这个交换机于是就作为了 host 的网络层网关(layer 3 gateway)。

使用 l3 的时候,每个交换机都在网络层(layer 3)工作,所以每个 interface 都属于一个独立的子网。至于具体的 IP 分配,可以参考 p4 utils 的官方文档。

第二个文件,send_receive.py,使用 scapy 这个库里的大部分函数,关于其中主要的步骤,在下面加了一些注释。

#!/usr/bin/env python
import sys
import socket
import random
import time
from threading import Thread, Event
from scapy.all import *


class Sniffer(Thread):
    '''
    创建一个 Sniffer 的类,抓包
    '''
    def  __init__(self, interface="eth0"):
        
        super(Sniffer, self).__init__()

        self.interface = interface
        self.my_mac = get_if_hwaddr(interface)
        self.daemon = True

        self.socket = None
        self.stop_sniffer = Event() # 创建一个停止抓包的 Event

    def isNotOutgoing(self, pkt):
        # 如果 packet 的源 MAC 地址不是
        return pkt[Ether].src != self.my_mac

    def run(self):
        # 创建一个 Layer 2 的 socket,只保留 IP packet
        self.socket = conf.L2listen(
            type=ETH_P_ALL,
            iface=self.interface,
            filter="ip"
        )
        sniff(opened_socket=self.socket, prn=self.print_packet, lfilter=self.isNotOutgoing, stop_filter=self.should_stop_sniffer)

    def join(self, timeout=None):
        # 终止我们的 sniffer
        self.stop_sniffer.set()
        super(Sniffer, self).join(timeout)

    def should_stop_sniffer(self, packet):
        # 如果停止抓包的 Event 被设置了,就不再抓包
        return self.stop_sniffer.isSet()

    def print_packet(self, packet):
        print "[!] A packet was reflected from the switch: "
        #packet.show()
        ether_layer = packet.getlayer(Ether)
        print("[!] Info: {src} -> {dst}\n".format(src=ether_layer.src, dst=ether_layer.dst))

def get_if():
    ifs=get_if_list()
    iface=None # "h1-eth0" 是我们的目标 interface,代表 host 1 上的 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 send_packet(iface, addr):
    '''
    构造一个 Ethernet + IP 的 packet
    '''
    raw_input("Press the return key to send a packet:")
    print "Sending on interface %s to %s\n" % (iface, str(addr))
    pkt =  Ether(src=get_if_hwaddr(iface), dst='00:01:02:03:04:05') # 设置 Ethernet header
    pkt = pkt /IP(dst=addr) # 再加上 IP header
    sendp(pkt, iface=iface, verbose=False)

def main():

    # 目标地址
    addr = "10.0.0.2"
    addr = socket.gethostbyname(addr)
    iface = get_if() # 获取当前的 interface

    listener = Sniffer(iface)
    listener.start()
    time.sleep(0.1)

    try:
        while True:
            # 每半秒发送一个 packet
            send_packet(iface, addr)
            time.sleep(0.5)

    except KeyboardInterrupt:
        print("[*] Stop sniffing")
        listener.join(2.0)

        if listener.isAlive():
            listener.socket.close()

if __name__ == '__main__':
    main()

第三个文件,reflector.p4,就是我们要修改的 P4 源码了。其中,所有的模块按照顺序,都排列在 main 函数里。这个练习里,我们要做的,是 Parser、Ingress、和 Deparser 部分。

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

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

开发步骤

简单的看过了需要的文件之后,我们可以开始一步一步开始开发了。

第一步,把 p4app.json 里定义的网络拓扑跑起来,只需要在它所在的路径下跑:

$ sudo p4run

这行命令会自动调用一个 python 脚本,解析 p4app.json,创建 mininet 的虚拟网络环境,编译 P4 代码,并安装在 bmv2 的软件交换机里。具体的流程感兴趣的话可以参考 p4 utils 的官方文档。

跑完之后,就有 mininet 的 CLI 了:

img

Mininet CLI

比如我们可以看一下正在运行的网络拓扑,我们就知道有一个 host,一个 switch 分别记作 h1 和 s1,并且它们之间的 link 是联通的。

mininet> nodes
h1 s1
mininet> links
h1-eth0<->s1-eth1 (OK OK)

关于 mininet 的更多使用方法可以参考自带的 help:

img

Mininet CLI Help

第二步,从 mininet 的 CLI 里,我们可以用 xterm h1 登陆 host h1,得到一个 shell 的界面。在这里,我们可以运行 python send_receive.py 从 h1 向 s1 发送 packet。但因为我们还没有改写 reflector.p4 这个文件,所以不会收到返回来的 packet:

img

接下来,我们看一下该怎么修改 reflector.p4 这个文件才能收到返回的 packet。

解决方案

在第一个 TODO 的地方,我们要做的只是简单的解析 header:

/* -*- P4_16 -*- */
#include <core.p4>
#include <v1model.p4>

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

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

struct metadata {
    /* empty */
}

struct headers {
    ethernet_t   ethernet;
}

/*************************************************************************
*********************** 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 1: parse ethernet header */
          packet.extract(hdr.ethernet);  // 使用 extract 就可以得到 ethernet header
          transition accept;
      }

}

第二个 TODO 的部分,也就是 Ingress,我们可以直接把交换地址的过程写在 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) {

    apply {
       /* TODO 2: swap mac addresses */
       macAddr tmp = hdr.ethernet.dstAddr;
       hdr.ethernet.dstAddr = hdr.ethernet.srcAddr;
       hdr.ethernet.srcAddr = tmp;
       /* TODO 3: set output port    */
       standard_metadata.egress_spec = standard_metadata.ingress_port;
    }
}

也可以单独写一个 action 出来:

control MyIngress(inout headers hdr,
                  inout metadata meta,
                  inout standard_metadata_t standard_metadata) {
    
    action swap_mac() {
      macAddr tmp = hdr.ethernet.dstAddr;
      hdr.ethernet.dstAddr = hdr.ethernet.srcAddr;
      hdr.ethernet.srcAddr = tmp;
    }

    apply {
       /* TODO 2: swap mac addresses */
       swap_mac();

       /* TODO 3: set output port    */
       standard_metadata.egress_spec = standard_metadata.ingress_port;
    }
}

第三个 TODO,类似上一篇讲 Deparser 的过程,不需要特殊的操作,只需要一个 emit:

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

control MyDeparser(packet_out packet, in headers hdr) {
    apply {
        /* TODO 4: deparse ethernet header */
        packet.emit(hdr.ethernet);
	}
}

写完所有的 TODO 之后,我们就可以再次编译了。重新编译的时候,我们不需要关掉已经在运行的 mininet,只需要:

mininet> p4switch_reboot s1;

就可以直接加载新写的 P4 代码了,节约时间。

编译完成之后,我们再在 h1 上跑 send_receive.py,就能收到镜像回来的 packet 了:

img

这个练习到这里就结束了。如果大家心满意足了,就可以在 CLI 里退出了,用 quit 或者 exit 或者 Ctrl-D。

Repeater

第二个练习,我们要实现的是一个简单的双端口交换机,能把 packet 在两个 hosts 之间进行传递。我们的网络拓扑结构如下图所示:

img

Repeater Exercise: Topology

在配置文件 p4app.json 中,拓扑结构体现为:

"topology": {
    "assignment_strategy": "l2",
    "links": [
        ["h1", "s1"],
        ["h2", "s1"]
    ],
    "hosts": {
        "h1": {},
        "h2": {}
    },
    "switches": {
        "s1": {}
    }
}

和上一个练习相比,这个结构多了一个 host h2,也多了一个 link。

我们的目标就是实现 h1 和 h2 之间能 ping 通。测试的时候在 mininet 的 cli 里直接用 mininet> h1 ping h2 测试就好了。

为了实现这样一个 repeater,我们有两种解决方案,第一种解决方案,就是在 Ingress 的部分写一个静态的规则。

// solution 1
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;
        }
    }
}

我们跑上 sudo p4run 之后,来到 mininet 的 CLI 之后,可以直接实验,ping 通就对了!

img

Ping test

当然,除了 ping,我们还可以用 iperf,测一下 throughput:

img

Iperf test

练习里还提供了两段 python,一个是 send.py,一个是 receive.py,我们打开 h2 的终端,跑上 python receive.py,再打开一个 h1 的终端,跑上 python send.py 10.0.0.2 "[whatever message you want to send here]",也可以验证:

img

Python send-receive test

当然,除了第一种“静态”的解决方案,我们还有一个用表来解决的方法。

首先,我们在 repeater.p4 里的 Ingress 部分建一个表,就叫 repeater,只做最基本的 Match-Action:

control MyIngress(inout headers hdr,
                  inout metadata meta,
                  inout standard_metadata_t standard_metadata) {

    // 定义一个 action,确定出口端
    action forward(bit<9> egress_port){ // 这里的参数是无方向的,因为是来查表得到的
        standard_metadata.egress_spec = egress_port;
    }

    table repeater {
        key = {
            standard_metadata.ingress_port: exact; // 完全 match 才可以
        }
        actions = { // 两种动作
            forward; // 一定是之前已经声明的 action
            NoAction;
        }
        size = 2; // 只需要两个端口的规则,所以表里只需要两个 entries
        default_action = NoAction;
    }

    apply {
        repeater.apply();
    }
}

有了这个代码,我们就相当于告诉了交换机,每次收到 packet 的时候,要去查表,再决定怎么转发。但到这里,我们的表还是空的。所以现在我们要在 repeater 的表里填上我们想要的规则。

第一步就是写一个简单的 txt 文件,比如叫 s1-commands.txt,里面写上:

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

这两行就会在 repeater 这个表里,填好两行 Match-Action 的规则了。这两行的语法,就是在表里添加规则的语法:

tabel_add <table_name> <action_name> <match_fields> => <action_parameters>

这里我们的 table_name 是 repeater,我们创建的 action_name 也就是那个“函数”就是 forward。更多关于这部分的语法可以参考这个文档。

写好了这个文件之后,只要最后在 p4app.json 的配置文件里,对我们的 switch把这个 txt 文件作为 cli_input 就好了:

"topology": {
    "assignment_strategy": "l2",
    "links": [
        ["h1", "s1"],
        ["h2", "s1"]
    ],
    "hosts": {
        "h1": {},
        "h2": {}
    },
    "switches": {
        "s1": {
          "cli_input": "s1-commands.txt"
        }
    }
}

这样,我们第二种方法也可以获得和第一种方法一样的效果。

小结

这篇文章,我们正式开始了 P4 的实战部分,熟悉了实验需要的 virtualbox、p4utils、mininet 等环境和工具。前两个练习,相对简单,但帮助我们复习了 P4 的基础语法和架构,比如 Parser->Ingress->Egress->Deparser 的流水线、standard_metadata 包含的数据、 table 的定义和使用等等。

之后的文章会继续实战,上手更复杂一点的习题。

参考

  1. ^P4 Reference Compiler https://github.com/p4lang/p4c
  2. ^Behavioral Model (bmv2) https://github.com/p4lang/behavioral-model
  3. ^Mininet http://mininet.org/
  4. ^P4 Utils https://github.com/nsg-ethz/p4-utils
  • 相关内容转载自本链接
#p4#
P4 学习笔记(五)- 实战链路层
P4 学习笔记(三)- 控制逻辑与完整的工作流
shankusu2017@gmail.com

shankusu2017@gmail.com

日志
分类
标签
GitHub
© 2009 - 2025
粤ICP备2021068940号-1 粤公网安备44011302003059
0%