ISUCON 7 予選参加

ISUCON 7 予選二日目にチーム「ワイハリマ」(yuta1024, nhirokinet, tyabuki)で参加しました。76,904 点でした。

今回の題材は「isutaba」というチャットツールで、

  • ログインすると部屋がたくさんあるので、それぞれの部屋でチャットができる(部屋の権限などはない)
  • 部屋の発言およびほかの部屋の未読バッヂは、ストリーム風にリアルタイムに表示される。それを実現するために、各ブラウザが js で無限に新着を確認する「/fetch」 JSON APIをたたき続ける。
  • ブラウザは /fetch が返ってきてそのレスポンスを処理したら、また次の /fetch をたたく。サーバ側の /fetch 側はデフォルトで 1 秒待機(sleep 1)だが、こちらは変更可能。
  • デフォルトで、 web app 2 台、 db (MySQL) 1 台の計 3 台構成。ただし、構成は自由に変更可能で、 FE サーバの集合は自由に選択可能。
  • デフォルトではユーザアイコンも含めて自由に変更可能で、全部 db に載っている。ユーザアイコンの URL は、デフォルトでは画像の sha + 拡張子。画像への権限管理なし。

という内容でした。

リポジトリ

isubata リポジトリ by チーム「ワイハリマ」

準備内容

最低限、よくある MySQL や nginx の設定、kataribe、公開鍵一覧と配布スクリプトを準備。プライベートリポジトリに関しては、性能に余裕のあるマシンで GitLab を用意し、全員のアカウントを作り、外からも使用可能なようにしておいた。

当日

思い出せる範囲で。個人で書いているので、他の二人の実施内容に関しては違っている可能性がある。

  • 開始が10時から12時に変更だったのでそれに合わせて集合したが、最終的には13時になったので最初の進捗
  • まず、サーバに全員分の鍵や、 MySQL には InnoDB の書き出し間隔の遅延、 MySQL インデックスの追加などを実施。
  • 一旦一台構成に移行し、 icons を MySQL ではなく静的ファイルとして扱うよう変更。(初期段階としては)ある程度伸びる。
  • サーバのミドルウェアを一部は新しいものに更新しつつ、設定をざっくりチューニング。また、設定ファイルを扱いやすくするなどを実施。キャッシュ回りもいじっていたようだ。ここは担当していないので詳細はよくわからないがここで点数が大幅に伸びていた模様。
  • リクエストが多い /fetch, /message について、 Slim 脱却を試みる。テンプレートを使っていなかったのでそれ自体の修正量は少なめ。
    • ここで、 /fetch での sleep(1) が話題に上る。単につぶすと性能が下がる。 /fetch 自体はスコアに勘定されないルールになっており、単に排除するといたずらに負荷をかけることになる。
  • 40k を超えたあたりで、 php-fpm のワーカ数を調整しようとして、 sleep(1) が話題に上る。これが php のプロセス数を食っているようだ。とりあえず増加。
  • 静的ファイルの icons を相手のサーバにも送り付ける口を作り web app を二台に戻すが、性能が上がらない。
  • とにかく /fetch の sleep の問題が大きいということで、 /fetch だけは専用の php-fpm プールに逃がすように変更してスコアが上昇。
  • 同時に、ベンチマーカーのエラーメッセージや kataribe の結果などから、 icons が異常に遅延(nginx 視点でテイルレイテンシ 10 秒)していることに気付く
  • 外向け 100Mbps 、一分間での画像リクエスト約 1.4k ということで、画像ファイル平均 500kB 程度とすると帯域をほぼ使い切っている計算。
  • メモリを使い切ってキャッシュが減っているのでは?という疑惑はあった
  • ベンチマークを何度か走らせると、 +/- 15% 程度は平気で増減する状態であり、ガチャの影響も大きかった
  • icons が大幅遅延するかどうかもガチャ次第だった。
  • web app を二台活用できない理由も、 icons が遅延する(あるいは上位チームがこの何倍ものスコアを叩き出せる)根本原因もわからないが、終盤が近づいたので web app を一台運用することを決定。
  • 再起動試験実施。
  • 再起動後二回目の試験で、運よく過去最高得点が出た。この時終了約15分前だったので、以後外れベンチマークを引かないように全ての作業を終了。

感想

今回は、ウェブアプリケーションとしてはかなり王道なチューニングが主で、それでいて難易度が高く仕上がっていて、良い問題だったと感じました。

まだほかのチームの write up はあまり見られていませんが、それでも特に複数台構成/ローカルファイルシステム同期における /icons の更新日時や、せめてもの二台目の活用方法など、少し読んだだけでも見直すべき部分が色々出てきました。

二日目の通過ラインは 210,464 点で、ここからさらに三倍食えるようにしないといけなかったようです。とはいえ、学びはあったので、今後予選通過を狙えるようにしたいです。

Ubuntu 16.04 に Eclipse をインストールして C++ を開発できるようにする

基本的には Java をインストールして Eclipse のウェブページからインストーラをダウンロードするだけなのだが、微妙につまづいたのでメモ。

インストール先

インストーラが自分のホームディレクトリにインストールする。なので、 Eclipse のインストールには root 権限は不要。
パスは通らないので、自分で通すかフルパスで起動する。

Cannot run program "/bin/bash" 的なエラーメッセージが表示される

フルパス書いてあるし /bin/bash も起動可能だしなぜ?となるが、メモリ不足だった。VM であまりメモリを振っていなかったところに Eclipse を入れたためで、 VM の割り当てメモリを増やすと動作。

C++ のプログラムを読み込んだら、基本的に include が解決できていない

std::vector とか、間違いなく標準であるようなものも設定しないと現れないようだ。適当なファイルを g++ -v でコンパイルすると途中で次のようなのが出るので、全部プロジェクトのプロパティの C/C++ General > Paths and Symbols > Includes に追加する。

#include "..." search starts here:
#include <...> search starts here:
 /usr/include/c++/5
 /usr/include/x86_64-linux-gnu/c++/5
 /usr/include/c++/5/backward
 /usr/lib/gcc/x86_64-linux-gnu/5/include
 /usr/local/include
 /usr/lib/gcc/x86_64-linux-gnu/5/include-fixed
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.

OpenSSL での証明書発行手順メモ

オレオレにしろちゃんとしたものにしろ、証明書の作成は毎回ググるし、その割には必要なコマンド群にありつけるまでに微妙に目的と違うことを書いているサイトをめぐるのに時間がかかるので、最低限のコマンドだけメモ。気が向いたら更新予定。だいたい何をしているかはわかっているけどコマンドオプションなどは覚えていられないから手っ取り早くコピペしたい人/時向け。

拡張子は、形式を尊重して key, crt の代わりに両方 pem にすることも。

秘密鍵作成

openssl genrsa -out hoge.key 4096

秘密鍵を暗号化保存したい場合は -des や -aes などのオプションを付与し、パスフレーズを入力する。

