Linux アプリを UID ルールで VPN に固定する方法
UID ルールが見えるのはローカルソケットだけであり、よくある loopback 例外がトンネルを殺す。まずアカウントに経路を与え、それから各手順を絞り込んで検証する。
一つのプログラムを Linux で VPN に固定するのは、主にルーティングの問題である。ファイアウォール規則は、経路が選ばれた後に何が起きるかだけを決める。トンネルがデフォルト経路を握る前に OUTPUT でプログラムの UID を一致させると、規則は保護したい通信そのものを落としてしまう。同じ規則は、プログラムの DNS 問い合わせに答えるリゾルバプロセスも見えない。順序を正せば、特権のない 一つのアカウントが信頼できる分割トンネルの選択子になる。
判断の前にアカウントへ経路を与える
所有者一致が答えるのは 一つの問いだけである。この UID に属するローカルソケットがパケットを作ったか。インターフェースを選ぶわけでも、メインテーブルがすでに物理 NIC から送り出している通信を動かせるわけでもない。だから一致する経路のない UID 規則は、プログラムを転送するのではなく遮断する。
まずアカウントを独自のテーブルに通し、そのうえでファイアウォールに結果を確認させる。これは iproute2 が uidrange を理解するホストで機能する。それを ip rule help が 一行で報告する。テーブル 100 はトンネル経由のデフォルト経路を持ち、規則は UID 1000 だけをそのテーブルへ送る。それ以外はすべてメインテーブルに残り、物理リンクを使い続ける。
ip rule add uidrange 1000-1000 table 100 ip route add default dev tun0 table 100
別のトンネルを必要とする 二つ目のアカウントにはテーブル 101 と独自の規則を与える。二つのプロファイルが同じテーブルでデフォルト経路を共有することはできないからだ。NetworkManager のプロファイルはシステム全体に及ぶため、この部分は手作業のままで、再起動時に誰かが復元しない限り規則は消える。トンネル定義の隣の起動スクリプトにテーブルを書き、再接続でアカウントが誤った経路に残らないようにする。
loopback 例外がトンネルを殺す理由
コピーされた規則の多くは、loopback から出ないパケットをすべて落とす。トンネルは loopback ではないので、その形はトンネルの通信も落とし、結果はファイアウォールの誤りではなく死んだ VPN のように見える。
OUTPUT が走る時点でカーネルはすでに出口インターフェースを選んでいる。だから -o はパケットのヘッダーではなく経路の答えと比較される。所有者一致 が参照されるのは OUTPUT と POSTROUTING に限られ、転送されたパケットには読むべきソケットがない。この組み合わせは、例外が loopback を否定するのではなく、トンネルのインターフェースを名指ししてその一致を否定しなければならないことを意味する。
loopback には依然として DROP の上に独自の ACCEPT が必要である。プログラムの最初の DNS パケットは lo を通ってローカルスタブへ行くからだ。二つの規則をこの順に置けば、アカウントはスタブに届き、次にトンネルに届き、ほかには何も届かない。
iptables -I OUTPUT 1 -o lo -m owner --uid-owner vpnapp -j ACCEPT iptables -A OUTPUT -m owner --uid-owner vpnapp ! -o tun0 -j DROP
IPv6 は別の規則セットであり、v4 だけの組は ip6tables をそのまま残す。AAAA レコードを優先するプログラムは、DROP に一度も届かずに物理リンクから出てしまう。両方の規則を ip6tables に反映するか、トンネルが v6 アドレスを持たないときはホストで IPv6 を無効にする。手で追加した規則も再起動で消えるので、netfilter-persistent save で保存するか、起動ユニットが読み込む nftables テーブルとして同じ論理を組み直す。
DNS の答えは別のプロセスから来る
プログラムから 127.0.0.53 への問い合わせは loopback の ACCEPT を通り、そこで UID の一致が止まる。スタブリゾルバは自身のサービスアカウントから上流へ転送するので、vpnapp に紐づけた規則は 二回目の問い合わせを決して見ない。リゾルバがリンク用に LAN サーバーを選ぶと、答えは物理 NIC から戻り、プログラム自身のパケットはトンネルに残る。目に見える結果は、名前が壊れたまま動くトンネルか、選んでいないサーバーが答える名前である。
名前解決はアプリケーションではなくトンネルのリンク側で直す。トンネルプロファイルに DNS サーバーとルーティングドメインを与え、そのうえで resolvectl query と resolvectl status tun0 で、重要な名前に対してそれを使っていることを確認する。ネットワーク名前空間の中では問題が消える。名前空間のリゾルバはトンネルしか知らないからだ。
アカウント自身から規則を確かめる
UID がどの経路を受け取るかをカーネルに尋ね、そのうえでそのアカウントとして要求を出し、出口アドレスを自分のアカウントのものと比べる。両方が同じアドレスを報告するなら、規則は試した通信以外の何かに一致している。下の最初のコマンドはパケットを送らずに経路検索を示すので、ルーティングの誤りとファイアウォールの誤りを切り分けられる。
ip route get 1.1.1.1 uid 1000 sudo -u vpnapp curl -s https://ifconfig.co
カウンターは規則が実際に発火したかどうかを示す。iptables -vnL OUTPUT を実行し、各規則の横のパケット数を読み、実際の通信の後に DROP の横がゼロなら一致の誤りとみなす。
転送される通信はこの設計の外にある。ブリッジの後ろのコンテナや VM には vpnapp が所有するローカルソケットがないので、所有者一致は決して発火せず、それらのパケットは代わりに FORWARD ポリシーに従う。そのワークロードを覆う必要があるなら、別に試し、ブリッジかコンテナのアドレスに紐づけたポリシーを書く。設計が成り立つのは、テーブル、インターフェース例外、リゾルバ、v6 規則がすべて同じ向きを指すときである。どれか 一つを外せば、アカウントは漏れるか接続を失う。