Unity 1週間ゲームジャム お題「かわる」に参加しました

はじめに

ここのところ、趣味で友人とゲーム制作を進めており、Unityの勉強をしています。

その勉強の一環で、Unity 1週間ゲームジャムというゲームイベントに2回参加しました。

Unity 1週間ゲームジャム お題「かわる」 | フリーゲーム投稿サイト unityroom

ゲーム制作初心者の自分ですが、本イベントに出た経験は得難いものがあり、同じくゲーム制作初心者で興味のある人に少しでもその魅力が伝わればと思い、本記事を書くに至りました。

Unity 1週間ゲームジャムとは?

Unityを使い、お題に沿って1週間でゲームを作るというイベントです。

詳細はこちらのページにありますが、お題が月曜日の0時に出題され、1週間後の次の日曜日の20時までにゲームを作り上げるというものです。

unityroom.com

なお、イベントの雰囲気自体はけっこう緩めで、評価期間の間であれば多少は遅刻しても問題はないので、その緩さも魅力だと思います。

評価の項目は「楽しさ」「絵作り」「サウンド」「操作性」「雰囲気」「斬新さ」で、どこか一つでも尖っているだけでも評価のされやすい軸だと思います(優れたゲームはどの項目でも上位になりますが……)

個人的に面白いなと思ったのは、他の人のゲームをプレイすればするほど、他の人に見つけられやすくなるというシステムです。

これによって他の人のゲームを遊ぶインセンティブができて、投稿するだけ投稿してあとは評価を待つというよりも、もっと和気あいあいとした雰囲気になっているように感じます。

あとは開発者視点の人が多いからか、コメントもけっこう優しめで、そこも初心者的にはグッドポイントだと思いました。

お題「かわる」に参加しての自分語り

参加までの経緯

Unityでゲーム制作を進めていたところ、友人から本イベントのことを聞きました。

1週間でゲームを作れるのかな?と思い、勇気は出ていませんでした。しかし、「遅刻しても投稿OK」という文言を見て「ハードルは低めなのかな?」と参加することにしました。

最初に参加したのは「1ボタン」というお題の回で、次のようなゲームを作りました。3色のブロックがあり、同じ色のブロックを3つ重ねると消える落ちものパズルゲームです。レーンが2軸あり、ボタンでどちらの軸にブロックを落とすかを切り替えます。

unityroom.com

なんとか完成させられたものの、特殊なブロックを用意するなどしてもう少しゲーム性を上げられたかなぁと悔しくなったのを覚えています。また、ちょうど流行り病にかかり、投稿にめちゃめちゃ遅刻してしまいました。

ただ、無理やりにでもゲームを完成させるゴールまでたどり着けたことは、何物にも代えがたい経験でした。

ゲーム制作初心者は身の丈に合わない規模のゲームに取り掛かりがちという話をよく聞きますが、例に漏れず自分もその感じでした。初めてゲームを完成させたことで、「ゲームって完成するんだ」というそもそもの驚きと、それを実際にプレイして評価をいただけた嬉しさを感じ、改めて時間があれば参加したいと思っていました。

作成したゲーム

作成したのはノベルゲームで、「視点が切り替わることで、心の声が見える」というコンセプトで作成しました。

ゲームのゴールとしては、両片思いの女子と男子の告白を成功させるというものです。しかし、お互い素直ではなく言葉だけではすれ違ってしまうので、プレイヤーは各登場人物の気持ちを知って、どういう意図で言ったのか、どういう背景がありそうなのかを会話劇を見て推測するというような流れです。

心の声とセリフが同時にテキストで文字送りされると結構面白いんじゃないかなと思い作りました。

unityroom.com

発想の経緯

「かわる」というお題を見ていた時、ちょうど恋愛漫画を読んでいました。

ほとんどの恋愛漫画は、男の子視点と、女の子視点でそれぞれモノローグが入ります。お互いが何を考えているのか、読者は分かっていてハラハラドキドキするわけですが、当然、登場人物同士は分かりません。

恋愛漫画のモノローグが、口には出せない複雑な感情のやり取りを生んで面白いのですが、それが実際のセリフとして出てくることはまれで、常々「登場人物はたぶんこの気持ちを全く知らずに、この人と接しているのだろうな。本来の人間性はどこまで伝わっているのだろうか」と考えていました。

そうした考えが頭の中にあり、あと今回はノベルゲームを作るということを決めていたので、お題を見てすぐに「視点が変わると心の声が見えるノベルゲームがいいのでは?」とアイディアを固めました。

今回の狙い

2回目に参加して自分の中で目的にしていたのは「とにかく1週間以内に完成させたい」ということと、「絵作りをもう少し頑張りたい」でした。

そこで策としてノベルゲームを作ることにしました。ノベルゲームは基本的に文字送り機能さえあればゲームになると思い、ゲーム部分を前半に作ってしまい、後半は雰囲気づくりに時間を掛けられると思ったのです。

なお、そうやって意気込んだのはいいものの、結局1日遅刻してしまいました。というのも……。

策施策に溺れる

結果で言うと、ノベルゲームを作るという作戦はうまくいきませんでした。

前述の考えでゲームシステムを作っていき、挙動が変なところがいくつかありますが、3日でほぼ実装が終わっていました。

しかし、一番大事な点を忘れていました。

1週間でシナリオ系のゲームを作るには、

1週間でシナリオを書かないといけないのです。

しかも、シナリオの面白さがそのままゲームの面白さに繋がってしまうのです。当たり前すぎる話なのですが、ぼけーっと生きているので普通に頭から抜けていました。

昔からシナリオ関係は暖簾に腕押しという感じで頭を抱えました。とはいえ、人間締め切りがあると何とかやり切れるもので、どうにかこうにかシナリオは準備できました。が、完成した時点ですでに2時間ほど遅刻しており、あまり調整ができなかった印象で、もう少し練れたよなぁ~と悔しい結果に終わりました。

また、シナリオというテキストベースの制作物を一人で確認するのはなかなか骨が折れました。シナリオを製作者の目のみで点検していくのはけっこう厳しく、1週間でやるならシナリオ担当の人と手分けしないと無理だなあと思いました。

雰囲気づくりの工夫

以前のゲームは絵作り部分に力が入っておらず、実際に評価値も低かったので、今回はもう少し雰囲気づくりを頑張ろうと思いました。

とはいえ、自分で絵を用意できる画力もなく、また立ち絵を切り替える必要もあるので、工数がかかりそうでどうしたものかなぁと思っていました。

色々と考えた中で、キャラをシルエットにしてしまえば色々と誤魔化せるのではと打算的な考えに行き着きました。シルエットであれば立ち絵が切り替わらなくてもそこまで違和感もないと思ったのです。そこで、素敵なシルエットの素材をお借りしまして、全体的にツートーンカラーなデザインに仕上げることにしました。

wataokiba.net

wataokiba.net

一点難しかったのは、ゲーム雰囲気は軽いのを目指していたのですが、シルエットかつノベルゲームということでどうしても「かまいたちの夜」を彷彿とさせて、どう配置を頑張ってもホラーっぽい雰囲気が拭えなかったところです。最終的に友人の助言も得て、明るめの色に調整して、あまり重く見えないようにしました。

もう一つ、「日常物」を扱うにあたって、BGMとしてどういうものが適しているのかという調整にかなり時間がかかりました。いわゆる日常系のBGMだと明るすぎる、恋愛系のBGMだと切なすぎる、ミステリー系のBGMだと耽美すぎ、ジャズやボサノヴァっぽい音楽を当ててみても大人っぽすぎ、アンビエント風にすると音が無さすぎる、、、、という感じで「日常」という音楽が分からなくなりました。今度からは日常系のゲームのBGMをよく聞いてみようと思います。

開発者としての感想

ゲームっぽい雰囲気づくりは出せたのかなと自負していて、自分としてはかなりお気に入りの作品でした。ただ作者の贔屓目を抜きにすると、肝心の「面白さ」はあまり提供できていなかったように思います。アイディアは面白そうだっただけに、着地が少しうまくいかなかったのが残念でした。

ありがたいことにプレイ動画を上げている方がいて、少し見させていただいたのですが、ヒントが少なすぎてとりあえず全パターン試してみるという方式が攻略しやすい状況になっていて、あんまりゲームとしてやっている意味がなさそうかなと思いました。

逆に、自分でも手応えを感じたのは、周回をやる価値が生まれているゲームシステムであることでした。通常のゲームでは、同じルートに何度もたどり着く意味は無いのですが、このゲームでは同じルートでも、どの視点にいるかによって印象が変わりうるので、そういった部分にはうまく機能していたかなと思いました。

総じて、ゲームとしての面白さはあまり出せなかったものの、ゲームシステムはもう少し磨けそうという自己認識でした。他のゲームにも流用できそうなので、このシステムをもう少し活かせそうなゲーム性を今後検討しようと思います。

このあたりはゲームをたくさん作って、プレイしてを重ねないとなかなか身に付かないところとは思うので、頑張ってみたいなと思います。

やっておいた方がよかったこと

続いて、2回参加して、初心者の人がやっておくべきだなと思う点がありましたので、そちらを紹介します。

サムネイルをGif画像にする

最初はタイトル画面を正方形にした画像を作ってサムネイルにしていたのですが、どうもほかの作品を見ているとサムネイルが動いており、確かに自分もそういうゲームから先に遊んでいたと思います。

やはりサムネイルをプレイ動画にしてしまった方が、一目でどんなゲームなのかがわかりやすく興味も持ちやすいので、Gif画像にしてしまった方がいいように思いました。

実際、サムネイルをプレイ動画に変えたとたん、ページ閲覧数が目に見えて伸びて、この工夫は間違いなく必要だなと感じました。

色々探していて良かったアプリケーションがScreenToGifでした。操作も分かりやすく、データサイズも簡単に小さくできて、サムネイルを作るには最適だと感じました。

www.screentogif.com

難易度調整(特にノベルゲーム)

Unity 1週間ゲームジャムに投稿されたゲームをプレイさせていただいて、けっこう難易度が高めのゲームが多い印象でした。狙って高く設定する方も多いと思うのですが、おそらくは自分の経験も踏まえると、難易度調整に割ける時間が少ないのだろうなと感じています。

アクションゲームやパズルゲームは難易度が高くても面白ければプレイが続くので、難易度の高さはそこまで悪い点だとは思われないのでしょうが、ノベルゲームの場合は違うような気がしました。

アクションゲームやパズルゲームと比べてプレイのフィードバックが返ってくるのが遅いので、なかなかクリアできない難易度だと虚無だな~と思わせる時間が多い気がします。よく知らない人のシナリオを読み込んでくれることはあまり期待できないので、ノベルゲームの場合は、自分が思っているよりも、もっと簡単な難易度にした方が良さそうです。

以下言い訳となりますが、自分の狙いとしては、ノベルゲームの選択肢があまりにもわかりやすいと単なる作業になるのではないかという危惧があり、今回のゲームのコンセプトを踏まえて、選択肢の文面ではなくそれを選んだ時の会話とモノローグを見て、あとからその選択肢が合ってそうか外れてそうかを推測させる方向性にしました。

ただ、全部の選択肢をそれで設計してしまったので、はじめの一歩が無い状態になっていました。推測できる範疇を超えている部分があり、もう少し簡単な選択肢にすべきでした。

実際のところ、適切な会話を選んで告白成功に導こう!というゲーム説明を聞いたら、普通はある程度分かりやすい会話パズルのような形になるだろうという話なので、ここは純粋に失敗だったなと思います。

(何ならミステリーを題材にした方が活かせていたかも?と思います)

シナリオ用のアセットはUnity Editor上で編集しない

シナリオのテキストを外部ファイルから読み込むことがWebGLで出来るのかな?と不安でした、それならと思い、string型のListを内部に持つScriptable Objectを作成し、そのアセットをUnity Editor上でいじってシナリオを作ることにしました。

これが本当に失敗で、とにかくシナリオが書きづらかったです。Google Docsで書いていたシナリオをUnity Editor上にコピペしていくという作業の連続で、だるいな~という気持ちになりました。

あとは、Scriptable ObjectのC#スクリプトを変更すると、シナリオデータが全部消えることに絶望しました。構造を変えてみるかとちょっとスクリプトをいじったところアセットデータが初期化され、今まで書いてたシナリオデータがまっさらの状態になっていました。フェイズや選択肢ごとにシナリオを分けていたのでダメージは少なかったんですが、最後の最後までこれに気づかなかったらどうなっていただろうと冷や汗をかきました(知らないだけで、回避できる方法があるのかもしれないのですが)。

.txtファイルやら.csvファイルやらに書いて、アセットに変換するツールがあれば、、、という感じでしたが、おそらく1週間以内にそれをするのは難しいので、あらかじめ用意しておくべきかなと思いました。

WebGLのテキストデータってどうやるのが正解なんでしょう?おすすめを知りたいです。

アセット管理にはResourcesを使う

普段はAddressable Assets Systemを使って開発をしていたので、ここでもその通りやっていったのですが、unityroomにアップロードした時に、どうもアセットが読み込まれていないことに気づきました。

どうやら、unityroomにはWebGLビルドで出来たファイルをアップロードできないようで、あちゃーとなりました。

あまりロード方式に依存しない作りにはなっていなかったのでよくよく見なおしてみたら大した調整は必要なかったのですが、unityroomに投稿するゲームではアセット管理にResourcesを利用するというのを肝に銘じました(ほかにもいい方法があるんでしょうか?)

参加してみての感想

イベントの設計上、いろんなひとにプレイしてもらえるので、フリーゲームの投稿プラットフォームに比べて評価が得やすく、どこが悪かったのかがある程度形に見えるのはよかったです。

結構オブラートに包んでくださるコメントが多く、初心者がとりあえずゲームを何か作りたい場合には、心も折れにくく良いのではないかと思います。

嬉しいことに、ギリギリ「斬新さ」のランキングに載っていまして(4月8日時点で50位でした)、頑張った甲斐があったなと思いました。ランキングは変動するので、もしかしたら抜かれる可能性はありますが、一時でもランキングに載っていたというのは光栄な気持ちです。

しかもプレイ動画を上げている方がいて、めちゃくちゃ驚きました。ゲーム実況していただくということが、まさかこんなにも天にも昇る気持ちだったとはと思いました。

プレイをして面白かったゲーム

仕事もあり多くの数はプレイできなかったのですが、自分がプレイして特に面白かったゲームを紹介させていただき、本記事の締めとさせていただきます。

身代わりスイカ割り

イカの絵柄を見て、どのスイカが仲間外れかを見分ける間違い探し系のゲームです。提示された画像群の中に全く同じペアがあり、ペアの存在しないスイカを見分けるというゲーム性なのですが、なるほどなぁと思ったのは、その全く同じペアも紛らわしい柄になっているというところです。最初にプレイしたときにこれ本当に正解あるの?!と思ったのですがよーく見てみると違っていて、普段使わない領域の脳が燃えているような感覚があり、面白かったです。あまりパズルゲームをやらないのですが、こういうのはよくあるのでしょうか? 自分には非常に新鮮に映りました。

題材がドット絵のスイカに着目している部分もなるほどなと思いました。柄や葉の形や向きなどで複数の組み合わせを作れるので、間違い探しにはもってこいのテーマだなと思いました。個人的に、スイカの柄をどうやって実装しているのかが気になります。

unityroom.com

 

いろかわフラワーズ

咲いた花のうち、色が徐々に変わっていっている花をクリックしていくゲームです。

花が咲く様子がきれいで、鳴るサウンドも軽やかなので、穏やかな気持ちでプレイできます。後半まで行くと難易度はなかなか高く、やりごたえがありました。

楽しさのあまり長時間やり込んでいたらランキング上位になっていました。

unityroom.com

 

ラク錬金術

純粋にゲームとしてのクオリティが高く、気づけば1時間ほどプレイしていました。面白すぎる。これがこの短期間で作れるというのがまず驚愕です。

ラストバトルでは敵と1対1の戦いになるのですが、ここが特にいろんなアイテムを持ち込んで試せる集大成のステージという感じがあり、錬金したアイテムをどういう組み合わせで持っていくと楽しく戦えるのか試行錯誤していました。

unityroom.com

 

Avoid!

移動可能領域の中で、ランダムに出現する敵から逃げ回るというゲームなのですが、この領域がおそらくランダム?で変わっていくのが面白かったです。一見安全そうな場所にいても「もしかしたら領域が変わってしまうのではないか」とハラハラするので、常に緊張感のあるゲーム性でした。

unityroom.com

 

四季登々!!(しきのりのり)

誘ってくれた友人が作ったゲームなのですが、ひいき目なしにめちゃくちゃ面白くて、投稿されてから一週間はほぼこのゲームばかり遊んでいました。

難易度は確かに高めだと思うのですが、リスポーンが非常に早く、難易度も頑張れば何とかできそう!と思わせるレベルで、いわゆるフロムゲーのような没入感がありました。

けっこう滑るので、油断していると速攻ゲームオーバーになるのですが、慣れた区間はするするとできるようになるので、成長が感じられるゲーム性でよかったです。

unityroom.com

 

今年は協力?干支パズル!

動き方が決まっている干支の動物を動かして、ゴールにたどり着こう!というパズルゲームです。プレイヤーはネズミとなって、隣接している動物に乗り移ると、動かせるようになります。

パズルゲームとして完成されていて、気づいたら時間が経っていました。難易度もちょうどよく、ガチャガチャ動かしていてもなんとかなるので、パズルの苦手な自分にも楽しめました。

