DEV Community

A. K.
A. K.

Posted on

One Browser per Country: Running Several VPN Locations Simultaneously

Introduction

There is always a case where I would like to use a VPN in order to browse some sites from other countries: it could be because I am comparing prices, looking at local products and other reasons. However, I do have numerous other tabs currently open and with work in progress. These would get disrupted if a computer network-wide VPN is used which switches the country for all processes on the computer. I do not want this disruption. Most (all?) common VPN apps provide the network-wide functionality.

I worked with Claude Code to mitigate this disruption based on some past ideas. Claude made the execution of the ideas immensely fast!

Let us take an abstract scenario: I want three browser windows on one Mac: one Poland, one in Germany and one in the Netherlands, all open simultaneously. My normal existing browser(s) had to stay on my home connection. A normal VPN app cannot do this, so what do we do? A naive answer is to build a smalI 'router' with Docker that can be set up with multiple VPNs.

The core aspect is to build a set of sandboxes. Each sandbox is a VPN connection to one country. Each sandbox has its own local proxy port. A browser, a command-line tool or another application uses a sandbox when you point it at that port. The setup now runs seven countries in parallel on one laptop.

This post explains the design with diagrams, from the full picture down to a single network request. It also describes the problems that I found on the way, because those problems teach more than the parts that worked at once.

Most of this post is written using Claude but verified by a human (me).

We discuss:

  • the reason why VPN apps and browser extensions cannot do this;
  • the design: one container, one proxy and one browser profile for each country;
  • how the traffic moves, and what each party on the path can see;
  • what happens when something fails;
  • the real problems: a dead server, rate limits, shared addresses and a country that was not where it said;
  • the limits of the method.

The code is on GitHub (link at the end). The setup is tested on macOS with Google Chrome and ExpressVPN. It can use any VPN provider that the open-source program Gluetun supports.

Terms

This post uses each term below with one meaning only.

Term Definition
VPN (virtual private network) An encrypted connection from your computer to a server of a VPN provider. Websites see the address of that server, not your home address.
VPN provider A company that operates VPN servers in many countries, for example ExpressVPN, NordVPN or Mullvad.
IP address A number that identifies a device on a network, for example 203.0.113.10. A website sees the IP address that a request comes from.
Tunnel The encrypted path between your computer and the VPN server. Traffic goes into the tunnel at one end and comes out decrypted at the other end.
Proxy A program that receives network traffic from an application and sends the traffic on for the application.
HTTP proxy A proxy type that browsers and most tools support. It carries web traffic (HTTP and HTTPS).
SOCKS5 proxy A more general proxy type. Some applications support only SOCKS5.
Port A number that identifies one service on a computer, for example 8889. Each proxy in this setup listens on its own port.
Loopback address The address 127.0.0.1. Only programs on the same computer can reach it.
Container A small, isolated Linux system that Docker runs. Each container has its own network interfaces, routes and firewall.
Docker Desktop The Mac application that runs containers. It runs them inside a hidden Linux virtual machine.
Gluetun An open-source program that runs in a container. It connects to a VPN provider and offers proxies to other programs.
Sandbox In this post: one Gluetun container that connects to one country, with one HTTP proxy port and one SOCKS5 proxy port.
Browser profile The folder where a browser keeps its cookies, history, logins and settings. Two profiles do not share cookies.
Browser instance One running copy of a browser application. Each instance in this setup has its own profile and its own proxy.
DNS (Domain Name System) The service that changes a name, for example example.com, into an IP address.
TLS The encryption that HTTPS uses between a browser and a website.
Kill switch A rule that blocks all traffic when the VPN tunnel does not work, so that no traffic leaves without encryption.
Split tunneling A VPN app feature that sends some applications through the VPN and other applications directly to the internet.
Browser fingerprint A set of facts that a website reads with JavaScript, for example the screen size, the fonts and the graphics hardware. Together, these facts can identify one computer.
Geolocation database A list that a website uses to find the country of an IP address. Different companies make different lists, and the lists do not always agree.

Why the usual tools cannot do this

A VPN app, split tunneling and a proxy extension all fail for the same reason: they choose one route for a whole application or a whole browser profile, never for one window.

Where each tool draws its line: VPN app, split tunneling, proxy extension and this setup

Each arrow points at the finest level that a tool can give its own route. Only separate browser instances reach the single window.

A VPN app changes the routes of the whole computer. When the app connects to Germany, every application uses Germany. The app has one connection to one country at a time.

Split tunneling chooses by application. You can tell the app "Chrome uses the VPN, Mail does not". But two Chrome windows are the same application, Google Chrome.app. Split tunneling cannot put one window in Poland and another window in Germany.