CSR 作成

openssl req -new -key hoge.key -out hoge.csr

外部の CA に署名してもらう場合(例えばブラウザでそのまま使えるちゃんとした証明書が必要な場合)はこの CSR を渡して証明書を受け取る。
このファイルは署名を受けとることができたらあとは削除して問題ない。

自己署名

openssl x509 -in hoge.csr -days 3650 -req -signkey hoge.key -out hoge.crt

自己署名でない場合、 -signkey ではなく -CA オプションを使用する。

Collabora Online の Ubuntu ネイティブパッケージのインストールのメモ

Collabora Online は、すぐ使えるものとしては以前 Docker イメージとして提供されていたが、最近になっていくつかの Linux ディストリビューション向けにパッケージが提供されるようになり、 Ubuntu 16.04 用の apt ソースの URL も提供されたので、それに従ってインストールした。その際、微妙に SSL だけ設定する必要があったのでメモ。これを機に別のページに、普通にオレオレ証明書のメモをいつでも見ることができるところに書こうと思った(追記:すぐ追加した:OpenSSL での証明書発行手順メモ | にろきのメモ帳)。現時点で必要というだけなので、将来的には自動実行されるようになるかもしれない。

NextCloud + Collabora | にろきのメモ帳 に従来構成でも必要だった手順を書いてある。

基本的には Collabora Online Development Edition (CODE) – Collabora Productivity にある通り、

# import the signing key
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 0C54D189F4BA284D
# add the repository URL to /etc/apt/sources.list
echo 'deb https://www.collaboraoffice.com/repos/CollaboraOnline/CODE ./' &amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;gt; /etc/apt/sources.list
# perform the installation
apt-get update &amp;amp;amp;amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp;amp;amp;amp; apt-get install loolwsd code-brand

で入って 9980/tcp で HTTPS で LISTEN しはじめる(あとはリバースプロキシを構成すれば使える)はずだったが、これだと loolwsd が起動に失敗した。journalctl -u loolwsd で見てみたところ、証明書があるべき場所にないようだ。あと、そもそも、原因の切り分けはしていないが数十 kB/s 程度しか出ず、大容量のパッケージをダウンロードする必要があったため数時間かかった(追記:数日後別の場所から実施したときは、警戒して最初から screen で実施したにも関わらず数分で終わった)。

/etc/loolwsd/loolwsd.xml を見ると鍵や証明書がおかれるべき場所がわかったので、以下のように実行したら動いた。

$ cd /etc/loolwsd/
$ sudo openssl genrsa -out key.pem 4096
$ sudo openssl req -new -key key.pem -out tmp.csr
$ sudo openssl x509 -in tmp.csr -days 3650 -req -signkey key.pem -out cert.pem
$ sudo cp cert.pem ca-chain.cert.pem
$ sudo rm tmp.csr
$ sudo service loolwsd start

(2018/12/26 追記)Setting up Nginx reverse proxy – Collabora Productivity を見るとそもそも loolwds 自体は TLS を使わず、 nginx のリバースプロキシ以降は平文通信する設定方法が書かれていたので、一台のマシンで nginx と loolwds が共存する場合はそちらの方がよさそう。/etc/loolwsd/loolwsd.xml を修正し、このページに書いてある通り ssl.enable を false に、ssl.terminate を true にする。

RouterBoard RB3011UiAS-RM 導入

自宅のルータは長らく iptables, dnsmasq あたりで構成した Ubuntu VM で実施してきたが、障害時の対応が面倒なのに加えて、サーバメンテナンス中という何かと急いでぐぐりたい場面で自宅インターネットが使えないという致命的な問題があったので、以前名前を聞いておもしろそうだった MikroTek 社の RouterBoard に移行した。Linux ベースの RouterOS なるものを搭載していて、 RB3011UiAS-RM は ARM ベースとのこと。ネットワーク機能も含めてなにかと仮想化がもてはやされるこのご時世、 VM からアプライアンスに移行するのは時代に逆行しているような気もするけど、自宅だとスケールメリットはおろか冗長構成ですらデータ保全ぐらいしかしないので、シンプルなアプライアンスがよいと判断。ちなみに、 1U Rackmount と書かれているが、この機種は奥行きが狭いのと、 1U ラックの横幅として言われる 19 インチ(482.6mm)はサーバルームで見るとでかそうに見えるが意外と家における大きさなのもあって、ラックサーバを置く気にならない自宅でも意外と現実的な大きさだった。ラックマウント用の金具の他に、普通にその辺に置く用のゴム足も付属した。

今回は、 RN3011UiAS-RMBaltic Networks から購入した。購入したときは取り寄せが必要で一か月くらいかかるとのことだったが、今見ると In Stock なので時期によるかもしれない。購入時、発送方法は料金だけを見て無料だった “Pick up at Lisle warehouse” を選んだが、「請求先も発送先も両方日本なので、この選択肢はおかしい。これらの選択肢の中から適切なものを指示してほしい」というメールがきて、返信すると対応していただけた。あとで気付いたのだけど Lisle は Baltic Networks の拠点の地域なので、これは Lisle warehouse という名前の業者に関する何かではなく、「Lisle の倉庫まで取りに行く」という意味だったのかもしれない。ちなみに AC アダプタのプラグは US 用で、日本と同じ形で 100V にも対応していたのでそのまま使用できた。品物 $145.00, 送料(FedEx International Economy) $49.59 で計 $194.59。

左 5 ポート、右 5 ポート、 SPF 1 ポートで、基本的にはポートごとに設定をすることができるようだ。ただ、左右それぞれ最大一つ master port を選ぶことができ、左右それぞれ master port を含むポートを 5 ポートの中から任意で選んで master port にぶら下げるとそれらは同一の L2 セグメントとして wire speed で通信できるようだ(この場合 slave port についてはちゃんと設定がいじれないようになっているっぽい)。左右をまたぐ場合は bridge を構成して実現するようになっている。初期設定では ether1 が DHCP で外、それ以外は中というよくある自宅ネットワークを想定したものになっており、 bridge の下に ether1 以外を治める左 master port、 右全部の master と SPF がぶら下がっていた。

まだあまりいじれていないが、メニューを見て色々できるな、と思ったのと同時に、例えばファイヤーウォールの設定画面で言えば iptables の設定で登場する用語がそのまま登場していた。DNS, DHCP に関しては GUI でいじれるが、 dnsmasq みたいに DNS が DHCP と連携するみたいな機能はないのでそこは手動でどうにかするしかなさそう。だいたいの設定は Web 画面でいじれそうで、試していないものの(VPN 等に使うと思われる)証明書発行用 GUI も Web にあった。ターミナルもブラウザから開くことができたほか、 ssh などでも使えるので、通常はルータへの接続のためにシリアルケーブルを用意する必要はなさそうだった(内蔵の液晶で IP アドレスもわかるし)。