unityroom.com

 

3人の勇者

ランキング1位のゲームなので、自分が今さら書くべきだろうかとも思うのですが。

ゲーム開始後の絵本を読むと「3人の勇者が立ち上がったものの全然うまくいかず、魔王に支配されました、めでたしめでたし」というストーリー。これはいったい何だろう?と絵本を何度もめくっていると、クリックできそうなオブジェクトがある。なんとなく触ってみると、なんと物語が変わっていく!

上記は自分のゲーム体験を簡単にまとめたものですが、物語が変わった瞬間に「なるほど!」とえも言えぬ感動を覚えました。

このイベントの中で印象に残ったゲームというだけでなく、自分が今まで生きてきた中で特に感銘を受けたゲームの一つになりました。

unityroom.com

 

調色のガーネット

指定された色をとにかく調合するのですが、色を完成させたら類似度を比較して終了というゲーム性なので、ゆったりとプレイできました。

どのくらい似ているかをランキング形式にしているので、上位に行こう行こうと何度もプレイしました。

unityroom.com

 

チェンジ!!!! カイザーファルコン!!!!

色の異なる3つのユニットを合体させて障害を切り抜けていくゲームです。ユニットを選択する順番でどのような姿になるのかが違うので、立ちはだかる障害に合わせてどの姿に変形するかを選んでいくというゲーム性です。

全体的に演出がキチっとしていて、見ていて気持ちがいいです。合体演出を見ているだけでも飽きません。

とにかく失敗時のコメントが面白くて、どういうコメントが出るのかなとあえて間違えてみる楽しみ方もできました。

unityroom.com

 

フィズルピ~ふわふわの友情の泡を広げよう!~

とにかく飛び抜けて絵が良くて、ぼーっと眺めているだけで癒され、ゲームをしながら見惚れていました。このレベルの高解像度をどうやれば実現できるんだろうと思いました。

unityroom.com

最近の米津玄師の曲が最高すぎるので語りたい(KICK BACKから地球儀まで)

はじめに

ここのところ、米津玄師(敬称略)を主題歌にしたビッグプロジェクトが相次いでおり、話題になっています。

自分としては、最近の米津玄師の曲がどんどん輝いて見えて、年々音楽の力やテーマの着眼点などに脂が乗ってきているように思えます。

これはいいなぁーと思うばかりだったのですが、せっかくなら文字にしようと記事を更新しました。

この記事では特に最近の「KICK BACK」「恥ずかしくってしょうがねえ」「LADY」「月を見ていた」、そして今日配信がスタートした「地球儀」について、語っていきたいと思います。

大変不勉強な限りで恐縮ですが、チェンソーマン以外の作品(FF16、君たちはどう生きるか)は自分は触れていませんので、作品のネタバレの心配はございません。全然関連作品に触れられておらず、作品と合わせて両輪のところを、片輪で無理やり走行しているような気がしますが、とにかく語りたいのです。

自分の思う米津玄師の魅力

音楽性という部分では、あまり音楽に詳しくないので、音楽家としてどこがどうすごいのかは私にはよくわかっていません。ただ、素人目でも分かることとしては、引き出しが多いということと、バランス感覚が優れているところではないかと思います。

例えば、今回語ろうとしている楽曲群も、ある程度どの曲にも作家性は出ているものの、続けて聞くと本当に一人のミュージシャンの曲を聴いてるのか、と思うほど振れ幅が大きいです。

また基本的にスタンダードな構成から飛びすぎることもせず、1曲の長さが極端に長い、というような上級者向けの曲も無いのでとっつきやすいです。本人も軸はJ-POPだと語っており、そこはかなり意識しているのではないかと思っています。

自分が分かる中で、米津玄師の突出して優れている魅力は、軸のしっかりした統制感のある総合演出家的な側面かと思います。それだけでなく遊び心もあって、今回はどういうアプローチでどういうテーマを表現するのだろうと、わくわくさせられます。

なかでも自分が愛してやまないのは歌詞の部分です。歌詞の詩情は、同年代のポップスクリエイターの中でも白眉ではないかと自分は思っています。

最近は主題歌を担当している機会も多く、作品のファンから「解釈の鬼」「米津玄師の解釈力に負けた」という評もよく耳にします。自分には少々大袈裟な言い回しに聞こえますが、音楽を聴いた人にそんな熱を抱かせる魅力があるのだと感じます。

また歌詞は音楽の機能としても練られたものに感じます。言葉数はかなり多いのですが、何となく口ずさんだ時に譜割が綺麗で歌っていて気持ちが良いです。リズム感の高いパートは破裂音を多めにしたり、語尾を揃えたりしてリズムを出したりと、こだわって作っていることが伺えます。

よく描かれるテーマの中で特に自分が感銘を受けているのは「永遠を強く錯覚できるほどに美しい刹那の瞬間がある」です。最近の曲だと「感電」や「PLACEBO」「春雷」がまさにそれを軸にしたようなテーマですが、全作品を通じてある程度根底にあるものに感じます。

この美しさは、「眼福」のように、いずれ消え去ってしまうことを予感させるようにも描かれていますし、「灰色と青」や「メトロノーム」のように、もう終わってしまったものとして描かれるものも多いです。

特にこのテーマの中でも自分が好きなのは、アルバムYANKEEの画集版の特典についてきた「メトロノーム」という漫画です(いま手元になく、もしかしたらタイトルを間違えていたらすみません)。

あらすじをそのまま伝えても魅力が下がるように思えてしまうため詳細は割愛しますが、言葉を交わさずに通じ合えたと錯覚する瞬間を描いた漫画で、読後感も美しいです。漫画のメトロノームが、周期の違うメトロノーム2台がピッタリになった瞬間を描いたものだとしたら、歌の方は徐々に離れていく瞬間を描いたものですね。

reissuerecords.net

 

最近の楽曲の考察や感想

KICK BACK

チェンソーマンのアニメのオープニングテーマです。King Gnuの常田大希氏との共同制作でも話題になりました。

コミカルなPVも半ばミームと化し、やたらコラを見かけます。この手の映像作品で心の底から面白いと思って笑ったのは初めての経験でした。

youtu.be

チェンソーマンは自分も好きな作品で、ワンチャン米津玄師が担当しないかなーと知り合いと話していたら、マジで担当になって驚いた記憶がありました。

チェンソーマンの第1部は生きるか死ぬかしかない世界に生きていた少年デンジが、人間性をどのように得ていくのかという話だと思っています。デンジは本来はクレバーな面も多いだろうことも窺えるのですが、特にアニメで放映された範囲で言えば、デンジの願う世界は本能的であっけらかんとしていて、全体的に暗いチェンソーマンの中で一際輝いている存在です。「幸せになりたい、楽して生きていたい」という脱力さえしてしまうアバウトな歌詞は、そういった部分を反映しているのだと思います。

ところで、何度も繰り返される「努力 未来 A BEAUTIFUL STAR」はモーニング娘。の「そうだ!We're ALIVE」からの引用であると各所で発言されていますが、これをなぜ引用しているのかというのは、ある種天啓のようなものであったと言っています。

――この曲には<努力 未来 A BEAUTIFUL STAR>という、モーニング娘。の「そうだ!We’re ALIVE」の歌詞のフレーズが引用されています。このアイディアはどういう由来だったんでしょうか?

米津:これは直感としか言いようがないです。なんかわかんないけど、とにかくやりたい、マジでどうしてもやりたいという感じでした。

https://www.billboard-japan.com/special/detail/3721

本人が、意味は自分でもよく分からないと発言されていますが、優れた作家がこれは必要と思ったのならば、本人がその意図をうまく言語化できずとも、暗黙的に敷かれた理由があると自分は思います。

これも自分は、どこか野生的に願いを求める部分の現れとして描いているのでは無いかと思います。言葉の明るさに比べてひずんだ声で、曲のメインフレーズでもあるため執拗なほどに繰り返し歌われます。本能の叫ぶ、自分が心の底から願ってやまないものを力強く勝ち取りたいというのを表現した部分なのかなと思います。

つんく♂氏は、本人のブログで、引用された歌詞についてどのような経緯で作ったのかを回想されており、非常に示唆的だなと感じました。

大事な部分は、「生かされてる」のでもなく、「のほほんと生きてる」のでもなく、「自分で前向いて生きるんだぜ!」ということ。

要するに、今回米津氏が引用してくれたフレーズに繋がっていくわけですが、努力すれば未来につながる。そして、その積み上がった努力の結果が、美しい星となるはずだ! というメッセージ。確かに、すべてはこの1行に詰まっています。

note.tsunku.net

もう一つ気になるのがタイトルのKICK BACKです。キックバックチェンソーで物を斬るときに刃が引っかかって自分の方向に跳ね返ってくることを指します。当然ながら死亡事故に繋がることが多く、労災事故関連の話ではよく聞く単語です。

ここで「何を斬ろうとしているのか」「跳ね返ってくる刃が表すものは一体何なのか」については、この作品からは直接読み取りにくい、のではないかと思っています。自分の解釈があまり深くないというのもありますが……。

これはむしろこの歌で完結させるというよりは、チェンソーマンの物語とセットにして考えると解釈の幅が広がるような仕組みにしているのかなぁと思います。

また曲調でも話題になったと思うのですが、やたら転調しています。耳コピに挑戦してみた時にすごい苦労しました。

監督からの指示でジェットコースターみたいな曲として転調を繰り返す歌を、というオーダーがあったようでした。

(中略)監督から貰ったオーダーで覚えているのが「ジェットコースターみたいな曲を作ってほしい」ということだったんですね。転調を繰り返して、パートごとにガラッと変わって、別の曲になっているかと思うような高低差のある曲で。振り回されながら聴いて、気がついたら一曲終わっているような曲を作ってほしいというオーダーがありました。

https://www.billboard-japan.com/special/detail/3721

個人的に、ジェットコースターというのも示唆的なものに感じます。上がって落ちて上がって落ちてと乱高下する様子はチェンソーマンという漫画を分かりやすく表現しています。

 

恥ずかしくってしょうがねぇ

シングルKICK BACKに収録された2曲目の作品です。ギターの不穏なフレーズから入る、ひたすらに暗い曲調です。歌詞も怒りに満ちています。

open.spotify.com

サビの出だしで出てくる「サングリアワイン」は飲み物ですが、サングリアは「Bloodletting=瀉血/流血」を意味するスペイン語のようです。

血という言葉は「いつかそのナイフが/あなたの胸にも突き刺さるだろう/血が流れていく/誰のものかわからぬまま」で出てきます。演出で目立つ個所に配置された歌詞であることを考えると、ここが作品の核となる部分であることは間違いないと思います。

「サングリアワイン 口に合わねえな」という歌詞は、そんな風に人を刺して溢れた血を酒として呷るのは性に合わないという話かと思います。そしてこの血は相手の血だけではなく、刺された自分の血にもなりうるという話かと思います。

歌詞を読んで浅はかにもパッと思い浮かんだのは、SNSやインターネットで延々と展開される、俎上に載せて、載せられてのやり取りです。これらの技術が登場する前からある話ですが、普及によってより目に入りやすくなっているのは事実かと思います。

すっかり嫌気が差して「あんたらみたいにゃなりたかねえな」と距離を置きたい一方で、結局のところ誰しもその一面はあるというところが、「誰もがユダなら」や「いつかそのナイフが/あなたの胸にも突き刺さるだろう」という歌詞に表れているのではないかと思います。

ただそのサビの歌詞の最後は「もっかいちゃんと話そうぜ」であり、見放さないようにしているというか、怒りをぶつけるだけでは終わっていません。こういったところがバランス感覚が表れている部分かなと思います。

LADY

上記2曲と打って変わって、ジャジーで爽やかな曲です。ジョージアのCMソングです。

youtu.be

サビの歌詞では相手に「レディ」や「ハニー」と呼び掛けています。米津玄師の歌詞で明らかに登場人物の性別を女性に寄せている楽曲は「Blue Jasmine」「vivi」などがありますが、いずれも「愛してやまないもの」を表現するのに使われている気がします。それを踏まえると、この呼び掛けた相手が「愛してやまないもの」というような感じだと思います。

ただ、この愛してやまないものも、日常的に触れていれば次第に何でもないものにも思えてきます。代り映えのない何でもない景色が続いていく中で、どう向き合っていこうか、というような話だと思います。

それこそ、さっき話したような自分の生活の中にある倦怠感ですね。わかりきってしまったものとの向き合い方というか。もちろん相手のことを100%わかるということなんて到底ありえないとは思うんですけど、自分の感覚としてはわかりきってしまったと感じることがある。そことのバチバチバトルという感じですね。

米津玄師「LADY」インタビュー|“変わり映えしない日々の倦怠感”を軽やかに歌にして (2/3) - 音楽ナタリー 特集・インタビュー (natalie.mu)

めずらしくストレートでロマンティックな歌詞ですが、とにかく「したい」が多いです。ということは、翻って普段はこんなことしていないわけで、恋心をなんとか再燃させようと必死な感じも出ています。軽いながらも考えておかないといけない、普遍的なテーマだなと思います。

そういったテーマだと頭に入れていると、Cメロの歌詞「いますぐ行方をくらまそう」が、最初に聞いた時に強烈な違和感がありました。行方をくらますは日常から飛び出すというのを表す語としてはネガティブというか攻撃的な印象があります。

そもそも、この「くらまそう」が、自分だけ行方をくらまそうと決心したのか、相手と一緒に行方をくらまそうと提案したのか、よく分かっていませんでした。

何かインタビュー記事にヒントはないかなぁとぼんやり眺めていると、下記のコメントが寄せられていることに気づきました。

平坦な生活からほんの少しだけフケられたらいいなという気持ちを音楽にしました。よろしくお願いします。

reissuerecords.net

このコメントを見たとき、「ゆめうつつ」で語っていたテーマを思い出しました。

「ゆめうつつ」で言うと、夢という自分のパーソナルスペース、社会と隔絶された自分にしかわかり得ない空間を、より大事にすべきであるということで。結局のところ言いたいのは、いい塩梅で生活を送るべきということ。

米津玄師「Pale Blue」インタビュー|ポップソングの面白さを追い求めたどり着いた、究極のラブソング - 音楽ナタリー 特集・インタビュー (natalie.mu)

この曲の言う行方をくらますというのは、二人で一緒に夢のような世界に行こうぜという、ロマンチックやドラマチックさを表現しているのかもしれません。

月を見ていた

FF16のテーマソングです。残念ながらPS5を持っておらずFF16は知り合いからトレーニングモードの部分を少し触らせてもらった程度です。

youtu.be

今回の記事はこの曲の感想を書きたいがために書いていました。というのも、この曲が描いているテーマに滅法弱いのです。

救いなど本当はどこにも無いのに救いを見つけたいという思い。健気というには傷だらけでむごい姿に、しかし美しさを感じずにはいられません。FF16はプレイしていないのですが、自分はFGO第2部6章のアヴァロン・ル・フェを想起しました。

まず、「月明かり 柳が揺れる」と初っ端から暗く静かな夜というイメージが広がります。このパートは楽器も低音を強調したピアノしか鳴っておらず、ひたすらに重く暗い雰囲気で、曲の印象を決定づけます。

ただ、ここで自分が重要だと思うのは、その暗さを、夜という闇ではなく、月明かりという微かに届く光で表している部分です。ただ暗いのではなく、希望が薄らと見える、その塩梅を探っているように見えます。

「あなた」という言葉が繰り返し出てきます。おそらくFF16に出てくる特定の誰かを想定しているのかもしれないのですが、むしろこれはその人も含む「自分が修羅の道を歩んで行った先に、救われた、報われたと思わせてくれる何か」という広い概念のことを指しているのだと思います。

「月」と「あなた」が表すものはほぼ同じかと思いますが、Cメロラストの「月を頼りに掴んだ枝が あなただった」を踏まえると、その役割は違いそうです。前者は救いまでの道を淡く照らす道中の希望、後者は最後に出会える救いそのものだろうと思われます。

吉田直樹氏との対談(後半はまだだろうか)で、本当はもっと暗い曲だったが、だんだん主人公の物語に触れるにつれて、主人公が幸せになってほしいと、希望を感じられる曲に変えたと発言しています。

youtu.be

ここで思い出すのは、タイトルの英訳のMoongazingです。「gaze」は「見る」という単語ですが、ニュアンス的には「しっかりとみる」「興味を持って見る」「何かを考えながら見る」というような意味があるようです。

www.oxfordlearnersdictionaries.com

「月を見ていた」の「見ていた」はぼんやりと見ていたのではなく、何か思いを持って見つめていたわけで、またそれは現在進行形で続いているようです。そういった部分で、暗いながらも希望を持った明るさや強さが表現されているのだと思います。

また、個人的に気になったのは、バスドラムのリズムです。基本的には「トン ウ トン」という感じで、落ち着きのある展開なのですが、サビやCメロの、この曲のコアとなる部分に差し掛かると、「トン ゥトトン」と少し速さを感じるリズムになっています。(この表記で伝わるのか?)

単純に盛り上げる場所なので、スピード感やパワフルさといった音楽的な効果を狙っているだけなのかもしれませんが、前のめりに歩いているんだよ、ということが強調されているのではないかと思えてなりません。

地球儀

昨日に早速公開された歌ですが、美しく力強い歌で涙ぐんでしまいました。宮崎駿監督最新作の、君たちはどう生きるかの主題歌です。

