送信元 MAC アドレス毎のトラヒック制限機能を Meter(RFC2698) を用いて P4/P4Runtime で実装する

# はじめに

[前回の記事](https://qiita.com/13ryuse4/items/96ed8b31382e1fdd79f1)では [P4Runtime](https://p4.org/p4runtime/spec/v1.3.0/P4Runtime-Spec.html) を用いてテーブルエントリ登録やトラヒックカウンタ値の取得を行うコントロールプレーン機能を実装しました． 今回は取得したトラヒックカウンタ値に基づき，指定のトラヒック量を超過した場合に一定時間トラヒック制限を適用する機能を実装します．特に，カウンタ値の取得は送信元 MAC アドレス毎に行い，またデータプレーンにおけるトラヒック制限は Meter ([RFC2698](https://tools.ietf.org/html/rfc2698)) を用いて実装します．

本記事で説明する内容のソースコードは[こちら](https://github.com/ryu-s-4/p4-practice/tree/master/traffic-limitter)で公開していますので併せて参照ください．P4 ってそもそも何？という方は[こちらの記事](https://qiita.com/13ryuse4/items/cb83abd80712616e0799)を，P4Runtime の基礎から説明が必要な方は[こちらの記事](https://qiita.com/13ryuse4/items/96ed8b31382e1fdd79f1)をそれぞれ参照ください．

### 本記事のサマリ

本記事には P4/P4Runtime で送信元 MAC アドレス毎のトラヒック制限機能を実装するための要素として下記の内容が記載されています．

- Meter (RFC2698) の動作原理
- direct meter / direct counter の概念，および P4 言語による実装
- direct meter / direct counter の P4Runtime による制御方法，および Go 言語による実装

### 注意

本記事で説明する P4 プログラムは [v1model](https://github.com/p4lang/p4c/blob/master/p4include/v1model.p4) をアーキティクチャとして使用しています．細かい実装については使用するデバイス（アーキティクチャ）毎に若干異なる点に注意してください．P4 におけるアーキティクチャの概念については[こちらの記事](https://qiita.com/13ryuse4/items/cb83abd80712616e0799)や[こちらの記事](https://qiita.com/13ryuse4/items/6f95ada4d248372603c2)で説明していますので併せて参照ください．

# Meter(RFC2698)の動作原理

Meter は Two Rate Three Color Marker (trTCM) とも呼ばれ，２種類の閾値に対し，トラヒック流量に応じた３種類の "色付け" を行う機構であり [RFC2698](https://tools.ietf.org/html/rfc2698) で規定されています．一般に meter はパケットの明示的なドロップやマーキングにより流量制御を行う場合に使用されますが，meter 自体はあくまで閾値に対する流量の超過を "色" で出力するのみであり，その結果をどう利用するかはデータプレーン側の実装に委ねられます．

Meter には ```committed``` と ```peak``` の２種類の information rate / burst size が規定されており，通過するトラヒックの流量（information rate = 帯域 / パケットサイズ = burst size）がそれぞれを超過した場合に対応する "色" を出力します．出力する "色" とその条件は下記となります．

- ```GREEN```  ⇒ ```committed``` および ```peak``` いずれも超過していない場合
- ```YELLOW``` ⇒ ```committed``` を超過しているが ```peak``` は超過していない場合
- ```RED```    ⇒ ```peak``` を超過している場合

これらの４種類の閾値はコントロールプレーンから設定し，データプレーンは任意のタイミングで（[v1model](https://github.com/p4lang/p4c/blob/master/p4include/v1model.p4) では ```read``` という関数，[PSA](https://p4.org/p4-spec/docs/PSA-v1.1.0.html#sec-meters) では ```execute``` という関数を使用して）対応する "色" を取得します．なお，各閾値はそれぞれ下記のように省略して記載される場合が多いようです（P4Runtime でも各閾値は下記のような名前の変数としてそれぞれ扱われます）．

- ```CIR``` : committed information rate
- ```CBS``` : committed burst size
- ```PIR``` : peak information rate
- ```PBS``` : peak burst size

meter の動作は ```committed``` および ```peak``` に対応する２つのトークンバケット（以下ではそれぞれ ```P``` と ```C``` と表記）を用いてモデル化されます．```P``` および ```C``` のサイズはそれぞれ ```PBS``` と ```CBS```（対応するバーストサイズ）です．初期状態では ```P``` および ```C``` はそれぞれ最大量（```PBS``` / ```CBS```）で満たされていますが，トラヒックが通過する毎に通過した量だけトークンバケットの中身が減少していきます．なお ```P``` および ```C``` の中身は毎秒 ```PIR``` / ```CIR``` ずつ増加し，最大で ```PBS``` / ```CBS``` まで中身が回復します．従って，トークンバケットの中身が空になると下記のいずれかを意味します．

- burst size 以上の大きさのパケットが通過
- 単位時間あたりに information rate 以上のトラヒックが通過

"色" の出力の仕方は ```color-blind``` mode と ```color-aware``` mode の２パターンありますが，基本はいずれも同じでトークンバケットが空かどうかで出力する "色" を決定します．すなわち，それぞれ下記の場合に各色を出力します．

- ```GREEN``` ⇒ ```P``` および ```C``` がいずれも空でない場合
- ```YELLOW``` ⇒ ```P``` は空でないが，```C``` が空である場合
- ```RED``` ⇒ ```P``` が空である場合

```color-blind``` mode の場合はトークンバケットが空かどうかのみで "色" の判定を行いますが，```color-aware``` mode の場合はトークンバケットが空かどうか，に加えて入力パケットに対する事前の "色付け" を許容し，パケットが事前に色付けされている場合はそちらの色を優先して出力します．

今回は上記で説明した meter を利用してトラヒック制限機能を実装します．特に送信元 MAC アドレス毎に meter を用意し，トラヒック制限を有効化している場合のみ ```read``` で色を出力 & 色が ```RED``` の場合にパケットドロップをデータプレーン側で実行することで，コントロールプレーンから設定した ```PIR``` および ```PBS``` にトラヒックを制限します．以下では，P4 および P4Runtime を用いてこれらの処理をどのように記述するか，について説明します．

# direct meter / direct counter の P4 実装

送信元 MAC アドレス毎に「トラヒック制限」および「トラヒックカウント」を行うために，***テーブルエントリ単位で***これらの機能が実行される [direct meter](https://p4.org/p4-spec/docs/PSA-v1.1.0.html#sec-direct-meter) および [direct counter](https://p4.org/p4-spec/docs/PSA-v1.1.0.html#sec-direct-counter) を使用します．direct meter / direct counter はそれぞれ P4 で下記のように定義されます（```limitter``` および ```meter_cnt``` という direct meter / direct counter をそれぞれ定義しています）．

```c:switchgin_meter.p4
control MyIngress(inout headers hdr,
                  inout metadata meta,
                  inout standard_metadata_t standard_metadata) {

    direct_counter(CounterType.bytes) meter_cnt;
    direct_meter<bit<2>>(MeterType.bytes) limitter;

    <中略>
}
```

```direct_meter<T>...``` の ```T``` には ```bit<W>``` を指定します．これは direct meter を ```read``` した際に出力される color のビット幅を表し ```2 <= W``` である必要があります．[今回の実装](https://github.com/ryu-s-4/p4-practice/blob/master/traffic-limitter/switching_meter.p4)では color を格納するための変数を 2bit 幅で定義しているので ```direct_meter<bit<2>>...``` として direct meter を定義しています．

direct_meter / direct counter の測定単位は ```MeterType``` および ```CounterType``` で指定しますが，v1model ではそれぞれ下記のいずれかから選択可能です．

```c:v1model.p4
enum CounterType {
    packets,
    bytes,
    packets_and_bytes
}

enum MeterType {
    packets,
    bytes
}
```

direct meter /  direct counter はテーブルエントリ単位で実行されるため，P4 プログラムでは table に紐付ける形で記述します．特に今回は送信元 MAC アドレス毎に direct meter / direct counter を実行するため，送信元 MAC アドレスを key とする table (```check_limit```) に対して下記のように direct meter / direct counter を紐付けます．

```c:swithcing_meter.p4
control MyIngress(inout headers hdr,
                  inout metadata meta,
                  inout standard_metadata_t standard_metadata) {

    <中略>

    table check_limit {
        key = { hdr.ethernet.srcAddr: exact; }
        actions = { 
            limit_traffic;
            NoAction;
        }
        size = 1024;
        default_action = NoAction;
        counters = meter_cnt;
        meters = limitter;
    }

    <中略>
}
```

table の設定項目に ```couters``` と ```meters``` があり，それぞれに対して先に定義した direct counter / direct meter を設定することで，当該 table の各エントリにこれらの counter と meter が紐つく形となります．

direct counter については[前回の記事](https://qiita.com/13ryuse4/items/6f95ada4d248372603c2)で実装した counter と若干異なり，明示的に ```count``` はせず，テーブルエントリに hit した際に自動的にトラヒック量がカウントされます．したがって，データプレーン側では基本的に何もせず，コントロールプレーンは所望のテーブルエントリを指定してそのエントリに紐つくカウンタ値を取得します．一方で meter についてはトラヒックの ```color``` を取得し，得られた ```color``` に応じてどのような処理を行うかをデータプレーン側で記述します．[今回の実装](https://github.com/ryu-s-4/p4-practice/blob/master/traffic-limitter/switching_meter.p4) では meter で取得した ```color``` が ```RED``` の場合（```PIR``` あるいは ```PBS``` を超過している場合）に当該パケットをドロップする実装としています．こちらは ```limit_traffic``` という action で実装しており，下記のように記述されます．

```c:switching_meter.p4

struct metadata {
    bit<2> color;
    bool drop_flag;
}

<中略>

control MyIngress(inout headers hdr,
                  inout metadata meta,
                  inout standard_metadata_t standard_metadata) {

    <中略>

    action limit_traffic() {
        limitter.read(meta.color);
        if (meta.color == V1MODEL_METER_COLOR_RED) {
            meta.drop_flag = true;
        }
    }

    <中略>
}
```

[v1model](https://github.com/p4lang/p4c/blob/master/p4include/v1model.p4) では meter が出力する ```color``` を下記のように定義しており ```read``` で取得した ```color``` を下記と比較することで meter が出力した ```color``` を判別します．なお，meter が下記 ```color``` を出力する閾値についてはコントロールプレーンから実装しますが，こちらの詳細は本記事の後半で説明します．

```c:v1model.p4
#define V1MODEL_METER_COLOR_GREEN  0
#define V1MODEL_METER_COLOR_YELLOW 1
#define V1MODEL_METER_COLOR_RED    2
```

上記の各要素を組み合わせることで送信元 MAC アドレス毎のトラヒック制限機能（ = 入力トラヒックが指定レート/サイズを超過した場合にパケットをドロップする処理）が実装可能です．入力パケットに対して，まずトラヒック制限要否を判断する table (```check_limit```) を適用し，対応するテーブルエントリ（送信元 MAC アドレス）の action が ```traffic_limit``` かつ meter の出力が ```RED``` の場合に ```drop_flag``` を true にして， ```drop()``` を実行し当該パケットをドロップします．こちらは P4 で下記のように記述されます．


```c:switching_meter.p4
control MyIngress(inout headers hdr,
                  inout metadata meta,
                  inout standard_metadata_t standard_metadata) {

    <中略>

    action drop() {
        mark_to_drop(standard_metadata);
    }

    <中略>

    apply {
        meta.drop_flag = false;
        check_limit.apply();
        if (meta.drop_flag == true) {
            drop();
        } else {
            /* L2 Switching */
        }
    }
}
```

```ckeck_limit``` table のエントリが監視対象の MAC アドレスに対応し，テーブルエントリの登録 / 削除によって監視対象の MAC アドレスを追加・削除します．また，各エントリ（監視対象の MAC アドレス）の action は初期登録時は ```NoAction()``` とすることでトラヒック制限機能を無効化しています．コントロールプレーンから各エントリのトラヒックカウンタ値を定期的に取得して，閾値を超えた場合に対応するエントリの action を ```NoAction``` から ```limit_traffic``` に切り替えることでトラヒック制限機能を有効化しています． これらのコントロールプレーン実装の詳細については[こちら](https://github.com/ryu-s-4/p4-practice/blob/master/traffic-limitter/main.go)を参照ください．

以上は direct meter / direct counter を用いて送信元 MAC アドレス毎のトラヒック制限機能を実現するためのデータプレーン側の実装となります．以下ではコントロールプレーンから direct meter / direct counter を制御する P4Runtime 実装（特に Go 言語による実装）について要点を説明します．

# direct meter / direct counter 制御の Go 実装

P4 で実装したトラヒック制限機能やトラヒックカウンタは P4Runtime を用いてコントロールプレーンから制御されます．P4Runtime では direct meter や direct counter はテーブルエントリと同様 ```Entity``` として扱われます．従って，各 ```Entity``` を制御 or 各 ```Entity``` から情報を取得する場合は[こちらの記事](https://qiita.com/13ryuse4/items/96ed8b31382e1fdd79f1)や[こちらの記事](https://qiita.com/13ryuse4/items/3a68a79e81f621b85dd5)で説明したように ```Entity``` の ```Write()``` や ```Read()``` という形で実装されます．

direct meter や direct counter の ```Entity``` はそれぞれ [p4runtime.pb.go]() で下記のように定義されています．

```go:p4runtime.pb.go

/* direct meter の Entity（と関連する変数）*/
type Entity_DirectMeterEntry struct {
    DirectMeterEntry    *DirectMeterEntry
}

type DirectMeterEntry struct {
    // The associated table entry. This field is required.
    // table_entry.action is ignored. Other fields specify the match.
    TableEntry    *TableEntry
    Config        *MeterConfig
}

type MeterConfig struct {
    // Committed information rate (units per sec)
    Cir int64
    // Committed burst size
    Cburst int64
    // Peak information rate (units per sec)
    Pir int64
    // Peak burst size
    Pburst int64
}
```

```go:p4runtim.pb.go
/* direct counter の Entity（と関連する変数）*/
type Entity_DirectCounterEntry struct {
    DirectCounterEntry    *DirectCounterEntry
}

type DirectCounterEntry struct {
    // The associated table entry. This field is required.
    // table_entry.action is ignored. Other fields specify the match.
    TableEntry    *TableEntry
    Data          *CounterData
}

type CounterData struct {
    ByteCount    int64
    PacketCount  int64
}
```

direct meter については下記のパラメータを設定した ```Entity``` を ```Update``` に格納し ```Write()``` でデータプレーン側に書き込みます．

- committed information rate（```Cir```）
- committed burst size（```Cburst```）
- peak information rate（```Pir```）
- peak burst size（```Pburst```）

先に説明した通り ```Cir``` / ```Cburst``` は ```YELLOW``` に対応するレート / サイズ，```Pir``` / ```Pburst``` は ```RED``` に対応するレート / サイズです．このとき ```UpdateType``` は ```MODIFY``` のみ許容されている点に注意が必要です．基本的に "direct" な meter や counter はテーブルエントリと表裏一体なので，これらの ```INSERT``` や ```DELETE``` は元のテーブルエントリに付随して実行されるようです．direct meter の ```Entity``` を生成し ```Write()``` で書き込む処理は Go で下記のように実装されます．

```go:main.go
import(
    v1 "github.com/p4lang/p4runtime/go/p4/v1"
)

func main(){

    /* direct meter の Entity 生成 */
    directmeterentry := &v1.Entity_DirectMeterEntry{
        DirectMeterEntry: &v1.DirectMeterEntry{
            TableEntry:  /* 設定したい table entry */,
            Config: &v1.MeterConfig{
                Cir:     /* committed information rate (in int64) */,
                Cburst:  /* committed burst size       (in int64) */,
                Pir:     /* peak information rate      (in int64) */,
                Pburst:  /* peak burst size            (in int64) */,
            },
        },
    }

    /* Update 生成 */
    update := &v1.Update{
        Type: v1.Update_MODIFY,
        Entity: &v1.Entity{
            Entity: directmeterentry,
        },
    }
    updates := []*v1.Update{update}

    /* Write() でデータプレーンに書き込み */
    atomicityType := /* Atomicity を指定 */
    var atomicity v1.WriteRequest_Atomicity
    switch atomicityType {
    case "CONTINUE_ON_ERROR":
        atomicity = v1.WriteRequest_CONTINUE_ON_ERROR
    case "ROLLBACK_ON_ERROR": // OPTIONAL
        atomicity = v1.WriteRequest_ROLLBACK_ON_ERROR
    case "DATAPLANE_ATOMIC": // OPTIONAL
        atomicity = v1.WriteRequest_DATAPLANE_ATOMIC
    default:
        atomicity = v1.WriteRequest_CONTINUE_ON_ERROR
    }
    write_request := v1.WriteRequest{
        DeviceId:   /* device id */,
        ElectionId: /* election id */,
        Updates:    updates,
        Atomicity:  atomicity,
    }
    write_response, err := client.Write(context.TODO(), &write_request)
    if err != nil {
        /* Error 処理 */
    }
}
```

なお上記の ```client``` は ```P4RuntimeClient``` のインスタンスであり，事前に StreamChannel の確立や Primary 選出，および ForwardingPipelineConfig の設定等が行われているものとしています．詳細は[前回の記事](https://qiita.com/13ryuse4/items/96ed8b31382e1fdd79f1)を参照ください．

direct counter からトラヒックカウンタ値を取得する場合も，direct meter と同様に ```Entity_DirectCounterEntry``` にカウンタ値を取得したいテーブルエントリを設定して ```Read()``` でデータプレーン側から値を取得します．direct counter の ```Entity``` を生成し ```Read()``` でカウンタ値を取得する部分は Go で下記のように実装されます．

```go:main.go
import(
    v1 "github.com/p4lang/p4runtime/go/p4/v1"
)

func main(){

    /* direct counter の Entity 生成 */
    directcounterentry := &v1.Entity_DirectCounterEntry{
        DirectCounterEntry: &v1.DirectCounterEntry{
            TableEntry: /* カウンタ値を取得したい table entry */,
        },
    }

    /* ReadClient の生成 */
    read_request := v1.ReadRequest{
        DeviceId: /* device id */,
        Entities: []*v1.Entity{&v1.Entity{ Entity: directcounterentry}},
    }
    read_client, err := client.Read(context.TODO(), &read_request)
    if err != nil {
        /* Error 処理 */
    }

    /* カウンタ値（DirectCounterEntry）の取得 */
    read_response, err := read_client.Recv()
    if err != nil {
        /* Error 処理 */
    }
    entities := read_response.GetEntities()
    if entity == nil {
        /* Error 処理 */
    }
}
```

```entities``` にカウンタ値をもつ ```DirectCounterEntry``` が格納されているため，こちらからカウンタ値を取得します．なお direct counter のカウンタ値を指定の値に変更したい（特に[今回の実装](https://github.com/ryu-s-4/p4-practice/blob/master/traffic-limitter/main.go)ではカウンタ値を 0 にクリアする）場合は ```Data``` に指定の値を格納し ```UpdateType``` を ```MODIFY``` としてデータプレーンに書き込むことで実装出来ます．例えば，あるテーブルエントリの direct counter をゼロクリアする場合は下記のように実装されます（```Write()```でデータプレーンに書き込む部分は direct meter と同様のため省略しています）．

```go:main.go
import(
    v1 "github.com/p4lang/p4runtime/go/p4/v1"
)

func main(){

    /* direct counter の Entity 生成 */
    var zero_int64 int64 = 0
    directcounterentry := &v1.Entity_DirectCounterEntry{
        DirectCounterEntry: &v1.DirectCounterEntry{
            TableEntry: /* カウンタ値を変更したい table entry */,
            Data: &v1.CounterData{
                ByteCount:   zero_int64,
                PacketCount: zero_int64,
            },
        },
    }

    /* Update を生成 */
    update := &v1.Update{
        Type: v1.Update_MODIFY,
        Entity: &v1.Entity{
            Entity: directcounterentry,
        },
    }
    updates := []*v1.Update{update}

    /* Write() でデータプレーンに書き込み */ 
}
```

# おわりに

本記事では送信元 MAC アドレス毎のトラヒック制限機能を P4/P4Runtime で実装するための基礎として下記を説明しました．

- Meter (RFC2698) の動作原理
- direct meter / direct counter の概念，およびその P4 実装
- direct meter / direct counter の P4Runtime による制御方法，およびその Go 実装