防火牆與進入 Linux Networking 必備的基礎指令 - `iptables`
2025-05-04 17:13:47

本篇文章使用 Ubuntu 22.04

防火牆

防火牆是透過設定一些有順序的規則,來管制進出封包的機制。這邊引用了鳥哥筆記的圖片來輔助,可以看到封包在進入防火牆這道關卡的流程會經過規則 1、規則 2 、 … ,過程具有順序性。

其中 iptables 是 Linux 系統上一個極為關鍵的防火牆工具,在進入 networking 這個大坑之前請教前輩的時候,他們也都說這個指令必學(嘎嘎頂XD)。

iptables 與 Netfilter

iptables 和 Netfilter 有很大的關係,Netfilter 框架是 Linux kernel 提供的一個模組,用來管理進出主機的 packet flow,而 iptables 作為一個命令列介面,透過直接與 Netfilter 互動 (呼叫 Netfilter 的 hooks) 以攔截並處理幾乎所有進出主機的封包:

  • INPUT:進入本機的封包(如 ssh、http 請求)
  • OUTPUT:由本機送出的封包(如 ping、curl)
  • FORWARD:經過本機轉送到其他主機的封包(通常在 router/gateway 上常見)

iptables 架構:三大核心元素

Tables

用來分類與組織不同的防火牆規則。每張表會依據它的處理目的被分類,換句話說,不同表會針對不同類型的封包處理決策做優化。

  • Filter 表:處理封包是否允許通過的決策(最常用的表)。
  • NAT 表:負責網路位址轉換(如 SNAT、DNAT),影響封包的路由行為。
  • Mangle 表:用來修改封包的特殊資訊(如 IP header 的 TTL 值等)。
  • Raw 表:提供處理狀態追蹤(state tracking)之前的控制點,常用於設定某些封包不被連線追蹤系統處理(NOTRACK)。
1
sudo iptables-save