open.spotify.com

最初の歌詞は「僕が生まれた日の空は」であり、道中には別れというモチーフもたびたび出てきます。このことからこの歌全体のテーマが人生にスポットライトをあてていることは明らかで、「君たちはどう生きるか」というタイトルにもリンクしているように思えます。

とにかく印象的なのは、Aメロでワンフレーズ歌った後、しばし音の無い瞬間が訪れるという展開です。詳しい人がいれば教えてほしいのですが、4分の4拍子で進んで一瞬2分の4拍子になって、1小節分休符があるという感じでしょうか。最初は全然拍感が掴めませんでした。

人生についてのテーマであることを考えても、ゆっくりと独白をしている印象で、止まりながらも歩みを進めてきたこれまでの道程を感じさせます。

とにかく、歌詞が美しい!

「この道の行く先に 誰かが待っている」「光さす夢を見る いつの日も」は、何でもない言葉であるはずなのに詩情深く、まばゆい光が見えるようです。「手が触れ合う喜びも 手放した悲しみも」などの冷ややかな陰を描いた歌詞には涙がこみ上げてきます。極めつけは「この道が続くのは 続けと願ったから」。力強くストレートな言葉ですが、これにもう痺れました。

ところで地球儀と聞くと、牧野信一の小説が想い浮かびます。現代の人からすると一見珍妙な言い回しが多く(「フェーヤー、フェーヤー、チョッ」「スピンアトップ・スピンアトップ・スピンスピンスピン」)、旧名センター試験で物議を醸したあの小説です。自分も当時試験室内で読んだ時は当惑しました。10年ぐらいたってから、50円で適当に買ってみた全集に偶然この短編が収録されていて、おお!と感慨深く思いました。

www.aozora.gr.jp

その小説内で出てくる、主人公が書きかけてやめた短編小説では「地球儀を回す」が「時間を進める」というような意味で登場します。厳密には「時間は進んでいないのだけど、進んでいると自分に言い聞かせる慰み」です。遠くの国へ行ってしまった父を想い、早く帰ってこいと「スピンスピンスピン」と言いながら地球儀を回し続ける息子の姿が出てきます。

この歌で「地球儀を回すように」している行為は「飽き足らず思い馳せる」と「「飽き足らず描いていく」です。過去や未来を思わせる言葉で、「地球儀を回す」には、上の作品と同様、時間を進める、戻すというニュアンスが入っているものと思われます。

ただし本当に時間がそう動いているわけではなく、どちらかといえばアルバムをめくるというような意味合いに近いのかと思います。「飽き足らず」からは、興味が尽きることなく延々とやっている様が垣間見え、宮崎駿氏のクリエイターとしての側面をとらえようとしている歌詞にも思えます。

また、地球儀は進んでもまた同じ場所に戻るという性質があります。生まれて死んで同じ場所に還っていくというある種の思想を感じずにはいられません。

あるいはそれが一周と限らないのであれば、人生の節目節目で同じ地点に立ち、自分の道程を思い返したり、そこから新たに発見した景色に感動する、そういったことも言い含まれているように思えます。

少し自分でもよく分かっていないのが、地球儀と同じくらい言及されている、「秘密」という言葉が意味するところです。もしかすると映画の内容に関係しているのでしょうか。

「地球」だったり「秘密」という言葉から、自分は天空の城ラピュタの主題歌、「君をのせて」を思い出しました。実際のところ全然「秘密」という言葉は出てきませんが、言葉にするのは難しい、夢のような何かが世界に隠されていて、それによって世界が輝いて見えるというような、そんなニュアンスから頭の中でリンクした形です。いつ聞いても、とにかく「感覚」に寄り添った歌で、神秘的な曲だなと思います。

open.spotify.com

例えば「扉を今開け放つ 秘密を暴くように」は、その何かを見つけようと飛び出しているようにも見えますし、「一欠片握り込んだ 秘密を忘れぬように」は、その何かのきっかけを見つけたその重大なものを、世間に明け渡さずに自分の中にそっとしておこうとしているようにも見えます。そして次に続く歌詞が、地球を模した地球儀を回しているというシーン。なんというか解釈の仕方がまだわかりませんが、ある意味そんなロマンや冒険心も含まれているのかなぁと、そんなことを考えました。

 

おわりに

「KICK BACK」から「地球儀」まで、自分の考えたことを取り留めもなく書き並べました。本当はシングル「Pale Blue」、「M八七」も全部書きたいと思っていたのですが、そこまで行くと、ただでさえ手なりで書いていると長くなりやすい記事が、ブログの文量を超えてしまう気がしたのでやめました。

まず自分は、テーマソング元の作品に触れたいと思います。

P4Runtimeでコントロールプレーンを開発する Python編 (1/2)

はじめに

前回の記事はP4言語の書き方,すなわちデータプレーンをどうやってプログラミングするかについて調べました*1
madomadox.hatenablog.com

今回は,コントロールプレーンをプログラミングするための,P4RuntimeというAPIについて調べました.
そして,P4Runtimeは現在GoとPythonで利用できるようでしたので,Pythonでコントロールプレーンをプログラミングしてみようと思います.
Python編と銘打っていますが,Goでの予定はございません.

本記事は,P4Runtime Specificationのversion 1.3.0を読んでまとめています.
https://p4.org/p4-spec/p4runtime/main/P4Runtime-Spec.pdf

また,P4言語のコンパイラはp4.orgによるp4c,ターゲットはbmv2,アーキテクチャはv1modelを利用したものであることを前提とします.

参考記事・参考文献

qiita.com
qiita.com
nextpublishing.jp

誤りがありましたら,ご指摘いただけますと幸いです.可及的速やかに修正いたします.

事前知識

P4Runtime

P4Runtimeは,ベンダーに依存せずに,共通のプラットフォームでP4エンティティ(P4言語で定義されたテーブルなど)を操作するためのAPIです.
P4Runtime Specificationにその仕様が定義されています.

P4のバイス(データプレーン)となるターゲットは1台以上のコントローラ(コントロールプレーン)から制御されます.
コントローラが複数ある場合は,その中から代表者のコントローラが選ばれます.代表者はメインコントローラ,その他大勢はスレーブコントローラと呼ばれます.
そしてコントローラがデバイスを制御するためのAPIの1つがP4Runtimeです.

P4Runtimeではコントローラがデバイスを制御するために,gRPCと呼ばれる技術を用いて通信します.
バイス内にはgRPCサーバが,そしてコントローラ内にはgRPCクライアントが実装されています.
バイスがコントローラのAPIを受け付けており,それに応じた処理をします.
そのため,デバイス内で動いているものは,P4Runtimeサーバということになります*2

Protocol BuffersとgRPC

P4RuntimeはProtobuf(Protocol Buffers)と呼ばれる形式で表現されたメッセージ定義によって提供されています.

Protocol Buffersとは,XMLJSONなどと同じく,データの構造を表現するフォーマットの1つです.

Protocol Buffersはスキーマ言語と呼ばれます.スキーマ言語ではデータの型やデータ同士の関係性などが定義できます.
このフォーマットは,gRPCというGoogleの開発したRPC(遠隔リモートプロシージャ)フレームワークに利用されています.ファイルの拡張子には.protoが利用されます.

protocという.protoファイル用のコンパイラプログラムがあり,これでprotoファイルをコンパイルすると,gRPC処理に関連するクラスが自動的に作られます.
サーバ側のクラスをServicer,クライアント側のクラスをStubと呼びます.
この雛形はGo,C++PythonJavaなど多岐にわたる言語で作成できるため,特定の言語に依存せずにgRPCを用いたアプリケーションを作成できる利点があります.

P4Runtimeで利用されるデータは,p4runtime.protoというProtocol Buffersファイルであらかじめ定義されています.
P4Runtimeでは,データプレーン側にgRPCサーバが,コントロールプレーン側にgRPCクライアントがあることになっているため,「コントローラプレーンを実装する」=「gRPCサーバにリクエストするgRPCクライアントを実装する」になります.
なお,データプレーンを自作しない限りは,データプレーン側で動作するgRPCサーバの処理はすでに実装済みであることを前提とします.

今回はP4RuntimeをPythonで書く予定ですので,PythonでのgRPCの例を出します.
.protoファイルをコンパイルすると,Protocol Buffersで記述したメッセージに関するクラスの定義があるxxxx_pb2.pyと,gRPCの通信に関するクラスの定義があるxxxx_pb2_grpc.pyが生成されます.
xxxx_pb2_grpc.pyからクライアント・サーバのクラスをインポートして利用(あるいは実装)し,やりとりするメッセージのクラスはxxxx_pb2.pyからインポートして利用します.

コントロールプレーンプログラミングの手順とPythonでのプログラミング

P4Runtimeを用いて,コントロールプレーンからデータプレーンを制御する際は,以下の手順に従います.

  1. コントローラとデバイス間の通信セッション確立
    • 必要な情報:MasterArbitrationUpdateメッセージ
    • 利用するRPC:StreamChannel
  2. コントローラからデバイスコンフィグとメタデータをデバイスにインストール
    • バイスコンフィグをデバイスにインストール
      • 必要な情報:SetForwardPipelineConfigRequestメッセージ
      • 利用するRPC:SetForwardPipelineConfig
    • 現在のデバイスコンフィグを確認
      • 必要な情報:GetForwardPipelineConfigRequestメッセージ
      • 利用するRPC:GetForwardPipelineConfig
  3. コントローラからデバイスを制御
    • エンティティの更新処理
      • 必要な情報:WriteRequestメッセージ
      • 利用するRPC:Write
    • エンティティの情報取得
      • 必要な情報:ReadRequestメッセージ
      • 利用するRPC:Read

それぞれ,gRPCクライアントから,デバイス上のgRPCサーバに対してリクエストを送信して実現します.
ここで,gRPCプロトコルの観点から見れば,データプレーンはgRPCサーバコントロールプレーンはgRPCクライアントの役割を持っています.
繰り返しになりますが,コントロールプレーンで実装するのはgRPCクライアントです.

今回はPythonでコントロールプレーンを作っていこうと思っていたのですが,実はすでにtutorials用にPythonのライブラリ?が作られていました.
github.com

本記事では,単に必要最低限の処理を理解することに焦点をあてて,下記のtutorialsのプログラムも参考にしつつ,より簡易的なプログラムでコントローラを作ることにします.
このtutorialsのリポジトリのライセンスはApache 2.0 Licenseです.
したがって,これ以降,私が記載したソースコードには,Apache 2.0 Licenseで配布されている成果物を一部利用したものがが含まれる箇所があります.

また,WriteとReadに関しては,実際にこのデータプレーンを配置したネットワーク環境で実験をする必要があることや,ソースコードの量も多くなることから,後半に分けています.
したがって,今回は,1.と2.までを実装します.

実験のための環境構築

実装をしていくにあたって,実際にgrpcサーバが稼動するデバイスを構築していこうと思います.
今回のターゲットはbmv2ですが,grpcが動くスイッチはswitch_simple_grpcとして用意されています.

ソースからコンパイルをしようとしたのですが,めちゃくちゃ手間取ってしまったので,下記ページにありました仮想マシンを利用させていただきました.
ユーザ名などの詳細は下記ページのREADME.mdをご確認ください.
github.com
ovaファイルとして提供されているので,VirtualBoxなどでインポートすることで仮想マシンを作成できます.
下記からは,この仮想マシン上にログインした環境でプログラミングをすることを前提とします.

なお,仮想マシンではp4runtime関連の.protoファイルがすでにコンパイルされており,p4runtime_pb2.py,p4runtime_pb2_grpc.pyなどがdist-packagesにインストールされています.
おそらく,これはP4RuntimeのPythonパッケージをpipでインストールした感じだと思いますので,実際はこれをインストールすれば,コントローラのプログラミングができる環境は構築できると思います.

>> import p4
>> p4.__file__
/usr/lib/python3/dist-packages/p4

そして今回は,tutorialsリポジトリのbasic.p4をデータプレーンのソースコードとして前提とすることにします.
github.com

まずは,はじめにgRPCのクライアントの基になるクラスを作ってみようと思います.
これに1.〜3.の内容を付け足していきます.
grpc.insecure_channel()でgRPCの通信チャネルを確立し,p4runtime_pb2_grpc.P4RuntimeStub()でリクエストを送ります.
xxx_grpcに定義されているXXXStubクラスを使ってリクエストを送信します.

from p4.v1 import p4runtime_pb2, p4runtime_pb2_grpc
import grpc

class MyController():
    def __init__(self, address, port, device_id):
        self.device_address = address + ":" + port
        self.device_id = device_id

        self.channel = grpc.insecure_channel(self.address)
        self.client_stub = p4runtime_pb2_grpc.P4RuntimeStub(self.channel)

1. コントローラとデバイス間の通信セッション確立

説明

コントローラとデバイスがセッションを確立するには,今のコントローラがメインコントローラとして選ばれる必要があります.
コントローラはdevice_idとrole_idという値の組で区別され,同じ組は,同じデバイスに対して同じ役割を持つコントローラ群と言えます.
各コントローラはelection_idを持っており,これが最も高いものがメインのコントローラとして選ばれます.
role_idとelection_idはコントローラが決めるもので,デバイス側ではタッチしません.

メインコントローラとして選ばれるためには,StreamMessageRequestというメッセージを作り,gRPCでデバイスに送信します.

ここからは,p4/v1/p4runtime.protoを見ながら,それぞれ構成を確認してみます.
github.com

まずは,rpcと書かれている箇所を読んでみようと思います.
ここがサービスの機能となる箇所です.

  // Represents the bidirectional stream between the controller and the
  // switch (initiated by the controller), and is managed for the following
  // purposes:
  // - connection initiation through client arbitration
  // - indicating switch session liveness: the session is live when switch
  //   sends a positive client arbitration update to the controller, and is
  //   considered dead when either the stream breaks or the switch sends a
  //   negative update for client arbitration
  // - the controller sending/receiving packets to/from the switch
  // - streaming of notifications from the switch
  rpc StreamChannel(stream StreamMessageRequest)
      returns (stream StreamMessageResponse) {
  }

これを見ると,どうやらStreamMessageRequestメッセージを利用するStreamChannelで接続の開始ができるようです.

  • connection initiation through client arbitration

次に,必要となるメッセージを見てみます.

message StreamMessageRequest {
  oneof update {
    MasterArbitrationUpdate arbitration = 1;
    PacketOut packet = 2;
    DigestListAck digest_ack = 3;
    .google.protobuf.Any other = 4;
  }
}

oneofと付いているので,updateは{}の中のうちどれか1つのメッセージが含まれるということが分かります.
今回使うものは,MasterArbitrationUpdateというメッセージです.
MasterArbitrationUpdateには次のような情報が含まれます.

message MasterArbitrationUpdate {
  uint64 device_id = 1;


  // The role for which the primary client is being arbitrated. For use-cases
  // where multiple roles are not needed, the controller can leave this unset,
  // implying default role and full pipeline access.
  Role role = 2;


  // The stream RPC with the highest election_id is the primary. The 'primary'
  // controller instance populates this with its latest election_id. Switch
  // populates with the highest election ID it has received from all connected
  // controllers.
  Uint128 election_id = 3;


  // Switch populates this with OK for the client that is the primary, and
  // with an error status for all other connected clients (at every primary
  // client change). The controller does not populate this field.
  .google.rpc.Status status = 4;
}

コントロールに複数のroleが必要ない場合は,roleを省略しても良いとあります.
また,statusもデバイス側が結果を返すもので,コントローラが入力するものではないともあります.

あとセットするものはelection_idです.election_idはhighとlowを持っています.
election_idが全体で128ビットあるうち,上位64ビットと下位64ビットを別々に指定します.
今回は特に理由はありませんが,00000(中略)000001という値にしようと思うので,high=0, low=1としようと思います.

message Uint128 {
  // Highest 64 bits of a 128 bit number.
  uint64 high = 1;
  // Lowest 64 bits of a 128 bit number.
  uint64 low = 2;
}
実装

上記を踏まえて,pythonプログラムで,StreamMessageRequestを構築して,StreamChannelで接続を確立します.
pythonでのgRPCに詳しくないのですが,stream系のRPCはイテレータ形式でリクエストを渡す必要があるようです.
qiita.com

といったところで,当初は下記のような書き方をしてみたのですが,その後後述するSetForwardingPipelineConfigでエラーが発生しました('Not primary'というエラーが表示されました).

    # New!!
    def MasterArbitrationUpdate(self):
        request = p4runtime_pb2.StreamMessageRequest()
        request.arbitration.device_id = self.device_id
        request.arbitration.election_id.high = 0
        request.arbitration.election_id.low = 1

        # イテレータ形式で送信
        message = []
        message.append(request)
        for resp in self.client_stub.StreamChannel(iter(message)):
            print(resp)

p4/v1/p4runtime.protoを確認すると,streamが終了するとセッションが切れますと書いてあるので,StreamChannelのrequestが消費されるとセッションが終了してしまい,後続のSetForwardingPipelineConfigが別のセッションでの通信と扱われてしまうという感じでしょうか?

indicating switch session liveness: the session is live when switch
// sends a positive client arbitration update to the controller, and is
// considered dead when either the stream breaks or the switch sends a
// negative update for client arbitration

これについては,下記でも説明がありました.どうやらイテレート可能なQueueを使ってこれを回避しているようでした.
github.com

