- Mullvad는 서버 하나에 여러 exit IP를 두지만, WireGuard 키 기반으로 결정적으로 배정해 접속마다 무작위로 바뀌지 않음
- 9개 서버에서 pubkey를 반복 변경해 모은 3,650개 데이터 포인트는 가능한 8.2조 개 조합 중 284개 조합에만 배정됨
- 각 서버의 exit IP는 풀 안에서 비슷한 백분위 위치에 놓이며, 한 조합은 여러 서버에서 대체로 81번째 백분위에 맞춰짐
- 원인은 pubkey나 터널 주소를 seed로 쓰고 풀 크기를 상한으로 넣는 seed 기반 RNG로 exit IP index를 고르는 구조로 보임
- IP 로그의 float 범위가 겹치면 서로 다른 Mullvad 서버를 써도 계정 간 상관관계가 가능해져 익명성 위험이 커짐
Mullvad exit IP가 식별 벡터가 되는 구조
- Mullvad는 서버 하나에 여러 exit IP를 제공하며, 같은 서버에 연결한 두 사용자가 보통 서로 다른 공개 IP를 받음
- 서버 수는 578대로, Proton VPN의 20,000대보다 적어 한 IP에 사용자를 몰아넣지 않는 수직 확장이 과도한 IP 차단·속도 제한을 피하는 데 유리함
- 하지만 서버에 연결할 때마다 exit IP가 무작위로 바뀌지 않고, WireGuard 키를 기반으로 결정적으로 선택됨
- WireGuard 키는 1~30일마다 회전하지만, 서드파티 클라이언트에서는 회전하지 않음
- 서버마다 독립적으로 고정 exit IP가 배정된다면, 몇 개 서버의 IP 조합만으로 다른 Mullvad 사용자와 구분될 수 있음
9개 서버에서 관찰한 exit IP 조합
- pubkey를 반복 변경하며 9개 서버의 exit IP를 수집하는 스크립트를 밤새 실행해 3,650개 pubkey 데이터 포인트를 얻음
- 각 서버의 exit 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조 개 이상의 exit IP 조합이 가능해 보임
- 실제 테스트된 모든 pubkey는 그중 284개 조합 중 하나에만 배정됨
- 가능한 조합 수에 비해 관찰된 조합 수가 매우 적어, 서버별 IP 선택이 독립적이지 않다는 단서가 됨
서로 다른 IP가 같은 백분위에 놓이는 패턴
- exit IP는 풀 시작 IP에서 얼마나 떨어져 있는지로 숫자 위치를 계산할 수 있음
- 예를 들어
au-syd-wg-101에서103.136.147.53은103.136.147.5부터 세면 1-based index가 49임 - 관찰된 조합의 IP 위치를 각 서버의 풀 크기로 나누면, 서로 다른 서버에서도 거의 같은 비율이 나타남
| Server | IP | Position | Pool size | Ratio |
|---|---|---|---|---|
| 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는 모든 서버에서 서로 이웃한 위치의 exit IP만 배정하는 것처럼 보임
seed 기반 난수 선택으로 보이는 원인
cl-scl-wg-001과za-jnb-wg-002는 관찰된 284개 IP 조합 전체에서 항상 같은 IP index를 공유함- 두 서버의 공통점은 풀 크기 11이며, 동일한 seed와 동일한 bounds를 가진 난수 호출이 같은 결과를 내는 구조와 맞음
- 정적 seed로 RNG를 초기화한 뒤 같은 범위에서 난수를 뽑으면 같은 결과가 반복됨
- Mullvad는 pubkey 또는 터널 주소를 seed로 쓰고, 풀 크기를 상한값으로 넣는 seed 기반 RNG로 exit IP index를 고르는 것으로 보임
- bounds가 바뀌어도 RNG의 엔트로피 풀은 영향을 받지 않으며, Rust에서는 첫 호출에서 같은 float가 생성되어 bounds에 곱해지는 방식과 맞아떨어짐
- 이 동작은
min + round((max - min) * float)처럼 단순화해 볼 수 있지만, 크게 단순화한 표현일 수 있음 - 이런 특성 때문에 풀 크기가 달라도 같은 seed에서 나온 float가 각 서버 풀의 비슷한 비율 지점으로 매핑됨
- Mullvad 클라이언트가 Rust로 작성되어 있어 Rust가 백엔드 언어일 가능성도 있지만, 이는 클라이언트 구현에 근거한 판단에 그침
random_range가 bounds 변화에 따라 어떻게 동작하는지 정확히 예상하기 어렵고, bounds 증가가 엔트로피와 섞여 다른 수를 만들 것이라고 생각하기 쉽지만 실제 관찰과는 다름
IP 로그 상관관계로 생기는 익명성 위험
- 특정 IP 조합에서 가능한 float 값의 최솟값과 최댓값을 추정하는 도구가 mullvad-seed-estimator로 제공됨
- 스크린샷의 특정 IP 집합은 0.2909~0.2943 사이의 float 값으로 해석되며, 차이는 0.0034임
- 이는 Mullvad 사용자 중 0.34% 가 이 IP들을 공유한다는 뜻이고, 활성 사용자 100,000명이라는 대략적 추정에서는 340명에 해당함
- 처음 예상한 수준만큼 고유하지는 않지만, 99% 이상 정확도는 낮지 않음
- 예를 들어 포럼 관리자가 전날 차단한 사용자의 sockpuppet으로 의심되는 새 계정을 확인할 때, 두 계정이 서로 다른 Mullvad 서버를 써도 IP 로그가
0.4334 - 0.4428과0.4358 - 0.4423처럼 겹치는 float 범위에 있으면 같은 사람일 확률이 99%를 넘음 - 데이터 유출이나 법적 절차로 확보한 IP 로그에도 같은 방식의 상관관계를 적용하면, VPN 뒤의 사용자가 익명성을 잃을 수 있음
보호 방법
- pubkey 하나당 서버를 한 번 이상 바꾸지 않는 것이 권장됨
- Mullvad 앱에서 로그아웃해 pubkey를 강제 회전할 수 있음