<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mak Atomic</title>
    <description>The latest articles on DEV Community by Mak Atomic (@marupxyyz01).</description>
    <link>https://dev.to/marupxyyz01</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4188828%2F72c2b7a2-5ba0-4fd4-ac1a-02178c0c36f4.jpg</url>
      <title>DEV Community: Mak Atomic</title>
      <link>https://dev.to/marupxyyz01</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/marupxyyz01"/>
    <language>en</language>
    <item>
      <title>How to Test Geo-Dependent Features in Your App</title>
      <dc:creator>Mak Atomic</dc:creator>
      <pubDate>Tue, 27 Oct 2026 09:38:51 +0000</pubDate>
      <link>https://dev.to/marupxyyz01/how-to-test-geo-dependent-features-in-your-app-1d5m</link>
      <guid>https://dev.to/marupxyyz01/how-to-test-geo-dependent-features-in-your-app-1d5m</guid>
      <description>&lt;p&gt;Your app shows prices in euros to visitors from Germany, hides a feature in one country, and shows a cookie banner only in the EU. You tested it by overriding the location in Chrome DevTools. It passed. In production, a user in Berlin still sees dollars.&lt;/p&gt;

&lt;p&gt;I've hit this pattern more than once, and the cause is always the same: the DevTools override changes one signal of location, and the server reads a different one. Below is the checklist I use now.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I work on &lt;a href="https://atomicvpn.io/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=geo-testing" rel="noopener noreferrer"&gt;Atomic VPN&lt;/a&gt;. The method works with any VPN or proxy that has exit servers in the countries you need, and I say where a VPN is the wrong tool at the end.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The four signals that mean "where is this user"
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Who reads it&lt;/th&gt;
&lt;th&gt;How I fake it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Geolocation API (&lt;code&gt;navigator.geolocation&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Frontend JS, after permission&lt;/td&gt;
&lt;td&gt;DevTools Sensors, Playwright &lt;code&gt;geolocation&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client IP&lt;/td&gt;
&lt;td&gt;Server, CDN, WAF&lt;/td&gt;
&lt;td&gt;VPN, proxy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Accept-Language&lt;/code&gt;, &lt;code&gt;Intl&lt;/code&gt; locale, timezone&lt;/td&gt;
&lt;td&gt;Server and frontend&lt;/td&gt;
&lt;td&gt;Playwright &lt;code&gt;locale&lt;/code&gt;, &lt;code&gt;timezoneId&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS resolver location&lt;/td&gt;
&lt;td&gt;CDNs that route by resolver&lt;/td&gt;
&lt;td&gt;A VPN with its own DNS, or change the resolver&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most "works on my machine" geo bugs come from testing one row and shipping logic that depends on another.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the DevTools override actually does
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://developer.chrome.com/docs/devtools/sensors" rel="noopener noreferrer"&gt;Sensors panel&lt;/a&gt; lets you pick a preset city, enter coordinates, or simulate "location unavailable". It overrides what the page gets from the Geolocation API. That's right for a store locator or a weather widget.&lt;/p&gt;

&lt;p&gt;It does not change the IP address of your requests. If your backend calls a GeoIP database, reads the &lt;code&gt;CF-IPCountry&lt;/code&gt; header that &lt;a href="https://developers.cloudflare.com/network/ip-geolocation/" rel="noopener noreferrer"&gt;Cloudflare adds&lt;/a&gt;, or your CDN applies country rules, all of them still see your real IP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: check what the internet thinks my location is
&lt;/h2&gt;

&lt;p&gt;Before any test, I check the exit IP and country. Cloudflare exposes a plain-text endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://www.cloudflare.com/cdn-cgi/trace | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^(ip|loc|colo)='&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get three lines: your IP, a two-letter country code (&lt;code&gt;loc&lt;/code&gt;), and the Cloudflare data center that served you (&lt;code&gt;colo&lt;/code&gt;). Run it with the VPN off, then on. If &lt;code&gt;loc&lt;/code&gt; doesn't change, the VPN isn't routing that traffic. That happens with split tunneling or a proxy set only in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: route traffic through the target country
&lt;/h2&gt;

&lt;p&gt;I connect to a server in the country I want to test. When the app has region-specific rules, such as state-level restrictions in the US, I pick a location closest to that region. Then I run the &lt;code&gt;curl&lt;/code&gt; check again and confirm &lt;code&gt;loc&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two things I verify at this point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The country matches in more than one database.&lt;/strong&gt; IP geolocation is an estimate. Cloudflare's docs say nothing inherently binds an IP address to a country, and providers can disagree. If your app uses MaxMind and your CDN uses its own data, check both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS follows the tunnel.&lt;/strong&gt; If the VPN doesn't handle DNS, your resolver may still sit in your home country, and a CDN that routes by resolver will send you to the wrong edge.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: add the other signals with Playwright
&lt;/h2&gt;

&lt;p&gt;An IP in Germany with a browser set to US English and a New York timezone is not a German user. It's a user your own analytics will flag as odd. &lt;a href="https://playwright.dev/docs/emulation" rel="noopener noreferrer"&gt;Playwright&lt;/a&gt; sets the rest in code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playwright&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newContext&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;de-DE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timezoneId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Europe/Berlin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;geolocation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;52.52&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;13.405&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;geolocation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://your-app.example/pricing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-test=price]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;innerText&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I run this while the VPN is connected to Germany. The IP, locale, timezone and coordinates now describe the same person.&lt;/p&gt;

&lt;p&gt;I keep one script per target market and the expected values next to it. A short matrix is enough:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Market&lt;/th&gt;
&lt;th&gt;Expected currency&lt;/th&gt;
&lt;th&gt;Consent banner&lt;/th&gt;
&lt;th&gt;Blocked features&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DE&lt;/td&gt;
&lt;td&gt;EUR&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;US-CA&lt;/td&gt;
&lt;td&gt;USD&lt;/td&gt;
&lt;td&gt;yes (state privacy notice)&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JP&lt;/td&gt;
&lt;td&gt;JPY&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;feature X&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What breaks, and why
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The VPN IP is classed as a datacenter or VPN address.&lt;/strong&gt; Some services detect that and show a CAPTCHA, a paywall variant or a block page. That says something real about how those users see your app, but it isn't the same as a residential visitor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared IPs trigger rate limits.&lt;/strong&gt; Many VPN users share an exit IP. A login form with aggressive IP throttling can lock you out mid-test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caches hide the bug.&lt;/strong&gt; A CDN may serve you a cached "German" page, or serve your cached page to the next tester. Add a cache-busting query string, or send &lt;code&gt;Cache-Control: no-cache&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cookies carry state.&lt;/strong&gt; A &lt;code&gt;country=US&lt;/code&gt; cookie from the first run beats the IP in the second. Playwright starts every context clean, which is why I use it for this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IPv6 leaks the real location.&lt;/strong&gt; If the VPN tunnels only IPv4 and your network has IPv6, the app may see your real IPv6 address. Run &lt;code&gt;curl -6 https://www.cloudflare.com/cdn-cgi/trace&lt;/code&gt; and see whether it answers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I run this with Atomic VPN
&lt;/h2&gt;

&lt;p&gt;Here's the workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the &lt;a href="https://atomicvpn.io/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=geo-testing" rel="noopener noreferrer"&gt;Atomic VPN homepage&lt;/a&gt;. It shows your current IP, city, ISP and DNS, so I get a "before" reading without leaving the browser.&lt;/li&gt;
&lt;li&gt;Connect in the app and pick the country (the app lists locations by country and city, with ping next to each).&lt;/li&gt;
&lt;li&gt;Reload the page and compare the "after" reading, then run the &lt;code&gt;curl&lt;/code&gt; trace from Step 1 to confirm &lt;code&gt;loc&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run the Playwright script for that market.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why this works for me: there are 6,500+ servers in 88+ countries, so a new market is a click rather than a new proxy contract. The WireGuard protocol is the default, so connecting and switching countries is quick. If you're on a restrictive network that blocks UDP, there's an OpenVPN option over TCP 443.&lt;/p&gt;

&lt;p&gt;Any VPN with similar coverage does the same job. If you need exit nodes in only one or two countries, a cheap VPS you control may be simpler (I wrote about that trade-off in &lt;a href="https://dev.to/"&gt;Self-Hosted WireGuard vs a VPN Service&lt;/a&gt;). &amp;lt;!-- TODO (author): replace &lt;a href="https://dev.to/"&gt;https://dev.to/&lt;/a&gt; with the real URL of the second article after publishing it. --&amp;gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a VPN is the wrong tool
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CI.&lt;/strong&gt; Pipelines need deterministic, scriptable exits. Use a proxy service with an API or geo-distributed runners, not a desktop VPN client.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance-critical rules.&lt;/strong&gt; Cloudflare's docs advise against using IP geolocation as the sole signal when location must be precise, such as state-based access restrictions. I test that logic with a VPN, but I design it with more than the IP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other people's services.&lt;/strong&gt; Test your own app. Using a VPN to get around a third party's regional restrictions may break their terms, and in some places the law.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does Chrome DevTools location override change my IP?&lt;/strong&gt;&lt;br&gt;
No. It changes what the Geolocation API returns to the page. Server-side checks based on your IP see your real address.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does my app still show the wrong currency after I connect a VPN?&lt;/strong&gt;&lt;br&gt;
Usually a cookie, a cached response, or a second signal (locale, account country) that outranks the IP. Clear storage, use a fresh browser context, and check which signal your pricing logic reads first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I test geo-blocking in CI with a VPN?&lt;/strong&gt;&lt;br&gt;
Not reliably. Use a proxy service with an API, or runners in the target regions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is IP geolocation accurate?&lt;/strong&gt;&lt;br&gt;
At country level, mostly. At city level it varies by provider and drifts as IP ranges change hands. Cloudflare states it can't guarantee its data matches other providers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do some sites block my VPN exit IP?&lt;/strong&gt;&lt;br&gt;
Many services keep lists of datacenter and VPN ranges. The IP is shared and not residential, so it gets caught.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it legal to use a VPN to test my own app?&lt;/strong&gt;&lt;br&gt;
Using a VPN is legal in most countries. Check local rules, and don't use it to bypass restrictions on services you don't own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.chrome.com/docs/devtools/sensors" rel="noopener noreferrer"&gt;Chrome DevTools: Sensors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.cloudflare.com/network/ip-geolocation/" rel="noopener noreferrer"&gt;Cloudflare: IP geolocation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://playwright.dev/docs/emulation" rel="noopener noreferrer"&gt;Playwright: Emulation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>testing</category>
      <category>networking</category>
      <category>vpn</category>
    </item>
    <item>
      <title>Fake Wi-Fi in the Sky: What an Evil Twin Can See and What HTTPS Still Protects</title>
      <dc:creator>Mak Atomic</dc:creator>
      <pubDate>Fri, 23 Oct 2026 11:25:29 +0000</pubDate>
      <link>https://dev.to/marupxyyz01/fake-wi-fi-in-the-sky-what-an-evil-twin-can-see-and-what-https-still-protects-33j7</link>
      <guid>https://dev.to/marupxyyz01/fake-wi-fi-in-the-sky-what-an-evil-twin-can-see-and-what-https-still-protects-33j7</guid>
      <description>&lt;p&gt;In April 2024, airline staff in Australia noticed a suspicious Wi-Fi network on a domestic flight. They reported it. When the flight landed in Perth, Australian Federal Police searched a passenger's bags and seized a portable wireless access device, a laptop and a phone. &lt;a href="https://www.bleepingcomputer.com/news/security/australian-charged-for-evil-twin-wifi-attack-on-plane/" rel="noopener noreferrer"&gt;BleepingComputer reported on the charges in July 2024&lt;/a&gt;: the man was accused of running fake access points on flights and at the Perth, Melbourne and Adelaide airports, with the networks leading to fake login pages that asked for email or social media credentials. &lt;a href="https://www.bitdefender.com/en-au/blog/hotforsecurity/australian-federal-police-arrest-suspect-for-wifi-credential-theft-on-flights" rel="noopener noreferrer"&gt;Bitdefender covered the same case&lt;/a&gt;. The allegations were not proven at the time of those reports.&lt;/p&gt;

&lt;p&gt;The trick has a name: the evil twin. This post explains why it works, what the person running one can and cannot read, and three checks that take less time than boarding.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An evil twin is an access point that copies a real network's name. Your device cannot tell them apart by name alone.&lt;/li&gt;
&lt;li&gt;HTTPS still protects page content on a fake network. What leaks is the site list, plain HTTP, and anything you type into a fake login page.&lt;/li&gt;
&lt;li&gt;A VPN hides your traffic from the access point, but it cannot save a password you type into the attacker's portal.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why the copy works
&lt;/h2&gt;

&lt;p&gt;A Wi-Fi network is announced by a name (SSID). Your phone remembers names it has joined before and reconnects automatically. On an open network, such as airport or in-flight Wi-Fi, there is no password the access point must prove it knows, so a second access point with the same name and a stronger signal looks identical to the original.&lt;/p&gt;

&lt;p&gt;Then comes the captive portal. Most public networks redirect you to a page where you accept terms or sign in. That page is also the attacker's best tool: it is a normal thing to see, and it asks for credentials. In the Australian case, that is how the alleged login-page theft was supposed to work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the person running the fake network sees
&lt;/h2&gt;

&lt;p&gt;Assume you joined it. The operator is now your network. They get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every plain HTTP request&lt;/strong&gt; in full: URL, headers, cookies, form fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your DNS queries&lt;/strong&gt;, so a list of the hostnames you look up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The hostname of every HTTPS site&lt;/strong&gt;, because it travels in the clear in the TLS handshake (a 15-line Python script can show you this on your own machine: build a TLS ClientHello offline and look for the hostname in the raw bytes).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Destination IPs and traffic volume.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Whatever you type into a page they serve you.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What they do not get: the content of pages loaded over HTTPS with a valid certificate. They cannot read your bank's page or your mail over a TLS connection unless your device accepts a forged certificate, and modern browsers show a loud warning when that happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  HSTS closes the downgrade trick
&lt;/h2&gt;

&lt;p&gt;A common attack on an open network is to strip HTTPS: you type &lt;code&gt;bank.com&lt;/code&gt;, the attacker answers on plain HTTP and proxies the rest. &lt;a href="https://datatracker.ietf.org/doc/html/rfc6797" rel="noopener noreferrer"&gt;HSTS, defined in RFC 6797&lt;/a&gt;, is the countermeasure. A site sends a header telling the browser to use HTTPS only for a set time. After that visit, the browser refuses plain HTTP for that site, even if you type &lt;code&gt;http://&lt;/code&gt;. First visits and sites without HSTS remain exposed, which is why "just look for the padlock" is only half the advice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three checks for a network you do not control
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Is the portal the real portal?
&lt;/h3&gt;

&lt;p&gt;Before you type anything into a login page, look at the address bar. A portal you reached by redirect should still use HTTPS with a certificate that matches the airline's or airport's domain. If the page asks for your email password or a social login to get online, close it. A genuine Wi-Fi portal needs, at most, an email address, a booking reference or a payment on the operator's own domain.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Is plain HTTP being tampered with?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;neverssl.com&lt;/code&gt; is a site that stays on HTTP on purpose, so captive portals can intercept it. Fetch it and look at the answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sI&lt;/span&gt; http://neverssl.com | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On an ordinary network you get &lt;code&gt;HTTP/1.1 200 OK&lt;/code&gt; with the site's own headers (mine returned &lt;code&gt;Server: Apache/2.4.68&lt;/code&gt;). On a network that intercepts traffic, you get a redirect (&lt;code&gt;302&lt;/code&gt;) to a portal or an unexpected host. That is not proof of an attack, since real portals do the same, but it tells you something is in the middle and which page it wants you on.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Does the certificate match what you expect?
&lt;/h3&gt;

&lt;p&gt;Ask for a site's certificate and read the issuer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; | openssl s_client &lt;span class="nt"&gt;-connect&lt;/span&gt; example.com:443 &lt;span class="nt"&gt;-servername&lt;/span&gt; example.com 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  | openssl x509 &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-issuer&lt;/span&gt; &lt;span class="nt"&gt;-subject&lt;/span&gt; &lt;span class="nt"&gt;-dates&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On my machine this printed an issuer of &lt;code&gt;Cloudflare TLS Issuing ECC CA 3&lt;/code&gt; and a subject of &lt;code&gt;CN=example.com&lt;/code&gt;. Run it on a trusted network first to get a baseline, then on the new network. A different issuer for the same site, or an &lt;code&gt;s_client&lt;/code&gt; error about verification, is a red flag. Do not click through the browser warning that goes with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a VPN fits
&lt;/h2&gt;

&lt;p&gt;A VPN builds an encrypted tunnel between your device and the VPN server. The access point, real or fake, then sees one connection to one endpoint and encrypted packets. The plain HTTP, DNS queries and SNI hostnames from the list above stop being readable to the access point. That is the real benefit on untrusted Wi-Fi.&lt;/p&gt;

&lt;p&gt;A VPN does not change three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A password you type into the attacker's portal.&lt;/strong&gt; The VPN protects the path, not your choice to submit credentials to the wrong page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The order of operations.&lt;/strong&gt; On most captive-portal networks you have to pass the portal before a VPN can connect. Verify the portal first, then connect, then do anything sensitive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust.&lt;/strong&gt; You moved from trusting the café router to trusting the VPN operator.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want a quick sanity check after connecting, run the three checks above again, plus the &lt;code&gt;curl&lt;/code&gt; trace. The visible IP should now be the VPN server's. &lt;a href="https://atomicvpn.io" rel="noopener noreferrer"&gt;Atomic VPN&lt;/a&gt; is one you can use for that comparison.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small habit list
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Turn off auto-join for open networks on your phone and laptop.&lt;/li&gt;
&lt;li&gt;Forget a network after you use it, especially airport and flight Wi-Fi.&lt;/li&gt;
&lt;li&gt;Treat any "log in with your email or social account to get Wi-Fi" page as hostile.&lt;/li&gt;
&lt;li&gt;Keep file sharing off on untrusted networks.&lt;/li&gt;
&lt;li&gt;Do payments and logins on mobile data when you can.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is an evil twin attack?
&lt;/h3&gt;

&lt;p&gt;A fake Wi-Fi access point that copies the name of a real one, so devices connect to it. The operator can then read unencrypted traffic and serve fake login pages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can someone read my passwords on public Wi-Fi if the site uses HTTPS?
&lt;/h3&gt;

&lt;p&gt;Not from the encrypted connection, as long as the certificate is valid. They can still capture what you type into a fake page they control.&lt;/p&gt;

&lt;h3&gt;
  
  
  How can I tell a fake Wi-Fi network from a real one?
&lt;/h3&gt;

&lt;p&gt;You often cannot by name. Check the portal's domain and certificate, watch for requests for email or social credentials, and compare behavior against a trusted network.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a VPN protect me on airport Wi-Fi?
&lt;/h3&gt;

&lt;p&gt;It hides your traffic from the access point, including DNS and the site list. It does not protect credentials you hand to a fake login page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is in-flight Wi-Fi more dangerous than other public Wi-Fi?
&lt;/h3&gt;

&lt;p&gt;The mechanism is the same. The 2024 case is notable because it was alleged to span flights and airports, but home, café and hotel networks have the same weakness.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is HSTS?
&lt;/h3&gt;

&lt;p&gt;A header, defined in RFC 6797, that tells a browser to use only HTTPS for a site for a set period. It blocks protocol-downgrade tricks after the first visit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;On a network you do not control, assume the operator can read plain HTTP, your DNS and the hostnames you visit. HTTPS keeps the content private. A VPN hides the rest from the access point. Neither helps if you type a password into the wrong page, so the habit that matters most is the dull one: do not log in to anything just to get online.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What is the strangest captive portal you have seen? Describe it in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>privacy</category>
      <category>networking</category>
      <category>wifi</category>
    </item>
    <item>
      <title>HTTPS Hides the Page, Not the Hostname: What Your ISP Still Sees</title>
      <dc:creator>Mak Atomic</dc:creator>
      <pubDate>Fri, 23 Oct 2026 11:23:39 +0000</pubDate>
      <link>https://dev.to/marupxyyz01/https-hides-the-page-not-the-hostname-what-your-isp-still-sees-kn7</link>
      <guid>https://dev.to/marupxyyz01/https-hides-the-page-not-the-hostname-what-your-isp-still-sees-kn7</guid>
      <description>&lt;p&gt;Everyone knows the padlock means "encrypted". Fewer people know what it leaves out.&lt;/p&gt;

&lt;p&gt;When your browser opens &lt;code&gt;https://example.com&lt;/code&gt;, it sends a first message to the server called the ClientHello. That message contains the hostname, in plain text, in a field called SNI (Server Name Indication). Your ISP, the café router and anyone else on the path can read it. They cannot read the page, the URL path or the form data. They can read which site you went to.&lt;/p&gt;

&lt;p&gt;You do not have to take that on trust. Here is a script that builds a ClientHello and looks inside it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The SNI field carries the site name unencrypted in every classic TLS connection, including TLS 1.3.&lt;/li&gt;
&lt;li&gt;Encrypted Client Hello (ECH), now &lt;a href="https://datatracker.ietf.org/doc/html/rfc9849" rel="noopener noreferrer"&gt;RFC 9849&lt;/a&gt;, hides it, but only when the browser, the server and DNS all cooperate.&lt;/li&gt;
&lt;li&gt;A VPN moves the observer: your ISP sees one tunnel endpoint instead of a list of hostnames, and the VPN operator can see what the ISP used to see.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Look inside a ClientHello (15 lines)
&lt;/h2&gt;

&lt;p&gt;This script performs the first step of a TLS handshake against nothing at all. It never touches the network. It only asks Python's &lt;code&gt;ssl&lt;/code&gt; module what it would send.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;

&lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_default_context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;incoming&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outgoing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MemoryBIO&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MemoryBIO&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;tls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wrap_bio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;incoming&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outgoing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;server_hostname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;do_handshake&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# no server: it only produces the ClientHello
&lt;/span&gt;&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SSLWantReadError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="n"&gt;client_hello&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;outgoing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;first byte:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client_hello&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(0x16 = TLS handshake record)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hostname readable in raw bytes:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;client_hello&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it with &lt;code&gt;python3 sni_demo.py&lt;/code&gt;. On my machine it prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;first byte: 0x16 (0x16 = TLS handshake record)
hostname readable in raw bytes: True
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those raw bytes are exactly what leaves your laptop before any encryption starts. The hostname is sitting in them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why TLS 1.3 did not fix this
&lt;/h2&gt;

&lt;p&gt;TLS 1.3 encrypts most of the handshake, but the ClientHello has to come first, before client and server share any keys. The server needs the name in order to pick the right certificate when one IP address hosts many sites. So the name goes out in the open.&lt;/p&gt;

&lt;p&gt;You can see the same thing from a command line against Cloudflare's trace endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://www.cloudflare.com/cdn-cgi/trace | &lt;span class="nb"&gt;grep &lt;/span&gt;sni
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On my machine this prints &lt;code&gt;sni=plaintext&lt;/code&gt;. Cloudflare is telling you what it saw.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an observer on your line sees
&lt;/h2&gt;

&lt;p&gt;For a single HTTPS visit, a passive observer gets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The server IP address.&lt;/strong&gt; Always visible, it is how packets get there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The hostname from SNI.&lt;/strong&gt; The part this article is about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The DNS lookup that came before.&lt;/strong&gt; A separate channel, covered in the next section.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timing and volume.&lt;/strong&gt; Roughly how much data moved and when.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They do not get the URL path, query string, cookies or page content. &lt;code&gt;example.com/health-clinic/appointments&lt;/code&gt; leaks &lt;code&gt;example.com&lt;/code&gt;, not the path.&lt;/p&gt;

&lt;h2&gt;
  
  
  DNS is the second leak
&lt;/h2&gt;

&lt;p&gt;Before the ClientHello, your device looked up the IP of &lt;code&gt;example.com&lt;/code&gt;. By default that lookup is unencrypted and goes to a resolver your network chose. Even if SNI were hidden, the DNS query would still name the site. That is why ECH is paired with encrypted DNS: the browser fetches the ECH key from DNS, and doing that over plain DNS would tell the observer where you are going anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  What ECH changes
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc9849" rel="noopener noreferrer"&gt;RFC 9849 defines Encrypted Client Hello&lt;/a&gt;. The idea: the client sends an outer ClientHello with a generic public name, and puts the real one inside an encrypted inner ClientHello. The server publishes a public key for this in DNS. &lt;a href="https://www.rfc-editor.org/rfc/rfc9848" rel="noopener noreferrer"&gt;RFC 9848 covers how that key is bootstrapped through DNS service bindings&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For ECH to work on a given connection, three things have to line up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The browser supports it.&lt;/strong&gt; Mozilla documents the state of Firefox's implementation on its &lt;a href="https://wiki.mozilla.org/Security/Encrypted_Client_Hello" rel="noopener noreferrer"&gt;ECH wiki page&lt;/a&gt;, and Chrome has shipped support too. Check the current release notes for your version, because defaults have changed over time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The site's DNS publishes the key.&lt;/strong&gt; No key, no ECH.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS itself is encrypted.&lt;/strong&gt; Otherwise the lookup gives the site away.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When all three hold, the observer sees the outer name, which is often a CDN's shared hostname, and the destination IP. If many sites sit behind the same IP, that is a real hiding place. If a site has an IP of its own, the IP alone still gives it away. ECH hides the name, not the address.&lt;/p&gt;

&lt;p&gt;You can check your own browser: open &lt;code&gt;https://www.cloudflare.com/cdn-cgi/trace&lt;/code&gt; and read the &lt;code&gt;sni=&lt;/code&gt; line. My &lt;code&gt;curl&lt;/code&gt; prints &lt;code&gt;plaintext&lt;/code&gt;. A browser that actually sent an encrypted inner name should show something different. If yours still says &lt;code&gt;plaintext&lt;/code&gt;, ECH is not active on that connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a VPN does to this picture
&lt;/h2&gt;

&lt;p&gt;A VPN does not make SNI encrypted. It moves your connection to the website out of your ISP's sight.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your ISP&lt;/strong&gt; sees a tunnel to one VPN server and a stream of encrypted packets. No per-site hostnames, no per-site IPs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The VPN operator&lt;/strong&gt; sees the traffic leaving its servers, including the SNI of connections that do not use ECH. You have moved trust from one party to another, so the operator's logging policy matters more than the protocol it uses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The website&lt;/strong&gt; still knows you connected. It sees the VPN server's IP instead of yours.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the two tools solve different problems. ECH hides the name from everyone on the path. A VPN hides the whole list from your local network. They stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optional: see it on the wire
&lt;/h2&gt;

&lt;p&gt;If you have Wireshark's &lt;code&gt;tshark&lt;/code&gt; installed, you can watch your own handshakes (I have not run this variant on this machine, so check the interface name for yours):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;tshark &lt;span class="nt"&gt;-i&lt;/span&gt; en0 &lt;span class="nt"&gt;-Y&lt;/span&gt; &lt;span class="s1"&gt;'tls.handshake.extensions_server_name'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-T&lt;/span&gt; fields &lt;span class="nt"&gt;-e&lt;/span&gt; ip.dst &lt;span class="nt"&gt;-e&lt;/span&gt; tls.handshake.extensions_server_name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open a few sites and you will see hostnames scroll by. Now connect to a VPN, capture on the physical interface again, and compare: the hostnames should be gone from that interface and the list shrinks to one destination.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does HTTPS hide which website I visit?
&lt;/h3&gt;

&lt;p&gt;No. It hides the content and the path. The hostname goes out in the ClientHello and in the DNS lookup unless ECH and encrypted DNS are both in use.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is SNI?
&lt;/h3&gt;

&lt;p&gt;Server Name Indication, a field in the TLS ClientHello that tells the server which hostname you want, so one IP can serve many certificates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does TLS 1.3 encrypt SNI?
&lt;/h3&gt;

&lt;p&gt;Not in the standard handshake. The ClientHello comes before any shared keys exist, so SNI is sent in plain text. ECH is the extension that fixes this.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is Encrypted Client Hello?
&lt;/h3&gt;

&lt;p&gt;A TLS extension standardized in RFC 9849 that wraps the real ClientHello, and with it the hostname, in encryption using a key the server publishes in DNS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a VPN hide the sites I visit from my ISP?
&lt;/h3&gt;

&lt;p&gt;Yes, from your ISP. It sees an encrypted tunnel to one server. The VPN provider can see what your ISP no longer can, so choose the provider as carefully as the protocol.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can the website still identify me behind a VPN?
&lt;/h3&gt;

&lt;p&gt;Yes, through cookies, logins and browser fingerprint. A VPN changes your IP, not who you are signed in as.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;If you are worried about the person sitting on your network, hide the whole list with a tunnel. If you are worried about everyone on the path, you need ECH and encrypted DNS as well, and you need sites that support them. Try the 15-line script first: seeing your own hostname in the raw bytes explains the problem better than any diagram.&lt;/p&gt;

&lt;p&gt;If you want a tunnel to compare against, &lt;a href="https://atomicvpn.io" rel="noopener noreferrer"&gt;Atomic VPN&lt;/a&gt; is one you can use for that comparison.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Got a different &lt;code&gt;sni=&lt;/code&gt; result in your browser? Tell me which browser and version in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>privacy</category>
      <category>networking</category>
      <category>tls</category>
    </item>
    <item>
      <title>WireGuard in Under 4,000 Lines: What Developers Get From a Smaller VPN Codebase</title>
      <dc:creator>Mak Atomic</dc:creator>
      <pubDate>Fri, 23 Oct 2026 11:14:28 +0000</pubDate>
      <link>https://dev.to/marupxyyz01/wireguard-in-under-4000-lines-what-developers-get-from-a-smaller-vpn-codebase-12i9</link>
      <guid>https://dev.to/marupxyyz01/wireguard-in-under-4000-lines-what-developers-get-from-a-smaller-vpn-codebase-12i9</guid>
      <description>&lt;p&gt;A VPN protocol is code that runs with high privileges and parses data from the open internet. The less of it there is, the less there is to get wrong.&lt;/p&gt;

&lt;p&gt;That was the pitch of the original WireGuard paper. Its authors wrote that they implemented the protocol for Linux &lt;a href="https://www.wireguard.com/papers/wireguard.pdf" rel="noopener noreferrer"&gt;"in less than 4,000 lines of code"&lt;/a&gt;, excluding cryptographic primitives, and that this makes it easy to audit and verify. They contrast it with OpenVPN, which the paper describes as a user-space daemon that supports the entire plethora of TLS functionality and is therefore "enormously complex".&lt;/p&gt;

&lt;p&gt;This post is for developers who use a VPN every day and want to know what that difference means in practice: what the config looks like, what you gain, and where the older option still wins.&lt;/p&gt;


&lt;div class="crayons-card c-embed"&gt;

  &lt;br&gt;
&lt;strong&gt;TL;DR&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;WireGuard's codebase is small by design, which makes review and reasoning about it easier. Small code is not the same as a verified provider.&lt;/li&gt;
&lt;li&gt;A working tunnel is about ten lines of config, and the keys are the identity. There is no certificate authority to run.&lt;/li&gt;
&lt;li&gt;OpenVPN still has a place: networks that block UDP, and setups that need its TLS tooling.&lt;/li&gt;
&lt;li&gt;Most commercial clients ship several protocols. The useful question is which one is the default and what the fallback is.

&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;In this post:&lt;/strong&gt; Comparison · Config · OpenVPN · Commercial clients · FAQ&lt;/p&gt;
&lt;h2&gt;
  
  
  What you are actually comparing &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;The paper compares WireGuard with IPsec and OpenVPN on three axes: simplicity, performance and cryptography.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;WireGuard&lt;/th&gt;
&lt;th&gt;OpenVPN&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Code size&lt;/td&gt;
&lt;td&gt;Under 4,000 lines for the Linux implementation, excluding crypto primitives&lt;/td&gt;
&lt;td&gt;Full TLS stack, including certificate parsing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where it runs&lt;/td&gt;
&lt;td&gt;Linux kernel implementation&lt;/td&gt;
&lt;td&gt;User-space daemon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transport&lt;/td&gt;
&lt;td&gt;UDP&lt;/td&gt;
&lt;td&gt;UDP or TCP, including port 443&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cryptography&lt;/td&gt;
&lt;td&gt;Fixed modern primitives, no cipher negotiation&lt;/td&gt;
&lt;td&gt;Negotiated through TLS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity&lt;/td&gt;
&lt;td&gt;Public keys&lt;/td&gt;
&lt;td&gt;Certificates and a CA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code size.&lt;/strong&gt; The paper's figure is under 4,000 lines for the Linux implementation, excluding crypto primitives. OpenVPN carries the full TLS stack, including certificate parsing, and runs as a user-space daemon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance.&lt;/strong&gt; The authors benchmarked WireGuard against IPsec and OpenVPN with iperf3 and reported that WireGuard won on both throughput and ping. They explain the OpenVPN gap by its user-space design, which adds copying between kernel and user space. These are the authors' numbers from the paper, so treat them as a starting point and measure on your own link.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cryptography.&lt;/strong&gt; &lt;a href="https://www.wireguard.com/" rel="noopener noreferrer"&gt;WireGuard's site lists&lt;/a&gt; a fixed set of modern primitives, including ChaCha20, Poly1305 and Curve25519. There is no cipher negotiation, so there is no downgrade path to a weak suite. If a primitive is ever broken, the protocol version changes rather than a setting.&lt;/p&gt;
&lt;h2&gt;
  
  
  What a config looks like &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.wireguard.com/quickstart/" rel="noopener noreferrer"&gt;WireGuard quickstart&lt;/a&gt; shows the low-level tools, and &lt;code&gt;wg-quick&lt;/code&gt; automates the rest. A client config with placeholder keys looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Interface]&lt;/span&gt;
&lt;span class="py"&gt;PrivateKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;client-private-key&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;Address&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.0.0.2/32&lt;/span&gt;
&lt;span class="py"&gt;DNS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.0.0.1&lt;/span&gt;

&lt;span class="nn"&gt;[Peer]&lt;/span&gt;
&lt;span class="py"&gt;PublicKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;server-public-key&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;Endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;vpn.example.com:51820&lt;/span&gt;
&lt;span class="py"&gt;AllowedIPs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0/0, ::/0&lt;/span&gt;
&lt;span class="py"&gt;PersistentKeepalive&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;25&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bring it up and check it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;wg-quick up wg0
&lt;span class="nb"&gt;sudo &lt;/span&gt;wg show
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details that matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AllowedIPs = 0.0.0.0/0, ::/0&lt;/code&gt;&lt;/strong&gt; sends all traffic through the tunnel. A narrower value gives you a split setup for just one subnet, which is how many teams reach a private network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PersistentKeepalive = 25&lt;/code&gt;&lt;/strong&gt; keeps the mapping alive through NAT. The quickstart suggests 25 seconds as a sensible value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keys are the identity.&lt;/strong&gt; A peer is a public key. You do not run a CA or revoke certificates; you remove a peer from the config.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where OpenVPN is still the right tool &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;WireGuard runs over UDP. That is fine at home and at the office, and it fails on some hotel, airport and corporate networks that only let TCP on port 443 through.&lt;/p&gt;

&lt;p&gt;OpenVPN can run over TCP, including port 443, so it looks like ordinary HTTPS to a basic firewall. It is slower there, because TCP inside TCP behaves badly on lossy links, but a slow tunnel beats no tunnel. It also has mature tooling for certificate-based setups, which some organizations already depend on.&lt;/p&gt;

&lt;p&gt;So the rule I use: WireGuard by default, OpenVPN when UDP is blocked.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to look for in a commercial VPN client &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;If you do not run your own server, you pick a provider, and the protocol list is one of the few things you can check from outside.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Default protocol.&lt;/strong&gt; It should be WireGuard unless you have a reason otherwise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A fallback.&lt;/strong&gt; At least one TCP-capable option for hostile networks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Many devices per account.&lt;/strong&gt; One config per device is the WireGuard model, so check the device limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://atomicvpn.io" rel="noopener noreferrer"&gt;Atomic VPN&lt;/a&gt; lists WireGuard as its default, with OpenVPN and IKEv2/IPsec as alternatives. The apps are available for Windows, macOS and Android, and iOS is listed as coming soon. If you are on a network that eats UDP, switching protocol in the app is the first thing to try before you blame the provider.&lt;/p&gt;

&lt;p&gt;One caution that applies to every provider: a small protocol codebase says nothing about what a company logs. The protocol protects the tunnel. The privacy policy decides what happens at the other end.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is WireGuard more secure than OpenVPN?
&lt;/h3&gt;

&lt;p&gt;It has a much smaller codebase and no cipher negotiation, which reduces attack surface. Neither protocol is secure if the provider logs your traffic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does WireGuard work on Windows, macOS and Android?
&lt;/h3&gt;

&lt;p&gt;Yes, WireGuard has clients for all three, and commercial VPN apps usually bundle it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is WireGuard faster?
&lt;/h3&gt;

&lt;p&gt;The paper attributes the OpenVPN gap to user-space overhead and reports better throughput and latency in its own benchmark. Your results depend on your link.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does WireGuard fail on some Wi-Fi networks?
&lt;/h3&gt;

&lt;p&gt;It uses UDP. Networks that only allow TCP 443 block it. Use OpenVPN over TCP there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need to run my own WireGuard server?
&lt;/h3&gt;

&lt;p&gt;No. You can use a provider. Self-hosting gives you control over the endpoint and logging, and costs you the maintenance.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does &lt;code&gt;AllowedIPs&lt;/code&gt; do?
&lt;/h3&gt;

&lt;p&gt;It is both a routing table and an access list for the peer. &lt;code&gt;0.0.0.0/0&lt;/code&gt; routes all IPv4 traffic through the tunnel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;If your network allows UDP, use WireGuard and spend your time on something other than tunnel configuration. If it does not, keep OpenVPN over TCP 443 as the fallback. Either way, check the protocol you actually connected with, because the default in the app is only a default.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://atomicvpn.io" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Want WireGuard without running your own server? See the protocols on atomicvpn.io.&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Which protocol do you run by default, and what made you switch? Tell me in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>networking</category>
      <category>security</category>
    </item>
    <item>
      <title>What Your Laptop Tells Every Server: A 10-Minute Leak Audit with curl, dig and DevTools</title>
      <dc:creator>Mak Atomic</dc:creator>
      <pubDate>Fri, 16 Oct 2026 12:58:35 +0000</pubDate>
      <link>https://dev.to/marupxyyz01/what-your-laptop-tells-every-server-a-10-minute-leak-audit-with-curl-dig-and-devtools-1j18</link>
      <guid>https://dev.to/marupxyyz01/what-your-laptop-tells-every-server-a-10-minute-leak-audit-with-curl-dig-and-devtools-1j18</guid>
      <description>&lt;p&gt;Open a terminal and run this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -s https://www.cloudflare.com/cdn-cgi/trace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You get a dozen lines back:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ip=203.0.113.7

loc=LV

colo=RIX

sni=plaintext
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;ip=&lt;/code&gt; is your public address. &lt;code&gt;loc=&lt;/code&gt; is the country Cloudflare maps it to. &lt;code&gt;colo=&lt;/code&gt; is the data center that answered. &lt;code&gt;sni=plaintext&lt;/code&gt; means the hostname you asked for travelled unencrypted in the TLS handshake.&lt;/p&gt;

&lt;p&gt;That is one request to one server. Every site, API and package registry you touch gets the same data and more. Below is a small audit you can run in ten minutes: three tests, what each one shows, and a second run behind a VPN to see what changes and what does not.&lt;/p&gt;


&lt;div class="crayons-card c-embed"&gt;

   &lt;strong&gt;TL;DR&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;our public IP, your DNS resolver and your WebRTC candidates are three separate exposure points. Fixing one does not fix the others.&lt;/li&gt;
&lt;li&gt;A VPN changes who sees your IP and your DNS queries. It does not change cookies, logins or browser fingerprint.&lt;/li&gt;
&lt;li&gt;Geolocation from an IP is a guess. Two services can disagree about the same address by a whole continent.

&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;



&lt;h2&gt;
  
  
  Test 1: What the server sees (public IP)
&lt;/h2&gt;

&lt;p&gt;Start with the request above. Then ask a second service about the same address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -s https://ifconfig.co/json | head -n 12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I ran both from the same machine, Cloudflare reported Latvia and the second service reported Tokyo. Same IP, same minute. IP geolocation databases are built from registry data, routing hints and guesses, and they lag behind when blocks of addresses move.&lt;/p&gt;

&lt;p&gt;Two things follow from this:&lt;/p&gt;

&lt;p&gt;"My location leaked through my IP" is usually true only at country or region level.&lt;br&gt;
A service that blocks or allows you by country is working from a database that can be wrong. If a site ever blocked you while you sat in the "right" country, this is why.&lt;/p&gt;
&lt;h2&gt;
  
  
  Test 2: Who answers your DNS questions
&lt;/h2&gt;

&lt;p&gt;Your browser knows the IP of a site because something resolved the name. That something is a DNS resolver, usually your router or your ISP, and it sees every hostname you look up, even when the page itself is HTTPS.&lt;/p&gt;

&lt;p&gt;Check which resolver the world sees when you ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dig +short whoami.akamai.net
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The answer is the IP of the recursive resolver that reached Akamai's authoritative server on your behalf. If it belongs to your ISP, your ISP is reading your lookups. If it belongs to a public resolver you chose, that operator is.&lt;/p&gt;

&lt;p&gt;Encrypted DNS closes the path between you and the resolver. &lt;a href="https://datatracker.ietf.org/doc/html/rfc8484" rel="noopener noreferrer"&gt;DNS over HTTPS is defined in RFC 8484&lt;/a&gt;. It does not hide your lookups from the resolver itself, only from the network in between. The IETF's position on why this matters is blunt: R&lt;a href="https://datatracker.ietf.org/doc/html/rfc7258" rel="noopener noreferrer"&gt;FC 7258 is titled "Pervasive Monitoring Is an Attack"&lt;/a&gt; and asks protocol designers to make mass surveillance harder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test 3: What the browser hands out via WebRTC
&lt;/h2&gt;

&lt;p&gt;WebRTC lets two browsers connect directly. To do that, a browser gathers "ICE candidates", which are possible network addresses, and some of them come from a STUN server that reports your public address back. MDN documents the candidate types: &lt;code&gt;host&lt;/code&gt; for local addresses, &lt;code&gt;srflx&lt;/code&gt; for the address a STUN server saw.&lt;/p&gt;

&lt;p&gt;Paste this into the DevTools console on any page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const pc = new RTCPeerConnection({

  iceServers: [{ urls: "stun:stun.l.google.com:19302" }],

});

pc.createDataChannel("probe");

pc.onicecandidate = (e) =&amp;gt; {

  if (e.candidate) console.log(e.candidate.candidate);

};

pc.createOffer().then((offer) =&amp;gt; pc.setLocalDescription(offer));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the lines that contain &lt;code&gt;typ srflx&lt;/code&gt;. The address in that line is the public IP the STUN server saw. Modern browsers hide local &lt;code&gt;host&lt;/code&gt; addresses behind random &lt;code&gt;.local&lt;/code&gt; names, but the &lt;code&gt;srflx&lt;/code&gt; candidate is the one that matters here. If it differs from the IP in Test 1, something in your setup routes WebRTC outside your tunnel or proxy. That mismatch is the classic "WebRTC leak".&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it again behind a VPN
&lt;/h2&gt;

&lt;p&gt;Write down three results: public IP, resolver IP, &lt;code&gt;srflx&lt;/code&gt; address. Connect to a VPN, then repeat all three tests.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiwwmnrat5cwup3n0lies.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiwwmnrat5cwup3n0lies.png" alt="Table: expected results of IP, DNS and WebRTC tests behind a VPN" width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want a visual version of the same data, the homepage of &lt;a href="https://atomicvpn.io" rel="noopener noreferrer"&gt;Atomic VPN&lt;/a&gt; has an "exposure" panel that lists the IP, city, ISP, device, browser and DNS a site can read from a single visit. I use it as a quick before-and-after check, but the three commands above are the real test because you can script them.&lt;/p&gt;
&lt;h2&gt;
  
  
  What a VPN does not change
&lt;/h2&gt;

&lt;p&gt;Be clear about the limits, because "VPN" is sold as a stronger promise than it is.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cookies and logins. If you are signed in, the site knows who you are regardless of IP.&lt;/li&gt;
&lt;li&gt;Browser fingerprint. Fonts, canvas, screen size and extensions identify a browser without any IP at all.&lt;/li&gt;
&lt;li&gt;What the VPN provider sees. You move trust from your ISP to the VPN operator. Read the logging policy and decide whether you accept it.&lt;/li&gt;
&lt;li&gt;Plain HTTP pages. The tunnel protects the path to the VPN server, not the path from there to the site.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  A small script to keep
&lt;/h2&gt;

&lt;p&gt;Save it as &lt;code&gt;leakcheck.sh&lt;/code&gt; and run it before and after you connect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/usr/bin/env bash

echo "== public IP / country / colo =="

curl -s https://www.cloudflare.com/cdn-cgi/trace | grep -E '^(ip|loc|colo)='

echo "== resolver seen by Akamai =="

dig +short whoami.akamai.net

{% enddetails %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Diff the output of the two runs. If the IP changed but the resolver did not, you have found a leak in about two seconds.&lt;/p&gt;

</description>
      <category>security</category>
      <category>privacy</category>
      <category>networking</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
