1. 網路拓撲與背景
家裡有兩台路由器在同一個 LAN 上運作,分別是中華電信數據機與 MikroTik hEX。這個架構是所有後續問題的根源。
| 設備 | IP | 角色 |
|---|---|---|
| 中華電信數據機 | 192.168.1.1 | 第一個 WAN 閘道,NAS 預設閘道 |
| MikroTik hEX | 192.168.1.2 | 第二個 WAN 閘道,PPPoE 撥接 |
| NAS(飛牛 trim-fa91) | 192.168.1.51 | Docker host,跑 OpenVPN 容器 |
NAS 上已有
openvpn 容器跑在 UDP 1194(正常運作中)。目標是新增一個 openvpn-tcp 容器,讓它走 TCP 443,以便在受限網路環境下也能連入。
核心陷阱:兩台 WAN 路由器共存於同一 LAN,封包的去程與回程走不同路徑,造成 非對稱路由(Asymmetric Routing),這是後續第三層坑的根源。
NAS 的預設閘道指向數據機(192.168.1.1),而不是 MikroTik(192.168.1.2)。這意味著從 MikroTik 進來的封包,回應會走數據機出去——非對稱路由就此產生。
2. 五層坑(依除錯順序)
Layer 1 NAS 系統服務佔用 Port 443
問題症狀
容器啟動後,外部連 192.168.1.51:443 無回應,或回應的是 NAS 的 Web UI 而不是 OpenVPN。
原因
TerraMaster OS 的 nginx 已經綁定 TCP 443,用來提供 NAS 管理介面。這是系統級服務,有完整性保護,手動修改 nginx 後會自動還原。
既然改不了 nginx,就在它之前用 iptables NAT 攔截流量。在 PREROUTING 鏈中設定 DNAT,讓目的地為 443 的封包在到達 nginx 之前就被導走。
# 在 PREROUTING 階段攔截 TCP 443 流量,導向 Docker 容器
iptables -t nat -A PREROUTING -p tcp --dport 443 \
-j DNAT --to-destination 172.17.0.9:1194
Layer 2 Docker Container Port Mapping + iptables DNAT
問題症狀
設定 DNAT 後,從外部仍無法建立連線,或者連線建立後立即中斷。
原因
Docker 容器內部的 OpenVPN 監聽在 1194(不是 443),需要同時處理兩件事:
- 容器的 port mapping:宿主機的某個 port 映射到容器的 1194
- iptables DNAT:從外部 443 導到容器的內部 IP + port
docker run -d \
--name openvpn-tcp \
--restart unless-stopped \
--cap-add=NET_ADMIN \
-v /vol1/docker/openvpn/config:/etc/openvpn \
-p 8443:1194/tcp \
kylemanna/openvpn \
/bin/sh -c "openvpn --config /etc/openvpn/server-tcp.conf"
容器映射到宿主機的 8443 而非直接用 443,是因為 nginx 佔著 443。實際流量是:外部 443 → iptables DNAT → 容器 IP 172.17.0.9:1194。
# 確認容器 IP
docker inspect -f '{{.NetworkSettings.IPAddress}}' openvpn-tcp
# 輸出:172.17.0.9
# 設定 DNAT:外部 443 → 容器 172.17.0.9:1194
iptables -t nat -A PREROUTING -p tcp --dport 443 \
-j DNAT --to-destination 172.17.0.9:1194
# 確認規則
iptables -t nat -L PREROUTING -n --line-numbers
Layer 3 非對稱路由(Asymmetric Routing)— 最難搞
問題症狀
TCP 三次握手中的 SYN 能送到容器,但 SYN-ACK 回不去。用 conntrack -L 查看會看到連線卡在 SYN_RECV 狀態。
conntrack -L -p tcp --dport 443
# 看到大量 SYN_RECV 狀態,沒有 ESTABLISHED
原因
封包流向:
- 客戶端 → MikroTik(外部 IP)→ NAS(經 DNAT)→ 容器 ✓
- 容器 → NAS → 數據機(因為預設閘道是 192.168.1.1)→ 外部 ✓
- 但 MikroTik 收不到回應封包(因為回程走了數據機),連線建立失敗 ✗
非對稱路由導致 MikroTik 的 conntrack 記不到回程封包,SYN-ACK 雖然送出去了,但 MikroTik 認為那是「未知連線的回應」而直接丟棄。
解法:CONNMARK + Policy Routing
核心思路:在封包進來時就記住「這個封包是從 MikroTik 進來的」(用 CONNMARK 打標記),回程時根據標記強制走 MikroTik 出去。
步驟 1:建立策略路由表# 建立路由表 100,指向 MikroTik
ip route add default via 192.168.1.2 table 100
# 建立路由規則:從 192.168.1.2 進來的封包,查表 100
ip rule add from 192.168.1.2 table 100
步驟 2:用 iptables mangle 打 CONNMARK
# 進站:從 MikroTik 進來的 TCP 443 封包,打上 mark 1
iptables -t mangle -A PREROUTING -p tcp --dport 443 \
-s 192.168.1.0/24 -j CONNMARK --set-mark 1
# 出站:有 mark 1 的連線回應,強制走路由表 100(MikroTik)
iptables -t mangle -A POSTROUTING -p tcp --sport 443 \
-j CONNMARK --restore-mark
步驟 3:設定回程路由
# 確認 masquerade 設定,讓回程封包正確 NAT
iptables -t nat -A POSTROUTING -s 172.17.0.0/16 \
-o eth0 -j MASQUERADE
# 確認 IP forward 已開啟
sysctl net.ipv4.ip_forward
# 應輸出:net.ipv4.ip_forward = 1
Layer 4 conntrack Strict Tracking + Docker FORWARD DROP
問題症狀
TCP 三次握手勉強完成(SYN → SYN-ACK → ACK),但後續的資料封包全部被丟棄。OpenVPN 無法建立 tunnel。
原因
兩個問題同時存在:
- conntrack 嚴格追蹤模式:Linux 預設的 TCP conntrack 要求封包嚴格遵循狀態機,任何不完整的連線會被自動清除。
- Docker 的 DOCKER-USER 鏈:Docker 預設在 DOCKER-USER 鏈設了 DROP 規則,非 Docker 管理的流量會被擋掉。
# 在 DOCKER-USER 鏈開頭加入 ACCEPT 規則
iptables -I DOCKER-USER -i eth0 -p tcp --dport 443 \
-j ACCEPT
# 確認規則位置(必須在 DROP 規則之前)
iptables -L DOCKER-USER -n --line-numbers
解法 2:放宽 conntrack 追蹤
# 將 TCP 連線追蹤改為寬鬆模式
# (允許不完整狀態機的封包通過)
echo 1 > /proc/sys/net/netfilter/nf_conntrack_tcp_be_liberal
# 永久生效
echo "net.netfilter.nf_conntrack_tcp_be_liberal = 1" \
>> /etc/sysctl.conf
sysctl -p
nf_conntrack_tcp_be_liberal 會降低 conntrack 的嚴謹度,可能增加一些安全風險。在家庭 NAS 環境下影響不大,但生產環境需謹慎評估。
Layer 5 Policy Routing 範圍過廣
問題症狀
前面的設定完成後,OpenVPN 連線成功了!但奇怪的是,NAS 上其他 Docker 容器之間的通訊也開始出問題。
原因
第三層的策略路由設定(ip rule add from 192.168.1.2 table 100)太過廣泛。所有從 MikroTik 進來的封包都被強制走了路由表 100,包括發往 Docker 網路(172.16.0.0/12)的封包。
# 刪除過廣的規則
ip rule del from 192.168.1.2 table 100
# 新增精準規則:只對非 Docker 內網的封包套用策略路由
# Docker 網路:172.16.0.0/12
ip rule add from 192.168.1.2 to 172.16.0.0/12 lookup main
ip rule add from 192.168.1.2 to ! 172.16.0.0/12 table 100
# 確認規則順序(數字越小優先級越高)
ip rule show
Docker 容器之間的通訊走 Docker 內建的虛擬網路,不需要經過外部路由器。政策路由只應作用於「從外部進來的流量」,Docker 子網路的封包應走 main 表(預設路由)。
3. DNS / DDNS 設定
VPN 連線的目標位址從 NAS 的 IP(數據機的公網 IP)改為 MikroTik 的公網 IP(PPPoE 撥接取得)。
| 項目 | 設定 |
|---|---|
| DNS 名稱 | vpn.ouwinner.com |
| A Record | 從數據機公網 IP → MikroTik PPPoE IP |
| 更新方式 | SSH 到 MikroTik 讀取 PPPoE IP,更新 Cloudflare DNS |
| 更新頻率 | 每 5 分鐘(crontab) |
#!/bin/bash
# /usr/local/bin/update-vpn-ddns.sh
# 從 MikroTik 讀取 PPPoE 公網 IP,更新 Cloudflare DNS
MIKROTIK_IP="192.168.1.2"
CF_API_KEY="your_cloudflare_api_key"
CF_ZONE_ID="your_zone_id"
CF_RECORD_ID="your_record_id"
DNS_NAME="vpn.ouwinner.com"
# SSH 到 MikroTik 取得 PPPoE IP
PUBLIC_IP=$(ssh admin@${MIKROTIK_IP} \
"/ip address get [find interface=pppoe-out1] address" \
| cut -d'/' -f1)
if [ -z "$PUBLIC_IP" ]; then
echo "$(date): Failed to get IP from MikroTik" >> /var/log/vpn-ddns.log
exit 1
fi
# 更新 Cloudflare A Record
curl -s -X PUT \
"https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records/${CF_RECORD_ID}" \
-H "Authorization: Bearer ${CF_API_KEY}" \
-H "Content-Type: application/json" \
--data "{\"type\":\"A\",\"name\":\"${DNS_NAME}\",\"content\":\"${PUBLIC_IP}\",\"ttl\":120,\"proxied\":false}" \
>> /var/log/vpn-ddns.log
echo "$(date): Updated ${DNS_NAME} → ${PUBLIC_IP}" >> /var/log/vpn-ddns.log
Cron 排程
# 每 5 分鐘自動更新
*/5 * * * * /usr/local/bin/update-vpn-ddns.sh
4. 持久化設定一覽表
以下所有設定在重開機後都需要自動生效,否則 VPN 會斷線。
| 內容 | 位置 / 方式 |
|---|---|
| 443→容器 DNAT DOCKER-USER 放行 Policy Routing conntrack 設定 |
/etc/rc.local或 systemd service |
| openvpn-tcp 容器設定 | docker run 參數容器名: openvpn-tcp加上 --restart unless-stopped |
| DDNS 自動維護腳本 | /usr/local/bin/update-vpn-ddns.sh+ crontab */5 * * * * |
| Client 端設定檔 | /vol1/docker/openvpn/client1-tcp443.ovpn |
#!/bin/bash
# === OpenVPN TCP 443 持久化設定 ===
# 等待 Docker 就緒
sleep 10
# Layer 1 & 2: iptables DNAT(搶佔 443 → 容器)
iptables -t nat -A PREROUTING -p tcp --dport 443 \
-j DNAT --to-destination 172.17.0.9:1194
# Layer 4: DOCKER-USER 放行
iptables -I DOCKER-USER -i eth0 -p tcp --dport 443 \
-j ACCEPT
# Layer 4: conntrack 寬鬆模式
echo 1 > /proc/sys/net/netfilter/nf_conntrack_tcp_be_liberal
# Layer 3: CONNMARK 策略路由
ip route add default via 192.168.1.2 table 100 2>/dev/null
ip rule add from 192.168.1.2 to ! 172.16.0.0/12 table 100 2>/dev/null
iptables -t mangle -A PREROUTING -p tcp --dport 443 \
-s 192.168.1.0/24 -j CONNMARK --set-mark 1
iptables -t mangle -A POSTROUTING -p tcp --sport 443 \
-j CONNMARK --restore-mark
# Layer 5: Docker 內網走 main 表
ip rule add from 192.168.1.2 to 172.16.0.0/12 lookup main 2>/dev/null
# 確認 IP forward
sysctl -w net.ipv4.ip_forward=1
exit 0
5. 除錯教訓
- Port 443 不是你的,先搞定佔用者。在 NAS 或伺服器上跑服務,永遠先用
ss -tlnp | grep :443確認誰佔著 443。改不了就用 iptables NAT 在前面攔截。 - 雙 WAN 路由器 = 非對稱路由。家裡有兩台路由器提供上網時,封包進出走不同路徑是最常見也最難察覺的問題。
conntrack -L看到 SYN_RECV 卡住就要想到這個。 - CONNMARK + Policy Routing 是解決非對稱路由的標準解法。用 iptables mangle 在封包進站時打標記,再用
ip rule根據標記決定出站路由。這不是 hack,是 Linux 進階路由的正式功能。 - Docker 的 iptables 很霸道。Docker 會自動管理 DOCKER-USER / DOCKER 鏈,預設會 DROP 非 Docker 管理的流量。新增服務時一定要在 DOCKER-USER 鏈加上 ACCEPT 規則,而且要放在 DROP 規則之前。
- 政策路由要精準,不要一刀切。
ip rule設得太廣會影響 Docker 內網通訊。記得用to ! 172.16.0.0/12排除 Docker 子網路,或者反過來,對 Docker 子網路加一條走 main 表的規則。
整個除錯過程的核心邏輯:先解決端口衝突(iptables DNAT),再解決路由問題(CONNMARK + policy routing),最後處理 Docker 的網路隔離(DOCKER-USER + conntrack settings)。每一層都有其獨立的解法,但需要全部正確配置才能跑通。
💬 留言板
登入 Google 帳號即可留言