今回,この箇所についてはtutorialsリポジトリで公開されているライブラリのコードを引用させていただきました.
StreamChannelの引数にQueueを与えておくと非同期で処理を待機させることができ,リクエストがpushされると,そのリクエストをStreamChannelで送出するという感じでしょうか?
PythonのgRPCライブラリの都合なのか,P4Runtimeの仕様的な都合なのか,いまいちこのあたりを使い慣れていないのでよく分かりませんが,詳しい方がいらっしゃれば教えていただきたいところです.

ということで,最終的なコードとしては下記のようになります.
(ライセンス表記について問題があれば,ご指摘いただけますと幸いです.)

from p4.v1 import p4runtime_pb2, p4runtime_pb2_grpc
import grpc

from queue import Queue # <== New!!
"""
    This IterableQueue function is ... 
    source:  https://github.com/p4lang/tutorials/blob/master/utils/p4runtime_lib/switch.py
    license: Apache 2.0, https://github.com/p4lang/tutorials/blob/master/LICENSE

"""
# New!!
class IterableQueue(Queue): 
    _sentinel = object()
    
    def __iter__(self):
        return(self.get(), self._sentinel)

    def close(self):
        self.put(self._sentinel)

class MyController():
    def __init__(self, address, port, device_id):
        self.device_address = address + ":" + port
        self.device_id = device_id

        self.channel = grpc.insecure_channel(self.address)
        self.client_stub = p4runtime_pb2_grpc.P4RuntimeStub(self.channel)

        self.request_queue = IterableQueue() # <== New !!
        self.request_stream = self.client_stub.StreamChannel(iter(self.request_queue))# <== New!!


    # New!!
    def MasterArbitrationUpdate(self):
        request = p4runtime_pb2.StreamMessageRequest()
        request.arbitration.device_id = self.device_id
        request.arbitration.election_id.high = 0
        request.arbitration.election_id.low = 1

        # キューにpush
        self.request_queue.put(request)
        for item in self.request_stream:
            return item
結果

結果を確認する前に,データプレーンとなるターゲットを起動します.
bmv2の場合は,simple_switch_grpcコマンドを実行します.その際,device_idを100に指定しておきます.

$ simple_switch_grpc basic.json --device-id 100
Calling target program-options parser
Server listening on 0.0.0.0:9559

すると,上記のようにポート番号9559でリクエストを待ち受けるようになります.
P4Runtimeのポート番号が,IANAで9559が指定されているようですね.
https://p4.org/p4-spec/p4runtime/main/P4Runtime-Spec.html

The server must listen on TCP port 9559 by default, which is the port that has been allocated by IANA for the P4Runtime service.

ターゲットを起動できたので,コントロールプレーンのプログラムを動かしてみます.

下記のコード(「実装」節で書いたコードにmainとなる処理を記載したもの)で結果を確認します.

(中略)
if __name__ == "__main__":
    controller = MyController(
                    address="127.0.0.1",
                    port="9559",
                    device_id=100
                )
    resp = controller.MasterArbitrationUpdate()
    print(resp)
$ python3 mycontroller.py

arbitration {
  device_id: 100
  election_id {
    low: 1
  }
  status {
    message: "Is primary"
  }
}

statusを見ると,"Is primary"と返ってきているので,無事にプライマリとして選出されたことがわかります.

2. コントローラからデバイスコンフィグをデバイスにインストール

SetForwardingPipelineConfig

説明

SetForwardingPipelineConfigを使うと,デバイスに設定をインストールできます.
これは,p4runtime.protoを見てみると,以下のrpcで実現できることがわかります.

// Sets the P4 forwarding-pipeline config.
rpc SetForwardingPipelineConfig(SetForwardingPipelineConfigRequest)
    returns (SetForwardingPipelineConfigResponse) {
}

このrpcの引数を見るとSetForwardingPipelineConfigRequestとあるので,これを送れば良いことが分かります.
さっそく,その構造をp4/v1/p4runtime.protoで確認してみます.

message SetForwardingPipelineConfigRequest {
  uint64 device_id = 1;
  uint64 role_id = 2 [deprecated=true];
  string role = 6;
  Uint128 election_id = 3;
  Action action = 4;
  ForwardingPipelineConfig config = 5;
}

roleは省略して良くて,device_idやelection_idも1. と同じように設定できます.

しかし,Actionが分かりませんので,そちらも読んでみます.
実態となるデータはenum型のようです.
以下を読んでみると,設定を保存してそれを反映させたいので,VERIFY_AND_COMMITを使えば良いのかなぁというのが分かります.

message SetForwardingPipelineConfigRequest
  enum Action {
    UNSPECIFIED = 0;


    // Verify that the target can realize the given config. Do not modify the
    // forwarding state in the target. Returns error if config is not provided
    // of if the provided config cannot be realized.
    VERIFY = 1;


    // Save the config if the target can realize it. Do not modify the
    // forwarding state in the target. Any subsequent read/write requests must
    // refer to fields in the new config. Returns error if config is not
    // provided of if the provided config cannot be realized.
    VERIFY_AND_SAVE = 2;


    // Verify, save and realize the given config. Clear the forwarding state
    // in the target. Returns error if config is not provided of if the
    // provided config cannot be realized.
    VERIFY_AND_COMMIT = 3;


    // Realize the last saved, but not yet committed, config. Update the
    // forwarding state in the target by replaying the write requests since the
    // last config was saved. Config should not be provided for this action
    // type. Returns an error if no saved config is found or if a config is
    // provided with this message.
    COMMIT = 4;


    // Verify, save and realize the given config, while preserving the
    // forwarding state in the target. This is an advanced use case to enable
    // changes to the P4 forwarding pipeline configuration with minimal traffic
    // loss. P4Runtime does not impose any constraints on the duration of the
    // traffic loss. The support for this option is not expected to be uniform
    // across all P4Runtime targets. A target that does not support this option
    // may return an UNIMPLEMENTED error. For targets that support this option,
    // an INVALID_ARGUMENT error is returned if no config is provided, or if
    // the existing forwarding state cannot be preserved for the given config
    // by the target.
    RECONCILE_AND_COMMIT = 5;
  }

続いて,ForwardingPipelineConfigについても確認してみます.
p4info,p4_device_configについては次節に飛ばすとして,Cookieはここでいま作成しているConfigを区別できれば何でも良いようですので,適当に決められそうです.

message ForwardingPipelineConfig {
  config.v1.P4Info p4info = 1;


  // Target-specific P4 configuration.
  bytes p4_device_config = 2;


  // Metadata (cookie) opaque to the target. A control plane may use this field
  // to uniquely identify this config. There are no restrictions on how such
  // value is computed, or where this is stored on the target, as long as it is
  // returned with a GetForwardingPipelineConfig RPC. When reading the cookie,
  // we need to distinguish those cases where a cookie is NOT present (e.g. not
  // set in the SetForwardingPipelineConfigRequest, therefore we wrap the actual
  // uint64 value in a protobuf message.
  message Cookie {
    uint64 cookie = 1;
  }
  Cookie cookie = 3;
}
P4Infoについて

ここからは,先程飛ばしたもののうちの一つ,p4infoについて書いていきます.
p4infoは,P4Infoフォーマットのmessageを指していて,テーブルやアクションなどのP4に関するオブジェクト(P4エンティティ)に関する情報が格納されています.
各エンティティにはIDが振られています.

このフォーマットはどう作るかと言うと,bmv2の場合ではp4cコマンドでオプションを指定すると,一緒に作られます.
実際にP4Infoファイルを作って中身を見てみます.
ここでは,下記のbasic.p4に関するP4Infoファイルを作成します.

P4言語で書いたソースコードを通常のp4cコマンドでコンパイルすると,basic.p4iファイルとbasic.jsonファイルが生成されます.
.p4iファイルは,プリプロセッサが出力したP4ファイル,.jsonがBMv2モデルのsimple_switch_xxxxのプログラムを実行するのに必要なJSONファイルが生成されます.
github.com

a file with suffix .p4i, which is the output from running the preprocessor on your P4 program.
a file with suffix .json that is the JSON file format expected by BMv2 behavioral model simple_switch.

このコマンドを,--p4runtime-filesオプションを付けて実行すると,P4Info用のファイルを生成できます.

$ p4c basic.p4 --p4runtime-files basic.p4info
$ ls
basic.json  basic.p4  basic.p4i  basic.p4info

basic.p4infoファイルの中身を見てみます.

pkg_info {
  arch: "v1model"
}
tables {
  preamble {
    id: 37375156
    name: "MyIngress.ipv4_lpm"
    alias: "ipv4_lpm"
  }
  match_fields {
    id: 1
    name: "hdr.ipv4.dstAddr"
    bitwidth: 32
    match_type: LPM
  }
   (中略)
}
actions {
  preamble {
    id: 21257015
    name: "NoAction"
    alias: "NoAction"
    annotations: "@noWarn(\"unused\")"
  }
}
actions {
  preamble {
    id: 25652968
    name: "MyIngress.drop"
    alias: "drop"
  }
}
actions {
  preamble {
    id: 28792405
    name: "MyIngress.ipv4_forward"
    alias: "ipv4_forward"
  }
  params {
    id: 1
    name: "dstAddr"
    bitwidth: 48
  }
  params {
    id: 2
    name: "port"
    bitwidth: 9
  }
}
type_info {
}

これを見ると,アーキテクチャや,定義したテーブル,アクションの情報が格納されてることがわかります.
テーブルやアクションにIDが振られていることも確認できると思います.

どんな情報が含まれるかについては,p4/config/p4info.protoに定義されています.p4info.protoから引用(コメントは本記事の著者が削除,一部中略)すると,
github.com

message P4Info {
  PkgInfo pkg_info = 1;
  repeated Table tables = 2;
  repeated Action actions = 3;
  repeated ActionProfile action_profiles = 4;
  repeated Counter counters = 5;
  repeated DirectCounter direct_counters = 6;
  repeated Meter meters = 7;
  repeated DirectMeter direct_meters = 8;
  repeated ControllerPacketMetadata controller_packet_metadata = 9;
  repeated ValueSet value_sets = 10;
  repeated Register registers = 11;
  repeated Digest digests = 12;
  repeated Extern externs = 100;
  P4TypeInfo type_info = 200;
}
     :
    (中略)
     :

message Table {
  Preamble preamble = 1;
  repeated MatchField match_fields = 2;
  repeated ActionRef action_refs = 3;
  uint32 const_default_action_id = 4;
  uint32 implementation_id = 6;
  repeated uint32 direct_resource_ids = 7;
  int64 size = 8;  // max number of entries in table
  enum IdleTimeoutBehavior {
    NO_TIMEOUT = 0;
    NOTIFY_CONTROL = 1;
  }
  IdleTimeoutBehavior idle_timeout_behavior = 9;
  bool is_const_table = 10;
  google.protobuf.Any other_properties = 100;
}
      :
     (中略)
      :
message Action {
  Preamble preamble = 1;
  message Param {
    uint32 id = 1;
    string name = 2;
    repeated string annotations = 3;
    repeated SourceLocation annotation_locations = 8;
    int32 bitwidth = 4;
    Documentation doc = 5;
    P4NamedType type_name = 6;
    repeated StructuredAnnotation structured_annotations = 7;
  }
  repeated Param params = 2;
}

であり,実際に生成されたP4Infoファイルには,上記の情報が含まれていることがわかります.
照らし合わせてみた例が下記のものです.

[P4infoファイル(basic.p4info)]

tables {
  preamble {
    id: 37375156
    name: "MyIngress.ipv4_lpm"
    alias: "ipv4_lpm"
  }
  match_fields {
    id: 1
    name: "hdr.ipv4.dstAddr"
    bitwidth: 32
    match_type: LPM
  }
  action_refs {
    id: 28792405
  }
}
[p4info.proto]

