お腹.ヘッタ。

関数型とかセキュリティとか勉強したい。進捗つらぽよ

XDP bpf_fib_lookup

XDPでルーティングの技術を使うということが多いがとても取っ付きづらくあまり情報が明文化されたまとまりがない。そこでこの文章ではlookup周りに関してまとめてみる。

bpf_fib_lookup

実装としてはここが始まり。 https://patchwork.ozlabs.org/patch/908451/ Fib(forwarding infomation baseつまりルーティングテーブル)をチェックする関数

 * int bpf_fib_lookup(void *ctx, struct bpf_fib_lookup *params, int plen, u32 flags)
 *  Description
 *      Do FIB lookup in kernel tables using parameters in *params*.
 *      If lookup is successful and result shows packet is to be
 *      forwarded, the neighbor tables are searched for the nexthop.
 *      If successful (ie., FIB lookup shows forwarding and nexthop
 *      is resolved), the nexthop address is returned in ipv4_dst
 *      or ipv6_dst based on family, smac is set to mac address of
 *      egress device, dmac is set to nexthop mac address, rt_metric
 *      is set to metric from route (IPv4/IPv6 only), and ifindex
 *      is set to the device index of the nexthop from the FIB lookup.
 *
 *      *plen* argument is the size of the passed in struct.
 *      *flags* argument can be a combination of one or more of the
 *      following values:
 *
 *      **BPF_FIB_LOOKUP_DIRECT**
 *          Do a direct table lookup vs full lookup using FIB
 *          rules.
 *      **BPF_FIB_LOOKUP_OUTPUT**
 *          Perform lookup from an egress perspective (default is
 *          ingress).
 *
 *      *ctx* is either **struct xdp_md** for XDP programs or
 *      **struct sk_buff** tc cls_act programs.
 *  Return
 *      * < 0 if any input argument is invalid
 *      *   0 on success (packet is forwarded, nexthop neighbor exists)
 *      * > 0 one of **BPF_FIB_LKUP_RET_** codes explaining why the
 *        packet is not forwarded or needs assist from full stack

入力

入力パラメーターはfibのパラメーターとルックアップの参照方法のフラグを入れることになる。

雑に適当にパラメーターを突っ込むとよい。こんな感じ

fib_params.family = AF_INET;
fib_params.tos = iph->tos;
fib_params.l4_protocol = iph->protocol;
fib_params.sport = 0;
fib_params.dport = 0;
fib_params.tot_len = bpf_ntohs(iph->tot_len);
fib_params.ipv4_src = iph->saddr;
fib_params.ipv4_dst = iph->daddr;

L4が含まれてる点については先ほどのfibruleにL4を紐付かせているケースがあるのでそれを考慮した結果。

  • ルックアップの参照方法
BPF_FIB_LOOKUP_*
DIRECT: Skip the FIB rules and go to FIB table associated with device
OUTPUT: Do lookup from egress perspective; default is ingress

以上に書いてる以上のことはないのですが、もう少し具体的な話は以下のコードを読むとわかる。

出力

lookupした時の結果と、結果に基づく情報が受け取れる。前者は関数の返り値、後者はfib_paramに書き込まれて実行が終わるのでこんな感じで書き込むだけで良い。

memcpy(eth->h_dest, fib_params.dmac, ETH_ALEN);
memcpy(eth->h_source, fib_params.smac, ETH_ALEN);

前者の話は以下のあたいが帰ってくる。

    case BPF_FIB_LKUP_RET_SUCCESS:         /* lookup successful */
    case BPF_FIB_LKUP_RET_BLACKHOLE:    /* dest is blackholed; can be dropped */
    case BPF_FIB_LKUP_RET_UNREACHABLE:  /* dest is unreachable; can be dropped */
    case BPF_FIB_LKUP_RET_PROHIBIT:     /* dest not allowed; can be dropped */
    case BPF_FIB_LKUP_RET_NOT_FWDED:    /* packet is not forwarded */
    case BPF_FIB_LKUP_RET_FWD_DISABLED: /* fwding is not enabled on ingress */
    case BPF_FIB_LKUP_RET_UNSUPP_LWT:   /* fwd requires encapsulation */
    case BPF_FIB_LKUP_RET_NO_NEIGH:     /* no neighbor entry for nh */
    case BPF_FIB_LKUP_RET_FRAG_NEEDED:  /* fragmentation required to fwd */

失敗した場合は雑にxdp_passをするとkernelが雑にarpを解決するのでそれを使うと良い。

参考

github.com

2020年にやりたいことを100個

Introduction

あけおめことよろです.takemioです.

某コンテストで一緒に活動をさせていただいてる,てるふのくんが良さそうな記事を書いていたのでオマージュということで自分も書くことにしました

medium.com

2019のイベント振り返り

昨年は高速パケット通信に関する技術を学んでいた年だったなぁと思います. あとは,そろそろ進路考えないととかいろんな点で考えさせられてました.

1~2月

  • その前の月に祖母が入院した.12月に母も倒れたりでちょっと冗談じゃなく忙しかった.

  • GCCと呼ばれるセキュリティキャンプの世界バージョンに参加した.どうやら初めての試みだったようで一期生だった.

    • 応募人数が多くなくて応募する時間が伸びたので伸びた後に書き始めて提出したという感じだったので,今年度の流れ見てると今年だったら通る自信はなかったなぁとなってる
    • 振り返ると大体酒しか飲んでる記憶がない.確かにエクスプロイトを書いていたりしたはずなんだが・・・

詳しくはこれを見てくれ

takeio.hatenablog.com

  • トラコンというコンテストの運営をやった.SRv6を問題を本戦で出した

takeio.hatenablog.com

3~4月

  • 渋谷にある緑のCという会社のインターンに参加した.
  • 初逆求人.疲弊

  • 東京駅前にあるメディア系企業をやめた

詳しくはこれを見てくれ

takeio.hatenablog.com

  • 新宿にあるピンク色のクラウド事業者にアルバイトをし始めた

    • 最近燃えてる会社です
  • 大学の新年度が始まって疲弊してた

5~6月

  • トラコンというコンテストのキックオフがあった.
    • また運営をやることになった
  • トラコン予備校というやつで講師をやった
  • DPDKフォーラムやinterop tokyoに出張してた

7~8月

  • 気がついたらバイトなのに出向してた
  • 大学のテスト期間で疲弊した
  • コンテスト運営のためのkubernetesクラスタ運用に疲弊してた
  • 白金にある草(==W)という企業のインターン に行ってた

詳しくは社のブログを書いたのでこれを見てくれ

www.wantedly.com

9~10月

  • 大学で研究室らしい物にルーティングされた
    • が!!!何も始まっていない.とりあえず黙々と研究室のファシリティを改善してる
  • 学祭とかに駆り出されてドーナッツを揚げて売る仕事をしてた(辛い)
  • wideの合宿に行ってた.疲れた

11~12月

  • 就活を始めた
  • セキュリティミニキャンプのチューターで広島に行った
  • コンテスト運営のためのkubernetesクラスタ運用に疲弊してた
  • コンテストのための合宿参加をしていた

パッと見るとあまり進捗がないですねー.頑張ったつもりなんですが,できないことができるようになってきたものもありますし来年は登壇とかも含めてoutputを心がけたい.

しかしこれでさえ自分一人ではできなかったことだと思いますので,本当に皆さんと出会えたことを感謝します.