A browser extension can change the proxy of the browser with the Chrome chrome.proxy interface. Chrome applies that proxy to all tabs and windows of the profile. An extension can switch the profile from Poland to Germany, or send some websites to Germany, but it cannot give each tab its own country. The cookies also stay the same when the proxy changes, so a website can link your visits from the different countries.

The solution needs two separations at the same time: a separate network path for each country, and a separate browser profile for each country.

The design: one container, one port and one profile for each country

Each country gets three parts of its own: a Gluetun container with its own VPN tunnel, a proxy port on the Mac, and a browser profile. Your normal applications keep the direct route.

The full picture: two sandboxes and the direct route, from the Mac to the websites

Sandbox traffic goes through a local port into its own container and its own tunnel. Direct traffic never touches the containers.

The parts for the Germany sandbox:

Part Value for Germany Purpose
Container vpn-sandbox-germany Runs Gluetun. Holds the VPN tunnel to a German server.
HTTP proxy port 127.0.0.1:8889 Browsers and most tools send their traffic here.
SOCKS5 proxy port 127.0.0.1:1081 For applications that need SOCKS5.
Browser profile ~/.vpn-sandbox/profiles/germany-chrome Keeps the cookies and logins of the German browser instance apart from all others.

Three design choices matter:

  1. The ports listen only on the loopback address. Other devices on your home network cannot use your VPN proxies.
  2. Each container has its own tunnel. The countries share nothing except the Wi-Fi connection of the Mac.
  3. One text file lists the sandboxes. A line in sandboxes.conf holds the name, provider, country, ports, time zone and default browser. To add a country, you add one line.

A Chrome instance uses a sandbox through two start options. --user-data-dir gives the instance its own profile folder. Chrome starts a separate instance for a separate profile folder, so the second option applies to that instance only: --proxy-server=http://127.0.0.1:8889.

Inside one sandbox: two interfaces, two routes and one firewall

Inside each container, the routing table sends all internet traffic into the tunnel, and the firewall blocks everything else. Together they make a kill switch.

A network interface is a connection point between a system and a network. The container has two:

  • eth0 connects the container to its Docker network. Requests from the Mac come in on eth0. The encrypted VPN packets also leave on eth0.
  • tun0 is the tunnel. Traffic that goes into tun0 comes out decrypted at the VPN server.

Inside the Germany sandbox: from the proxy port through the proxies, DNS, routes, tunnel and firewall to the VPN server

A request enters on eth0, passes the proxy and the routing table into the tunnel, and leaves eth0 again as encrypted UDP. The values come from the running Germany container.

The routing table

A routing table tells the system which interface to use for each destination address. I read these values from the running Germany container:

Destination Interface Purpose
0.0.0.0/1 (addresses 0.x.x.x to 127.x.x.x) tun0 The first half of all internet addresses goes into the tunnel.
128.0.0.0/1 (addresses 128.x.x.x to 255.x.x.x) tun0 The second half goes into the tunnel.
The VPN server address only eth0 The encrypted packets reach the server outside the tunnel. Without this route, the packets would loop back into the tunnel.
default (all other addresses) eth0 Never used for internet traffic.

Why two halves and not one default route? A more specific route always wins over a less specific one. The two /1 routes together cover all addresses, and each is more specific than default. Thus, all internet traffic goes into the tunnel, and the default route stays only for the VPN server.

The firewall (kill switch)

The default action of the container firewall is drop: it discards every packet that no rule permits. The rules permit only:

Direction Permitted traffic
Out All traffic into tun0.
Out UDP to the VPN server on port 1195, on eth0. This is the encrypted tunnel itself.
In and out Traffic to and from the Docker network, for the proxy requests from the Mac.
In and out Replies to connections that are already open, and traffic inside the container.

When the tunnel fails, no rule permits plain internet traffic on eth0. The traffic cannot leave the container without encryption. Gluetun also starts the firewall before the VPN connection, so the kill switch works during the start too.

DNS

The proxies ask the DNS server inside the container, at 127.0.0.1:53. That DNS server sends its questions with DNS over TLS (encrypted DNS) through the tunnel. Your internet provider never sees the names of the websites. For SOCKS5, the address form socks5h:// matters: the h tells the application to let the proxy do the name lookup.

Anatomy of one request

When the German browser instance opens https://example.com, the container looks up the name, opens the connection through the tunnel, and then steps aside: the browser and the website talk with TLS end to end.

One HTTPS request: 9 messages between the browser, the proxy, the DNS server, the VPN server and the website

