RAREのP4コードのレビュー その一

最初に

ここでは、Barefoot TofinoのハードウェアP4スイッチに対して、まとまったP4プログラムが公開されているRAREを題材にして、P4プログラムの全体の流れを説明します。

https://bitbucket.software.geant.org/projects/rare/repos/rare/browse

本記事ではP4のソースコードの読み方、プログラムのtipsについて概要を説明します。その上で、次回はIPv4ルーティングなど、機能観点でRAREのプログラムの実装を解説していきたいと思います。ご質問ありましたら日本P4ユーザ会のSlackに投稿いただけると有難いです。なお、RAREをBarefoot Tofinoシミュレータで動作させた記事もありますのでご参照ください。

https://www.apresiatac.jp/blog/202102043810/

P4プログラム全体の流れ

RAREのP4プログラムは複数のファイルに分かれていますが、bf_router.p4から全てのP4ファイルをインクルードし、実行する形を取っています。ですので、まずはbf_router.p4から読み始めます。このソースコードの、まず一番最後の以下を確認します。

 93   Pipeline(
 94       ig_prs_main(),
 95       ig_ctl(),
 96       ig_ctl_dprs(),
 97       eg_prs_main(),
 98       eg_ctl(),
 99       eg_ctl_dprs()
100   ) pipe;
101   Switch(pipe) main;

※この記事では左側にオリジナルのコードの行番号を記載しています。参照しているソースコードは以下のコミットです。

https://bitbucket.software.geant.org/projects/RARE/repos/rare/commits/f7d046d7595987cc0290dd3865eabcaf06da9226

mainがこのP4プログラムの入口になりますが、そこで指定されているpipeは、上記の通り、以下の6つの処理を実行することが明記されています。

  • ig_prs_main()  
    • Ingress方向のparser(パケットのヘッダ部分の字句解析)
  • ig_ctl()
    • Ingress方向のP4テーブルによるパケット制御 (Match/Action)
  • ig_ctl_dprs()
    • Ingress方向のdeparser(制御結果からパケットデータを再生成)
  • eg_prs_main()
    • Egress方向のparser(パケットのヘッダ部分の字句解析)
  • eg_ctl()
    • Egress方向のP4テーブルによるパケット制御 (Match/Action)
  • eg_ctl_dprs()
    • Egress方向のdeparser(制御結果からパケットデータを再生成)

IngressとEgressに処理が分かれている一つのメリットは、ブロードキャストおよびマルチキャストのパケット制御を考慮できる点があります。まずIngressの処理にて、パケットのヘッダを変更しながら、出力先ポートを決定、あるいはブロードキャスト・マルチキャストドメインを決定します。Ingressの処理にてブロードキャスト、マルチキャストと判断された場合は、決められたブロードキャストドメイン内にパケットを複製し、複製されたパケット毎にEgressの処理を実行することができます。そのため、例えばブロードキャストフレームに対して、VLANヘッダの付与や任意のカプセル化の適用などの制御を、出力先ポート毎に変更することが可能になります(P4の前進技術であるOpenFlowでは難しかった点)。ですので、Ingress側では出力先ポートを決定することが重要な役割の一つになります。また、出力先のポートによってパケットの制御が変わるものについては、Egress側で処理を実行する必要があります。このIngress処理、Egress処理の考え方については、以下のチュートリアル資料のP.10を参照ください。

https://opennetworking.org/wp-content/uploads/2021/05/2021-P4-WS-Vladimir-Gurevich-Slides.pdf

上記の通り、IngressとEgressにて大きく二つに処理が分かれることを念頭に、次はファイルの先頭からソースコードを読みます。

16    #include <core.p4>
17    #include <tna.p4>

上記は使用するP4モデルが定義されたヘッダファイルを読み込んでいます。”tna”は、Tofino Native Architectureの略です。このヘッダをインクルードしていることから、このP4プログラムはTNAモデルをサポートしているハードウェアであれば動作することが保証されます。このTNAモデルのヘッダファイルは以下で公開されていますので、ハードウェアが付与するmeta dataの定義を把握することができます。

https://github.com/barefootnetworks/Open-Tofino/tree/master/share/p4c/p4include