100 things what I want to do in this year.

  1. 良さげな高級椅子を買う.
  2. 体重を落とす.(三十ぐらい落ちると嬉しいが・・・難しいかな?)
  3. オンライン英語勉強ツールを使っていく
  4. 本がいっぱいあるけど,読んだやつを誰かに譲ろうかな
  5. 自分のプロフィールサイトをいい感じにfixをしたい.技術ブログ以外におきもちブログ作ったけど使わなくなってしまった.多分CMSとかないからだと思う
  6. そろそろスマホを買い換えたい.実はシムフリーとかじゃないのでその辺ので一つ
  7. vimを使いこなしたい.あまりcliでいっぱいやるの得意ではないので鯖を触るとき困りがち
  8. zshを使いなしたい.macの標準が変わったので乗り換えたけど勝手が地味に違う.
  9. いい感じの持ち歩けるカメラを買う
  10. 鞄の中身を減らす.なんだかいらないものを持って出かけがちなので.
  11. 情報のインプット元を整理するRSSとか,slackに垂れ流すとか,MLはそうしてもいいかも
  12. 出張とかではなくて純粋に旅行をして自分の心や体験を時間に追われず養いたい
  13. ARCとかABCにでて問題を解く.DP描けるようになりたい
  14. DBの勉強をする.正規化や実行計画を理解したい
  15. CPUを作る.わからないところはわからないので頑張る
  16. 台湾に行く.HITCONとかいきたいけど,そういうのではなくても一人で海外に行ってみたい.
  17. 筋トレをするぞ!体力が無になってる.
  18. 自作キーボード作って満足してしまったので,使いこなしたい
  19. IPAの高度資格(NW or SC)を取る.取らずにバカにしてしまいがちなのでそれはやめたい
  20. 任天堂SWを買う
  21. 車の免許を取る
  22. 論文を書いてとりあえず国内でもいいので投稿するという一連を体験したい
  23. 長期の休みを戦略的に利用する
  24. 未踏に応募する
  25. インフラエンジニアたるものGoを利用したプロトタイプみたいなのをさくっと描けるようなりたい
  26. ソートアルゴリズム何もみないで何個かさくっと描けるようになりたい.hoeg.sorted() をやめよう
  27. 新しい靴とサンダルを描く.ビルケンシュトックとナイキの良さげなやつが欲しい
  28. イヤホン新しいのを書いたい
  29. 40~100GNicを買う
  30. 就活をやり込む
  31. 家に転がってるラズパイでなんか便利アプリを作る
  32. 金を稼ぐ
  33. お家ネットワークをただstaticで動かしてるだけなのでダイナミックルーティングさせる
  34. お家ネットワークにaironetをちゃんと動かす
  35. 3Dプリンターを買う
  36. 1割でもいいから得たお金を貯金する
  37. お金を貯めてセーブルのティーカップとソーサーを買う
  38. 自分にとって未踏なので四国に行く.
  39. アルバイトのコミット率を上げる
  40. 関数型で一個プロダクト作る
  41. DPDKを使いこなせるようなる
  42. 自分が良いと思える条件の就職先を見つけて内定を取る
  43. 原点回帰するために開発系のプロコンに応募する.
  44. ピアリング を貼る練習をする
  45. PRを出せるぐらいOSSを使い込む
  46. Linux kernel netdevにパッチを送る
  47. 少しでもいいので,どこかの任意団体に寄付する.
  48. デートを楽しめる余裕を身につける
  49. 個人の名刺を一新する
  50. ドールをお迎えしてみる.
  51. ライブに行ってみる
  52. スマートウォッチを買って生活してみる
  53. いい感じのデスクトップ環境を模索する,タイル型に慣れたい
  54. エスペラントの勉強をする
  55. 自炊バリエーションを増やす.野菜を美味しく食える料理を覚えたい
  56. コーヒーの美味しさを覚えてウンチクを話す
  57. 徹夜をしないように時間を計算する
  58. 良さげなスピーカーを買ってみる
  59. ワインわかんないのでワインに詳しくなりたい
  60. 飲み友達を作る
  61. 彼女を作る.というか好きな人を作るが正解?
  62. 工事担任者総合種の部分合格を合格に持っていく
  63. 無線系の資格を取る.アマチュア3級とか?w
  64. BPFに詳しくなる
  65. BSD系のネットワークスタックを勉強する
  66. OSを作ってみる
  67. VPNを復活させる
  68. 髪の毛をまた青に染める.前回は脱色たりずスーモになってしまったので辛い
  69. コミュニケーションがクソ下手なので気を付けたい
  70. 定期的にブログを書く.エモすぎて辛そうなのでそれは分けたほうがいいかも?
  71. 海外のカンファレンスに行く
  72. 株式取引をやってみる
  73. ↑それに関することをプログラミングで取り組んでみる
  74. 暗号通貨を買ってみる.使ってみる
  75. 3連休があるときは何かのプロトタイプを作ってみる
  76. カラオケでのウケがいい十八番を作る!
  77. 新しいお店を開拓する
  78. 人のために使う時間を用意する
  79. 自分が成長するための時間を用意する
  80. 本を書く!同人誌とかでもいいかな
  81. コミケに行く
  82. シベリア列車に乗りたい
  83. CTFに出てネットワークとバイナリを解く
  84. DBを作る
  85. 分散技術の勉強をする
  86. P2Pを実装してみる
  87. kerasを利用したFWとか実装してみる
  88. 定期的に反省をする
  89. JANOGで話す
  90. ダーツをまたやる
  91. 北海道一周をする
  92. バイクを買う
  93. EPCの再実装をする
  94. 自分をカッコ良くしたい
  95. 3GPPを一通り読んでコアモバイルに関すること理解して,少なくても同年代で一番詳しくなる
  96. プライベートLTEの実験をする
  97. k8sに関してもう少し詳しくなる
  98. 活字をもっと読むようにする
  99. 人の接し方をABテストして学んでいく
  100. スマホから離れるようにしてみる

Conclusion

なんというかテクニカル寄りな事ばかりでちょっと他のことやったほうがいいのではと言う気持ちになりました.101があったら技術以外のことを初めてみると言うところでしょうか.

もしこの上げた中で気になるものがあれば声かけてください!

今年も各位にはご迷惑をおかけするかもしれませんがよろしくお願いします.

Quicheをnginxで利用するためにmac向けにbuildして見た

adventar.org

これはSecHack365 修了生 Advent Calendar 2019の24日目記事です。

これは何

なんで俺はクリスマスイブにこんなこと書いてるんだろうか CloudflareのHTTP/3ライブラリ QuicheはRustで書かれているツールです。

github.com

この前nginxで利用できるパッチを提供してくれて、これを利用することでnginxをHTTP/3対応させることができます。

mac向けにbuildするやり方がちょっとめんどくさかったので、本稿はそのことに言及しつつ動かしていくところまでやってみる。

なお、mac向けではなくジェネリックにインストールする方法は https://github.com/cloudflare/quiche/tree/master/extras/nginx に書いていたり、すでに記事にしている人がいるので合わせて見てください。

build

rustのインストールを先にしておく。

curl https://sh.rustup.rs -sSf | sh
source $HOME/.cargo/env

clangのバージョンは以下の通りです。

❯❯❯ clang -v                                                                                                     
Apple clang version 11.0.0 (clang-1100.0.33.16)
Target: x86_64-apple-darwin19.2.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

nginxを持ってきてquicheを持ってくる

curl -O https://nginx.org/download/nginx-1.16.1.tar.gz
tar xzvf nginx-1.16.1.tar.gz

git clone --recursive https://github.com/cloudflare/quiche

記事を書いてる際に一点macos特有の問題がfixされたので一応紹介しておくと、macosではcdylibをフラグから削除しておく必要があったのですがそれは解決されました。気になる人は以下のissueを見てください。 https://github.com/cloudflare/quiche/issues/238

ということでissueに感謝しつつ、このままパッチを当て、configureをします。

cd nginx-1.16.1
patch -p01 < ../quiche/extras/nginx/nginx-1.16.patch

./configure                                 \
       --prefix=$PWD                           \
       --build="quiche-$(git --git-dir=../quiche/.git rev-parse --short HEAD)" \
       --with-http_ssl_module                  \
       --with-http_v2_module                   \
       --with-http_v3_module                   \
       --with-openssl=../quiche/deps/boringssl \
       --with-quiche=../quiche

これで生成されたmakefileには欠点があり、 生成されたところのパスはobjs/Makefileだが、nginxのコードで初期化してないところがあってそれを無視するためにclangのオプションの -Wconditional-uninitialized を削除する必要があります。

また、最後にリンクをするところでバグるのでLINK = $(CC) -framework Security -framework Foundationに書き換えてmacos側で利用されてるセキュリティ周りの標準ライブラリを入れる必要もあります。