message Table {
  Preamble preamble = 1;
  repeated MatchField match_fields = 2;
  repeated ActionRef action_refs = 3;
  (中略)

出力されたp4infoのファイルはフォーマットに沿っているので,ファイルの文字列を読み込んでgoogle.protobuf.text_format.Merge()でmessageに変換することができます.
google.protobuf.text_format.Merge()では新しくmessageのクラスの変数を作成します.
これを直接代入しようとするとエラーが出るので,CopyFrom()を使って値を更新します.

p4info = p4info_pb2.P4Info()
with open(p4info_file, "r") as f:
    google.protobuf.text_format.Merge(f.read(), p4info)

request.config.p4info.CopyFrom(p4info)
p4_device_configについて

p4_device_configは,ターゲットに関する情報で,bmv2の場合は,p4cで作成された.jsonファイルがそれに当たります.
p4_device_configの型はbytesなので,.jsonファイルをバイナリ形式で読み出して設定します.

device_config = None
with open(p4_device_config_file, "rb") as f:
    device_config = f.read()
request.config.p4_device_config = device_config
実装

最後に,上記を踏まえてプログラムを書いていきます.
p4infoに関するクラスが必要なので,p4.config.v1からp4info_pb2をimportします.

from p4.config.v1 improt p4info_pb2 # <== New!!
from p4.v1 import p4runtime_pb2, p4runtime_pb2_grpc
import grpc
import google.protobuf.text_format # <== New!!

from queue import Queue

"""
    This IterableQueue function is ... 
    source:  https://github.com/p4lang/tutorials/blob/master/utils/p4runtime_lib/switch.py
    license: Apache 2.0, https://github.com/p4lang/tutorials/blob/master/LICENSE

"""
class IterableQueue(Queue):
    _sentinel = object()
    
    def __iter__(self):
        return(self.get(), self._sentinel)

    def close(self):
        self.put(self._sentinel)

class MyController():
    def __init__(self, address, port, device_id):
        self.device_address = address + ":" + port
        self.device_id = device_id

        self.channel = grpc.insecure_channel(self.address)
        self.client_stub = p4runtime_pb2_grpc.P4RuntimeStub(self.channel)

        self.request_queue = IterableQueue()
        self.request_stream = self.client_stub.StreamChannel(iter(self.request_queue))

    def MasterArbitrationUpdate(self):
        request = p4runtime_pb2.StreamMessageRequest()
        request.arbitration.device_id = self.device_id
        request.arbitration.election_id.high = 0
        request.arbitration.election_id.low = 1

        # キューにpush
        self.request_queue.put(request)
        for item in self.request_stream:
            return item

    #New!!
    def SetPipelineConfigForward(self, p4info_file, p4_device_config_file): 
        request = p4runtime_pb2.SetForwardingPipelineConfigRequest()
        request.election_id.high = 0
        request.election_id.low = 1
        request.device_id = self.device_id

        # p4infoに関する情報をmessageとして取得
        p4info = p4info_pb2.P4Info()
        with open(p4info_file, "r") as f:
            google.protobuf.text_format.Merge(f.read(), p4info)

        # device_configをbytesとして取得
        # jsonファイルをバイナリ形式で読み込む
        device_config = None
        with open(p4_device_config_file, "rb") as f:
            device_config = f.read()

        request.config.p4info.CopyFrom(p4info)
        request.config.p4_device_config = device_config
        request.config.cookie.cookie = 1
        request.action = p4runtime_pb2.SetForwardingPipelineConfigRequest.VERIFY_AND_COMMIT
        resp = self.client_stub.SetForwardingPipelineConfig(request)
        return resp
結果

下記のコードで結果を確認します.(手を抜いてファイル名を直打ちで書いてます)

(中略)
if __name__ == "__main__":
    controller = MyController(
                    address="127.0.0.1",
                    port="9559",
                    device_id=100
                )
    controller.MasterArbitrationUpdate()
    resp = controller.SetPipelineConfigForward(p4info_file="basic.p4info", p4_device_config_file="basic.json")
    print(resp)
$ python3 mycontroller.py

device_id: 100                                                                                                                                                                        
election_id {                                                                                                                                                                         
  low: 1                                                                                                                                                                              
}                                                                                                                                                                                     
action: VERIFY_AND_COMMIT                                                                                                                                                             
config {                                                                                                                                                                              
  p4info {
          (.p4infoの内容が記載されている.中略)                                                                                                                                                                            
   }
  p4_device_config: "(.jsonの内容が記載されている.中略)"
  cookie {
    cookie: 1
  }
}

自分が設定した内容が返ってくるだけですが,結果を確認することができました.

GetForwardPipelineConfig

説明

SetForwardingPipelineConfigが終わったので,今度はForwardGetPipelineConfigを使って,現在の設定を確認してみます.
p4runtime.protoを確認してみると,GetForwardingPipelineConfigRequestというmessageが必要なようです.

  // Gets the current P4 forwarding-pipeline config.
  rpc GetForwardingPipelineConfig(GetForwardingPipelineConfigRequest)
      returns (GetForwardingPipelineConfigResponse) {
  }

GetForwardingPipelineConfigRequestを見てみると,ResponseTypeはデフォルトでALLが選択されるようなので,device_idだけ設定すれば良さそうです.

message GetForwardingPipelineConfigRequest {
  // Specifies the fields to populate in the response.
  enum ResponseType {
    // Default behaviour. Returns a ForwardingPipelineConfig with all fields set
    // as stored by the target.
    ALL = 0;
    // Reply by setting only the cookie field, omitting all other fields.
    COOKIE_ONLY = 1;
    // Reply by setting the p4info and cookie fields.
    P4INFO_AND_COOKIE = 2;
    // Reply by setting the p4_device_config and cookie fields.
    DEVICE_CONFIG_AND_COOKIE = 3;
  }
  uint64 device_id = 1;
  ResponseType response_type = 2;
}
実装

ここは単純に,GetForwardingPipelineConfigRequestについてdevice_idだけ設定すれば送れば良さそうです.

from p4.config.v1 improt p4info_pb2
from p4.v1 import p4runtime_pb2, p4runtime_pb2_grpc
import grpc
import google.protobuf.text_format

from queue import Queue

"""
    This IterableQueue function is ... 
    source:  https://github.com/p4lang/tutorials/blob/master/utils/p4runtime_lib/switch.py
    license: Apache 2.0, https://github.com/p4lang/tutorials/blob/master/LICENSE

"""
class IterableQueue(Queue):
    _sentinel = object()
    
    def __iter__(self):
        return(self.get(), self._sentinel)

    def close(self):
        self.put(self._sentinel)

class MyController():
    def __init__(self, address, port, device_id):
        self.device_address = address + ":" + port
        self.device_id = device_id

        self.channel = grpc.insecure_channel(self.address)
        self.client_stub = p4runtime_pb2_grpc.P4RuntimeStub(self.channel)

        self.request_queue = IterableQueue()
        self.request_stream = self.client_stub.StreamChannel(iter(self.request_queue))

    def MasterArbitrationUpdate(self):
        request = p4runtime_pb2.StreamMessageRequest()
        request.arbitration.device_id = self.device_id
        request.arbitration.election_id.high = 0
        request.arbitration.election_id.low = 1

        self.request_queue.put(request)
        for item in self.request_stream:
            return item


    def SetPipelineConfigForward(self, p4info_file, p4_device_config_file): 
        request = p4runtime_pb2.SetForwardingPipelineConfigRequest()
        request.election_id.high = 0
        request.election_id.low = 1
        request.device_id = self.device_id

        p4info = p4info_pb2.P4Info()
        with open(p4info_file, "r") as f:
            google.protobuf.text_format.Merge(f.read(), p4info)


        device_config = None
        with open(p4_device_config_file, "rb") as f:
            device_config = f.read()

        request.config.p4info.CopyFrom(p4info)
        request.config.p4_device_config = device_config
        request.config.cookie.cookie = 1
        request.action = p4runtime_pb2.SetForwardingPipelineConfigRequest.VERIFY_AND_COMMIT
        resp = self.client_stub.SetForwardingPipelineConfig(request)
        return resp

    # New!!
    def GetPipelineConfigForward(self):
        request = p4runtime_pb2.GetForwardingPipelineConfigRequest()
        request.device_id=self.device_id
        resp = self.client_stub.GetForwardingPipelineConfig(request)
        return resp
結果

SetForwardingPipelineConfigのレスポンスと同じ内容が見えます.

(中略)
if __name__ == "__main__":
    controller = MyController(
                    address="127.0.0.1",
                    port="9559",
                    device_id=100
                )
    controller.MasterArbitrationUpdate()
    controller.SetPipelineConfigForward(p4info_file="basic.p4info", p4_device_config_file="basic.json")
    resp = controller.GetPipelineConfigForward()
    print(resp)
$ python3 mycontroller.py

device_id: 100                                                                                                                                                                        
election_id {                                                                                                                                                                         
  low: 1                                                                                                                                                                              
}                                                                                                                                                                                     
action: VERIFY_AND_COMMIT                                                                                                                                                             
config {                                                                                                                                                                              
  p4info {
          (.p4infoの内容が記載されている.中略)                                                                                                                                                                            
   }
  p4_device_config: "(.jsonの内容が記載されている.中略)"
  cookie {
    cookie: 1
  }
}

まとめ

P4Runtimeは共通のプラットフォームでP4エンティティを操作するためのAPIとして仕様化されています.
通信方式はgRPCを前提としており,Protocol Buffersというフォーマットで定義されたデータを用いて通信します.
P4Runtimeに関するデータはProtocol Buffersで定義されており,こちらを利用します.

P4RuntimeはGo言語とPython言語で提供されており,今回はPythonでコントロールプレーンのプログラミングをしました.
ターゲットの制御にあたっては,(1)MasterArbitrationUpdateでメインコントローラとして選出され,(2)SetForwardingPipelineConfigでターゲットに設定をインストールし,(3)Writeでデバイス上のP4エンティティの内容を更新します.
simple_switch_grpcという,gRPCが動くbmv2ターゲットを起動して,(1)と(2)について,実際にレスポンスが返ってくることを確認しました.

Write,Readなど,実際に本質的な制御に利用する箇所(3)については後半で書いていこうと思います.

*1:疲れてはやめて,飽きてはやめてで書き溜めていたところ,いつの間にか前回の記事から1年ほど経ちました.

*2:コントローラはローカル環境かリモート環境かに置かれます.ローカルの場合はP4デバイスの中にデータプレーンとコントロールプレーンが同居しており,リモートの場合はデータプレーンの外にコントロールプレーンが置かれます.

プログラマブルデータプレーンのためのプログラミング言語P4について勉強する

はじめに

SIGCOMMの論文を読んでいてP4というプログラミング言語を知りました。
面白そうだったので勉強しようと思ったのですが、どこまで環境構築すれば動くようになるのかいまいちやり方が分からず、思い立ってはやめを繰り返していました。
2021年は心機一転,そろそろ書き方は知っておきたいと思い本腰を入れて勉強することにしました。
そんな折、日本P4ユーザ会様がP4のハンズオン勉強会をオンラインで開催されていたため、そちらにも参加させていただき、理解を深めることができました。
connpass.com

本記事の内容は以下のチュートリアルを進めていくにあたって,理解した内容を自分なりにまとめたものになります。
誤りがございましたらご指摘いただけますと幸いです。可及的速やかに修正いたします。
github.com

[追記 2021.08.19 細かなtypoや言い回しは特に断らずちょこちょこ直していますが、明らかに誤解を生みそうな文章、認識が完全に誤っていた文章に関しては追記を付して変更・編集しています]

[追記 2021.08.20 本記事はP4の言語仕様ver1.2.1時点のものを参照して作成しました。現在はver1.2.2が策定されており、一部異なる可能性がございます]
https://p4.org/p4-spec/docs/P4-16-v1.2.2.pdf

SDN (Software Defined Network)

データプレーンとは何かを説明するにあたり、まずは簡単にSDNについて紹介します。

SDNとは、Software Defined Networkの略で、ネットワークの設定・構成等を柔軟に動的に制御するための技術です。

SDNでは、従来のネットワークスイッチのルーティングを、ルーティングのルールを決定する機能と、パケット転送などのパケットを処理する機能に分離します。
このそれぞれのモジュールを、コントロールプレーン、データプレーンと呼びます。コントロールプレーンは中央集権的にデータプレーンとなるネットワーク機器を管理することができます。

従来技術では、ネットワーク機器ごとに異なるルーティングルールが設定されているため、統括的な管理には手間がかかります。

データプレーンは各々フローテーブルを持っており,到着したパケットの処理をどうするべきか、フローテーブルを参照して決定します。このフローテーブルはコントロールプレーンが制御しています。
例えば、OpenFlowでは、フローテーブルはヘッダ情報、カウンタ、アクションが定義されています。

SDNでは、コントロールプレーンを介してフローテーブルをデータプレーンに伝えることにより、複数のネットワーク機器に設定を反映することができます。

このようにして、まるでソフトウェア的にネットワークの構成・設定をプログラミングできるように扱えます。

P4について

P4とは、データプレーンがどのようにパケットを処理するかを記述するために開発されたプログラミング言語です。P4という名前は、論文のタイトルにもなっているProgramming Protocol-independent Packet Processorsの略語になっています。

Bosshart, Pat, Dan Daly, Glen Gibb, Martin Izzard, Nick McKeown, Jennifer Rexford, Cole Schlesinger, et al. 2014. “P4: Programming Protocol-Independent Packet Processors.” SIGCOMM Comput. Commun. Rev. 44 (3): 87–95.
https://dl.acm.org/doi/10.1145/2656877.2656890

従来のネットワーク機器のパケット処理に関する機能は、ASICの設計に依存しており、ASICの持つ機能しか利用できないという制約がありました。
この機能を変更しようとなると、ASICから再設計する必要があり、多大なコストがかかります。

P4では、P4に対応している機器であれば、機器の特性に依らずデータプレーンの機能をプログラミングすることができます。

P4の言語仕様にはP4-14とP4-16が存在します。これはオリジナルのP4に改良を加えていったものがP4-16となっており、旧来のP4をP4-14と呼ぶようになったようです。現在はP4-16での開発が主流です。

PISA (Protocol Independent Switching Architecture)

P4では以下のようなパイプライン処理を持つアーキテクチャを想定し、パイプラインの各ブロックの処理をプログラミングしていくことになります。

「Parser」→「Match-Action Unit」→「Deparser」

このアーキテクチャPISA(Protocol Independent Switching Architecture)と呼ばれています。

ブロックの意味は、それぞれ以下の通りです。

  • Parser: ヘッダ情報を解析し、その情報をメタデータとしてパイプラインの各ブロックに渡す。
  • Match-Action Unit: 得られたメタデータを用いてフローテーブルを参照し、対応する処理(アクション)を実行する
  • Deparser: ヘッダを再構成し、パケットを送出する

ターゲットとアーキテクチャモデル

P4でプログラミングするデータプレーン機器のことをターゲットと呼びます。
ターゲットとなるハードウェアに関しては、FPGAやASICなどがあります。また、P4の仕様に完全準拠するBehavioral-Model (BMv2)というP4対応の簡易的なソフトウェアスイッチもあります。これはP4開発の練習やテストのためのターゲットであり、性能は控えめになっています。

P4でPISAをプログラミングするにあたって,「プログラミングが可能なブロック」「各ステージの持つインターフェイス」「各ステージの機能」を定義したものをアーキテクチャと呼びます。

Specifically, arch.p4 defines what P4-programmable blocks are available, the interface for each stage, and the capability for each stage. Who is responsible for writing such an architecture program? The P4 Consortium is one source of such a definition, but different switch vendors have created their own architecture specifications to closely describe the capabilities of their switching chips.

Chapter 4: Bare-Metal Switches — Software-Defined Networks: A Systems Approach Version 2.1-dev documentation

アーキテクチャは基本的にプログラミング対象となるデータプレーン機器のベンダが提供するファイルです。P4でデータプレーンの機能をプログラミングする際は、ターゲットがサポートするアーキテクチャを利用する必要があります。

チュートリアルではBMv2をターゲットとして利用しています。
ちなみにBMv2にはいくつかのバリエーションがあるのですが、チュートリアルなどで利用されているsimple_switch、simple_switch_grpcは、v1model.p4というアーキテクチャを利用できます。
github.com

アーキテクチャモデルは他にPSA (Portable Switch Architecture)があります。
これは、汎用的に利用できることを目的にしたモデルで、現在開発が進められています。
[追記 2021.08.20 曖昧かつ誤解のあった記述であったため文章を再考しました]
ベンダごとにアーキテクチャが異なるとプログラムの書き方が変わってしまうため、汎用性がありません。
PSAでは異なるデバイスに対しても共通の方法でプログラミングできるように、P4 Architecture Working Groupで標準化が進められているものです。
2021年8月現在はver 1.1が策定されています。


https://p4lang.github.io/p4-spec/docs/PSA.pdf

P4言語を書いてみる

定義ファイルのインクルード

P4言語は、コアとなるプログラムをインクルードするところから始まります。
最初の部分を見てみると、以下のようになっています。C言語に似た書き方です。

#include <core.p4>
#include <v1model.p4>

core.p4はコアとなるヘッダファイルであることは推察されますが、v1model.p4は何でしょうか。
これは、アーキテクチャの定義ファイルです。
今回はBMv2をターゲットとするので、対応しているアーキテクチャの1つであるv1model.p4を利用します。
実際には、ネットワーク機器のアーキテクチャに合わせた定義ファイルをインクルードしてくることになります。

変数の定義

変数は一般的なプログラミングと同様の方法で定義することができます。
型もいくつかありますが、そのうちよく利用されるものを以下に列挙します。
また、typedefを用いて変数の型に別名をつけることも可能です。

  • bit<n>: n bitで表現される符号なしinteger
  • int<n>: n bitで表現される符号つきinteger
  • varbit<n>: n bitで表現されるbitstring
  • bool: bool型
  • header: 順序付きのデータコレクション。ヘッダ情報を表現するときに用いる。isValid()というメソッドで検証することができる。
  • struct: 順序なしのデータコレクション。

上の情報を使い、イーサネットヘッダを変数として定義すると、以下のようになります。

typedef bit<32> macAddr 
header ethernet_t {
   macAddr dst;
   macAddr src;
   bit<16> type;
};

パイプラインの実装方法

各ブロックをどのように定義し実装するべきかに関しては、アーキテクチャの定義ファイル(ここではv1model.py)に記載されています。
tutorials/p4-cheat-sheet.pdf at master · p4lang/tutorials · GitHub

v1model.p4ではパイプライン処理は以下のように定義されています。したがって、v1modelアーキテクチャのデータプレーンをプログラミングする際には、以下の構造にしたがってパイプラインの処理を実装していくことになります。

// v1model pipeline elements
parser Parser<H, M>(
    packet_in pkt,
    out H hdr,
    inout M meta,
    inout standard_metadata_t std_meta
);

control VerifyChecksum<H, M>(
    inout H hdr,
    inout M meta
);

control Ingress<H, M>(
    inout H hdr,
    inout M meta,
    inout standard_metadata_t std_meta
);

control Egress<H, M>(
    inout H hdr,
    inout M meta,
    inout standard_metadata_t std_meta
);

control ComputeChecksum<H, M>(
    inout H hdr,
    inout M meta
);

control Deparser<H>(
    packet_out b, in H hdr
);

// v1model switch
package V1Switch<H, M>(
    Parser<H, M> p,
    VerifyChecksum<H, M> vr,
    Ingress<H, M> ig,
    Egress<H, M> eg,
    ComputeChecksum<H, M> ck,
    Deparser<H> d
);

各ブロックの実装方法

それぞれの機能をどのようにP4で実装していくかを見ていきます。
なお、タイトルに*が付いているものは少し細かい話になるので、まずは大まかな流れを知りたいという人は飛ばしていただいて問題ないという意味で付けています。

Parser

Parserは,届いたパケットのヘッダ情報を解析して,のちのMatch-Action Pipelineで利用するための情報を取得します.例えば,MACアドレスIPアドレスなどです.
これらのデータは,パイプラインで利用できるような形式(メタデータ)として抽出され、Match-Action Pipelineに渡されていきます。

parse

Parserをプログラミングする箇所はparserブロックです。

parseブロックでは、startという初期状態と、acceptrejectの2種類の終了状態が定義されたステートマシンを想定しています。
acceptは「パースに成功した」、rejectは「(パケットにエラーがあるなどして)パースに失敗した」を指します*1

parseブロックは、必ずstartから始まってからどちらかの終了状態に到達しなければなりません。ただし、初期状態と終了状態以外の状態は、stateステートメントを用いて自分で定義することができます。

一般的に、パケットの各種ヘッダ(イーサネットヘッダ、IPヘッダなど)を抽出するための状態をそれぞれ定義して、start→各種ヘッダの解析状態→acceptと状態を遷移させていくことで、ヘッダを順番に抽出していきます。

P4では、transitionステートメントで状態を遷移させていきます。以下はtransitionの使い方を示したものです。ここでは単純にstartから直接acceptへと状態を遷移します。
特に処理を記述していないので何も実行されません。

parser MyParser(packet_in packet,
                out headers hdr,
                inout metadata meta,
                inout standard_metadata_t standard_metadata) {

    state start {
        transition accept
    }
}

v1model.p4の定義と比較してみます。

parser Parser<H, M>(
    packet_in pkt,
    out H hdr,
    inout M meta,
    inout standard_metadata_t std_meta
);
select

一般的には、selectステートメントを併用して状態遷移を制御し、処理を記述していくことになります。
selectステートメントは一般的なプログラミング言語のswitch-case文の使い方に非常に似ています。

実際に、TutorialsのBasicForwardingのsolutionよりソースコードを引用させていただき、状態遷移制御のやり方を確認します。
(コメントにつきましては著者が追加しております。)
github.com

parser MyParser(packet_in packet,
                out headers hdr,
                inout metadata meta,
                inout standard_metadata_t standard_metadata) {

    state start {
        transition parse_ethernet;  //1. 
    }

    state parse_ethernet {
        packet.extract(hdr.ethernet); //2. 
        transition select(hdr.ethernet.etherType) { //3. 
            TYPE_IPV4: parse_ipv4;  //4. 
            default: accept; //5. 
        }
    }

    state parse_ipv4 { 
        packet.extract(hdr.ipv4); //6. 
        transition accept; //7. 
    }

}
  1. stateステートメントで定義した「parse_ethernet」に状態を遷移させる
  2. parse_ethernet状態に遷移し、まずイーサネットヘッダを抽出する
  3. selectステートメントイーサネットヘッダのイーサネットタイプを確認する
  4. もしもイーサネットタイプがTYPE_IPV4(プログラム上部で0x800と定義されています)であれば、parse_ipv4に遷移
  5. TYPE_IPv4でなければ、これ以上抽出したいヘッダ情報はないので、accept状態に遷移してパース終了
  6. parse_ipv4状態に遷移し、IPヘッダを抽出する。
  7. これ以上抽出したいヘッダ情報は無いので、accept状態に遷移してパース終了

ちなみにこちらはIPヘッダのみを利用するという前提に立っています。それ以外のヘッダを抽出したい場合は適宜状態を定義して遷移させていくことになります。

Match-Action Unit

Parserでヘッダ情報を取り出しました。このヘッダ情報にはフローエントリのkeyとなりうる情報が含まれています。keyというのは送信元IPアドレスなどのことです。

次にやりたいことは、そのkeyに対応するactionを実行することです。すなわち、送信元IPアドレスがこうだったら転送、違っていれば廃棄、というようなことをしたいわけです。

P4では、keyとactionを定義していくことになります。

control

Match-Action Unitはマルチステージ型で構成されており、各ステージに固有のメモリとALUが搭載されているのですが、各ステージはcontrolブロックを用いて定義することができます。parserブロックで抽出されたメタデータは、controlブロックに渡されています。

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

v1model.p4の定義と比較してみます。

control Ingress<H, M>(
    inout H hdr,
    inout M meta,
    inout standard_metadata_t std_meta
);
方向*

controlに渡されている引数には、inoutというものが付けられています。これはデータの方向を示す型です。

controlや後述のactionでは、DirectionalなものとDirectionlessなものの2種類の変数を利用可能です。Directionalなものはデータプレーンから(すなわちパイプライン上から)送られて来るデータ、Directionlessなものはコントロールプレーンから来るデータです。

Directionlessなものは特に問題無いのですが、Directionalなデータに関しては別途in, out, inoutの3種類の方向を付与されます。
inはread-onlyで変更不可能なデータ、outは変更される予定のあるデータ*2につけられます。
すなわち基本的には、inとなっているデータは代入式の右側、outとなっているデータは代入式の左側で出てくることになります。

inoutはinputとしてもoutputとしても利用されるデータであり、上の例と照らし合わせれば、代入式の右側にも左側にも来ることができます。例えば、値を入れ替えるswap処理や、インクリメント、デクリメント処理をする時などに利用されます。

Directionlessなデータに関しては後述のactionにて説明します。

action

acitionの処理を記述するには、actionステートメントを利用します。

構文としては、以下のとおりです(Parserと同じくbasic.p4より引用)。一般的なプログラミング言語で言う関数定義と類似しています。

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

    action drop() { //1.
        ...
    }
    
    action ipv4_forward(macAddr_t dstAddr, egressSpec_t port) {  //2.
        standard_metadata.egress_spec = port;
        hdr.ethernet.srcAddr = hdr.ethernet.dstAddr;
        hdr.ethernet.dstAddr = dstAddr;
        hdr.ipv4.ttl = hdr.ipv4.ttl - 1;
    }
 ...
}