這行指令我是在 VirtualBox 的 Ubuntu terminal 使用,輸出如下(#號是我個人註解說明),顯示了主機上目前的 iptables 規則(filter 表與 nat 表),兩張表主要是由 Docker 自動產生與管理的。

1
2
3
4
5
6
7
8
9
*nat
# ...
COMMIT
# Completed on Sun May 4 13:12:29 2025
# Generated by iptables-save v1.8.4 on Sun May 4 13:12:29 2025

*filter
# ...
COMMIT
  • filter
    • INPUT:控制進入主機的封包
    • OUTPUT:控制由主機送出的封包
    • FORWARD:控制經由主機轉送的封包(e.g. container 間流量),與下列 nat table 相關性較高。
  • nat
    • PREROUTING:在進行路由判斷之前所要進行的規則(DNAT/REDIRECT)
    • POSTROUTING:封包離開主機前,進行 SNAT/MASQUERADE 規則處理
    • OUTPUT:本機發出的封包,進行 NAT 處理

Chains

鏈定義封包在傳遞過程中「何時」會被檢查,並且「在哪裡」做出處理決策。其實這篇筆記的第一張圖,也就是規則執行流程也屬於一個 chain。

在執行指令 sudo iptables-save 後的輸出內容中,表下面的幾行都是 chain 的定義,例如對於 nat 表而言,:PREROUTING:INPUT 都是 chain,以下是輸出與說明:

  • PREROUTING:封包進入主機前(未經路由)處理
  • INPUT:封包要送進本機的 NAT 前處理(較少使用)
  • OUTPUT:本機發出的封包,進行 NAT 處理
  • POSTROUTING:封包離開主機前,進行 SNAT/MASQUERADE 處理
  • DOCKER:Docker 自定義鏈,處理 DNAT 導向 container
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
*nat
:PREROUTING ACCEPT [0:0] # 封包進來後,還沒路由前先經過這裡
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [44:3818]
:POSTROUTING ACCEPT [44:3818] # 封包出主機前的最後處理點
:DOCKER - [0:0] # Docker 專屬自定義 chain
# ...
COMMIT

*filter
:INPUT ACCEPT [183:17706]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [167:15135]
:DOCKER - [0:0]
:DOCKER-ISOLATION-STAGE-1 - [0:0]
:DOCKER-ISOLATION-STAGE-2 - [0:0]
:DOCKER-USER - [0:0]
# ...
COMMIT

Targets

當封包經過某條鏈時,它會依照鏈內的規則逐一比對。如果符合某條規則的條件,則會執行該規則設定的 target。常見的 terminating targets 如下:

  • ACCEPT:接受封包。
  • DROP:丟棄封包,不回應對方,使對方無法知道主機是否存在。
  • REJECT:拒絕封包,並明確通知對方連線被拒絕。

如果要透過指令檢查每個 chain 中有哪些規則,可以用

1
iptables -L -n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Chain INPUT (policy ACCEPT)
target prot opt source destination

Chain FORWARD (policy DROP)
target prot opt source destination
DOCKER-USER all -- 0.0.0.0/0 0.0.0.0/0
DOCKER-ISOLATION-STAGE-1 all -- 0.0.0.0/0 0.0.0.0/0
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
DOCKER all -- 0.0.0.0/0 0.0.0.0/0
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
DOCKER all -- 0.0.0.0/0 0.0.0.0/0
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
DOCKER all -- 0.0.0.0/0 0.0.0.0/0
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0

Chain OUTPUT (policy ACCEPT)
target prot opt source destination

Chain DOCKER (3 references)
target prot opt source destination

Chain DOCKER-ISOLATION-STAGE-1 (1 references)
target prot opt source destination
DOCKER-ISOLATION-STAGE-2 all -- 0.0.0.0/0 0.0.0.0/0
DOCKER-ISOLATION-STAGE-2 all -- 0.0.0.0/0 0.0.0.0/0
DOCKER-ISOLATION-STAGE-2 all -- 0.0.0.0/0 0.0.0.0/0
RETURN all -- 0.0.0.0/0 0.0.0.0/0

Chain DOCKER-ISOLATION-STAGE-2 (3 references)
target prot opt source destination
DROP all -- 0.0.0.0/0 0.0.0.0/0
DROP all -- 0.0.0.0/0 0.0.0.0/0
DROP all -- 0.0.0.0/0 0.0.0.0/0
RETURN all -- 0.0.0.0/0 0.0.0.0/0

Chain DOCKER-USER (1 references)
target prot opt source destination
RETURN all -- 0.0.0.0/0 0.0.0.0/0

如果要透過指令檢查特定 chain 中有哪些規則,可以用

1
2
sudo iptables -t nat -L <CHAIN_NAME> -v -n
sudo iptables -t filter -L <CHAIN_NAME> -v -n

例如當我執行 sudo iptables -t nat -L DOCKER -v -n,就會顯示 nat 表中 DOCKER 這條 chain 裡目前的所有規則,包含封包數、來源/目的、interface 等資訊如下:

1
2
3
4
5
Chain DOCKER (2 references)
pkts bytes target prot opt in out source destination
0 0 RETURN all -- docker0 * 0.0.0.0/0 0.0.0.0/0
0 0 RETURN all -- br-ed05190a252d * 0.0.0.0/0 0.0.0.0/0
0 0 RETURN all -- br-e4d2a678019e * 0.0.0.0/0 0.0.0.0/0

封包如何進行規則處理?

從鳥哥筆記的圖裡可以發現網路封包在進入伺服器前,會經過好幾組規則,每個鏈裡面都有自己的一組規則,每個表又會有很多鏈。

NAT 網路位址轉換:SNAT/DNAT 的設定

透過指令sudo iptables -t nat -L -v -n,可以從 nat 表中 chains 的規則理解使用了哪幾種 NAT 類型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Chain PREROUTING (policy ACCEPT 10 packets, 1074 bytes)
pkts bytes target prot opt in out source destination
1 134 DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT 10 packets, 1074 bytes)
pkts bytes target prot opt in out source destination

Chain OUTPUT (policy ACCEPT 142 packets, 13057 bytes)
pkts bytes target prot opt in out source destination
0 0 DOCKER all -- * * 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT 142 packets, 13057 bytes)
pkts bytes target prot opt in out source destination
0 0 MASQUERADE all -- * !docker0 172.17.0.0/16 0.0.0.0/0
0 0 MASQUERADE all -- * !br-ed05190a252d 172.19.0.0/16 0.0.0.0/0
0 0 MASQUERADE all -- * !br-e4d2a678019e 172.18.0.0/16 0.0.0.0/0

Chain DOCKER (2 references)
pkts bytes target prot opt in out source destination
0 0 RETURN all -- docker0 * 0.0.0.0/0 0.0.0.0/0
0 0 RETURN all -- br-ed05190a252d * 0.0.0.0/0 0.0.0.0/0
0 0 RETURN all -- br-e4d2a678019e * 0.0.0.0/0 0.0.0.0/0