よくわからないと思うのでとりあえず該当の先頭5行を示します.これになるようにobjs/Makefileを書き換えましょう。

CC =    cc
CFLAGS =  -pipe  -O -Wall -Wextra -Wpointer-arith -Wno-unused-parameter -Wno-deprecated-declarations -Werror -g
CPP =   cc -E
LINK =  $(CC) -framework Security -framework Foundation

fixをしたら以下のコマンドを入力しましょう。

make

objsに nginx ができていれば完成です。

オレオレ証明書作り

opensslが入ってることが前提です

~/W/a/nginx-1.16.1 ❯❯❯ openssl version
OpenSSL 1.0.2s  28 May 2019

入っていない人や古い人は以下のよう感じで雑に入れてください。

brew install openssl
brew link openssl --force
openssl version

では雑に鍵を作りましょう。実験なので、コマンド叩いたら雑にenter key叩きまくるだけで大丈夫です。

~/W/a/n/conf ❯❯❯ openssl genrsa 2048 > server.key
Generating RSA private key, 2048 bit long modulus
...................................................................................................................................................................+++++
............................................................+++++
unable to write 'random state'
e is 65537 (0x10001)
~/W/a/n/conf ❯❯❯ openssl req -new -key server.key > server.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:localhost
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
~/W/a/n/conf ❯❯❯ openssl x509 -days 3650 -req -signkey server.key < server.csr > server.crt
Signature ok
subject=/C=AU/ST=Some-State/O=Internet Widgits Pty Ltd/CN=localhost
Getting Private key
unable to write 'random state'
~/W/a/n/conf ❯❯❯ ls
fastcgi.conf   koi-utf        mime.types     scgi_params    server.csr     uwsgi_params
fastcgi_params koi-win        nginx.conf     server.crt     server.key     win-utf

これで server.pem, server.key という名前のファイルができました。

コンフィグ

とりあえず以下のコンフィグをnginx-1.16.1/conf に書いてください。pathのところは良しなにしてください。

events {
    worker_connections  1024;
}

http {
    server {
        # Enable QUIC and HTTP/3.
        listen 4443 quic reuseport;

        ssl_certificate      path/server.crt;
        ssl_certificate_key  path/server.key;

        # Enable all TLS versions (TLSv1.3 is required for QUIC).
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;

        location / {
            root   html;
            index  index.html index.htm;
        }
    }
}

動かしてみる

まず,errorfileを作ります。

~/W/a/nginx-1.16.1 ❯❯❯ pwd
~/nginx-1.16.1
mkdir logs
touch logs/error.log

次に実行をして見ましょう。

cd objs
sudo ./nginx -c conf/nginx.conf

これで起動したはずです。

次はアクセスしたいですよね。アクセスを行うことを確認して見ましょう。今回はquicheのexampleを利用してアクセスします。

cd ../quiche
cargo build --examples
target/debug/examples/http3-client https://127.0.0.1:4443/index.html --no-verify   |head -n3 

結果としてはhtmlファイルを置いていないので以下のような感じでコネクションが解決してることがわかります!

<html>
<head><title>403 Forbidden</title></head>
<body>

確かにパケットが飛んできてそうですね〜!

f:id:taketarou2:20191224135600p:plain
dump

まとめ

結構簡単にHTTP/3を体験できることがわかりました。自分の開発用に知ってると嬉しいかなと思って書いて見ました。 もし興味がある人は Chrome Canary

www.google.com

(開発バージョンのchrome)を利用して --enable-quic --quic-version=h3-23 を渡して起動するとHTTP/3が動いたりします!よければ遊んでみてください。

追記

yukiさんにコメントをいただいて、どうやらchromeの最新では--enable-quic --quic-version=h3-24 になったそうです。訂正させていただきます🙇‍♂️

最新版ですと、h3-23ではなくh3-24が必要ですー

「The latest Chrome Canary supports h3-24 instead of h3-23」
https://groups.google.com/a/chromium.org/forum/#!topic/proto-quic/trZsbXM_2CM

websocketについて

これはBBSakura アドカレ12日目です。今更ながら、websocketについて気になってたので調べて、なおかつ実装を書く機会があったのでその実装コードを 踏まえて説明したいと思います。 結局めんどくさくなったので実装コードは気になったら見てください()

これが今回書いた実装です

websocketとは?

一言で言うと双方向通信を低コストで行うための仕組みです。RFC6445で標準化されています。 背景としては近年のインタラクションが激しいソフトウェアでは様々な方法でUXを邪魔せず、なおかつそれでソフトウェアの書き手が疲弊しないような仕組みが求められています。 そのために現在はHTTP/2やHTTP/3など様々な通信プロトコルが用意されています。その中でもwebsocketは過去のHTTP1.1のロングポーリングを使った力技な双方向通信実現をやめるための一手となる技術でした。 今回はそんなwebsocketについて説明してきます。

コネクションの成立方法

クライアントからサーバーにリクエストを投げつけてコネクション確立をするところがスタートだったりします。

具体的にはHTTPの通信のinterfaceからupgradeという形を用いてwebsocketプロトコルへの移行をしています。イメージとしてはhttpをやめてL7をwebsocketのプロトコルに切り替える感じです。 以下の画像のようになりますが、まずはそこまでを説明します。

f:id:taketarou2:20191213222714p:plain

まず、クライアントからリクエストを投げつけます。今回はecho.websocket.orgにリクエストを投げる例を以下に示します。 もし自分で試してみたい場合は https://github.com/takehaya/internet_exercises/blob/master/exec_4/websock/example.pyws = create_connection("ws://localhost:5001/")ws = create_connection("ws://echo.websocket.org")に書き換えていただいてwiresharkを叩くとパケットが覗けます👀

GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: echo.websocket.org:80
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: nRHLKGL+8fuaCJmJTh+JTA==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

これは普通にGETメソッドを叩いてるだけなんですが、特徴的な部分としては UpgradeヘッダーとConnectionヘッダー、Sec-WebSocket-*ヘッダーでしょうか。 何してるか説明していくと、まずこの UpgradeヘッダーとConnectionヘッダーを利用して HTTPから WebSocketへのプロトコルのアップグレードを表現しています。

Sec-WebSocket-Versionは、切り替えるプロトコルのバージョンの指定になります。 もしサーバーが送られてきたバージョンに対応出来なければコネクションを切断すると言う形になります。現在のWebSocketの最新バージョンは13なのでこれを指定します。

ちなみに14とかのinvalidな値にしてみるとBad Requestと怒られが発生します。

GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: echo.websocket.org:80
Sec-WebSocket-Version: 14
Sec-WebSocket-Key: 3qsHtEfHaDKaCDkpiMzoLQ==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

HTTP/1.1 400 Bad Request
Content-Type: text/html
Date: Thu, 12 Dec 2019 23:03:59 GMT
Server: Kaazing Gateway
Content-Length: 63

次に、Sec-Websocket-Keyヘッダーは、任意のクライアントとのコネクション確立の独立性を示すために作ります。値は自分は雑に os.urandom(16) とかで作りました。 サーバはSec-Websocket-Keyヘッダーに指定された値を大元として新しく値を生成を行いSec-WebSocket-Acceptヘッダにその値を指定してレスポンスを返します。 クライアントとしては自分のSec-Websocket-Keyの値が使われているかどうかが確認出来る様になってます。その為、自分のリクエストに対するレスポンスである事が保証出来ます。

なお自分は実装をサボったので、一応実装方法はわかるぜってことで書いておくと、

Sec-WebSocket-Acceptは

  1. Sec-WebSocket-Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"を連結する
  2. sha1にする
  3. base64エンコードをする

で作られるので、 クライアントで検証するときは 自分で投げつけた鍵をsha1とってそれがマッチするかをcompare_digestとかで見てあげると実装がかけると思います

と、まぁ詰まるところこれらはプロトコルの切り替え要求です。簡単ですね。

レスポンスは以下のようになります。

HTTP/1.1 101 Web Socket Protocol Handshake
Connection: Upgrade
Date: Thu, 12 Dec 2019 22:41:45 GMT
Sec-WebSocket-Accept: 5IuPVeGrbXxYZ6sAQ5Z/YjBVl20=
Server: Kaazing Gateway
Upgrade: websocket