1. drop()というactionを定義します。
2. ipv4_forward()というactionを定義します。データプレーンのどのポートからパケットを送出するかを決定したあと、送信元MACアドレスhdr.ethernet.dstAddr(データプレーン自身)に、宛先MACアドレスをフローテーブルに従って決定された転送先dstAddr、そしてttlを1つ減らします(つまりL2での通常のルーティングの実装です)。

standard_metadata_tは、アーキテクチャ(v1model.p4)で定義されているデータで、以下があります。
egressSpec_tは、データプレーンの持つポート(ネットワークスイッチの文脈でのポート)です。

struct standard_metadata_t {
    bit<9> ingress_port;
    bit<9> egress_spec;
    bit<9> egress_port;
    bit<32> clone_spec;
    bit<32> instance_type;
    bit<1> drop;
    bit<16> recirculate_port;
    bit<32> packet_length;
    bit<32> enq_timestamp;
    bit<19> enq_qdepth;
    bit<32> deq_timedelta;
    bit<19> deq_qdepth;
    bit<48> ingress_global_timestamp;
    bit<48> egress_global_timestamp;
    bit<32> lf_field_list;
    bit<16> mcast_grp;
    bit<32> resubmit_flag;
    bit<16> egress_rid;
    bit<1> checksum_error;
    bit<32> recirculate_flag;
}
Directionlessな変数*

actionに渡している引数には今回は方向型が付けられていません。

方向型が付けられない、Directionlessなデータとしては次のような場合があります。

  1. コンパイル時に決定するデータ
  2. actionに渡されるパラメータのうち、コントロールプレーンからのみセットできるデータ
  3. actionに渡されるパラメータのうち、他のアクションによってセットされるデータ(inとして扱われる)

今回のmacAddr_t dstAddr、egressSpec_t portに渡される具体的な値は、pod-topo/s*-runtime.jsonで指定されています。
つまりこれは2.の「コントロールプレーンからセットされるデータ」であるため方向型が付けられていないということになります。
コントロールプレーンに関しては後述のP4Runtimeにて説明させていただきます。

table

keyとactionを定義するために、tableステートメントを利用します。

以下が、Parserと同じbasic.p4より引用したソースコードです。

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

    action drop() {
        ...
    }
    
    action ipv4_forward(macAddr_t dstAddr, egressSpec_t port) {
        ...
    }
    
    table ipv4_lpm {  //3.
        key = { //4.
            hdr.ipv4.dstAddr: lpm;
        }
        actions = {  //5. 
            ipv4_forward;
            drop;
            NoAction;
        }
        size = 1024;
        default_action = drop();
    }
 ...
}

3. tableステートメントを用いてテーブル情報を定義します。
4. keyとして、IPv4の宛先IPアドレスを利用します。lpmとは、logest prefix matchの略であり、マッチングのルールとして、IPアドレスのビット列の一致する長さが最長の経路を選択するという意味です。core.p4で定義されています。*3
5. actionsとして、ipv4_forward, dropを登録します。

applyブロック

tableで定義した情報を実際に適用するため、applyブロックを利用します。
controlブロックには、applyブロックがなければいけません。*4
[追記 2021.08.19 まるでその根拠があると断定しているような書き方になっていたので、大幅に編集しました]

tableで定義した情報を実際に適用するには、tableが持つapply()メソッドを呼び出します。
tableの持つapply()メソッドを呼び出すと、そのtableに格納されているフローテーブルエントリから、キーにヒットするものを検索し、対応するactionを実行します(言語仕様の13.2.3参照)。

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

    action drop() {
        ...
    }
    
    action ipv4_forward(macAddr_t dstAddr, egressSpec_t port) {
        ...
    }
    
    table ipv4_lpm {
        ...
    }
    
    apply {
        if (hdr.ipv4.isValid()) { //6
            ipv4_lpm.apply();
        }
    }
}

6. applyブロック内でテーブル情報を適用します。isValid()によってヘッダを検証し、もし有効なものであればテーブルの持つapply()メソッドで実際にテーブル情報を適用します。

apply()メソッドを呼び出しているのは、applyブロック内です。このapplyブロックは何者でしょうか。

controlブロックには、applyブロックがなければいけないと書かれている文献もありましたが、言語仕様にはそのような内容は明示的に書かれていません。

そのため、もう少し突っ込んで調べました。

言語仕様のApplendix Fを見ると、

Actions may be called directly from a control apply block.

とあります。
また、その下の表を見ると、control apply block内では、「control」、「extern」、「table」、「action」、「function」を呼び出すことが可能と書いており、さらに「table」を呼び出せるのは、control内のapplyブロックのみということが書かれています。
そして、その少し前に、

Calling a parser, control, or table means invoking its apply() method.

とあります。
上記を踏まえると、定義したtableをapply()メソッドで呼び出すためには、controlブロック内でapplyブロックを用意してそこに記述しなくてはならない、という認識のほうが正しいかもしれません。
(controlブロックにapplyブロックがなければならない、というのは、tableを用意してわざわざそれを利用しないことはない、というような意味かもしれません。もう少し調査します。)


ところで、宛先IPアドレスがどうだったらipv4_forwardを実行し、dropを実行するのかが分かりません。実際の対応付けはどこでやっているのでしょうか。

対応付けに関しては、コントロールプレーンが定義しており、データプレーンは単にそのコントロールプレーンから渡された情報をもとに処理を行います。
コントロールプレーンがgRPCを介してデータプレーンの持つフローテーブルのエントリを更新している形です(詳しくは実験・P4Runtimeの章)。

P4で記述するのは、あくまでデータプレーンで実行される処理であり、その他のことに関してはタッチしません*5

Deparser

Deparserではヘッダを再構成してパケットを作成し、それを宛先に対して送信します。
もしもヘッダデータを書き換えているのであれば、その内容が反映されます。

以下がDeparserの例です。

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

1. applyステートメントで実行する内容を記述します。
2. イーサネットヘッダをセットします。
3. IPヘッダをセットします。

packet_out packetはcore.p4に記述されています。packet.emtiによりヘッダ情報をシリアライズします。これでパケットのヘッダを、順番にセットしていっていることになります。

PISAブロックの適用

PISAの主要ブロックを適切に定義したあとは、これを実際に適用します。構文は下記のとおりです。*6

V1Switch(
    MyParser(),
    MyVerifyChecksum(),
    MyIngress(),
    MyEgress(),
    MyComputeChecksum(),
    MyDeparser()
) main;

これでデータプレーンのプログラミングが完成しました。

P4を実行する

実行環境の構築

P4ファイルを書き終えたので、今度はコンパイルのための実行環境を準備します。
まずは、P4をコンパイルするp4lang-p4cを以下のページにしたがってインストールします。
github.com

なお、今回は以下の環境で実験をします。
カーネルのバージョン [追記 2021.08.19 誤りがあったため修正しました。NetFliterの記事のものをコピーしたまま修正するのを忘れていました。]

<s>5.4.0-47-generic</s>
5.11.0-25-generic

OSのバージョン

Ubuntu 20.04 LTS

嬉しいことに、Ubuntu20.04であれば、リポジトリが用意されているのでここからインストールしてきます。

$ sudo add-apt-repository ppa:dreibh/ppa
$ sudo apt update
$ sudo apt install p4lang-p4c

自分の場合、元々の環境が悪かったのか、依存関係の問題でclangのインストールができず、p4lang-p4cがインストールできませんでした。
aptitudeコマンドを使うといくつか解決方法を教えてくれるのですが、libc6のバージョンを2.31-0ubuntu9.3から2.31-0ubuntu9.2にダウングレード(3を選択)したところ、うまくインストールできました。
unix.stackexchange.com

$ sudo aptitude install clang
...
 libc6-i386 : 依存: libc6 (= 2.31-0ubuntu9.2) 2.31-0ubuntu9.3 がインストール済みです
...

以下のアクションでこれらの依存関係の問題は解決されます:

     以下のパッケージを削除する:                                            
1)     libc-dev-bin [2.31-0ubuntu9.3 (now)]                                 

     以下のパッケージをインストールする:                                    
2)     libc-dev-bin:i386 [2.31-0ubuntu9.2 (focal-updates)]                  

     以下のパッケージをダウングレードする:                                  
3)     libc6 [2.31-0ubuntu9.3 (now) -> 2.31-0ubuntu9.2 (focal-updates)]     
4)     libc6:i386 [2.31-0ubuntu9.3 (now) -> 2.31-0ubuntu9.2 (focal-updates)]
5)     libc6-dbg [2.31-0ubuntu9.3 (now) -> 2.31-0ubuntu9.2 (focal-updates)] 
6)     libc6-dev [2.31-0ubuntu9.3 (now) -> 2.31-0ubuntu9.2 (focal-updates)] 



この解決方法を受け入れますか? [Y/n/q/?]y

さて、次はターゲットとなるBMv2をインストールします。
github.com

依存するパッケージが、すでにp4lang-p4cを入れた時に入っているような?と思いそのまま作業を進めたところうまく./configure -> makeができました。
パスを通すほどでもないなと思ったら、make installは行わずとも大丈夫です。

$ git clone https://github.com/p4lang/behavioral-model.git
$ cd behavioral-model

$  ./autogen.sh
$ ./configure
$ make
$ sudo make install

実際に実行する

実験用ネットワークの構築

コンパイラとターゲットをインストールできたので、実際に実験用のネットワーク環境を構築していきます。
なお、この構築方法については、下記記事を参考に書かせていただきました。
opennetworking.org

www.slideshare.net

P4環境の整ったVMなどを用意しても良いのですが、簡単に試す際にはNetwork Namespaceが利用されています。Network Namespaceとは、カーネル名前空間機能の一種であり、ネットワーク関連の機能を分離させて動作させることのできる技術です。コンテナ技術にも利用されています。

ホストマシンにBmv2をインストールしているので、今回は以下のような環境を想定しました。

まずは、host0とhost1というネットワーク名前空間を作ります。

$ sudo ip netns add host0
$ sudo ip netns add host1

続いて、ホストマシンとhost0、ホストマシンとhost1をつなぐvethペア(それぞれveth0とveth0-host、veth1とveth1-hostという名前にします)を作ります。

$ sudo ip link add veth0 type veth peer name veth0-host
$ sudo ip link add veth1 type veth peer name veth1-host

今度は、作ったvethをそれぞれのネットワーク名前空間に移します。veth0をhost0に、veth1をhost1に移します。

$ sudo ip link set veth0 netns host0
$ sudo ip link set veth1 netns host1

ここまでくれば、あとは必要なインターフェースにIPアドレスを付与して、up状態にします。

$ sudo ip netns exec host0 ip addr add 10.0.0.1/24 dev veth0
$ sudo ip netns exec host1 ip addr add 10.0.1.1/24 dev veth1

$ sudo ip netns exec host0 ip link set veth0 up
$ sudo ip netns exec host1 ip link set veth1 up

$ sudo ip link set dev veth0-host up
$ sudo ip link set dev veth1-host up
P4プログラムのコンパイル

次に、P4プログラムをコンパイルします。
コンパイルするプログラムは以下です。
github.com

p4cコマンドでコンパイルし、コンパイルに成功すると、basic.p4iファイルと、basic.jsonファイルが生成されます。

p4c basic.p4
ソフトウェアスイッチの起動とテーブル情報の書き込み

次に、P4プログラムの書き込みのターゲットとなるソフトウェアスイッチを起動します。
これは、simple_switchプログラムを使います(場所はbehavioral-model/targets/simple_switch)
iオプションで、スイッチのポートがどのネットワークインタフェースに紐づくかを指定できます。
ホストとhost0の通信路であるveth0-hostをポート0に、ホストとhost1の通信路であるveth1-hostをポート1に設定します。
log-consoleオプションをつけることで、ログが端末に表示されるようになります。

$ sudo ./simple_switch -i 0@veth0-host -i 1@veth1-host --log-console <p4cで生成されたjsonファイルのパス>

今度は、CLIプログラムでソフトウェアスイッチにテーブル情報を書き込みます。
プログラムは/behavioral-model/targets/simple_switchにあります。

コマンド名はhelpで確認でき、またコマンドの使い方は help <コマンド名> で確認できます。

./runtimeCLI

     :
     :

RuntimeCmd: help

Documented commands (type help <topic>):
========================================
act_prof_add_member_to_group       set_crc16_parameters
     :
     :

RuntimeCmd: show_tables
MyIngress.ipv4_lpm             [implementation=None, mk=ipv4.dstAddr(lpm, 32)]

RuntimeCmd: show_actions
MyIngress.drop                 []
MyIngress.ipv4_forward         [dstAddr(48),	port(9)]
NoAction                       []

早速、ソフトウェアスイッチにテーブル情報を書き込みます。
スイッチに「宛先IPアドレスが10.0.0.1宛てのパケットが来たら、host0のMACアドレスに転送」、
「10.0.1.1宛のパケットが来たらhost1のMACアドレスに転送」というルールを書き込むことにします。

このような場合にはtable_addコマンドを使います。
help table_addとすることで、コマンドの書式を確認することが出来ます。

table_add <table name> <action name> <match fields> => <action parameters> [priority]

今回の例では、どのように書けば良いでしょうか。P4プログラムのMyIngressの箇所を見てみます。
ここでは、コメントに書いたとおりの対応関係になります。

  • table name : ipv4_lpm
  • action name : ipv4_forward
  • match fileds : ホスト0 or ホスト1のIPアドレス(宛先として)
  • action parameters : ホスト0 or ホスト1のMACアドレス(宛先として)、ソフトウェアの出力ポート
control MyIngress(inout headers hdr,
                  inout metadata meta,
                  inout standard_metadata_t standard_metadata) {

    action drop() {
        ...
    }
    
    action ipv4_forward(macAddr_t dstAddr, egressSpec_t port) { // action parameters (dstAddrとport)
        ...
    }
    
    table ipv4_lpm {  // table name
        key = {
            hdr.ipv4.dstAddr: lpm;  // match fields(host0あるいはhost1のIPアドレス/プレフィックス)
        }
        actions = {
            ipv4_forward;  // action name
            drop;
            NoAction;
        }
        size = 1024;
        default_action = drop();
    }
 ...
}

それでは早速、ルールを追加していきます。
10.0.0.1のときはパケットの宛先MACアドレスをhost0のものにし、その出力ポートをveth0-hostのポートである0(スイッチ起動時に指定したもの)にします。
10.0.1.1のときはパケットの宛先MACアドレスをhost1のものにし、その出力ポートをveth1-hostのポートである1にします。

