在本次实验中,我们需要利用ONOS完成对数据面的控制
1.使能packet的IO功能,验证链路发现
main.p4提供了和P4Runtime的通信的消息的定义格式,分别是PacketIn和PacketOut,他们都被加上了一个注解,表示这是一个控制器交互的数据包包头格式
@controller_header("packet_in")
header cpu_in_header_t {
port_num_t ingress_port;
bit<7> _pad;
}
@controller_header("packet_out")
header cpu_out_header_t {
port_num_t egress_port;
bit<7> _pad;
}
这些报头用于携带数据包的初始的交换机入端口,并指定数据包转发的预期出端口。
- 当Stratum中的P4Runtime代理从交换机CPU端口接收到数据包时,它希望找到数据包的CPU_in_header_t标头作为帧中的第一个标头,为了解析这个数据包,它去查看P4Info文件的controller_packet_metadata部分,以确定在帧的开头剥离的比特数,并填充PacketIn消息的相应元数据字段,在本例中就是一个入端口。
- 类似地,当Stratum接收到P4Runtime PacketOut消息时,它使用在PacketOut的元数据字段中找到的值对帧进行序列化,并在将其提供给管道解析器之前将cpu_out_header_t预附加到帧中
因此,在p4代码中,我们应该实现如下几个功能
1.1 解析数据包
这个早在第二部分就讲过了,这里做一个回忆,解析指的是解析来自于控制面的packet_out消息(解析packet_in消息是控制面的工作)
state parse_packet_out {
packet.extract(hdr.cpu_out);
transition parse_ethernet;
}
1.2什么时候反馈控制器
这里设计了两个操作,分别是直接发送给CPU,另一个则是克隆一份发给CPU
CPU_CLONE_SSION_ID:为要克隆到CPU端口的数据包指定镜像会话。与此会话ID相关联的数据包将被克隆到CPU_port,并通过其出口进行传输(由桥接/路由/acl表设置)。要使克隆工作,P4Runtime控制器需要首先插入一个将此会话ID映射到CPU_PORT的CloneSessionEntry。
action send_to_cpu() {
standard_metadata.egress_spec = CPU_PORT;
}
action clone_to_cpu() {
// Cloning is achieved by using a v1model-specific primitive. Here we
// set the type of clone operation (ingress-to-egress pipeline), the
// clone session ID (the CPU one), and the metadata fields we want to
// preserve for the cloned packet replica.
clone3(CloneType.I2E, CPU_CLONE_SESSION_ID, { standard_metadata.ingress_port });
}
1.3 收到了Packet OUT消息要怎么做
这里,我们在Ingress控制流的apply中,加入一段代码:
if (hdr.cpu_out.isValid()) {
standard_metadata.egress_spec = hdr.cpu_out.egress_port;
hdr.cpu_out.setInvalid();
exit;
}
这段代码的核心思想就是如果这个包是控制面下来的包,就要把它转发到由控制面指定的位置去
1.4 如何发出一个Packet IN消息
这里,我们在Egress控制流的apply中,加入一段代码:
if (standard_metadata.egress_port == CPU_PORT) {
hdr.cpu_in.setValid();
hdr.cpu_in.ingress_port = standard_metadata.ingress_port;
exit;
}
如果要从CPU_PORT发出去,说明这是一个packetin数据包,把它的进端口封包到它的cpu_in中,这样,等它到控制面的时候,控制面就知道了这个包是来自于交换机的这个端口的,这里设计了一下cpu_in是有效的。
1.5 封装Packetin数据包
如果cpu_in是valid的,它就会封包
apply {
packet.emit(hdr.cpu_in);
packet.emit(hdr.ethernet);
packet.emit(hdr.ipv4);
packet.emit(hdr.ipv6);
packet.emit(hdr.srv6h);
packet.emit(hdr.srv6_list);
packet.emit(hdr.tcp);
packet.emit(hdr.udp);
packet.emit(hdr.icmp);
packet.emit(hdr.icmpv6);
packet.emit(hdr.ndp);
}
总的来说,来讲讲控制面和数据面交互是怎么做的:
- 我们先只说LLDP和BDDP:控制器会发送一个PACKET_OUT的数据包到数据面,这个PACKET_OUT数据包指定了它的出端口port A
- 当交换机leaf1收到这个数据包,把它拆开来一看,发现它的出端口port A要去leaf2,于是它拆了包,然后不做任何操作了,直接发出去到leaf2
- leaf2从port B收到了这个包,此时这个包的格式已经不是PACKET_OUT了,它是一个LLDP协议包或者BDDP协议包,此时leaf2去它的ACL表里检索,发现匹配到了这个包,然后它执行了对应的操作:把它克隆一份
- 其中,原件按照它原有的逻辑继续走
- 复印件到管道的元数据的ingress和原件一样,其他不存在,然后复印件把自己的ingress(port B)信息封装在PACKET_IN包头中,并激活这个数据包的PACKET_IN包头,然后发给控制器
- 控制器收到了一个PACKET_IN的包,它打开这个包一看得知这个包在数据面的ingress是(port B),然后进一步拆开,发现它是一个LLDP或者BDDP数据包,然后控制面回忆起自己曾经向leaf1发过这个包叫它去port A的,这个包路线是控制器->eaf1 port A->leaf2 port B->控制器,控制器就得到了两个交换机相连的端口
- 但是我们知道,ACL中并不只关注了LLDP和BDDP,它还关注了ARP以及NDP的NS和NA,也就是说,控制器还能跟踪到IPv4和IPv6的主机的信息
2.测试上述功能
在这里,我们用一个代码测试了它的功能:
PacketOutTest
- runTest函数:生成了一大堆的数据包,然后去testPacket这些数据包的IO的功能
- testPacket(self, pkt)函数:把packetout消息的cpu_out的出端口分别发出去
PacketInTest
- runTest函数:生成了一大堆的数据包,然后去testPacket这些数据包的IO的功能
-
testPacket(self, pkt)函数:insert这个数据包对应的表的表项,然后,看每一个端口,它有一个期望的数据包格式类型,testutils发包工具发出数据包,如果符合期望,就ok了
from ptf.testutils import group
from base_test import *
CPU_CLONE_SESSION_ID = 99
@group("packetio")
class PacketOutTest(P4RuntimeTest):
"""Tests controller packet-out capability by sending PacketOut messages and
expecting a corresponding packet on the output port set in the PacketOut
metadata.
"""
def runTest(self):
for pkt_type in ["tcp", "udp", "icmp", "arp", "tcpv6", "udpv6",
"icmpv6"]:
print_inline("%s ... " % pkt_type)
pkt = getattr(testutils, "simple_%s_packet" % pkt_type)()
self.testPacket(pkt)
def testPacket(self, pkt):
for outport in [self.port1, self.port2]:
# Build PacketOut message.
# Modify metadata names to match the content of your P4Info file
# ---- START SOLUTION ----
packet_out_msg = self.helper.build_packet_out(
payload=str(pkt),
metadata={
"egress_port": outport,
"_pad": 0
})
# Send message and expect packet on the given data plane port.
self.send_packet_out(packet_out_msg)
testutils.verify_packet(self, pkt, outport)
# Make sure packet was forwarded only on the specified ports
testutils.verify_no_other_packets(self)
@group("packetio")
class PacketInTest(P4RuntimeTest):
"""Tests controller packet-in capability my matching on the packet EtherType
and cloning to the CPU port.
"""
def runTest(self):
for pkt_type in ["tcp", "udp", "icmp", "arp", "tcpv6", "udpv6",
"icmpv6"]:
print_inline("%s ... " % pkt_type)
pkt = getattr(testutils, "simple_%s_packet" % pkt_type)()
self.testPacket(pkt)
@autocleanup
def testPacket(self, pkt):
# Insert clone to CPU session
self.insert_pre_clone_session(
session_id=CPU_CLONE_SESSION_ID,
ports=[self.cpu_port])
# Insert ACL entry to match on the given eth_type and clone to CPU.
eth_type = pkt[Ether].type
# Modify names to match content of P4Info file (look for the fully
# qualified name of the ACL table, EtherType match field, and
# clone_to_cpu action.
self.insert(self.helper.build_table_entry(
table_name="IngressPipeImpl.acl_table",
match_fields={
# Ternary match.
"hdr.ethernet.ether_type": (eth_type, 0xffff)
},
action_name="IngressPipeImpl.clone_to_cpu",
priority=DEFAULT_PRIORITY
))
for inport in [self.port1, self.port2, self.port3]:
# Expected P4Runtime PacketIn message.
exp_packet_in_msg = self.helper.build_packet_in(
payload=str(pkt),
metadata={
"ingress_port": inport,
"_pad": 0
})
# Send packet to given switch ingress port and expect P4Runtime
# PacketIn message.
testutils.send_packet(self, inport, str(pkt))
self.verify_packet_in(exp_packet_in_msg)
看完了代码,开始测试:
make p4-test TEST=packetio
3.开启ONOS完成链路发现
链路发现和主机发现的功能已经实现在ONOS中了,在控制器中的应用程序的解释器PipelineInterpreter中让他能匹配P4Info,这样,就能完成对数据包IO的真正实现了,记得make netcfg把网络配置推送到ONOS中,你发现了两个主机。
make app-reload,加载一下应用程序,现在,你能实现h1a和h1b互ping的功能了
在这里,我们先做一个小结:
你会发现一个现象,h1a和h1b能互相ping,但是h2就不行了,这是为什么呢?
h1a在一开始,它要找主机h1b,它发出了对h1b的组播,ipv6组播的mac地址也是个组播,是0x333300000..所以,当组播请求经过leaf1的时候,leaf1根据自己身上的2层组播地址转发,转发的出端口是主机向的端口,也就是3456,所以,理论上h1c,h1b,h2都会收到它的组播,但是实际上
- 同一个广播域内的h1c,它不知道h1a的地址,h1a虽然要组播他要去的地方,但是h1c收到了发现h1a的3层的目的地址不是它,h1c就不再打开看了,在同一广播域内,leaf1呈现l2功能,是一个交换机
- 同一个leaf的h2,虽然与h1a和h1b,h1c都是与leaf1相连,但是,h1a为了要找h2,它知道h2不是这个子网的,在这个子网内别说组播了,广播都没用,所以h1a要找到与外网相连的leaf1,它要知道路由器怎么走(router的mac),才能知道h2的MEC,然而我们没有实现第三层,所以它连路由器在哪都不知道,leaf1在h1a的眼中,只不过是一个交换机而已。
- 不同的leaf的h3和h4,他们根本就收不到信息,因为我们还没有实现3层转发功能