これは 101 Switching Protocolsが返ってきてるので切り替え準備ができたと言うことがわかりました。 これらのことを WebSocket opening handshake と呼びます。

どのように双方向通信をしてるか

ハンドシェイクが成功すると次はwebsocketの通信がいよいよできるようになります。 これらはTCP上で動いていて、ハンドシェイクで確立したTCPコネクションを引き続き使いまわして双方向にデータのやり取りをすることになります。

WebSocketは、フレームと呼ばれる単位系でデータの受送信を行います。フレームはあるデータの構造をとっていて具体的には以下のような構造になります。

      0                   1                   2                   3
      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     +-+-+-+-+-------+-+-------------+-------------------------------+
     |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
     |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
     |N|V|V|V|       |S|             |   (if payload len==126/127)   |
     | |1|2|3|       |K|             |                               |
     +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
     |     Extended payload length continued, if payload len == 127  |
     + - - - - - - - - - - - - - - - +-------------------------------+
     |                               |Masking-key, if MASK set to 1  |
     +-------------------------------+-------------------------------+
     | Masking-key (continued)       |          Payload Data         |
     +-------------------------------- - - - - - - - - - - - - - - - +
     :                     Payload Data continued ...                :
     + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
     |                     Payload Data continued ...                |
     +---------------------------------------------------------------+

これをいくつか説明すると以下のような感じになる。

  • Fin:メッセージが終了するか(これが1ではない場合はフラグメントされている場合などがあげれる)
  • RSV1~3:予約済み。非定義が飛んできたらWebSocketを失敗させる
  • opcode

    • 0x0:継続フレーム
    • 0x1:テキストフレーム
    • 0x2:バイナリフレーム
    • 0x3〜0x7:追加の データ フレーム用に予約済み
    • 0x8:接続close を表す
    • 0x9:ping
    • 0xA:pong
    • 0xB〜0xF:追加の 制御 フレーム用に予約済み
  • mask: マスクが有効になっているか

  • Payload len: ペイロードの長さ
  • Masking-key

まぁよくわからないと思うので Hello, World と言うデータを送った時の一例を見てみると以下のようになります。

f:id:taketarou2:20191213224010p:plain

このパケットのみで成立するので、Fin:1、RSVはセマンティクスが何もないので指定されません、テキストデータなので 0x1 でテキストデータのtypeで送っています。

>>> len("Hello, World")
12

確かにペイロードの長さが12となっていますね。 と言うことで非常に簡単な作りで通信を達成しています。

これらはmaskすることができてその場合は以下のようになります。

f:id:taketarou2:20191213224036p:plain

maskしているときは mask:TrueMasking-key が追加されています。

他にもping pongと言ったopcodeがありますが、これを利用してサーバー側がビジー状態になってないかなどをみることができます(ただ、個人的にはTCPのコネクションで制御できてしまう範囲で大体事足りるのではと思うんですが)

HTTP/2との考察

詳しい読者はもう理解してると思いますが自分はHTTP/2の違いってなんだろうと思いましたので書きます。

HTTP/2を雑に説明するとHTTP/1.1で動く追加プロトコルで、特出すべきはHTTP Streamという概念が導入されてRFC7540として標準化されています。 これができたモチベーションとしてはAJAXなどの準リアルタイム双方向型の通信への利用などを考えるときにリクエストからレスポンスでひとまとめになるシェイクハンドのオーバーヘッドが無視できない大きさになってきたというものでした。 例えば我々がストリーミングでビデオ再生を行うときに知らず知らず使っていてyoutubeなどをみてるときによく目を凝らしてデベロッパーツールとかを使ってみると利用してるというわけです。

これだけ聞いてるとwebsocketじゃなくてHTTP2でも良いのでは😇と思ってしまいますね。なんならそもそも言い出したのどっちもGoogleですし😇僕もそれでいいと思います。(他に明確な違いを思いつけば教えて欲しいです!)

しかしこれの制御方法等が異なったり、websocketはhttp2よりも簡単な実装になっています。例えばよく知られているのが1つのhttpセッションで複数のストリームを論理的に成立させれますが、websocketの回線はhttpセッションと一対一の構成になります。これをみてhttp2の方がうまくやりくりしてると思いますが、やりたいことはほとんど同じように思います。というわけで自分が思うに同じような問題を同じような方法論でしかし異なるアプローチをした結果のように思います。

しかしながら、RFC8441、Bootstrapping WebSockets with HTTP/2が発行されて、http2でwebsocketを使うと言うものです。 まだまだ進化していて素晴らしいですね。これの個人的に面白いなと思う部分は HTTP GETメソッド でHTTP1.1/Websocketは 101スイッチングプロトコルでupgradeをして切り替わりますが、http2ではクライアントサイドでHTTP CONNECTを要求し、サーバーサイドからバージョンやプロトコル拡張機能を返してそれに同意する場合status200を返すというフローになっています。これを使うとwebsocketのセッションはhttp2の単一のストリームのみが利用される。つまりHTTP2の利用をしながらWebsocketを利用できて効率的になって嬉しいというのがあります。

まとめ

Websocketはいいぞ!簡単な作りで便利!!

後書き

実はこれは大学で TCPで動くチャットアプリを作れって言われたんですが、正直何も面白い部分がなかったので高校の時にSocket.ioでなんかそういうの作ったなーと思い出したところから雑にWebsocket勉強するついでに実装するか〜ってなったものに説明をつけたものへの技術ネタ供養でした。

どうでもいいんですがSocket.ioはあれただのHTTP GETメソッドを投げてやりとりしてたりでいい感じに内部のライブラリが通信方法を選択してるんですよね。便利。

というわけで既存の実装をかなり参考にしたりしたのですが非常に概念が面白く学ぶところが多かったなと思いました。 残念ながらTLS対応部分は時間の関係で挫折してしまったのですが3日ぐらいでsend receiveはサクッと作れたので非常に簡単な実装ということがわかりました。 次機会があればQUITのクライアントを書きたいなと思います。

今日から始めるXDPと取り巻く環境について

はじめに

この記事はBBSakuraNetworkアドベントカレンダーの5日目です。

自分はBBSakuraでアルバイトをしている大学生です。 さくらでバイトをしていたところ気がついたら出向していて今はコアモバイルの開発に携わっています。アルバイトに出向ってあるんですね。びっくり。 今はB3なのでそろそろ大学で研究しないといけなくて焦りを感じてます。

さて、今回話すのは自分の趣味の話をします。 eBPF と言われるパケット処理のための処理技術があるのですがそれが好きで、最近はBPFを利用した高速パケット技術の手法としてXDPを追いかけています。今回はそれを始めるにはどうすればいいかをみたいな話を紹介したいと思います。

対象読者としては高速パケット処理をやりたいけど、どうすればいいかわからない!って人をターゲットにしてます。始めるときにみると手引きみたいなknowledgeを目指します(自分向けのメモぽさがあるな) 書かない話としてはパケット高速化についての細かい手法は話しません。(むしろ良さそうなのがあれば教えて欲しい)

とは言っても界隈では有名な yunazunoさんやhigebuさん、YutaroHayakawaさん、がいい感じの記事や話をしているので(興味を持ったらこの人たちをウォッチすべきです!)そちらを見ておけば大体入門できるんですが、とはいえ自分なりに基本的な導入とはじめにつまづいた事とか最近の界隈の話をしようかなと思います。

雑にXDPを理解するための概要

XDP(eXpressDataPath)はLinuxカーネルで動く高パフォーマンスのプログラマブルデータパスです。 sk_buffというデータ構造の割り当てをする前に直接データをBPFを使ってカーネルランドで処理をすることで高速な通信を実現しています。

どれくらい早いかというとこんな感じです。

cf. https://people.netfilter.org/hawk/presentations/xdp2016/xdp_intro_and_use_cases_sep2016.pdf

XDP自体の詳細はリンク先の論文を参照してください。

