iptables & netfilter — filter, NAT, hooks
Netfilter hooks trong kernel, tables filter/nat/mangle, chains INPUT/FORWARD/PREROUTING/POSTROUTING, và rule Docker tự thêm — nền cho SNAT/DNAT module sau.
TL;DR: Netfilter là framework hook trong kernel trên path IP; iptables (hoặc nftables) là frontend user-space gắn rule vào hook đó. Rule nằm trong table (filter, nat, mangle…) và chain (PREROUTING, INPUT, FORWARD, OUTPUT, POSTROUTING). Gói tới process host đi INPUT; gói xuyên host (container ↔ WAN, container ↔ container qua IP forward) đi FORWARD. Docker bridge inject chain riêng (DOCKER-USER, DOCKER, …) và thường MASQUERADE trên POSTROUTING — survey rule trước, SNAT/DNAT sâu ở Module 2. Pitfall kinh điển: thứ tự rule + FORWARD policy DROP làm “LAN ảo sống, Internet chết”.
UAT đêm trước go-live: hardening playbook chạy xong, hai container trên docker0 vẫn ping nhau. Sáng ra, curl registry từ container timeout. Junior soi INPUT và UFW — “firewall host chặn”. Bạn hỏi: “traffic container ra ngoài đi FORWARD hay INPUT? Counter chain DOCKER-USER / DOCKER-FORWARD tăng khi thử không?” L2 bridge (bài 03) lành; path IP forward + netfilter mới gãy — policy FORWARD DROP và jump Docker bị đụng, không phải “app registry chậm”.
Bài 01 — Linux network stack đã đặt tên hook trên đường gói; bài 03 cho LAN ảo qua docker0. Bài này gắn tables/chains và rule Docker thật vào đúng hook — bản đồ đọc iptables -L / -t nat -L trước khi đào SNAT/DNAT ở Module 2.
Bài này giải thích cơ chế bên dưới netfilter + survey rule Docker (chưa deep SNAT), fade: bạn tự map path gói vào chain trước khi đối chiếu lời giải.
1. Analogy — Trạm kiểm soát trên hành lang toà nhà
Nhớ analogy bưu cục bài 01: gói đã vào toà (ring → softirq → sk_buff). Bây giờ hành lang có trạm kiểm soát:
- Cửa vào sảnh — quét mọi người vừa bước vào, trước khi biết họ lên tầng nội bộ hay đi xuyên sang toà bên (PREROUTING).
- Quầy lễ tân tầng — chỉ người đến làm việc trong toà này (INPUT).
- Hành lang xuyên toà — khách đi xuyên sang cửa khác, không dừng làm việc tại đây (FORWARD).
- Cửa ra — tem / đổi nhãn trước khi ra đường (POSTROUTING — nơi NAT “đóng gói” địa chỉ ra ngoài hay gặp).
- Lối ra từ phòng nội bộ — nhân viên toà xuất phát từ trong (OUTPUT).
| Đời thường | Netfilter / iptables |
|---|---|
| Khung “có trạm nào trên hành lang” | Netfilter hooks trong kernel |
| Sổ quy tắc từng trạm | iptables/nft rule |
| Loại sổ (cho phép / đổi tem / ghi chú) | Tables: filter / nat / mangle |
| Từng trạm có tên | Chains: PRE / INPUT / FORWARD / OUTPUT / POST |
| “Ai viết rule cho container” | Docker daemon inject chain DOCKER* |
Hook = chỗ đứng trên đường gói. Table = loại quyết định. Chain = hàng rule tại chỗ đó. Container “không ra net” hầu như luôn là FORWARD + nat POSTROUTING, không phải INPUT chặn SSH host.
2. Netfilter vs iptables — ai làm gì?
Netfilter là framework trong kernel: tại các điểm cố định trên path IP, kernel gọi hook — danh sách callback có thể ACCEPT, DROP, QUEUE, hoặc sửa gói (NAT, mark). Gói vẫn là sk_buff trên softirq/stack path bài 01; hook cắm vào path đó.
iptables (và nftables ngày nay) là user-space + module kernel tương ứng: bạn khai báo rule, tool đẩy vào cấu trúc netfilter. Docker Engine mặc định vẫn tạo rule kiểu iptables cho bridge network (có backend nftables trên phiên bản mới — cùng ý: rule filter/NAT cho isolation, publish port, masquerade).
| Netfilter | iptables / nft | |
|---|---|---|
| Tầng | Kernel hooks | Frontend + rule store |
| Bạn “thấy” | Counter, path, drop | iptables -L, nft list |
| Docker đụng | Hook PRE/FORWARD/POST… | Chain DOCKER*, MASQUERADE |
flowchart TB
SKB["sk_buff on IP path"] --> HOOK["Netfilter hook"]
HOOK --> RULES["iptables or nft rules"]
RULES -->|"ACCEPT"| CONT["Continue stack"]
RULES -->|"DROP / REJECT"| DROP["Packet dropped"]
RULES -->|"NAT target"| NAT["Rewrite addr or port"]Vì sao không “firewall process” user-space mỗi gói? Throughput và latency: quyết định nằm trên soft path kernel. User-space chỉ cấu hình. Khi debug: hỏi hook + chain + counter rule, không hỏi PID “firewall.exe”.
NAT khái niệm (SNAT/DNAT/port forward) đã có ở Foundations — NAT và port forwarding. Bài này map chỗ NAT bám trên Linux (PRE/POST + table nat), chưa đào ví dụ SNAT container sâu (→ M2 — container ra Internet SNAT).
3. Tables — filter, nat, mangle (và raw)
Một table nhóm rule theo mục đích. Cùng tên chain có thể xuất hiện ở nhiều table — thứ tự table × hook do kernel định (đừng học thuộc lòng hết; nhớ filter vs nat cho container là đủ lúc đầu).
| Table | Việc chính | Chain hay gặp (IPv4) |
|---|---|---|
| filter | Cho phép / chặn (ACCEPT, DROP, REJECT) | INPUT, FORWARD, OUTPUT |
| nat | Đổi địa chỉ/port (DNAT, SNAT, MASQUERADE, REDIRECT) | PREROUTING, INPUT, OUTPUT, POSTROUTING |
| mangle | Sửa mark/TTL/… (ít “policy firewall”) | Mọi hook chính |
| raw | Connection tracking exceptions (nâng cao) | PREROUTING, OUTPUT |
filter trả lời: “gói này được đi tiếp không?”
nat trả lời: “địa chỉ/port trên header có cần đổi không?” (thường chỉ gói đầu của connection; conntrack nhớ flow).
mangle ít dùng khi mới đọc Docker — biết “có table này để mark” là đủ.
# List filter table (default)
sudo iptables -L -n -v
# List NAT table — Docker MASQUERADE / publish sống ở đây
sudo iptables -t nat -L -n -v
# Optional: mangle
sudo iptables -t mangle -L -n -v
-n = không reverse DNS (nhanh, ổn định lab). -v = counter packet/byte — vàng khi hỏi “rule này có dính traffic không?”.
4. Chains builtin — PRE, INPUT, FORWARD, OUTPUT, POST
4.1 Bản đồ path (nhắc bài 01, gắn chain)
flowchart LR
IN["Packet in"] --> PRE["PREROUTING"]
PRE --> RT{"Local or Forward?"}
RT -->|"local"| INPUT["INPUT"]
RT -->|"forward"| FWD["FORWARD"]
INPUT --> LOCAL["Local process"]
FWD --> POST["POSTROUTING"]
POST --> OUTDEV["Egress device"]
APP["Local app send"] --> OUTPUT["OUTPUT"]
OUTPUT --> POST| Chain | Gói nào | Câu hỏi policy |
|---|---|---|
| PREROUTING | Mới vào stack IP, trước routing decision đầy đủ | DNAT / publish port “đổi đích” sớm |
| INPUT | Sau routing: dành cho host này | SSH, API host, health check node |
| FORWARD | Không terminate trên host — đi ra interface khác | Container ↔ WAN, router, nhiều bridge |
| OUTPUT | Sinh ra từ process trên host | Ít là path container bridge cổ điển |
| POSTROUTING | Sắp ra device | SNAT / MASQUERADE (đổi nguồn ra ngoài) |
Hai câu tách lỗi ngay:
- Gói tới process trong container (sau DNAT) vs gói tới daemon trên host — khác path, khác chain.
- Container ping peer cùng
docker0chủ yếu L2 bridge (bài 03) — có thể không đi FORWARD L3. Container ra Internet gần như chắc FORWARD + POSTROUTING nat.
4.2 Rule matching & target (đủ để đọc)
Trong một chain, rule được duyệt từ trên xuống (first match wins với target kết thúc như ACCEPT/DROP; một số target kiểu LOG không dừng).
| Khái niệm | Ý nghĩa ngắn |
|---|---|
| Match | Điều kiện: iface (-i/-o), IP/port, protocol, state/conntrack… |
| Target / jump | -j ACCEPT, -j DROP, -j MASQUERADE, hoặc jump sang chain khác (-j DOCKER-USER) |
| Policy | Default khi hết rule: ACCEPT hoặc DROP (iptables -P FORWARD DROP) |
| Counter | Cột pkts/bytes với -v — rule “chết” = 0 dù traffic đáng lẽ khớp |
# Policy of built-in chains (filter)
sudo iptables -S | head
# Example lines you may see:
# -P FORWARD DROP
# -A FORWARD -j DOCKER-USER
4.3 Fade — tự map path trước lời giải
Tự viết ra trước khi xem §4.4. Với mỗi kịch bản, ghi table + chain builtin (và nếu đoán được: custom Docker chain) gói chắc chắn đi qua, và chain nào không liên quan:
- SSH vào daemon
sshdtrên host (không container). - Container trên
docker0curl1.1.1.1(ra Internet qua NAT host). - Client ngoài
curlhost:8080trong khi container publish8080→80(chỉ tên hook/chain; chưa cần cú pháp DNAT).
Gợi ý định hướng: hỏi “gói terminate trên host process nào?” và “có đổi IP nguồn/đích không?” — không mở man page target NAT trước khi map xong.
4.4 Lời giải — map kịch bản
| Kịch bản | Path chính | Không phải chỗ “chặn container” đầu tiên |
|---|---|---|
| (1) SSH host | PREROUTING (thường ít rule) → routing local → filter INPUT | FORWARD (gói không forward) |
| (2) Container → Internet | Từ netns container → veth → bridge → IP forward host: filter FORWARD (+ jump DOCKER*) → nat POSTROUTING (MASQUERADE) | filter INPUT (không phải path “vào sshd”) |
| (3) Ngoài → publish port | nat PREROUTING (DNAT đổi đích → IP container) → forward → filter FORWARD / DOCKER* → vào bridge/veth container; response chiều ngược + SNAT/MASQUERADE theo conntrack | Nhầm “chỉ cần mở INPUT 8080” mà quên Docker đã DNAT + FORWARD |
Cơ chế: routing decision sau PREROUTING tách local vs forward. Publish port đổi địa chỉ đích sớm (DNAT) nên gói “tưởng vào host:8080” có thể không còn là local-delivery thuần — vì thế rule UFW trên INPUT dễ bị bypass cảm giác: traffic đã rẽ sang path NAT/forward trước khi bạn tưởng tượng “firewall host cổ điển”. Chi tiết DNAT publish → Module 2 bài 05; ở đây chỉ cần đúng hook.
5. Docker survey — chain và rule tự thêm
Phạm vi bài: đọc và định vị rule Docker bridge. Không viết lại toàn bộ graph NAT; không tắt iptables daemon option nếu chưa hiểu hệ quả (docs Docker: tắt dễ gãy masquerade / isolation).
5.1 filter — FORWARD và họ DOCKER*
Trên host có Docker bridge, bạn thường thấy (tên chain có thể thêm bớt theo version Engine — đọc máy bạn):
| Chain (filter) | Vai trò survey |
|---|---|
| DOCKER-USER | Chỗ bạn được khuyến nghị gắn policy thêm; xử lý trước phần lớn rule Docker khác trên path forward |
| DOCKER-FORWARD / DOCKER / … | Rule Engine: established, port publish, isolation bridge… |
| FORWARD (builtin) | Thường có jump sang DOCKER-USER rồi các chain Docker; policy hay là DROP |
Docker docs: khi Engine bật IP forwarding, nó có thể set default FORWARD policy DROP và chỉ ACCEPT những gì rule Docker (và DOCKER-USER) cho qua — host không tự thành router mở cho mọi iface trừ khi bạn chủ động cho phép.
# Survey filter path (host with Docker)
sudo iptables -L FORWARD -n -v
sudo iptables -L DOCKER-USER -n -v
sudo iptables -L DOCKER -n -v 2>/dev/null || true
# Newer engines may show DOCKER-FORWARD, DOCKER-BRIDGE, ...
sudo iptables -L -n -v | head -80
Vì sao append -A FORWARD ... của bạn “không chạy”?
Nếu đầu chain FORWARD đã -j DOCKER-USER và gói ACCEPT/DROP xong trong nhánh Docker, rule bạn append cuối FORWARD không bao giờ thấy gói. Đúng chỗ policy user: DOCKER-USER (insert/append trong chain đó), không phải “cuối FORWARD cho chắc”.
5.2 nat — MASQUERADE và chain DOCKER
sudo iptables -t nat -L -n -v
sudo iptables -t nat -L POSTROUTING -n -v
sudo iptables -t nat -L DOCKER -n -v 2>/dev/null || true
| Quan sát điển hình | Ý nghĩa |
|---|---|
MASQUERADE (hoặc SNAT) trên POSTROUTING cho subnet bridge (vd 172.17.0.0/16) | Container ra ngoài: nguồn “giả” thành IP host trên iface egress |
Chain DOCKER trong nat | Publish port / map — survey; thao tác tay dễ lệch daemon |
Không có rule nat + ip_forward=1 | Có forward logic nhưng đổi nguồn thiếu → WAN lạ/reply không về |
Deep cơ chế SNAT/MASQUERADE từng bước + lab → 04 — Container ra Internet (SNAT). Deep publish DNAT → 05 — Publish port (DNAT).
5.3 Liên hệ bridge — khi nào cần netfilter?
| Traffic | Bridge L2 (bài 03) | Netfilter (bài này) |
|---|---|---|
Hai container cùng docker0, cùng subnet | Path chính: veth → FDB → veth | Thường không cần NAT; filter forward có thể vẫn đụng tuỳ isolation rule |
| Container → Internet | Tới gateway IP trên docker0 rồi rời L2 domain | FORWARD + POSTROUTING MASQUERADE + ip_forward |
| Ngoài → port publish | Vào host iface | PREROUTING DNAT + FORWARD vào bridge/veth |
flowchart TB
C["Container"] -->|"veth"| BR["docker0 bridge L2"]
BR -->|"same subnet peer"| C2["Other container"]
BR -->|"IP to gateway host"| L3["Host IP forward"]
L3 --> FW["filter FORWARD + DOCKER*"]
FW --> POST["nat POSTROUTING MASQUERADE"]
POST --> WAN["Outside"]6. Pitfall tổng hợp
❌ Nhầm 1: “Chặn/cho phép container bằng filter INPUT.”
✅ Path container bridge xuyên host là FORWARD (sau khi rời netns). INPUT là gói local host. Hardening chỉ soi INPUT → mù half path container.
❌ Nhầm 2: “Thêm rule -A FORWARD cuối chain là đủ kiểm soát Docker.”
✅ Docker jump sớm sang DOCKER-USER / chain riêng. Gói ACCEPT/DROP trong đó không rơi xuống rule append cuối. Dùng DOCKER-USER cho policy thêm (và nhớ: sau DNAT, match theo IP nội bộ container trừ khi dùng conntrack original dest — nâng cao).
❌ Nhầm 3: “FORWARD policy DROP = Docker hỏng hoàn toàn.”
✅ DROP default là bình thường khi Docker quản forward: rule ACCEPT có chọn lọc phải tồn tại. Hỏng khi mất jump/rule Docker (flush tay, firewall khác ghi đè) hoặc DOCKER-USER DROP nhầm — không phải “thấy DROP là gỡ Docker”.
❌ Nhầm 4: “Ping cùng bridge OK ⇒ netfilter ổn.”
✅ Ping L2/L3 nội bộ không chứng minh MASQUERADE/FORWARD egress. Checklist tách: L2/FDB (bài 03) → ip_forward → iptables -t nat -L -v + FORWARD -v.
❌ Nhầm 5: “iptables: false cho sạch rule host.”
✅ Docs Docker: tắt tạo rule dễ gãy masquerade/isolation/publish. Muốn siết policy: DOCKER-USER, không “tắt netfilter Docker” nếu chưa có thay thế đầy đủ.
Ba bước trước khi đổ lỗi registry/DNS:
- Đúng chain: egress container →
FORWARD/DOCKER-USER, không chỉINPUT. - Counter:
iptables -L -n -v— rule/policy nào tăng khi reproduce? - NAT còn không:
-t nat -L POSTROUTING -n -vcòn MASQUERADE (hoặc SNAT) cho subnet bridge?
Policy FORWARD DROP có chủ đích vẫn OK nếu rule Docker ACCEPT đúng path. Flush nhầm / rule user che jump mới là outage UAT đầu bài.
📚 Deep Dive — man page & docs
Spec / reference:
- Netfilter documentation — kiến trúc hook, HOWTO packet filtering / NAT.
iptables(8)— table, chain, target,-L/-S/-v.- Docker — packet filtering and firewalls — IP forward, FORWARD drop, tương tác UFW/firewalld (lưu ý bypass cảm giác).
- Docker with iptables — chain
DOCKER-USER,DOCKER-FORWARD,DOCKER, nat masquerade/port-map; đừng sửa lung tung rule Engine tạo. - Docker bridge driver —
docker0user-facing (ghép bài 03).
Ghi chú version: Engine mới có thể dùng nftables backend — survey bằng nft list ruleset cùng mental model table/chain/hook. Bài lab lệnh dùng iptables vì vẫn phổ biến trên host Docker hiện tại.
Liên hệ các bài khác
- 00 — Tổng quan module — mảnh 4: kiểm soát gói sau stack + bridge.
- 01 — Linux network stack — softirq/
sk_buff/vị trí hook; bài này đặt rule lên hook. - 02 — Virtual network devices — veth đưa gói netns ↔ host trước khi filter/NAT.
- 03 — Linux bridge — L2
docker0; bài này = L3 forward + netfilter khi rời LAN ảo. - Foundations — NAT và port forwarding — khái niệm SNAT/DNAT; đây là chỗ chúng bám trên Linux/Docker.
- M2 — Container ra Internet (SNAT) — đào MASQUERADE/SNAT có lab (defer từ survey bài này).
- M2 — Publish port (DNAT) — DNAT publish end-to-end.
Tóm tắt
- Netfilter = hook kernel trên path IP; iptables/nft = frontend khai rule.
- Tables: filter (allow/deny), nat (đổi addr/port), mangle (mark) — container hằng ngày: filter + nat.
- Chains: PREROUTING → routing → INPUT | FORWARD → POSTROUTING; OUTPUT cho gói local origin.
- Docker bridge: jump FORWARD → DOCKER-USER → …; nat
DOCKER+ MASQUERADE POSTROUTING — đọc counter trước khi sửa tay. - Pitfall: nhầm INPUT, append FORWARD sau jump Docker, tưởng policy DROP = bug, tin ping L2 đủ chứng minh egress.
- Fade: tự map 3 kịch bản SSH / container→WAN / publish → đối chiếu bảng §4.4.
- SNAT/DNAT sâu = Module 2; Module 1 chỉ cần bản đồ đúng chỗ.
7. Tự kiểm tra
Q1Netfilter và iptables khác nhau chỗ nào? DROP gói xảy ra ở tầng nào?▸
Q2Ba table filter, nat, mangle mỗi cái trả lời câu hỏi gì trên đường gói?▸
Q3Gói SSH vào sshd trên host đi chain filter nào? Gói từ container bridge ra Internet đi chain nào — vì sao không phải cùng một chỗ?▸
Q4PREROUTING và POSTROUTING khác nhau về 'sớm/muộn' thế nào so với quyết định routing? DNAT publish và MASQUERADE gắn gần chain nào?▸
Q5DOCKER-USER dùng để làm gì? Vì sao rule iptables -A FORWARD ... của bạn có thể 'không bao giờ khớp' dù policy FORWARD là DROP?▸
DOCKER-USER là chain Docker để user gắn policy thêm, thường được jump tới trước phần lớn rule Docker khác trên path forward. Rule append cuối builtin FORWARD nằm sau jump: nếu gói đã ACCEPT/DROP trong nhánh Docker/DOCKER-USER, rule cuối không thấy gói — kể cả khi default policy là DROP.Q6Hai container cùng docker0 ping nhau OK nhưng không ra Internet. Bridge/FDB lành. Bạn survey netfilter theo thứ tự nào?▸
ip_forward, iptables -L FORWARD -n -v (jump Docker, counter), rồi iptables -t nat -L POSTROUTING -n -v tìm MASQUERADE/SNAT subnet bridge. Đừng bắt đầu bằng filter INPUT của host.Q7Fade: client ngoài truy cập host:8080 publish vào container. Bạn map nhầm chỉ 'mở INPUT 8080 là đủ'. Thiếu hook/table nào trong mental model?▸
Bài tiếp theo: Tổng kết module — Linux network internals
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên