WireGuard cryptokey routing 與 AllowedIPs 原理
了解 WireGuard 如何以 AllowedIPs 同時充當路由表與存取清單,解析 cryptokey routing、Noise 交握與金鑰輪替,讀懂每份 wg0.conf 設定。
WireGuard 的運作原理:一個核心概念
WireGuard 會將每個封包繫結至公開金鑰。這套機制稱為 cryptokey routing,也是整個設計的核心:peer 旁的 AllowedIPs 行會同時作為離開本機之封包的路由表,以及來自該 peer 之封包的存取控制清單。一個設定,執行兩項工作。以這種方式理解 AllowedIPs,每個 WireGuard 設定檔都會變得容易讀懂。
WireGuard 沒有以 IP 位址為索引的工作階段表,也沒有使用者資料庫。peer 是一組公開金鑰,以及該金鑰可使用的位址集合。交握與計時器的作用,是在底層網路變更時維持這項繫結。若要先建立可運作的 tunnel,再理解理論,請先依照在自己的 VPS 上自架 WireGuard VPN建立 tunnel;之後遇到設定行令人困惑時,再回到這裡查看。
AllowedIPs 是路由表,也是存取清單
先看出站方向。核心會以一般方式,透過主要路由表將封包路由至 wg0 裝置。接著,WireGuard 會依最長前綴優先,將封包的目的地位址與一張包含所有 peer 允許前綴的表格比對。比對結果會指定 peer,再由 peer 指定 public key、session key 與 UDP endpoint。封包會使用該 peer 的金鑰加密,然後傳送至該 endpoint。
如果沒有任何 peer 的 AllowedIPs 涵蓋目的地,就不會傳送任何內容,因為沒有可用來傳送的金鑰。
ping: sendmsg: Required key not available這個錯誤只代表一件事:你嘗試連線的位址未列在任何 peer 底下。另一個錯誤 ping: sendmsg: Destination address required 則表示已比對到 peer,但 WireGuard 沒有該 peer 的 endpoint,因為未設定 endpoint,且尚未學習到 endpoint。
接著看入站方向。UDP 封包會抵達 listen port。WireGuard 會從標頭中的 receiver index 找出 session,依據滑動式重放視窗檢查 counter,然後解密並驗證 payload。完成這些步驟後,WireGuard 才會讀取內部封包,而該封包的來源位址必須位於傳送端 peer 的 AllowedIPs 範圍內。不符合時,封包會遭到丟棄。啟用 dynamic debug 後,核心會列印原因,格式如下:
wg0: Packet has unallowed src IP (10.8.0.9) from peer 2 (203.0.113.10:51820)這就是伺服器端 peer 會取得 /32 的原因。設定為 AllowedIPs = 10.8.0.2/32 的 peer 只能從 10.8.0.2 傳送封包,不能使用其他來源位址。若改在該處寫入 0.0.0.0/0,這個單一 client 就能注入來源位址為 tunnel 內任何位址的封包,包括其他 client 的位址。
重疊的前綴會依特定性解析,因為查找採用最長前綴比對。兩個 peer 使用相同前綴時,行為則不同:該項目會移至最後設定的 peer,而第一個 peer 會停止接收這些流量,且任何地方都不會列印錯誤。wg show wg0 allowed-ips 會列印核心實際使用的表格;當磁碟上的檔案與執行中的狀態不一致時,以這份表格為準。
以加密金鑰路由的角度讀取設定檔
伺服器端:
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = <server private key>
[Peer]
PublicKey = <laptop public key>
AllowedIPs = 10.8.0.2/32用戶端:
[Interface]
Address = 10.8.0.2/32
PrivateKey = <laptop private key>
[Peer]
PublicKey = <server public key>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25同一個關鍵字在兩端的作用完全不同。在用戶端,它表示「將所有目的地傳送給這個對等端」。在伺服器端,它表示「只接受來自這個對等端的這個位址」。這種不對稱性取決於設定值,而不是角色。
其中一半的金鑰根本不是通訊協定設定。Address、DNS、MTU、PostUp 和 SaveConfig 屬於 wg-quick,也就是負責啟動介面的 shell script。核心不會看到這些設定。wg-quick strip wg0 會輸出 wg 工具實際載入的精簡設定;這是最快確認兩者區分方式的方法。
握手實際執行的工作
WireGuard 的握手採用 Noise Protocol Framework 中的 Noise_IKpsk2。對系統管理員而言,IK 是其中關鍵的部分:發起端已預先知道回應端的靜態公開金鑰,因為該金鑰就是 [Peer] 區塊中的 PublicKey;發起端則會在第一則訊息中,以加密方式傳送自己的靜態公開金鑰。因此不需要交換憑證,也不需要來回傳遞身分資訊。被動監聽者無法判斷是哪個金鑰正在發起連線,除非它持有回應端的私密金鑰。
代價是需要 1 次往返。發起訊息為 148 bytes,回應為 92 bytes,之後便會立即傳送資料。每一端會在每次握手產生新的暫時性 Curve25519 金鑰組,工作階段金鑰則來自一連串 Diffie-Hellman 結果,並混合靜態金鑰與暫時性金鑰。之後會捨棄暫時性私密金鑰,因此可提供前向保密性:即使有人今天記錄你的網路流量,並在明年取得伺服器的私密金鑰,仍然無法解密當時記錄的資料。
握手發起訊息會攜帶 TAI64N 時間戳記。每個對等端都會記住從另一端收到的最大時間戳記,因此會拒絕重放的發起訊息。資料封包會攜帶作為 nonce 使用的 64-bit 計數器。接收端會保留最近收到的計數器滑動視窗,因此即使發生重放或大量重新排序,也不需要 TCP 式的連線狀態。
工作階段金鑰的有效時間不長,而且這些計時器是編譯時固定值,無法設定。
The data behind this chart
[
{
"label": "REKEY_TIMEOUT",
"seconds": 5,
"notes": "resend a handshake initiation that got no answer"
},
{
"label": "KEEPALIVE_TIMEOUT",
"seconds": 10,
"notes": "send a keepalive after receiving data and sending none back"
},
{
"label": "REKEY_ATTEMPT_TIME",
"seconds": 90,
"notes": "give up on the handshake and report the peer as down"
},
{
"label": "REKEY_AFTER_TIME",
"seconds": 120,
"notes": "sender begins a fresh handshake for a new session key"
},
{
"label": "REJECT_AFTER_TIME",
"seconds": 180,
"notes": "the old session key is refused and traffic stops"
}
]這些是通訊協定規格中的常數,不是測量結果。其中的 5 會驅動整個工作階段生命週期。使用 120 秒後,傳送端會開始新的握手;經過 180 秒後,則會直接拒絕使用舊金鑰,因此必須完成新的握手,網路流量才能恢復。若發起訊息沒有收到回應,系統會每隔 5 秒重新傳送,並在 90 秒後放棄。這就是為什麼 wg show 會將 latest handshake 顯示為相對時間,以及為什麼忙碌且健康的通道會維持較小的時間值。如果你正在持續傳送網路流量,但該時間值仍然增加,表示握手失敗,而不是通道處於閒置狀態。
為何 peer 沒有 client 或 server 角色
兩端執行完全相同的程式碼,並使用相同的設定格式。系統沒有 server 模式。你感受到的不對稱來自 Endpoint,而 Endpoint 是選用項目。
設定 endpoint 的 peer 可以發起 handshake。未設定 endpoint 的 peer 會等待,然後從第一個通過正確驗證的封包中取得對端的位址與連接埠。系統會儲存這個取得的 endpoint;每當從新的位址收到有效封包時,也會更新該 endpoint。這就是 roaming 的運作方式:筆記型電腦從 Wi-Fi 移動到行動網路時,仍可維持相同的 tunnel,因為工作階段是以 key 與 index 識別,而不是以 IP 位址識別。這不是重新建立連線,因為雙方從未以 TCP 的方式建立連線。
相同機制也會產生一項值得注意的事實:擁有公開位址的 peer 一律會保存對端最近已知的公開 IP,而 wg show 會顯示該位址。
固定原語,沒有協商空間
WireGuard 沒有加密套件清單。使用 ChaCha20-Poly1305 進行驗證式加密、Curve25519 進行金鑰協議、BLAKE2s 進行雜湊,以及 HKDF 進行金鑰衍生。每個部署都使用這些原語,因此沒有需要解析的協商階段,也沒有降級至較弱選項的途徑。這項取捨確實存在:如果其中一個原語遭到破解,修正方式是推出整個協定的新版本,並更新兩端,而不是變更設定。這項單一決策移除了 TLS 型通道大部分的程式碼與失效模式,而這正是 WireGuard 與 OpenVPN 的比較 的主要重點。
掃描器無法收到該連接埠的回應
每個握手訊息都包含名為 mac1 的欄位。這是訊息驗證碼(MAC),以衍生自回應端靜態公開金鑰的金鑰,對訊息計算而得。不知道該公開金鑰的傳送端無法產生有效的 mac1,接收端會直接丟棄這類封包,完全不回應。不會傳回錯誤、重設封包或 ICMP 訊息。
可觀察到的結果是 UDP 掃描完全收不到回應。
sudo nmap -sU -p 51820 vpn.example.comnmap 會回報 open|filtered。這與防火牆靜默丟棄封包時的結果相同。對於尚未持有公開金鑰的來源而言,無論 WireGuard 是否正在監聽,該連接埠的行為都一樣。
第二個欄位 mac2 用於處理阻斷服務壓力。接收端負載過高時,會對有效的啟動訊息回傳與傳送端來源位址繫結的 64-byte cookie 回覆。在傳送端回送該 cookie 之前,接收端不會執行成本高昂的公開金鑰運算。這能在消耗任何 CPU 資源前確認來源位址確實有效,而且只有在負載過高時才會啟用。
為什麼 0.0.0.0/0 會將 peer 變成預設路由
因為 AllowedIPs 是路由表,AllowedIPs = 0.0.0.0/0, ::/0 會讓該 peer 取得所有目的地的路由。這就是完整通道設定的全部內容。
讓它正常運作的路由機制,比這一行本身更值得注意。若直接透過 wg0 設定預設路由,就會形成迴圈。承載流量的加密 UDP 封包也必須離開這台機器,因此同樣會符合自己的預設路由。wg-quick 透過 policy routing 避免這個問題。它會使用 fwmark 標記 WireGuard 自身送出的封包,將通道的預設路由放在獨立的路由表中,並加入規則,讓只有未標記的流量進入該路由表。執行 ip rule show 即可看到結果:
32764: from all lookup main suppress_prefixlength 0
32765: not from all fwmark 0xca6c lookup 51820
32766: from all lookup main0xca6c 是 51820 的十六進位表示,而 51820 也是路由表編號。suppress_prefixlength 0 規則會讓主路由表略過自己的預設路由,因此本機子網路等更具體的路由仍會優先使用,其餘流量則會落到通道路由表。分割通道不需要這些設定:像 AllowedIPs = 10.8.0.0/24, 10.20.0.0/16 這種範圍較窄的清單,會直接成為主路由表中的一般路由。
完整通道本身無法解決名稱解析問題,因為用戶端從本機網路取得的 resolver 通常仍會沿用,而且其路由通常更具體。這是另一項工作,詳見 會從 WireGuard 通道外洩漏的 DNS。
PersistentKeepalive 的實際用途
WireGuard 在沒有流量時不會傳送任何資料。沒有 heartbeat、沒有工作階段更新,也不會在網路上傳送封包。這種靜默狀態有助於節省電力,也能避免上述的掃描情境,但會導致某一種特定架構無法運作。
位於 NAT(network address translation)或 stateful firewall 後方的 peer,只有在該設備中存在連線對映時,外部才能連入;而這個對映是由傳出的封包建立。常見的 UDP 對映存留時間約從 30 秒開始。對映過期後,中間設備會丟棄來自公開側的封包,隧道會看似失效,直到 NAT 後方的 peer 傳送資料為止。PersistentKeepalive = 25 每 25 秒傳送一個經過驗證的空封包,低於常見的最短存留時間,因此能讓對映維持開啟。
請在 NAT 後方的 peer 上設定此選項。具有公開位址且 UDP 埠已開放的伺服器不需要此設定,在該處設定只會增加流量。不要將它與自動 keepalive 混淆。自動 keepalive 會在 peer 收到資料後,經過 10 秒且自身沒有資料要傳回時觸發。此機制一律啟用,無法設定。
透過通道路由 LAN 並不是 WireGuard 的功能
假設 peer B 位於家用網路 192.168.50.0/24,而 peer A 必須存取該網路。這需要兩個獨立系統共同設定,其中只有一個是 WireGuard。
WireGuard 的部分:在 A 上,將 192.168.50.0/24 加入 B 的 AllowedIPs。這會讓 A 將該前綴的路由指向 B,也會讓 A 接受 B 傳來、來源位址屬於這些前綴的封包。若未設定,cryptokey routing 便沒有目的地的金鑰,也沒有來源位址的許可。
核心的部分:在 B 上,net.ipv4.ip_forward 必須設為 1,否則核心會丟棄所有目的地不是 B 本身的解密封包。B 的防火牆 forward chain 也必須允許這些流量。LAN 上的主機需要有返回 10.8.0.0/24 的路由,否則 B 必須套用 source NAT,讓回應經由 B 返回。
WireGuard 將解密後的封包交給核心後,工作就結束了。之後的處理都是一般的 Linux 路由與封包過濾。因此,這類故障會出現在 nft list ruleset 計數器或 ip -s link show wg0 中,而不是 wg show 中。若要透過 Web 介面管理 peers,可以 在 Docker 中執行 wg-easy,由它產生 peer 設定項目;不過轉送規則仍由主機負責。
WireGuard 為何在核心中運作
wg0 是網路裝置驅動程式。封包會透過一般路由堆疊抵達該裝置,在 softirq context 中加密,接著透過 UDP socket 傳出,全程不會進入 userspace。這就是其高傳輸量的原因,也是該模組能維持約 4000 行程式碼的原因。程式碼規模足夠小,便於審查,並在 2020 年 3 月併入 mainline Linux 5.6。Ubuntu 24.04 和 Debian 13 都已隨附該模組,因此只缺少 wireguard-tools 套件。
作為一般網路介面,這會帶來實際影響。tcpdump -ni wg0 會顯示明文的內部封包,而 tcpdump -ni eth0 udp port 51820 會顯示加密後的外部封包。比較兩者即可立即判斷哪個方向發生故障。netfilter 和流量整形會將 wg0 視為其他連結介面處理。核心模組無法使用時,例如使用主機核心的容器虛擬化環境,wireguard-go 會透過 TUN 裝置在 userspace 中實作相同的通訊協定。由於每個封包都會兩次跨越核心邊界,傳輸量會明顯降低。
WireGuard 無法保護你的事項
這個威脅模型刻意設定得很窄,而如此安靜的通訊協定很容易讓人抱持不切實際的期待。請直接說清楚。
- 它不會隱藏你正在使用 WireGuard。握手訊息的大小固定,第一個位元組會指出訊息類型,而傳輸使用 UDP。深度封包檢測很容易辨識它,不接受 VPN 的網路也能封鎖它。混淆功能是刻意排除的。
- 它不會隱藏流量大小或傳輸時間。承載資料只會填補到 16 位元組邊界,因此觀察者仍能看出你何時傳送資料,以及大致的資料量。
- 它會保留最後已知的端點。具有公開位址的 peer 會儲存另一端目前的公開 IP,而
wg show會顯示該位址。再加上設定檔中固定的 tunnel 位址,這會形成一個跟著使用者在不同網路間移動的穩定識別碼。在你自己的 VPS 上這沒有問題。這也是商業服務會在通訊協定之上再加一層的原因。 - 它驗證的是金鑰,而不是人員。持有私密金鑰檔案的人就是該 peer。請將
/etc/wireguard設為 mode 700,並將金鑰檔案設為 600。 - 它沒有撤銷清單,也沒有到期機制。只有從持有該 peer 設定的每台伺服器刪除 peer 項目,才能終止存取權;靜態金鑰則會一直有效,直到你將其移除。
這些限制不代表 WireGuard 不安全。它代表 WireGuard 維持精簡,而精簡正是其目的:它負責驗證與加密,並將身分管理及位址配置交給你在其上建構的功能。WireGuard 與 Tailscale 的比較所述的協調層,正是用來填補這個缺口,並使用你剛讀到的相同資料平面。配置這類層之後,下一個決定是哪些人可以連線到你在 tunnel 內執行的服務;對單一連接埠而言,Tailscale serve 與 funnel 的選擇會處理這項決定。
FAQ
WireGuard 中的 cryptokey routing 是什麼?
Cryptokey routing 是將每個封包繫結至公開金鑰的規則。每個 peer 項目都會在 AllowedIPs 中包含一份前綴清單。對外傳送時,WireGuard 會將封包的目的位址與每個 peer 的清單比對,並優先採用最長前綴,因此這份清單會作為路由表。對內接收時,封包解密並完成驗證後,其內部來源位址必須落在同一個 peer 的清單內,否則會遭到丟棄,因此這份清單也會作為存取控制清單。WireGuard 不需要獨立的路由設定,也不需要獨立的內部防火牆,因為同一份清單同時執行這兩項工作。
兩端都需要設定 PersistentKeepalive 嗎?
不需要。請在位於 NAT(網路位址轉譯)或有狀態防火牆後方的一端設定,通常是用戶端。WireGuard 閒置時不會傳送任何資料,因此讓遠端端點連回該 peer 的對映通常會在 1 分鐘內失效,接著通道會呈現單向中斷。PersistentKeepalive = 25 每 25 秒傳送一個經過驗證的空封包,讓對映保持開啟。具有公開位址且 UDP 埠已開放的 peer 不需要設定此選項。
為什麼透過通道執行 ping 會顯示「Required key not available」?
因為目的位址不在任何 peer 的 AllowedIPs 內,所以 cryptokey routing 找不到可用來加密封包的金鑰,核心便拒絕傳送。請執行 wg show wg0 allowed-ips,並將輸出內容與您要 ping 的位址比對。類似的錯誤 Destination address required 是另一個問題:系統已比對到 peer,但 WireGuard 沒有該 peer 的 endpoint,原因是尚未設定 endpoint,且該 peer 尚未傳入任何經過驗證的封包。
防火牆能偵測並封鎖 WireGuard 嗎?
可以。WireGuard 會驗證並加密您的網路流量,且不會嘗試隱藏自身特徵。Handshake 訊息固定為 148 和 92 bytes,每則訊息的第一個 byte 會識別其類型,而傳輸協定是 UDP,因此深度封包檢測可以輕易識別此協定。封鎖 UDP 或對協定進行指紋辨識的網路會阻止 WireGuard。若要隱藏通道,必須將其包裝在其他協定或工具中;這是獨立的工具,不是 WireGuard 的設定。