上一篇 P4 学习笔记(五)- 实战链路层 里面我们实战练习了链路层的 Learning Switch,简单体验了一下和 control plane 通过 clone 和 digest 通信的过程。这篇文章,我们会练习配置一个自定义的网络拓扑结构和网络层的路由。在之前的练习的基础上,我们会接触到的新知识点有:
- 网络层的 header 定义和 TTL 处理
- p4utils 中用
manual
的方式自定义网络拓扑和 IP 地址 - 一些简单的 debug 手段
目录
自定义网络拓扑结构
之前的练习,我们都是用 l2
作为 assignment_strategy
,网络层的话,有对应的 l3
配置,但是如果想要测试自己定义的拓扑结构的话,比如下面这个经典的 pod 结构,还是 manual
的方式自由度更高:
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
学会使用这几种测试方法,在之后定义其他的网络拓扑结构的话,就会知道如何解决问题了。
小结
这篇文章,我们学会了定义我们需要的网络拓扑结构,并且在网络层配置路由表,实现网络报文的转发,就当是建立了一个小型的数据中心吧!