OpenVPN は /dev/net/tun について errno 2 を訴えて停止するが、その一方で lsmod は tun モジュールをあっさり一覧に出す。どちらの事実も同時に成り立つ。モジュールが提供するのはドライバーであり、クライアントが開くパスは、何か別のものが作るデバイスノードだ。両者を一つの対象とみなすから、モジュールを再読み込みするという明白な修理が何も変えないのである。

二つの対象、二つの異なる持ち主

カーネルモジュールとデバイスノードは、それぞれ独立したライフサイクルを持つ独立した対象である。モジュールをロードするとドライバーが登録され、devtmpfs のあるシステムではカーネルが対応するノードの作成を求める。devtmpfs のないシステムでは何も作られず、ドライバーは自分を指すパスもないままそこに居座る。モジュールは同時に、ロード済みで健全でありながら、ファイルシステムからはまったく到達不能でありうる。

だからエラーの文面とモジュール一覧は矛盾しない。クライアントはパスを開き、そのパスは存在せず、カーネルは tun ドライバーに問い合わせる前に ENOENT を返す。モジュールを再読み込みしても欠けたパスは直らず、パスを作っても欠けたドライバーは直らない。

lsmod | grep -w tun
stat -c "%F %t:%T" /dev/net/tun
grep " /dev " /proc/mounts

devtmpfs と明示的な mknod の違い

通常このノードを作る仕組みは二つあり、どちらが責任を持つべきかを知れば、何を直すべきかが分かる。devtmpfs はマウントされたインスタンス内でカーネルにデバイスノードを作らせ、udev がその上に所有者と権限を適用する。明示的な mknod は、どちらも起動時に動かないイメージや名前空間のための手作業の道筋である。

意図ではなく結果を比べる。devtmpfs が /dev にマウントされているのにノードが無いなら、想定しているドライバーではないか、起動後に何かがノードを消している。 /dev が展開済み rootfs の中の普通のディレクトリなら、自動の仕組みは存在せず、イメージを組む者がノードを作らなければならない。

stat -c "%F major=%t minor=%T" /dev/net/tun
# expect: character special file major=a minor=c8

どちらの仕組みも動かない場所では、ノードを手で作るしかなく、順序が重要になる。まず親ディレクトリを作る。 mknod はそれを作り出せないからだ。次にキャラクターデバイス、そして root だけが書けるモード。ノードを作る前にモジュールを再読み込みし、既存のパスを消すのは ls がデバイスではなくディレクトリだと示してからにすること。

sudo modprobe tun
sudo mkdir -p /dev/net
sudo mknod /dev/net/tun c 10 200
sudo chmod 600 /dev/net/tun
ls -l /dev/net/tun

最も速い見分け方は stat が報告する型であり、そのパスにディレクトリがあるのは最小構成のイメージで最もよくある驚きだ。善意の mkdir を Dockerfile に書くとまさにそれが残り、モジュールをいくらロードしてもディレクトリがキャラクターデバイスに変わることはない。Major 10 と minor 200 が tun の登録された組であり、十六進では a と c8 と表される。

四つの痕跡、四つの原因

四つの原因でほぼすべての事例を説明でき、それぞれがログに異なる痕跡を残す。まず起動環境から見る。ディスクレスのイメージ、組み込みボード、initramfs の段階では、devtmpfs も udev 管理の /devもマウントされないことが多く、どのプロセスもノードを書かない。次に /devを載せているもののマウントオプションを読む。 nodev はパスの存在を許したまま開くことを拒むからだ。堅牢化されたホストから取った bind マウントは、その旨を名乗らずにこのフラグを引き継ぐ。読み取り専用のマウントはもっと早く、もっと大きな音で失敗する。 mknod はそこで EROFS を返し、EPERM はファイルシステムが身を守っているのではなく、プロセスに権能が足りないことを意味する。最後に、独自の /dev tmpfs を持つコンテナランタイムでは、ノードをその名前空間に注入する必要があり、デバイス cgroup が major と minor の組を許可していなければならない。

権限をまた変える前に、ポリシーの層を確認する。SELinux や AppArmor は、ファイル属性がすべて許していても開くことを拒否でき、その拒否はクライアントの出力ではなく監査ログに現れる。拒否に一度も現れない権限変更は、まったく違う層に費やした時間である。

sudo journalctl -k -n 20 --no-pager
sudo ausearch -m AVC -ts recent | tail -5

一覧ではなくインターフェースで証明する

一覧が成功しても、パスが存在することしか分からない 。健全な tun ノードを読み取りで開くと、呼び出しはエラーで返る。デバイスに読み取りの意味論がなく、通常のファイルでもないからだ。このエラーこそがここでの成功の合図であり、そうと分かれば故障として追いかけずに済む。

head -c 1 /dev/net/tun
head: error reading '/dev/net/tun': File descriptor in bad state

決定的な試験はインターフェースを作る。ドライバー、ノード、そしてあなたの権限を一度に試すからだ。追加が成功してリンクが現れれば、三つとも動いている。失敗したときは errno が原因を分ける。ENOENT は使えるノードが無いこと、EPERM は権能・マウントフラグ・ポリシーのいずれか、EBUSY は別のプロセスがすでにインターフェースを持っていることを示す。

sudo ip tuntap add dev tun7 mode tun
ip -brief link show tun7
sudo ip tuntap del dev tun7 mode tun

その試験は、実際に失敗する文脈で実行する。サービスユニット、コンテナ、定期ジョブは、ログインシェルとは違う権能の組を持つことがある。間違った文脈で通った試験は、失敗する文脈について何も証明しない。

デバイスが問題でない場合

インターフェースの試験が成功してもクライアントが失敗するなら、残る問題はプロファイル、サービス文脈、経路のいずれかであり、デバイスは正常だ。そのときはデバイスノードの編集をやめるべきだ。さらなる変更は、動いているシステムからの距離を広げるだけである。

毎回同じ五つの確認を実行し、新しい確認を発明しない。モジュールが競合するモジュールなしでロードされているか、パスが正しい major と minor を持つキャラクターデバイスか、マウントオプションがきれいか、プロセスの文脈が権能を持っているか、失敗の時刻にポリシーの拒否があるか。五つすべてを生き延びた障害は、メッセージが何と言おうとデバイスの問題ではない。

関連記事