LoginSignup
14
11

More than 3 years have passed since last update.

P4 で記述した簡易 L2 Switch にタグ VLAN(802.1Q)を対応させて VLAN 毎のトラヒックカウンタを実装する(準備編)

Last updated at Posted at 2020-06-07

はじめに

P4 で記述した L2 Switch に下記の機能を追加します.

  • タグ VLAN(802.1Q)毎に MAC テーブル/Broadcast domain を分割
  • VLAN 毎のトラヒックカウンタ

各機能の P4 での記述方法,および BMv2(P4 対応の仮想スイッチ)での機能確認方法について解説します.追加機能のコードを理解するための基礎文法についても簡単に記述しますが,詳細は公式ページを参照ください.

思いのほか内容が膨らんでしまったため,P4 の基礎文法等を説明する「準備編」と付加機能の実装を説明する「実装編」の 2 段構成としました.まずは準備編として P4 の基礎文法から説明します.

P4 の基礎文法

P4 はパケット処理に特化したプログラミング言語の一つです.特に,パケット処理を行う H/W そのものを P4 で抽象化することで,柔軟な機能追加を実現しています.

ユーザが自作機能を開発する際は,利用する H/W に対応する architecture を include し,その上に自作機能を記述していきます.architecture としては,OSS では v1modelpsa 等が知られており.ベンダ製チップでは tofino が有名です.なお,psa は Portable Switch Architecture の略で,P4.org というコミュニティが標準アーキティクチャとして仕様策定を行っているようです.PSA の詳細アーキティクチャは公式ページを参照ください.今回は BMv2 にて実装されている v1model を例に P4 プログラムの記述方法を簡単に解説します.

ヘッダ定義

識別するヘッダを定義します.今回はタグ VLAN(802.1Q)に対応した L2 Switch なので Ether ヘッダ,VLAN タグが識別出来ればよいです.これらを P4 で記述すると下記のようになります.

header_definition.p4
header ethernet_t {
    bit<48> dstAddr;
    bit<48> srcAddr;
    bit<16> etherType;
}

header vlan_t {
    bit<3> priority;
    bit<1> dei;
    bit<12> id;
    bit<16> etherType;
}

記述は非常にシンプルで,上位フィールドから順番に "ビット数" と "変数名" を列挙すればよいです.また,特定のビット数には名前を付けることが可能で,例えば MAC アドレスのビット長を表す macAddr_t を typedef bit<48> macAddr_t のように定義することも出来ます.この場合は bit<48> の部分を macAddr_t で代替可能です.

これらはヘッダの"型" を定義しており,実際のヘッダはこれらの "型" を組み合わせて定義します.今回は上記2つの "型" のみを取りうるため header は下記のように記述します.

header.p4
struct headers {
    ethernet_t ethernet;
    vlan_t     vlan;
}   

C 言語の構造体に似ていますね.上述の header_definition.p4 で定義した "型" をもつ変数としてヘッダの構成要素(Ether ヘッダや IP ヘッダ等)を定義するイメージです.

こちらの headers という構造体には取りうるヘッダを全て記述します.入力パケットをこちらで定義した "型" のいずれかに沿って Parse していきますが,注意すべき点としては「全てのヘッダを過不足なく Parse する必要は無い」という点です.入力パケットがその "型" で取り出された場合は isValid() が true となり,取り出されていない場合は false となります.例えば(ethernet_t という型に沿って)ethernet が Parse された場合は ethernet.isValid() == true となります.

Parser 定義

入力パケットは上述の headers 構造体で定義した各 "型" のいずれかに沿って Parse されます.今回は下記のような条件分岐を行い,入力パケットを Parse します.

  • 入力パケットはまず ethernet_t(Ether ヘッダ)の "型" に沿って Parse
    • 取り出した Ether ヘッダの etherType が 0x8100(802.1Q VLAN タグ)の場合
      • vlan_t(VLAN タグ)の型に沿って Parse
    • 取り出した Ether ヘッダの etherType が 0x8100 以外の場合
      • Parse 終了

こちらを P4 で記述すると下記のようになります.

parser.p4
state start {
    transition parse_ethernet;
}