以下の画像は論文より抜粋した画像です。 具体的な処理の場所はXDPと書いているところで、他に関係するところは基本的に水色の部分がXDPに関係してきます。

確かにカーネルランドにあるデバイスドライバーで真っ先にパケットを受けているというのがわかると思います。

BPFとは

BPF(BerkeleyPacketFilter)というパケットフィルタ機構です。これはカーネルランドで高速にパケットを処理することができるコアの機構です。今回のXDPのコアでも利用されている機構でもあります。

この部分は誤解を恐れずに説明するとパケット処理専用のCPUをカーネルランドでエミュレートしたようなもので、MIPSに近い命令セットを持ったVMが存在しています。これらはtcpdumpなどで現在も使われている機能です。

近年ではこれらはLinuxにおいて拡張されてe(xtend)BPFと呼ばれるようになり、パケットのみならずSeccompなどのシステム間のセキュリティ機構として使われるようになり、またトレーサーとして使われています。

今回はネットワークにおいての利用の話をメインでしていきます。実行のイメージとしてはこのような形になるはずです

eBPF Map

eBPFMapというのはBPFがユーザーランド等とやりとりしたい時に使えるテンポラリーなデータ領域のことを指します。平たく説明すると共有で使えるKeyValueなテーブルです。

イメージとしてはこのような感じです。 image alt cf. https://qiita.com/sg-matsumoto/items/8194320db32d4d8f7a16

そこにはeBPFがeBPFMapに書き込んでユーザーランド側でその情報を得たり、その逆でユーザーランドからそこに書き込んだり、またeBPFがeBPFにパケット情報などを処理を渡すこともできます。 XDPもカーネルランドで動作していることからユーザーランドとやり取りすることができません。そこでBPFの機能となってるeBPFMapを使ってユーザーランドとやり取りをします。具体的なユースケースはNFVやLBのサービスの登録などで利活用されます。

Generic XDP

パフォーマンス上の理由から、XDPの処理はsk_buff(linuxのネットワークスタックに使われるソケットバッファー)が割り当てられる前にデバイスドライバーから直接呼び出されます。このことは先程のXDPのアーキテクチャの画像から理解できるとおもいますが、ということはXDPの動作にはデバイスドライバー側でのサポート対応が必須ということが言えます。

しかしながらまだまだ未対応のNICドライバーが殆どです。そこでXDPをどの環境でも使えるGenericXDPというモノがlinux kernel4.12に導入されました。速度は出ませんが開発等では非常に有用です。

利活用例

facebook: katran

facebookが社内のデータセンターで利活用しているL4LBです。以前はIPVSを利用していたのですが、自作するようになりました。

特出してるところはL4LB部分で以下の事柄です。

lineでも L4LBを自作し同じようなことをしていますのでそちらも合わせてみると良さそうです(リンクは後述する)

cilium

これはサービスやコンテナ間の通信をセキュアかつ負荷分散を高速にするために作られたフレームワークです。k8sなどのオーケストレーション上で動かすことを想定しています。

cloudflare: L4Drop, XDP DDoS Mitigation

cloudflare が行っているもので、XDPを使ってDDoS緩和をしています。 方針としてはXDPを後述するtail_callという方法で多段におきます。そしてサンプルを抽出し、解析をして その結果をgatebot と呼ばれるルール注入装置に渡してdropするかを判断するルールをXDPに注入します。 - cf. https://netdevconf.info/2.1/papers/Gilberto_Bertin_XDP_in_practice.pdf

- https://blog.cloudflare.com/l4drop-xdp-ebpf-based-ddos-mitigations/

これは実際にプロダクションとして稼働していて、毎秒800万パケット以上をドロップを一台で達成しているそうです。

ミクシィ、Static NAT

Global <-> privateの静的NATに利用してるらしい cplaneはgRPCで動いているコントロールサーバーがある。

12Mppsぐらい処理ができる。またパケットのコレクターとしても利用している。 ハマった課題については大規模なバックボーンを持たないと起きないことが書いていて一見の価値があります。

interop tokyo, end.ac

interop tokyoの2019で展示されたSRv6のファンクションにXDPをinterfaceに使ったAF_XDPを利用したものがあります。

これはAM -> midle node(ex. LB, DPI, mirroring)→SRv6 decap(DX4)→application のmidle nodeが IPv6が使えない場合を解決できるFunctionで、AMの場合はmidle nodeがv6を理解できて、その場合だとSRHを外さなくて転送だけであれば解釈する部分が事足ります。

しかし、v4ということはSRHを外す必要が出てきます。 そこで具体的には End.ACのノードに着信したらSRHをキャッシュして、outerpacketを外し、その時にinner packetがv4の時にToSにあるキーを入れておいて戻ってきたらそれを元にlookupするという方針にします。

これによってSRHは外してしまったが、元のパケットとSRHを照合できる。という便利なファンクションです。

ちなみに RFCを見るとintarop公開時の時の End.AC からEnd.AT に変わったみたいです。なんでこういう名前になったんだろう・・・

後述するAF_XDPをやる際にはこれを参考にするといいかもしれないです。

と、このようにすでにXDPの技術は多くのところで利活用されています。

XDPを用いたのパケット読み込み例

このセクションでは前述したXDPの仕組みや周辺はどのようにプログラムを書かれて動くのかというのを紹介したいと思います。

以降で説明するマシーン構成はclient, serverという2つのマシーンが通信ができるという前提です。またclientにはBCC(BPF Compiler Collection) + pythonが入っている前提です。 雑にこの辺(INSTALL.md)を見てinstallすると試せると思います

BCC(BPF Compiler Collection)はユーザランドで動作するツール群です。XDPプログラムの読み込みやカーネルランド側の操作を補助しつつ、Pythonバインディングが用意されているので、カーネルで動作するXDPプログラムはCで書きながらもユーザランド側の高度なマネジメントをpythonで書くということが可能になります。

以下は通信パケットがPort 8080の受送信に関わってる場合にDROPする例です。

serverではpython -m SimpleHTTPServer 8080で適当な8080で動くhttpサービスを立ち上げつつ、clientではwgetなどができるかまずは確認をして、その後、以下のソースのコードをアタッチしましょう。

#define KBUILD_MODNAME "foo"
#include <uapi/linux/bpf.h>
#include <linux/in.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <linux/ip.h>

int xdp_prog(struct xdp_md *ctx) {
    
    /* 
     * xdp_mdにあるメンバーから取得するべきは以下の2つ
     * data: パケットの先頭ポインタ
     * data_end: パケットの終端ポインタ
     * dataの先頭なので大体の場合はethを初期値では読むことができます
     */
    void* data = (void*)(long)ctx->data;
    void* data_end = (void*)(long)ctx->data_end;
    struct ethhdr *eth = data;
    
    // 次のパケットを読むためのオフセット(ethを読んだ後なので次はipヘッターの先頭を参照することになる)
    uint64_t nh_off = 0;
    nh_off = sizeof(struct ethhdr);
    
    /*
     * 以下の条件はdata+nh_off + 1がdata_endの範囲を超えていないのかをチェックしてます
     * 今回の場合は次のipヘッター領域を邪魔していないのかを見ています
     * もしこれを怠った場合、eBPF verifierにエラーを吐かれてアタッチができないハマりポイントなので要注意
     * */
    if (data + nh_off + 1  > data_end){
      return XDP_PASS;
    }
    
    // ipプロトコルを取得
    uint16_t h_proto;
    h_proto = eth->h_proto;

    // プロトコルがipv4であれば
    if (h_proto == htons(ETH_P_IP)){
        
        // data + ethの領域分を飛ばしてipヘッター部分を読み込む
        // 領域が出ていないのかチェック
        struct iphdr *iph = data + nh_off;
        if (iph + 1 > data_end){
            return XDP_PASS;
        }
      // tcpかudpかのプロトコル取得
      // iphdr分読み飛ばすためにオフセットを入れる(eth + iph)
      h_proto = iph->protocol;
      nh_off += sizeof(struct iphdr);
    }else{
        // v4以外ならばパケットをドロップしないで通過させます
        return XDP_PASS;
    }

    //tcpでやりとりしているか
    if(h_proto == IPPROTO_TCP){
        struct tcphdr *tcph = data + nh_off;
        // 領域を見る
        if (data + nh_off + sizeof(struct tcphdr) > data_end){
            return XDP_PASS
        }
        // ポートの取得
        uint16_t dst_port;
        uint16_t src_port;
        src_port = tcph->source;
        dst_port = tcph->dest

    // 8080ならドロップ
      if(dst_port == 8080 || src_port == 8080){
         return XDP_DROP;
      }
    }
    
    // v4以外ならばパケットをドロップしないで通過させます
    return XDP_PASS;
}

