Publish port — DNAT và docker -p
Inbound publish port: docker -p 8080:80 → DNAT PREROUTING + FORWARD, tự port-forward iptables, pitfall bind 127.0.0.1 vs 0.0.0.0.
TL;DR: docker run -p 8080:80 không “mở firewall ma thuật” — nó map gói tới host:8080 thành container_ip:80. Kernel đổi đích bằng DNAT trên nat PREROUTING (sớm, trước routing đầy đủ), rồi gói forward qua bridge/veth vào netns app. Reply nhờ conntrack reverse-NAT. Path: client → host:port → DNAT → FORWARD → bridge → container. Pitfall kinh điển: publish chỉ 127.0.0.1:8080 (demo localhost OK, LAN/WAN chết) hoặc có DNAT mà FORWARD DROP (counter nat tăng, gói vẫn không vào container).
Staging smoke: junior docker run -d -p 8080:80 my-api, curl localhost:8080 trên máy dev xanh. PM cầm laptop cùng Wi‑Fi, gõ http://<IP-LAN-máy-dev>:8080 — timeout. Junior soi ufw status và bảo “firewall chặn”. Bạn hỏi: “docker port / ss -lntp | grep 8080 listen địa chỉ nào?” · “iptables -t nat -L -n -v rule DNAT match 127.0.0.1 hay 0.0.0.0?” · “gói từ laptop đi PREROUTING + FORWARD, không phải INPUT tới process host.” App trong container sống; publish bind loopback hoặc thiếu path forward — không phải “API chưa start”.
Khái niệm DNAT/port forward đã có ở Foundations — NAT; hook/chain ở M1 — iptables. Bài 04 SNAT là chiều ra (đổi nguồn POSTROUTING). Bài này trace + implement chiều vào — đúng mapping docker -p.
Bài này faded: lab worked dựng DNAT PREROUTING + FORWARD; bạn tự điền rule khi chỉ có DNAT mà FORWARD DROP, và tự chọn bind 127.0.0.1 vs 0.0.0.0 trước khi đối chiếu.
1. Analogy — Chuông ngoài toà → phòng cụ thể
Nhớ netns = căn hộ, bridge = switch hành lang, SNAT = tem số toà khi ra đường. Publish port là chiều khách gõ cửa từ ngoài:
- Khách chỉ biết số toà + nhánh chuông (
host:8080) — không biết số phòng nội bộ (10.10.0.10:80). - Lễ tân đổi đích trên sổ: “nhánh 8080 → phòng 10, cổng trong 80” — đúng DNAT.
- Đổi sổ lúc khách vừa bước vào sảnh — PREROUTING (sớm), không đợi xong mới đoán “có phải thư cho host process không”.
- Bảo vệ đi kèm: cho phép đi xuyên hành lang tới phòng (FORWARD), không chỉ “mở quầy lễ tân” (INPUT).
| Đời thường | Linux / Docker publish |
|---|---|
| Số toà + nhánh chuông | host_ip:8080 (hoặc 0.0.0.0:8080) |
| Số phòng + cổng trong | container_ip:80 |
| Đổi đích trên sổ lúc vào sảnh | DNAT nat PREROUTING |
| Cho đi xuyên hành lang | filter FORWARD ACCEPT path |
| Sổ cuộc gọi hai chiều | conntrack reverse map |
| Chỉ mở chuông trong căn (localhost) | Bind publish 127.0.0.1 |
| Chuông mặt tiền / mọi cổng | Bind 0.0.0.0 (mặc định -p 8080:80) |
Ra đổi nguồn (SNAT/POSTROUTING). Vào đổi đích (DNAT/PREROUTING).
docker -p = khai nhánh chuông ngoài → phòng container — không phải “app tự listen trên host”.
2. Đường gói inbound: client → host:port → DNAT → container
2.1 Topology giả định (tiếp SNAT / mini-challenge)
| Object | Vai trò | Ví dụ |
|---|---|---|
Netns c1 | “Container” chạy app | 10.10.0.10/24, listen :80 |
c1-eth0 / veth-c1 | Veth pair | peer host master br0 |
br0 | Bridge + gateway | 10.10.0.1/24 |
| Host publish | Cổng “mặt tiền” | TCP 8080 → DNAT 10.10.0.10:80 |
Netns ext (lab) | Client “ngoài” giả lập | 192.0.2.2 → hit host 192.0.2.1:8080 |
flowchart LR
subgraph EXT["Client / netns ext"]
CURL["curl host:8080"]
end
subgraph HOST["Init / host netns"]
IN["iface in"]
PRE["nat PREROUTING DNAT"]
FW["filter FORWARD"]
BR["br0"]
V["veth-c1"]
end
subgraph C1["Netns c1"]
ETH["c1-eth0 10.10.0.10"]
APP["app :80"]
end
CURL --> IN
IN --> PRE
PRE -->|"dst becomes 10.10.0.10:80"| FW
FW --> BR
BR --> V
V --> ETH
ETH --> APP2.2 Từng bước L3 (request)
- Client gửi TCP tới
host_publish_ip:8080(trên lab:192.0.2.1:8080). - nat PREROUTING: rule DNAT đổi destination thành
10.10.0.10:80. Conntrack ghi mapping. - Routing host: đích giờ là IP trên bridge domain → không local-delivery thuần cho process host trên
:8080→ path forward rabr0/ veth. - filter FORWARD: policy/rule phải ACCEPT path vào container (và reply
RELATED,ESTABLISHED). DROP ở đây = timeout dù DNAT “đúng trên giấy”. - Bridge + veth: frame vào netns
c1; app accept trên:80(trong netns container, không phảisstrên host — bài 01). - Reply: conntrack un-DNAT (client vẫn thấy src là host:8080, không thấy tự nói chuyện với
10.10.0.10).
Vì sao “mở INPUT 8080” không đủ?
Sau DNAT, gói không còn là “traffic tới daemon host listen 8080” theo mental model firewall cổ điển. Path chính là PREROUTING (nat) → FORWARD (filter) → bridge. UFW/rule chỉ soi INPUT dễ mù publish Docker — đã map ở M1 iptables §4.4.
2.3 SNAT vs DNAT — một bảng chốt
| SNAT / MASQUERADE (bài 04) | DNAT (bài này) | |
|---|---|---|
| Đổi field | Source | Destination |
| Chiều điển hình | Container / host ra ngoài | Client vào service nội bộ |
| Chain nat cổ điển | POSTROUTING | PREROUTING |
| CLI Docker gắn | Egress bridge (Engine MASQUERADE) | docker run -p / publish |
| Câu hỏi debug | “Container curl WAN?” | “Ngoài hit host:port vào app?” |
3. docker -p 8080:80 map rule gì?
3.1 Cú pháp publish (đủ dùng hằng ngày)
| CLI | Ý nghĩa |
|---|---|
-p 8080:80 | Host mọi iface (0.0.0.0) cổng 8080 → container cổng 80 |
-p 127.0.0.1:8080:80 | Chỉ loopback host 127.0.0.1:8080 → container :80 |
-p 10.0.0.5:8080:80 | Chỉ bind IP host đó |
-p 80 / -P | Host port ngẫu nhiên / mọi EXPOSE — vẫn cùng cơ chế DNAT/proxy |
Survey trên host có Docker (đọc, đừng xoá mù):
# What did Engine publish?
docker port <container>
ss -lntp | grep -E '8080|docker-proxy' || true
# NAT: DNAT / chain DOCKER thường sống ở đây
sudo iptables -t nat -L PREROUTING -n -v
sudo iptables -t nat -L DOCKER -n -v 2>/dev/null || true
# Filter path vào container
sudo iptables -L FORWARD -n -v
sudo iptables -L DOCKER -n -v 2>/dev/null || true
3.2 Mapping mental model → object Engine
| Object bài / netfilter | Docker bridge điển hình |
|---|---|
DNAT host:8080 → 10.10.0.10:80 | Rule trong chain DOCKER (nat), jump từ PREROUTING |
| FORWARD ACCEPT vào container:80 | Rule filter DOCKER / DOCKER-FORWARD (+ DOCKER-USER trước) |
| App listen trong netns | Process trong container; docker exec / nsenter để ss |
Host-local curl localhost:8080 | Thường docker-proxy userland và/hoặc rule nat bổ sung — không chỉ “một dòng PREROUTING” |
Với client ngoài host, path kernel DNAT PREROUTING + FORWARD là lõi cần hiểu. Với curl trên chính host, Engine historically hay có process docker-proxy listen host port rồi nối vào container — vẫn cùng ý publish, thêm một lớp host-local. Lab dưới tái DNAT + FORWARD bằng tay và dùng netns ext để gói thật sự đi PREROUTING (không phụ thuộc docker-proxy).
3.3 Vì sao phải DNAT sớm (PREROUTING)?
Routing decision sau PREROUTING tách: gói cho local process vs forward. Publish cần client gõ IP host + port host, nhưng app chỉ listen IP container. Đổi đích sớm → routing thấy 10.10.0.10 → forward vào bridge. Nếu chỉ “mở port” trên host mà không DNAT, không có process host accept :8080 (trừ docker-proxy) — hoặc process host khác ăn nhầm port.
4. Worked lab — port forward tay (không Docker)
Chạy trên Linux có netns + iptables + sudo (+ python3 cho HTTP nhỏ). macOS/Docker Desktop không cùng view ip netns.
4.1 Skeleton: container netns + bridge + client “ngoài”
# --- cleanup leftovers ---
sudo ip netns del c1 2>/dev/null || true
sudo ip netns del ext 2>/dev/null || true
sudo ip link del br0 2>/dev/null || true
sudo ip link del veth-ext 2>/dev/null || true
# --- bridge + "container" c1 ---
sudo ip netns add c1
sudo ip link add br0 type bridge
sudo ip link set br0 up
sudo ip addr add 10.10.0.1/24 dev br0
sudo ip link add veth-c1 type veth peer name c1-eth0
sudo ip link set c1-eth0 netns c1
sudo ip link set veth-c1 master br0
sudo ip link set veth-c1 up
sudo ip netns exec c1 ip link set lo up
sudo ip netns exec c1 ip link set c1-eth0 up
sudo ip netns exec c1 ip addr add 10.10.0.10/24 dev c1-eth0
sudo ip netns exec c1 ip route add default via 10.10.0.1
# --- "external" client on another L3 leg (hits PREROUTING on host) ---
sudo ip netns add ext
sudo ip link add veth-ext type veth peer name ext0
sudo ip link set ext0 netns ext
sudo ip link set veth-ext up
sudo ip addr add 192.0.2.1/24 dev veth-ext
sudo ip netns exec ext ip link set lo up
sudo ip netns exec ext ip link set ext0 up
sudo ip netns exec ext ip addr add 192.0.2.2/24 dev ext0
# Sanity: LAN container + gateway
sudo ip netns exec c1 ping -c 1 10.10.0.1
4.2 App listen trong container
# HTTP on :80 inside c1 (background). Bind all addresses in THAT netns.
sudo ip netns exec c1 python3 -m http.server 80 --bind 0.0.0.0 >/tmp/c1-http.log 2>&1 &
sleep 0.5
# Control: host reaches container IP directly (gateway path — NOT testing publish yet)
curl -sS -o /dev/null -w "direct:%{http_code}\n" --connect-timeout 3 http://10.10.0.10:80/ || true
4.3 Predict — trước DNAT
Từ netns ext, chạy curl http://192.0.2.1:8080/ trước khi thêm bất kỳ rule NAT/FORWARD publish. Host có IP 192.0.2.1 trên veth-ext; chưa có process listen host :8080, chưa DNAT.
Đoán: success hay fail? Viết một lý do cơ chế (listen? DNAT? FORWARD?) trước §4.4.
# Expect fail (connection refused / timeout) before publish rules
sudo ip netns exec ext curl -sS -o /dev/null -w "before:%{http_code}\n" \
--connect-timeout 3 http://192.0.2.1:8080/ || echo "before:fail"
4.4 Bật forward + DNAT + FORWARD path
sudo sysctl -w net.ipv4.ip_forward=1
# DNAT: packets to host:8080 on the "uplink" -> container:80
sudo iptables -t nat -A PREROUTING -i veth-ext -p tcp --dport 8080 \
-j DNAT --to-destination 10.10.0.10:80
# Forward path into / out of container (lab-simple)
sudo iptables -A FORWARD -i veth-ext -o br0 -p tcp -d 10.10.0.10 --dport 80 -j ACCEPT
sudo iptables -A FORWARD -i br0 -o veth-ext -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# Verify rules + counters later
sudo iptables -t nat -L PREROUTING -n -v
sudo iptables -L FORWARD -n -v
Thử lại từ client “ngoài”:
sudo ip netns exec ext curl -sS -o /dev/null -w "after:%{http_code}\n" \
--connect-timeout 3 http://192.0.2.1:8080/
# Expect HTTP 200 from python http.server
# Counters should move on DNAT + FORWARD lines
sudo iptables -t nat -L PREROUTING -n -v
sudo iptables -L FORWARD -n -v
Đọc chứng cứ: pkts trên rule DNAT tăng khi curl từ ext = gói khớp publish. curl trực tiếp 10.10.0.10:80 từ host không cần DNAT — đừng nhầm hai path.
Rule FORWARD lab hẹp theo lab iface (veth-ext/br0). Host Docker siết qua chain DOCKER* / DOCKER-USER. Đừng copy ACCEPT rộng lên prod; học đủ mảnh (DNAT + FORWARD), siết có chủ đích (M1).
5. Fade — FORWARD DROP và bind loopback
5.1 Tự chẩn: chỉ DNAT, FORWARD chết
Lab §4 đang after:200. Ai đó hardening:
sudo iptables -P FORWARD DROP
# (giả sử xoá hết rule FORWARD lab; DNAT PREROUTING vẫn còn)
sudo iptables -D FORWARD -i veth-ext -o br0 -p tcp -d 10.10.0.10 --dport 80 -j ACCEPT 2>/dev/null || true
sudo iptables -D FORWARD -i br0 -o veth-ext -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true
Triệu chứng: curl http://10.10.0.10:80/ từ host có thể vẫn OK (tuỳ path local); từ ext, curl http://192.0.2.1:8080/ fail.
Viết ra: (1) table/chain nào giết inbound publish? (2) hai hướng rule tối thiểu để lab sống lại mà không -P FORWARD ACCEPT toàn cục? Không mở đáp án dưới trước.
Đáp án fade (đối chiếu):
- filter FORWARD — DNAT đổi đích xong gói vẫn phải forward vào
br0; policy DROP + không ACCEPT khớp = chết. Nat table còn ≠ traffic vào app. - Khôi phục lab: ACCEPT
veth-ext → br0dport 80 tới10.10.0.10; ACCEPTbr0 → veth-extRELATED,ESTABLISHED. Rồicurllại từext.
sudo iptables -A FORWARD -i veth-ext -o br0 -p tcp -d 10.10.0.10 --dport 80 -j ACCEPT
sudo iptables -A FORWARD -i br0 -o veth-ext -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
sudo ip netns exec ext curl -sS -o /dev/null -w "recovered:%{http_code}\n" \
--connect-timeout 3 http://192.0.2.1:8080/
5.2 Tự chọn: 127.0.0.1 hay 0.0.0.0?
Với mỗi yêu cầu, ghi publish bind nào (127.0.0.1:8080:80 vs 8080:80 / 0.0.0.0) và một câu vì sao:
- Dev chỉ cần hit API từ browser trên cùng máy Docker; cấm máy khác trong quán cà phê đụng port.
- Demo sprint: tester mang laptop, hit
http://<IP-LAN-máy-dev>:8080trong phòng họp. - Production edge: reverse proxy trên cùng host nối
127.0.0.1:8080; Internet chỉ nói chuyện với proxy:443.
Viết xong mới đối chiếu bảng dưới.
| Nhu cầu | Chọn | Vì sao |
|---|---|---|
| (1) Chỉ localhost dev | 127.0.0.1:8080:80 | Không listen mặt LAN — giảm surface khi cafe Wi‑Fi |
| (2) Demo LAN | 8080:80 (0.0.0.0) | Client khác host phải tới IP LAN + port publish |
| (3) Proxy cùng host | 127.0.0.1:8080:80 (phổ biến) | App không public thô; TLS/terminate ở proxy |
Tương đương lab (ý tưởng, không bắt buộc gõ hết): DNAT match thêm -d 127.0.0.1 chỉ bắt loopback; match không giới hạn dest IP (hoặc -d IP LAN) = publish “mặt tiền”. Docker: -p 127.0.0.1:8080:80 vs -p 8080:80.
6. Pitfall tổng hợp + map Docker
❌ Nhầm 1: “curl localhost xanh ⇒ publish OK cho cả team.”
✅ Localhost chỉ chứng minh path loopback / proxy host. Client LAN/WAN cần bind 0.0.0.0 (hoặc IP đúng iface) + path PREROUTING + FORWARD.
❌ Nhầm 2: “Chỉ cần DNAT, không cần FORWARD.”
✅ Sau DNAT gói forward vào netns. FORWARD DROP / mất rule DOCKER* = publish chết; counter nat có thể vẫn tăng.
❌ Nhầm 3: “Soi ss trên host thấy app listen :80.”
✅ App listen trong netns container. Host thấy docker-proxy / rule nat, không phải PID app (trừ host network mode).
❌ Nhầm 4: “SNAT và DNAT cùng một nút ‘bật NAT’.”
✅ SNAT egress đổi source POSTROUTING; publish đổi dest PREROUTING. Hai chiều, hai field.
❌ Nhầm 5: “iptables -t nat -F cho sạch.”
✅ Xoá publish + MASQUERADE Docker hàng loạt. Policy thêm: DOCKER-USER, không flush mù.
# Cleanup THIS lesson (rules + netns). Adjust if you re-added FORWARD lines.
sudo iptables -t nat -D PREROUTING -i veth-ext -p tcp --dport 8080 \
-j DNAT --to-destination 10.10.0.10:80 2>/dev/null || true
sudo iptables -D FORWARD -i veth-ext -o br0 -p tcp -d 10.10.0.10 --dport 80 -j ACCEPT 2>/dev/null || true
sudo iptables -D FORWARD -i br0 -o veth-ext -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true
# Stop http.server in c1 if still running (best-effort)
sudo ip netns exec c1 pkill -f "http.server 80" 2>/dev/null || true
sudo ip netns del c1 2>/dev/null || true
sudo ip netns del ext 2>/dev/null || true
sudo ip link del br0 2>/dev/null || true
sudo ip link del veth-ext 2>/dev/null || true
| Docker / runtime | Object bài này |
|---|---|
-p 8080:80 | DNAT host 8080 → container 80 |
-p 127.0.0.1:8080:80 | Publish chỉ loopback |
Chain nat DOCKER | Rule DNAT Engine inject |
filter DOCKER / FORWARD | Cho gói vào veth/container |
docker-proxy | Lớp host-local listen (bổ sung) |
| “LAN không vào được, localhost được” | Bind 127.0.0.1 hoặc firewall path PREROUTING |
flowchart TB
A["Symptom: client cannot hit host:8080"] --> B{"App listen inside container netns?"}
B -->|"no"| APP["Fix process bind / port in netns"]
B -->|"yes"| C{"nat PREROUTING / DOCKER DNAT match port?"}
C -->|"no"| DNAT["Add/fix DNAT or docker -p"]
C -->|"yes"| D{"FORWARD / DOCKER accept path?"}
D -->|"DROP"| FW["Fix FORWARD or DOCKER-USER"]
D -->|"yes"| E{"Publish bind 127.0.0.1 only?"}
E -->|"yes + client remote"| BIND["Republish 0.0.0.0 or correct IP"]
E -->|"no"| NEXT["tcpdump veth/br0 — next lesson"]📚 Deep Dive — man page & docs
Spec / reference:
- RFC 3022 — Traditional NAT + RFC 2663 — DNAT/port mapping (Foundations).
iptables(8)— tablenat, targetDNAT, chains PREROUTING / OUTPUT.- Netfilter NAT — hook PREROUTING + conntrack reverse.
- Docker networking — bridge + published ports.
- Docker packet filtering / iptables —
DOCKER-USER, tương tác firewall host. - Docker with iptables — chain nat
DOCKER, publish map (đọc, hạn chế sửa tay rule Engine).
Ghi chú: Backend nftables: cùng mental model hook/table; survey nft list ruleset. Lab dùng iptables cho khớp docs Docker phổ biến.
Liên hệ các bài khác
- 04 — Container ra Internet (SNAT) — chiều ra (đổi nguồn); bài này vào (đổi đích).
- 03 — Mini-challenge bridge — LAN ảo + gateway; publish thêm cửa mặt tiền host.
- 01 — Network namespaces — app listen trong netns;
sshost ≠sscontainer. - 06 — Debug nsenter / tcpdump — bắt gói trên veth/br0 khi DNAT/FORWARD lệch.
- M1 — iptables & netfilter — PREROUTING/FORWARD + survey chain Docker.
- Foundations — NAT & port forwarding — DNAT khái niệm; bài này implement trên host bridge-style.
Tóm tắt
- Publish port = DNAT (đổi đích) trên nat PREROUTING + filter FORWARD vào bridge/container; reply nhờ conntrack.
docker -p 8080:80≈ map host:8080 → container_ip:80 (chainDOCKER/ docker-proxy trên Engine).- Đường gói: client → host:port → DNAT → FORWARD → br0/veth → app trong netns.
127.0.0.1:hostPort:containerPortchỉ loopback; omit IP /0.0.0.0= publish mọi iface — pitfall demo localhost.- Pitfall: tin
curl localhost, DNAT không FORWARD, nhầm SNAT/DNAT, flush nat Docker. - Fade: tự chẩn FORWARD DROP + tự chọn bind loopback vs all-interfaces.
7. Tự kiểm tra
Q1Liệt kê hai mảnh netfilter tối thiểu (ngoài app đang listen trong netns) để client ngoài hit host:8080 vào container:80. Thiếu mỗi mảnh triệu chứng gần đúng là gì?▸
Q2Trace một request từ client ngoài tới host:8080 publish vào 10.10.0.10:80: object/hook nào lần lượt, và field IP nào bị DNAT đổi?▸
Q3docker run -p 8080:80 khác -p 127.0.0.1:8080:80 ở điểm bind nào? Kịch bản nào xanh localhost mà LAN fail?▸
-p 8080:80 publish trên 0.0.0.0 (mọi iface host). -p 127.0.0.1:8080:80 chỉ loopback. Kịch bản fail: dev curl localhost OK; máy khác trong LAN curl IP LAN:8080 timeout/refuse vì không có listener/DNAT cho địa chỉ non-loopback.Q4Vì sao sau DNAT, debug chỉ iptables -L INPUT (filter) dễ bỏ sót root cause publish?▸
-t nat -L PREROUTING/DOCKER -n -v và FORWARD / chain Docker + counter khi reproduce.Q5Fade: DNAT PREROUTING còn, pkts DNAT tăng khi client thử, nhưng app không nhận connection. Bạn nghi chain nào trước và vì sao counter nat tăng vẫn tương thích?▸
iptables -L FORWARD -n -v / chain DOCKER, tcpdump trên br0/veth.Q6Phân biệt nhanh publish DNAT bài này với MASQUERADE egress bài 04 — đổi field nào, chain nat nào, chiều nào?▸
Q7Junior flush iptables -t nat -F trên host Docker đang chạy nhiều -p. Hệ quả publish/egress nào, và chỗ đúng để siết policy user là gì?▸
Bài tiếp theo: Debug container networking — nsenter và tcpdump
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