ポートフォワーディングは設定済みだが、INPUT への許可のみで接続が通らない。まずパケットがルーティングとフックをどのように通過するかを特定し、次にルール、トラッキング、NAT について議論する。XDP/TC は異なる処理レイヤーに位置する。
参照目標 読書位置
パスとチェーンの制御フロー 第 1~2 節
ルールの分離実験と conntrack 第 3~4 節
NAT、BPF、オフロードの観測 第 5~7 節
1. パケットのパスを判断する
ローカル受信、ローカル送信、フォワーディングは異なるフックを経由する
PREROUTING
外部パケットのエントリポイント
ルーティング判断
ローカル / フォワーディング
INPUT
ローカルプロトコルスタックへ
FORWARD
転送パケット
POSTROUTING
出口処理
OUTPUT
ローカル生成パケット
OUTPUT の前には通常、初期ルーティングが既に存在する。ルールでアドレスや mark を変更すると、再ルーティングが必要になる可能性がある。
ローカルサービス宛ての外部パケットは、通常、PREROUTING、INPUT を経る。ローカルプロセスから送信されるパケットは、OUTPUT、POSTROUTING を経る。ルーティングによる転送パケットは、PREROUTING、FORWARD、POSTROUTING を経る。ブリッジ、netdev ingress、トンネリング、オフロードには他のパスが存在し、すべてのパケットが必ずすべての 5 つのフックを通過するわけではない。
DNAT はルーティングの前に宛先アドレスを変更するため、ゲートウェイアドレス宛てのトラフィックを内部サーバーへの転送に変えることができる。したがって、ポートフォワーディング後のパケットには、INPUT への許可だけでなく、FORWARD ルールが必要になる場合が多い。
同じフック内では、数値の小さい priority から順に実行される。異なる base chain はフックを共有できるが、同じ priority の相対順序はポリシー依存になってはならない。Netfilter hooks を参照。
2. nftables の table、chain、rule、set
オブジェクト 役割
family プロトコル/処理ドメインを指定。例:ip、ip6、inet、bridge、netdev
table chain、set、map などのオブジェクトを整理。固定された実行フェーズではない
base chain フック、priority、policy にバインドされ、実行エントリポイントとなる
regular chain jump/goto などのルールによって入り、フックには直接バインドされない
rule 順序通りに評価されるマッチ式とアクション
set / map 集合クエリまたはキーから値へのマッピングを表現し、重複ルールを削減
nftables は依然としてチェーン内のルールの順序で評価を行う。線形なルールチェックから完全に解放されたわけではない。集合は表現力と特定クエリの効率を改善できるが、「任意の nft ルールセットは一度だけ走査され、iptables より速い」と無条件に主張することはできない。
iptables コマンドは nft バックエンドを使用する場合があり、コマンド名だけでバックエンドが legacy xtables であると判断することはできない。複数のツールで共同管理する場合は、実際のバックエンドと現在の ruleset を確認する必要がある。
accept が常に最終的な通過証ではないわけではない
ある base chain において accept はそのチェーンの処理を終了するが、同じフックの他の base chain や、より後ろのフックでは依然として拒否できる。drop は通常、そのパケットの処理を直ちに終了させる。regular chain の return/jump と base chain の policy は、別の制御フローである。
実行位置 送信元に対するアクション 最終的に通過したか
input base chain、priority 0 SSH を accept 未通過。後のチェーンでも処理される
input base chain、priority 10 送信元を drop パケットは拒否される
2 つのチェーンが結果を共同で決定する。前のチェーンの末尾に accept を繰り返しても、後のチェーンの drop を上書きすることはできない。nftables chain 設定 を参照。
以下に、異なる priority の base chain を 2 つ固定し、それぞれ宛先ポートと送信元条件を変更する。あるチェーンの accept の後でも、後のチェーンは拒否できる。drop は、後のルールが実行される機会を失わせる。
図を準備しています 再読み込み
前のチェーンで accept しても、なぜ後のチェーンで drop できるのか? accept は現在の base chain の処理を終了するが、後のチェーンは依然として拒否できる。drop はこのパスを終了させる。同じフックを持つ複数の base chain は、priority の小さい順に実行される。
{"id":"netfilter-base-chain-verdicts","title":"前のチェーンで accept しても、なぜ後のチェーンで drop できるのか?","summary":"accept は現在の base chain の処理を終了するが、後のチェーンは依然として拒否できる。drop はこのパスを終了させる。同じフックを持つ複数の base chain は、priority の小さい順に実行される。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label for=\"port\" data-i18n=\"port\"></label><select id=\"port\"><option>22</option><option>443</option></select><label class=\"check\"><input type=\"checkbox\" id=\"blocked\" checked><span data-i18n=\"blocked\"></span></label><div class=\"chain\"><header>A · input · priority 0</header><div class=\"rule\" id=\"a1\"><code>tcp dport 22 → accept</code><span></span></div><div class=\"rule\" id=\"a2\"><code>all → drop</code><span></span></div></div><div class=\"chain\"><header>B · input · priority 10</header><div class=\"rule\" id=\"b1\"><code>source ∈ deny → drop</code><span></span></div><div class=\"rule\" id=\"b2\"><code>policy accept</code><span></span></div></div><div class=\"card\"><header data-i18n=\"verdict\"></header><div id=\"verdict\" class=\"readout\"></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.chain{border-left:2px solid var(--accent);padding-left:12px;margin:22px 0}.chain header{font:12px var(--viz-font-ui);margin-bottom:8px}.rule{border:1px solid var(--rule);border-radius:6px;padding:12px;margin:7px 0;min-height:6em;font-size:12px}.rule code{font-size:12px;overflow-wrap:anywhere}.rule span{display:block;color:var(--muted);margin-top:6px}.rule[data-state=skip]{opacity:.5;border-style:dashed}.rule[data-state=accept]{border-color:var(--accent);background:var(--accent-soft)}.rule[data-state=drop]{border-color:var(--second);background:var(--second-soft)}.readout{min-height:3.2em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}function draw(){const ssh=$('#port').value==='22',blocked=$('#blocked').checked,states={a1:ssh?'accept':'miss',a2:ssh?'skip':'drop',b1:!ssh?'skip':blocked?'drop':'miss',b2:!ssh||blocked?'skip':'accept'};for(const[k,s]of Object.entries(states)){$('#'+k).dataset.state=s;$('#'+k+' span').textContent=t(s);}set('verdict',t(ssh&&!blocked?'passed':'denied'));set('explanation',t('note'));}$('#port').onchange=$('#blocked').onchange=draw;draw();","audio":false,"strings":{"accept":"本チェーンを終了","blocked":"送信元が B の拒否集合にヒット","denied":"破棄された","drop":"パスを終了","heading":"前のチェーンで accept しても、なぜ後のチェーンで drop できるのか?","intro":"宛先ポートと後のチェーンの送信元拒否条件を切り替える。実際に通過したルールとスキップしたルールを順に照らし合わせ、accept の作用範囲を観察する。","miss":"マッチせず、継続","note":"A 内の accept は A の後の drop をスキップするが、B はスキップしない。送信元拒否をオン/オフし、同じ accept の後の異なる結果を照らし合わせる。","passed":"このフックを通過","port":"TCP 宛先ポート","reset":"リセット","scope":"2 つの input base chain、priority 0 と 10。同じ priority は使用しない。このフックの制御フローのみを追跡し、最終的に通過してもアプリケーションサービスが利用可能であることは保証されない。regular chain、jump/return、conntrack、および他のフックは省略。","scopeLabel":"モデルの範囲と規約","skip":"実行されなかった","verdict":"このフックの最終結果"}}
3. カウントのみを行う学習用ルールセット
observe.nft として保存する。デフォルトの accept ポリシーを変更せず、3 つの主要なパスからこのチェーンを通過するパケットをそれぞれカウントする。完全なセキュリティポリシーではない。
table inet kb_observe {
chain input {
type filter hook input priority 0; policy accept;
counter
}
chain forward {
type filter hook forward priority 0; policy accept;
counter
}
chain output {
type filter hook output priority 0; policy accept;
counter
}
}
非特権 user/network namespace をサポートするマシンでは、新しいネットワーク namespace を用いて確認およびロードできる。
unshare -- user -- map-root-user -- net sh - eu - c '
nft -c -f observe.nft
nft -f observe.nft
nft list table inet kb_observe
'
-c はトランザクションを適用せずにチェックするが、カーネルの関連するサポートと権限が必要である。ここでは外部インターフェースに接続していないため、namespace はプロセス終了時に回收される。システムで非特権 namespace が無効化されている場合、この例は失敗する。ホストのファイアウォールに直接ロードしてはいけない。実際のトラフィックのキャプチャには、同じ namespace 内でテストインターフェースと負荷を確立する必要がある。
nft のバッチトランザクションは、ユーザー空間でコマンドを逐条実行することによる部分的な更新ウィンドウを回避できるが、conntrack/NAT の状態がルール変更とともに再構築されることを意味するものではない。
4. Conntrack の状態は TCP socket 状態ではない
conntrack はプロトコル、双方向タプル、zone、状態、タイマーなどを記録し、TCP を処理できるだけでなく、UDP などのコネクションレスプロトコルに対してもフロートラッキングを作成できる。ct state established と ss で表示される TCP ESTAB は、同じ状態マシンではない。
NEW、ESTABLISHED、RELATED、INVALID、UNTRACKED はトラッキングの視点で記述される。RELATED には、関連する ICMP エラーや制御された helper によって確立された関係が含まれる可能性がある。notrack は十分に早い位置で実行する必要があり、トラッキングに依存する NAT/ルールの動作を変更する。
エントリの作成、確認、タイムアウト、破棄は、アプリケーションソケットのオープン/クローズとは異なる。テーブルが満杯の場合、新しいフローのレート、タイムアウト、zone、メモリ、異常トラフィックを確認する必要がある。nf_conntrack_max を引き上げるだけで解決すると仮定してはならない。count/max は容量の観測であり、エンドツーエンドの健康指標ではない。
5. NAT はマッピングを確立し、後のパケットは状態を再利用する
典型的なステートフル NAT は、フローの最初のパケットで binding を確立し、後のパケットは conntrack に保存されたマッピングに基づいて変換を行う。各パケットが NAT チェーンを再走査して新しいマッピングを選択するわけではない。NAT ルールを変更した後、既存のフローは古いマッピングを引き続き使用する可能性がある。
方向 アドレスの変更
クライアント発信 10.0.0.8:40000 が 198.51.100.20:443 にアクセス
ゲートウェイソース変換 ソースアドレスが 203.0.113.5:51000 に変更
戻りトラフィック ゲートウェイの 51000 宛てに送信され、逆方向の状態に基づいてクライアントの 40000 に復元
これらのアドレスは推論のためだけに使用される。戻りパケットの復元は既存のマッピングに依存しており、NAT を単方向の文字列置換として理解してはならない。
SNAT は通常、出口でソースマッピングを選択する。MASQUERADE は出口インターフェースのアドレスに基づいて動作し、インターフェースの状態変化に関連する処理を行う。DNAT は通常、PREROUTING/OUTPUT で宛先を変更する。nat INPUT チェーンを「ローカル宛ての DNAT」の汎用的な位置として記述してはならない。ソース変換の用途は別途理解する必要がある。
マッピングの外側:フォワーディングの許可と戻りルーティングは依然として成立する必要がある
NAT は IP forwarding を自動的に有効にせず、戻りルーティングを修復したり、FORWARD を許可したりしない。hairpin アクセスには、戻りトラフィックが同じステートデバイスを経由するように、対応するソース変換が必要になる場合もある。NAT ドキュメント を参照。
6. XDP と TC:処理位置が利用可能な情報を決定する
ネイティブ XDP はドライバの早期受信パスに位置し、少ないプロトコルスタックのオーバーヘッドでパケットの破棄、送信元への返送、またはリダイレクトを行うことができる。ジェネリック XDP はすでに skb パスにあり、ハードウェアオフロードは NIC のサポートに依存する。これら 3 つを固定の「skb 前フック」として混同してはならない。
XDP_PASS は、後のスタック処理を継続することを示す。XDP_TX は、受信デバイスから送信元へ返送することを示す。XDP_REDIRECT は、正しいリダイレクトターゲットと関連する準備に依存する。戻りアクションは、ルーティングの作成、MAC の変更、チェックサム更新、接続トラッキングの維持の完全な代替手段ではない。
TC は skb を処理し、キュー、分類、アドレス変更、リダイレクトなどの機能と組み合わせて適している。direct-action は TC_ACT_* を使用し、XDP の定数を戻り値としてコピーしてはならない。socket filter は、socket への受信データへの制御を委ねるものであり、「socket 作成フック」ではない。
BPF helper は conntrack、ルーティングなどに対して制限付きのインターフェースを持つが、可用性と責任はタイプ/バージョンによって異なり、プログラムがデフォルトで Netfilter の状態とルール全体を継承すると考えることはできない。プログラムのタイプについては eBPF プログラムタイプ を参照。
7. トラフィックのオフロードは観測可能なパスを変更する
Netfilter flowtable とハードウェアオフロードにより、確立されたフローはより短いパスを通過できる。最初のパケットでのポリシー確立と、後のトラフィックの実際の通過位置とカウントは、一緒に分析する必要がある。ある FORWARD カウンタがパケットごとに増加しないからといって、すべてのトラフィックが停止したわけではない。
パケットキャプチャが空の場合:まず観測点を特定し、パケットの到達を判断する
同様に、XDP による早期パケットドロップは、通常の packet socket によるキャプチャポイントよりも前で発生する可能性がある。ローカル tcpdump で見えない場合、パケットが来なかったのか、それとももっと前で処理されたのかは判断できない。調査時には、物理ポートのカウント、XDP/TC の状態、Netfilter trace/counter、socket 状態を組み合わせ、レイヤーごとに範囲を狭めていく。flowtable ドキュメント を参照。