アタッチするための共通になるコードを以下に示します。 python loader.py ens3のような感じで動かす事ができます。

from bcc import BPF
import pyroute2
import time
import sys

device = sys.argv[1]
mode = BPF.XDP

# generic xdpとの切り替えはflag=2に変更することで行うことができます。
# flags = 2 # XDP_FLAGS_SKB_MODE
flags = 0

# load BPF program
b = BPF(src_file="./function.c", cflags=["-w"])
fn = b.load_func("xdp_prog", mode)

b.attach_xdp(device, fn, flags)


print("CTRL+C to stop")

while 1:
    try:
        time.sleep(1)
    except KeyboardInterrupt:
        print("Removing filter from device")
        b.remove_xdp(device, flags)
        break

パケットの書き「換える」例

先ほどのコードでどのようにXDPでパケットを扱えば良いかが雰囲気としてわかったと思います。 簡略化のためにコアの必要な部分のみ載せます。以下のコードはポートを書き換える例です。

struct tcphdr *tcph = data + nh_off;
if (data + nh_off + sizeof(struct tcphdr) > data_end){
    return XDP_DROP;
}
tcph->dest = newdest;

実際に書き換えたあとは必要に応じてchecksumを再計算します。

また、memcpyを使っても問題ありません。

パケットを書き「加える」例

以下のコードはIPIP(IPv4)encapする例です。 このコードではipv4_csum_inlineget_macaddr*という関数はipチェックサムを計算する関数と使用したい宛先のmacaddressを読み込む関数です。これらはライブラリ中に定義されておらず適当な自作関数であることに注意してください。

以下のコードで注目すべきは bpf_xdp_adjust_headという関数です。これはXDPプログラム中でパケットの取り扱ってるxdp_md構造体の先頭領域を広げる事ができる関数です。これによってxdpで受け取ったパケットを書き加えたり、外したりできます。

今回の場合はIPv4のencapを行いたいのでipヘッターの領域分を広げる必要があります。

注意点としては先頭領域の拡張なので以下の画像のようにEthhdrを移動した上で新しくIPhdr分を書き加えてあげる必要があります。

static inline int encaption_IPIP_v4(struct xdp_md *xdp)
{
    struct ethhdr *new_eth;
    struct ethhdr *old_eth;
    struct iphdr *new_iph;
    struct iphdr *old_iph;

    uint64_t csum = 0;

    if(bpf_xdp_adjust_head(xdp, 0 - (int)sizeof(struct iphdr))){
        return 0;
    }
    void* data = (void*)(long)xdp->data;
    void* data_end = (void*)(long)xdp->data_end;
    new_eth = data;
    new_iph = data + sizeof(struct ethhdr);
    old_eth = data + sizeof(struct iphdr);
    old_iph = data + sizeof(struct iphdr) + sizeof(struct ethhdr);
    if (new_eth + 1 > data_end ||
        old_eth + 1 > data_end ||
        new_iph + 1 > data_end ||
        old_iph + 1 > data_end){
            return 0;
        }
        
    uint16_t payload_len;
    payload_len = ntohs(old_iph->tot_len);
    
    uint8_t mac_address[6] = get_macaddr();
    __builtin_memcpy(new_eth->h_source, old_eth->h_dest, sizeof(new_eth->h_source));
    __builtin_memcpy(new_eth->h_dest, mac_address, 6);
    new_eth->h_proto = htons(ETH_P_IP);
    
    //IPIPにencapしたパケットの設定をする
    new_iph->version = 4;
    new_iph->ihl = sizeof(*new_iph) >> 2;
    new_iph->frag_off =  0;
    new_iph->protocol = IPPROTO_IPIP;
    new_iph->check = 0;
    new_iph->tos = 0;
    new_iph->tot_len = htons(payload_len + sizeof(*new_iph));
    new_iph->saddr = get_macaddr_s();
    new_iph->daddr = get_macaddr_d();
    new_iph->ttl = 8;
    
    // ip checksumを計算する
    ipv4_csum_inline(new_iph, &csum);
    new_iph->check = csum;
    return 1;
}

XDP周りでの引っかかりポイントと知見

XDPを使うにあたって2段階のチェックされるタイミングがあります。 1つはコンパイル時。2つめがeBPFVerifierと呼ばれるプログラムチェック機構です。 1つ目は大体構文とライブラリのパスが通っておらず失敗するパターンなので多くのエンジニアに解決可能な問題のことが多いですので割愛します。

しかし問題2つ目に挙げたeBPF Verifierです。これは安全ではないコードを実行させないというものなのですが、どうして安全ではないかがわかりにくく、デバックするのにあたっても非常に難しいです。 具体的にはXDPのプログラムをNICにアタッチすらさせてもらえず貧弱なエラーメッセージがただ出てくるのみとなっております。正直初学者には非常に厳しいです。

非常に慣れず苦労したのでここではそれについての簡単な知見を書き残したいと思います。

まず、XDPのコードは読んでいるとわかるのですがいくつかのイディオムがあります。 例えば、以下のコードは先程示したコードからの抜粋ですが iph + 1 > data_end という比較をしています。これはiphの先頭から次の領域が存在してるかのチェックをしていて、これによって無効な領域に飛んでいないかをチェックしています。これを怠った場合、安全ではないとVerifierに怒られてしまいます。

struct iphdr *iph = data + nh_off;
if (iph + 1 > data_end){
    return XDP_PASS;
}

他にもbuiltin_memcpyを使わなくてならないという制約があります。eBPFはeBPF組み込みの関数しか呼び出すことができないという制約があるので、さもないと謎のエラーとヒントを出してアタッチができないという状態が起きてしまいます。

__builtin_memcpy(tcph->check, newcheck, sizeof(uint16_t));

このようにいくつかのイディオムが存在していて、BPF特有の書き方が存在します。同じようにmemsetなどもbuiltinで使い、関数はinline展開が基本です。そのため自作の関数はalways_inlineなどでinline展開を強制する必要があります。 なので、個人的な意見としては慣れない間は極力後述するサンプルコード群になるべく近いベーシックなコードを書くのが変な失敗を踏み抜かないコツです。

他にもどんなバイトコードを吐くのかを見るのも非常に得策です。 その時は以下のように自身でclangでコンパイルしてみて、その上でそれをobjdumpしてみるという手があります。最適化によってバイトコードが簡約されるというのとbccとは環境が異なるところが存在するのでそこには気をつけましょう。

 clang -O2 -Wall -target bpf -c xdp_drop.c -o xdp_drop.o
 llvm-objdump -S -no-show-raw-insn xdp_drop.o

また、アタッチしたあとにうまくいかない場合があると思います。そんな時は「そもそも条件分岐に来ているのだろうか?」と実際の挙動が気になるのではないでしょうか。

その時はbpf_trace_printkeBPF mapを使う方法があります。残念ながらXDPやBPFにはDebuggerのような高度なものはないのでどちらも実質的にはprintfデバッグです。

前者のbpf_trace_printkは本当に用途がただのprintfデバッグゆえ説明することがあまりないので割愛しますが、後者のeBPFmapを使うケースとしてはデバッグする時にステートがほしい時やデータを計測して整形したい時などに使いやすいと考えます。理由としてはeBPFmapはbccを通じてデータを定期的に取得することができて、一度テーブルに書き込むような状態になるのでステートを持たすことができます。

以下に簡易的な例を示します。このコードの場合行き先アドレスが8080の場合8080をindexにしている値をインクリメントすることができます。 これに他の条件によって値を変化させるなどして使います。ただ実際は本質的にはprintfデバッグであることはどちらも変わらないのでお好みで使うといいと思います。