RuntimeCmd: table_add MyIngress.ipv4_lpm MyIngress.ipv4_forward 10.0.0.1/24 => host0のMACアドレス 0
Adding entry to lpm match table MyIngress.ipv4_lpm
match key:           LPM-0a:00:00:01/24
action:              MyIngress.ipv4_forward
runtime data:        host0のMACアドレス	00:00
Entry has been added with handle 0
RuntimeCmd: table_add MyIngress.ipv4_lpm MyIngress.ipv4_forward 10.0.1.1/24 => host1のMACアドレス 1
Adding entry to lpm match table MyIngress.ipv4_lpm
match key:           LPM-0a:00:01:01/24
action:              MyIngress.ipv4_forward
runtime data:        3a:e3:c5:6e:85:19	00:01
Entry has been added with handle 1

table_dumpコマンドでテーブル内容を確認してみると、以下のようになっています。

RuntimeCmd: table_dump MyIngress.ipv4_lpm
==========
TABLE ENTRIES
**********
Dumping entry 0x0
Match key:
* ipv4.dstAddr        : LPM       0a000001/24
Action entry: MyIngress.ipv4_forward - a230e2b741d3, 00
**********
Dumping entry 0x1
Match key:
* ipv4.dstAddr        : LPM       0a000101/24
Action entry: MyIngress.ipv4_forward - 3ae3c56e8519, 01
==========
Dumping default entry
Action entry: MyIngress.drop -
パケットを実際に送ってみる

さて、ここから実験を行います。
簡単にScapyで下記のようなプログラムを書きました。
host0からhost1に対して、あるいはhost1からhost0に対してパケットを1つ送信します。

from scapy.all import *
import sys

switch_veth0_mac = "52:0b:93:d7:52:51"
switch_veth1_mac = "36:86:0a:a1:c7:e5"

host0_mac = "a2:30:e2:b7:41:d3"
host1_mac = "3a:e3:c5:6e:85:19"

host0_ip = "10.0.0.1"
host1_ip = "10.0.1.1"

if len(sys.argv) < 2:
    print(f"Usage {sys.argv[0]} <mode | 0=host0, 1=host1>")
    exit()

mode = sys.argv[1]
print(mode)

if mode == "0":
    ether = Ether(dst=switch_veth0_mac, type=0x0800)
    ip = IP(dst=host1_ip)
    request = (ether/ip)
    sendp(request, iface="veth0")
else:
    ether = Ether(dst=switch_veth1_mac, type=0x0800)
    ip = IP(dst=host0_ip)
    request = (ether/ip)
    sendp(request, iface="veth1")

では、host0からhost1に向かってパケットを送信します。

$ sudo ip netns exec host0 sudo python3 test.py 0

simple_switchを起動した画面を確認すると、キー(宛先IPアドレス)がテーブルエントリにマッチしていることがわかります。

Match key:
* hdr.ipv4.dstAddr    : LPM       0a000101/24
Action entry: MyIngress.ipv4_forward - host1のMACアドレス,1,

host1上でtcpdumpを使って確認しておくと、パケットが届いていることがわかります。

$ sudo ip netns exec host1 sudo tcpdump -i veth1
IP 0.0.0.0 > 10.0.1.1:  hopopt 0

host1からhost0に向かってパケットを送信してみても同様です。

$ sudo ip netns exec host1 sudo python3 test.py 1
Match key:
* hdr.ipv4.dstAddr    : LPM       0a000001/24
Action entry: MyIngress.ipv4_forward - host0のMACアドレス,0,
$ sudo ip netns exec host0 sudo tcpdump -i veth0
IP 0.0.0.0 > 10.0.0.1:  hopopt 0

P4Runtime

P4言語のコンパイルから実行まで確認してみました。

流れをまとめると、次のようになります。

  1. P4でプログラムを記述
  2. P4プログラムをコンパイルしてデータプレーンのターゲットに合わせたバイナリデータを生成(ベンダーの提供しているアーキテクチャモデルの定義ファイルも含まれる)
  3. バイナリデータをデータプレーンにロード
  4. データプレーンのテーブル情報を作成・更新・削除

肝となるのは、4番目の操作です。これはどのようにすれば良いでしょうか。例えば、P4コンパイラコンパイルすると、データプレーンにアクセスするためのAPIが自動生成されます。このAPIを使ってコントロールプレーンの機能を実装すれば実行可能です。しかし、このAPIは、作成したP4プログラムに依存しています。そのため、P4プログラムを書き換えてしまうとAPIが変わってしまい、再度コントロールプレーンを実装し直さなければなりません。

またターゲットとなるハードウェアによってはCLIが用意されており、これを用いてアクセスすることも可能です。先程の例では、BMv2で提供されている「runtimeCLI」というプログラムを利用したと思います。これでP4プログラムに依存せずにアクセスが可能となりますが、これはターゲットとなるハードウェアに依存してしまいます。

汎用的に制御できるように、P4ではP4Runtimeと呼ばれるフレームワークでP4の実行基盤を制御することができるようになっています。P4Runtimeでは、コントロールプレーンとデータプレーンの間をgRPCでやり取りさせています。これにより、プログラムやハードウェアに依存しないAPIの提供を実現しています。

ワークフロー

P4プログラムをコンパイルすると、P4Infoと呼ばれる形式のファイルが生成されます。
これには、P4プログラムで定義したテーブルやアクション、パラメータなどのIDや、その構造に関する情報が含まれています。
このP4infoファイルはgRPCで使えるようにProtobufという書式で書かれています。
P4infoファイルがコントロールプレーンとデータプレーンでロードされることで、両者でP4に関する各種情報を参照することができるようになります。

今回のチュートリアルでは、P4Runtimeというページにて演習することが可能です。
github.com


ここでは、mycontroller.pyというPythonプログラムによってコントロールプレーンが実装されています。
github.com