とりあえず、最低限のファイヤーウォールだけ設定して、普通にルーティング(IPv4 で、ネットワークは異なって RouterBoard のポートも別だがマスカレードではないルーティング)経由で iperf してみたところ、

------------------------------------------------------------
Client connecting to ***, TCP port 5001
TCP window size: 85.0 KByte (default)
------------------------------------------------------------
[  3] local *** port 54934 connected with *** port 5001
[ ID] Interval       Transfer     Bandwidth
[  3]  0.0-10.0 sec  1.05 GBytes   901 Mbits/sec

さすがにただのルーティングは充分速度が出るようだった。

また、実際に RB3011UiAS-RM 自身で IPv4 マスカレードするように構成して、インターネット(上流に IPv4 を IPv6 にトンネルする ISP 支給のルータがいるので PPPoE ではない)に繋いで www.speedtest.net で試してみたところ、

500Mbps 以上のスループットが出せることが確認できた。ファイヤーウォールの項目数を増やすとどうなるかはわからないのと、そもそも普通の ISP 経由のインターネット経由なのでこれが上限でもない(事実導入前と比べて遅くはなっていない)ので資料としては不充分なものの、自宅のインターネット用としては充分な速度が出ると言えそうだった。

内蔵液晶パネルに何を表示するかは選択可能だが、 Informative Slideshow だと全ポートの合計スループットから電圧、ファームウェアバージョン、時刻まで色々と出してくれる。一方、 Slideshow だとポートごとのグラフを出してくれるようだ。実用的というよりかは眺めていて飽きないという内容といえる。ちなみに、液晶自体は自動パワーオフの時間が設定可能だが、どちらかというと電源ランプの青色 LED が眩しいので(我が家のように)寝床と隔てがない空間に置く場合は、人によって(例えば僕のような場合)は直接光と壁への反射について対策をしたほうがよい。

ちなみに設定は困ってぐぐったら割と MikroTik Wiki が出る。ちなみに設定だいたいをエクスポートする /export というコマンドもあるようだ。

一方、最初に困ったのは AC アダプタで、運よく米国のサイトから仕入れたため AC アダプタもそのまま使用できたが、 DC 側が接触が微妙なようで、少し抜ける側に力が加わっただけで DC 入力が途絶えるようだ。力を加えようと思わないのに途絶えるほどではないのと、アダプタと本体どちらの不具合か、または仕様かもわからないのにどれほどかかるかわからない海外への依頼をするのも面倒なので運用でカバーすることにした(後半に追記あり;やっぱり解決することにした)。本体裏には謎の溝がありアダプタの DC ジャック付近のケーブルはそこにかませて仮固定できる。また、電圧は 10-28V (付属アダプタの出力は 24V)と幅広く受け付けているので直径さえあえば適当な AC アダプタを使うことはできそう。

また、この機種は感圧式タッチパネル付き液晶画面を備えているが、購入当初は(初期設定で 1234 の)暗証番号の入力すらも困難な状態だった。これに関しては Web UI 経由で Recalibrate を押し、その後タッチパネル側で指示に従ってキャリブレーションすることで完全に解消された。

まだ設定は完了していないが、一通り安定導入後も遊び用のポートを空けておいていじってみたいと思えた。

(2017/8/16 追記)証明書の署名操作(実際には秘密鍵の生成から実施している可能性がある)で分単位から十分単位の時間がかかるようだ。WebUI だとすぐ、ターミナルだと一定時間経った後にレスポンスがあるが(後者はタイムアウトとの表示)、実際には証明書の生成作業を行っており、先ほどレスポンスが返ってきた操作は終わっていない、かといってテンプレートの削除は Template being used! というエラーメッセージになる、そして数分待つと削除できる、という状態になった。WebUI の Tools > Profile で様子を見ることができる。見る限り、 CPU を 100% (コア単位)使っているようなのだが、この機械はデュアルコアなので 200% まで使えるので複数同時に走らせたりしなければ大丈夫そうだ。ARM CPU にしても秘密鍵生成、電子署名ってそんな時間かかる……?とは思うが、かかるものは仕方がないのでそういうものと思って使うのがよさそう。

(2017/8/28 追記)AC アダプタがゆるい問題は、秋月電子通商で外径 5.5mm 、内径 2.1mm で中央がプラスの、24V/1.5A の AC アダプタ(1,500 円)を買ってきたところ解決した。少なくとも今のところ、店舗の張り紙によると取り扱っているのはこの大きさ/極性しかないらしいので、大きさがあうものが取り扱われていたのは運がよかったということか。
なお、オンラインにあった説明書で 5.5mm/2mm と書かれていた。また、 [solved] Power connector problem in RB2011-RM and RB3011-RM – MikroTik RouterOS に同様の現象で悩んでいる状況の書き込みがあり、色々と分析がなされていたが、新機種にバンドルされた電源ユニットはゆるすぎて、旧機種のものが新機種にもちょうどよいというコメントや、内径が 2.5mm のアダプタだとゆるくなって、 2.1mm のものが正しいというコメントが書かれていた。秋月電商のアダプタは店の張り紙で内径 2.1mm と明記されていて確かにそのくらいに見えて、付属品は自信はないが 2.1mm より大きいような気はする(下の写真は付属品の方)。

NextCloud + Collabora

NextCloud と Collabora Office を連携させると複数人で同時に(共有リンク経由でも) LibreOffice ファイルを編集できて便利そうなので入れて見みました。なんだか色々ややこしかったというかハマりどころが多かったのでメモです。

NextCloud 設定

NextCloud 自体は、よくある設定のまま。細部は違えど、 Nginx Configuration — Nextcloud 11 Server Administration Manual 11 alpha documentation に書かれたままでよく、はまりどころも少ない。

