开发者生态
morning
Mulvad 退出 IP 的识别能力令人惊讶
2026-05-15
1 阅读
RGBCube
Mulvad 退出 IP 作为指纹识别向量 2026-05-14 Mulvad 是为数不多的为其服务器提供多个退出 IP 的 VPN 提供商之一。如果两个人连接到同一台服务器,他们通常会得到不同的公共 IP。由于只有 578 台服务器(与 Proton VPN 的 20,000 台服务器相比),这种垂直扩展是有意义的,可以避免将太多用户塞到一个 IP 上,这对于具有过度热心 IP 阻止和速率限制的网站来说将是一个问题。令人惊讶的是,每次连接到服务器时,给定的退出 IP 并不是随机的,而是根据您的 WireGuard 密钥确定性地选择,该密钥每 1 到 30 天轮换一次(除非您使用第三方客户端,在这种情况下它永远不会轮换)。但是等等..如果每台服务器都为您分配一个独立选择的静态退出IP,难道仅仅其中的一些就足以在所有其他Mullvad 用户中唯一地识别您吗?为了进行测试,我编写了一个脚本,该脚本反复更改我的公钥并获取一组 9 个服务器的退出 IP。让其运行一晚会产生 3650 个 pubkey 的数据点,这足以绘制出每个服务器的退出 IP 范围: Hostname Start IP End IP # IPs au-syd-wg-101 103.136.147.5 103.136.147.64 60 cl-scl-wg-001 149.88.104.4 149.88.104.14 11 de-ber-wg-007 193.32.248.245 193.32.248.252 8 dk-cph-wg-002 45.129.56.196 45.129.56.226 31 fi-hel-wg-201 185.65.133.10 185.65.133.75 66 us-lax-wg-001 23.234.72.36 23.234.72.126 91 us-nyc-wg-602 146.70.168.132 146.70.168.190 59 us-sjc-wg-302 142.147.89.212 142.147.89.224 13 za-jnb-wg-002 154.47.30.145 154.47.30.155 11 这些服务器的池大小总计超过 8.2 万亿个退出 IP 组合,因此您认为每个 pubkey被分配一个独特的 IP 组合,因为冲突的可能性非常低。然而,不知何故,我测试的所有公钥都只分配了 284 种组合中的一种。这是怎么回事?不同的 IP,相同的比例 您可以通过计算退出 IP 与池起始 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 位置 池大小比率 au-syd-wg-101 103.136.147.53 49 60 0.816 cl-scl-wg-001 149.88.104.12 9 11 0.818 de-ber-wg-007 193.32.248.251 7 8 0.875 dk-cph-wg-002 45.129.56.220 25 31 0.806 fi-hel-wg-201 185.65.133.63 54 66 0.818 us-lax-wg-001 23.234.72.109 74 91 0.813 us-nyc-wg-602 146.70.168.179 48 59 0.813 us-sjc-wg-302 142.147.89.222 11 13 0.846 za-jnb-wg-002 154.47.30.153 9 11 0.818 每个 IP 都位于其池的同一百分位内,在本例中为第 81 个。这解释了组合数量有限的原因,Mullvad 只会在其所有服务器上分配相邻的退出 IP。但为什么?功能还是错误?奇怪的是,服务器 cl-scl-wg-001 和 za-jnb-wg-002 在所有 284 个观察到的 IP 组合中始终彼此共享 IP 索引。它们的共同点是池大小为 11,这为我们提供了有关正在发生的情况的线索。在任何语言中,如果您使用静态种子启动 RNG,则具有相同边界的 rand 调用将始终产生相同的结果: use rand::{Rng, SeedableRng};使用 rand::rngs::StdRng; fn main () { 让种子 = 1234 ; for _ in 1 .. 100 { 让 mut rng = StdRng::seed_from_u64(seed);让数字 = rng.random_range( 0 .. 1000 );打印! ( " {} " , number) // 将始终打印 56 } } 因此,这两个服务器之间的共享索引表明 Mullvad 可能正在使用某种基于种子的 RNG 来选择退出 IP 索引,其中上限参数是池大小。这相当简单,但是当边界改变时会发生什么?使用 rand::{Rng, SeedableRng};使用 rand::rngs::StdRng; fn main () { 让种子 = 12345 ;对于 10 .. 100 的界限 { let mut rng = StdRng::seed_from_u64(seed);让 number = rng.random_range( 0 .. 边界);让ratio = number as f64 /bound as f64 ;打印! ( " {} {:.3} " , 数字, 比率) } } 5 0.500 5 0.455 6 0.500 6 0.462 7 0.500 7 0.467 8 0.500 9 0.529 9 0.500 10 0.526 10 0.500 11 0.524 11 0.500 12 0.522 12 0.500 13 0.520 13 0.500 14 0.519 14 0.500 15 0.517 ... 事实证明,RNG 的熵池不受您提供的边界的影响,并且至少在 Rust 中,每个上都会生成相同的浮点数第一次调用并用作边界的乘数比例,如下所示: min + round((max - min) * float) (这可能是一个巨大的过度简化)这与我们在 Mulvad 的退出 IP 选择算法中看到的行为一致,因此可以肯定地说这就是其原因。考虑到客户端也是用 Rust 编写的,Rust 作为后端语言也是有意义的。问题是,我的程序员朋友几乎没有人能够准确描述 random_range 在第二个代码片段中会产生什么,而实际行为也让我感到惊讶。是有原因的