お腹.ヘッタ。

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

linux kernelでパケットがなぜかルーティングされないとかドロップされるときにみる記事

本記事は 2020linuxアドベントカレンダーで書こうと思ってた話の供養です。 卒論で書けなかったんですすいません....

本人的にはいい感じに設定を入れてるのにどーーーしてなのか全くいい感じにパケットが通らない時がある。 そんな時にロジカルにトラブルシュートをできるようになりたくありませんか?

ここではパケットが通らない時の実際の事例2つを挙げてそれをどのようにして探し出して解決したかを紹介します。

対象読者は iproute2をググれば使える人たちぐらいを想定してます。簡単な記事です:)

TL;DR

こういう時使えるツール類は以下の通りなので気合で使ってにゃん

  • trace-cmd: なんの関数が呼ばれたかわかる便利な相棒。XDPでもお世話になったりするし便利
  • perf: さっきと同じ感じ。
    • こいつらを眺めるときは kernelshark を使うとグラフィカルで目に優しい
  • ipftrace2: skbbuffにmetadataをつけてやることでパケットがkernelのどこを通ったがわかる便利ツール
  • dropwatch: udpパケットがどこでドロップしてるかわかる。しかしL2L3などのレイアーは共通なので十分に使えるので初手これを使っておくと楽そう。
  • bpftrace: そもそも関数の返り値とか眺めたいときはこれを使うと良い。ただしプローブが書かれてないときは自分で書く必要がある。

    • 今回は取り扱わないが機会があれば書く。
  • どんな事例の前でも共通して見ておく&やっておくといいこと

    • 正しくプロトコルとiproute2などの設定を入れるコマンドを理解してるかを見ておく。RFCやドラフトは読みましたか?manページは見ましたか?
    • tcpdump -i eth1 -vvv とかで期待してるパケットが来てるか
    • ethtool -S eth1 でRX or TXでドロップされてるのかとか。どの辺で死んでそうかをイメージしておく
    • ip n とかでL2が解決してるかを確認

事例1: どうしてか SRv6 End が動いてくれない

EndというのはSRv6のパケットを受けてSRHにあるipv6アドレスのポインター次に進めてそれに合わせたにルーティングする技術のことです。 前提として使ってたのは

  • fedora33
  • iproute2

となります。また どんな事例の前でも共通して見ておくといいこと で書いたことは全て行ったあとです。

今回は正しいパケットが来てるのにリダイレクトされないというところが問題でした。

この場合ですととりあえずどんな関数コールをされているのかをあたりをつける必要があります。 なのでそもそもSRv6のEnd動作をするkernelの関数コード (input_action_end) まで到達してるかをtrace-cmdで確認しました。

具体的にはまず https://github.com/torvalds/linux/blob/master/net/ipv6/seg6_local.cのコードを眺めます。 みるとわかるのですが input_action_end* のような形で一定のパターンで実装が記述されています。SRv6自体は複数のlocal functionをRFCにて定義されてるため、同じようなノリで書かれてる関数がいくつかあるのではないかと推論されるため、同じようなパターンを持つコードにかかれるのではないかと想定されます。

またトレース可能かどうかを調べるのも良いでしょう。trace-cmdの場合は以下のようにすると所望の関数へのプローブが対応してるかが分かります。

cat /sys/kernel/debug/tracing/available_filter_functions |grep input_action_end
input_action_end_dt6
input_action_end_dx6
input_action_end_dx2
input_action_end_dx4
input_action_end
input_action_end_x
input_action_end_t
input_action_end_b6
input_action_end_b6_encap
input_action_end_bpf

で、実際やってみるのは以下のようにします。

trace-cmd record -p function_graph -g input_action_end
trace-cmd report

しかし実際には動いておらずレコードがされませんでした。

