1つのポートだけVPNの外に出すにはポリシールーティングが必要
NATルールはアドレスを書き換えるだけで、出口のリンクは選びません。TCPポートを1つだけトンネルの外に保つルール、テーブル、マークを解説します。
この依頼はいつも同じ文の言い換えです。ブラウジングにはVPNを使い続けるが、SSHやバックアップ処理だけは通常の接続を使わせたい、というものです。この件でよく流れる助言、すなわち iptables の NAT ルールでポートを転送する方法は、その役目を果たしません。NAT ルールはフロー内部のアドレスとポートを書き換えるだけで、パケットがどのインターフェースから出るかは決して選びません。その判断はルーティング検索のものであり、ルーティング検索がポートを見るには助けが要ります。
単純なルートがポートと一致できない理由
Linux は、ポリシールールで候補テーブルを絞り込んだうえで、宛先アドレスをテーブルで引いてルートを選びます。そのルールは、ファイアウォールマーク、送信元アドレス、受信インターフェース、UID に一致させられます。ルートエントリ自体にポートのフィールドはありません。トランスポートヘッダが意味を持つ前にルートが選ばれるからです。
現在の iproute2 とカーネルはルールの選択子を広げています。ipproto、sport、dport が受け付けられるため、ip rule add ipproto tcp dport 22 table 100 は意図を一行で表します。実際のホストで ip rule add help を実行し、あなたのビルドが dport を列挙するか確認してください。列挙されればコマンド一つで完了です。されなければ、以下のマーク経由の手順が同等です。
いずれにせよ、ルールはポインタにすぎません。テーブル100には依然としてルートが必要で、そこが忘れられる部分です。
三つの部品か、一つか
現代的な形は、上の単一ルールと代替テーブル内のルートです。古典的な形は間に netfilter を挟みます。パケットにマークを付け、そのマークをルールで一致させます。どちらも同じ検索に行き着きます。違いはポートをどこで読むかです。
マーク方式があるのは、netfilter がトランスポートヘッダを見るのに対し、ルーティングのコードは歴史的に見ていなかったからです。仕組みはこれだけであり、代替手段が賢いコマンド一つではなく協調する三つの部品である理由もここにあります。
三つの部品を順に
まず、迂回させたいフローにマークを付けます。ルート決定より前にマークが存在するよう、十分早く動くチェーンで行います。
nft add table inet vpnexempt
nft 'add chain inet vpnexempt out { type route hook output priority mangle; }'
nft add rule inet vpnexempt out tcp dport 22 meta mark set 0x1次に、マークされたパケットを別のテーブルへ送ります。
ip rule add fwmark 0x1/0x1 table 100三つ目に、そのテーブルに物理リンク経由のデフォルトルートを与えます。アドレスは例です。自分のものに置き換えてください。
ip route add default via 192.168.1.1 dev wlan0 table 100三つ目の手順を飛ばすと、マークされたパケットはテーブル100でルートを見つけられず失敗します。症状はルールがネットワークを壊したように見えますが、実際にはパケットに残された唯一の経路を消していたのです。
最初のパケットの後も動かし続けるもの
マークは送信パケットに適用されます。応答は、そのリンクが割り当てたアドレスで物理インターフェースに届きます。メインテーブルはその宛先について依然トンネルを優先するかもしれません。応答経路を一貫させる仕組みは二つあります。リンクアドレスに対する送信元ベースのルールと、フローの寿命の間ずっと判断を保つコネクション追跡です。マークを conntrack に保存し、応答時に復元するのが、状態を持つ例外を安定させる通常の方法です。
逆方向パスフィルタはもう一つの静かな破壊者です。カーネルが戻り経路を厳格に検証し、応答がメインテーブルの選ばないインターフェースに届くと、パケットは何のメッセージもなく破棄されます。rp_filter の既定値はディストリビューションや、インターフェースが転送されるかによって異なります。無効だと決めつけず、実際の値を読んでください。
トンネル自身のトランスポートにマークを付けてはいけません。WireGuard や UDP OpenVPN のトンネルは TCP 22 ではないので上のレシピは安全ですが、トンネルの宛先以外すべて、と書いた例外は暗号化されたパケットを捕まえ、ルーティングループに見える形でトンネルを壊し得ます。
決めつけずに確認する
宛先とポートについて、カーネルがどの経路を取るか尋ね、そのうえで実際の転送を実行します。
ip route get 203.0.113.10
ip rule show
nft list rulesetマークがあるのにカウンタがまったく動かないなら、ルールは一致していません。カウンタは、書いた設定とカーネルが適用しているポリシーの差です。
これが間違った道具である場合
ポート単位の例外は本質的に脆いものです。サービスはポートを移し、HTTP/3 は UDP 443 として届き、一つのアプリケーションが種類の異なる複数のフローを開くこともあります。本当の要件がこのプログラムに通常リンクを使わせることなら、ルーティング層は適切な場所ではありません。ネットワーク名前空間やアプリケーションごとのプロキシのほうが向いています。
要件がこのサブネットに通常リンクを使わせることなら、マークは完全に省き、より具体的なルートを追加してください。より単純で、一つのテーブルで見え、次の人がルールセットを読まずに監査できます。
ファイアウォールに触る前に、三つの要件のどれが実際に必要かを書き出してください。上の仕組みは精密であり、誤った層に適用された精密さは、ただ停止が長くなるだけです。