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 学习笔记(六)- 实战网络层,自定义拓扑结构

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

上一篇 P4 学习笔记(五)- 实战链路层 里面我们实战练习了链路层的 Learning Switch,简单体验了一下和 control plane 通过 clone 和 digest 通信的过程。这篇文章,我们会练习配置一个自定义的网络拓扑结构和网络层的路由。在之前的练习的基础上,我们会接触到的新知识点有:

  • 网络层的 header 定义和 TTL 处理
  • p4utils 中用 manual 的方式自定义网络拓扑和 IP 地址
  • 一些简单的 debug 手段

目录

  • 自定义网络拓扑结构
  • P4 Source
  • 配置 Control Plane
  • 运行和测试

自定义网络拓扑结构

之前的练习,我们都是用 l2 作为 assignment_strategy,网络层的话,有对应的 l3 配置,但是如果想要测试自己定义的拓扑结构的话,比如下面这个经典的 pod 结构,还是 manual 的方式自由度更高:

img

pod 拓扑结构

定义这样的结构,我们要在 p4app.json 的 topology 里这样写:

"topology": {
    "assignment_strategy": "manual",
    "links": [
        ["h1", "s1"], ["h2", "s1"], ["h3", "s2"], ["h4", "s2"], 
        ["s1", "s3"], ["s2", "s4"], ["s2", "s3"], ["s1", "s4"]
    ],
    "hosts": {
        "h1": {
            "ip": "10.0.1.1/24",
            "mac": "08:00:00:00:01:11",
            "commands": [
                "route add default gw 10.0.1.10 dev h1-eth0",
                "arp -i h1-eth0 -s 10.0.1.10 08:00:00:00:01:00"
            ]
        },
        "h2": {
            "ip": "10.0.2.2/24",
            "mac": "08:00:00:00:02:22",
            "commands": [
                "route add default gw 10.0.2.20 dev h2-eth0",
                "arp -i h2-eth0 -s 10.0.2.20 08:00:00:00:02:00"
            ]
        },
        "h3": {
            "ip": "10.0.3.3/24",
            "mac": "08:00:00:00:03:33",
            "commands": [
                "route add default gw 10.0.3.30 dev h3-eth0",
                "arp -i h3-eth0 -s 10.0.3.30 08:00:00:00:03:00"
            ]
        },
        "h4": {
            "ip": "10.0.4.4/24",
            "mac": "08:00:00:00:04:44",
            "commands": [
                "route add default gw 10.0.4.40 dev h4-eth0",
                "arp -i h4-eth0 -s 10.0.4.40 08:00:00:00:04:00"
            ]
        }
    },
    "switches": {
        "s1": {},
        "s2": {},
        "s3": {},
        "s4": {}
    }
}

这里要注意的是 link 定义的顺序,因为 p4utils 是按照 link 添加的先后顺序,来定义 switch 上面的端口数的。

其次要说的就是每个 host 的 commands 都先配置了默认的 gateway 和对应的 IP 地址,然后配置了对应的链路层地址(MAC)。

这样一来,当我们运行 sudo p4run 的时候,就会有上面图中所示的网络拓扑结构了。

P4 Source

第二步,就可以写我们的 P4 源码了,过程和之前的练习类似,只不过这次我们要多加一个 IP header:

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

const bit<16> TYPE_IPV4 = 0x800;

/* 定义 Header */

typedef bit<9>  egressSpec_t;
typedef bit<48> macAddr_t;
typedef bit<32> ip4Addr_t;

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

// ip header 是我们新加进来的
header ipv4_t {
    bit<4>    version;
    bit<4>    ihl;
    bit<8>    diffserv;
    bit<16>   totalLen;
    bit<16>   identification;
    bit<3>    flags;
    bit<13>   fragOffset;
    bit<8>    ttl;
    bit<8>    protocol;
    bit<16>   hdrChecksum;
    ip4Addr_t srcAddr;
    ip4Addr_t dstAddr;
}

struct metadata {
    /* empty */
}

struct headers {
    ethernet_t   ethernet;
    ipv4_t       ipv4; // 记得放进 headers 的 struct 里
}

/* 熟悉的 Parser 配方 */

