ブラウザーをトンネル経由にしたいという依頼が、宛先ポート80と443に一致するファイアウォールルールとして返ってくる。正確に見えて、正確ではない。ポート一致が選ぶのはパケットのトランスポートヘッダーであり、どのプログラムがソケットを開いたかではない。現代の閲覧はTCPのこの二つのポートに限定されない。そのルールは含めたいトラフィックの一部を取りこぼし、触れるつもりのなかったトラフィックを捕まえる。書く前に、三つの異なる境界のうち本当にどれが欲しいのかを決めるべきだ。

そのルールが表すのはトランスポートの種別であり、アプリケーションではない

Linuxでは、宛先ポート一致はTCPヘッダーだけを見て、その上は見ない。そのパケットに印を付けてポリシールートで誘導する仕組みは文書どおりに動くが、この仕組みはペイロードの意味も、どのプロセスが送ったかも知らない。印はパケットに付いた番号であって、アプリケーションに付いた身元ではない。

nft add rule inet mangle output tcp dport { 80, 443 } meta mark set 0x1
ip rule add fwmark 0x1 table 100
ip route add default dev wg0 table 100

すぐに二つの帰結が続く。TCP 443に接続するソフトウェア更新ツールはブラウザーと同じ扱いを受け、それが問題ないこともあれば、次の更新サイクルを待つ障害になることもある。また、別のポート、たとえば8443で意図的に公開したWebサービスは、利用者が気にしていたWebトラフィックでありながらルールの完全な外側に落ちる。

要件が本当にそのポートの話であるなら、番号は正当な選択子だ。チケットに「ブラウザー」と書かれ、ルールセットにポート範囲が実装された瞬間に、それは作り話になる。このずれは文書化の細部ではない。レビューで擁護できるルールと、誰も試験できないルールの違いである。

HTTP/3はあなたのTCPルールを静かにすり抜ける

HTTP/3はQUIC上で動き、QUICはUDPで運ばれる。TCPにだけ一致するルールはそれを見られないので、試験ページを満たした同じブラウザーが、誰も設定を変えないまま、後続の要求をUDPで送ってトンネルを迂回できる。RFC 9114はHTTP/3をQUICベースのプロトコルと記述しており、QUICは一般的な配備では既定ポート443のUDPを使う。

あるブラウザーがHTTP/3を試すかどうかは、その設定、サイト、そして以前の試行が成功したかによる。だから記憶で推論するには向かない。代わりに道具に尋ねるべきだ:

curl --http3 -sS -o /dev/null -w '%{http_version} %{time_total}\n' https://example.com
ss -tunp | grep -E ':443\b'

curlの行は交渉されたバージョンを出力するので、3が出ればUDP経路が使われたと確認できる。次にソケット一覧で、その接続がトンネルのインターフェースに束ねられているのか物理側なのかが分かる。あなたのcurlビルドにHTTP/3がなければ、そのフラグは黙って退避せず大きな音で失敗する。これはこれ自体の小さな慈悲だ。

DNSは自分で決めるべき別の判断である

Webトラフィック用のポートルールは名前解決について何も言わない。ブラウザーはシステムのリゾルバー、ブラウザー内で設定されたリゾルバー、あるいは最後に接続したものが押し込んだリゾルバーを使うことがある。どれもルールが選ぶ経路とは別の経路で問い合わせを送りうるので、古典的な分裂が起きる。ページは読み込まれ、名前は想定外の場所で解決され、社内サイトは公開アドレスに解決されるか、まったく解決されない。

これを明示的に決める。社内名をトンネル経由で解決する必要があるなら、リゾルバーはその内側に置き、ルールセットはそのリゾルバーへのDNSを許可しつつ他の問い合わせを外に保つ必要がある。ブラウザーが公開事業者へのDNS over HTTPSを使うなら、問い合わせはあなたのリゾルバーを完全に迂回し、ポートルールは役に立たない。選択はルーティングルールと同じ変更記録に書く。次の障害を調べる人が両方を並べて読むからだ。

そしてページではなくリゾルバーで確認する:

resolvectl query intranet.example.internal
resolvectl status | grep -A3 'Current DNS Server'

答えが返らないことは、ブラウザーのエラーがネットワークのせいにしていても名前解決の問題だ。この二つの障害は利用者には同じに見え、まったく別のチケットである。

三つの境界、三つの異なる仕組み

欲しい境界に名前を付けた瞬間に、混乱の大半は消える。これらは互換ではなく、トンネル断のときの故障の仕方もそれぞれ違う。

What you actually wantMechanism that fitsWhat it does not cover
These transport ports use this exitFirewall mark plus policy routeApplications, HTTP/3 over UDP, DNS
This browsing session stays separateApplication-aware VPN, or a separate network namespace plus a proxyOther applications in the same session
Only these private sites are reachableDestination-based routes with private DNSPublic sites on the same address family

ブラウザープロファイルを分けるのは、中央の行の罠だ。クッキーと履歴は分かれるが、ホストのルーティングテーブルは何も変わらないので、どちらのプロファイルも同じ経路を通る。隔離にはカーネルが強制するもの、つまり名前空間、コンテナ、あるいは自前のトンネルインターフェースを持つアプリケーションが必要だ。

どの行が当てはまっても、トンネルが落ちたときに何が起きるべきかを書き留める。インターフェースとともに消える印ベースの経路は、通常は物理の既定経路に戻る。これはfail-openの挙動だ。閲覧には許容でき、それ以外には許容できないかもしれず、それは実装の詳細ではなく判断である。

カバーしていると主張するフローを試験する

受け入れ試験は、都合のよかったポート番号ではなく、書き留めた境界に従わなければならない。トンネルを通らなければならないフローを一つ、通ってはならないフローを一つ、考えていなかったトランスポートを使うフローを一つ選ぶ:

ip route get 203.0.113.40
curl -sS -o /dev/null -w 'v4 %{http_version} %{remote_ip}\n' https://example.com
nft list ruleset | grep -A2 'dport'

経路の照会は、宛先が実際にどのインターフェースを使うかを教えてくれる。これがほとんどの報告が間違える事実だ。curlの行は交渉されたバージョンとアドレスファミリーを確認する。ルールセットの出力は、試験中に追加して範囲を絞り忘れたルールを捕まえる。こうして一時的な印が恒久的なfail-openルーティングになる。

三つの出力すべてを時刻付きで記録し、変更と一緒に保管する。後で誰かが「VPNがイントラネットを壊した」と報告したとき、意図した境界と実装した境界の差はその記録に見える。修正は議論ではなくルール変更になる。

関連記事