IPv6 と Docker と NAT

IPv6 のアドレスが1つしかない状況で、ネットワークが分離されたコンテナから IPv6 で通信しようとすると IPv6 だろうと NAT が必要になる。 このへん Docker がどう扱うのかよくわかってなくて、結局 Docker の外側で docker0 とか ip6tables を管理することで動いた……

まず docker0 に使うレンジを決める。ここでは fdbb:3f26:ceda::/4810.11.0.0/16 とする。 docker0 を systemd-networkd で作る。

% cat /etc/systemd/network/docker0.netdev
[NetDev]
Name=docker0
Kind=bridge
% cat /etc/systemd/network/docker0.network
[Match]
Name=docker0

[Network]
Address=10.11.0.1/16
Address=fdbb:3f26:ceda::1/48

dockerd を起動する。このときネットワーク系のオプションを切っておく。

% cat /etc/systemd/system/docker.service.d/override.conf
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd --ipv6 --fixed-cidr-v6=fdbb:3f26:ceda::/48 --bridge=docker0 --ip-forward=false --ip-masq=false --iptables=false -H fd://
# systemctl start docker.service

デフォルトだと dockerd が IPv4 でやっていたことを、IPv4IPv6 の両方について自分でやる。

% cat /etc/sysctl.d/30-ip-forward.conf
net.ipv4.conf.all.forwarding = 1
net.ipv4.conf.default.forwarding = 1
net.ipv6.conf.all.forwarding = 1
net.ipv6.conf.default.forwarding = 1
# iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# iptables -A FORWARD -s 10.11.0.0/16 -i docker0 ! -o docker0 -j ACCEPT
# iptables -A POSTROUTING -s 10.11.0.0/16 ! -o docker0 -j MASQUERADE
# ip6tables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# ip6tables -A FORWARD -s fdbb:3f26:ceda::/48 -i docker0 ! -o docker0 -j ACCEPT
# ip6tables -A POSTROUTING -s fdbb:3f26:ceda::/48 ! -o docker0 -j MASQUERADE

これで Docker コンテナ内から IPv6 で通信できるようになる。

