macOS では、キルスイッチはその背後にあるパケットフィルターの方針しだいであり、pf はその方針を強制する部分です。方針は同時に二つの相反することをしなければなりません——トンネルが成立するようにクライアントをサーバーへ通すこと、そしてそれ以外が直接外へ出るのを止めることです。公開されているルールセットの多くは後者をこなし、前者を忘れるため、機械は保護を得るどころか接続を失います。機能する版は、端点、リゾルバ、トンネルだけを名指しし、それ以上は何も挙げません。

パケットフィルターが決められること、決められないこと

pf はインターフェース、アドレス、プロトコル番号、ポートを見て、そこで止まります。どのプロセスがソケットを開いたかは決して分からないため、一つのプログラム向けに書いた許可は、同じポートに届けるすべてのプログラムへの許可になります。寛大な UDP の例外がキルスイッチを普通のネットワークに戻すのは、こうしてです。アプリを意識した遮断はクライアントかシステム拡張の仕事であり、このファイルの仕事ではありません。

フィルターは経路も作りません。クライアントが落ちても、既定の経路が死んだトンネルを指したままのことがあり、pf はそれを Wi-Fi への静かな逆戻りではなく、遮断されたパケットに変えます。遮断は意図した結果であり、回避すべき不具合ではありません。

結果を決めるのは、ルールの文面よりも順序であることが多いのです。quick がないと、pf は一致した後も評価を続け、最後に一致したルールが勝つため、下のほうの広い通過が上にある慎重な例外を打ち消します。通過に quick を付ければ最初の一致が確定し、方針の書き手が普通に意図するのはこちらです。

まずインターフェースと端点の事実を読む

無線インターフェースの番号は Mac ごとに同じではなく、トンネルデバイスの番号も起動のたびに変わりえます。存在しないデバイスを名指しするルールは解析時に失敗し、これは騒がしくて捕まえやすい一方、誤った実在デバイスを名指しするルールはきれいに解析され、止めたかった通信を許可してしまいます。

事実はクライアントが接続している間に集めます——トンネルデバイスはそのときだけ存在するからです。既定の経路を持つインターフェース、クライアントが作ったデバイス、そして接続先サーバーのアドレスとポートが必要です。最後の値こそ多くのルールセットが落とすものであり、それを落とすからこそ、コピーしたルールセットは自分のトンネルを遮断します。

route -n get default
ifconfig -l
scutil --nc list

最初のコマンドは現在勝っているインターフェースを名指しし、二番目はカーネルが知るすべてのデバイスを並べ、三番目はシステムが起動できる VPN 設定を示します。その答えを書き留め、次にクライアント自身の設定からサーバーのアドレスとポートを読み取ります。

トンネルの経路を中心にルールセットを書く

下のファイルは既定で拒否し、五つだけを再び通します——ループバック、トンネルデバイス、クライアントからサーバーへの経路、トンネルができる前に使うリゾルバ、そして無線リンクを使い物にする二つのやり取りです。インターフェース名とサーバーのアドレスは自分の機械のものなので、確認した値に置き換えてください。トンネルの番号も自分のものとは一致しません。

set skip on lo0
block all
pass out quick on utun0
# maintain this line when the server address changes
pass out quick on en1 inet proto udp to 203.0.113.10 port 1194
# only the resolver the client uses before the tunnel exists
pass out quick on en1 inet proto udp to 192.0.2.53 port 53
pass out quick on en1 inet proto udp from any port 68 to any port 67
pass out quick on en1 inet proto udp to 224.0.0.251 port 5353

DHCP の行があるのは、Mac がアドレスを持つ前にアドレスを求めるからで、要求は使える送信元アドレスなしにポート 68 からポート 67 へ出ます。マルチキャストの行は、無線リンクを使う価値があるかをシステムに判断させるもので、これを外すと、動いているネットワークでも接続なしと報告する機械になります。端点の行は OpenVPN がそもそもサーバーに届くようにするもので、これがないと block all が外側のハンドシェイクを落とし、トンネルは決して上がりません。リゾルバの行は一般的な DNS 許可より狭く、機械が行うすべての問い合わせではなく、一つのアドレスと一つのポートを名指しします。

その端点とリゾルバの例外が、存在しなければならない二つです——ほかはすべて遮断されたままです。クライアントが文字どおりのアドレスに接続するなら、リゾルバの行を外し、端点の行を残します。サーバーのアドレスが変わるなら、次の接続の前にその行を更新するか、クライアント自身のフックから pf テーブルを埋め、代わりにテーブルに一致させます。

トンネルを落としてから再起動する

読み込む前に解析します——構文エラーは試運転では安く、遠隔の作業では高くつくからです。次にファイルを読み込み、フィルターが切れていたら有効にし、実際に何が効いているかを読み返します。読み返しは儀式ではありません——別のアンカーがあなたの書いていないルールを足すことがあるので、印刷された集合と自分のファイルを比べます。

sudo pfctl -n -v -f /etc/pf.conf
sudo pfctl -f /etc/pf.conf
sudo pfctl -e
sudo pfctl -s rules
sudo pfctl -s Anchors
sudo pfctl -a <anchor> -s rules
sudo pfctl -s info

ルールを読み込んだ状態でトンネルを落とし、ページを読み込んでみます。通常の通信は止まるはずで、機械が眠って目覚めた後も止まったままのはずです——その切り替わりで古い状態が忍び込むからです。再接続して、ルールセットに触れずに通信が戻ることを確かめます。トンネルを落としてもブラウザーが動くなら、どこかのルールが思ったより広く、読み返しがたいていそれを名指しします。

再起動こそ人が飛ばす部分です。macOS は起動時に /etc/pf.conf を読みますが、ファイルを読むこととフィルターを有効にすることは同じではなく、そのため いま動いているルールセットが再起動後に無効になっていることがあります。後で sudo pfctl -s info を実行し、状態の行がフィルターを有効と報告することを確かめてください。

出口と再起動の道を正直に保つ

すべてを遮断するルールセットは、遠隔で管理している機械からあなたを締め出すこともあります。できるときはローカルのセッションから編集し、前のファイルを新しいものの隣に置き、必要になる前に戻し方を知っておきます。下のコマンドはフィルターを無効にし、取っておいた複製を読み込み直します——これは、たったいま切ったトンネル越しではなく、コンソールから実行する順序です。五秒で戻せることは、賢いルールに毎回勝ります。

sudo pfctl -d
sudo pfctl -f /etc/pf.conf.previous
sudo pfctl -s rules

永続化には、pfctl -e を実行し起動時にファイルを読み込む launch daemon が要ります。ほかの特権作業と同じように扱ってください——絶対パス、後から読めるログ、そして物理的に手の届く機械での試しです。黙って失敗するデーモンは、機械を無防備のまま、普通の見た目で残します。

隙間を記録します——まずは自前のトンネルデバイスを作る二つ目の VPN クライアントです。あなたのルールは一つのトンネルインターフェースしか名指ししないので、二つ目は遮断されたままか、後から足した広いルールに拾われます。テザリングとホットスポットのインターフェースはときどきしか現れず、同じ注記に値します。それらの隙間はどれも方針を省く理由にはなりません——それは方針がなお成り立つ条件です。

関連記事