このことから endを行うコードまでは到達してないことがわかります。ということでその前にdropしてるということがわかります。 では次にそもそもどこを通ってどのようなコールが行われるのかを考えたいですよね? 次に使ったのは ipftrace2を利用しました。これはskbというパケットが含まれる構造体にあるmetadataの領域に特定の値を仕込んでおくことでそのパケットがどこを通るのか出力します。

sudo ip6tables -t raw -A PREROUTING -d fc00:2::2 -j MARK --set-mark 0xdeadbeed
sudo ipft -m 0xdeadbeed

これを使うと以下のような出力が出ます。

677084806878622 001          __skb_checksum_complete
677084806882860 001                        kfree_skb
677084806887163 001           skb_release_head_state
677084806892984 001                 skb_release_data
677084806897402 001                     kfree_skbmem
677084836856575 001                  ip6_route_input
677084836879043 001                        ip6_input
677084836881744 001                     nf_hook_slow
677084836895052 001                  nf_ip6_checksum
677084836899705 001          __skb_checksum_complete
677084836903818 001                        kfree_skb
677084836907878 001           skb_release_head_state
677084836913613 001                 skb_release_data
677084836918676 001                     kfree_skbmem
677084865837153 001                  ip6_route_input
677084865860250 001                        ip6_input
677084865863010 001                     nf_hook_slow
677084865876901 001                  nf_ip6_checksum
677084865881458 001          __skb_checksum_complete
677084865885631 001                        kfree_skb
677084865889919 001           skb_release_head_state
677084865895676 001                 skb_release_data
677084865900353 001                     kfree_skbmem
...

大体どこを通ってるかを理解することができました。

ちなみにiptables以外にもtcを利用することでマークをつけることもできます。以下の例はtun1を通るパケットにマーク1, eth1を通るパケットにマーク2を与えている例です。

sudo tc qdisc add dev tun1 ingress
sudo tc filter add dev tun1 parent ffff: protocol ip matchall action skbedit mark 1
sudo tc qdisc add dev eth1 ingress
sudo tc filter add dev eth1 parent ffff: protocol ip matchall action skbedit mark 2

ではこの辺を起点に調べると良いでしょう。ただこの状態だとiptablesを使ってるのでnfがどこにかかってるのかわかりません。一旦切りましょう ここからは二通りの方法があります。

一つはtrace-cmdやperfで虱潰しに眺めることです。以下のようなコマンドで調べれたりします。

sudo perf record -g -a -e skb:kfree_skb
sudo perf script

他にもtrace-cmdで ip6_input をみるとこんな感じになります https://gist.github.com/takehaya/8e07656af34bc6b7b132843147f364c5

二つ目は dropwatch を使うことです。 初手これでもいいぐらいだったんですが どこでdropしてるかが即座にわかります。

以下のようにしたらinstallできるはずです

sudo apt install -y libnl-3-dev libnl-genl-3-dev libreadline-dev libpcap-dev binutils-dev make automake libtool m4 autoconf clang pkg-config
git clone https://github.com/nhorman/dropwatch.git
cd dropwatch
./autogen.sh
./configure
make
sudo make install

今回はこれで最後に落とし込んでしまいましょう。

sudo dropwatch -l kas
dropwatch> start

30 drops at nf_hook_slow
...

さて、先ほどのトレースにも出てきましたが nf_hook_slow というのが出てきました。 雑にぐぐるこのような記事がありました。 nfというのはnetfilter系のコードでiptables周りとかをコールすると動くものであると理解できると思います。

あとはちょっとした連想ゲームです。

iptablesとは?=>linux 組み込みのfirewallfedoraにおいてfirewallを司るものは??

そう、原因は firewalldでした。

ということで解決方法は以下の通りになります。

systemctl stop firewalld

事例2: どうしてか SRv6 End.DX4 が動いてくれない

End.DX4というのはSRv6のパケットでなおかつinnerがipv4のパケットを受けてouterをdecapした上で任意のNexthopにルーティングする技術のことです。 前提として使ってたのは

  • ubuntu18.04
  • iproute2