Time runs from top to bottom. Dashed arrows are replies. The two blue arrows carry the page itself, encrypted by TLS through the proxy and the VPN server.

Four points in this sequence matter:

  1. The browser sends only a name and a port to the proxy. The HTTP command CONNECT example.com:443 asks the proxy to open a connection. The page content stays encrypted by TLS.
  2. The container does the DNS lookup, not the Mac. The Mac never asks its home router for the address of example.com.
  3. The website sees the address of the VPN server. The German server received the tunnel on one address and sent the traffic to the website from a second address. Neither address is your home address.
  4. The proxy and the VPN server forward encrypted data that they cannot read. They see the name of the website and the amount of data, not the content.

A request through the SOCKS5 port takes the same path. Only the first message is different: the application speaks the SOCKS5 protocol instead of CONNECT.

Two layers of encryption, and who can see what

Between the Mac and the VPN server, the traffic has two layers of encryption: TLS from the browser, inside the VPN tunnel. After the VPN server, only TLS remains.

Encryption layers: TLS from the browser to the website, and the VPN tunnel from the container to the VPN server

The grey band is TLS, which covers the whole path. The blue band is the VPN tunnel, which covers only the part through your home network and your internet provider.

Party Can see Cannot see
Your router and internet provider That you connect to a VPN server, how much data, and when The websites, the website names, the page content
The VPN provider Your home address and the websites that you connect to The page content
The website The VPN address, the browser fingerprint, the time zone of your Mac Your home address
The proxy on your Mac The website names and ports The page content

The second row is the price of every VPN: you move your trust from your internet provider to the VPN provider. A plain http:// page has no TLS layer, so the VPN provider can read it.

When something fails, nothing falls back to your home connection

Each failure ends in an error, never in a silent direct connection. This is the most important safety property of the design.

Failure behavior: three checks, three errors and one success, with no path to the home connection

No arrow leads to your home connection. Each "no" ends in an error that you can see.

  • The container does not run. Nothing listens on the proxy port. Chrome shows ERR_PROXY_CONNECTION_FAILED. Chrome has no fallback for a proxy that you set with --proxy-server.

  • The tunnel is down. The firewall permits no plain traffic on eth0, so the request waits and then fails with a timeout.

  • The VPN server accepts the login but forwards nothing. This happened to me with one server (see "Problems" below). Gluetun runs a health check, a regular test of the connection, and restarts the tunnel when the test fails.

The scripts add a fourth safeguard. The scripts that open a browser or run a command test the proxy first. If the test fails, they do not start the application at all.

How to use it

After a one-time setup, you start a country with one command and open a browser in it with a second command.

One-time setup. Install Docker Desktop. Get the manual-configuration credentials of your VPN provider (for ExpressVPN: the OpenVPN username and password from the "Manual Configuration" page, not your account login). Copy the template credentials file and fill it in:

cp vpn-sandbox/.env.expressvpn.example vpn-sandbox/.env.expressvpn
Enter fullscreen mode Exit fullscreen mode

Git ignores this file, so your credentials never reach the repository.

Add a country. Add one line to vpn-sandbox/sandboxes.conf:

# name     | provider   | country | http_port | socks_port | timezone      | browser
germany    | expressvpn | Germany | 8889      | 1081       | Europe/Berlin | chrome
Enter fullscreen mode Exit fullscreen mode

Daily commands:

Task Command
Start one sandbox, or all ./vpn-sandbox/start-vpn.sh germany or ... all
Show the address of each sandbox ./vpn-sandbox/check-ip.sh
Open a browser in a sandbox ./vpn-sandbox/open-browser.sh germany
Open a different browser ./vpn-sandbox/open-browser.sh germany firefox
Run one command in a sandbox ./vpn-sandbox/run-in.sh germany curl https://ipinfo.io
Open a shell in a sandbox ./vpn-sandbox/run-in.sh germany zsh
Stop one sandbox, or all ./vpn-sandbox/stop-vpn.sh germany or ... all

The output of check-ip.sh shows each route side by side (example values):

SANDBOX        PORTS        SOCKS5 PUBLIC IP (through the HTTP proxy)
direct         -            -      "ip":"203.0.113.10","city":"YourCity","country":"US",
poland         8888/1080    ok     "ip":"188.212.135.227","city":"Warsaw","country":"PL",
germany        8889/1081    ok     ip=45.86.202.250 loc=DE
croatia        8892/1084    ok     "ip":"85.203.20.36","city":"Zagreb","country":"HR",
Enter fullscreen mode Exit fullscreen mode

