在p4的tutorials-master\exercises\firewall中设计了一个防火墙实验,通过将p4代码编译成json部署到交换机中以实现防火墙的功能,网络的拓扑如下
在readme中可以看到,我们需要在s1上部署防火墙功能,以使得h1 h2能够在内网建立连接,h1 h2也能够从内网与h3 h4建立连接,但是h3 h4并不能与h1 h2建立连接。在本次实验中,控制平面的相关代码已经写好了,所以只需对数据平面实现相关的功能即可。
在实现防火墙.p4前,我们先做一个小测试,make run进入mininet的命令行之后,对内网,内->外,外->内进行了iperf测试,可以看到都是可以建立连接的。
| 类型 | 测试结果 |
| 内网->内网 | succeed |
| 内网->外网 | succeed |
| 外网->内网 | succeed |

在这里readme也给了我们很贴心的一些提示
对于第一条,要求我们定义好以太网、IP、TCP的头部,其实在p4文件中已经帮我们定义好了。
- /*************************************************************************
- *********************** H E A D E R S ***********************************
- *************************************************************************/
-
- 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;
- }
-
- 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;
- }
-
- header tcp_t{
- bit<16> srcPort;
- bit<16> dstPort;
- bit<32> seqNo;
- bit<32> ackNo;
- bit<4> dataOffset;
- bit<4> res;
- bit<1> cwr;
- bit<1> ece;
- bit<1> urg;
- bit<1> ack;
- bit<1> psh;
- bit<1> rst;
- bit<1> syn;
- bit<1> fin;
- bit<16> window;
- bit<16> checksum;
- bit<16> urgentPtr;
- }
-
- struct metadata {
- /* empty */
- }
-
- struct headers {
- ethernet_t ethernet;
- ipv4_t ipv4;
- tcp_t tcp;
- }
用解析器把上诉的几个包头解析出来,这也帮我们实现好了
- /*************************************************************************
- *********************** 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;
- default: accept;
- }
- }
-
- state parse_ipv4 {
- packet.extract(hdr.ipv4);
- transition select(hdr.ipv4.protocol){
- TYPE_TCP: tcp;
- default: accept;
- }
- }
-
- state tcp {
- packet.extract(hdr.tcp);
- transition accept;
- }
- }
丢弃数据包的操作,也帮我们实现好了
- action drop() {
- mark_to_drop(standard_metadata);
- }
哈希的操作,计算两个不同的哈希算法得到的哈希值,哈希算法所依赖的参数是5个元素,分别是ipv4的源地址,目的地址,两个端口,ipv4的协议号,这里的hash算法是来自于v1model.p4中的,计算完哈希值以后,把相应的值填到布隆过滤器对应的桶中,哈希的值在调用了hash函数后,被保存在了MyIngress的
bit<32> reg_pos_one; bit<32> reg_pos_two;
bit<1> reg_val_one; bit<1> reg_val_two;中
action compute_hashes(ip4Addr_t ipAddr1, ip4Addr_t ipAddr2, bit<16> port1, bit<16> port2){ //Get register position hash(reg_pos_one, HashAlgorithm.crc16, (bit<32>)0, {ipAddr1, ipAddr2, port1, port2, hdr.ipv4.protocol}, (bit<32>)BLOOM_FILTER_ENTRIES); hash(reg_pos_two, HashAlgorithm.crc32, (bit<32>)0, {ipAddr1, ipAddr2, port1, port2, hdr.ipv4.protocol}, (bit<32>)BLOOM_FILTER_ENTRIES); }
这个哈希方法在源码中可以看到,它的四个参数分别是结果,算法类型,数据,最大值
/*** * Calculate a hash function of the value specified by the data * parameter. The value written to the out parameter named result * will always be in the range [base, base+max-1] inclusive, if max >= * 1. If max=0, the value written to result will always be base. * * Note that the types of all of the parameters may be the same as, or * different from, each other, and thus their bit widths are allowed * to be different. * * @param O Must be a type bit * @param D Must be a tuple type where all the fields are bit-fields (type bitor int ) or varbits. * @param T Must be a type bit * @param M Must be a type bit */ @pure extern void hash(out O result, in HashAlgorithm algo, in T base, in D data, in M max);
这个操作其实在basic.p4中就有实现了,如果做过第一个实验应该不陌生
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(); }
一个简单的设置方向的操作
action set_direction(bit<1> dir) { direction = dir; }
有一张检查端口的表,如果发现一个数据包要进内网,就把方向设1,否则设0,表中的端口的相关数据信息已经在控制面中完成了
table check_ports { key = { standard_metadata.ingress_port: exact; standard_metadata.egress_spec: exact; } actions = { set_direction; NoAction; } size = 1024; default_action = NoAction(); }
1和2是检查ipv4的包头和tcp包头是否合法,这个不过多阐述,要实现防火墙的单向tcp建立连接,就要用到布隆过滤器了,具体的工作思想如下,来自于内网的主机想要与外网建立连接,同一连接上,这个外网肯定也要能够访问内网。因此,对于s1交换机来说,就要对外网来的连接进行审核,也就是用布隆过滤器审核,如果这个外网来的包所在的连接在布隆过滤器中,说明这个连接是从内网建立出来的,允许通行,否则禁止。
注意,一个连接在布隆过滤器中保存的时候,依赖的参数是建立连接时从内网发出去的包的包头提取出来的信息,所以外部包所在的连接是否在布隆过滤器中,就要把它包头的信息反着读,也就是把目的地址和源地址反过来,才能得到所在连接的审核结果。
这里是重点,建立连接的时候,由于下图所示,其实TCP连接一开始会生成一个SYN包,在S1中,如果有一个来自于内网的SYN包到来,就说明有一个新的连接需要建立,这个时候,s1就把这个SYN包抓出来,然后一顿操作,填在布隆过滤器中。
- 如果是内网的SYN包,存一下连接,通行
- 如果是内网的普通包,直接通行
- 如果是外网包,如果所在连接是内网发起的,通行
- 如果是普通外网包,丢弃,大概示意图如下
apply { if (hdr.ipv4.isValid()){ ipv4_lpm.apply(); if (hdr.tcp.isValid()){ direction = 0; // default if (check_ports.apply().hit) { // test and set the bloom filter if (direction == 0) { compute_hashes(hdr.ipv4.srcAddr, hdr.ipv4.dstAddr, hdr.tcp.srcPort, hdr.tcp.dstPort); } else { compute_hashes(hdr.ipv4.dstAddr, hdr.ipv4.srcAddr, hdr.tcp.dstPort, hdr.tcp.srcPort); } // Packet comes from internal network if (direction == 0){ // TODO: this packet is part of an outgoing TCP connection. // We need to set the bloom filter if this is a SYN packet // E.g. bloom_filter_1.write(, ); if (hdr.tcp.syn == 1){ bloom_filter_1.write(reg_pos_one, 1); bloom_filter_2.write(reg_pos_two, 1); } } // Packet comes from outside else if (direction == 1){ // TODO: this packet is part of an incomming TCP connection. // We need to check if this packet is allowed to pass by reading the bloom filter // E.g. bloom_filter_1.read(, ); bloom_filter_1.read(reg_val_one,reg_pos_one); bloom_filter_2.read(reg_val_two,reg_pos_two); if(reg_val_one !=1 || reg_val_two !=1) drop(); } } } } }
封包操作
control MyDeparser(packet_out packet, in headers hdr) { apply { packet.emit(hdr.ethernet); packet.emit(hdr.ipv4); packet.emit(hdr.tcp); } }
这里只是描述了一个包的流程是什么样的,这里不多说了。
首先,我先试试内网->内网和内网->外网,是ok的

仔细看我打开的两个终端,我分别让h1和h3都作为服务器试一下,可以看到,h1作服务器是不行的。
| 类型 | 测试结果 |
| 内网->内网 | succeed |
| 内网->外网 | succeed |
| 外网->内网 | fail |

使用wireshark抓包(抓s1的eth1接口)可以发现,如果是10.0.1.1也就是h1是连接的发起者(SYN的source是10.0.1.1)就可以进行握手并通信
但是如果是反过来,外网访问内网时,就什么都没有
在靠近内网这一侧的端口上看不到包,因为外网的SYN包在eth3或者eth4时被丢弃了,这里查看了eth3口,可以看到,SYN包试图发了多次并没有得到响应
通过这一次的实验,了解到了v1model提供的hash函数的用法,以及如何使用寄存器模拟布隆过滤器