となります。また どんな事例の前でも共通して見ておくといいこと で書いたことは全て行ったあとです。 今回の問題はなぜかnexthopにパケットが飛ばないでした。

さて先ほどと同じようなノリで呼び出されるべきな関数のトレースから見てみましょう。

trace-cmd record -p function_graph -g input_action_end_dx4
  plugin 'function_graph'
Hit Ctrl^C to stop recording
^CCPU0 data recorded at offset=0x4b5000
    4096 bytes in size
trace-cmd report
cpus=1
          <idle>-0     [000]  4724.772017: funcgraph_entry:                   |  input_action_end_dx4() {
          <idle>-0     [000]  4724.772043: funcgraph_entry:                   |    decap_and_validate() {
          <idle>-0     [000]  4724.772043: funcgraph_entry:                   |      get_srh() {
          <idle>-0     [000]  4724.772044: funcgraph_entry:        0.698 us   |        ipv6_find_hdr();
          <idle>-0     [000]  4724.772045: funcgraph_entry:        0.082 us   |        seg6_validate_srh();
          <idle>-0     [000]  4724.772046: funcgraph_exit:         2.053 us   |      }
          <idle>-0     [000]  4724.772046: funcgraph_entry:        0.171 us   |      seg6_hmac_validate_skb();
          <idle>-0     [000]  4724.772046: funcgraph_entry:        0.203 us   |      ipv6_find_hdr();
          <idle>-0     [000]  4724.772047: funcgraph_exit:         3.766 us   |    }
          <idle>-0     [000]  4724.772047: funcgraph_entry:        0.058 us   |    dst_release();
          <idle>-0     [000]  4724.772049: funcgraph_entry:                   |    ip_route_input_noref() {
          <idle>-0     [000]  4724.772049: funcgraph_entry:                   |      ip_route_input_rcu() {
          <idle>-0     [000]  4724.772051: funcgraph_entry:                   |        make_kuid() {
          <idle>-0     [000]  4724.772051: funcgraph_entry:        0.472 us   |          map_id_range_down();
          <idle>-0     [000]  4724.772052: funcgraph_exit:         1.000 us   |        }
          <idle>-0     [000]  4724.772052: funcgraph_entry:        3.247 us   |        fib_table_lookup();
          <idle>-0     [000]  4724.772056: funcgraph_entry:                   |        fib_validate_source() {
          <idle>-0     [000]  4724.772056: funcgraph_entry:        0.053 us   |          l3mdev_master_ifindex_rcu();
          <idle>-0     [000]  4724.772057: funcgraph_entry:                   |          make_kuid() {
          <idle>-0     [000]  4724.772057: funcgraph_entry:        0.055 us   |            map_id_range_down();
          <idle>-0     [000]  4724.772057: funcgraph_exit:         0.457 us   |          }
          <idle>-0     [000]  4724.772058: funcgraph_entry:        0.820 us   |          fib_table_lookup();
          <idle>-0     [000]  4724.772059: funcgraph_entry:        0.105 us   |          l3mdev_master_ifindex_rcu();
          <idle>-0     [000]  4724.772059: funcgraph_exit:         3.146 us   |        }
          <idle>-0     [000]  4724.772060: funcgraph_entry:        0.066 us   |        ip_handle_martian_source.isra.40();
          <idle>-0     [000]  4724.772060: funcgraph_exit:       + 10.579 us  |      }
          <idle>-0     [000]  4724.772060: funcgraph_exit:       + 11.295 us  |    }
          <idle>-0     [000]  4724.772060: funcgraph_entry:                   |    kfree_skb() {
          <idle>-0     [000]  4724.772061: funcgraph_entry:                   |      skb_release_all() {
          <idle>-0     [000]  4724.772061: funcgraph_entry:        0.098 us   |        skb_release_head_state();
          <idle>-0     [000]  4724.772061: funcgraph_entry:                   |        skb_release_data() {
          <idle>-0     [000]  4724.772062: funcgraph_entry:                   |          skb_free_head() {
          <idle>-0     [000]  4724.772062: funcgraph_entry:        0.665 us   |            page_frag_free();
          <idle>-0     [000]  4724.772063: funcgraph_exit:         1.184 us   |          }
          <idle>-0     [000]  4724.772063: funcgraph_exit:         1.788 us   |        }
          <idle>-0     [000]  4724.772063: funcgraph_exit:         2.793 us   |      }
          <idle>-0     [000]  4724.772064: funcgraph_entry:                   |      kfree_skbmem() {
          <idle>-0     [000]  4724.772064: funcgraph_entry:        0.240 us   |        kmem_cache_free();
          <idle>-0     [000]  4724.772065: funcgraph_exit:         0.758 us   |      }
          <idle>-0     [000]  4724.772065: funcgraph_exit:         4.366 us   |    }
          <idle>-0     [000]  4724.772065: funcgraph_exit:       + 23.213 us  |  }