Other applications. run-in.sh sets the standard proxy variables HTTP_PROXY, HTTPS_PROXY and ALL_PROXY and then runs your command. Most command-line tools read these variables: curl, git, pip, npm, yt-dlp. Desktop applications with a proxy setting can use the ports directly.

Caution: a tool that ignores the proxy variables connects directly, without the VPN, and shows no error. Test each new tool once against a site that shows your address.

Problems on the way, and their causes

Five problems looked like bugs in my setup. None of them was. Each one taught something about VPNs in general.

1. A server that accepts the login but forwards nothing

The first country I tried was Romania. The container connected, the login succeeded, and the log said Initialization Sequence Completed. But every request timed out.

The interface counters showed the cause. The tunnel interface tun0 had sent 19,400 bytes and received 0 bytes. The server accepted the tunnel but sent nothing back. The same container with a Netherlands or Germany server worked at once.

Lesson: a successful VPN login does not prove a working VPN. Test real traffic. The start script was updated to wait until a request through the proxy succeeds.

2. "Rate limit hit" from an IP lookup service

The check script asked ipinfo.io for the location of each sandbox. For Germany, the service answered HTTP 429: Rate limit hit. The VPN worked: other sites loaded through the same proxy.

The cause: thousands of VPN customers share each VPN address, and they all use the same free services. The free request quota of that address was already gone. The script was updated to fall back to the Cloudflare trace page (/cdn-cgi/trace), which shows the address and the country code.

Lesson: a VPN address is not your address. You share its reputation with strangers.

3. "Your download has not finished yet"

A file-download site refused a free download in the Poland browser with "Your download has not finished yet". At the same time, my normal browser had a download running on the same site. It looked as if the site had linked the two browsers.

It had not. I checked every connection of the Poland browser instance: all internet traffic went to the proxy port, and the proxy added no header with my home address. The two browsers had two different public addresses. The site limits free downloads by IP address, and another customer of my VPN provider, on the same Poland address, most likely had a download running.

Lesson: the same shared-address effect, from the other side. A limit "per IP address" applies to everybody behind that VPN address.

4. A country that is not where it says

The Belarus sandbox connected without errors. But three geolocation databases placed its address in three different countries:

Service Country it showed
ip-api.com Belarus
ipinfo.io and Cloudflare Netherlands
ifconfig.co United States

The address belongs to a hosting company with data centers in Western Europe and the US. The provider most likely runs "Belarus" as a virtual location: the server stands in another country, and the address is only registered as Belarusian.

Lesson: "the country of an IP address" is an opinion of a database, not a fact. Check the location with the databases that matter for your target websites.

5. All sandboxes stopped after a Docker update

After a restart of Docker Desktop, all seven containers stood still with exit code 1. The containers have the restart policy unless-stopped, but Docker had also installed an update during the restart and did not start them again. The exit code itself was harmless: the HTTP proxy only took more than 400 milliseconds to shut down.

Lesson: read the log before you restart anything. Here, the log showed a normal shutdown, so a plain restart was the correct action.

Limits

The setup separates network paths and cookies. It does not make one computer look like several computers.

Limit What it means
Browser fingerprint All browser instances run on the same hardware, with the same screen, fonts and graphics chip. A website can see the same fingerprint from two countries at the same time. A different browser in each sandbox (Chrome in one, Firefox in another) makes the fingerprints only partly different.
Time zone Every instance reports the time zone of the Mac. A Polish address with a California time zone is a strong sign of a VPN.
Shared addresses Other VPN customers share your VPN address. Their behavior affects your rate limits, download limits and blocks.
TCP only The proxies carry TCP traffic: web pages, downloads and most tools. They do not carry UDP traffic, which some video calls and games use.
Device limit Each running sandbox is one connected device on your VPN account. Seven sandboxes use seven devices.
Terms of use Many websites forbid VPNs or several connections to get around their limits. Read the terms of a website before you use this method on it.

For stronger separation, run a full virtual machine for each country. A virtual machine has its own hardware fingerprint, and every application inside it uses the VPN, UDP included. The cost is more memory and more setup.

Conclusion

One container, one port and one profile for each country give you several VPN locations side by side on one Mac. The routes send all container traffic into the tunnel. The firewall makes a kill switch. The separate profiles keep the cookies apart. And nothing ever falls back to your home connection.

The problems taught more than the design. A VPN login is not a working VPN. A VPN address is shared with strangers. The country of an address is the opinion of a database.

The code, the setup steps and more diagrams are in the repository: github.com/arunmk/network-sandbox (MIT license). It works with any VPN provider that Gluetun supports. Reports from other providers and browsers are welcome.

Top comments (0)