BPF_HASH(counter_table, uint32_t, uint16_t);
int packet_counter(struct xdp_md *ctx) {
    void* data_end = (void*)(long)ctx->data_end;
    void* data = (void*)(long)ctx->data;

    struct ethhdr *eth = data;

    uint16_t *value;
    uint16_t zero = 0;
    uint16_t h_proto;

    uint16_t dest_port;
    uint32_t index;

    if (ethhdr + 1  > data_end){
        return XDP_PASS;
    }
    
    h_proto = eth->h_proto;
    if (h_proto == htons(ETH_P_IP)) {
        h_proto = get_ip_proto(data, data_end);

        if (h_proto == IPPROTO_TCP) {
            dest_port = get_dest_port(data, nh_off, data_end);
            if (dest_port == 8080) {
                index = (uint32_t)dest_port;
                value = counter_table.lookup_or_init(&index, &zero);
                (*value) += 1;
            }
        }
    }
    return XDP_PASS;
}

また bpftoolと呼ばれるツールを利用して動作中の eBPF の情報を確認するというのも有効でしょう。

実行中の見るときはjitが邪魔とかそういう時もあると思いますのでその辺に注意。 ちなみには今後は自動でjitがディフォルトで有効になります。

cf. https://lore.kernel.org/netdev/40baf8f3507cac4851a310578edfb98ce73b5605.1574541375.git.daniel@iogearbox.net/

ちなみにプロダクションでXDPを使ってるfacebook曰く JIT is your friend というようにjitは有効にした方が高速です

cf. http://vger.kernel.org/lpc_net2018_talks/LPC_XDP_Shirokov_v2.pdf

XDPを取り巻く環境

対応nic

XDPはソフトウェアレベルで基本動くのですが、ドライバーのサポートによってhardwareでオフロード(ハードウェア動作)をさせることができるというのがあります。 有名なものだとMellanoxのmlx4,5シリーズがあります。これらはdropとTXにそのままreturnするというものをサポートしています。 かなり値段としては高いのですが、intelnicドライバーixgbeというものが対応しているのでIntelnicでアクセスするという方法を使うと安く済みそうです。 また、cloud基盤等で動かす場合はvirtio-netが対応してるのでVMでも使うことができるのでサービスメッシュやSR-IOVなどでも利活用することが可能で嬉しいです