おや?今回は上手いこと到達してるのがわかりますね。 上から順番に調べていくと ip_handle_martian_source.isra.40(); という他と比べると何に使ってるのかわからないのがあることに気がつくと思います。 (残念ながらnetworkに詳しくないとエスパー気味かもしれません)

ちょろっと調べるとMartian packet: https://en.wikipedia.org/wiki/Martian_packet と言うものであると理解できます。

IANAの場合は martian packetを interfaceがそのネットワークを利用してないのに到着するパケットと言ってます。詰まるところサブネットを構成してないのに到着するパケットのことです。 詳しくはぐぐるといろいろ出るのでそこに任せます。これとかみると良いでしょう https://www.thegeekdiary.com/how-to-interpret-linux-martian-source-messages/

さて、以上のことから答えは出ました。RPfilterを無効にしましょう

sysctl net.ipv4.conf.eth0.rp_filter=0

簡単だったでしょう?

まとめ

事例がSRv6だけなのは自分の趣味ですが(すいません)、解決方法を見ればなーーんだwって思いましたよね?

さらにハマるパターンというのは少ないので大体

  • rpfilter
  • firewalld
  • フォワード忘れてる
  • arpが解決されてない、

とかそう言うレベルなのでとりあえず当てずっぽうで入れてしまえるんですよね。

しかし初見だとそんなkernelパラメーター知らんがなとなってどう解決すればいいか頭に出てこないと思います。なのでロジカルに解決できるというのは非常に重要なことだと自分は思っています。 本記事が皆さんの毎日生き生きlinux networking生活に役立てば幸いです。

fedora33にofedをインストールする方法

Why

ofedと言うmellanoxのnic driverがある。例えばVPI(IBとETH切り替え可能な仕組み)などを利用する場合はofedを入れないと動かなかったりする。またCisco T-RexなどのDPDKを使う時もそうなる。

しかしながらmellanox nic driverはインストール時にバリデーションがかかっており、対応していないディストリのバージョンでは動かない。例えば現在はfedora32まで対応しているが最新の33に対応してないと言う問題がある。このような場合のワークアラウンドをこの記事では述べる。

How

まずは事前に必要になるパッケージを入れる。前者は mlnxofedinstall と言うperlで書かれたインストーラーを動かすのに必要。後者は実行後に必要になる。

yum -y install perl-File-Tail perl-File-Copy perl-File-Compare perl-sigtrap
yum -y install kernel-rpm-macros python-srpm-macros rpm-build python3-devel tk gcc-gfortran tcsh elfutils-libelf-devel

次に現在最新のfedora32までに対応したofedを持ってくる

wget http://www.mellanox.com/downloads/ofed/MLNX_OFED-5.2-1.0.4.0/MLNX_OFED_LINUX-5.2-1.0.4.0-fc32-x86_64.tgz
tar xfvz MLNX_OFED_LINUX-5.2-1.0.4.0-fc32-x86_64.tgz
cd MLNX_OFED_LINUX-5.2-1.0.4.0-fc32-x86_64/