從上述結果可以看到我的 VM 目前用了以下兩種 NAT 類型,但沒有 SNAT、DNAT 等 NAT 類型:

NAT 類型 出現位置 說明
MASQUERADE POSTROUTING 鏈 將 container 對外封包偽裝成主機 IP(動態 SNAT)
RETURN DOCKER 鏈 結束處理、跳出 DOCKER 鏈(非 NAT 類型,但重要邏輯)

透過 MASQUERADE 的 NAT,讓 VM 內容器可以主動存取到外部網路的連線,但是外部網路無法存取到容器。這樣的運作可以搭配這張圖做解釋:

圖中元件 對應 VirtualBox / Docker 環境
INTERNET 外部網路(如主機對外網卡、路由器、真實 Internet)
eth0 Ubuntu VM 的 NAT 或橋接網卡(連到外部)
eth1 Ubuntu VM 的 Docker bridge(例如 docker0)
Firewall: MASQUERADE Ubuntu VM + iptables(設定 MASQUERADE 的地方)
LAN 多個 Docker container,模擬區域網路使用者

當某個容器(例如 IP = 10.1.1.101)想要上網時,封包首先從 container 發出 → 透過 docker0 傳到 Ubuntu VM 的 eth1(圖中 LAN → eth1),接著 iptables 對這個封包執行 MASQUERADE,把 來源 IP 改為 Ubuntu VM 的 eth0(即 172.19.19.130),改寫後的封包送出 Internet,外部網站將回應送回 172.19.19.130 → VM 收到後,由 conntrack 追蹤反向轉換 → 傳回 container

MASQUERADE vs SNAT, DNAT

MASQUERADE 和 SNAT 的功能是一樣的,他們都會改寫封包的來源 IP 位址,讓內部主機的封包在送出到外網時,看起來是由 NAT 主機送出的。不過不一樣的是 MASQUERADE 不需要手動指定來源 IP,它會自動使用封包要送出的介面(如 eth0)的 IP。在這樣的情況,SNAT 適合用於固定 public IP 的伺服器,在 Docker 等動態網路場景,MASQUERADE 是預設且常用的選擇。

iptables 實作 - 用 Docker 建立小型 LAN 環境

  1. 建立 Docker 自訂網路,子網 10.10.0.0/24、Gateway(主機對內橋接 IP):10.10.0.1
    1
    2
    3
    4
    sudo docker network create \
    --subnet=10.10.0.0/24 \
    --gateway=10.10.0.1 \
    mynet
  2. 建立兩個 Container 當作內部主機
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 建立 container c1,IP: 10.10.0.2
    sudo docker run -dit --rm \
    --name c1 --net mynet --ip 10.10.0.2 \
    busybox sh

    # 建立 container c2,IP: 10.10.0.3
    sudo docker run -dit --rm \
    --name c2 --net mynet --ip 10.10.0.3 \
    busybox sh
  3. 測試內部連線,讓 c1 ping 到 c2
    1
    sudo docker exec -it c1 ping -c 3 10.10.0.3
    1
    2
    3
    4
    5
    6
    7
    8
    PING 10.10.0.3 (10.10.0.3): 56 data bytes
    64 bytes from 10.10.0.3: seq=0 ttl=64 time=0.318 ms
    64 bytes from 10.10.0.3: seq=1 ttl=64 time=0.119 ms
    64 bytes from 10.10.0.3: seq=2 ttl=64 time=0.138 ms

    --- 10.10.0.3 ping statistics ---
    3 packets transmitted, 3 packets received, 0% packet loss
    round-trip min/avg/max = 0.119/0.191/0.318 ms
  4. 應用 iptables 規則測試,首先要在主機 (這邊是在 VM) 新增防火牆規則。這邊會把 DROP 規則插入在規則第一條,避免導向 DOCKER chain,裡面預設就是放行 container 內部互通。
    1
    sudo iptables -I FORWARD 1 -s 10.10.0.2 -d 10.10.0.3 -j DROP
  5. 再次測試,結果如下
    1
    2
    3
    4
    PING 10.10.0.3 (10.10.0.3): 56 data bytes

    --- 10.10.0.3 ping statistics ---
    3 packets transmitted, 0 packets received, 100% packet loss
  6. 觀察封包與連線追蹤,查看連線追蹤紀錄(conntrack)
    1
    2
    unknown  2 6 src=10.10.0.1 dst=224.0.0.22 [UNREPLIED] src=224.0.0.22 dst=10.10.0.1 mark=0 use=1
    conntrack v1.4.5 (conntrack-tools): 1 flow entries have been shown.
  7. 刪除規則
    1
    sudo iptables -D FORWARD -s 10.10.0.2 -d 10.10.0.3 -j DROP
  8. 再次實驗
    1
    2
    3
    4
    5
    6
    7
    tcp      6 33 TIME_WAIT src=10.0.2.15 dst=91.189.91.98 sport=58758 dport=80 src=91.189.91.98 dst=10.0.2.15 sport=80 dport=58758 [ASSURED] mark=0 use=1
    udp 17 21 src=10.10.0.1 dst=224.0.0.251 sport=5353 dport=5353 [UNREPLIED] src=224.0.0.251 dst=10.10.0.1 sport=5353 dport=5353 mark=0 use=1
    udp 17 20 src=127.0.0.1 dst=127.0.0.53 sport=54935 dport=53 src=127.0.0.53 dst=127.0.0.1 sport=53 dport=54935 mark=0 use=1
    udp 17 28 src=10.0.2.15 dst=185.125.190.57 sport=40618 dport=123 src=185.125.190.57 dst=10.0.2.15 sport=123 dport=40618 mark=0 use=1
    unknown 2 80 src=10.10.0.1 dst=224.0.0.22 [UNREPLIED] src=224.0.0.22 dst=10.10.0.1 mark=0 use=1
    udp 17 20 src=10.0.2.15 dst=10.0.2.3 sport=56977 dport=53 src=10.0.2.3 dst=10.0.2.15 sport=53 dport=56977 mark=0 use=1
    conntrack v1.4.5 (conntrack-tools): 6 flow entries have been shown.