state parse_ethernet {
    packet.extract(hdr.ethernet);
    transition select(hdr.ethernet.etherType) {
        0x8100  : parse_vlan;
        default : accept;
    }
}

state parse_vlan {
    packet.extract(hdr.vlan);
    transition accept;
}

"state" という表記が示すように,Parser は状態遷移図としてモデル化され,各 "型" に沿ってParse する部分は一つの状態として記述されます.この時,下記のルールを守る必要があります.

  • 最初の状態は必ず "start" でなければならない
  • 最後の状態は必ず "accept" or "reject" でなければならない

最終状態が "accept" の場合はそのまま処理が続行し, "reject" の場合は入力パケットは破棄されます.なお,最終状態を明記しなかった場合は "reject" となるようです(言語仕様より)

各状態では packet_in 型の変数(コード中では packet と記述されており,入力パケットと等価なものとして認識すれば大丈夫です)が具備する extract() method により,引数で指定した "型" に沿って Parse し,値が格納されます.extract() の引数で指定した構造体変数の型に沿って入力パケットが Parse され,その結果が引数で指定した構造体変数に書き込まれる,というイメージで認識しておけばよいかと思います.例えば,入力パケットから Ether ヘッダを取り出したい場合は,ethernet_t 型の変数である ethernet を下記のように引数に指定しpacket.extract(hdr.ethernet)と記述します.

"hdr" という変数は headers 構造体のインスタンスのようなイメージです.こちらは Parser 定義部そのものの引数として,例えば下記のように記述されます.

parser_skerton.p4
parser ParseName(packet_in packet,
                 out headers hdr,
                 inout metadata_t meta,
                 inout standard_metadata_t standard_metadata) {
    /* Parse 処理記述(上記 parser.p4 参照) */
}

こちらは architecture に依存する部分となり,利用する H/W によっては引数が若干異なる場合があります.上記は v1model を用いる場合の記述となります.

状態間の遷移は transition あるいは transition select() と記述します.前者は遷移先の状態が1つの場合,後者は遷移先の状態が複数の場合にそれぞれ使用します.特に後者の場合は select() の括弧内で指定したフィールド値に基づいて遷移先の状態を選択します.例えば,Ether ヘッダの etherType に基づいて状態遷移する場合は select(hdr.ethernet.etherType) のように記述します.

Ingress/Egress 定義

Parse された入力パケットに対する処理は Ingress/Egress にて記述します.なお,Ingress はパケットを受信(+ Parse)してキューにバッファリングするまでの処理,Egress はキューにバッファリングされて出力ポートから発出されるまでの処理を指します.

Ingress や Egress の構成は非常にシンプルで「action」「table」「apply」に大別されます.action ではコントロールプレーンから受け取った parameter を用いて,入力パケットに対して行う一連の処理を記述します.table では key と action の紐付けを定義します.apply では table や action を入力パケットにどのように適用するか,の処理シーケンスを定義します.入力パケットに適用したい処理のまとまりを action で定義し,それらをどのような key 値に基づいて match させるかを table で定義し,それらをどのような順序で入力パケットに適用していくかを apply で定義する,という感じです.

と,長々と書いてしまいましたが,実際の記述例を見たほうが理解が早いかと思います.例えば,コントロールプレーンから「出力ポート番号」を受け取って,その値を出力ポートに設定する action は下記のように記述できます.

action.p4
action switching(bit<9> port) {
    standard_metadata.egress_spec = port;
}

standard_metadata については発展的な内容となるため詳細は省略しますが,ここでは「スイッチ側が用意した制御変数」というイメージでよいかと思います.standard_metadata にはスイッチを制御するため or スイッチから情報を取得するための様々な変数が定義されており,その中の egress_spec は入力パケットの出力ポートを決定する制御変数となります.standard_metadata.egress_spec = port とすることで,コントロールプレーンにより登録されたポート(port)を出力ポートとして設定しているのです.なお,standard_metadata は architecture 毎に定義されており,v1model の standard_metadata は v1model.p4 にて定義されています.

上記で定義した action を入力パケットの宛先 MAC アドレスと紐付けて table に登録するためには下記のように記述します.