次にmlnxofedinstall をエディタで開いて fedora-releaseあたりでfindをすると以下のようなのが出てくる。

} elsif ($dist_rpm =~ /fedora-release(|-common)-(\d+)/ and
        ($2 >= 12) and ($2 <= 32)) {

これを33に対応させておく。

} elsif ($dist_rpm =~ /fedora-release(|-common)-(\d+)/ and
        ($2 >= 12) and ($2 <= 33)) {

そしたらインストールを以下のコマンドで行う。

./mlnxofedinstall --add-kernel-support --distro fc32
# reboot after run
/etc/init.d/openibd restart

Tips

mlnxofedinstall をするとパッケージが足りなくて止まったりするがその時はそれを親切に出してくれることがほとんどなのでそれを入れて mlnxofedinstall を再度繰り返すと良い。

How to debug the XDP Program 2020

最近は秋風を感じる日もあって夏も終わっちゃうなぁとしみじみしていますいかがお過ごしでしょうか。

宣伝ですが、最近は就活も終わり @gtpv2 という垢で linux netdevを見てキャッキャッするだけのツイートをしています。ぜひそっちも仲良くしてください。

最近 Twitter 上で猫も杓子も XDP!XDP!XDP! と流行ってきているのをとても感じています。以前は一部のキレッキレの企業エンジニアが書いてたと思ったら学生の研究になってたり。

私としましても、最近P4Runtime 経由で ARP のやりとりを kernel に無理やり移譲するみたいな実装を書いたりしているとやはり kernel様 の加護を得ているパケット処理フレームワークが世の中には求められているなぁとしみじみ思います。世はまさに大XDP時代ですね。

しかしながら、XDP を使うと eBPF 特有の書きにくさ、デバッグのし辛さがあり俺はパケットをいい感じに書き換えたいだけなのになんでこんな制約だらけのマゾみたいなコード書いてるんだろうとキレそうになるのでパケット処理王になるための道は非常に遠いと思います。

さて、今回はそれに対する処方箋として XDP のプログラムをデバッグをする方法をメモとしてまとめます。 昨年書いた記事(今日から始めるXDPと取り巻く環境について)のデバッグ話を補完するような感じでしょうか。XDPわからない太郎な人はこれを読んでからみるのを勧めます。そこの玄人様は退屈だと思うのでブラウザバックでお願いします〜

なお、結論から書いておきますが高速パケット処理話は基本的に気合で直す以外の選択肢はないので、そもそも速度いらないならAFなんちゃらなソケットから処理してもらうのが良さそうです...

アタッチが失敗する

意味通りアタッチが失敗してなんか知らないけど、うまく動かないときの話

大体こういうのは eBPFVerifier にブチギレられてることが多いと思います

llvm-objdump -S -no-show-raw-insn hoge.o

とりあえずこれでアセンブラを眺めてみる。 clangでコンパイルするときに -g を付けていればバイトコードに対応したソースコードも表示されるんでとりあえずコンパイルするときはつけておくと良い。 アセンブラの見方は割と気合でやるしかないんですが、別にアセンブラが読めなくても、死んでいる箇所とコードの対応がわかるのでその周辺がダメなのかもとあたりをつけてあげることはできると思います。

ちなみにちょっと初心者ある話としてはこんな風に言われてたらprogの名前が言われて

# ip link set dev ens3 xdpgeneric obj xdp_accept_ssh.o
Program section ‘prog’ not found in ELF file!

例えばコードに SEC("accept_ssh_only_main") とか書いていたら以下のようにプログラムのセクションを書いてください。

ip link set dev ens3 xdpgeneric obj xdp_accept_ssh.o sec accept_ssh_only_main

これはディフォルトのセクションネームが違うときは明示的に指定する必要があるという話です

Printデバッグ

主に実行時の条件文に入ったかなどの挙動を見る時に利用します

大体の人が bpf_trace_printk をして実行時の出力を見る感じでしょうか? 自分もそうなんですが、どうやってみてますか?BCCで流れてきた物を見てるだけでしょうか?

自分の場合は trace-cmd を利用し,以下のようなコマンドで動かしてます。

sudo trace-cmd record -e 'xdp:*' -O trace_printk
sudo trace-cmd report > trace.log

printk を仕込んで deploy して実行したあと、その当該ホストでこれを一つ目のコマンドを動かして採取しそれを取り出します。 雑に less とかして眺めるのもよいですが、結構いっぱいprintを仕込んだ場合は kernelsharkなどを使ってGUIで眺めておくと目に優しいです。

XDPcap

これはXDPから吐き出しているパケットが、どういう挙動をした結果どういうパケットを吐いているのかを見る時に使えます。 cloudflareが造ってる謹製のツールで、これを使うとXDPがどんな挙動でどういう判断を下したのかをみることができます。具体的にはpacketに XDP(TX|PASS|...) の判断の情報加えわって pcap にdumpできます。

まず事前準備として XDPのプログラムに渡す ヘッターファイルと tcpdumpにあたるコントロールコマンドをfetchします。

wget https://raw.githubusercontent.com/cloudflare/xdpcap/master/hook.h
go get -u github.com/cloudflare/xdpcap/cmd/xdpcap

次にxdpが動くノードでマウントをしてない場合はしておくと良い。

sudo mount bpffs /sys/fs/bpf -t bpf

ノリとしては以下のようなコードを描いて(これはpassするだけの簡単なコード)

#include "hook.h"

// https://github.com/cloudflare/xdpcap
struct bpf_map_def SEC("maps") xdpcap_hook = XDPCAP_HOOK();

int xdp_pass_func(struct xdp_md *ctx)
{
    return xdpcap_exit(ctx, &xdpcap_hook, XDP_PASS);
}

このプログラムを起動した状態で icmpだけ取り出したい場合は以下のようなコマンドを動かすことができます

sudo xdpcap /sys/fs/bpf/xdpcap_hook - "icmp" | sudo tcpdump -n -r -
sudo xdpcap /sys/fs/bpf/xdpcap_hook dump.pcap "icmp"

f:id:taketarou2:20200921084002p:plain
XDP_TX の例

eBPF Mapへの書き込み

Printデバッグと同様ですが、ある程度大きいペイロードとして利用する時に使ったり、定数書き換えとかに利用します 。

特にコードぽいものは示しませんが、デバッグ用に適当に構造体を作ってそれをやり取りするだけで便利です。 例えばペイロードごとねじ込むみたいなことをしてあげると全部は見たくないが・・・みたいなニーズに合わせやすかったり、何かしらの挙動のデバッグをする時に適当に定数書き換えをしておくと雑に動きを変えれて良いです。実際XDPのグローバル変数は長さ1のmapとして展開されます。

挙動のテスト

関数の実行の整合性を見る時に利用します。 具体的には bpf_prog_test_run という関数を使ってやってローカルでgeneric ebpfを呼び出してE2Eテストをすることができます。

複雑なプログラムなどを事前に細かい粒度で開発するのにお勧めです。例えばチェックサムだったり、encap/decapの場合は有効でしょう。

コントロール側の雰囲気としてはこんな感じです。

const char *file = "./main.o"; //対象の関数
struct bpf_object *obj;
__u8 buf[PACKETSIZE];
int err, prog_fd;
__u32 duration, retval, size, repeat=1;

struct ipv4_packet pkt_v4;

err = bpf_prog_load(file, BPF_PROG_TYPE_XDP, &obj, &prog_fd);
if (err) {
    fprintf(stderr, "ERR: loading eBPF object file (%d): %s\n", err, strerror(-err));
    return -1;
};
err = bpf_prog_test_run(
    prog_fd, // file disk
    repeat, // repeat nums
    &pkt_v4, // input packet
    sizeof(pkt_v4), // input packet size
    buf, // output packet
    &size, // output packet size
    &retval,  // return bpf type
    &duration
);

こればかり描いても仕方ないのでお気持ちを少し語っておくと、自分はよくGo+XDPで書いてるんですが、残念ながらGo+bpf_prog_test_runはあまり向いてなくてどういうことかというと go testsudo の相性がダメというところに帰着してきます。具体的にはGOPATH以下がrootの権限になるので次回以降動かす時にビミョい気持ちになります。

なので個人的なお勧めとしてはこういうことが起きない C or BCC(Python) でやると良さげだと思います。 もしCでやるときは気をつけて欲しいこととしてはunpackの挙動のアノテーション__attribute__((__packed__)); としないとclangは解釈してくれないので(GCCは問題がない)その辺も注意ポイントです。 BCCは良さげなyonazuno先生の日本語記事があるのでそちらをご覧ください。

終わりに

いかがでしたか?(1回言ってみたかった)

この記事はtwitterXDPわかんないよー! という話が聞こえたのでそれに対するアンサーになればと思い書きました。簡単な資料になってしまいましたが、自分としてはそれだけパケット処理は筋肉が問われる開発なんだなぁと改めて思いました。資料が少し参考になれば幸いです。

他にも最近だと、 bpftool だけではなく xdp-toolsというツールが出てきているんですが、雑に説明しておくと、こんな感じ

  • xdp-loader: XDPプログラム用のシンプルなローダー、NICに対してのマルチアタッチに対応してたりする(最新の機能で、テールコールのように複数プログラムをつけることができる。詳しくはLPC2020発表を参照
  • xdp-dump: XDPcapを入れなくてもXDPプログラムに入る前、またはXDPプログラムからの出口でdumpできるツールです。残念ながらexitの時のパラメーターは取れないけど・・・
  • xdp-filter: XDPを使ったパケットフィルタしてくれる君

READMEを読むとわかると思うのでそれは興味のある人への宿題として任せます。

このような便利ツールを使うだけではなく、いっその事オレオレ便利ツールを作ってしまうのもデバッグのコツでしょう。 例えば、いくつかのebpfmapが存在しているならばパケット処理以外の理由が関わってる部分があると思います。例えばネクストホップのテーブルやベアラなどがあるでしょう。何かしらの調査の時はそれの中身をdumpするだけのツールをさくっと別に作るだけでも何が正しいのかわかるのでデバッグが捗ります。

また、デバッグとはちょっとかけ離れますが、BTFを利用したバリデーションなどをしておくとパラメーターを正しく入れるので嬉しいと思います(C言語のメタプロ補助データという噂も聞きますがw) まぁそもそもcilium/ebpfなどのアプローチとしてELFからeBPFMapを生成するときなどはBTFから情報を得てsyscallを叩いていますのでこの辺の構成に慣れておくと良いでしょう。筆者も辛い気持ちで読んでいます(げっそり)

(ところで、この辺かなりP4のP4infoみたいになってますよね・・・似てくるのだろうか。)

まぁ最近も eBPF と XDP 周りでアップデートが激しいのでそのうち、その辺も動かしてる所感とか(e.g. CO-REの導入やマルチキャスト周りとか)の駄文を書き散らしたいなと思います。

それにしても...いろいろ 無駄に キャッチしてるのにパケット処理ネタ話しできるような話を今年は発表する場所すらないんだけど、コロナは悲しいなぁ...できる時にやっておくは重要ですね 😭😭😭

では〜

参考
  1. パケット処理の独自実装や高速化手法の比較と実践| JANOG45@札幌
  2. xdpcap: XDP Packet Capture
  3. xdp-tools

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のクライアントを書きたいなと思います。