他のP4モデルとしては、標準で公開されているものとして、v1model、PSA等があります。

https://github.com/p4lang/p4c/blob/main/p4include/v1model.p4
https://p4lang.github.io/p4-spec/docs/PSA-v1.1.0.html

以下は、パケットのヘッダを解析するための定義をインクルードしています。キーワードだけを見ると、標準的なL2/L3/L4のプロトコルの他に、MPLS、VXLAN、L2TP、GREなどのカプセル化プロトコルなども見えます。

25    #include "include/hdr_cpu.p4"
26    #include "include/hdr_bier.p4"
27    #include "include/hdr_internal.p4"
28    #include "include/hdr_ethernet.p4"
29    #include "include/hdr_arp.p4"
30    #include "include/hdr_llc.p4"
31    #include "include/hdr_vlan.p4"
32    #include "include/hdr_mpls.p4"
33    #include "include/hdr_ipv4.p4"
34    #include "include/hdr_ipv6.p4"
35    #include "include/hdr_tcp.p4"
36    #include "include/hdr_udp.p4"
37    #include "include/hdr_gre.p4"
38    #include "include/hdr_pppoe.p4"
39    #include "include/hdr_l2tp.p4"
40    #include "include/hdr_vxlan.p4"

上記の定義を使って、以下にて、本P4プログラムで使用するheaderとmeta dataの定義を読み込みます。

44    /*------------------ I N G R E S S  H E A D E R S --------------------------- */
45    #include "include/hdr_ig_headers.p4"

46    /*------------------ I N G R E S S  G L O B A L  M E T A D A T A ------------ */
47    #include "include/mtd_ig_metadata.p4"

headerは、パケットデータの中でP4プログラムにて参照、書換などの制御するヘッダフィールドです。meta dataはP4プログラム内で使用可能な任意のデータで、Barefoot Tofinoなどのハードウェアに依存して予め定義されたものと、プログラム作成者が任意で定義が可能なものが存在します。これらheaderとmeta dataの定義を使って、以下にてparserの処理を実行するプログラムを読み込んでいます。(parserの詳細は後述)

48    /*------------------ I N G R E S S   P A R S E R -----------------------------*/
49    #include "include/ig_prs_main.p4"

次に、各機能に対するP4テーブル(Match/Actionテーブル)の定義をインクルードしています。

50    /*------------------ I N G R E S S - M A T C H - A C T I O N ---------------- */
51    #include "include/hsh_ipv4_ipv6_hash.p4"

52    #include "include/ig_ctl_bundle.p4"
53    #include "include/ig_ctl_pkt_pre_emit.p4"
54    #include "include/ig_ctl_vlan_in.p4"
55    #include "include/ig_ctl_acl_in.p4"
56    #include "include/ig_ctl_acl_out.p4"
57    #include "include/ig_ctl_vrf.p4"
58    #include "include/ig_ctl_bridge.p4"
59    #include "include/ig_ctl_mpls.p4"
60    #include "include/ig_ctl_ipv4.p4"
61    #include "include/ig_ctl_ipv6.p4"
62    #include "include/ig_ctl_ipv4b.p4"
63    #include "include/ig_ctl_ipv6b.p4"
64    #include "include/ig_ctl_nat.p4"
65    #include "include/ig_ctl_pbr.p4"
66    #include "include/ig_ctl_qos_in.p4"
67    #include "include/ig_ctl_qos_out.p4"
68    #include "include/ig_ctl_mcast.p4"
69    #include "include/ig_ctl_flowspec.p4"
70    #include "include/ig_ctl_tunnel.p4"
71    #include "include/ig_ctl_pppoe.p4"
72    #include "include/ig_ctl_copp.p4"
73    #include "include/ig_ctl_outport.p4"

上記の定義を元に、以下にてP4のMatch/Actionを実行する処理を読み込んでいます(詳細は後述)。

74    #include "include/ig_ctl.p4"

上記のMatch/Actionの実行結果をもとに、以下のdeparserの処理によって、最終的にパケットを再生成します(詳細は後述)。

76    #include "include/ig_ctl_dprs.p4"

