sukechです。
今回はP4という言語でFPGAを動かした時の話をします。
##そもそもP4って?
P4というのはネットワークのデータプレーンをプログラムするための言語となっています。データプレーンをプログラムできるので、オリジナルの新しいプロトコルを実装したり、変わったプロトコルスタックを構成したりが自由に行えます。
ユースケースとしてSRv6(Segment Routing v6)やINT(In-band Network Telemetry)といったネットワークの最新の機能であったり、Load BalancerやNAT(Network Address Transfer)のような従来からある機能の実装まで非常に幅広く使えます。
また、P4には2つのバージョンがあるようでP4_16とP4_14というのがあってP4_16の方が最新版みたいです。
P4についてさらに詳細を知りたい方はp4.orgを確認してください。
https://p4.org/
今回はP4を使って簡易的なパケットフィルタをFPGAに実装してみました。
##検証環境
・FPGA Smart NIC
インテル社製 Intel® FPGA PAC N3000
N3000には10Gと20Gのタイプがあり、今回は25Gタイプを使いました。
・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.6 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コード
今回私が記述したコードは4つのファイルから構成されてます。それぞれについて簡単に解説しておきます。
・top.p4
名前の通り最上位階層のP4コードです。ここでは記述したテーブルが適用される条件等を記載します。IPv4が来ているときはIPv4用のフィルタ、IPv6が来ているときはIPv6用のフィルタといった具合です。
#include "headers.p4"
#include "parser.p4"
#include "tables.p4"
// Just set identification string of the P4 core
@pragma core_identification simple_filter
control ingress {
if(valid(ipv4)){
// If the IPv4 is valid
apply(table_ipv4_filter);
}
if(valid(ipv6)){
// If the IPv6 is valid
apply(table_ipv6_filter);
}
apply(table_ePort);
}
・table.p4
次がテーブルのP4コードです。こちらにメインとなるパケットの処理が記載されるので重要な部分になります。
大きく分けるとテーブル定義とアクションの2つに分けることができます。
アクションは単純にパケットに対してどのような処理を行うのかを記載します。今回のコードだとパケットを通すか落とすかといった内容なのであまり面白くないですが、実際にはパケットの中身の指定したフィールドを変更することも可能です。
テーブル定義では読み込む変数の設定と所属するアクションを記載します。動作時に変数の値によってどのアクションを適用するかのルールを設定します。このコードだとパケットのSorceアドレスを見て通るか落とすかを決めることになります。
// Actions =====================================================================
action permit() {
no_op();
}
action drop_p() {
drop();
}
action eport_ctrl( offset ) { // offset value [ 0 or 128 ]
modify_field(intrinsic_metadata.egress_port,intrinsic_metadata.ingress_port);
add_to_field(intrinsic_metadata.egress_port,offset);
}
// Tables ======================================================================
table table_ipv4_filter {
reads {
ipv4.srcAddr : exact;
}
actions {
permit;
drop_p;
}
max_size: 128;
}
table table_ipv6_filter {
reads {
ipv6.srcAddr : exact;
}
actions {
permit;
drop_p;
}
max_size: 128;
}
// Table set egress port for atom
table table_ePort {
actions {
eport_ctrl;
}
}
・parser.p4
こちらは次のheaders.p4で定義しているヘッダの解析を行います。今回はシンプルな例なので少し眺めてもらえば理解できるかと思います。
// Protocol numbers ============================================================
#define ETH_PROT_IPV4 0x0800
#define ETH_PROT_IPV6 0x86dd
#define IP_PROT_TCP 0x06
#define IP_PROT_UDP 0x11
// Instances of headers ========================================================
header ethernet_t ethernet_0;
header ipv4_t ipv4;
header ipv6_t ipv6;
header tcp_t tcp;
header udp_t udp;
// Parse graph =================================================================
// Start
parser start {
return parse_ethernet;
}
// ethernet
parser parse_ethernet {
extract(ethernet_0);
return select(latest.etherType) {
ETH_PROT_IPV4 : parse_ipv4;
ETH_PROT_IPV6 : parse_ipv6;
default : ingress;
}
}
parser parse_ipv4 {
extract(ipv4);
return select(latest.protocol) {
IP_PROT_TCP : parse_tcp;
IP_PROT_UDP : parse_udp;
default : ingress;
}
}
parser parse_ipv6 {
extract(ipv6);
return select(latest.nextHead) {
IP_PROT_TCP : parse_tcp;
IP_PROT_UDP : parse_udp;
default : ingress;
}
}
parser parse_tcp {
extract(tcp);
return ingress;
}
parser parse_udp {
extract(udp);
return ingress;
}
・headers.p4
最後はヘッダの定義です。ヘッダ解析時にはこちらで定義しているヘッダ長や各要素が適用されます。ここでオリジナルのヘッダを記載してオリジナルのプロトコルが作れたりします。
header_type ethernet_t {
fields {
dstAddr : 48;
srcAddr : 48;
etherType : 16;
}
}
header_type ipv4_t {
fields {
version : 4;
ihl : 4;
diffserv : 8;
totalLen : 16;
identification : 16;
flags : 3;
fragOffset : 13;
ttl : 8;
protocol : 8;
hdrChecksum : 16;
srcAddr : 32;
dstAddr : 32;
}
}
header_type ipv6_t {
fields {
ver : 4;
trafClass : 8;
flowLab : 20;
payLen : 16;
nextHead : 8;
hopLim : 8;
srcAddr : 128;
dstAddr : 128;
}
}
header_type tcp_t {
fields {
srcPort : 16;
dstPort : 16;
seq : 32;
ack : 32;
dataOffset : 4;
res : 6;
flags : 6;
window : 16;
checksum : 16;
urgentPtr : 16;
}
}
header_type udp_t {
fields {
srcPort : 16;
dstPort : 16;
segLength : 16;
checksum : 16;
}
}
##ビットストリーム生成
ビットストリームの生成はコマンド1つで生成できます。
./n3000-fw.sh --gen-p4 top.p4 --pac-dir --gen-dir gen_out --gen-qip --gen-fw
少し時間はかかりますが、2時間ほどでビットストリームが生成されます。
ちなみに今回はP4で記述したコードをFPGAのビットストリームに直接変換してますが、P4のブロックだけをIPとして使うこともできるみたいです。ただしその場合はFPGAの開発ノウハウが必要となります。
私のようにFPGA開発やったことない人はP4から直接ビットストリーム生成で使うのがおすすめです。
次に生成されたビットストリームを書き込んでFPGAをP4で記述したフィルタとして使えるようにします。
※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
##動作確認
では動作確認に移ります。
以下のようなループバックの構成で検証しました。
OS上から見えているネットワークインタフェースenp20s0f0からパケットを転送するとN3000のP4ブロックを通ってQSFPから外へ出ます。ただ今回はループバックのモジュールを使用しているのそのままパケットはP4ブロックを通り再度enp20s0f0に戻ります。
※実際のネットワークインタフェース名は自身の環境に合わせて変更してください。
まずはP4ブロックの起動です。
sudo np4atomtool daemon --start --device /dev/intel-fpga-fme.0
こんな感じで返ってくればOKです。
Starting...
Load plugin path : /usr/lib/np4-atom/plugins/libplugin_n3000.so
Device name : Processing accelerators: Intel Corporation Device N3000
OK
次にP4ブロックの情報を確認します。
np4atomtool atom --info
同じコードを使用していれば以下のように返ってきます。
P4 Atoms
Atom 0:
Identification : "simple_filter"
Table "table_ePort"
Capacity : 0
Type : ternary
Table "table_ipv4_filter"
Capacity : 128
Type : exact
Table "table_ipv6_filter"
Capacity : 128
Type : exact
続いてパケットのフィルタリングルールを設定します。
以下のようなルールファイルを作成して保存します。
・rules.txt
NP4 ruleset 3.0
table_ipv4_filter default action permit
table_ipv6_filter default action permit
table_ePort default action eport_ctrl params ( offset 0 )
table_ipv4_filter keys ( ipv4.srcAddr 1.2.3.4 ) action drop_p
ポイントとなる部分を少し解説すると、まずそれぞれのテーブルに対してデフォルトアクションが定義されています。これは名前の取りデフォルト時の挙動の設定です。次の最後の行ですがこちらでどんなSource IPアドレスのパケットが来たらどんな動作をする、という設定をしています。今回だと1.2.3.4というSourceアドレスが来たらドロップのアクションを実施する設定です。
ではこのルールファイルを以下のコマンドでロードします。
np4atomtool atom --load rules.txt
特に何も返ってきませんがそれでOKです。
ではここから実際にパケット転送してみます。
Source IPアドレス1.2.3.4はドロップする設定になっているのでそれ以外のパケットを投げて通るか見てみます。
enp20s0f0から転送して自分で受けます。転送はtcpreplayコマンド、受信パケット確認はtcpdumpコマンドを使用します。
私の方ではSource IPアドレスが1.2.3.4と1.2.3.5のパケットをScapy等でpcapファイルとして作っています。
実際のコマンドは以下の通りです。
順番としてはtcpdump実行してからもう一つのターミナルでtcpreplayする感じです。
sudo tcpdump -i enp20s0f0 -Q in
sudo tcpreplay -i enp20s0f0 -t 1235.pcap
今回はきちんと通るSource IPアドレスなのでtcpdump側にこんな感じで受信パケット見えると思います。
これでパケットがちゃんと通ることが確認できました。
次にドロップされるか見てみましょう。
ドロップされる1.2.3.4のSource IPアドレスのパケットを転送してみましょう。
sudo tcpdump -i enp20s0f0 -Q in
sudo tcpreplay -i enp20s0f0 -t 1234.pcap
ん~、tcpdump側には何も受信パケットが出てこない、、実験成功です!!!
(かなり地味だったりしますが。。。)
##参考情報
NICT(情報通信研究機構)から来年度中にP4テストベッドとしてFPGA Smart NICがサポートされる予定です。
P4を使ったFPGA Smart NICの活用に興味がある方は該当の環境を検討するのもいいかと思います。
##まとめ
P4を使ってFPGAにパケットフィルタを実装することができました。
FPGAの知識なくてもP4だけでここまで動かせるのはうれしいですね。
かなり簡単なので皆さんも短時間で同じ実装は出来ると思いました。
今度はSRv6とはNATとかもやってみたいですね。
あとは遅延測定するのにタイムスタンプとか使ってみるのも面白そうです。
また何かいいネタあれば投稿しますのでよろしくお願いします。