parser MyParser(packet_in packet,
                out headers hdr,
                inout metadata meta,
                inout standard_metadata_t standard_metadata) {

    state start {
        transition parse_ethernet;
    }

    state parse_ethernet {
        packet.extract(hdr.ethernet);
        transition select(hdr.ethernet.etherType) {
            TYPE_IPV4: parse_ipv4;
            default: accept;
        }
    }

    state parse_ipv4 {
        packet.extract(hdr.ipv4);
        transition accept;
    }

}

/* 暂时先跳过 Checksum 的验证阶段 */

control MyVerifyChecksum(inout headers hdr, inout metadata meta) {   
    apply {  }
}


/* 定义 Ingress 流程 */

control MyIngress(inout headers hdr,
                  inout metadata meta,
                  inout standard_metadata_t standard_metadata) {
    action drop() {
        mark_to_drop(standard_metadata);
    }
    
    action ipv4_forward(macAddr_t dstAddr, egressSpec_t port) {
        standard_metadata.egress_spec = port;
        hdr.ethernet.srcAddr = hdr.ethernet.dstAddr;
        hdr.ethernet.dstAddr = dstAddr;
        hdr.ipv4.ttl = hdr.ipv4.ttl - 1; // 更新 ttl
    }
    
    table ipv4_lpm {
        key = {
            hdr.ipv4.dstAddr: lpm; // 按照 longest-prefix 匹配
        }
        actions = {
            ipv4_forward;
            drop;
            NoAction;
        }
        size = 1024;
        default_action = drop();
    }
    
    apply {
        if (hdr.ipv4.isValid()) { // 确定 IP header 是正确的
            ipv4_lpm.apply(); // 才能用 ipv4_lpm 的表
        }
    }
}

/* 暂时不需要 Egress */

control MyEgress(inout headers hdr,
                 inout metadata meta,
                 inout standard_metadata_t standard_metadata) {
    apply {  }
}

/* 计算新的 Checksum */

control MyComputeChecksum(inout headers  hdr, inout metadata meta) {
     apply {
	update_checksum(
	    hdr.ipv4.isValid(),
            { hdr.ipv4.version,
	      hdr.ipv4.ihl,
              hdr.ipv4.diffserv,
              hdr.ipv4.totalLen,
              hdr.ipv4.identification,
              hdr.ipv4.flags,
              hdr.ipv4.fragOffset,
              hdr.ipv4.ttl,
              hdr.ipv4.protocol,
              hdr.ipv4.srcAddr,
              hdr.ipv4.dstAddr },
            hdr.ipv4.hdrChecksum,
            HashAlgorithm.csum16);
    }
}

/* 熟悉的 Deparser 配方 */

control MyDeparser(packet_out packet, in headers hdr) {
    apply {
        packet.emit(hdr.ethernet);
        packet.emit(hdr.ipv4);
    }
}

/* 把所有的流程排列好 */

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

这一个练习里,P4 的源码不是很复杂,但是添加了一些网络层的标准操作,就当是复习一遍 P4 每个环节了。

配置 Control Plane

用我们上次练习中用过的 p4utils,我们可以配置一下每个 switch 上面的路由表,也就是 ipv4_lpm:

import nnpy
import struct
from p4utils.utils.topology import Topology
from p4utils.utils.sswitch_API import SimpleSwitchAPI
from scapy.all import Ether, sniff, Packet, BitField