以上がIngress処理の全体となります。上記のIgress deparserによってIngress側で一度パケットを生成し、それをEgress側にて再び受信したかのように以下の処理を順番に実行します。

  • parser
    #include "include/eg_prs_main.p4"
  • match & action
    #include "include/eg_ctl.p4"
  • deparser
    #include "include/eg_ctl_dprs.p4"

以上が、P4プログラムの全体像を俯瞰した流れの説明です。ここからは、各処理について詳細にP4プログラムを読み込んでいきます。

header定義の説明

本プログラムのheaderの定義はhdr_ig_headers.p4に記載されていますが、以下に抜粋します。

18    struct headers {
19        internal_header_t internal;
20        cpu_header_t cpu;
21        ethernet_t ethernet;
22        vlan_t vlan;
23    #ifdef HAVE_PPPOE
24        pppoe_t pppoeC;
25        pppoe_t pppoeD;
26    #endif
27    #ifdef HAVE_TAP
28        ethernet_t eth6;
29    #endif
30    #ifdef HAVE_TUN
31        ipv4_t ipv4d;
32        ipv6_t ipv6d;
33    #endif
34    #ifdef HAVE_GRE
35        gre_t gre2;
36    #endif
37    #ifdef NEED_UDP2
38        udp_t udp2;
39    #endif
40    #ifdef HAVE_L2TP
41        l2tp_t l2tp2;
42    #endif
43    #ifdef HAVE_VXLAN
44        vxlan_t vxlan2;
45    #endif
46    #ifdef HAVE_MPLS
47        mpls_t mpls0;
48        mpls_t mpls1;
49    #endif
50    #ifdef HAVE_MPLS
51    #ifdef HAVE_BIER
52        bier_t bier;
53    #endif
54    #endif
55        ethernet_t eth2;
56        arp_t arp;
57        llc_t llc;
58        ipv4_t ipv4;
59        ipv6_t ipv6;
60    #ifdef HAVE_SRV6
61        ipv4_t ipv4b;
62        ipv6_t ipv6b;
63    #endif
64    #ifdef HAVE_GRE
65        gre_t gre;
66    #endif
67        tcp_t tcp;
68        udp_t udp;
69    #ifdef HAVE_L2TP
70        l2tp_t l2tp;
71    #endif
72    #ifdef HAVE_VXLAN
73        vxlan_t vxlan;
74    #endif
75    }

headerは、前述の通り、パケットデータの中でP4プログラムにて参照、書換を行うヘッダフィールドの定義でした。上記を見ると、ethernet、vlanなど、パケットの先頭から順番に関係するプロトコルのヘッダが定義されています。注意として以下の二点があります。

  • header定義の先頭に定義されているcpu, internalの特殊ヘッダ
    • cpuヘッダは、コントロールプレーンがTofinoの物理ポートからパケットを送信する際にP4プログラム内で参照する内部データです。このcpuヘッダは最終的に物理ポートからパケットが送信される際には取り除かれているため、装置の外でパケットをキャプチャしても見ることができないデータです。
    • internalヘッダは、Ingressにて処理した内容をEgressに伝えるために装置内部のみで使用されるヘッダです。具体的に言えば、Ingressのdeparserにてinternalヘッダをパケットに付与し、Egress parserにてinternalヘッダを取り込むことで、Ingressの判断結果をEgressのMatch/Actionの処理に伝えることができます。
  • ifdefによる使用プロトコルの選択
    • ifdefはc言語と使い方は同じです。例えば、HAVE_VXLANをdefineしておいてからコンパイルすると以下の行はコンパイル対象になります。逆に、HAVE_VXLANをdefineしていなければ、コンパイラは以下の行を無視します。こうすることで、一つのソースコードに複数の機能をプログラミングしておきながら、ビルドする際に使用する機能を選択することが可能になります(ifdefは好き嫌いが分かれると思いますが、ifdef以外にも方法はあります)。
72    #ifdef HAVE_VXLAN
73        vxlan_t vxlan;
74    #endif

Parserの説明

Ingress Parserのmain関数であるig_prs_main()は、ig_prs_main.p4ファイルに定義されています。この関数の最初の部分を、ヘッダインクルードを削除し、整形すると以下の形になります。

