mullvad

Stars and mentions

Last 30 days, Oct 4 – Nov 2

GitHub stars0
mentions3
01234Oct 4Oct 18Nov 2
UTC days · repositories mullvad owns0 repositories

Package downloads

No package download history available yet.

Top repositories

mullvad’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside mullvad’s account where mullvad opened or reviewed pull requests in the past 365 days.

Who noticed mullvad

Everyone who starred or forked mullvad’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

3posts
Nov 30 posts
  1. Mullvad spun their own implementation off of boringtun as well (called gotatun[0]) [0] mullvad/gotatun

  2. VPN Kill Switch Test From a Browser Tab: What Polling Can and Can't Prove TL;DR - A browser can't see the firewall rules behind a VPN kill switch. It only sees answers: which address a server saw, or no reply at all. - Sampling leaves gaps, and a leak shorter than the gap between two samples can vanish completely. - No reply is not a pass. It only proves nothing came back. You click Quit in the VPN app, and the tab that was supposed to record the leak is now behind it. That is the basic problem with any VPN kill switch test run from a web page: to test the kill switch you have to break the tunnel on purpose, and breaking it takes you away from the tab doing the measuring. That detail shaped the VPN kill switch test on MyIPScan. I built MyIPScan, and this post covers how a browser-based recorder works and what it cannot tell you. What a kill switch actually is (and why a browser can't see it) A kill switch lives below the browser. Mullvad's open-source app is a good reference because its security document spells it out. The app integrates with the system firewall: "This means WFP on Windows, PF on macOS and nftables on Linux." When it cannot build a tunnel, it enters an error state that "locks the firewall so no traffic can flow (except the always active exceptions)." The same document separates two things people often merge. The app's "kill switch" is "the fact that the connecting, disconnecting and error states prevent leaks via firewall rules." The "Lockdown mode" setting, which the document says is "regularly misunderstood as the kill switch," only decides whether the disconnected state blocks traffic too. None of this is visible from JavaScript. A page can only send requests and record what happened to them. How the VPN kill switch test records a drop The test runs in three steps, and the first two exist to give the recorder something to compare against: 1. Capture your own network. Disconnect the VPN completely and record the address your ISP gives you. 2. Capture the tunnel. Connect again and record the VPN's exit address. 3. Force the drop. Start recording, then break the tunnel the way it breaks in real life: quit the VPN app or disable the network adapter. Run it on a desktop: a phone's background tab will likely stop sampling. Each reading is one request to the site's own /api/ip-sample endpoint, which, in the page's words, "sees the address the request arrives from and sends it back with its AS number, provider name and country." The loop asks for a sample every 1.5 seconds, and every 0.6 seconds for twelve samples after anything changes, so transitions get finer resolution than the quiet stretches. Every sample lands in one of four buckets: | Outcome | Meaning | Counted as exposure? | |---|---|---| | No reply | Nothing came back | No | | Tunnel address | Matches step 2 | No | | Your own address | Matches step 1 | Yes | | Some other address | Matches neither | No, timed separately | Only "your own address" counts as a leak. A different VPN server after a reconnect is not your ISP, so it gets its own timer. Below is my illustration of how such a loop can be written, not the site's code: const samples = []; async function takeSample() { const t = performance.now(); try { const res = await fetch('/api/ip-sample', { cache: 'no-store', // never answer from cache signal: AbortSignal.timeout(3000), // a hung request is a result too }); const { ip } = await res.json(); samples.push({ t, ip, hidden: document.hidden }); } catch { samples.push({ t, ip: null, hidden: document.hidden }); // "no reply" } } A recorder should request with cache: 'no-store', which makes the browser fetch "from the remote server without first looking in the cache" (MDN, RequestInit); a cached answer could show the tunnel address after the tunnel is gone. It should also note document.hidden per sample, to explain later why the gaps grew. Why the gap you ask for is not the gap you get The loop asks for 1.5 seconds, or 0.6 seconds in fast mode. The browser decides what you actually get. According to MDN's setTimeout page, "Firefox Desktop has a minimum timeout of 1 second for inactive tabs," and Firefox for Android has a minimum of 15 minutes for inactive tabs "and may unload them entirely." Chrome throttles hidden-tab timers too, and the MyIPScan page sums up the desktop case as browsers clamping "background timers to about one per second." Chrome also has intensive throttling, introduced in Chrome 88, where timers are checked once per minute. MDN lists its conditions: nesting count of 5 or higher, the page invisible for more than 5 minutes, silent for more than 30 seconds, and WebRTC inactive. It only applies after 5 minutes hidden, so it is not relevant to a seconds-long test. Put the one-second clamp next to step 3. Forcing a drop means switching to the VPN app, so the measuring tab is hidden at the moment you care about. The 0.6-second fast mode gets clamped to about 1 second while the tab is hidden, and slow requests stretch the spacing further. Each request's round-trip time also adds to the timing uncertainty. So the page says the recorder reports "the gap it achieved rather than the gap it wanted, and says how long the tab was hidden." If the achieved gap was, say, 2.0 seconds, then any statement about leaks shorter than that is off the table. Turning samples into a window with two bounds For a run of consecutive "your own address" readings, there are two defensible numbers: - Lower bound: the page defines it as "the span actually observed, from the first matching sample to the last." The leak lasted at least this long. - Upper bound: the widest window the neighbouring samples still allow. The leak could have started just after the previous sample and ended just before the next one. Again, my illustration, not the site's code: function exposureBounds(samples, i, j) { // i..j = matching run const lower = samples[j].t - samples[i].t; const before = samples[i - 1]?.t ?? samples[i].t; const after = samples[j + 1]?.t ?? samples[j].t; return { lower, upper: after - before }; } The page's own example output is "at least 3.6s, at most 4.8s". With 0.6-second fast-mode sampling on both sides of the run, 3.6 + 2 × 0.6 = 4.8, which is consistent with the page's example. A single number like "4.2 seconds" would look more precise and be less true. What polling can't prove Silence is not success. A "no reply" sample fits a working kill switch, but also Wi-Fi reassociating, DNS failing, or a timeout. The page puts it as: "Samples that get no reply show that traffic was not moving, not who stopped it." Short leaks fall between samples. If the achieved gap is 1.5 seconds, a 400 ms window of unprotected traffic can sit entirely between two requests and never appear. The page says it directly: "A window shorter than that gap can be missed entirely." Zero matches means "none observed at this resolution", not "none occurred". It sees HTTP from one tab, not the whole machine. Background apps, an OS update service, a torrent client, DNS through another resolver: any of that can leak without touching the tab's requests. It errs toward under-reporting. If your own address changed after you captured it (the page names "a reconnected router, a mobile handover, carrier-grade NAT"), "a genuine exposure can land in the 'some other address' column instead." The error stays in the conservative direction. One run is one run. The page: "It is one session, one device, one browser and one moment," and "It is not a test of any VPN application." Nobody should publish "Brand X has no kill switch" from a single recording. On privacy, every sample is a request, so the server sees your address each time it answers. The page states that "Readings are not logged or stored on the server," that the recording "stays in this tab until you close it," and that to name the provider "only the AS number is looked up (RIPEstat), never your address." It also offers a switch to show full addresses instead of masked ones. Where this leaves a browser-based test Polling from a tab is a decent smoke test: it can catch a leak that lasts seconds and time it with error bars. It cannot certify a kill switch. For that, capture packets at the interface (Wireshark or tcpdump on the physical adapter while you kill the VPN), because that is where the firewall rules act. If you build anything like this, keep one rule: report the resolution you achieved next to every result, and never let a missing answer count as a passing one.

  3. Reddit · · r/mullvadvpn · u/crumbl3r · mullvad/mullvadvpn-app

    Custom DNS DoH Hi Mullvad Team, I understand that custom DNS queries are already encrypted inside the VPN tunnel. However, that protection ends at the exit server, where traditional DNS traffic to external resolvers continues unencrypted. This creates additional privacy and security risks: - Traffic correlation: Services like Control D assign unique Legacy IPv6 DNS addresses to individual endpoints. These persistent identifiers can allow observers to link DNS activity across different VPN exit servers and sessions. - DNS confidentiality: Network operators between the VPN exit and DNS provider can inspect DNS queries, even when Multihop and DAITA are enabled. - DNS integrity: Unencrypted DNS traffic remains vulnerable to interception and manipulation. An additional concern is the potential contradiction with DAITA’s privacy objectives. DAITA is specifically designed to make traffic analysis and correlation more difficult. However, using a unique Legacy IPv6 DNS address introduces a persistent identifier at the VPN exit, potentially undermining part of that protection. For example, if a Control D endpoint is associated with an identifiable user account, an observer with access to DNS traffic recordings from Mullvad exit servers could potentially link previously observed DNS queries to that account, even if Control D itself stores no DNS query logs. This is particularly relevant to Multihop: while it separates the user’s real IP address from exit traffic, a persistent, endpoint-specific DNS address can still allow observers to link DNS activity across otherwise separate VPN exit servers and sessions. This does not directly reveal the user’s real IP address, but it creates an avoidable correlation point that DAITA cannot protect against, since its traffic analysis defenses operate on the VPN entry connection. Custom DoH would address these risks by encrypting queries to the DNS provider and protecting endpoint identifiers embedded in HTTPS URLs. I understand that DoH cannot prevent all traffic analysis, but it would eliminate an unnecessary source of identifiable metadata. Could you reconsider implementing optional custom DoH support, particularly on iOS, while preserving Multihop and DAITA? This would meaningfully improve privacy for users who rely on external DNS providers with custom filtering rules. mullvadvpn-app#9121

    3 points · 1 comment
Trending awards, every completed day and week
Gold
01st place finishes
Silver
02nd place finishes
Bronze
03rd place finishes
Awarded repositories
0Owned by mullvad now

No trending awards yet

None of the repositories mullvad owns has finished in the top three of a completed trending day or week, by stars, forks or PR authors. See who is trending now.

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.