P4 Compiler for FPGA とN3000カードでNAT機能を実現

sukechです。
昨年はP4言語で簡単なフィルタをFPGAに実装しましたが、今回はNATの実装例を紹介します。

##検証環境
検証環境は昨年とほぼ同じですが、P4コンパイラの推奨に合わせてCentOS 7.9を使用しています。

・FPGA Smart NIC
インテル社製 Intel® FPGA PAC N3000

・Acceleration Stackバージョン
1.3.1
ダウンロードリンク：https://www.intel.com/content/www/us/en/programmable/products/boards_and_kits/dev-kits/altera/intel-fpga-pac-n3000/getting-started.html

・サーバー
HPE社製 DL380

・OS
CentOS Linux version 7.9 kernel 3.10

##インストール
・Acceleration Stackのインストール方法はインテルのドキュメントを参照してください。コンパイルまで行うためRuntimeでなくDevelopmenntoをインストールします。
ドキュメント：https://www.intel.com/content/dam/www/programmable/us/en/pdfs/literature/ug/ug-ias-n3000.pdf


・P4コンパイラはインテル社と契約すると使用できます。ここではセットアップ手順は記載しませんが、数分で簡単にインストールできました。P4コンパイラを使ってみたい方はぜひインテルさんや代理店に聞いてみてください。
また昨年は対応していなかったP4_16も使えるようになったので、P4_16でコードを作成しました。


##今回使ったP4コード
今回はP4コードは2つのファイルから構成されてます。それぞれについて簡単に解説しておきます。

・headers.p4
ヘッダの定義ファイルです。ヘッダ解析時にはこちらで定義しているヘッダ長や各要素が適用されます。ここでオリジナルのヘッダを記載してオリジナルのプロトコルが作れたりします。

```c:headers.p4
const bit<16> ETHERTYPE_IPV4 = 0x0800;
const bit<16> ETHERTYPE_VLAN = 0x8100;
const bit<16> ETHERTYPE_IPV6 = 0x86dd;

const bit<8> IPPROTO_TCP = 0x06;
const bit<8> IPPROTO_UDP = 0x11;

header ethernet_h {
    bit<48> dst_addr;
    bit<48> src_addr;
    bit<16> ether_type;
}

header vlan_tag_h {
    bit<3>  pcp;
    bit<1>  cfi;
    bit<12> vid;
    bit<16> ether_type;
}

header ipv4_h {
    bit<4>  version;
    bit<4>  ihl;
    bit<8>  diffserv;
    bit<16> total_len;
    bit<16> identification;
    bit<3>  flags;
    bit<13> frag_offset;
    bit<8>  ttl;
    bit<8>  protocol;
    bit<16> hdr_checksum;
    bit<32> src_addr;
    bit<32> dst_addr;
}

header ipv6_h {
    bit<4>   version;
    bit<8>   traffic_class;
    bit<20>  flow_label;
    bit<16>  payload_len;
    bit<8>   next_hdr;
    bit<8>   hop_limit;
    bit<128> src_addr;
    bit<128> dst_addr;
}

header tcp_h {
    bit<16> src_port;
    bit<16> dst_port;
    bit<32> seq_no;
    bit<32> ack_no;
    bit<4>  data_offset;
    bit<4>  res;
    bit<8>  flags;
    bit<16> window;
    bit<16> checksum;
    bit<16> urgent_ptr;
}

header udp_h {
    bit<16> src_port;
    bit<16> dst_port;
    bit<16> length;
    bit<16> checksum;
}

```

・nat_top.p4
メインとなるP4コードです。ここでparser=>ingress processing=>deparserの処理を記載していきます。
処理のメインとなるテーブルはingress processing内に記載していきます。

