獄に禁する プログラム作成挑戦記その3
初級者がプログラム作成に挑戦する記事の第二段の第三回です。
今回は、前回作成したHTTPプロキシを禁獄するための監獄を作成しました。
対象OS
これまでのプログラムは、汎用性を持たせるために、POSIXという規格の範囲内で作成していました。しかし、今回の監獄は、POSIXの機能だけでは実現できません。実現のためには、LinuxというOSに独自の機能が必要になります。したがって、今回のプログラムはLinuxに依存しています。
Linuxは、ブサイクなペンギンがマスコットのOSであり、C言語で書かれています。一般的には、「アセンブラを使ってでも無理やり動かす実装がなされており、BSD系に比べて行儀が悪いパチモノのUnix」として知られています。
監獄
なぜ、WebプロキシやTorなどを動かすのに監獄が必要なのか?それは心配だからです。これらのプログラムは、外部と通信しています。そのため、乗っ取られるかもしれません。
乗っ取られたときの対策として、被害を最小限に止めるために、プロキシを閉じ込めてしまうことが考えられます。そのための仕組みが監獄です。
今回のプログラムでは、監獄を構築するために、仮想環境と権限制御(capability)という二つの技術を組み合わせました。
仮想環境
本記事でいう仮想環境は、所謂コンテナ型の仮想環境です。コンテナ型の仮想環境は、プロセス(広義のプロセス)の生成時に、共有情報の一部を非共有とすることで、プロセスの環境を分離します。その非共有となる情報を(Linuxでは)名前空間と呼びます。
名前空間
名前空間には、複数の種類があります。例えば、マウントポイントの情報に関するマウント名前空間や、ネットワークスタックに関するネットワーク名前空間があります。
名前空間は、その種類によって、新規に作成されるか(ネットワーク等)、または元の複製として作成されます(マウント等)。作成された名前空間は、プロセス(およびその子孫)に固有の情報として独立して管理されます。
以上の説明から分かるように、名前空間は、プロセスの存在が前提となっています。そのため、プロセスが終了すると、基本的には名前空間は消滅します。例外は、名前付き(ネットワーク)名前空間の場合です。名前付き名前空間は、それに対する参照が形成されている名前空間であり、その参照がなくなるまで存続します。
今回のプログラムでは、マウント、IPC、PIDおよびネットワークの名前空間を用いて監獄を構成しました。
マウント名前空間
マウント名前空間は、監獄の構築に際して間接的に利用されます。「間接的に利用」について順に説明します。
本来の目的は、収監すべきプロセス(つまり乗っ取られる可能性があるプロセス)からファイルを保護することです。
そのために、収監プロセスを特定のディレクトリに閉じ込めることが考えられます。より具体的には、収監プロセスのルートディレクトリを変更することで、そのディレクトリツリー以外へのアクセスを禁止します。
ルートディレクトリを変更するには二つの方法があります。一つは、プロセスが保持するルートディレクトリ情報を書き換えるchrootです。
もう一つは、ルートディレクトリのマウントポイントを挿げ替えるpivot_rootです。
本プログラムでは、pivot_rootを使用しています。ただし、何の準備もなくpivot_rootを実行すると通常環境のルートディレクトリまで変更されてしまいます(酷いことになります)。
これを避けるために、マウント名前空間を利用します。具体的な手順は以下の通りです。
1 まず、新規にプロセスを生成すると共に、その生成パラメータとしてマウント名前空間の分離を指定します。
これにより、プロセスの名前空間内では、マウントポイント情報が、通常環境から切り離された状態となります。
2 次に、分離した名前空間内でpivot_rootを実行します。
上述したようにマウントポイント情報は、新規なプロセスに固有のものなので、そのプロセスのルートディレクトリだけが変更されます。
3 最後に、プロセスにプロキシなど所望のプログラムを実行させます。
以上により、監獄のファイルシステムが構築されます。
PID名前空間、IPC名前空間
これらの名前空間は、通常環境のプロセスを収監プロセスから保護するために利用されます。より詳細には、名前空間を分離することで、他のプロセスに関する情報(PID)や、他のプロセスとの同期に関する情報(IPC)を収監プロセスから隠してしまいます。
これにより、収監プロセスは、通常環境のプロセスに干渉できなくなります。
ネットワーク名前空間
ネットワーク名前空間は、その名の通りネットワークを隔離するために利用されます。ここでの隔離は、内部(ローカルネット)のみに対する隔離です。なぜなら収監プロセスは外部(インターネット)との通信を必要とするので、外部に対しては隔離できません。
隔離を実際に行うのはファイアウォールです。その設定を容易にするために、ネットワーク名前空間を分離しています。
なお、分離されたネットワーク名前空間は、まっさらな初期状態となります。そのため、インターフェイスや経路などの設定が必要です。これらの設定は、後述するNetlinkを用いて行われます。
capability
Linuxのcapabilityは、最上位者(スーパーユーザー)が持つ権限(ルート権限)を分割して、それらを個別に制御する仕組みです。capabilityを使うことで、より簡単にシステムの保護を図ることができます。Torを例にこれを説明します。
Torは、安全性を確保するために、ルート権限ではなく特定のユーザー(ディフォルトではtorというユーザー)の権限で動作するようになっています。ユーザー権限での動作を確実にするために、必ずTor自身が、ユーザーIDの切換えを行います。
そのユーザーIDの切換えは、ルート権限を必要とする処理です。そのため、Torを起動する際には、Torにルート権限を与える必要があります。
つまり、ルート権限を放棄するために、ルート権限が必要になるので、ルート権限を与えなければなりません。
上述したように、capabilityとは、ルート権限を細かく分けて制御(有効化や無効化)することができる仕組みです。
このcapabilityによれば、Torに対して、Torが必要とするユーザID切換権限のみが有効化されたルート権限(正確にはeuid=0)を与えることが可能になります。
これにより、プロセスが乗っ取られたとしても、被害を最小限に食い止めることができます。
(似たような仕組みがAndroidにも導入されていますが、あちらはSELinux(MAC)です。SElinuxは資源の利用権限を制御する仕組みです。それに対し、capabilityはシステムコールの呼び出し権限を制御する仕組みです。)
なお、前回作成したプロキシは、ユーザー権限のみでも作動するのでcapabilityは必要ありません。
苦労したところ
・監獄を畳むときの後始末
一時ファイル
一時ファイルとは、プログラムの実行中に作成されるファイルであって、終了後には不要となるファイルのことです。不要なファイルなので、終了時などに削除されるのが'普通'です。
そのような一時ファイルが、監獄プログラムにも存在しています。例えば、名前付き名前空間の参照がそれです。参照は、名前付き名前空間を通常空間から特定するために用いられるで、通常空間内に作成されます。
この参照も当然ながら監獄の終了時に始末しなければなりません。
問題点
しかし、上述したように監獄は通常環境から分離されています。そのため、監獄内のプロセスからは、通常環境に存する参照を削除できません。
この問題を解決するには、通常環境に残って、収監プロセスの終了時に、後始末を行うプロセスが必要になります。
解決策
色々と試行錯誤した結果、生成するプロセスの数を三つ(cleanup、init、収監)にすることで解決しました。
・Initシステムとの協調
WebプロキシやTorは、システムに常駐して役務を提供します。このようなプログラムは、一般的には、OSのinitシステムによって管理されます。
今回作成した監獄プログラムも常駐プログラムです。つまり、監獄プログラムもinitシステムによって管理されるべきものです。
終了方法
私のOSのinitシステムは、OpenRCです。OpenRCにおけるプロセスの終了方法は、そのプロセスにシグナル(SIGTERM)を送るというものです。
そのシグナル送信処理は、具体的には、以下の二つの処理からなります。
1. 収監されているプロセスのpidを調べる
2. そのpidを引数としてシグナル関数を呼出す
問題点
ここで、PID名前空間が分離されていることから以下の点が問題となります。
1. 収監プロセスのpidは、通常環境のものと監獄環境のものとがあり、少なくとも一方を取得する必要がある。
まず、通常環境におけるpidは、PID名前空間の分離後に作成されるので、調べようがありません…ありません…ありま…
この文を書きながら思いついたのですが、収監プロセスは、親プロセスのプロセスグループに属するので/procを総当たりすれば分かりますね…
と、いうわけです。
面倒なことをしなくても、監獄プログラムの起動時にそのpidをファイルに記録しておき、終了時に、記録されたpidのプロセスと同じpgidを持つプロセスを列挙して、それらのうちで最も若いもの(つまり最後に起動されたもの)にシグナルを送信すれば万事解決です。
はい!これで満足ですか!
まとめ
今回のプログラムでは、名前空間の分離とcapabirilityの設定が作業の中心になると思ってました。
しかし、両者とも洗練されたインターフェイスが提供されており、関数呼び出し一つで簡単にできてしまいました。
Netlink
最も面倒くさかったのはNetlinkです。Netlinkは、カーネルからユーザーに提供されるインターフェイスの一つです。旧来のインターフェイスとは異なり、可変長メッセージを交換するという新しい方式が採用されています(おやおやマイクロカーネルごっこですか?アライグマに笑われますよ)。
Netlinkを使ってできることは、たくさんあります。当然ながら、そのために必要な設定もたくさんになります。
残念なことに、Netlinkの全てを網羅するような解説は、インターネットでは、見つかりませんでした。そのため、断片情報をかき集めてパズルのように組み合わせなければなりませんでした。
しかし、これは好機かもしれません。集めた情報を上手く纏めれば有料記事にできそうだからです。
需要が低いかもしれませんが問題にはなりません。なぜなら、数の不足は価格で補足すればよいからです。
こういう汎用性がない情報にもかかわらず、金を払ってもよいと考えている人は、切実な事情を抱えているはずです。困っている人の足元を見て法外な値段をふっかけることは、商売の基本です。
次回は、これまでのまとめを書きたいと思います。
記事が無駄に長いと途中で飽きて嫌になるので、1000字ぐらいに収めたいと思います。(そうしないと、この記事のように、なにが言いたいのかよく分からないゴミが出来上がります。)
古往今来得ざれば即ち書き得れば即ち飽くは筆の常也。と云うわけで御座います、この浅ましき乞食めに何卒皆々様のご慈悲をお願い致します。