iptables のルールがブリッジ経由の VPN トラフィックを認識しない理由
ブリッジされたフレームは、監視している IP チェーンを迂回することがあります。別のルールを追加する前に、ネイティブの bridge ファミリーと旧来の br_netfilter を比較してください。
同じホスト上の二台の仮想マシンがブリッジ経由で LAN に到達しているのに、ip forward チェーンに置いたルールのカウンターはまったく動かない。フレームは実在し、トラフィックも流れているのに、そのルールが単に経路上にない。Linux がブリッジトラフィックを公開する二つの方法を比べれば、この沈黙の理由が説明でき、旧来のモジュールを一つ読み込むことが見た目より悪い答えである理由も説明できる。
一つのホストを通る二つの経路
パケットがこのホストを離れる方法は大きく二つあり、その違いがどのフックに見えるかを決める。宛先が物理ポートの先にあり、フレームがブリッジの二つのポート間でスイッチされる場合、ホストは Ethernet フレームを転送し、ルーティングコードは呼ばれず、ルーティングされたホップも発生しない。宛先がホスト自身、あるいはトンネルインターフェース経由で到達するネットワークであれば、パケットはルーティングされ、IP 層のフックが実際に適用される。チェーンとファミリーに関する以降の問いはすべて、フレームが二つの経路のどちらを通るかから導かれるので、この分岐を覚えておいてほしい。
その区別こそが、何も数えないルールの背景にある物語のすべてだ。veth9a1 から enp3s0 へのブリッジされたフレームは IP スタックではなくブリッジを横断するので、ip forward チェーンには一致するものが何も見えない。古い例のインターフェース名はどちらの経路なのかを決めないので、最初の一歩はルールセットではなくトポロジーを見ることだ。
トンネルは、名付ける価値のある第三のケースを加える。TUN デバイスは IP パケットをホストに渡すので、それらはルーティングされ、IP チェーンを実際に通過するのに対し、ブリッジに接続された TAP デバイスはフレームを渡し、ブリッジのケースと同じように振る舞う。決めるのは製品名ではなくデバイスの種類だ。
ブリッジされたフレームを見られる二つのファミリーを比較する
Linux は nftables の bridge ファミリーを通じてネイティブなブリッジフィルタリングをサポートしており、飛行中のフレームを検査して破棄するのに旧来のモジュールは必要ない。カーネル自身の ブリッジのドキュメント がその推奨を直接示しており、検索結果にモジュールを読み込めと言われる前に知っておく価値がある。
| Observation point | What it sees | What it costs |
|---|---|---|
nftables bridge family chains | Ethernet frames forwarded between bridge ports, in both directions | Rules must be written in bridge syntax with link-layer matches |
Legacy br_netfilter plus an ip chain | Bridged IPv4 and IPv6 packets, re-exposed through IP-layer hooks | Existing host rules may begin matching traffic they never saw before |
inet chain for routed traffic | Packets the host routes, including traffic arriving from a TUN device | No visibility into purely switched frames |
iptables の代わりに nft コマンドを使っても、それ自体ではルールが正しいファミリーに置かれるわけではない。bridge ファミリーで宣言されたチェーンはフレームを見るが、ip、ip6、inet で宣言されたチェーンは、どのバイナリが作ったとしてもフレームを見ない。ファミリーはチェーンの性質であり、入力したツールの性質ではない。
旧来の経路は動作が異なり、だからこそ習慣でどちらかの側を選ぶのではなく比較すべきだ。br_netfilter を読み込み、bridge-nf-call 設定を有効にすると、ブリッジされた IPv4 と IPv6 のトラフィックが IP 層のフックを通じて再び公開され、通常の iptables ルールでも一致できるようになる。フレームは変わらず、変わるのは通過するフックの集合であり、つまり既存のホストルールすべてに新しい対象が加わったということだ。
ルールを追加する前に実際の経路を読む
三つのコマンドで、ブリッジが何に接続されているか、旧来のモジュールが有効か、bridge ファミリーにどんなチェーンが既にあるかが分かる。答えが二つの設計のどちらを拡張しているかを決めるので、何かを書く前に実行してほしい。この手順を省くと、正しいルールが何も通らないチェーンに入ってしまう。
bridge link show
lsmod | grep br_netfilter
nft list ruleset | grep -A4 "chain forward"
出力はケースをきれいに分ける。ブリッジの下に並ぶポートはどのインターフェースがスイッチされているかを示し、lsmod の結果が空ならブリッジされたフレームに対する IP 層の可視性はまったくなく、bridge ファミリーの forward チェーンが既にあればポリシーの所有者がすでに存在してルールは独自の新しいチェーンではなくそこに属する。
次に、読み返しやすいルールを一つだけ、カウンター付きで判定を変えずに追加し、代表的なフローを一つだけブリッジに通す。カウンターが動けば、フレームがルールに到達した証拠だ。証明できるのはそれだけなので、すべてを受け入れるポリシーでもカウンターは動くという前提で、拒否される経路も同じように確認する。
nft add rule bridge filter forward iifname "veth9a1" oifname "enp3s0" counter accept旧来のモジュールが動作を変える場所
稼働中のブリッジで旧来の経路を有効にするのは、可視性を足すという中立な行為ではない。ブリッジトラフィックに対して到達不能だったルールが到達可能になり、別のインターフェース向けに書かれたホストルールも含まれ、さらに ip forward チェーンの既定の drop ポリシーが以前は素通りしていたフレームに突然適用されうる。そのブリッジ経由で管理しているホストでは、結果はコンソールなしの締め出しだ。これは仮想の障害モードではなく、たった今広げた経路で到達しているマシンでフックの集合を広げたときの普通の結果だ。
より安全な順序は、どのファミリーがポリシーを持つかを決め、そこにルールを書き、sysctl を変える前に独立した復旧経路を確保することだ。二つのファミリーがどちらもルールを持つならどちらが正なのかを文書化してほしいが、重なり合う二つのポリシーは、読むべきファイルが一つもないまま、ある場所では通し別の場所では落とす原因になる。
分離そのものにも完全な設計が要る。一つの送信元を一つの宛先に許可しても一つのフローが動くことしか証明できないが、ふつうの要件は VM が隣接ホストやホスト本体に到達できないことだ。許可側のカウンターは動くことを最初から期待しているのだから、拒否ルールのカウンターのほうが許可ルールのカウンターより重要だ。
最後に、ホスト宛ての入力のケースは分けて考える。ブリッジ自身の IP 宛てのトラフィックは forward ではなく input に届くので、どのファミリーを選んでも forward のルールが見ることはない。三つのケース、二つのファミリー、一つの教訓として、経路がフックを決め、フックがルールの存在自体を決める。