18    parser ig_prs_main(packet_in pkt,
19                       /* User */
20                       out headers hdr, out ingress_metadata_t ig_md,
21                       /* Intrinsic */
22                       out ingress_intrinsic_metadata_t ig_intr_md)
23    {
25        state start {
26            pkt.extract(ig_intr_md);
27            pkt.advance(PORT_METADATA_SIZE);
29            transition meta_init1;
30        }

Parserはstartの処理(状態)から開始します。startにて最初にpkt.extractされているig_intr_mdは、Barefoot Tofinoが付与するmeta dataで、この中に受信ポートや受信時刻(タイムスタンプ)等の情報が格納されています。その後のport metadataもTofinoが付与するmeta dataですが、本P4プログラムではこのmeta dataを使用しないため、pkt.advance()によってport metadataを取り込まず、ポインタだけを先に進めています。次のparseの処理(状態)として、以下のmeta_init1に遷移します。

31        state meta_init1 {
32            transition select(ig_intr_md.ingress_port) {
33                CPU_PORT:
34                    prs_cpu;
35                RECIR_PORT:
36                    prs_recir;
37                default:
38                    meta_init2;
39            }
40        }

ここで、Tofinoが付与した受信ポート番号によって遷移先を分岐(transition select)しています。Tofinoは物理ポート以外にも、以下の二つの特別な意味を持ったポートがあります。

  • CPU_PORT
    • メインCPUからパケットを送受信するポートです。コントロールプレーンから物理ポートを介してパケットを送信する際、P4スイッチはCPU_PORTからパケットを受信します。このCPU_PORTから受信するパケットには、コントロールプレーンが付与した先述のcpuヘッダがあります。このcpuヘッダを見てP4プログラムにて必要な処理をした上で、コントロールプレーンから指示されたポートからパケットを送信します。
  • RECIR_PORT
    • Tofinoでは、Parser -> Match/Action -> Deparserの一連の処理を実行した後に、再度先頭のParserから処理を実行(Recirculation)することができます。Recirculationされて再びParserにパケットが到達したときは、受信ポートがRECIR_PORTになります。

受信ポートが、上記の特殊なポートでは無く通常の物理ポートだった場合は、以下のmata_init2の処理(状態)に遷移します。

41        state meta_init2 {
42            ig_md.ingress_id = (SubIntId_t)ig_intr_md.ingress_port;
43            transition select(ig_intr_md.resubmit_flag) {
44                1w1:
45                    prs_resub;
46                default:
47                    prs_ethernet;
48            }
49        }

ここでも、transition selectによって遷移先を変更しています。Tofinoが付与するmeta dataの中に、resubmit_flagがありますが、この値によって遷移先が分岐します。このresubmitは、recirculationと同様に、一度パイプライン処理を実行してから、再度parserから処理を実行する機能になります。resubmitの場合は、もともと受信した物理ポートが受信ポートとして保持される点が違いです。

上記の特殊なヘッダを処理してから、prs_ethernetの処理に遷移します。ここからはEhternetヘッダから順番にパケットのヘッダを字句解析しますが、これらの処理はig_prs_hdr.p4に記載されています。ここは、v1modelなどのソフトスイッチ向けのP4モデルと、実装方法に大きな違いはありませんので、説明は割愛します。

Match/Actionの説明

ここでは、IngressのMatch/Actionのソースコードの流れを確認します。Match/Actionの処理のmain関数は、ig_ctlでしたが、これはig_ctl.p4に記載されています。ここでは、あえて、全ての機能がdefineされたとして、ifdefを削除すると以下のプログラムになります。

 18    control ig_ctl(inout headers hdr, inout ingress_metadata_t ig_md,
 19                   in ingress_intrinsic_metadata_t ig_intr_md,
 20                   in ingress_intrinsic_metadata_from_parser_t ig_prsr_md,
 21                   inout ingress_intrinsic_metadata_for_deparser_t ig_dprsr_md,
 22                   inout ingress_intrinsic_metadata_for_tm_t ig_tm_md)
 23    {

 38        IngressControlBundle() ig_ctl_bundle;
 40        IngressControlMPLS()ig_ctl_mpls;
 43        IngressControlPPPOE() ig_ctl_pppoe;
 45        IngressControlPktPreEmit()ig_ctl_pkt_pre_emit;
 47        IngressControlBridge()ig_ctl_bridge;
 49        IngressControlIPv4()ig_ctl_ipv4;
 50        IngressControlIPv6()ig_ctl_ipv6;
 52        IngressControlIPv4b() ig_ctl_ipv4b;
 53        IngressControlIPv6b() ig_ctl_ipv6b;
 56        IngressControlTunnel() ig_ctl_tunnel;
 59        IngressControlCoPP()ig_ctl_copp;
 62        IngressControlAclIn() ig_ctl_acl_in;
 64        IngressControlVlanIn()ig_ctl_vlan_in;
 65        IngressControlVRF()ig_ctl_vrf;
 67        IngressControlNAT() ig_ctl_nat;
 70        IngressControlPBR() ig_ctl_pbr;
 73        IngressControlQosIn() ig_ctl_qos_in;
 76        IngressControlFlowspec() ig_ctl_flowspec;
 79        IngressControlAclOut() ig_ctl_acl_out;
 82        IngressControlQosOut() ig_ctl_qos_out;
 85        IngressControlMcast() ig_ctl_mcast;
 87        IngressControlOutPort() ig_ctl_outport;

 88        Counter< bit<64>, SubIntId_t> ((MAX_PORT+1), CounterType_t.PACKETS_AND_BYTES) pkt_out_stats;

 89        apply {
 90            ig_dprsr_md.drop_ctl = 0; // hack for odd/even ports
 92            if (hdr.ipv6.isValid()) ig_md.pktlen = hdr.ipv6.payload_len + 40;
 94            if (hdr.mpls1.isValid()) ig_md.pktlen = ig_md.pktlen + 8;
 95            else if (hdr.mpls0.isValid()) ig_md.pktlen = ig_md.pktlen + 4;
 98            ig_ctl_vlan_in.apply(hdr, ig_md, ig_intr_md);
 99            if (ig_intr_md.ingress_port == CPU_PORT) {
100	           pkt_out_stats.count(ig_md.source_id);
101	           ig_tm_md.ucast_egress_port = (PortId_t)hdr.cpu.port;
102	           ig_tm_md.bypass_egress = 1;
103	           hdr.cpu.setInvalid();
104	       } else {
105	           if (ig_intr_md.ingress_port == RECIR_PORT) {
106	               hdr.cpu.setInvalid();
107	           }
109	           ig_ctl_pppoe.apply(hdr,ig_md,ig_intr_md, ig_dprsr_md, ig_tm_md);
112	           ig_ctl_acl_in.apply(hdr, ig_md, ig_intr_md, ig_dprsr_md, ig_tm_md);
115	           ig_ctl_qos_in.apply(hdr, ig_md, ig_intr_md, ig_dprsr_md, ig_tm_md);
117	           ig_ctl_vrf.apply(hdr, ig_md);
119	           ig_ctl_mpls.apply(hdr, ig_md, ig_intr_md, ig_dprsr_md, ig_tm_md);
122	           ig_ctl_flowspec.apply(hdr, ig_md, ig_intr_md, ig_dprsr_md, ig_tm_md);
125	           ig_ctl_nat.apply(hdr,ig_md,ig_intr_md);
128	           ig_ctl_pbr.apply(hdr,ig_md,ig_intr_md);
131	           ig_ctl_bridge.apply(hdr, ig_md, ig_intr_md);
133	           if (ig_md.ipv4_valid == 1) {
134	               ig_ctl_ipv4.apply(hdr, ig_md, ig_intr_md, ig_dprsr_md, ig_tm_md);
135	           } else if (ig_md.ipv6_valid == 1) {
136	               ig_ctl_ipv6.apply(hdr, ig_md, ig_intr_md, ig_dprsr_md, ig_tm_md);
137	           } else if (ig_md.arp_valid == 1) {
138	               ig_md.nexthop_id = CPU_PORT;
139	           }
141	           if (ig_md.srv_op_type == 4) {
142	               ig_ctl_ipv4b.apply(hdr, ig_md, ig_intr_md, ig_dprsr_md, ig_tm_md);
143	           } else if (ig_md.srv_op_type == 6) {
144	               ig_ctl_ipv6b.apply(hdr, ig_md, ig_intr_md, ig_dprsr_md, ig_tm_md);
145	           }
147	           ig_ctl_pkt_pre_emit.apply(hdr, ig_md, ig_intr_md, ig_tm_md);
       
148	           if (ig_md.nexthop_id == CPU_PORT) {
150	               ig_ctl_tunnel.apply(hdr,ig_md,ig_intr_md, ig_dprsr_md, ig_tm_md);
153	               ig_ctl_mcast.apply(hdr,ig_md,ig_intr_md, ig_dprsr_md, ig_tm_md);
156	               ig_ctl_copp.apply(hdr, ig_md, ig_intr_md, ig_dprsr_md, ig_tm_md);
158	           } else {
159	               if (hdr.vlan.isValid()) hdr.vlan.setInvalid();
161	               if (hdr.pppoeD.isValid()) hdr.pppoeD.setInvalid();
163	               hdr.ethernet.ethertype = ig_md.ethertype;
164	               ig_ctl_outport.apply(hdr, ig_md, ig_dprsr_md, ig_tm_md);
166	               ig_ctl_acl_out.apply(hdr, ig_md, ig_intr_md, ig_dprsr_md, ig_tm_md);
169	               ig_ctl_qos_out.apply(hdr, ig_md, ig_intr_md, ig_dprsr_md, ig_tm_md);
171	               ig_ctl_bundle.apply(hdr, ig_md, ig_dprsr_md, ig_tm_md);
172	           }
173	       }
174	   }
176    }

順番にP4プログラムを読んでいきます。

18    control ig_ctl(inout headers hdr, inout ingress_metadata_t ig_md,
19                   in ingress_intrinsic_metadata_t ig_intr_md,
20                   in ingress_intrinsic_metadata_from_parser_t ig_prsr_md,
21                   inout ingress_intrinsic_metadata_for_deparser_t ig_dprsr_md,
22                   inout ingress_intrinsic_metadata_for_tm_t ig_tm_md)

ig_ctl()の引数には、header(hdr)、ユーザが定義したmetadata(ig_md)の他に、Tofinoのハードウェアが使用するmeta dataが指定されます。引数に記載されている”in”はread onlyで書換を行わないデータであることを、”inout”は読み書きの両方が行われるデータであることを示します。inoutのmeta dataの中で、ig_dprsr_mdはdeparserの際に参照するデータ、ig_tm_mdはTraffic manager(Ingress処理されたパケットをEgressに転送するハードウェア)に伝えるデータになります。Traffic managerは、マルチキャストあるいはブロードキャストの場合にパケットを複製する役割があるため、それらの指示をig_tm_mdに含めておく必要があります。

38        IngressControlBundle() ig_ctl_bundle;
40        IngressControlMPLS()ig_ctl_mpls;
43        IngressControlPPPOE() ig_ctl_pppoe;
45        IngressControlPktPreEmit()ig_ctl_pkt_pre_emit;
47        IngressControlBridge()ig_ctl_bridge;
 (割愛)

上記のコードは、他のp4ファイルにて定義されたコントロール(Match/Actionテーブルやapply文のP4のコードが一式にまとまったもの)を、このP4プログラムにて使用するための宣言です。例えば、IngressControlBundleはig_ctl_bundle.p4に定義されていて、このP4プログラム内では、ig_ctl_bundleという名前で実行するということが宣言されています。その上で、以下のapply文にて順番にMatch/Actionを実行します。

 89        apply {
 90            ig_dprsr_md.drop_ctl = 0; // hack for odd/even ports
 92            if (hdr.ipv6.isValid()) ig_md.pktlen = hdr.ipv6.payload_len + 40;
 94            if (hdr.mpls1.isValid()) ig_md.pktlen = ig_md.pktlen + 8;
 95            else if (hdr.mpls0.isValid()) ig_md.pktlen = ig_md.pktlen + 4;
 98            ig_ctl_vlan_in.apply(hdr, ig_md, ig_intr_md);
 99            if (ig_intr_md.ingress_port == CPU_PORT) {
100	            pkt_out_stats.count(ig_md.source_id);
101	            ig_tm_md.ucast_egress_port = (PortId_t)hdr.cpu.port;
102	            ig_tm_md.bypass_egress = 1;
103	            hdr.cpu.setInvalid();
104	       } else {
105	           if (ig_intr_md.ingress_port == RECIR_PORT) {
106	               hdr.cpu.setInvalid();
107	           }
109	           ig_ctl_pppoe.apply(hdr,ig_md,ig_intr_md, ig_dprsr_md, ig_tm_md);
112	           ig_ctl_acl_in.apply(hdr, ig_md, ig_intr_md, ig_dprsr_md, ig_tm_md);
115	           ig_ctl_qos_in.apply(hdr, ig_md, ig_intr_md, ig_dprsr_md, ig_tm_md);
117	           ig_ctl_vrf.apply(hdr, ig_md);
  (割愛)

applyの中では上から順番に各処理を実行し、if文などにて必要に応じて条件分岐を行います。このapply文の中で、直接headerやmeta dataを書換もできますが、ig_ctl_vlan_in.apply()のように、他P4ファイルで定義したP4プログラムの中でもheaderやmeta dataの書換が行われています。個々のP4ファイルに詳細なプログラムが記載されていますが、少なくともIngressのMatch/Actionの処理の全体は、このig_ctlに集約されています。

個々のP4テーブルの定義は複雑であり全てを解説することは難しいため、本記事では説明を割愛します。次回の記事にて、基本的なIPv4のルーティングについて、これらのP4テーブルがどのように実行されているかを解説する予定です。

上記のapply文を全て実行することで、パケットに対してheaderおよびmeta dataの更新が完了します。ここで更新されたデータに従い、次のdeparserにてパケットを再生成します。

Deparser

Ingress Deparserのmain関数であるig_ctl_dprsは、ig_ctl_dprs.p4にて定義されています。NATやTUNなどのifdefの文を削除すると、deparser自体は以下のソースコードになります。

18    control ig_ctl_dprs(packet_out pkt, inout headers hdr, in ingress_metadata_t ig_md,
19                        in ingress_intrinsic_metadata_for_deparser_t ig_dprsr_md)
20    {

21        Checksum()ipv4_checksum;

26        apply {
27            if (ig_md.saw_rsvp == 0) {
28                hdr.ipv4.hdr_checksum = ipv4_checksum.update( {
29                    hdr.ipv4.version, hdr.ipv4.ihl,
30                    hdr.ipv4.diffserv,
31                    hdr.ipv4.total_len,
32                    hdr.ipv4.identification,
33                    hdr.ipv4.flags,
34                    hdr.ipv4.frag_offset,
35                    hdr.ipv4.ttl,
36                    hdr.ipv4.protocol,
37                    hdr.ipv4.src_addr,
38                    hdr.ipv4.dst_addr
39                } );
40            }

90            pkt.emit(hdr);
91        }
92    }

deparserでは必要に応じてchecksumの再計算を実施します。その後、pkt.emitを実行することで、hdrの中で有効になっているheaderがパケットに付与されて、Traffic managerへ転送されます。その後、Traffic managerからEgress方向のpipelineにパケットが転送され、Egress側でも、Parser -> Match/Actionテーブル -> Deparserと、一連の処理が実行され、最終的にTofinoの物理ポートの外へパケットが送出されます。

まとめ

以上、RAREのP4プログラムのソースコードを俯瞰的に読んでみました。基本的なプログラミングの流れは、TNAモデルも標準的なv1modelやPSAも、それほど大きな違いはありません。ただ、Tofino向けにはハードウェアと連携するために定義されたmeta dataを意識する必要があります。また、RAREは、MPLS、VXLANなど様々なプロトコルが実装されている関係で、P4のコードを機能ごとにファイルに分け、ifdefなどによってビルド対象のプロトコルを選択できるような工夫がされていました。

今回は、P4プログラムの流れを俯瞰的に説明しましたが、実際に本プログラムを動作させるためにはP4のプログラムだけではなく、コントロールプレーンとの連携も必要です。次回は、どのようにコントロールプレーンとP4テーブルが連携することで、ルーティングなどの機能が実現されているのか説明する予定です。

コメントを残す