Packet Reflector
第一个练习很简单,我们在前一篇文章已经见过了它的代码实现,本质上其实就是把源地址和目标地址调换一下,再把 packet 从哪里来的就送回哪里去。但这个练习的主要目的,是为了让我们熟悉开发环境,包括如何建立一个虚拟的网络。
文件描述
这个练习用到的文件有三个:
- p4app.json:这个文件用来描述我们要建立的网络拓扑结构;
- send_receive.py:这段 python 脚本用于发送和接收 packets;
- reflector.p4:这个就是我们要完成的 P4 代码。
其中 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 部分。
/* -*- 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);
transition accept;
}
}
/*************************************************************************
************ C H E C K S U M V E R I F I C A T I O N *************
*************************************************************************/
control MyVerifyChecksum(inout headers hdr, inout metadata meta) {
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) {
action swap_mac() {
macAddr_t tmp;
tmp = hdr.ethernet.dstAddr;
hdr.ethernet.dstAddr = hdr.ethernet.srcAddr;
hdr.ethernet.srcAddr = tmp;
}
apply {
swap_mac();
/* TODO 2: swap mac addresses */
/* TODO 3: set output port */
standard_metadata.egress_spec = standard_metadata.ingress_port;
}
}
/*************************************************************************
**************** E G R E S S P R O C E S S I N G *******************
*************************************************************************/
control MyEgress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
apply { }
}
/*************************************************************************
************* C H E C K S U M C O M P U T A T I O N **************
*************************************************************************/
control MyComputeChecksum(inout headers hdr, inout metadata meta) {
apply { }
}
/*************************************************************************
*********************** 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);
}
}
/*************************************************************************
*********************** S W I T C H *******************************
*************************************************************************/
V1Switch(
MyParser(),
MyVerifyChecksum(),
MyIngress(),
MyEgress(),
MyComputeChecksum(),
MyDeparser()
) main;
编写完代码后
- 运行拓扑图
sudo p4run
- 新打开一个终端,运行下面代码,这样就终端就转移到了h1上
mx h1
- 进行测试
reapeater练习
实现两个主机之间的通信,可以有两种方式实现
编写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;
}
}
}
下发控制表
在p4文件中,使用action传入参数,规定switch的出口
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();
}
}
写一个控制表txt并加入到拓扑图中
table_add repeater forward 1 => 2
table_add repeater forward 2 => 1
"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 的定义和使用等等。