```c:top.p4
#include <core.p4>
#include <v1model.p4>
#include <intel_model.p4>

#include "headers.p4"


struct headers {
    ethernet_h         ethernet;
    ipv4_h             ipv4;
    ipv6_h             ipv6;
    tcp_h              tcp;
    udp_h              udp;
}

struct csum_metadata_t {
    bit<16>                tcp_length;
    bit<8>                 pad_8b;
}

struct metadata {
    intrinsic_metadata_t   md_intr;
	csum_metadata_t        md_csum;
}

/**
 * ~~~~~~~~~~~~~~~~~~~
 * Parser
 * ~~~~~~~~~~~~~~~~~~~
 */

parser nat_parser(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.ether_type) {
            ETHERTYPE_IPV4:     parse_ipv4;
            ETHERTYPE_IPV6:     parse_ipv6;
            default:            accept;
        }
    }

    state parse_ipv4 {
        packet.extract(hdr.ipv4);
        transition select(hdr.ipv4.protocol) {
            IPPROTO_TCP: parse_tcp;
            IPPROTO_UDP: parse_udp;
            default:      accept;
        }
    }

    state parse_ipv6 {
        packet.extract(hdr.ipv6);
        transition select(hdr.ipv6.next_hdr) {
            IPPROTO_TCP: parse_tcp;
            IPPROTO_UDP: parse_udp;
            default:      accept;
        }
    }

    state parse_tcp {
        packet.extract(hdr.tcp);
		transition accept;
    }

    state parse_udp {
        packet.extract(hdr.udp);
		transition accept;
    }
}

/**
 * ~~~~~~~~~~~~~~~~~~~
 * Deparser
 * ~~~~~~~~~~~~~~~~~~~
 */

control nat_deparser(packet_out packet, in headers hdr)
{
    apply {
        packet.emit(hdr.ethernet);
        packet.emit(hdr.ipv6);
        packet.emit(hdr.ipv4);
        packet.emit(hdr.udp);
        packet.emit(hdr.tcp);
    }
}

/**
 * ~~~~~~~~~~~~~~~~~~~
 * Verify checksum
 * ~~~~~~~~~~~~~~~~~~~
 */

control nat_verifyChecksum(inout headers hdr, inout metadata meta)
{
    apply { }
}

/**
 * ~~~~~~~~~~~~~~~~~~~
 * Ingress
 * ~~~~~~~~~~~~~~~~~~~
 */

control nat_ingress(inout headers hdr, inout metadata meta, inout standard_metadata_t standard_metadata)
{
    // Actions
    action drop() {
        mark_to_drop(standard_metadata);
    }

    action permit() {
        NoAction();
    }

    action eport_ctrl(bit<8> offset) { // offset value [ 0 or 128 ]
        meta.md_intr.egress_port = meta.md_intr.ingress_port + offset;
    }
	
	action pkt_cp_ctrl(bit<8> dup_en_mask, bit<8> dup_dma0, bit<8> dup_dma1, bit<4> dup_eth0, bit<4> dup_eth1){
		meta.md_intr.duplicate_en_mask = dup_en_mask;
		meta.md_intr.duplicate_dma0 = dup_dma0;
		meta.md_intr.duplicate_dma1 = dup_dma1;
		meta.md_intr.duplicate_eth0 = dup_eth0;
		meta.md_intr.duplicate_eth1 = dup_eth1;
	}
	
	action srcnat_tcp(bit<32> ipaddr, bit<16> port) {
        hdr.ipv4.src_addr = ipaddr;
        hdr.tcp.src_port  = port;		
    }

    action dstnat_tcp(bit<32> ipaddr, bit<16> port) {
        hdr.ipv4.dst_addr = ipaddr;
        hdr.tcp.dst_port  = port;
    }

    action srcnat_udp(bit<32> ipaddr, bit<16> port) {
        hdr.ipv4.src_addr = ipaddr;
        hdr.udp.src_port  = port;
    }

    action dstnat_udp(bit<32> ipaddr, bit<16> port) {
        hdr.ipv4.dst_addr = ipaddr;
        hdr.udp.dst_port  = port;
    }

	// Table set egress port for atom
	table tbl_ePort {
		actions = {
			eport_ctrl;
			pkt_cp_ctrl;
		}
		size = 16;
	}
	
    table tbl_nat_tcp {
        key = {
            hdr.ipv4.src_addr : exact;
            hdr.ipv4.dst_addr : exact;
            hdr.tcp.src_port  : exact;
            hdr.tcp.dst_port  : exact;
        }

        actions = {
            srcnat_tcp;
            dstnat_tcp;
			permit;
			drop;
        }

        size = 512;
    }
	
	table tbl_nat_udp {
        key = {
            hdr.ipv4.src_addr : exact;
            hdr.ipv4.dst_addr : exact;
            hdr.udp.src_port  : exact;
            hdr.udp.dst_port  : exact;
        }

        actions = {
            srcnat_udp;
            dstnat_udp;
			permit;
			drop;
        }

        size = 512;
    }

    apply {
		tbl_ePort.apply();
        if (hdr.ipv4.isValid()) {
            if (hdr.tcp.isValid()) {
			    tbl_nat_tcp.apply();
			}
			if (hdr.udp.isValid()) {
			    tbl_nat_udp.apply();
			}
        }
    }
}

/**
 * ~~~~~~~~~~~~~~~~~~~
 * Egress
 * ~~~~~~~~~~~~~~~~~~~
 */

control nat_egress(inout headers hdr, inout metadata meta, inout standard_metadata_t standard_metadata)
{
    apply { }
}

/**
 * ~~~~~~~~~~~~~~~~~~~
 * Compute checksum
 * ~~~~~~~~~~~~~~~~~~~
 */

control nat_computeChecksum(inout headers hdr, inout metadata meta)
{
    apply { }
}

/**
 * ~~~~~~~~~~~~~~~~~~~
 * Switch
 * ~~~~~~~~~~~~~~~~~~~
 */
V1Switch(
    nat_parser(),
    nat_verifyChecksum(),
    nat_ingress(),
    nat_egress(),
    nat_computeChecksum(),
    nat_deparser()
) main;

```

