Mullvad 是少數提供多個伺服器出口 IP 的 VPN 服務商之一。如果兩個人連接到同一個伺服器,他們通常會獲得不同的公開 IP 位址。
相較於 Proton VPN 的 20,000 個伺服器,Mullvad 僅有 578 個伺服器,這種垂直擴展模式有助於避免過多用戶擠在同一個 IP 上,這在對 IP 封鎖和速率限制較嚴格的網站上會是個問題。
令人驚訝的是,每次連接到伺服器時,你獲得的出口 IP 並非隨機分配,而是根據你的 WireGuard 金鑰(pubkey)確定性地選取,該金鑰每 1 到 30 天輪換一次(除非你使用第三方客戶端,這種情況下它永遠不會輪換)。
等等… 如果每個伺服器都為你分配一個獨立選取的靜態出口 IP,那麼其中幾個 IP 是否足以在所有 Mullvad 用戶中唯一識別出你?
我編寫了一個腳本,不斷更改我的 pubkey 並為一組 9 個伺服器獲取出口 IP。讓它運行一夜,產生了 3650 個 pubkey 的數據點,足以繪製出每個伺服器的出口 IP 範圍:
這些池的大小加起來超過 8.2 兆兆個出口 IP 組合,所以你會認為每個 pubkey 都會被分配到一個獨特的 IP 集合,因為發生碰撞的機率極低。
然而,我測試的 3650 個 pubkey 都只被分配到 284 種組合中的一種。
你可以透過計算一個出口 IP 相對於池中起始 IP 的距離來計算其數值位置。
例如,由 au-syd-wg-101 分配的 IP 103.136.147.53,其基於 1 的索引將是 49(X.X.X.53 - X.X.X.5 + 1)。
現在,如果你採用上面連結的 284 種組合中的任何一種的 IP 位置,並將其除以池的大小,就會出現一個常見的比例:
每個 IP 都落在其池中的相同百分位,在此例中是第 81 個百分位。
這解釋了組合數量有限的原因,Mullvad 只會在所有伺服器上分配鄰近的出口 IP。但為什麼呢?
奇怪的是,cl-scl-wg-001 和 za-jnb-wg-002 這兩個伺服器在所有觀察到的 284 種 IP 組合中,始終共享彼此的 IP 索引。
它們的共同點是池大小為 11,這給了我們關於發生了什麼的線索。
在任何程式語言中,如果你用一個靜態種子初始化一個 RNG,使用相同範圍調用的 rand-between 總會產生相同結果:
因此,這兩個伺服器之間的共享索引表明 Mullvad 可能正在使用某種基於種子的 RNG 來選擇出口 IP 索引,其中種子是 pubkey(或可能是隧道位址),而上限參數是池大小。
這相當直接,但當邊界改變時會發生什麼?
結果是,RNG 的熵池不受你提供的邊界影響,至少在 Rust 中,每次第一次調用都會生成相同的浮點數,並作為乘數應用於邊界,如下所示:min + round((max - min) * float)(這可能是一個巨大的簡化)。
這與我們在 Mullvad 出口 IP 選擇演算法中看到的行為一致,因此可以安全地說這就是原因。
考慮到客戶端是用 Rust 編寫的,Rust 作為後端語言也是合理的。
問題是,我幾乎沒有程式設計師朋友能夠準確描述第二個程式碼片段中的 random_range 會產生什麼,實際行為也讓我感到驚訝。可以合理地認為,邊界的每次增加都會與熵產生偏差並導致不同的數字,儘管事實並非如此。
Mullvad 的開發者是否也分享了這種常見的誤解,同時實際上打算讓出口 IP 組合數量不受限制?我不知道,但這是一個有趣的想法。
我製作了一個工具,可以推斷給定 IP 組合的最小和最大浮點數值,網址為 https://tmctmt.github.io/mullvad-seed-estimator/ 。
螢幕截圖中的這一組 IP,對於一個 0.0034 的差異,解析出的浮點數值介於 0.2909 和 0.2943 之間,這意味著 0.34% 的 Mullvad 用戶共享這些 IP。粗略估計有 100,000 名活躍的 Mullvad 用戶,這相當於 340 名用戶。
這肯定不像我最初認為的那樣獨特,但同時,>99% 的準確性真的很糟糕嗎?
舉個例子,想像你是一個論壇版主,你懷疑一個新出現的帳號實際上是你前一天封鎖的用戶的小號。你檢查 IP 日誌,儘管使用了不同的 Mullvad 伺服器,但兩個帳號都解析到重疊的浮點數範圍 0.4334 - 0.4428 和 0.4358 - 0.4423。這給你提供了 >99% 的機率表明他們是同一個人。
現在將此應用於透過數據洩露和合法管道獲得的 IP 日誌,你就可以看到如何透過類似的關聯攻擊,在 VPN 保護下被去匿名化。