なお、このコントロールプレーンのプログラムは、今まで見てきたbasic.p4ではなく、下記のbasic_tunnel.p4に対応するものであるため、一部違う点があります。
(basic_tunnel.p4に関しては、https://github.com/p4lang/tutorials/blob/master/exercises/basic_tunnel/basic_tunnel.p4を参照)

(以下既存のコメントを削除、新規のコメントに関しては著者が追加)

mycontroller.py
...
import p4runtime_lib.helper #1.
...
def main(p4info_file_path, bmv2_file_path):
    p4info_helper = p4runtime_lib.helper.P4InfoHelper(p4info_file_path) #2. 

    try:
        #3. 
        s1 = p4runtime_lib.bmv2.Bmv2SwitchConnection(
            name='s1',
            address='127.0.0.1:50051',
            device_id=0,
            proto_dump_file='logs/s1-p4runtime-requests.txt')
        s2 = p4runtime_lib.bmv2.Bmv2SwitchConnection(
            name='s2',
            address='127.0.0.1:50052',
            device_id=1,
            proto_dump_file='logs/s2-p4runtime-requests.txt')

        ...

        # 4. 
        s1.SetForwardingPipelineConfig(p4info=p4info_helper.p4info,
                                       bmv2_json_file_path=bmv2_file_path)
        print "Installed P4 Program using SetForwardingPipelineConfig on s1"

        s2.SetForwardingPipelineConfig(p4info=p4info_helper.p4info,
                                       bmv2_json_file_path=bmv2_file_path)

        print "Installed P4 Program using SetForwardingPipelineConfig on s2"

        # 5. 
        writeTunnelRules(p4info_helper, ingress_sw=s1, egress_sw=s2, tunnel_id=100,
                         dst_eth_addr="08:00:00:00:02:22", dst_ip_addr="10.0.2.2")
  1. P4Runtimeを利用するためのライブラリをインポートします。
  2. P4Infoに関するヘルパーを定義(コンパイラに生成されたP4Infoファイルをロードし、その設定に基づいてデータがセットされる)
  3. Bmv2SwitchConnection()により、データプレーンとの接続試行(gRPCコネクション)
  4. SetForwardingPipelineConfig()によってパイプラインの設定をデータプレーンにインストール
  5. データプレーンに対して新規のルールを書き込み

SetForwardingPipelineConfig()は、後述のWriteTableEntry()と同様、P4RuntimeのProtobuf定義に基づくものです。

writeTunnelRulesは上部に定義されています。

def writeTunnelRules(p4info_helper, ingress_sw, egress_sw, tunnel_id,
                     dst_eth_addr, dst_ip_addr):
    ...
    # 1) Tunnel Ingress Rule
    table_entry = p4info_helper.buildTableEntry(
        table_name="MyIngress.ipv4_lpm",  # 1.
        match_fields={
            "hdr.ipv4.dstAddr": (dst_ip_addr, 32)   # 2.
        },
        action_name="MyIngress.myTunnel_ingress", # 3.
        action_params={
            "dst_id": tunnel_id,   # 4.
        })
    ingress_sw.WriteTableEntry(table_entry) # 5.
    print "Installed ingress tunnel rule on %s" % ingress_sw.name
    ...
  1. テーブル名として、P4プログラムで定義したMyIngressのipv4_lpmテーブルを指定
  2. マッチフィールド(key)となる箇所に"10.0.2.2/32"を指定
  3. 対応するアクションとして、MyIngressのmyTunnel_ingressを指定
  4. アクションの持つパラメータ"dst_id"として、tunnel_idの持つ値を指定
  5. WriteTableEntry()によってデータプレーンにルールをインストール

なお、これは今回のチュートリアルで用意されているソースコードにしたがったものであり、これが汎用的な書き方というわけではありません(WriteTableEntry()など、P4RuntimeのProtobuf定義に基づくものを除く)。

gRPCの使い方がいまいちわかっていないので今後調査しようと思います。

[2022.07.03 追記]
調査した結果について新しく書きました。
madomadox.hatenablog.com

まとめ

P4はSDN環境においてデータプレーンの処理を記述するためのプログラミング言語です。
P4ではPISAと呼ばれるパイプライン処理が想定されており、この各種ブロックの処理を実装していくことになります。
また、P4Runtimeと呼ばれるP4のランタイムフレームワークにより、コントロールプレーンとデータプレーンの汎用的な制御を行うことができます。

最後に、簡単な実験によって、P4プログラムが動作することを確認しました。
今後はgRPCの調査を深め、P4Runtimeに関する記事を書いていきます。

*1: acceptとrejectに到達した場合の処理はアーキテクチャ側(BMv2)で定義されます。rejectになった場合は色々と方策を取れます。例えば、これ以上処理をしないようにしたり、エラーログだけ吐いてパースに成功した分だけ後続に渡したりなどです。言語仕様の12.3の"An architecture must specify the behavior when the accept and reject states are reached. "以降参照。

*2:P4ではL値と呼ばれます

*3:match_kind型。他にexact, ternaryがあります。

*4:言語仕様には明示的に書かれていないのですが、リンク先にはそう書かれています。、下記リンクでは "Every control block must contain an 'apply' block." とあります。https://github.com/jafingerhut/p4-guide/blob/master/demo1/demo1-heavily-commented.p4_16.p4

*5:ここがずっと不可解で、勉強会に参加してようやく腑に落ちました

*6:チェックサムの計算や検証など、今回定義していないものもあります。

2020年に読んだ本を振り返る

はじめに

2020年の12月には書いていたのだけど、完全に書いたのを忘れていた。

2020年はひと月に一冊は本に触れようという目標を立てていた。

備忘録として今年触れた作品について簡単な所感を並べていく。

こうしてみると、ひと月で読むには無理のある分量の本を買いすぎな気がする。

小説

ロリータ(ウラジーミル・ナボコフ、訳 若島正

ロリータという少女を取りつかれたように愛してしまった中年男性ハンバート・ハンバートの物語。

「恋愛小説であると同時に、ミステリでもありロード・ノヴェルであり、…」という書評のままの内容で、たとえ内容を紹介してみてくださいと言われたとしても答えに窮する感じだった。色んなネタを仕込んでいるのであろう、註釈の量がえげつなかった。

ロリータとの出会いを書いた第一部、いろいろあってロリータと旅をしに出る第二部との二つに大きく分かれるが、個人的に第二部が好きだった。車を走らせながらロリータと古びたラブホテルを点々としていく旅は、始終不穏な終わりしか感じられない。

驟雨・原色の街(吉行淳之介

吉行淳之介の短編集。表題作はいずれも娼婦が関わる作品。ロリータに続けて読んだので倫理観が終わってしまった。

倫理観がどうという話は実際はこの小説を語るのに無粋な問題で、人間の複雑な感情を性を通して異常なほどつぶさに描写しているという印象があった。

「原色の街」という題に似つかわしくない澱んだ色彩と、閉塞感が好きだった。

恐るべき子供たちジャン・コクトー、訳 東郷青児

第一次大戦後のパリが舞台。ポールとその姉エリザベート、友人のジェラール、アガートの4人が織りなす愛憎の物語。

ニンフの視座を持ち合わせていて、現世の人間とはどうあっても交差できない人間が好きで、その例に漏れずこの小説も好きだった。

子供時代にあるコントロールできない暴力性と人生に投げやりな態度がなんとなく好きで、「部屋」を作り出してそこで社会から抜けて退廃的に生きている子供たちの様子が最高だった。

花・死人に口なし 他7編(シュニッツラー、訳 番匠谷栄一・山本有三

死や罪、愛についての感情の機微が丁寧に書かれていて、分かる〜となることが多かった。バカの感想。特に「死人に口なし」の狼狽具合は自分も同じ状況であればそうなるであろうことは想像に難くない。文春砲にあった時の気持ちを味わえた。

「花」「死人に口なし」「わかれ」などで描写される、死人となった方がむしろ生きている人間に多弁に語り掛けてくるような感覚は分かる。

技術書

時系列解析 自己回帰型モデル・状態空間モデル・異常検知【Advanced Python 1】(島田 直希)

ARIMAなどの時系列解析を実装してみたかったので購入。個人的にこういう題だとフルスクラッチで実装するのかと期待してしまったが、ライブラリでの実装だった。マッチングがうまくいかなかった。

数多くの時系列解析の教科書で挫折した初心者の自分にとって、読むにはちょうど良い内容だった。この本のおかげで難易度高めの本もある程度読めるようになった。

時系列解析入門 [第2版]: 線形システムから非線形システムへ (SGCライブラリ 160)(宮野 尚哉, 後藤田 浩)

第1版を図書館で読んだことがあったが、新たに第2版でエントロピーの箇所などが加筆されたと聞いて購入。AR,MA,ARIMAなどについて丁寧に導出されていて非常によく理解できた。カオス理論の箇所はSF小説を読んでいるような気持ちになった。

エントロピーとカオス理論にここまで関連があったのかと驚いた。

個人的にサンプルエントロピーの話が面白かった。

ベイズ深層学習(須山敦志)

ベイズ推定とニューラルネットワークの基礎と、分布サンプリング法、ベイズニューラルネットワークと盛り沢山な内容で非常に勉強になる内容だった。

レビューで初心者でも分かりやすく丁寧と言われていたが、流石にそれは嘘だと思う難易度だった。少なくともベイズニューラルネットワークそれぞれの基礎が無いと理解は進まないと思う。

しかし丁寧であることは紛れもなく真実だった。後半部で出てくる式変形は基本的に全て本書の前半部の内容でカバーされているため、非常に分かりやすかった。いまいち理解が進まない箇所があるが、単に自分に数学的素養がないのが原因で本書の問題ではありません。

個人的に確率分布の式変形をしてカルバックライブラーダイバージェンスと期待値をあっちこっちする部分の数式がパズルのようで面白かった。

ハッカーの学校 IoTハッキングの教科書(黒林檎、村島 正浩)

ハードウェアハッキングの知識が全くなく、勉強したかったため購入した。

インターネットで公開されていたセキュリティキャンプの資料を参考に、たぶんこれじゃないかと推測しながら道具を揃えて、持っていたラズパイでSPIのところまではできた。ケチ臭い性分が発揮されている。念の為、本で得た知識は断じて悪用していませんし、今後決してすることはありません。

テスターなど高価で手が出なかった道具は揃えられなかったのでそこらへんはまた機会があればやりたい。

RFCの読み方―インターネット技術の公式仕様書(瓜生 聖, 秋月 昭彦)

下記記事を書くために図書館で借りた。

RFCに苦手意識があったのだけど、この本のおかげですらすら読めるようになった。

簡単な例から初めて少しずつ複雑なRFCにシフトしていく内容で無理なく知識を身に着けることができた。

2004年の本であるが内容は全く色褪せていないと感じた。

madomadox.hatenablog.com

Distributed Denial of Service Attacks: Real-world Detection and Mitigation (İlker Oezçelik, Richard Brooks)

自分の研究分野に近い内容だったので即ポチした。9千円近くとえらい値段が高かった。この手の値段とフォーマットの英書をよく見かけるけれどある程度こうしたアカデミックな本を発刊する機会があるのだろうか。

DDoSの歴史、ツールの種類、検知方法、緩和方法、実験環境の構築方法など有益な情報があって助かった。これを研究を始めたばかりで無駄サーベイ・開発に費やしてしまった頃の自分に読ませたい。

Deflectという技術があるのを知らなかった。調べてみたがいったん公開停止されているような気がする。

 

積んでしまった本

マシンリソースの不足、興味が持続できなかったという理由でほんの一部だけ読んだだけの本。

コンピュータシステムの理論と実装 ―モダンなコンピュータの作り方(Noam Nisan, Shimon Schocken 訳 斎藤 康毅)

低レイヤーに対する苦手意識を克服するべく購入したが途中で苦手意識が肥大化して興味が薄れてしまった。来年できるといいなぁ。

つくりながら学ぶ!PyTorchによる発展ディープラーニング(小川雄太郎)

最新のディープラーニング技術を勉強したくて購入。

マシンリソースが足りず学習ができなかったため1章だけ読んで放置してしまった。

 

プログラミング言語C++ 第4版(ビャーネ・ストラウストラップ、訳 柴田 望洋)

目を通してSTLなどがそういうことだったのか~とはなったが全部は読めなかった。この手の本はリファレンスとして必要に応じて読まないとモチベーションが続かない感がある。プログラミング言語の本は何か一冊持っておくべきということだったので購入したが、本当にその通りだなぁと感じた。言語の特徴などを分からずにぐちゃぐちゃに書いてしまうため。

Effective PythonPythonプログラムを改良する90項目~ (Brett Slatkin、訳 石本 敦夫・黒川 利明)

2章まで読んだけれど、上と同じように感じて持続できなかった。とはいえ目が鱗の情報が多くて買って良かった。

コンピュータネットワーク 第5版(アンドリュー・S・タネンバウム,デイビッド・J・ウエザロール,訳 水野忠則・相田仁・東野輝夫・太田賢・西垣正勝・渡辺尚)

興味のあった輻輳制御アルゴリズムQoSだけ読んで放置してしまった。他の部分も必要に応じて読んだけれども、業務などに利用するには少し古かったりする箇所があるなぁという印象。

トラフィック測定のためのサンプリング技術についてまとめる(Sample and Hold法)

はじめに

ネットワークトラフィック測定技術、ストリーミングアルゴリズムに興味があり色々と調べているのですが、今回はサンプリング技術についてまとめようと思います。
その中でも、2002年に発表されたSample and Hold法を提案している論文[1]を読んだので手法部分にフォーカスしてまとめます。
一部数式の記号を変更している箇所がございます。また、認識に誤りがある箇所がございましたらご指摘いただけますと幸いです。可及的速やかに訂正いたします。

[1] Cristian Estan and George Varghese, (2002). “New Directions in Traffic Measurement and Accounting.” In Proceedings of the Conference on Applications, Technologies, Architectures, and Protocols for Computer Communications, SIGCOMM, pp. 323–336, New York, NY, USA: Association for Computing Machinery.

論文のURLはこちらになります(フリーアクセスです)。
https://dl.acm.org/doi/10.1145/633025.633056

トラフィック測定とサンプリング技術

単純なトラフィック測定方法

ネットワークトラフィックの様態を知るためその特徴量を正確に把握することが求められます。その際、監視対象をフロー単位でまとめることが一般的です。

フローとは、通信の同一性を区別するために用いられる情報です。一般的には「送信元IPアドレス」「宛先IPアドレス」「送信元ポート番号」「宛先ポート番号」「プロトコル(IPヘッダ)」の5つの組で表現されます。

フロー情報で通信の同一性を区別できるので、フロー単位でトラフィックを監視して、単位時間ごとにその出現回数をカウントすれば、ネットワークトラフィックのパケットレートやエントロピー値などの特徴量を計算することができるようになります。なお、次のインターバルに移った時にはカウンタの値はクリアします。

単純な方法での問題点

しかしながら、ネットワークトラフィックの流量は大規模かつ高速であるため、単純な方法で実装してしまうと次のような問題点が発生します。

  • メモリ消費量の問題:出現回数を数えるということはメモリにフロー用のカウンタ(連想配列が主)を用意する必要があります。IPアドレスは理論的に2^{32}種類あることを考えればフローの種類数は膨大であり、出現回数も非常に多いことが予想されるので、メモリを大量に消費する可能性があります。
  • スケーラビティの問題: 1パケット到着するたびにフローの出現回数を更新していくという方法では、単位時間あたりのメモリアクセス回数が膨大に増加してしまい、大量にトラフィックが到着したときに対応できなくなる可能性があります。

サンプリング法

メモリアクセス回数増加とメモリ消費量という課題に対して、サンプリング法が利用されています。サンプリング法を利用したものとしては、NetFlowなどが有名です。

サンプリング法では、サンプリングしたパケットのみを利用してフローの出現回数をカウントします。これにより処理対象となるフロー数が制限されることになりメモリ消費量を小さくできる可能性があります。また、サンプリングされたフローに関してだけメモリ中のカウンタを更新するため、通常の方法に比べてメモリアクセス数も少なくなります。

サンプリング法では単位時間(あるいはバイト数)ごとに、所与の確率p(=サンプリングレート)で、その間に到着したパケットそれぞれについてサンプリングするかどうか決めます。パケットのサンプリングに成功した場合は、パケットのフロー情報を取り出しそのフローに対応するカウンタ(以降フローカウンタ)を作ります。すでにフローカウンタが作られている場合は、出現回数を加算します。サンプリング処理を繰り返していって、フローカウンタを更新していくことで、フローごとの出現回数を算出します。

しかし、当然ながらサンプリングされたパケットのみを監視対象としてカウントしていくので、出現回数の推定値の誤差が大きくなることが予想されます。

Sample and Hold法

Sample and Hold法はサンプリング法の長所はある程度残しつつ、推定精度を向上させるために考案されました。
それでも推定精度にはまだ難はあり、同論文で別の手法も提案されていますが、実装が単純でリソースの消費量も少ないという点が利点です。

Sample and Hold法はサンプリング法と同じく所与のサンプリングレートpでパケットを取得するのですが、その前にエントリが存在しているかどうかを確認します。
そして、フローカウンタが存在する場合は、サンプリングレートに関係なくパケット情報を取得し、対応するエントリを更新します。
フローカウンタが存在しない場合は、通常のサンプリング法と同様の処理を行います。

その違いについて直感的に捉えてみると、単純サンプリング法はすべてをサンプリングするので、真の出現回数との差が大きくなってしまう可能性があります。
一方、Sample and Hold法は一度サンプリングされたフローに関してはそれ以降取りこぼすことがないため、正確性が増します。ただしメモリアクセス回数はサンプリング法に比べて増加することがあります。

Sample and Hold法の理論的エラー率

論文では、その精度についても分析をしています。まずはフロー数の推定精度、そしてフローの観測成功確率をご紹介します。

フロー数についての推定精度

まずはフローの出現回数についての推定精度です。あるフローの実際の出現回数をs、Sample and Hold法による推定出現回数をcとして、その推定誤差を\sqrt{E[(s-c)^{2}}]という式で評価することにします。

このSample and Hold法では一度でもサンプリングされれば情報をずっと保持します。つまり、推定出現回数cであるということは、本来s回観測されるはずだったがそれまでに(s-c)回サンプリングに失敗したということになります。

成功するまでにx = (s-c)回サンプリングを失敗した場合を考えると、このときの確率を(1-p)^{x}pという式で表すことができます。

このときxは幾何分布(Geometric Probability Distribution)に従い、それぞれ期待値 E[x] = \frac{1}{p}、分散はVar[x] = \frac{1-p}{p^{2}}標準偏差SD[x] = \frac{\sqrt{1-p}}{p}となります。

ここで一般的な分散の定義式を変形すれば、二乗誤差の期待値E[x^{2}] = V[x] + E[x]^{2}となるので、

E[x^{2}] = V[x] + E[x]^{2} = \frac{1-p}{p^{2}} + \frac{1}{p^{2}} = \frac{2-p}{p^{2}}

したがって、

\sqrt{E[(s-c)^{2}}] = \frac{\sqrt{2-p}}{p} となります。

観測成功率

メモリ容量的にフローテーブルを持てる数に限りがある場合を考えます。限りがあるということは、フロー数が多すぎると新規にフローテーブルを作れなくなるため、出現回数を全くカウントできないフローが存在する可能性があります。

ここからは、論文に示されている例示を紹介します。

ある測定期間中に、メモリ許容量を1%超える数のフローで構成されたトラフィック流入してきたとし、その1%のフロー数は最大で100あるとします。
この時確保されるであろうフローカウンタは、100%のフロー数が10,000だとすれば、直感的には10,100必要になるはずです。
しかしここでは、フローメモリの限界が10,000だとします。トラフィックがメモリ許容量を超えている時のフローの観測成功率を考えます。

フローカウンタ数は10,000に抑えたいため、もしトラフィックのデータ量がBバイト到着するときは、サンプリングレートp=10,000/Bに設定します(この例、というより論文全体では単位バイト数ごとにパケットのサンプリングを行います)。これで多くの場合は10,000種類のフローだけが取得されるようになります。

この時、メモリ許容量を1%超えるフローをFとして、その観測成功率を求めます。サンプリングレートが10,000/Bであることを考えると、
FB/100バイト以上は送信されていることになります。(Bの1%)。

このFが全くサンプリングされない確率を考えると、 (1-10000/B)^{B/100}となります。
この値は、 B/100 = B^{\prime}とおけば、 (1-10000/B)^{B/100} = (1-100/B^{\prime})^{B^{\prime}} と変形できます。

これを \displaystyle \lim_{x \to \infty} \left( 1+\frac{x}{n} \right)^{n} = e^{x}と照らし合わせてみると、

 (1-100/B^{\prime})^{B^{\prime}} \approx e^{-100}となります。この100は、先程から考えている、メモリの許容量を1%超えたフロー数Fと一致します。

つまり、メモリ許容量を超えたたために全くサンプリングされない確率は、そのはみ出ているフロー数をfとすると、 e^{-f}で近似することができます。
すなわち、メモリの許容量を超えているトラフィックが到着した際に、許容量を超えているフロー数fについてフローカウンタが存在する確率は 1-e^{-f}で近似できます。

では、Fの5%のフロー数(5つ)が送信された時、それに対応するフローカウンタが存在する確率を確認します。

全くサンプリングされない確率はe^{-5}で近似できるということなので、 1-e^{-5}となり、フローカウンタが存在する確率は99%を超えることになります。

(自分の)TODO

※この章は論文著者が示した今後の課題ではなく自分のやり残したことを書いています。

単純な方法では色々と不都合があるので、同論文ではSample and Hold法以外にもMultistage Filter法という手法を考案しています。
また、これらの2つの手法をさらに改善する方法として、3つの工夫にも言及しています。

この記事はSample and Hold法単体に目を向けたものですので、これらについては別の記事でまとめようと思います。

まとめ

ネットワークトラフィックの様態を知るためその特徴量を正確に把握することが求められます。その際、監視対象をフロー単位でまとめることが一般的です。

単純に実装してしまうとメモリ消費量とメモリアクセス回数が膨大になり処理効率が悪くなってしまいます。これを解決するためサンプリング法によって計算対象となるパケット数を減少させる方法が利用されています。

一方、サンプリング手法も推定精度に難があります。そこで推定精度を改善するため、一度サンプリングされたフローはその後常にカウント対象とするSample and Hold法が考案されました。

理論的な推定誤差・メモリ許容量を超えた時にフローカウンタが存在する確率については、論文に詳細に書かれた例示と数式をもとに自分なりにまとめました。

実証実験などの詳細については論文をご覧ください。
(ブログで詳細に正確に書こうとするともはや翻訳のようになり、著作権的にまずいのではないか…というのと、正直しんどいというのとがあります)。

勉強に利用した文献

T. H. Cormen, C. E. Leiserson, R. L. Rivest, (1992). Introduction To Algorithms 1st Edition, MIT PRESS, (T.H. コルメン,C. E. ライザーソン,R. L. リベスト 浅野哲夫,岩野和生,梅尾博司,山下雅史,和田幸一 (共訳) (1995). 『アルゴリズムイントロダクション [第1巻] 数学的構造とデータ構造 』 近代科学社 初版)
※図書館で借りたんですが丁寧に書いてあってめちゃくちゃわかりやすかったです。

NetFilterを利用したパケットフィルタリングプログラミング

はじめに

iptablesについて調べていると,どうやらNetFilterの仕組みを自分でも利用できるということを知りました.
実際にそれを用いてパケットフィルタリングのプログラムを作成します.

作成プログラム

8.8.8.8に対するICMPトラフィックをフィルタリングするプログラム

関連知識

LKM (Loadable Kernel Module)

LinuxのNetFilterの仕組みを用いて自分なりの機能を作成するには,NetFilterに関するLKM(Loadable Kernel Module)を新たに作成してカーネルにロードする必要があります.

これにより,ネットワークスタック関連の機能をフック(プログラムのある特定の箇所に独自の機能を追加・挿入)できるため,マシンにパケットが到着したり,出ていったりする時に動作させたいコールバック関数を実装してフィルタリングすることができます.

LKMはカーネルの機能を拡張するための追加モジュールです.

Linuxシステムプログラムに新たに機能を追加・変更しようとすると,カーネルのプログラム全体をリビルドすることになり,効率が悪くなってしまいます.

そこで,変更頻度が少ない基本的なモジュールは元々カーネルに組み込んでおいて,変更頻度の高いモジュールはLKMを用いて導入することにします.LKMでは動的にモジュールを追加できるため,カーネルのプログラム全体をリビルドする必要が無くなります.

LKMをロードすることで,ロードされたモジュールは特権モードで実行され,システムのハードウェアを利用することができます.アンロードも同様に可能なので,必要なくなったら削除することもできます.

LKMについては下記が詳しいです.
www.atmarkit.co.jp

NetFilter

NetFilterはネットワークスタック関連の機能をフックできるLinuxカーネルフレームワークで,主にパケットフィルタリングに利用されます.

Linux関連でのファイアウォールのツールとして一般にiptablesが利用されていますが,それはNetFilterの機能を利用して実装されています.

NetFilterと,フック可能なタイミングについては下記が詳しいです.
qiita.com
wiki.bit-hive.com

実装

プログラムがカーネルに組み込まれるため,あんまり変なフィルタリングの設定をしてしまうと普段使いの際に悪影響を及ぼします.
また,できる限り自前で持っているプログラムを用いて動作確認できる方が望ましいと考えました.
そのため,自システムから8.8.8.8に対して送信されるICMPトラフィックをフィルタリングするようにして,pingコマンドを用いてそれを確認します.

環境

カーネルのバージョン

5.4.0-47-generic

OSのバージョン

Ubuntu 20.04 LTS

ソースコード

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/ip.h>
#include <linux/inet.h>
#include <linux/netdevice.h>
#include <linux/netfilter_ipv4.h>
#include <linux/skbuff.h>

MODULE_LICENSE("GPL");

//登録するコールバック関数の定義
//sk_buff型の変数skbが引数として渡される。
static unsigned int handle_hook(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
        // skbからネットワークヘッダを取得
        struct iphdr *iph = (struct iphdr *)skb_network_header(skb);

        // パケットの宛先IPアドレスが8.8.8.8であり、IPヘッダのプロコトル情報が0x01(ICMP)の場合
        if((be32_to_cpu(iph->daddr) & 0xffffffff) == 0x08080808 && iph->protocol == 0x01){
                return NF_DROP //パケットをDROP(破棄);
        }

        return NF_ACCEPT; //パケットをACCEPT(送信)
}

// 登録するフックのルール
static struct nf_hook_ops hook_ops = {
        .hook   = handle_hook, //コールバック関数
        .pf     = PF_INET, //IPV4
        .hooknum = NF_INET_LOCAL_OUT, //フックのタイミングはローカスシステムから外部にパケットが送信されるとき
        .priority = NF_IP_PRI_FILTER, //フィルタのフック処理
};

//モジュールのロード時の処理
//モジュールのロード時にnf_register_net_hook関数でフックを登録
int init_module(){
        int err;
        err = nf_register_net_hook(&init_net, &hook_ops); //フックを登録

        if(err < 0){
                return err;
        }

        return 0;
}

//モジュールのアンロード時の処理
void cleanup_module(){
        nf_unregister_net_hook(&init_net,&hook_ops); //フックの登録を解除
}
        

実験

pingコマンドを用いて作成したLKMがきちんと動作しているか確認することにします.
mymodule.cというファイル名でコードを作成したことを想定します.

コンパイル方法

1. LKMのためのMakefileの作成

obj-m := mymodule.o

LKMを作成するための最小限の内容にしています。
2. makeによるコンパイル

$ make -C /lib/modules/$(uname -r)/build M=$(pwd) modules

コンパイルに成功すると、mymodule.koファイルが作成されます。
3. モジュールをロード

 $ sudo insmod mymodule.ko

4. モジュールがロードされたか確認

$ lsmod
Module                  Size  Used by
mymodule               16384  0
:
:
:

5. モジュールをアンロードする場合

$ sudo rmmod mymodule

pingコマンドによる動作確認

$ ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) バイトのデータ
ping: sendmsg: 許可されていない操作です
ping: sendmsg: 許可されていない操作です
ping: sendmsg: 許可されていない操作です
$ ping 192.168.10.1
PING 192.168.10.1 (192.168.10.1) 56(84) バイトのデータ
64 バイト応答 送信元 192.168.10.1: icmp_seq=1 ttl=63 時間=21.2ミリ秒
64 バイト応答 送信元 192.168.10.1: icmp_seq=2 ttl=63 時間=10.8ミリ秒
64 バイト応答 送信元 192.168.10.1: icmp_seq=3 ttl=63 時間=9.37ミリ秒

8.8.8.8に対してのICMPトラフィックは送信されず、その他のIPアドレスに対してのものは送信していることがわかります。

まとめ

NetFilterを用いたパケットフィルタリングプログラムの実装方法を調べました。

NetFilterの機能を利用するには、NetFilterに関するLKMを作成してフック処理を登録する必要があります。

実験用のプログラムとして、8.8.8.8に対するICMPトラフィックをフィルタリングするプログラムを作成し、pingコマンドによる簡単な実験を行いました。