ただvirtioはクセがあり、kvmで立てる際はqueues=vcpu*2, vectors=queues*2 + 2, -mq=on (cf. https://www.linux-kvm.org/page/Multiqueue ) でなおかつ 

root@xdptest1:~# ethtool -g eth0
Ring parameters for eth0:
Pre-set maximums:
RX:        1024
RX Mini:    0
RX Jumbo:    0
TX:        1024
Current hardware settings:
RX:        1024
RX Mini:    0
RX Jumbo:    0
TX:        1024

1024以上となるようにしなくてはいけません。 もしならなければ、https://github.com/qemu/qemu/blob/36609b4fa36f0ac934874371874416f7533a5408/hw/net/virtio-net.c#L52 のsizeが1026になるように書き換えてビルドしてあげる必要があります。

BPFのプログラムのアタッチについて

BPFを利用する際に netlink経由からアタッチする場合と iproute2経由 と bcc経由で実行するというのがあります。

この時実はアタッチする構成やローダーによって利用可能か機能や仕組みや意味論が変わっている場合があります。 よく知られているのは inner map の互換性は iproute2bcc にはないということです。 誤解を恐れずにいうとinner map とは KVである ebpf map でbyte型のようなアトミックなデータだけではなく、リレーショナルさせる技術のことです。これは現状netlink経由のローダーでしか利用できません。 しかし、netlinkは簡単にアタッチできるようなサンプルが少ないの玉に傷です。iproute2やbccで普通にパケット処理をする分に事足りますのでこちらで入門すると良いと思います。

AF_XDP

AF_XDPと呼ばれる、XDPの高速処理な部分を活かしながらもBPF特有の書き方の難しさを解決した仕組みで、カーネルバイパスする機能です。アプローチとしてはnetmapに非常に似ています。以下の画像はOVSからの引用ですが、雰囲気が伝わると思います

DPDKのアプローチとしてはポーリングし続ける方式なのですがそこへの利用もされています。 https://www.dpdk.org/wp-content/uploads/sites/35/2018/10/pm-06-DPDK-PMD-for-AF_XDP.pdf

最近のテクニカルな部分の話

tailcall

  • cf. https://lwn.net/Articles/645169/
  • eBPFにはtailcallという実行中のBPFプログラムからあるBPFプログラムにジャンプするということができます。
  • ユースケースとしてはプログラムの整合性を保ったまま別の独立のプログラムを連携させたり、現状のeBPFの最大命令数は4096個であるためにプログラムの巨大化に対応するということが可能になります。
    • tail callの面白いのはこれのjumpの先を決定するためにebpf mapをルックアップしてて,つまりユーザー空間側mapを書き換えるとで飛ばす場所を変更できるということが簡単にできるというのがあります。
    • ちなみにkernel v5.3において100万の命令を利用可能できるようにしたいという言及があるために、プログラムの巨大化での利用は今後はしなくても良くなる可能性があります。
    • cf. https://lwn.net/Articles/794934/
  • 最近では Chain-calling と言われる概念が提案されていて、自分でそのtailcallsのmapを制御しなくてもいいようにルールベースで実行するフローが提案されています

global 変数のサポート

今まではglobal領域で変数や定数として使いたい場合は eBPFmap を利用して扱っていたのですがそれを毎回書くのは正直オペレーションとして邪魔でしかないので、そこで内部的に長さ1のmapを利用して隠蔽して普通の変数として利用できるようになりました (おそらくBPF_PSEUDO_MAP_FDを使ってると思うんですが、さらっと読んだだけなので間違えたらすいません。)

ちなみに定数のみのアプローチであればBPFプログラム内の定数を変更する際にアタッチ前に直接BTFのバイナリを書き換える方法もあります。

bpf trampoline

retpolineと呼ばれる間接呼び出しをROPで置き換える投機的実行に関する脆弱性の対策方法がありますが、しかしそれが元でbpfをkernelでコールする時に分岐予測が効かないので速度が落ちてしまうという問題がありました。 そこでそれを回避することで従来と比べてeBPFの呼び出しを高速に利用できるというものが提案されました。(つまりXDPの高速化につながります!)

cf. https://lore.kernel.org/bpf/20191102220025.2475981-4-ast@kernel.org/

さらに最近提案されたのはそのアイディアはそのままでは使いにくいので、bpfdispatcher と呼ばれるの概念を導入して xdp_call.h でXDPでも利用しやすくされたものを出てきています。今後もパフォーマンスが上がっていくのが目に見えるようで嬉しいですね。

XDPを始めるのに見ると良いリンク群

見出し通りですが、それだけ貼っても仕方ないので個人の感想を段落にして書いてます。

まとめ

雑にいろいろ書いてきましたが、XDPは今までのパケット処理のつらさが減るという部分もあります。linuxのヘッター類、つまり我々がず〜っと利活用してきたプロトコルスタックに使われてた資産が使えるので構造体などのサブセットを自前で頑張らずとも用意されていたり。 他にもlinuxのネットワークスタックとシームレスに作られていて(skbuffはxdpbuffから作られてる)、 例えばarpパケットの場合はlinuxにpassしてあげることでarp tableの管理をlinux側に丸投げするということが可能で、取り出す場合はfiblookupを使うだけでfibがわかる手軽さです。これは他のパケット高速処理系には真似ができないと思います。

しかしトレードオフもあり、BPFのチェックや、あまりにニッチすぎることをするとドライバーによって未実装の機能があったりしてつらい場合もあり、clangのバージョンを雑に上げたらバグるとか特にkernelLandで動かすのでバグる時のデバッグがつらいなどがあります。

もちろん他の高速パケット処理手法も似たり寄ったりですが、実はちょっとした入門だけなら敷居が低いということが伝わると嬉しいです。

自分でパケットを投げれるというのは非常に楽しいのでぜひやってみてください!! (間違いがあればそっと連絡をくれると助かります!)

東京都八重洲口にある某メディア企業を退職しました

こんにちは。takemioです。東京都八重洲口にある某メディア企業でインターン行ってた頃に携わった保育士と保護者をつなぐことをするプロダクトが絵本を売ってる会社に売られてしまいちょっと悲しかったです(きっと色んな人に愛してもらえるプロダクトなので続いていくといいなー)

さて初退職エントリです。

といいましてもアルバイトの話ですが。。。。

おまえはだれ

  • 竹といいます
  • 宮城県で大学生してる
  • 新3年生
  • CSを勉強してる。セキュリティとネットワークと意味論に興味
  • 詳しくはOSINTしてほしい

そこでは何してたの?

脆弱性検査ツールを作ってました。

なんで入社したの

去年東京都八重洲口にある某メディア企業のウィンターインターンに参加したから。優秀なメンター様の力でそこではラッキーなことに歴代最年少(タイ)参加(?)で審査委員賞をもらったとかラッキーをした。

そんなこんなで懇親会でヘットハントされて現在へ。

なんで辞めるの?

去年の4月から今年の3月の一年の任期で行ってました。

お気持ち

NDA上やってたことは何も言えないんですが、、、トータルなお気持ちとしては初めてのフルリモートでのアルバイトという部分であまり慣れていなかったり、大会とかに参加したりしてちょくちょく進捗が悪くて煽られまくってた私を雇ってくれて感謝という感じでした。

ですが、そもそもの仕事の割り振りが月の中で稼働ができる時間(月何時間で!という感じだった)とうまく噛み合わなくてレビューを放置されたり忙しい時ほどタスクを投げられたりでちょっとそこが非常に辛くてお互い嫌な思いをしたと思います。

特にテキストベースなやり取りがそれに拍車を掛けていたように感じました。また正直投げられたタスクの粒度をどれくらいでやればいいのかとかそういうのがよくつかめなかったり、学部生だと可動できる時間が夜になりがちでミーティングとかがやりにくい部分が多くてはじめはかなり消耗してました。

他にもガッツリ時間が取られるイベントに参加したりするとそれの埋め合わせとかが大変なので結果的に月何時間やったのかを申告するとかのほうが良さそうな気がします。

一緒にやっていた大学院生はうまく稼働ができていたのでどうしてるのかとScheduleを聞いてみると基本的に研究室のなにかにぶつからなければ良さそうな感じだったのでフルリモートは授業を取り終わってる院生や学部四年とかならおすすめなのかも知れません。

ですが時々東京にでていって労働したりすると比較的いい感じにスイッチがはいるのでそこの辺が鍵ぽいです。東京の学生は羨ましいですねー。。。。

まぁさておき、自分主導でできない部分(仕様がわかりにくいor何が仕様なのかわかんない)とかじゃあそれならどうしてほしいとかどう改善してほしいのかみたいないろいろやり取りを振り返るといくつかの反省点が出てきて次に活かしたいなと思います。

たとえば反省としては初めの仕様のキャッチアップのときにフェイスtoフェイスでやらせてもらったり、技術的キャッチアップのときはそのための時間を数時間でもいいのでそれだけのために当てたりとかしたほうがあとからが非常に良かったと思いました。リモートでアドホックなやつは時間が立って忘れたりとか結構大変だと思いました。

ですが、仕事の体験としては初めは正直つまらないと思ってたんですが(今思うとそもそもの仕様がどこにあるのかとかあまり明文化されてなかったりどこから構築してるのかとかキャッチアップできてなかったのが理由だとは思いますが)仕事をする方々は優秀でこのようにしてコードをきれいにしていくのかとか。 どのようにすると良いアーキテクチャなのかとかが見ていて勉強になったので徐々に体験をよく感じました。また納得するまで議論できたのも体験が良くて良いと思います。

他にも特に関わってたエンジニアはほとんど一流(名前は略します)で、彼らの仕事や姿勢を間近で見れたのはかなりバリューでした。特にビジネスからも見れるエンジニアはすごい。組織を回っていうのはそういうことだと思うんですよね。ちゃんと見習います。

その過程で自分もなんでこれでこうしたほうがいいのかとかを考えてて動くようにしてたのは良かったかなと思います。

終わりに

いろいろ言いましたが総じて良い会社だと思っていて、新卒で東京都八重洲口にある某メディア企業行きたい人々の気持ちがなんとなくわかった気がします。セキュリティのことをするには大きくて面白い会社だと思います。僕も「まだ、ここにない、出会い。」を探してます

ちなみに稼いだお金はほぼ全額自分の大学の学費と教科書代とPCと食費と交通費に消えていきました。無事3年次に上がれたのですが四年次に上がれるかはわかりません。

なお次の会社もそこそこ大きくて内定はしてますが正確にはまだ契約書を交わしてないので決まってませんのでセキュリティかネットワークインフラについての労働力やR&Dを求めている人がいたらお声がけください。

ICTSC2018 の運営委員を務めました

こんにちは。takemioです。最近自分の名前の表記ブレが激しいのでどうしようかと思って悩んだ結果 https://takehaya.github.io/^(竹(たけ)|たけみお|たけはや)($|くん|ちゃん) でどうぞとかかいてしまったんですが、どうすればいいか悩んでます。いい案があればください。

さて、見出しのとおりですがICTSC2018 の運営委員を務めました。

表題の ICTSC2018 とは、正式名称を「ICTトラブルシューティングコンテスト2018」と呼びますが、学生主体の学生による学生のためのインフラ技術中心のトラブルシューティング大会です。以下トラコンと略します。決して某虎の㍁の婚活サービスではありません。

インフラ技術といっても色々ありますが、ここで指しているのは主に OSI参照モデルの L2-L7 あたりを指します。運営委員はL1もやってますが、問題としては出題されていないです。

私は今年から運営にjoinしてやってました。

なぜjoinしたのか

そもそも大会参加者になりたかったが、弊学から人を集めれそうにないって当時インターンで同期で運営だったきょんたんに相談したらリファラルされて気がついたら採用された。ありがたい・・・

一年間のフロー

5月に顔合わせ会があり、夏に一次予選がありました。そのあとはトラコン予備校と言うトラコン参加者に対して希望者がいればそちらまで出向いて講義をするという試みをしました。その後二次予選があり、3月に本戦でした。

やったこと

私がやったことは予選問題の作成と予備校の講師。また本戦問題の出題をメインにインフラの構成の手伝いとかしてました。

予選問題はパケットフィルタリングというテーマで問題を作りました。netfilter queueを使った問題や、tcpdumpの条件構文の問題。またそれらのバックエンドのBPFのバイトコードを読んでもらって挙動を理解してもらうということをしてもらいました。 詳しくは以下のリンクの解説を参照してください。

https://icttoracon.net/tech-blog/2018/08/27/ictsc2018-prep01-packet-w/

予備校では大阪と福岡を担当しました。 大阪ではパケットフィルタの解説、福岡ではdockerの解説をしました。嬉しいことに福岡で講義をした学生たちは後述する本戦で入賞したので少しでもここに役立っていたのかなとか、、、勝手にわいが育てたと思ってますw(すいません。本当におめでとうございます!)

そして本戦ではSRv6の問題を作成しました。この問題はSRv6でサービスチェインさせるもので少し難しい問題を作りました(半分趣味) 詳しくは以下のリンクの解説を参照してください。

http://icttoracon.net/tech-blog/2019/03/21/ictsc2018-f-12/

他にもバックボーンの検討段階を手伝ったりDNS立てたりいろいろしてましたが、ホットステージ後半はSRv6問題の構築に手間取っていてあまり手伝えていませんでした。つらい・・・

さいごに

まずは本大会のスポンサー様と参加者各位に御礼申し上げます。このような運びで無事大会が(本戦一日目午前中が大変だったりしたが)終わったのは各位のおかげだと思います。そして入賞者の皆さんおめでとうございます!

さて来年度もより一層面白い問題を作ったりしますのでよろしくおねがいします。