class L3Controller(object):

    def __init__(self):

        self.topo = Topology(db="topology.db)
        # 定义 switch 的 ID
        self.sw_name = ["s{}".format(i+1) for i in range(4)] 
        # switch 的 MAC 字典
        self.sw_mc_addr = {sw: "09:00:00:00:0{}:00".format(i+1) for i, sw in enumerate(self.sw_name)} 
        # 定义 host 的 ID
        self.hosts = ["h{}".format(i+1) for i in range(4)]
        # 初始化每个 switch 的 SimpleSwitchAPI
        self.controller = {sw: SimpleSwitchAPI(self.topo.get_thrift_port(sw)) for sw in self.sw_name}

        self.init()

    def init(self):

        for controller in self.controller.values(): controller.reset_state()

        # 配置 s1
        sw = "s1"
        self.controller[sw].table_add("ipv4_lpm", "ipv4_forward", ["10.0.1.1/32"], ["08:00:00:00:01:11", "1"])
        self.controller[sw].table_add("ipv4_lpm", "ipv4_forward", ["10.0.2.2/32"], ["08:00:00:00:02:22", "2"])
        self.controller[sw].table_add("ipv4_lpm", "ipv4_forward", ["10.0.3.3/32"], [self.sw_mc_addr["s3"], "3"])
        self.controller[sw].table_add("ipv4_lpm", "ipv4_forward", ["10.0.4.4/32"], [self.sw_mc_addr["s4"], "4"])

        # 配置 s2
        sw = "s2"
        self.controller[sw].table_add("ipv4_lpm", "ipv4_forward", ["10.0.1.1/32"], [self.sw_mc_addr["s3"], "4"])
        self.controller[sw].table_add("ipv4_lpm", "ipv4_forward", ["10.0.2.2/32"], [self.sw_mc_addr["s4"], "3"])
        self.controller[sw].table_add("ipv4_lpm", "ipv4_forward", ["10.0.3.3/32"], ["08:00:00:00:03:33", "1"])
        self.controller[sw].table_add("ipv4_lpm", "ipv4_forward", ["10.0.4.4/32"], ["08:00:00:00:04:44", "2"])

        # 配置 s3
        sw = "s3"
        self.controller[sw].table_add("ipv4_lpm", "ipv4_forward", ["10.0.1.1/32"], [self.sw_mc_addr["s1"], "1"])
        self.controller[sw].table_add("ipv4_lpm", "ipv4_forward", ["10.0.2.2/32"], [self.sw_mc_addr["s1"], "1"])
        self.controller[sw].table_add("ipv4_lpm", "ipv4_forward", ["10.0.3.3/32"], [self.sw_mc_addr["s2"], "2"])
        self.controller[sw].table_add("ipv4_lpm", "ipv4_forward", ["10.0.4.4/32"], [self.sw_mc_addr["s2"], "2"])

        # 配置 s4
        sw = "s4"
        self.controller[sw].table_add("ipv4_lpm", "ipv4_forward", ["10.0.1.1/32"], [self.sw_mc_addr["s1"], "2"])
        self.controller[sw].table_add("ipv4_lpm", "ipv4_forward", ["10.0.2.2/32"], [self.sw_mc_addr["s1"], "2"])
        self.controller[sw].table_add("ipv4_lpm", "ipv4_forward", ["10.0.3.3/32"], [self.sw_mc_addr["s2"], "1"])
        self.controller[sw].table_add("ipv4_lpm", "ipv4_forward", ["10.0.4.4/32"], [self.sw_mc_addr["s2"], "1"])

if __name__ == "__main__":
    controller = L3Controller()

运行和测试

到了这一步,我们可以运行环境并开始测试了。

sudo p4run

进入 mininet 的 CLI 之后,我们可以测试:

mininet> h1 ping h3 -c1

如果能 ping 通的话,就说明我们的配置没有问题。

如果发现有问题的话,有以下几种方法可以 debug。

第一个方法就是在 log/s1.log 这一类的日志文件里,检查每个 switch 是否有正常的处理所有的 packet。

第二个方法就是在 pcap/s1-eth1_out.pcap 这一类的 pcap 文件里,看一下 ping 的时候断在了哪个节点。比如 h1 ping h3 会在上面定义的 pod 拓扑结构里先后在:

=== forward ===
s1-eth1_out 
-> s1-eth3_in 
-> s4-eth2_out 
-> s4-eth1_in 
-> s2-eth3_out 
-> s2-eth1_in
=== backward ===
-> s2-eth1_out
-> s2-eth3_in
-> s4-eth1_out
-> s4-eth2_in
-> s1-eth3_out 
-> s1-eth1_in

这些 pcap 中出现。大家可以对照一下配置的 routing rules 分析一下为什么是这样的顺序,作为练习。

第三个方法,也是我们之前用过的方法,就是通过 thrift_port 连接到 SimpleSwitchAPI,确认一下我们的表有没有被正确的填充:

$ simple_switch_CLI --thrift-port 9090
Obtaining JSON from switch...
Done
Control utility for runtime P4 table manipulation
RuntimeCmd: table_dump ipv4_lpm

学会使用这几种测试方法,在之后定义其他的网络拓扑结构的话,就会知道如何解决问题了。

小结

这篇文章,我们学会了定义我们需要的网络拓扑结构,并且在网络层配置路由表,实现网络报文的转发,就当是建立了一个小型的数据中心吧!

  • 相关内容转载自本链接
#p4#
免费的编程中文书籍索引
P4 学习笔记(五)- 实战链路层
shankusu2017@gmail.com

shankusu2017@gmail.com

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