table.p4
table mac_exact {
    key = { hdr.ethernet.dstAddr: exact; }
    actions = { switching; }
}

key = { ... } で table エントリの key を設定し,各エントリが 取りうる全ての actionactions = { ... } に列挙します.このとき全ての action という部分は重要です.上記の table は action として switching のみを取りうる table として定義していますが,必ずしも転送するわけではなく,意図的にパケットドロップさせたい場合もあるかと思います.この場合は actions = { ... } にパケットドロップを行う action(ここでは drop と表記)を追加します.

table_with_drop.p4
 table mac_exact {
     key = { hdr.ethernet.dstAddr: exact; }
     actions = {
         switching;
+        drop;
     }
 }

table の定義で注意すべき点としては,table の記述はあくまで各エントリが取りうる action を列挙するのみで,実際の key 値がどのような action を取りうるか,すなわち table エントリそのものはコントロールプレーンが登録する,という点です.例えば,"11:22:33:44:55:66" という宛先 MAC アドレスに対しては(switching action によって)出力ポート "3" を設定し,また "66:55:44:33:22:11" という宛先 MAC アドレスに対してはパケットをドロップするというaction(ここでは drop と表記)を設定するとします.この場合,これらの組み合わせを下記のようにコントロールプレーンが登録します.

key action action parameter
11:22:33:44:55:66 switching 3 (000000011)
66:55:44:33:22:11 drop -

action, table を定義したら,最後はこれらをどのような順序で適用していくか,を apply{} に記述します.入力パケットに VLAN タグが付いているかどうかで場合分けし,VLAN タグが付いていない場合のみ mac_exact table を適用(入力パケットの宛先 MAC アドレスを key として mac_exact のエントリを走査)する処理は下記のように記述されます.

apply.p4
apply{
    if ( !hdr.vlan.isValid() ) {
        mac_exact.apply();
    }
}

非常にシンプルですね.isValid() は "そのヘッダが Parse されたかどうか" を表す bool 型の変数を返します.! は NOT(論理否定)です.従って !hdr.vlan.isValid()は hdr の vlan が Parse されていない場合に true となります.

入力パケットに table を適用する場合は テーブル名.apply() と記述します.今回は mac_exact table を適用するため mac_exact.apply() と記述することで,入力パケットの hdr.ethernet.dstAddr を key として mac_exact table を走査し,エントリが hit した場合は対応する action を実行します.

(おまけ)入力パケットへのヘッダ挿入

余談ですが,入力パケットに新たにヘッダを追加する場合は setValid() という method を使用します.例えば,VLAN タグが付いていないパケットに新たにVLAN タグを付与して転送する場合は下記のようにコードを追記すればよいです.

ingress_advanced.p4
 apply {
     if ( !hdr.vlan.isValid() ) {
        mac_exact.apply();
+       hdr.vlan.setValid();
+       hdr.vlan.priority  = /* 任意の値を入力 */;
+       hdr.vlan.dei       = /* 任意の値を入力 */;
+       hdr.vlan.id        = /* 任意の値を入力 */;
+       hdr.vlan.etherType = /* 任意の値を入力 */;
     }
 }

上記の記述は,アクセスポートで VLAN パケットを受信し,トランクポートから VLAN パケットを発出する場合等に使えるかと思います.

Deparser 定義

パケット処理の最後は,Parse したヘッダの再構築(Deparse)です.出力ポートから発出するヘッダを構成します.今回の場合は Ether ヘッダ or VLAN タグを取りうるため,下記のように記述します.

deparser.p4
control DeparserName(packet_out packet, in headers hdr) {
    apply{
        packet.emit(hdr.ethernet);
        packet.emit(hdr.vlan);
    }
}

基本的にはheaders.p4 で定義した headers 構造体のメンバ変数を packet.emit() の引数として列挙すればよいです.emit() method は引数の isValid() が true の場合に出力パケットのヘッダに当該ヘッダをスタックします.そのため,例えば VLAN タグを Parse しなかった場合(hdr.vlan.isValid() == falseの場合)は packet.emit(hdr.vlan) と記述されていても出力ヘッダに VLAN タグをスタックしません.

