This script was designed to provide a lightweight, scriptable alternative to the official mullvad client. The script only requires python3, wireguard-tools (for wg and wg-quick) and then doas/sudo (required for system operations). Most systems come with python3 and sudo usually, only requiring WireGuard to be installed.
This was originally singular monolithic script written in bash, but quite quickly went past what you should in bash. The original was made as an alternative for no UI and no OpenVPN, since this time Mullvad has offically dropped OpenVPN in their official client, link to article.
The setup script, mullvad-setup.py, automates the process of fetching the relay list, generating keys, and writing configuration files to /etc/wireguard.
The script fetches the latest server list from the Mullvad API:
API_RELAYS_URL = "https://api.mullvad.net/public/relays/wireguard/v1/"
def fetch_relays():
"""Fetches the list of WireGuard relays."""
print(f"{CYAN}[+] Fetching server list from Mullvad...{NC}")
try:
with urllib.request.urlopen(API_RELAYS_URL) as response:
return json.loads(response.read().decode('utf-8'))
except Exception as e:
error_exit(f"Unable to fetch relay list: {e}")It then iterates through the returned JSON to generate individual configuration files for each relay, assigning the user’s specific IP address and DNS settings:
def write_configs(relays_data: dict, private_key: str, address: str):
"""Generates configuration files for all relays."""
# ...
config_content = f"""[Interface]
PrivateKey = {private_key}
Address = {address}
DNS = {DNS_SERVER}
[Peer]
PublicKey = {pubkey}
Endpoint = {ipv4}:51820
AllowedIPs = 0.0.0.0/0, ::/0
"""The core manager (src/mullvad.py) acts as a wrapper around wg-quick and the Mullvad verification API. It automatically handles privilege escalation via doas or sudo when required.
The connection logic verifies the server exists in the local cache before attempting to bring up the interface:
def connect(target_server: str) -> bool:
"""Connects to the specified server."""
servers = get_server_list()
if servers and target_server not in servers:
print(f"{RED}Error: Server '{target_server}' not found in server list.{NC}")
return False
# ...
result = run_command(["wg-quick", "up", config_path])The tool includes a verification command that checks the connection against Mullvad’s API to ensure the tunnel is active:
def verify() -> int:
"""Checks connection status via Mullvad API."""
try:
response = urllib.request.urlopen('https://am.i.mullvad.net/json', timeout=5)
data = json.loads(response.read().decode('utf-8'))
mullvad_exit_ip = data.get('mullvad_exit_ip', False)
# ...
if mullvad_exit_ip:
print(f"{GREEN}Connection Verified!{NC}")
return 0
except Exception as e:
print(f"{RED}Verification failed: {e}{NC}")
return 1# List available servers
mullvad list
# Connect to a specific server
mullvad connect se-sto-wg-011
# Connect to a random server in a specific region (e.g., Sweden)
mullvad random se
# Verify the connection
mullvad verifyTransition to Official Client: I have since transitioned from this custom Python script to the official Mullvad client. While the custom script served its purpose, the official client offers significant advantages for a production Gentoo environment.
Comparison Summary:
| Feature | Custom Python Script | Official Mullvad Client |
|---|---|---|
| Setup & Config | mullvad-setup.py fetches list; generates /etc/wireguard/*.conf. | mullvad-vpn handles config generation and updates automatically. |
| Connection Handling | Wrapper around wg-quick (wg-quick up/down). | Dedicated daemon handling interface bring-up/down. With the upcoming addition of Mullvad’s transition to gotatun. |
| Firewalling | Relies on system firewall (iptables/nftables) configured separately. | Built-in firewall rules to prevent traffic leaks before tunnel is active. |
| Kill Switch | None (requires external script). | Native “Disconnect Killswitch” feature. |
| Relay Selection | Manual or simple random command based on cached list. | Robust selection logic (country, city, ISP, exit country, etc.). |
| Verification | Checks IP via am.i.mullvad.net/json (manual). | Built-in verification tool mullvad status. |
| Maintenance | Requires manual script updates and config cache refreshing. | Managed via portage (Gentoo ebuilds) with dependency tracking. |
Conclusion: The shift to the official client eliminates the need to maintain the Python script and the setup utility. It also ensures that security patches and relay updates are handled by the package manager, rather than relying on a local cache file. The native firewall integration provides better protection against accidental leaks during connection drops, which was a limitation of the wg-quick based approach.