iptables 實作 - 查看封包流向與 iptables 關係

在鳥哥那張圖裡面可以看到封包流向,這邊做個簡單的實驗驗證封包經過 PREROUTING → 路由判斷 → INPUT

  1. 加入規則: sudo iptables -I FORWARD 1 -s 10.10.0.2 -d 10.10.0.3 -j DROP
  2. docker exec c1 ping -c 3 10.10.0.3
  3. sudo iptables -L FORWARD -v -n --line-numbers 可以看到結果如下,pkts 表示封包數量,他從 0 變成 3,表示剛剛那三個封包都有成功進入到 FORWARD chain,而這是因為封包經過 PREROUTING 並被判斷「不是送給本機」,就會經過 FORWARD
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    Chain FORWARD (policy DROP 0 packets, 0 bytes)
    num pkts bytes target prot opt in out source destination
    1 3 252 DROP all -- * * 10.10.0.2 10.10.0.3
    2 12 1008 DOCKER-USER all -- * * 0.0.0.0/0 0.0.0.0/0
    3 12 1008 DOCKER-ISOLATION-STAGE-1 all -- * * 0.0.0.0/0 0.0.0.0/0
    4 10 840 ACCEPT all -- * br-13eaf78191b4 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
    5 2 168 DOCKER all -- * br-13eaf78191b4 0.0.0.0/0 0.0.0.0/0
    6 0 0 ACCEPT all -- br-13eaf78191b4 !br-13eaf78191b4 0.0.0.0/0 0.0.0.0/0
    7 2 168 ACCEPT all -- br-13eaf78191b4 br-13eaf78191b4 0.0.0.0/0 0.0.0.0/0
    8 0 0 ACCEPT all -- * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
    9 0 0 DOCKER all -- * docker0 0.0.0.0/0 0.0.0.0/0
    10 0 0 ACCEPT all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
    11 0 0 ACCEPT all -- docker0 docker0 0.0.0.0/0 0.0.0.0/0
    12 0 0 ACCEPT all -- * br-ed05190a252d 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
    13 0 0 DOCKER all -- * br-ed05190a252d 0.0.0.0/0 0.0.0.0/0
    14 0 0 ACCEPT all -- br-ed05190a252d !br-ed05190a252d 0.0.0.0/0 0.0.0.0/0
    15 0 0 ACCEPT all -- br-ed05190a252d br-ed05190a252d 0.0.0.0/0 0.0.0.0/0
    16 0 0 ACCEPT all -- * br-e4d2a678019e 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
    17 0 0 DOCKER all -- * br-e4d2a678019e 0.0.0.0/0 0.0.0.0/0
    18 0 0 ACCEPT all -- br-e4d2a678019e !br-e4d2a678019e 0.0.0.0/0 0.0.0.0/0
    19 0 0 ACCEPT all -- br-e4d2a678019e br-e4d2a678019e 0.0.0.0/0 0.0.0.0/0

References