NextCloud と Collabora を連携させるにあたって、セキュリティ上の理由で別のサブドメインで運用することが推奨されていた気がするが、ソースが見当たらない(あと、なぜか今 https://nextcloud.com/collaboraonline/ につながらない)。

Colabolla Online Development Edition 設定

Collabora に関しては、基本は先ほどのページの内容で実施するが、いくつか修正が必要だった。

まず、以前 VPS でやってうまくいかなかった経験から、一度 LAN 内のサーバでオレオレ鍵で試していたが、これだと権限がない旨が出力された。https://help.nextcloud.com/t/libreoffice-nextcloud-access-forbidden-issue/6358/17 に書かれているように、 NextCloud から Collabora に繋ぎに行くときに SSL 証明書を検証するのに失敗したときもこのエラーが出るらしく、オレオレ証明書を使う場合は (NextCloud のディレクトリ)/resources/config/ca-bundle.crt に自分が使う証明書を追記する必要があった。

続いて、つなぐと Collabora の画面が真っ白で止まった。nginx 側の設定を NextCloud のものからコピーしたが、そこに

add_header X-Frame-Options "SAMEORIGIN";

が含まれていたので削除した。(追記:削除する意味は各自で考えること!安全ではないように思えるので再検討中)

続いて、

wsd-00024-00033 15:52:08.397614 [ websrv_poll ] ERR  #20 Exception while processing incoming request: [GET /lool/https%3A%2F%2F(略) H...]: Invalid or unknown request.| wsd/LOOLWSD.cpp:1665

みたいなエラーが出た。Collabora: Invalid or unknown request – support / collabora – Nextcloud community に書かれている通り、 nginx 側の、 Collabora で使うドメインで

chunked_transfer_encoding off;

を追記すると動いた。

最終的に、Collabora 側の nginx の設定は、

server {
        #listen 80 default_server;
        #listen [::]:80 default_server;
        listen 443 ssl;
        listen [::]:443 ssl;

        ssl_certificate /etc/nginx/ssl/**;
        ssl_certificate_key /etc/nginx/ssl/**;

        chunked_transfer_encoding off;

        server_name ***;
        add_header X-XSS-Protection "1; mode=block";
        add_header X-Robots-Tag none;
        add_header X-Download-Options noopen;
        add_header X-Permitted-Cross-Domain-Policies none;

        proxy_buffering off;

        location ^~ /loleaflet {
                proxy_pass https://localhost:9980;
                proxy_set_header Host $http_host;
        }

        location ^~ /hosting/discovery {
                proxy_pass https://localhost:9980;
                proxy_set_header Host $http_host;
        }

        location ^~ /lool {
                proxy_pass https://localhost:9980;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "upgrade";
                proxy_set_header Host $http_host;
        }

}

という感じになった。(検証環境のものなので、 TLS 自体の設定はかなり手抜きになっている)

あとは以前試した時は繋ぐと Connecting で 1 分ぐらい待たされたりタイムアウトしたりする現象があったが、最新の CODE 2.1 を入れたところ解消したようだ。

HP MicroServer Gen8 導入

従来使用してきた hp ML115 Gen 5 もそろそろ寿命が近そうだったところで、 hp MicroServer Gen8 が安くで売られているのを見つけたので購入し、移行した。Amazon の HP ProLiant MicroServer Gen8 販売ページでは、在庫が安定しないようだが Celeron モデルが株式会社ゼット(zmart.jp)の販売で 38,458 円で入手できた。

Microserver Gen8

在庫及び配送は Amazon が担当し、販売元は並行輸入品を廉価販売する株式会社ゼットが担当するようだった。機械はチェコ製(本体裏面では「MADE IN CZECH REPUBLIC」「捷克制造/捷克製造」)らしい。(欧州製にも関わらず一部記載は英語/中文簡体/中文繁体で、また一部記載は欧米の言語色々だった。謎。)

こちらの販売元で購入したものは、並行輸入品につき、電源コードが日本用ではなかった(付属品一覧を見る限り英国用と欧州用の二本?)ため、電源コードは別途自前で購入する必要があった。ディスプレイ付属のケーブルを流用できる端子だったので、とりあえず家にあった別の機器の流用で動作確認しつつ電気屋で買った。

メインメモリ

標準では 4GB のメモリが付属したが、2スロットで16GB(8GB×2)にまで増設できる。32GBは検索したところだと成功例がヒットせず、失敗例が多くあるようだ。ということで、HP Microserver Gen8を購入 | 三日坊主の軌跡を参考に、このページで動作実績のあるKTH-PL316E/8Gを二つ購入し、計16GBのメモリを認識させることができた。iLO と組み合わせた場合 HP 純正でないためアラートがくるとリンクした動作実績に書いてあったが、それはスルーした。

どこを分解すればメモリを交換できるかよくわからなかったので検索したところ、MicroServer Gen8 のメモリを交換している YouTube 動画がヒットした。

HDD

簡単と見せかけて、最初に購入した HGST の 6TB のディスクは騒音の問題に悩まされた。x64 な Ubuntu に搭載する HDD に相性問題なんかないだろうと思いながらも一応勧められるままにツクモの交換保証に加入していたが、この保証は「思ったよりうるさい、家に置くにはうるさすぎる」みたいな理由でも期間内なら別の機種への交換が可能(価格が上がる場合は差額、下がる場合はそのまま)で、しかも回数に制限がないため、思わぬ形で救われた。結局、 Western Digital Red 6TB (WD60EFRX 、Pro ではない方)に落ち着いた(まあこれでもシーク音はまあまあ鳴るが、僕の中では許容範囲)。

音の問題はそもそも感じ方や使い方に個人差がある上、静かな方のディスクでもうるさいと言っている人がいる一方でシーク音が大変うるさいディスクでもアイドル時に静かなためか静かと評価する人がいるのでレビューの見方が難しい。とはいえ傾向はあるので、最終的にはレビューを複数サイトでちゃんとみてればもう少しスムーズにいけたな、とは思った。

後で気付いたことだが、Linux mdadm で高速に RAID1 array を構築する – Qiita 等で記述されているように、特にオプションを指定しないまま mdadm で RAID 1 を構築するとどういうわけか「多分二つのディスクの内容は一致すると思う」みたいな状態から開始するらしく、同期が完了するまでは両ディスクから読み込んだうえで片方のディスクで書き込みなおすためにシークが大量発生するようだ。このせいで騒音が増大していたともいえるし、このお陰で本格的にデプロイする前から騒音に気付けたともいえる。

あと、交換後の HDD の片方が初期不良だったようで、なんだかインストーラが変な動きをするのでエラーログなどから片方のディスクがおかしいと判断、基本的に書き込みエラーだったので USB-SATA 経由で Western Digital の Data Lifeguard Diagnostic for Windows の全クラスタデータ削除に突っ込んでみたところ、数分で FAIL となった。仕方がないので秋葉原にあるツクモのサポートセンターに持ち込んで症状を伝えたらその場で交換していただけて、そちらを入れたら問題なかった。ちなみにツクモはサポートセンターと販売店が別ということを知らなかったので、ツクモの販売店に20:10ぐらいに持ち込んだら、20:00までにサポートセンターに持ち込めば対応していただける旨を教えていただけた。何をするにも下調べが大事。

HDD が初期不良であった場合のエラーログについては、 Ubuntu のインストーラのメニュー画面の中に「syslog を HTTP で公開する」みたなオプションがあることを初めて知った。LAN 内の他の端末でブラウザで見たりダウンロードしたりできて便利だった。

(今回やたらお世話になったので色々ツクモを称賛してるけど別にツクモの中の人とかじゃないよ)

インストール

Ubuntu インストールにあたってはハードウェア固有の罠はなかったはず。通常通りソフトウェア RAID を構築して OS をインストールすればよい。

Ethernet

まず、LAN端子は、二つとされていながらiLOなるネットワークベースの技術も搭載されていると書かれていた。公式ウェブページから想像つきにくいまま購入したが、結局は「通常のLAN端子2つ」「OSから見えず、iLOが使う端子一つ」の計3つが搭載されていた。(いずれも Broadcom チップらしい)

Ubuntu 16.04 では eno1/eno2 として見えたが、その順序について少し厄介だった。最初1と書かれた端子にのみケーブルをつないでいたところ、ケーブルが刺さっている方が eno1 となった。ところがその後で 2 と書かれた方にもケーブルを刺して再起動したところ、これまで eno1 と書かれていたものが eno2 になり、新しく刺したほうが eno1 として認識された。謎。

謎のランプ

先ほどの写真にも写っているしそもそも公式の画像でもだいたい写っている通り、このサーバの下部には謎の青いランプがある。飾りかと思っていたら、添付文書では正常稼働の確認の文脈でこのランプが青く光っていることの確認が電源ランプの確認と別に書かれていた。「Health LED bar」とか名前がついており、電源ボタンについているランプとは別物らしい。

その他

  • hp ML115 Gen5 と比べても通常時のファンの音が大変静か(ただし、比較対象の hp ML115 Gen5 はリアケースファンが 2000-5200 rpm で、僕は気にしなかったがアイドル時でもうるさいという人は多いはず)
  • 起動してから OS に制御が渡されるまではファンの回転数が制御されず普通にそこそこファン音がするのは hp ML115 Gen5 と同様(とはいえ、さすがに同じ状態の hp ML115 Gen5 よりは静か)
  • BIOS が色々検査している画面が妙にグラフィカル
  • カタログの時点でわかるが念のため追記:HDDはホットプラグ非対応。HDDを交換しようとすればまあ気付くであろう位置に注意書きがある。
  • カタログの時点でわかるが念のため追記:光学ドライブは別売りオプション。先ほどの写真の通り、なくとも見栄えが悪いというほどではない。
  • 今回は Ubuntu で使用したため実体験と全く関係のない話だが、 hp が公式にカスタムした ESXi イメージを配布している(添付文書にリンクあり)ため、どうやら ESXi からオンボード RAID が使えるらしい。ただし、HP ProLiant MicroServer Gen8を買って地雷を順番に踏んでった話 – なおたこブログにはパフォーマンスに難がある旨が記されている。
  • 日本用電源コードすら付属しない並行輸入品でも、 BIOS 画面は English/日本語 の二択だった。謎。
  • iLO は便利そうなのに試しすらしないで稼働開始してしまった。すみません。

SECCON 2016 Online CTF Writeup

SECCON 2016 Online CTF にチーム「yharima」で参加していました。なかなか難しい問題が多く、まだまだ勉強せねば、という感じがしました。145位、500点でした。

VoIP

pcap ファイルが渡されるので Wireshark で開き、 VoIP 機能で開くと音が再生できてフラグをしゃべっていた。少し聞き取りづらかったが、がんばって聞き取って提出。

Memory Analysis

メモリダンプがおかれており、 volatility というツールがヒントに書かれている。

偽の svchost がアクセスしているウェブサイトを調べてほしい、そのサイトに行けばフラグが手に入る、というもの。また、ヒントには「hosts」とも書いてあった。

volatility の使い方を軽く確認したあと、ウェブサイトを見ていると書いてあるのでまずは通信内容を確認した。

$ volatility_2.5_linux_x86 -f ./forensic_100.dat --profile=WinXPSP2x86 connections
Volatility Foundation Volatility Framework 2.5
Offset(V)  Local Address             Remote Address            Pid
---------- ------------------------- ------------------------- ---
0x8213bbe8 192.168.88.131:1034       153.127.200.178:80        1080

IP アドレスはわかったが、繋ぎに行っても nginx の初期インストール画面のようなものが表示されたので、「hosts」というヒントを元に C:\Windows\System32\drivers\etc\hosts を探すことに。filescan や dumpfiles を試みたがなかなか手に入らないので、最終的にはstrings して「153.127.200.178」で grep した。

$ strings forensic_100.dat | grep 153.127.200.178
153.127.200.178    crattack.tistory.com attack.tistory.com 
153.127.200.178    crattack.tistory.com 
153.127.200.178:80

ホスト名は候補が二つに絞られたが、 /etc/hosts を手元に書いてこのホスト名・IPアドレスの組み合わせにつないでもまだフラグはない。いろいろ見ているうちに、 HTTP をしゃべっている内容のようなものが見えていたので、このやり取りもまだメモリに残っているのでは、ということで “GET /” を探した。

$ strings forensic_100.dat | grep 'GET /' 
GET /image/237C0C3E576BA0AD06F720 HTTP/1.1
GET /entry/Data-Science-import-pandas-as-pd HTTP/1.1
GET /entry/Data-Science-import-pandas-as-pd HTTP/1.1
GET /entry/Data-Science-import-pandas-as-pd HTTP/1.1
GET /entry/Data-Science-import-pandas-as-pd HTTP/1.1

ようやく URL がわかったので、 アクセスするとフラグ。

$ curl http://attack.tistory.com/entry/Data-Science-import-pandas-as-pd
SECCON{_h3110_w3_h4ve_fun_w4rg4m3_}

Anti-Debugging

バイナリファイルが渡され、調べてみると Windows の exe ファイルだったので Windows 上で起動。起動するとパスワードを聞かれ、パスワードが違うと言われた。

Windows のバイナリデバッグはしたことがなかったので、全体的に yuta1024 に手伝ってもらって(というよりほぼ教えてもらって)実施。まずは OllyDbg をインストールした。

OllyDbg には上の画像のような文字列検索機能があり、それぞれ参照元の命令がある場所も検出できた。パスワードが違う的なエラーの周辺で、色々している判定っぽい処理があったので、そこで一旦止めると “I have a pen.” という文字列がメモリ上にあった。これがパスワードではないかと推測して入力すると表示が変わり、今度はデバッグ環境であることが検出された。デバッグではない環境で実行するとパスワードが正しいと言われたが何も表示されなかった。

エラーメッセージには OllyDbg や VMware などいろいろなものが書かれており、その少し前のコードを見るとだいたい直前に「CMP DWORD PTR SS:[***],*」「JNZ SHORT bin.********」の二つを実施しており、おそらくは正規の実行では通らないようになっていたので、 OllyDbg の機能で JNZ を JMP に(命令の一バイト目を 0x75 から 0xEB に)書き換えた。

すると、今度はゼロ除算で落ちるようになった。その部分の IDIV 命令(上の画像の 0x4015DE)だけ NOP で塗りつぶすと、何もしないで正常終了するようになった。文字列一覧では暗号化されたフラグと思わしき意味ありげな文字列があったので、その参照元を見るとその IDIV と同じ関数の少し下にあった。

そのため、落ちる IDIV から、フラグと思わしき文字列の参照元の直前までを NOP で塗りつぶして実行すると、フラグが出力された。

(追記)同じチームのメンバーの writeup です。

SECCON 2016 Online writeup – yuta1024’s diary

ISUCON 6 予選参加

チーム「にゃーん」で ISUCON 6 予選参加して惨敗してきました。厳しい。というわけで記録です。といっても、結局自分が担当した部分以外はあまり把握していないので、それは各自がブログか何かを書くのを待ちます。

まず、インスタンスを起動して、あまり分ける必要がない二つのアプリケーションが動いていることを確認したので、れにが二つをマージしていました。その間に DB のインデックスで露骨にまずいものがないか見ていましたが、一応張ったもののそれほど大幅ということはありませんでした。

htmlify なる関数があり、これが今回のメインのようでした。記事(初期状態で約七千)のタイトルを本文中から自動抽出してリンクに変換するものですが、全ての記事のタイトルを抽出したうえで正規表現や SHA1 を活用した実装になっていて遅いこと遅いこと。

なお、今回は Ruby だったこともあり初期実装は0点でした。

そのあと、他のチームメンバーは Web 周りのチューニングなどをしてそれなりに点数を伸ばしていたようですがあまり把握しておらず、僕は htmlify をなんとか早くしようとしていました。

他のメンバーと違い、僕は Ruby の知識があまりなかった状態でこれを見たので、 Ruby の置換が同様に書かれたほかの言語と比べても遅いことに絶望して、なんとか早くするアルゴリズムを模索しました。とりあえずなんにしてもその部分を Ruby で書くのは諦めて、 C++ で書いて外部コマンドで呼び出すことにしました。(引数で辞書と入力を渡し、出力は標準出力)

二分探索をしようとしたりさまよっていましたが、最終的にはハッシュマップで実装しました。与えられた文字列に対して短いほうから辞書にある最大の長さまで試す、ただし途中で打ち切るための情報を埋め込む、というものです。ポインタでいろいろ扱える方が楽だったのでポインタをベースとし、何かとコピー回数の増える std::string はなるべく使わないように変更、 C++ の std::map は hashmap ではないことを思い出して std::unordered_map に変えましたが、パフォーマンス計測値、スコアともに改善はしたものの依然遅いままでした。

#include <vector>
#include <string>
#include <string.h>
#include <algorithm>
#include <stdio.h>
#include <unistd.h>
#include <map>
#include <iostream>
#include <unordered_map>

using namespace std;

void urlwrite(const char *str) {
	while(*str!='\0'){
		if ((*str>='a' && *str<='z') || (*str>='A' && *str<='Z') || (*str>='0' && *str<='9') || *str=='.' || *str=='-' || *str == '_' || *str == '~') {
			fwrite(str, 1, 1, stdout);
			++str;
			continue;
		}
		int t = *((unsigned char*)str);
		int b1 = t/16;
		int b2 = t%16;

		char tmp[3];
		tmp[0]='%';
		tmp[1]=(b1<10)?(b1+'0'):(b1+'A'-10);
		tmp[2]=(b2<10)?(b2+'0'):(b2+'A'-10);

		fwrite(tmp,1,3,stdout);
		++str;
	}
}

void htmlwrite(const char *str) {
	while(*str!='\0'){
		switch (*str) {
			case '&':
				fwrite("&amp;", 1, 5,stdout);
				break;
			case '<':
				fwrite("&lt;", 1, 4, stdout);
				break;
			case '>':
				fwrite("&gt;", 1, 4, stdout);
				break;
			case '\'':
				fwrite("&#039;", 1, 6, stdout);
				break;
			case '\"':
				fwrite("&quot;", 1, 6, stdout);
				break;
			case '\n':
				fwrite("<br />\n",1,7,stdout);
				break;
			case '\r':
				fwrite("<br />\r",1,7,stdout);
				break;
			default:
				fwrite(str,1,1,stdout);
		}
		++str;
	}
}


int main (int argc, char *argv[]) {
	unordered_map <string, int> dic;

	setvbuf(stdout, NULL, _IOFBF, 102400);

	size_t longest=0;
	for(int i=2;i<argc; ++i){
		string key=string(argv[i]);
		for(int j=1;j<4;++j){
			string tmp=key.substr(0,j);
			if(!dic.count(tmp)){
				dic[key.substr(0,j)]=1;
			}
		}
		{
			int j=6;
			string tmp=key.substr(0,j);
			if(!dic.count(tmp)){
				dic[key.substr(0,j)]=1;
			}
		}
		dic[key]=2;
		longest=max(longest, key.length());
	}

	while (*argv[1]!='\0') {
		int skiplen=1;
		string title;
		int flag=0;

		for(int i=1;i<=longest;++i){
			char tmp=argv[1][i];
			argv[1][i]='\0';
			string key=string(argv[1]);
			argv[1][i]=tmp;
		
			if(dic.count(key)){
				if(dic[key]==2){
					title=key;
					flag=1;
				} 
			}else{
				if(i<4 || i==6){
					break;
				}
			}
		}

		if(flag){
			fwrite("<a href=\"/keyword/", 1,18,stdout);
			urlwrite(title.c_str());
			fwrite("\">", 1,2,stdout);
			htmlwrite(title.c_str());
			fwrite("</a>", 1,4,stdout);
			skiplen=title.length();
		}else{
			char tmp=argv[1][1];
			argv[1][1]='\0';
			htmlwrite(argv[1]);
			argv[1][1]=tmp;	
		}
		argv[1]+=skiplen;
	}
}

※ちなみに、<a href=”/keyword/~~”> の部分は、 Ruby の見本実装では <a href=”http://192.0.2.1/keyword/~~”> のように URL 変換済み、 PHP の見本実装では <a href=”/keyword/~~”> のように path 部分のみでした。後者で書きましたが、不思議でした。

その後、 Trie 木を実装しましたが、初期化コストが大きく辞書を作るのに 100ms 程度かかってしまいました。外部コマンドなので毎回初期化していたのと、特にバイナリでいい感じに複数のデータを受け渡す実装の時間もなく、トップページを表示するのに 10 回読みだしてしまい、却って遅かったのでこちらは導入に至りませんでした。Trie 木生成後の処理は一桁速くなったように思われる(雑な計測からの体感)ので、 Ruby のモジュールにするか、これ専用のサーバを立てて記事追加・削除・データ変換が毎回行えるようにすればかなり高速化できたものと思われますが、すでに終盤でそこまでする時間がありませんでした。

#include <vector>
#include <string>
#include <string.h>
#include <algorithm>
#include <stdio.h>
#include <unistd.h>
#include <map>
#include <iostream>
#include <unordered_map>

using namespace std;

void urlwrite(const char *str) {
        while(*str!='\0'){
                if ((*str>='a' && *str<='z') || (*str>='A' && *str<='Z') || (*str>='0' && *str<='9') || *str=='.' || *str=='-' || *str == '_' || *str == '~') {
                        fwrite(str, 1, 1, stdout);
                        ++str;
                        continue;
                }
                int t = *((unsigned char*)str);
                int b1 = t/16;
                int b2 = t%16;

                char tmp[3];
                tmp[0]='%';
                tmp[1]=(b1<10)?(b1+'0'):(b1+'A'-10);
                tmp[2]=(b2<10)?(b2+'0'):(b2+'A'-10);

                fwrite(tmp,1,3,stdout);
                ++str;
        }
}

void htmlwrite(const char *str) {
        while(*str!='\0'){
                switch (*str) {
                        case '&':
                                fwrite("&amp;", 1, 5,stdout);
                                break;
                        case '<':
                                fwrite("&lt;", 1, 4, stdout);
                                break;
                        case '>':
                                fwrite("&gt;", 1, 4, stdout);
                                break;
                        case '\'':
                                fwrite("&#039;", 1, 6, stdout);
                                break;
                        case '\"':
                                fwrite("&quot;", 1, 6, stdout);
                                break;
                        case '\n':
                                fwrite("<br />\n",1,7,stdout);
                                break;
                        case '\r':
                                fwrite("<br />\r",1,7,stdout);
                                break;
                        default:
                                fwrite(str,1,1,stdout);
                }
                ++str;
        }
}

class Trie {
        public:
                Trie* children[256];
                uint32_t ex[8];
                int exist=0;

                Trie(){
                        memset(ex,'\0',32);
                }

                void insert(unsigned char *str) {
                        if(*str=='\0') {
                                exist=1;
                                return;
                        }


                        if(!((ex[(*str)/32]>>((*str)%32))&1)){
                                children[*str]=new Trie();
                                ex[(*str)/32]|=1UL<<((*str)%32);
                        }
                        children[*str]->insert(str+1);
                }

                int find(unsigned char *str) {
                        int tmp=-2;
                        if (exist) {
                                tmp=-1;
                        }

                        if((ex[(*str)/32]>>((*str)%32))&1){
                                tmp=max(tmp, children[*str]->find(str+1));
                        }

                        if(tmp==-2)
                                return -2;
                        else
                                return tmp+1;

                }

};

int main (int argc, char *argv[]) {
        unordered_map <string, size_t> dic;
        Trie trie;

        setvbuf(stdout, NULL, _IOFBF, 102400);

        for(int i=2;i<argc; ++i){
                trie.insert((unsigned char*)argv[i]);
        }


        cout<<"<br>clock="<<clock()<<"<br>";

        while (*argv[1]!='\0') {
                int ret = trie.find((unsigned char*)argv[1]);
                int skiplen=1;

                if(ret>0){
                        char tmp=argv[1][ret];
                        argv[1][ret]='\0';
                        fwrite("<a href=\"/keyword/", 1,18,stdout);
                        urlwrite(argv[1]);
                        fwrite("\">", 1,2,stdout);
                        htmlwrite(argv[1]);
                        fwrite("</a>", 1,4,stdout);
                        skiplen=strlen(argv[1]);
                        argv[1][ret]=tmp;
                }else{
                        char tmp=argv[1][1];
                        argv[1][1]='\0';
                        htmlwrite(argv[1]);
                        argv[1][1]=tmp;
                }
                argv[1]+=skiplen;

        }
        cout<<"<br>clock="<<clock()<<"<br>";
}

この辺をバックグラウンドで走らせる準備などが必要なのだと学ぶことができました。

Linux マシンを IPv6 ルータにする(Ubuntu 16.04)

Interlink で IPv6 オプションがリリースされ、「ほぼ固定だけど長期間使用しないと変更される可能性がある」 IPv6 prefix が PPPoE で降ってくるようになりました。(「IPv6接続サービス」提供開始のご案内

ということで、Ubuntu 16.04 の VM 経由で PPPoE に繋いでルーターにしてみました。

Raspberry PiでIPv6 PPPoE対応ルータを作る – ももいろテクノロジーフレッツ光ネクスト用IPv6 PPPoEアダプターを作ってみる — SOUM/misc を参考に実施したので、こちらを参照するとよいと思いますが、自分用のメモを兼ねて残します。

なお、色々試してうまく行ったあと記憶を基に再現しているので、追加の手順が必要だったなどあればコメントでお願いします。

VM 構成

  • ens160: フレッツ網に繋がる NIC (ONU 直結、またはそれと等価な状態)
  • ens192: 今回の設定に関係させてはいけない NIC (実験のため設置)
  • ens224: 部屋のネットワークに繋がる NIC。ここに繋いだ端末を IPv6 インターネットに繋ぐのがこの記事の目標

パケットが適切に届けば VM でも特に変わらない。

(余談ですが ens160 みたいなのは、 eth0 みたいなのと同じく NIC のデバイス名。Predictable Network Interface Device Name を Ubuntu 16.04 などのシステムが導入しているのでこういう名前になっている模様。)

フレッツ網からの RA ブロック

NTT東西のフレッツ網は日本国内で IPv6 イントラネットを構成しており、 PPPoE しようとしてフレッツ網に接続すると IPv6 の RA (イントラネットにも関わらずグローバルアドレス)が降ってきてしまう。

状況次第ではあるが、 ISP から直接フレッツ網に IPoE と呼ばれるものを引き込んでくれる契約をしていなければ、この IPv6 網に繋がってしまう.
もし他のプロバイダが IPoE 設定になっていると今度は複数のインターネットに繋がるプレフィックスに同一マシンがつながってしまうことになる。いずれも(理解してそうなっているのでなければ)よろしくない。(IPv6 – Wikipediaに詳しく書かれている)

今回はプロバイダに対して DHCPv6 接続するので、全ての RA の受信を遮断する設定にした。/etc/sysctl.conf に追記。ens160 が、フレッツ網(PPPoEや、フレッツのイントラネットに接続)に繋がる NIC だとする。

$ cat /etc/sysctl.conf | tail -n 4 | head -n 2
net.ipv6.conf.all.accept_ra = 0
net.ipv6.conf.ens160.accept_ra = 0

$ sudo sysctl -p /etc/sysctl.conf

IPv6 over PPPoE (pppoeconf)

apt で pppoeconf をインストールして pppoeconf コマンドで設定。ダイアログが出てくるのに答えれば PPPoE セッション自体は張ってくれるが、 IPv4 と違って少し追加設定が必要。/etc/ppp/peers/dls-provider を修正。

$ cat /etc/ppp/peers/dsl-provider
# Minimalistic default options file for DSL/PPPoE connections

noipdefault
defaultroute
replacedefaultroute
hide-password
#lcp-echo-interval 30
#lcp-echo-failure 4
noauth
persist
#mtu 1492
#persist
#maxfail 0
#holdoff 20
plugin rp-pppoe.so
nic-ens160
user "***@***"
usepeerdns
+ipv6
ipparam ipv6default

修正したら、

$ sudo poff dsl-provider
$ sudo pon dsl-provider

で再接続して反映。ifconfig ppp0 でリンクローカルアドレスがインターフェイスに付与されていることを確認。

デフォルトルートの設定

PPPoE を起動した際、ルーティングテーブルに追加されるようにする。なお、 /etc/ppp/ipv6-up.d/ 以下のファイルを新規追加する場合は実行権限が必要。

$ cat /etc/ppp/ipv6-up.d/v6route
#!/bin/sh
if [ -z "${CONNECT_TIME}" ]; then
    if [ "${PPP_IPPARAM}" = "ipv6default" ]; then
        ip -6 route add default dev ${PPP_IFACE}
    fi
fi

$ ip -6 route
(snip.)
default dev ppp0  metric 1024  pref medium

DHCPv6 クライアント (wide-dhcpv6)

このままではまだインターリンクとの間に貼られた PPPoE セッションでのみ使えるリンクローカルアドレスしか手に入っていないので、 DHCPv6-PD クライアントを実行し、グローバル IPv6 アドレスの /64 prefix の移譲を受ける。

apt で wide-dhcpv6-client をインストールした。

/etc/wide-dhcpv6/dhcp6c.conf を設定

profile default
{
  information-only;

  request domain-name-servers;
  request domain-name;

  script "/etc/wide-dhcpv6/dhcp6c-script";
};

interface ppp0 {
  send ia-pd 0;
  request domain-name-servers;
  script "/etc/wide-dhcpv6/dhcp6c-script";
};

id-assoc pd 0 {
  prefix-interface ens224 {
    sla-id 1;
    sla-len 8;
  };
};

なお、 ens224 は PPPoE で使っている NIC ですらなく、 LAN 側で使っているインターフェイス。不思議だが、 ppp0 に指定してもうまくいかない上、逆にちゃんと MAC アドレスを持つ NIC なら IPv6 接続に関係のないインターフェイスを指定してもアドレス自体は取得できた。DHCPv6 を含む IPv6 のアドレス取得メカニズム自体にも MACアドレスを利用するものはあるので関係があっても不思議ではないが、検証するのが大変そうなので今のところ深追いはしていない。

ただ、どうもこの後の設定に影響はするらしく、最終的には室内 LAN に繋がる NIC を指定することとなった。

ファイヤーウォール (ip6tables)

RedHat 系だと標準で保存した iptables を起動時に読み込む機能が備わっているが、Debian 系はそういうわけでもない。と思っていたら、 netfilter-persistent なるパッケージがあり、これをインストールすると似たことができる模様。

apt で netfilter-persistent をインストールする。iptables や ip6tables をいじったら、 sudo service netfilter-persistent save などとして保存できる。/etc/iptables/rules.v[46] に保存するようなので、これを直接編集することもできなくはないと思われる。

というわけで、あとは ip6tables を設定する。LAN 内がプライベートアドレスのため変換が必要となり iptables でマスカレードを指示していた IPv4時代と異なり、基本的に全てのマシンがグローバルアドレスを持ち、普通に end to end で通信するのを中継するだけなので、 iptables はなくとも通信は可能ではある(中継設定自体は後述)。が、安全のため、とりあえず従来と同様外から中に通信を開始するのをブロックしてみた。

ip6tables 自体は以下の感じ。ens224 が IPv6 を提供する部屋LAN、 ens192 が今回の設定に関与させない(IPv6 に繋がない)NIC。

# Initialize
ip6tables -X
ip6tables -F

# Default Rule
ip6tables -P OUTPUT ACCEPT
ip6tables -P FORWARD DROP
ip6tables -P INPUT DROP

# Established
ip6tables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# LAN
ip6tables -A INPUT -i ens224 -j ACCEPT
ip6tables -A INPUT -i ens192 -j DROP

# loopback
ip6tables -A INPUT -i lo -j ACCEPT
ip6tables -A OUTPUT -o lo -j ACCEPT

# ICMPv6
ip6tables -A INPUT -p icmpv6 --icmpv6-type echo-request -j ACCEPT
ip6tables -A INPUT -p icmpv6 --icmpv6-type echo-reply -j ACCEPT
ip6tables -A INPUT -p icmpv6 --icmpv6-type router-solicitation -j ACCEPT
ip6tables -A INPUT -p icmpv6 --icmpv6-type router-advertisement -j ACCEPT
ip6tables -A INPUT -p icmpv6 --icmpv6-type neighbor-solicitation -j ACCEPT
ip6tables -A INPUT -p icmpv6 --icmpv6-type neighbor-advertisement -j ACCEPT

# FORWARD
ip6tables -A FORWARD -i ens192 -j DROP
ip6tables -A FORWARD -o ens192 -j DROP
ip6tables -A FORWARD -i ens224 -j ACCEPT
ip6tables -A FORWARD -i ppp0 -m state --state RELATED,ESTABLISHED -j ACCEPT

# OUTPUT
ip6tables -A OUTPUT -o ens192 -j DROP

転送設定

ルータなのでここが基幹部分と言える(といっても Linux の機能を呼び出すだけ)。ファイヤーウォールの設定が済んだら設定する。/etc/sysctl.conf に追記。

$ cat /etc/sysctl.conf | tail -n 1
net.ipv6.conf.all.forwarding = 1

$ sudo sysctl -p /etc/sysctl.conf

Router Advertisement (radvd)

RA を用いて接続するマシンの自動設定を行うため radvd を入れた。ただ、現状を踏まえると DHCPv6 を導入した方がいいかもしれない(次のクライアント側の設定の問題)。

cat /etc/radvd.conf

interface ens224 {
  AdvSendAdvert on;
  MinRtrAdvInterval 3;
  MaxRtrAdvInterval 10;
  prefix ::/64 {
    AdvOnLink on;
    AdvAutonomous on;
    AdvRouterAddr on;
  };

  RDNSS 2001:db8::1 2001:db8::2
  {
  };
};

RDNSS の部分の二つの IPv6 アドレスは実際にはプロバイダが公知している DNS キャッシュサーバの IPv6 アドレスを直書きした。書いたら radvd を再起動。

sudo radvdump ens224 などとすると、配布内容を確認できた。

クライアント側

ここでは IPv6 のみに繋がるクライアントを一台構成した。IPv4 を設定する場合は基本的に独立して設定してよいはず。

RFC5006RFC6106などで RDNSS プロトコルが定められており、DHCPv6 を用いなくとも RA で DNS キャッシュサーバのアドレスを通知することができ、radvd も対応している。しかし、少なくとも Ubuntu 16.04 標準の機能でこれを有効にする方法は見つからなかった。そのため、この説明では DNS キャッシュサーバのアドレスをクライアント側で直書きしている。微妙。今後調査したい。

それ以外は自動設定で問題なかった。

/etc/network/interfaces

# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

source /etc/network/interfaces.d/*

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
# This is an autoconfigured IPv6 interface
auto ens160
iface ens160 inet6 auto
    dns-nameservers 2001:db8::1 2001:db8::2

しかし、現状では DHCPv6 のほうがよさそうな気もするので、今後試してみたい。