P4はじめました


1. はじめに
2. P4ってなに？
3. P4 tutorial Basic
4. P4コンパイルしてみた
5. 終わりに

## はじめに

この記事はシスコの有志による Cisco Systems Japan Advent Calendar 2018 の 12 日目として投稿しています。

* 2017年版: https://qiita.com/advent-calendar/2017/cisco
* 2018年版: https://qiita.com/advent-calendar/2018/cisco


去年VPPでSRv6をやったので続きを書こうかと思っていましたが、いくつかすでに動かした記事があったので、他に気になって勉強したかったP4をやってみることにしました。


## P4ってなに？

P4とは「パケット処理のデータプレーンをプログラミング可能にする」プログラミング言語です。
[P4.org](https://p4.org/)には多くの機器ベンダーや通信事業者、学術系の研究者などが参加しており、注目の高さが伺えます。
P4が何かを詳しく知りたい方は[Tutorial](http://bit.ly/p4d2-2018-spring)をご参照いただくのが良いかと思います。
この記事ではP4ガチで素人のNetwork EngineerがTutorialを元に触ってみた内容を紹介します。


## P4 tutorial Basic

簡単に触れそうな環境がGithubのP4 Tutorialに上がっていたので触ってみました。
Vagrantなので以下に記載の手順でさくさくと進めてみます。(ここの内容は割愛)
https://github.com/p4lang/tutorials
まずはBasicのexerciseに手を付けてみます。

```
vagrant@p4:~/tutorials/exercises/basic$ ls
basic.p4  README.md   s1-runtime.json  s3-runtime.json  solution
Makefile  receive.py  s2-runtime.json  send.py          topology.json
```

ちゃんとファイルが揃っていてbasic.p4だけいじればパケット転送可能な状態になっています。
またbasic.p4の中でもある程度情報雛形はできていて、いくつか頑張ればコンパイルできそうです。

最初のTODOはParserです。
>TODO: Parsers for Ethernet and IPv4 that populate ethernet_t and ipv4_t fields.

Parserとはなんぞやとは[Tutorialの資料](https://github.com/p4lang/tutorials/blob/master/P4_tutorial.pdf)に書いてありました。

* Parsers are functions that map packets into headers and metadata, written in a state machine style
* Every parser has three predefined states 
	* start 
	* accept
	* reject
* Other states may be defined by the programmer
* In each state, execute zero or more statements, and then transition to another state (loops are OK)

上記をまず定義します。
このTutorialではHeaderとMetadataは定義してくれているので、stateを自分で書けってことですね。

```
/*************************************************************************
*********************** 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 {
        /* TODO: add parser logic */
        transition accept;
    }
}
```

資料見てみると、要はethertypeがIPv4(0x800)だったらAcceptとか書いていくわけですね。
まずはEthernetでIPv4を試したいので、色々参照して参考に以下のように書いてみます。
CやJavaのSwicth-case文に似てると書いてありますが、これお作法知らないときついですね。。。

```
/*************************************************************************
*********************** 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 accept;
    }

}
```

さて次のToDoは何か見てみるとIngress Processingです。
>TODO: An action (called ipv4_forward) that:
    Sets the egress port for the next hop.
    Updates the ethernet destination address with the address of the next hop.
    Updates the ethernet source address with the address of the switch.
    Decrements the TTL.

>TODO: A control that:
    Defines a table that will read an IPv4 destination address, and invoke either drop or ipv4_forward.
    An apply block that applies the table.


資料を見ると以下の説明があります。

* Similar to C functions (without loops) 
* Can declare variables, create tables, instantiate externs, etc.
* Functionality specified by code in apply statement
* Represent all kinds of processing that are expressible as DAG:
	* Match-Action Pipelines
	* Deparsers
	* Additional forms of packet processing (updating checksums)
* Interfaces with other blocks are governed by user- and
architecture-specified types (typically headers and metadata)

要はIngressのパケットの処理方法を定義する必要があるってことですね。

```
/*************************************************************************
**************  I N G R E S S   P R O C E S S I N G   *******************
*************************************************************************/

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

    action ipv4_forward(macAddr_t dstAddr, egressSpec_t port) {
        /* TODO: fill out code in action body */
    }

    table ipv4_lpm {
        key = {
            hdr.ipv4.dstAddr: lpm;
        }
        actions = {
            ipv4_forward;
            drop;
            NoAction;
        }
        size = 1024;
        default_action = NoAction();
    }

    apply {
        /* TODO: fix ingress control logic
         *  - ipv4_lpm should be applied only when IPv4 header is valid
         */
        ipv4_lpm.apply();
    }
}
```

上記の事前定義された内容を見ると受信したパケットのHeaderがちゃんとIPv4かチェック、Action定義して(MAC addressの書き換えとTTL処理など)Applyできるようにしてくださいということですね。
ここTutorialの資料見てるだけじゃ関数がよくわからないので、ちょっと難しいですね。。。
一応それっぽいことは[Cheat sheet](https://drive.google.com/file/d/1Z8woKyElFAOP6bMd8tRa_Q4SA1cd_Uva/view)に書いてありますが。。。


```
/*************************************************************************
**************  I N G R E S S   P R O C E S S I N G   *******************
*************************************************************************/

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

    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 = NoAction();
    }

    apply {
        if (hdr.ipv4.isValid()) {
            ipv4_lpm.apply();
    }
}
```

残るTODOはDEPARSERのところでした。
>TODO: A control that:
    Defines a table that will read an IPv4 destination address, and invoke either drop or ipv4_forward.
    An apply block that applies the table.


```
/*************************************************************************
***********************  D E P A R S E R  *******************************
*************************************************************************/

control MyDeparser(packet_out packet, in headers hdr) {
    apply {
        /* TODO: add deparser logic */
    }
}
```

まずはTutorial資料を見ます。

* Assembles the headers back into a well-formed packet
* Expressed as a control function 
	* No need for another construct!
* packet_out extern is defined in core.p4: emit(hdr): serializes header if it is valid
* Advantages:
	* Makes deparsing explicit...
	  ...but decouples from parsing

Packetのフォーマットに戻しましょう。ということですが、` packet.emit`という関数使えばOKそうです。
色々サンプル見てみてもみんな` packet.emit`を使っているので、一種のお作法的なものでしょうか。
core.p4の中身を見るのはまた今度にしてとりあえずコンパイルを目指します。
ということでEthernet HeaderとIPv4 Headerを付けてあげます。

```
/*************************************************************************
***********************  D E P A R S E R  *******************************
*************************************************************************/

control MyDeparser(packet_out packet, in headers hdr) {
    apply {
        packet.emit(hdr.ethernet);
        packet.emit(hdr.ipv4);
    }
}

```

これでコンパイルできるはず？

## P4コンパイルしてみた
さてやることはコンパイルするだけです。
と思ってやってみたらFailする。。。

```
vagrant@p4:~/tutorials/exercises/basic$ make run
mkdir -p build pcaps logs
p4c-bm2-ss --p4v 16 --p4runtime-file build/basic.p4info --p4runtime-format text -o build/basic.json basic.p4
make: p4c-bm2-ss: Command not found
../../utils/Makefile:35: recipe for target 'build/basic.json' failed
make: *** [build/basic.json] Error 127

```

なぜだろうと思って調べてみると、
>The tutorial exercises don't work with the latest version of PI.

VagrantでTutorialの手順通りやっているのに。。。
ここでなかなかハマってしまいました。最終的にはgit cloneしなおしてVagrantあげなおしました。
もう一度同じ手順を踏んでmakeしてみます。

```

vagrant@p4:/home/p4/tutorials/exercises/basic$ sudo make run
mkdir -p build pcaps logs
p4c-bm2-ss --p4v 16 --p4runtime-file build/basic.p4info --p4runtime-format text -o build/basic.json basic.p4
sudo python ../../utils/run_exercise.py -t topology.json -b simple_switch_grpc
Reading topology file.
Building mininet topology.
Switch port mapping:
s1:  1:h1	2:s2	3:s3
s2:  1:h2	2:s1	3:s3
s3:  1:h3	2:s1	3:s2
Configuring switch s3 using P4Runtime with file s3-runtime.json
 - Using P4Info file build/basic.p4info...
 - Connecting to P4Runtime server on 127.0.0.1:50053 (bmv2)...
 - Setting pipeline config (build/basic.json)...
 - Inserting 4 table entries...
 - MyIngress.ipv4_lpm: (default action) => MyIngress.drop()
 - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.1.1', 32] => MyIngress.ipv4_forward(dstAddr=00:00:00:01:03:00, port=2)
 - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.2.2', 32] => MyIngress.ipv4_forward(dstAddr=00:00:00:02:03:00, port=3)
 - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.3.3', 32] => MyIngress.ipv4_forward(dstAddr=00:00:00:00:03:03, port=1)
Configuring switch s2 using P4Runtime with file s2-runtime.json
 - Using P4Info file build/basic.p4info...
 - Connecting to P4Runtime server on 127.0.0.1:50052 (bmv2)...
 - Setting pipeline config (build/basic.json)...
 - Inserting 4 table entries...
 - MyIngress.ipv4_lpm: (default action) => MyIngress.drop()
 - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.1.1', 32] => MyIngress.ipv4_forward(dstAddr=00:00:00:01:02:00, port=2)
 - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.2.2', 32] => MyIngress.ipv4_forward(dstAddr=00:00:00:00:02:02, port=1)
 - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.3.3', 32] => MyIngress.ipv4_forward(dstAddr=00:00:00:03:03:00, port=3)
Configuring switch s1 using P4Runtime with file s1-runtime.json
 - Using P4Info file build/basic.p4info...
 - Connecting to P4Runtime server on 127.0.0.1:50051 (bmv2)...
 - Setting pipeline config (build/basic.json)...
 - Inserting 4 table entries...
 - MyIngress.ipv4_lpm: (default action) => MyIngress.drop()
 - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.1.1', 32] => MyIngress.ipv4_forward(dstAddr=00:00:00:00:01:01, port=1)
 - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.2.2', 32] => MyIngress.ipv4_forward(dstAddr=00:00:00:02:02:00, port=2)
 - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.3.3', 32] => MyIngress.ipv4_forward(dstAddr=00:00:00:03:03:00, port=3)
s1 -> gRPC port: 50051
s2 -> gRPC port: 50052
s3 -> gRPC port: 50053
**********
h1
default interface: h1-eth0	10.0.1.1	00:00:00:00:01:01
**********
**********
h2
default interface: h2-eth0	10.0.2.2	00:00:00:00:02:02
**********
**********
h3
default interface: h3-eth0	10.0.3.3	00:00:00:00:03:03
**********
Starting mininet CLI

======================================================================
Welcome to the BMV2 Mininet CLI!
======================================================================
Your P4 program is installed into the BMV2 software switch
and your initial runtime configuration is loaded. You can interact
with the network using the mininet CLI below.

To view a switch log, run this command from your host OS:
  tail -f /home/p4/tutorials/exercises/basic/logs/<switchname>.log

To view the switch output pcap, check the pcap files in /home/p4/tutorials/exercises/basic/pcaps:
 for example run:  sudo tcpdump -xxx -r s1-eth1.pcap

To view the P4Runtime requests sent to the switch, check the
corresponding txt file in /home/p4/tutorials/exercises/basic/logs:
 for example run:  cat /home/p4/tutorials/exercises/basic/logs/s1-p4runtime-requests.txt

mininet>

```

わーい。無事立ち上がったようです。
どんなトポロジで立ち上がっているかはtopology.jsonにあります。
今回はデフォルトなので三角形トポロジです。
また、IP addressの設定などはruntime_jsonに記載があります。


```json:topology.json
{
    "hosts": [
        "h1",
        "h2",
        "h3"
    ],
    "switches": {
        "s1": { "runtime_json" : "s1-runtime.json" },
        "s2": { "runtime_json" : "s2-runtime.json" },
        "s3": { "runtime_json" : "s3-runtime.json" }
    },
    "links": [
        ["h1", "s1"], ["s1", "s2"], ["s1", "s3"], 
        ["s3", "s2"], ["s2", "h2"], ["s3", "h3"]
    ]
}
```

```json:s1-runtime.json
{
  "target": "bmv2",
  "p4info": "build/basic.p4info",
  "bmv2_json": "build/basic.json",
  "table_entries": [
    {
      "table": "MyIngress.ipv4_lpm",
      "default_action": true,
      "action_name": "MyIngress.drop",
      "action_params": { }
    },
    {
      "table": "MyIngress.ipv4_lpm",
      "match": {
        "hdr.ipv4.dstAddr": ["10.0.1.1", 32]
      },
      "action_name": "MyIngress.ipv4_forward",
      "action_params": {
        "dstAddr": "00:00:00:00:01:01",
        "port": 1
      }
    },
    {
      "table": "MyIngress.ipv4_lpm",
      "match": {
        "hdr.ipv4.dstAddr": ["10.0.2.2", 32]
      },
      "action_name": "MyIngress.ipv4_forward",
      "action_params": {
        "dstAddr": "00:00:00:02:02:00",
        "port": 2
      }
    },
    {
      "table": "MyIngress.ipv4_lpm",
      "match": {
        "hdr.ipv4.dstAddr": ["10.0.3.3", 32]
      },
      "action_name": "MyIngress.ipv4_forward",
      "action_params": {
        "dstAddr": "00:00:00:03:03:00",
        "port": 3
      }
    }
  ]
}
```

さてなにができるか見てみると色々できそうです。
まずやることと言ったらPingでしょう。

```
mininet> h1 ping h2
PING 10.0.2.2 (10.0.2.2) 56(84) bytes of data.
64 bytes from 10.0.2.2: icmp_seq=1 ttl=62 time=4.96 ms
64 bytes from 10.0.2.2: icmp_seq=2 ttl=62 time=2.81 ms
64 bytes from 10.0.2.2: icmp_seq=3 ttl=62 time=3.32 ms
64 bytes from 10.0.2.2: icmp_seq=4 ttl=62 time=2.83 ms
^C
--- 10.0.2.2 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3008ms
rtt min/avg/max/mdev = 2.814/3.485/4.966/0.880 ms
```

ちゃんと動いてますね！
Pingが通って満足したので、ここで今回は終了したいと思います。


## 終わりに

とりあえずP4触ってみました。
Dataplaneを触るのは難しいですね。想定どおりに動かないときのトラシューは非常に厳しそうに感じました。
(というより決められたステップ以外は素人には手を出しようがないですね。。。)
他にも様々なTutorialのシナリオがありましたが、時間があるときにチャレンジしてみようと思います。
ご覧いただきありがとうございました。


## 免責事項

本サイトおよび対応するコメントにおいて表明される意見は、投稿者本人の個人的意見であり、シスコの意見ではありません。本サイトの内容は、情報の提供のみを目的として掲載されており、シスコや他の関係者による推奨や表明を目的としたものではありません。各利用者は、本Webサイトへの掲載により、投稿、リンクその他の方法でアップロードした全ての情報の内容に対して全責任を負い、本Web サイトの利用に関するあらゆる責任からシスコを免責することに同意したものとします。