% cat Dockerfile
FROM ubuntu:16.04
RUN apt update && apt install -y curl
% docker build -t ubuntu-curl .
% docker run --rm ubuntu-curl curl -sv -o /dev/null https://ipv6.google.com/
*   Trying 2404:6800:4004:816::200e...
* Connected to ipv6.google.com (2404:6800:4004:816::200e) port 443 (#0)
(snip)

ただ、ULA なアドレスを使っているせいで A も AAAA も持っているようなドメインの場合、デフォルトだと IPv4 が優先されてしまう。

% docker run --rm ubuntu-curl curl -sv -o /dev/null https://google.com/
*   Trying 172.217.25.78...
* Connected to google.com (172.217.25.78) port 443 (#0)
(snip)

この優先度は /etc/gai.conf で決められているので、とりあえずそこを変えれば IPv6 が優先されるようになる *1

% cat gai.conf
label ::1/128       0
label ::/0          1
label 2002::/16     2
label ::/96         3
label ::ffff:0:0/96 4
% docker run -v $PWD/gai.conf:/etc/gai.conf:ro --rm ubuntu-curl curl -sv -o /dev/null https://google.com/
*   Trying 2404:6800:4004:819::200e...
* Connected to google.com (2404:6800:4004:819::200e) port 443 (#0)
(snip)

今回は Docker でやったけど、systemd-nspawn (machinectl) を使うときもやることは同じ。 docker0 と同じ要領で br0 を作って systemd-nspawn --network-bridge=br0 で起動する。 systemd-nspawn の場合は勝手にアドレスを割り当てたりしてくれないので、それはコンテナ内の systemd-netword でやるようにする。 このときデフォルトだと host0 に対して余計な設定があるので、コンテナ内で ln -s /dev/null /etc/systemd/network/80-container-host0.network で切っておく。

*1:curl の場合は -6 で IPv6 を強制できるけど

Ruby で実行時に引数のクラスを推測するやつ

https://gist.github.com/eagletmt/3e064fcbe2935a8356bc8658c8e472c1

require_relative '../infer_type'
RSpec.configure do |config|
  config.before(:suite) { @infer_type = InferType.new; @infer_type.start }
  config.after(:suite) { @infer_type.finish }
end

たとえば上記のように RSpec のフックに仕掛けて INFER_TYPE_TARGET=YourAwesomeApp:: bundle exec rspec とかやるとテストから適当に推測されたクラスが表示される。 とりあえず nullable かどうかと、TrueClass と FalseClass を Boolean と解釈するように調整を入れていて、このへんをがんばるともう少しいいかんじのクラスを表示できそう。

これと同じようなかんじで、YARD で書かれた型と一致するかどうかチェックできると便利そう。

tmux 2.2 以降で East Asian Ambiguous Width Character を正しく表示させる方法

これまで tmux は文字幅を得るために独自のテーブルを持っていて、その独自テーブルでは East Asian Ambiguous Width というものを一切考慮していないので、CJK な環境ではパッチをあてて使うことがよく行われていた (tmux cjk patch とかでググるといろいろ出てくると思う)。

tmux 2.2 からは wcwidth(1) を使うようになり、独自テーブルをやめてロケールの情報から文字幅を得るようになった https://github.com/tmux/tmux/commit/26945d7956bf1f160fba72677082e1a9c6968e0c 。 が、このコミットをよく見ると setlocale(LC_CTYPE, "en_US.UTF-8") で固定されており、LC_ALL や LC_CTYPE に関係なく en_US.UTF-8 が使われる。 tmux は UTF-8 を前提としており、そこを固定したい気持ちは分からなくもないが……

なので対応としては、en_US.UTF-8 ロケールで文字幅を変更してやれば、パッチなしで East Asian Ambiguous Width の問題を回避できる。 http://eagletmt.hateblo.jp/entry/2016/03/23/020117 に書いたような方法で /usr/share/i18n/charmaps/UTF-8-CJK.gz を生成し、/etc/locale.gen で en_US.UTF-8 UTF-8-CJK にして locale-gen したところうまくいった。 普段 en_US.UTF-8 と ja_JP.UTF-8 を使い分けているような人はこの方法だと厳しいけど、基本 ja_JP.UTF-8 しか使ってない人は en_US.UTF-8 で文字幅を変えても悪影響は無いと思う。

fluentd のバッファファイルを直接加工する

たとえば変なレコードが混じってしまったせいで何度リトライしてもバッファのフラッシュに失敗するようなときに、バッファファイル (buffer_type file で作られるやつ) を使っていれば、そのファイルをいじることで応急処置ができる。

バッファファイルは [tag, time, record] という三つ組の列を msgpack でシリアライズした形式になっていて、v0.12.x と v0.14.x で time の型が違う *1 けど、どちらのバージョンでも以下のようなコードでバッファファイルを読み書きできそう。 なお v0.12.x において Fluent::Engine.msgpack_factory が追加されたのは v0.12.17 から なので注意。

require 'fluent/engine'

in_path = '/path/to/some-buffer.q0123456789abcdef.log'
out_path = '/path/to/modified-buffer.log'

def modify(tag, time, record)
  # do something
  [tag, time, log]
end

File.open(in_path) do |fin|
  unpacker = Fluent::Engine.msgpack_factory.unpacker(fin)
  File.open(out_path, 'w') do |fout|
    packer = Fluent::Engine.msgpack_factory.packer(fout)
    unpacker.each do |triplet|
      packer.write(modify(*triplet))
    end
  end
end

一旦 fluentd を停止してバッファファイルが触られないようにしてから、上のようなスクリプトで変更を加えたバッファファイルを作成し、 mv /path/to/modified-buffer.log /path/to/some-buffer.q0123456789abcdef.log で上書きしてから fluentd を起動するとよさそう。

ここまでの流れ

学生や新卒の人と話したりするときに、これまでエンジニア的にどうやって今の状態になったかみたいな話を何度もする機会があって、 その度に色々思い出しながら話してたんだけど、自分用に整理したかったのでついでにオンラインでアクセスできる場所に置くことにする。 なにか新たに思い出したり思い出補正が発覚したりしたら適宜修正していく。

小学生くらい (- 2002)

学校に自由に使える PC があって、卒業するくらいの時期には古いやつと新しいやつの2つがあって、新しいやつのほうに入っていたタイピングゲームで主に友人の K 君と遊んでいた記憶がある。 もう全然覚えてないけど、時期的には古いほうが Windows 95 で新しいほうが Windows 98 だろうか。 自宅にも PC があって、麻雀ソリティアで遊んでいた記憶がある。 PC に初めて触れたのはこのくらい。タイピングゲームに熱中したおかげで、タッチタイピングはこのへんで習得していた *1

中学生くらい (2002 - 2005)

自宅にインターネット回線や Windows 98 のラップトップがきた。もちろんラップトップは家族共用。 このへんでインターネットにはまり始めて、ゲームの攻略サイトの掲示板とかによく書き込んでた。 あと K 君が自分でちょっとしたゲームを作っていて、僕も気になったので HSP で何か書いてみていた。 プログラミングというものを初めてやったのがこれで、なんとなく楽しんでいた気がする。 ただ、そんなに長続きはしなかったと思う。 HSP については今は文法すら覚えてない。

高校生くらい (2005 - 2008)

高校進学と同時に自分用の Windows XP のラップトップを買ってもらい、インターネットにがっつりはまった。 というかだいたい 2ch を見ていた。 2ch にいるような人だとプログラマの割合が高めで、そういう人たちと話しているうちに、HSP の経験や PC というのに興味を持ち始めていたこともあって、何か作りたいものがあったわけじゃないけど C 言語に入門した。 猫でもわかるC言語プログラミング のサイトやそこの本を主に読んでた。 最初は Borland C++ Compiler を使っていたけど、cmd.exe にも多少慣れてきてから cygwin をメインで使うようになって gccコンパイルしていた。 エディタも TeraPad とか Notepad++ をメインで使いつつも、cygwin の中で Vim を使い始めていた。 cygwin もそうだけど、EmacsVim を使ったほうがプロっぽいみたいな風潮があって (?)、最初は Emacs にチャレンジしたんだけど小指を痛めて挫折し、一方 Vim のほうはそういうことがなかったので当時は謎の操作に戸惑いながら無理して Vim を使っていた。

C/C++ の初心者本に載ってるようなことはなんとなく分かるようになって、その後 C に構文が似ているらしいという理由で Perl も勉強し始めた。 CGI とかも書いてみてはいたけど、LWP でインターネットから情報を簡単にとってこれることに感動して、今でいうとスクレイピングみたいなことをよくやっていた。 HTML を DOM としてパースするみたいな発想は全くなくて (というかそもそも HTML/XML というフォーマットや DOM というものについて知らなかったと思う)、ひたすら正規表現だけでがんばっていた。 おかげで正規表現は覚えた。

このへんでプログラムを書くのがすごく楽しくて好きになっていた。 リアルでプログラムを書く仲間みたいなのはいなかった (K 君は別の高校に進学して疎遠になっていた) けど、2ch のプログラム板とかはよく見ていて、大学生の宿題の丸投げスレの問題に解答したり、コードゴルフというものを知って POJ で勝手にコードゴルフにチャレンジしたりしていた。 この頃 Java も少し書いてみたりしていたけど、あまり興味を持てず勉強はしていなかった *2Win32 API とか Swing とかで GUI を書いてみようとしていた時期もあったけど、めんどくさすぎるわりに大したものができなくて、ターミナルで満足していた。

大学生くらい (2008 - 2014)

進学と同時に MacBook を買って、cygwin から開放されて普通のターミナルの使いやすさに驚いた。cygwin ではビルドできなかったり動かなかったりした CLI ツールも動いてすごく便利に感じた。 その結果、色々なプログラミング言語を試せるようになって、Ruby とか Python とか Haskell とか Lisp とか色々入門していた。 その中でも RubyHaskell は手になじんで、その後もしばらく使い続けて、Haskell は最近はもう全然書く機会がなくなってしまったけど Ruby は仕事で毎日使うようになった。 あと大学の図書館で エキスパートCプログラミング という本に出会って、これが本当に面白くて、C を理解する上で大いに参考になっただけではなく、プログラミング言語そのものやランタイムやツールチェインに興味を持つきっかけの一つになったと思う。 そんな流れでもっとマイナーな言語を使ってみたりしつつ、プログラミング言語への興味が高まってそれ系の研究室に所属することになったりした。

この頃からインターネットの中心 (?) が 2ch から Twitter にシフトしていきつつ、はてなダイアリーに何か書いたり、GitHub に dotfiles や雑コードを上げるようになったり、勉強会的なものに少し参加してみたりするようになった。 リアルのほうでも情報系の学科にいったので、プログラムの話をできる友人ができた。 Twitter がきっかけで B2 の後半くらいから学内の ICPC の練習会にも参加するようにもなった。 技術的な成長は B1 から B3 あたりの時期が際立っていたと思う。 色々な言語の仕様を読んだり処理系の一部を読んでみたり、ちょっと背伸びして公開されている論文を読んでみたり、なにかコードを書くアイデアが出たらあえてマイナーな言語で書いてみたり *3。 まぁ競技プログラミングに関しては全然強くはなれなかったけど……

B3 の頃からデスクトップ環境を Linux にしたり、Linux サーバで PT2 でアニメ録画をし始めたりして、Linux とかサーバ管理について少しずつ学んでいった。 Web アプリケーションに興味を持ち始めたのはたぶん B4 の後半か M1 の前半くらいで、sinatraRails に入門しつつ VPS を借りて nginx とか使い始めた。 GUI アプリケーションを書くより HTML を書くほうが圧倒的に楽だと感じて、ここから徐々に sinatraRails を使うようになっていた。 といっても不特定多数の人が使う (使える) ようなものではなく、ただ自分だけが使うツールの GUI として使っていた。 研究室に所属してからは同じ研究室の同期を始めとして同じフロアの人たちに恵まれて、似たような興味分野の人同士で色々話したり聞いたりできて本当に楽しかった。

現在

社会人になってからだいたい何してたかは書き残しているのでそっちに http://eagletmt.hateblo.jp/entry/2015/12/31/212651Rails とかインフラにちょっと詳しくなったりした。

プログラミングは手段でしかなくてゲームを作りたいとかサービスを作りたいとかそういう目的があってプログラミングを学んだり仕事にしたりする人もいると思うんだけど、僕は全くそんなことはなくてただ単純にコードを書くこと自体が面白くて楽しくて書き続けてきた。 なので宿題丸投げスレや競技プログラミング、そして仕事みたいに課題を与えてくれるものがあると助かっていた。 面接とかで今まで何作りましたかとか今後何作りたいですがみたいなことを聞かれても困ることが多かった。まぁこれは今もわりとそうな気がする。 コードを書くのは今では仕事になっているけど趣味でもあり続けている。

*1:これ以降、タイピング速度は今に至るまで上がってない気がする……

*2:そして今に至るまで真面目に書いたことはない

*3:なおこの頃書いたコードは後でほとんど Ruby で書き直された

RTX 810 と Openswan で site-to-site VPN

自宅から 10.0.0.3 とかで VPS にあるサーバにアクセスしたいし、逆に VPS から 192.168.10.8 とかで自宅のサーバにアクセスしたい。

環境

  • 自宅 (RTX 810)
    • グローバルIP 1.1.1.1
    • プライベートIP 192.168.10.1
    • サブネット 192.168.10.0/24
  • VPS (Openswan)
    • グローバルIP 2.2.2.2
    • プライベートIP 10.0.0.2
    • サブネット 10.0.0.0/16
  • pre-shared-key HOGEFUGA

自宅側の設定

Web UI から VPN 接続の設定→IPsecを使用したネットワーク型 LAN間接続VPN からだいたい設定できる。 最終的に以下のような設定になった。

...
ip route 10.0.0.0/16 gateway tunnel 1
...
tunnel select 1
 tunnel name vps
 ipsec tunnel 1
  ipsec sa policy 1 1 esp aes-cbc sha-hmac
  ipsec ike group 1 modp1024
  ipsec ike hash 1 sha
  ipsec ike keepalive use 1 off
  ipsec ike local address 1 192.168.10.1
  ipsec ike pre-shared-key 1 *
  ipsec ike remote address 1 1.1.1.1
  ipsec auto refresh 1 off
 ip tunnel tcp mss limit auto
 tunnel enable 1
...
ip filter 200085 pass * 192.168.10.1 udp * 500
ip filter 200086 pass * 192.168.10.1 esp * *
...

VPS 側の設定

Openswan

/etc/ipsec.conf

version 2.0

config setup
    dumpdir=/var/run/pluto/
    virtual_private=%v4:10.0.0.0/8,%v4:192.168.0.0/16,%v4:172.16.0.0/12,%v4:25.0.0.0/8,%v6:fd00::/8,%v6:fe80::/10
    oe=off
    protostack=auto

include /etc/ipsec.d/*.conf

/etc/ipsec.secrets

include /etc/ipsec.d/*.secrets

/etc/ipsec.d/rtx810.conf

conn rtx810
    auto=start
    type=tunnel
    authby=secret
    keyexchange=ike
    ike=aes128-sha1;modp1024
    phase2=esp
    phase2alg=aes128-sha1;modp1024
    pfs=no

    left=1.1.1.1
    leftid=1.1.1.1
    leftsourceip=10.0.0.200
    leftsubnet=10.0.0.2/16

    right=2.2.2.2
    rightid=192.168.10.1
    rightsubnet=192.168.10.1/24

/etc/ipsec.d/rtx810.secrets

1.1.1.1 192.168.10.1: PSK "HOGEFUGA"

/etc/sysctl.d/50-openswan.conf

net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.ip_forward = 1

これで ipsec verify に FAIL がなくなって起動するはず。

iptables

esp と udp 500 を開けておく。 あとは他のサーバから Openswan がいるサーバを通って通信させるために forward も許可しておく。

iptables -A INPUT -p udp -m multiport --dports 500 -j ACCEPT
iptables -A INPUT -p esp -j ACCEPT
iptables -A FORWARD -s 10.0.0.0/16 -d 192.168.10.0/24 -j ACCEPT
iptables -A FORWARD -s 192.168.10.0/24 -d 10.0.0.0/16 -j ACCEPT

これで VPN 接続を張れるはず。

route

Openswan を立てているサーバ以外から 192.168.10.0/24 にアクセスするときは 10.0.0.2 を通るようにする。 ip route add 192.168.10.0/24 via 10.0.0.2 dev ens4 とすればいいんだけど、この設定を永続化するために systemd-networkd では network ファイルに書いておく。

/etc/systemd/network/ens4.network

...

[Route]
Gateway=10.0.0.2
Destination=192.168.10.0/24

Docker コンテナ内の net.core.somaxconn を変える

--net=host でコンテナを起動すれば、network namespace が分離されないので net.core.somaxconn の値はホスト側と一致する。

% cat /proc/sys/net/core/somaxconn
1024
% docker run --net=host ubuntu:16.04 cat /proc/sys/net/core/somaxconn
1024

けど普通に docker run すると、ホスト側の値にかかわらず、コンテナ内ではデフォルトの128になる。

% cat /proc/sys/net/core/somaxconn
1024
% docker run ubuntu:16.04 cat /proc/sys/net/core/somaxconn
128

で、コンテナ内でこの値を変えようとしても、デフォルトでは許可されていない。

% docker run -it ubuntu:16.04 bash
root@1704e07731c0:/# cat /proc/sys/net/core/somaxconn
128
root@1704e07731c0:/# echo 512 > /proc/sys/net/core/somaxconn
bash: /proc/sys/net/core/somaxconn: Read-only file system

これを回避する方法はいくつかあって、1つは --privileged で実行する方法。

% docker run --privileged -it ubuntu:16.04 bash
root@41d5397d065b:/# echo 512 > /proc/sys/net/core/somaxconn
root@41d5397d065b:/# cat /proc/sys/net/core/somaxconn
512
root@41d5397d065b:/# exit
% cat /proc/sys/net/core/somaxconn
1024

ホスト側とは別の network namespace で値を変えているだけなので、ホスト側の値には影響は無い。 ただ、これだと不要な権限も色々と渡ってしまうのでできれば避けたい。

そこで、必要なものだけ rw で bind mount して、そこに書き込むという方法がある。

% docker run --volume /proc/sys/net/core/somaxconn:/somaxconn -it ubuntu:16.04 bash
root@9bc22f86b0f3:/# cat /somaxconn
128
root@9bc22f86b0f3:/# cat /proc/sys/net/core/somaxconn
128
root@9bc22f86b0f3:/# echo 512 > /somaxconn
root@9bc22f86b0f3:/# cat /somaxconn
512
root@9bc22f86b0f3:/# cat /proc/sys/net/core/somaxconn
512
root@9bc22f86b0f3:/# exit

なお Docker 1.12.0 からは --sysctl というオプションがつくようなので、試してないけど 1.12.0 以降は docker run --sysctl net.core.somaxconn=512 でよさそう。 https://github.com/docker/docker/pull/19265