這篇是叫 Claude 從我的筆記整理出來,事情真的發生過,字是他打的,連語氣也是叫他學我的,我只負責瞄一下的喔。
不是 QVPN 的問題啦,是 router 太有記性了
家裡這條 tunnel 是做什麼的
先講一下環境。家裡的 NAS(duducar,一台 QNAP)用內建的 QVPN 跑一個 WireGuard client,連到我在 Linode 東京的 VPS(ymknode)。ymknode 是 hub,我所有的機器都是它的 peer,拿到一個 10.1.8.x 的 IP,所以不管人在哪裡都可以直接 ssh 到家裡的 NAS 去看東西、跑 rsync。
graph LR
NAS["duducar (NAS)<br/>192.168.18.18"] --> R["家裡的 router<br/>dual WAN, NAT"]
R -- "UDP 51820" --> V["ymknode (Linode)<br/>WireGuard hub"]
L["筆電 / 其他 peers"] --> V
平常都很正常,可以連著好幾個禮拜,然後某一天就突然連不上了。每次都覺得是 QVPN 的問題,反正重開機就好了。但是重開一台 NAS 有點麻煩啊,上面還跑著一堆東西,這樣來來回回大概有半年了吧。
這次早上又發生了,剛好兩邊都開著 session,就想說來查一下到底是怎麼回事好了,反正現在查東西也不用自己敲。
兩邊都說自己沒問題
先看 ymknode 那邊,wg show 說 duducar 這個 peer 的 last handshake 是 1 小時 41 分鐘前。嗯,就是一個多小時前斷的。
NAS 這邊先照慣例重啟一下 WireGuard:
$ sudo ~/bin/qnap-wg-restart.sh
結果還是沒有 handshake。再看 NAS 這邊的 wg show:
interface: wgc3001
listening port: 51820
peer: ...
endpoint: <ymknode>:51820
transfer: 0 B received, 2.31 KiB sent
persistent keepalive: every 25 seconds
sent 一直在增加,received 永遠是 0。也就是 handshake initiation 有一直送出去,但是從來沒有回來過。可是 route 是對的,ping <ymknode> 也通得好好的喔。
送得出去,收不回來,但 ping 是通的
那就在另一邊聽聽看
既然 NAS 說有送,那就到 ymknode 上 tcpdump 看看有沒有收到:
$ sudo tcpdump -ni eth0 udp port 51820
這裡有個小技巧,WireGuard 的封包不用解密也看得出來是什麼,因為長度是固定的:
| 封包 | 長度 |
|---|---|
| handshake initiation | 148 bytes |
| handshake response | 92 bytes |
| keepalive | 32 bytes |
結果家裡的舊 IP 一個封包都沒有,從任何地方也都沒看到 148 bytes 的 initiation。有看到的那些 32 bytes 是其他 peer 的 keepalive,不是 duducar 的。
所以 NAS 說有送,ymknode 說沒收到,中間就只剩下家裡的 router 了。
家裡的 public IP 換了
這時候才發現,家裡的 public IP 在一個多小時前換了。router 是 dual WAN 的,兩條線的 IP 會輪流出現,時間點跟 last handshake 完全對得上。
那為什麼換個 IP 就壞掉呢?這要從 NAT 怎麼運作講起。
NAS 的 192.168.18.18 是私有 IP,出去的時候 router 會把來源換成 public IP,然後在 connection tracking table 記一筆,才知道回來的封包要還給誰:
192.168.18.18:51820 -> <ymknode>:51820 via 36.228.x.x (舊的 public IP)
UDP 沒有連線的概念,所以這種 entry 是靠時間過期的,一般幾十秒到幾分鐘沒有封包就會被清掉。
問題來了,WireGuard 的 PersistentKeepalive = 25 每 25 秒就打一個封包出去,目的就是要讓 NAT entry 不要過期,這樣 server 才能隨時找到 NAT 後面的 client。這本來是好意,但 IP 換掉之後這筆 entry 綁的還是舊 IP,keepalive 每 25 秒去 refresh 它一次,它就永遠活著。每個封包都從一個已經不存在的 IP 送出去,當然收不到回應。
讓 NAT entry 不過期的 keepalive,就是讓壞掉的 NAT entry 不過期的那個 keepalive
然後為什麼重啟 WireGuard 也沒用?因為 QVPN 的設定檔寫死了 ListenPort = 51820,重啟之後還是從同一個 port 出去,來源 IP:port 跟 目的 IP:port 四個數字都一樣,router 當然就直接拿同一筆爛掉的 entry 來用。
那為什麼 ping 又是通的呢?因為 ICMP 是另一種 protocol,會拿到自己的一筆新 entry,跟 UDP 51820 那筆沒關係。這就是一直被誤導的原因,ping 通了就以為網路沒問題。
重開 NAS 會好,也只是因為停機的那幾分鐘沒有 keepalive,entry 終於過期了,不是 QVPN 被修好了。
換個 port 就好了
既然知道是 NAT entry 的問題,那就不要用 51820 出去就好了:
$ sudo wg set wgc3001 listen-port 51821
來源 port 一變,對 router 來說就是一條全新的 flow,拿到一筆新的 entry,綁的是現在的 public IP。幾秒鐘之內就 handshake 了,ymknode 那邊看到 duducar 從新 IP 的 51821 進來。半年的問題,一行指令。
不過這只是 runtime 改的,QVPN 重啟的時候又會把 wgc3001.conf 重寫回 ListenPort = 51820,所以還要想辦法讓它不要寫。
讓 QVPN 自己挑 port
在 Threads 上有寫到「想想為什麼 client 一定要 51820 呢」。其實 WireGuard 的 client 根本不需要固定的 listen port,不寫 ListenPort 的話 kernel 會隨機挑一個,是 QVPN 幫你填進去的。
翻了一下 QNAP 的 /etc/init.d/vpn_wireguard_client.sh,它產生設定檔的邏輯是只有在 VPN Port 不是 0 的時候才會寫 ListenPort 這行。那就把它設成 0 吧:
$ sudo cp /etc/config/vpn.conf /etc/config/vpn.conf.bak-20261002
$ sudo setcfg WG_CLIENT1 "VPN Port" 0 -f /etc/config/vpn.conf
$ sudo /etc/init.d/vpn_wireguard_client.sh restart 1
重啟之後 WireGuard 隨機挑了 32898,每次重啟都會是新的 port,也就是新的 NAT entry。以後 IP 再換掉的話,qnap-wg-restart.sh 跑一下就會好,不用再重開整台 NAS 了。
兩個小地方:
- restart 的時候會印 ipset 的「set with the same name already exists」跟 rtnetlink 的「file exists」,那是 stop 沒把 ipset 跟 route 清乾淨,東西都還在,不影響。
- 之後如果在 QVPN 的網頁介面動過這個 profile,要再檢查一次
VPN Port有沒有被改回 51820。
下次怎麼看
整理一下這次的判斷順序,下次遇到類似的可以直接照著走:
- 兩邊
wg show,看 last handshake 跟 transfer。sent > 0但received = 0就是封包在路上消失了。 ping對方,通的話代表 route 跟線路沒問題,但不代表 UDP 沒問題。- 到對方那邊
tcpdump udp port 51820,用長度認封包,看 148 bytes 的 initiation 有沒有到。 - 沒到的話,查家裡的 public IP 有沒有剛換過。
- 換個 listen port 試試看,幾秒內有 handshake 就是 NAT entry。
sent > 0、received = 0、ping 通,要看的是 NAT,不是 key
學到的事
一個問題拖了半年都用重開機解決,結果真的坐下來看只花了一個早上。以前是懶得查,現在是旁邊有人幫忙敲 tcpdump 算封包長度,就比較願意坐下來了呢。
一言不合就 tcpdump,比重開機快多了喔