なお,Deparser における "packet" は Parser における "packet" とは型が異なります.Parser で使用する "packet" は packet_in 型,Deparser で使用する "packet" は packet_out 型です.こちらも発展的な内容となるため,packet_in 型の変数は入力パケット,packet_out 型の変数は出力パケットと認識しても差し支えないかと思います(具体的な定義は core.p4 で与えられます.詳細はこちらを参照ください).

P4 による L2 Switch 実装

以上のP4プログラム(の断片)を組み合わせることで,P4 による簡易 L2 Switch が実装可能です.ここで想定する L2 switch の仕様は下記です.

  • 入力パケットのヘッダには Ether ヘッダ( + 802.1Q VLAN タグ)がスタックされている.
  • 入力パケットに VLAN タグがスタックされていない場合
    • 宛先 MAC アドレスに基づき出力ポートを選択する.
  • VLAN タグがスタックされている場合
    • 入力パケットを drop する(VLAN タグも考慮して switching する実装は「実装編」にて説明します).

なお,宛先 MAC アドレスと出力ポートの組は事前にコントロールプレーンより登録するものとします.

P4 で記述すると下記のようになります.

L2switch.p4
/** INCLUDE ARCHITECTURE **/
#include <core.p4>
#include <v1model.p4>

/** HEADERS **/
header ethernet_t {
    bit<48> dstAddr;
    bit<48> srcAddr;
    bit<16> etherType;
}

header vlan_t {
    bit<3> priority;
    bit<1> dei;
    bit<12> id;
    bit<16> etherType;
}

struct headers {
    ethernet_t ethernet;
    vlan_t     vlan;
}

struct metadata_t {
}

/** PARSER **/
parser ParseName(packet_in packet,
                 out headers hdr,
                 inout metadata_t meta,
                 inout standard_metadata_t standard_metadata) {

    state start {
        transition parse_ethernet;
    }

    state parse_ethernet {
        packet.extract(hdr.ethernet);
        transition select(hdr.ethernet.etherType) {
            0x8100  : parse_vlan;
            default : accept;
        }
    }

    state parse_vlan {
        packet.extract(hdr.vlan);
        transition accept;
    }
}

/** INGRESS **/
control IngressName(inout headers hdr,
                    inout metadata_t meta,
                    inout standard_metadata_t standard_metadata) {

    action drop() {
        mark_to_drop(standard_metadata);
    }

    action switching(bit<9> port) {
        standard_metadata.egress_spec = port;
    }

    table mac_exact {
        key = { hdr.ethernet.dstAddr: exact; }
        actions = {
            switching;
            drop;
        }
    }

    apply{
        if ( !hdr.vlan.isValid() ) {
            mac_exact.apply();
        } else {
            drop();
        }
    }
}

/** EGRESS **/
control EgressName(inout headers hdr,
                   inout metadata meta,
                   inout standard_metadata_t standard_metadata) {
    apply {}
}

/** DEPARSER **/
control DeparserName(packet_out packet, in headers hdr) {
    apply{
        packet.emit(hdr.ethernet);
        packet.emit(hdr.vlan);
    }
}

/* Verify Checksum (NOT used here) */
control verifyChecksum(inout headers_t hdr, inout metadata_t meta) {
    apply { }
}

/* Update Checksum (NOT used here) */
control updateChecksum(inout headers_t hdr, inout metadata_t meta) {
    apply { }
}

/* Pipeline Definition */
V1Switch(parserImpl(),
         verifyChecksum(),
         ingressImpl(),
         egressImpl(),
         updateChecksum(),
         deparserImpl()) main;

簡易的な L2 Switch は概ね上記のように記述することで実装可能です.なお,Egress は特に処理が必要無いため空白としています.

おわりに

P4 の基礎文法,および簡易 L2 Switch の実装を説明しました.「実装編」では上記で実装した L2 Switch に対し下記の機能追加を行います.

  • タグ VLAN(802.1Q)毎に MAC テーブル/Broadcast domain を分割
  • VLAN 毎のトラヒックカウンタ
14
11
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
11