实验要求
- 在本次实验中,目的是编写一个P4程序,使主机能够监控网络中所有链路的使用情况
- 本练习基于基本的IPv4转发练习,因此请确保在尝试此练习之前完成此练习(basic.p4)
- 具体来说,我们将修改基本P4程序以处理源路由探测包,以便它能够在每个跳处获取出口链路利用率,并将其传递给主机以进行监控。
实验内容
Step1:设计包头
1.为了获取包经过了多少跳,定义了一个包头 probe_t
header probe_t { bit<8> hop_cnt; }
2.在每一跳的过程中,收集我们想要的信息,比如交换机的端口号,进出时间等
header probe_data_t { bit<1> bos; bit<7> swid; bit<8> port; bit<32> byte_cnt; time_t last_time; time_t cur_time; }
3.指定交换机应该将这个探测包发往哪里,保证了探测包的确定性转发路径
header probe_fwd_t { bit<8> egress_spec; }
包头的总体结构如下,但是实际的包头结构要么是ipv4包要么是探测包
struct headers { ethernet_t ethernet; ipv4_t ipv4; probe_t probe; probe_data_t[MAX_HOPS] probe_data; probe_fwd_t[MAX_HOPS] probe_fwd; }
Step2: 设计两个寄存器
`byte_cnt_reg` :统计自最后一个探测数据包从端口发送出去以来从每个端口发送出去的字节数。
`last_time_reg`:存储上一个探测包从端口出去的最后时间
Step3: 对照实验
先把没有完善的p4代码编译并载入交换机创建mininet运行一下,我打开同一个主机的两个终端,一个发送数据包,一个接收数据包,至于数据包的发送原理,就是简单的python网络编程提及的东西,我在以后放在java网络编程一块讲。
可以看到,接收端收到的域都是0,并没有提现任何信息,这是因为我的监测模块还没实现
Step4: 控制面逻辑
控制面逻辑的json主要包含4个元素,分别是p4目标,也就是底层实现,以及p4info,这是p4文件编译后生成的信息文件,还有bmv2_json指的是p4文件编译后的json,最后一个是table_entires,描述了table里面每一个键值对,注意到,和以往不同的是,这里有Egress控制流的表了(swid)
{ "target": "bmv2", "p4info": "build/link_monitor.p4.p4info.txt", "bmv2_json": "build/link_monitor.json", "table_entries": [ { "table": "MyEgress.swid", "default_action": true, "action_name": "MyEgress.set_swid", "action_params": { "swid": 2 } }, { "table": "MyIngress.ipv4_lpm", "default_action": true, "action_name": "MyIngress.drop", "action_params": { } }, { "table": "MyIngress.ipv4_lpm", "match": { "hdr.ipv4.dstAddr": ["10.0.1.1", 32] }, "action_name": "MyIngress.ipv4_forward", "action_params": { "dstAddr": "08:00:00:00:03:00", "port": 4 } }, { "table": "MyIngress.ipv4_lpm", "match": { "hdr.ipv4.dstAddr": ["10.0.2.2", 32] }, "action_name": "MyIngress.ipv4_forward", "action_params": { "dstAddr": "08:00:00:00:04:00", "port": 3 } }, { "table": "MyIngress.ipv4_lpm", "match": { "hdr.ipv4.dstAddr": ["10.0.3.3", 32] }, "action_name": "MyIngress.ipv4_forward", "action_params": { "dstAddr": "08:00:00:00:03:33", "port": 1 } }, { "table": "MyIngress.ipv4_lpm", "match": { "hdr.ipv4.dstAddr": ["10.0.4.4", 32] }, "action_name": "MyIngress.ipv4_forward", "action_params": { "dstAddr": "08:00:00:00:04:44", "port": 2 } } ] }
Step5:数据平面逻辑设计(解析器)
解析器设计,我们先要解析这些包,在设计解析器逻辑的时候,可以看出:
- 我们的包要么是普通的ipv4包,要么是探测包
- 探测包经历的跳数被赋值给了 meta.parser_metadata.remaining也就是元数据中的解析器剩余要解析的探测信息
- 如果已经剥开到第0条的数据了,那就解析它的转发包头,这里注意,next指针指向当前的包的转发包头的数组的当前位置,每次调用完都会指向下一个,last指的是上一次提取出来的包头信息,指的就是上一次next指示的东西。如果解析到remaining=0就accept
- 如果剥开还不是第0条,就去循环剥开它的探测包的data,知道bos=1也就是剥到了最后一个probe_data的包头了,就去剥fwd包头,这个时候就不用去设置什么bos了,因为fwd每次循环剥开就倒扣remaining就可以了,(那为啥不在data包头就扣掉呢?因为这个remaining要在fwd用,先用了话就是0了,根本不会解析fwd包头,那如果再设置一个remaing来剥开这个data包头不就好了。。。这样还可以节省一个bos位😂)
/************************************************************************* *********************** 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 { transition parse_ethernet; } state parse_ethernet { packet.extract(hdr.ethernet); transition select(hdr.ethernet.etherType) { TYPE_IPV4: parse_ipv4; TYPE_PROBE: parse_probe; default: accept; } } state parse_ipv4 { packet.extract(hdr.ipv4); transition accept; } state parse_probe { packet.extract(hdr.probe); meta.parser_metadata.remaining = hdr.probe.hop_cnt + 1; transition select(hdr.probe.hop_cnt) { 0: parse_probe_fwd; default: parse_probe_data; } } state parse_probe_data { packet.extract(hdr.probe_data.next); transition select(hdr.probe_data.last.bos) { 1: parse_probe_fwd; default: parse_probe_data; } } state parse_probe_fwd { packet.extract(hdr.probe_fwd.next); meta.parser_metadata.remaining = meta.parser_metadata.remaining - 1; // extract the forwarding data meta.egress_spec = hdr.probe_fwd.last.egress_spec; transition select(meta.parser_metadata.remaining) { 0: accept; default: parse_probe_fwd; } } }
Step6:数据平面的Ingress控制流
Ingress控制流设计,在这个入口设计中可以看到,和basic.p4差不多,除了这个apply中,增加了probe包头的操作,定义一下要出去的端口是哪个,然后去给它加一跳
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; } table ipv4_lpm { key = { hdr.ipv4.dstAddr: lpm; } actions = { ipv4_forward; drop; NoAction; } size = 1024; default_action = drop(); } apply { if (hdr.ipv4.isValid()) { ipv4_lpm.apply(); } else if (hdr.probe.isValid()) { standard_metadata.egress_spec = (bit<9>)meta.egress_spec; hdr.probe.hop_cnt = hdr.probe.hop_cnt + 1; } }
Step7:数据平面的Egress控制流
在出口控制流这边,我们需要给包附上它的包头信息
- 在这里就用上了最早提到了两个寄存器,分别是字节计数器和时间寄存器
- 定义了一个设置交换机id的操作,给探测包的data字段中的swid设置值
- 在table swid中,并没有设置任何的键,这是因为对于每一个探测包我们都这么做
- 定义最后时间,设置当前时间,从standard_metadata.egress_global_timestamp就可以拿到当前时间
- 把寄存器里面的计数器的值,读取到byte_cnt中,然后对byte_cnt = byte_cnt + standard_metadata.packet_length;
- 如果来了个探测包,计数器刷新,但是计数器的值存在了byte_cnt总给未来赋上包头用
- 探测包进入这个交换机中,就头插一个data包头进去,如果经历跳数是1(跳数的计算早在解析器的时候就弄了),说明这个是第一跳,就把这个包头设为最后一个data包头
- 运用swid表,也就等于是指定了下一跳的方向
/************************************************************************* **************** 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) { // count the number of bytes seen since the last probe register<bit<32>>(MAX_PORTS) byte_cnt_reg; // remember the time of the last probe register<time_t>(MAX_PORTS) last_time_reg; action set_swid(bit<7> swid) { hdr.probe_data[0].swid = swid; } table swid { actions = { set_swid; NoAction; } default_action = NoAction(); } apply { bit<32> byte_cnt; bit<32> new_byte_cnt; time_t last_time; time_t cur_time = standard_metadata.egress_global_timestamp; // increment byte cnt for this packet's port byte_cnt_reg.read(byte_cnt, (bit<32>)standard_metadata.egress_port); byte_cnt = byte_cnt + standard_metadata.packet_length; // reset the byte count when a probe packet passes through new_byte_cnt = (hdr.probe.isValid()) ? 0 : byte_cnt; byte_cnt_reg.write((bit<32>)standard_metadata.egress_port, new_byte_cnt); if (hdr.probe.isValid()) { // fill out probe fields hdr.probe_data.push_front(1); hdr.probe_data[0].setValid(); if (hdr.probe.hop_cnt == 1) { hdr.probe_data[0].bos = 1; } else { hdr.probe_data[0].bos = 0; } // set switch ID field swid.apply(); // TODO: fill out the rest of the probe packet fields // hdr.probe_data[0].port = ... // hdr.probe_data[0].byte_cnt = ... // TODO: read / update the last_time_reg // last_time_reg.read(<val>, <index>); // last_time_reg.write(<index>, <val>); // hdr.probe_data[0].last_time = ... // hdr.probe_data[0].cur_time = ... } } }
注:在源码中提到寄存器的read函数会把存储在指定索引中的寄存器数组的状态,并将其作为写入结果参数的值返回,索引的值就是standard_metadata.egress_port
register(bit<32> size); // FIXME -- arg should be `int` but that breaks typechecking /*** * read() reads the state of the register array stored at the * specified index, and returns it as the value written to the * result parameter. * * @param index The index of the register array element to be * read, normally a value in the range [0, size-1]. * @param result Only types T that are bit<W> are currently * supported. When index is in range, the value of * result becomes the value read from the register * array element. When index >= size, the final * value of result is not specified, and should be * ignored by the caller. */
Step8:实验
实验结果显示,我们的包头有新的信息了!