##ビットストリーム生成
ビットストリームの生成はコマンド1つで生成できます。
ちなみにP4コンパイラはN3000の10G版と25G版の両方に対応してして最後にあるオプションの--ethを使って指定できます。

```
# ./bsp-fw.sh --gen-p4 ./nat_top.p4 --use-flr --use-ext-stat --rtl ~/inteldevstack/rtl/n3000_1_3_v1.5.7/ --gen-dir gen_out --gen-qip --gen-fw --compiler p4c --eth 8x10G
```

ビットストリーム完成後、以下のコマンドで書き込みを実施します。
※2番目のコマンドの"15:00.0"はPCIeのバス番号なので使用する環境で異なるので変更してください。バス番号はlspciコマンド等で確認できます。

```
# python3.6 /usr/local/bin/PACSign SR -t UPDATE -H openssl_manager -i pac-n3000-secure-update-raw.bin -o unsigned.bin -y
# sudo fpgasupdate unsigned.bin 15:00.0
```


##動作確認
続いて動作検証です。
今回は以下の図のようなイメージのNAT機能をP4を使用してFPGA Smart NICに実装しました。
入ってきたパケットのIPアドレス、ポート番号の条件に応じて、それぞれを書き換えるマッチ&アクションテーブルをP4で記述しています。
テーブルルールを設定することで変更されるIPアドレスやポート番号を指定します。

![nat_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/721696/ee16932b-9c50-e0db-8d60-10e4dbd6987e.png)



まずはP4ブロックの起動です。

```
# np4atomtool daemon --start --device /dev/intel-fpga-fme.0
```
無事にいけば以下のように返ってきます。

```
Starting...
Load plugin path : /usr/lib/np4-atom/plugins/libplugin_opae.so
Device name : Processing accelerators: Intel Corporation Device Based on OPAE Driver
BDF daemon start
```

続いてパケットのフィルタリングルールを設定します。
コマンドやルールファイルのロードで設定ができます。
今回はコマンドで設定を行いました。
以下がコマンド例です。

```
# nat_ingress.tbl_nat_tcp keys ( ipv4.dst_addr 1.1.1.1 ipv4.src_addr 2.2.2.2 tcp.dst_port 100 tcp.src_port 200 ) action nat_ingress.dstnat_tcp params ( ipaddr 10.10.10.10 port 1000)
```
このようにルールを設定した場合、ソースアドレスが1.1.1.1でデスティネーションアドレスが2.2.2.2、TCPのソースポートが100、デスティネーションポートが200の条件に一致するパケットが入力されると、デスティネーションアドレスとポートがそれぞれ10.10.10.10:1000と書き換えられるようになります。

今回の検証では以下の設定を入れて評価を実施しました。

```
# nat_ingress.tbl_nat_tcp keys ( ipv4.src_addr 10.10.10.10 ipv4.dst_addr 20.20.20.20 tcp.src_port 1000 tcp.dst_port 2000 ) action nat_ingress.dstnat_tcp params ( ipaddr 2.2.2.2 port 2222 )
# nat_ingress.tbl_nat_udp keys ( ipv4.src_addr 10.10.10.10 ipv4.dst_addr 20.20.20.20 udp.src_port 1000 udp.dst_port 2000 ) action nat_ingress.drop
# nat_ingress.tbl_nat_tcp keys ( ipv4.src_addr 30.30.30.30 ipv4.dst_addr 40.40.40.40 tcp.src_port 3000 tcp.dst_port 4000 ) action nat_ingress.drop
# nat_ingress.tbl_nat_udp keys ( ipv4.src_addr 30.30.30.30 ipv4.dst_addr 40.40.40.40 udp.src_port 3000 udp.dst_port 4000 ) action nat_ingress.srcnat_udp params ( ipaddr 3.3.3.3 port 3333 )
```


実際にパケット転送した結果が以下となっています。
上の図が入力パケットをキャプチャしたもの、下の図が出力パケットをキャプチャしたものとなっています。
少しわかりずらいかもしれませんが、設定されたルール通りにパケットが変換/ドロップされているのがわかります。

![input.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/721696/be74cfdb-e7b0-df0c-4901-d81af6ca1681.png)
入力パケット


![output.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/721696/5d80d7d2-3de0-6662-5e39-3f53d3a4c078.png)
出力パケット

##まとめ
P4を使ってFPGAにNAT機能を実装することができました。
やはり短期間で実装できてしまうのが非常にいいところですね。
今回ですと2日ほどあれば検証まで出来ました。
昨年のP4コンパイラのバージョンでは対応していなかったP4_16がサポートされたので興味を持つ方も多いかと思います。