Skip to content

Instantly share code, notes, and snippets.

@e9x
Last active March 6, 2024 02:15
Show Gist options
  • Save e9x/2677b1c284c9e46bee36452aed884d31 to your computer and use it in GitHub Desktop.
Save e9x/2677b1c284c9e46bee36452aed884d31 to your computer and use it in GitHub Desktop.
mullvad config doohickey
#!/usr/bin/env python3
import os
import json
import random
from mullvad import (
generate_config,
wg_interface,
dns_blocklist,
connect_protos,
allow_traffic,
)
# Mullvad VPN wireguard config generator
# programmatically generates configs with multihop
# if you haven't already, download the wireguard relays from mullvad and place it in the current directory
# wget -O wireguard.json https://api.mullvad.net/www/relays/wireguard
# CONFIG
# directory to write wireguard configs
outdir = "/home/user/configs/"
interface = wg_interface(
# device name is optional
device_name="your device name",
address="your assigned ipv4/32,your assigned ipv6/128",
private_key="your private key",
)
# fmt: off
# DNS blocklist
# for example, block multiple services:
# dns_blocklist_flags = dns_blocklist.ADS | dns_blocklist.TRACKING | dns_blocklist.MALWARE | dns_blocklist.ADULT | dns_blocklist.GAMBLING | dns_blocklist.SOCIAL
dns_blocklist_flags = dns_blocklist.ADS
# fmt: on
# protocol to use when connecting to the mullvad relay
connect_proto = connect_protos.IPV4
# traffic to allow through the wireguard interface
allow_traffic_flags = allow_traffic.IPV4 | allow_traffic.IPV6
# killswitch according to https://mullvad.net/en/help/wireguard-and-mullvad-vpn#killswitch
killswitch = False
# set to false to not use multihop
multihop = True
# filter the list of relays to use for multihop
# for example, you want to use a relay close to you
def filter_multihop_relays(relay: dict):
return relay["city_code"] in ["syd", "mel"]
# filter the list of relays to generate a config for
# for example, you want to use a relay close to you
def filter_config_relays(relay: dict):
return relay["country_code"] in ["au", "br"]
# END OF CONFIG
try:
os.mkdir(outdir)
except FileExistsError:
pass
# delete previous config
for root, dirs, files in os.walk(outdir):
for name in files:
if os.path.splitext(name)[1] == ".conf":
os.remove(os.path.join(root, name))
relays_file = open("wireguard.json", "rt")
relays = json.load(relays_file)
relays_file.close()
multihop_relays = list(filter(filter_multihop_relays, relays))
config_relays = list(filter(filter_config_relays, relays))
for relay in config_relays:
# pick a random relay
multihop_relay = random.choice(multihop_relays) if multihop else None
config = generate_config(
interface,
relay,
dns_blocklist_flags=dns_blocklist_flags,
multihop=multihop_relay,
connect_proto=connect_proto,
allow_traffic_flags=allow_traffic_flags,
killswitch=killswitch,
)
file = open(os.path.join(outdir, relay["hostname"] + ".conf"), "w")
file.write(config)
file.close()
print("saved", len(config_relays), "configs to", outdir)
import re
from requests import get
from enum import IntFlag
# generate the dns_blocklist_servers from the blocklist github
# used for development
readme = get(
"https://raw.githubusercontent.com/mullvad/dns-blocklists/main/README.md"
).text
supported = ["ad", "tracker", "malware", "adult content", "gambling", "social media"]
def get_syntax(blocking: [str]):
for v in blocking:
if not v in supported:
raise IndexError("block type is not supported:", v)
syntax = []
if "ad" in blocking:
syntax.append("dns_blocklist.ADS")
if "tracker" in blocking:
syntax.append("dns_blocklist.TRACKING")
if "malware" in blocking:
syntax.append("dns_blocklist.MALWARE")
if "adult content" in blocking:
syntax.append("dns_blocklist.ADULT")
if "gambling" in blocking:
syntax.append("dns_blocklist.GAMBLING")
if "social media" in blocking:
syntax.append("dns_blocklist.SOCIAL")
return " | ".join(syntax)
print("dns_blocklist_servers = [")
for server in re.findall(r" {4}(\d+.\d+.\d+.\d+) - (.*?)$", readme, flags=re.MULTILINE):
dns: str = server[0]
name: str = server[1]
# typo
if name == "Trackers only":
name = "Tracker blocking only"
# another typo, tracker is tracking
if name == "Gambling blocking, malware blocking and tracking blocking":
name = "Gambling blocking, malware blocking and tracker blocking"
# another typo, missing a comma
if name == "Gambling blocking ad blocking and malware blocking":
name = "Gambling blocking, ad blocking and malware blocking"
name = name.lower().lstrip()
# typo was made twice, adult content blocking is just adult blocking
name = re.sub(r"adult blocking", "adult content blocking", name)
name = re.sub(r' \("everything"\)$', "", name)
name = re.sub(r" only$", "", name)
name = re.sub(r",? and ", ", ", name)
# name = re.sub(r" (only|blocking)$", "", name)
# name = re.sub(r" blocking, ", ", ", name)
name = re.sub(r" blocking($|,)", "\\1", name)
blocking = name.split(", ")
syntax = get_syntax(blocking)
print(f' ("{dns}", {syntax}),')
print("]")
class dns_blocklist:
ADS = 1
TRACKING = 2
MALWARE = 4
ADULT = 8
GAMBLING = 16
SOCIAL = 32
# fmt: off
dns_blocklist_servers: list[tuple[str, int]] = [
("100.64.0.1", dns_blocklist.ADS),
("100.64.0.2", dns_blocklist.TRACKING),
("100.64.0.3", dns_blocklist.ADS | dns_blocklist.TRACKING),
("100.64.0.4", dns_blocklist.MALWARE),
("100.64.0.5", dns_blocklist.ADS | dns_blocklist.MALWARE),
("100.64.0.6", dns_blocklist.TRACKING | dns_blocklist.MALWARE),
("100.64.0.7", dns_blocklist.ADS | dns_blocklist.TRACKING | dns_blocklist.MALWARE),
("100.64.0.8", dns_blocklist.ADULT),
("100.64.0.9", dns_blocklist.ADS | dns_blocklist.ADULT),
("100.64.0.10", dns_blocklist.TRACKING | dns_blocklist.ADULT),
("100.64.0.11", dns_blocklist.ADS | dns_blocklist.TRACKING | dns_blocklist.ADULT),
("100.64.0.12", dns_blocklist.MALWARE | dns_blocklist.ADULT),
("100.64.0.13", dns_blocklist.ADS | dns_blocklist.MALWARE | dns_blocklist.ADULT),
("100.64.0.14", dns_blocklist.TRACKING | dns_blocklist.MALWARE | dns_blocklist.ADULT),
("100.64.0.15", dns_blocklist.ADS | dns_blocklist.TRACKING | dns_blocklist.MALWARE | dns_blocklist.ADULT),
("100.64.0.16", dns_blocklist.GAMBLING),
("100.64.0.17", dns_blocklist.ADS | dns_blocklist.GAMBLING),
("100.64.0.18", dns_blocklist.TRACKING | dns_blocklist.GAMBLING),
("100.64.0.19", dns_blocklist.ADS | dns_blocklist.TRACKING | dns_blocklist.GAMBLING),
("100.64.0.20", dns_blocklist.MALWARE | dns_blocklist.GAMBLING),
("100.64.0.21", dns_blocklist.ADS | dns_blocklist.MALWARE | dns_blocklist.GAMBLING),
("100.64.0.22", dns_blocklist.TRACKING | dns_blocklist.MALWARE | dns_blocklist.GAMBLING),
("100.64.0.23", dns_blocklist.ADS | dns_blocklist.TRACKING | dns_blocklist.MALWARE | dns_blocklist.GAMBLING),
("100.64.0.24", dns_blocklist.ADULT | dns_blocklist.GAMBLING),
("100.64.0.25", dns_blocklist.ADS | dns_blocklist.ADULT | dns_blocklist.GAMBLING),
("100.64.0.26", dns_blocklist.TRACKING | dns_blocklist.ADULT | dns_blocklist.GAMBLING),
("100.64.0.27", dns_blocklist.ADS | dns_blocklist.TRACKING | dns_blocklist.ADULT | dns_blocklist.GAMBLING),
("100.64.0.28", dns_blocklist.MALWARE | dns_blocklist.ADULT | dns_blocklist.GAMBLING),
("100.64.0.29", dns_blocklist.ADS | dns_blocklist.MALWARE | dns_blocklist.ADULT | dns_blocklist.GAMBLING),
("100.64.0.30", dns_blocklist.TRACKING | dns_blocklist.MALWARE | dns_blocklist.ADULT | dns_blocklist.GAMBLING),
("100.64.0.31", dns_blocklist.ADS | dns_blocklist.TRACKING | dns_blocklist.MALWARE | dns_blocklist.ADULT | dns_blocklist.GAMBLING),
("100.64.0.32", dns_blocklist.SOCIAL),
("100.64.0.33", dns_blocklist.ADS | dns_blocklist.SOCIAL),
("100.64.0.34", dns_blocklist.TRACKING | dns_blocklist.SOCIAL),
("100.64.0.35", dns_blocklist.ADS | dns_blocklist.TRACKING | dns_blocklist.SOCIAL),
("100.64.0.36", dns_blocklist.MALWARE | dns_blocklist.SOCIAL),
("100.64.0.37", dns_blocklist.ADS | dns_blocklist.MALWARE | dns_blocklist.SOCIAL),
("100.64.0.38", dns_blocklist.TRACKING | dns_blocklist.MALWARE | dns_blocklist.SOCIAL),
("100.64.0.39", dns_blocklist.ADS | dns_blocklist.TRACKING | dns_blocklist.MALWARE | dns_blocklist.SOCIAL),
("100.64.0.40", dns_blocklist.ADULT | dns_blocklist.SOCIAL),
("100.64.0.41", dns_blocklist.ADS | dns_blocklist.ADULT | dns_blocklist.SOCIAL),
("100.64.0.42", dns_blocklist.TRACKING | dns_blocklist.ADULT | dns_blocklist.SOCIAL),
("100.64.0.43", dns_blocklist.ADS | dns_blocklist.TRACKING | dns_blocklist.ADULT | dns_blocklist.SOCIAL),
("100.64.0.44", dns_blocklist.MALWARE | dns_blocklist.ADULT | dns_blocklist.SOCIAL),
("100.64.0.45", dns_blocklist.ADS | dns_blocklist.MALWARE | dns_blocklist.ADULT | dns_blocklist.SOCIAL),
("100.64.0.46", dns_blocklist.TRACKING | dns_blocklist.MALWARE | dns_blocklist.ADULT | dns_blocklist.SOCIAL),
("100.64.0.47", dns_blocklist.ADS | dns_blocklist.TRACKING | dns_blocklist.MALWARE | dns_blocklist.ADULT | dns_blocklist.SOCIAL),
("100.64.0.48", dns_blocklist.GAMBLING | dns_blocklist.SOCIAL),
("100.64.0.49", dns_blocklist.ADS | dns_blocklist.GAMBLING | dns_blocklist.SOCIAL),
("100.64.0.50", dns_blocklist.TRACKING | dns_blocklist.GAMBLING | dns_blocklist.SOCIAL),
("100.64.0.51", dns_blocklist.ADS | dns_blocklist.TRACKING | dns_blocklist.GAMBLING | dns_blocklist.SOCIAL),
("100.64.0.52", dns_blocklist.MALWARE | dns_blocklist.GAMBLING | dns_blocklist.SOCIAL),
("100.64.0.53", dns_blocklist.ADS | dns_blocklist.MALWARE | dns_blocklist.GAMBLING | dns_blocklist.SOCIAL),
("100.64.0.54", dns_blocklist.TRACKING | dns_blocklist.MALWARE | dns_blocklist.GAMBLING | dns_blocklist.SOCIAL),
("100.64.0.55", dns_blocklist.ADS | dns_blocklist.TRACKING | dns_blocklist.MALWARE | dns_blocklist.GAMBLING | dns_blocklist.SOCIAL),
("100.64.0.56", dns_blocklist.ADULT | dns_blocklist.GAMBLING | dns_blocklist.SOCIAL),
("100.64.0.57", dns_blocklist.ADS | dns_blocklist.ADULT | dns_blocklist.GAMBLING | dns_blocklist.SOCIAL),
("100.64.0.58", dns_blocklist.TRACKING | dns_blocklist.ADULT | dns_blocklist.GAMBLING | dns_blocklist.SOCIAL),
("100.64.0.59", dns_blocklist.ADS | dns_blocklist.TRACKING | dns_blocklist.ADULT | dns_blocklist.GAMBLING | dns_blocklist.SOCIAL),
("100.64.0.60", dns_blocklist.MALWARE | dns_blocklist.ADULT | dns_blocklist.GAMBLING | dns_blocklist.SOCIAL),
("100.64.0.61", dns_blocklist.ADS | dns_blocklist.MALWARE | dns_blocklist.ADULT | dns_blocklist.GAMBLING | dns_blocklist.SOCIAL),
("100.64.0.62", dns_blocklist.TRACKING | dns_blocklist.MALWARE | dns_blocklist.ADULT | dns_blocklist.GAMBLING | dns_blocklist.SOCIAL),
("100.64.0.63", dns_blocklist.ADS | dns_blocklist.TRACKING | dns_blocklist.MALWARE | dns_blocklist.ADULT | dns_blocklist.GAMBLING | dns_blocklist.SOCIAL),
]
# fmt: on
def get_dns_server(dns_blocklist_flags: int) -> str:
for dns, flags in dns_blocklist_servers:
if flags == dns_blocklist_flags:
return dns
raise IndexError("bad dns blocklist flags", dns_blocklist_flags)
def join_with_comma(lst: list[str]) -> str:
if len(lst) > 1: # Check if lst has more than 1 element
return ", ".join(lst[:-1]) + ", and " + lst[-1]
elif lst: # Check if lst is not empty
return lst[0]
else:
return ""
def dns_comment(dns_blocklist_flags: int) -> str:
blocking = []
if dns_blocklist_flags & dns_blocklist.ADS:
blocking.append("ads")
if dns_blocklist_flags & dns_blocklist.TRACKING:
blocking.append("tracking")
if dns_blocklist_flags & dns_blocklist.MALWARE:
blocking.append("malware")
if dns_blocklist_flags & dns_blocklist.ADULT:
blocking.append("adult content")
if dns_blocklist_flags & dns_blocklist.GAMBLING:
blocking.append("gambling")
if dns_blocklist_flags & dns_blocklist.SOCIAL:
blocking.append("social media")
return "Block " + join_with_comma(blocking)
class allow_traffic:
"""
set of flags, can be ipv4, ipv6, or both
"""
IPV4 = 1
IPV6 = 2
def get_allow_traffic(allow_traffic_flags: int) -> [str, str]:
if allow_traffic_flags == allow_traffic.IPV4:
return ("0.0.0.0/0", "Only IPv4 traffic")
elif allow_traffic_flags == allow_traffic.IPV6:
return ("::0/0", "Only IPv6 traffic")
elif allow_traffic_flags == allow_traffic.IPV4 | allow_traffic.IPV6:
return ("0.0.0.0/0,::0/0", "Both IPv4 and IPv6 traffic")
else:
raise IndexError("bad allow traffic flags", allow_traffic_flags)
class wg_interface:
def __init__(self, address: str, private_key: str, device_name: str | None = None):
self.device_name = device_name
self.address = address
self.private_key = private_key
killswitch_config = """
PostUp = iptables -I OUTPUT ! -o 0 -m mark ! --mark $(wg show 0 fwmark) -m addrtype ! --dst-type LOCAL -j REJECT && ip6tables -I OUTPUT ! -o 0 -m mark ! --mark $(wg show 0 fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
PreDown = iptables -D OUTPUT ! -o 0 -m mark ! --mark $(wg show 0 fwmark) -m addrtype ! --dst-type LOCAL -j REJECT && ip6tables -D OUTPUT ! -o 0 -m mark ! --mark $(wg show 0 fwmark) -m addrtype ! --dst-type LOCAL -j REJECT"""
class connect_protos:
"""
can be either ipv4 or ipv6
"""
IPV4 = 4
IPV6 = 6
def get_relay_addr_in(relay, proto: int) -> str:
if proto == connect_protos.IPV4:
return relay["ipv4_addr_in"]
elif proto == connect_protos.IPV6:
return relay["ipv6_addr_in"]
else:
raise IndexError("bad relay proto", proto)
def get_endpoint(
relay: dict,
multihop: dict | None,
connect_proto: int,
):
if multihop:
return (
get_relay_addr_in(multihop, connect_proto)
+ ":"
+ str(relay["multihop_port"])
)
else:
return get_relay_addr_in(relay, connect_proto) + ":51820"
def generate_config(
interface: wg_interface,
relay: dict,
connect_proto: int = connect_protos.IPV4,
allow_traffic_flags: int = allow_traffic.IPV4 | allow_traffic.IPV6,
dns_blocklist_flags: int = dns_blocklist.ADS,
killswitch=False,
multihop: dict | None = None,
):
traffic = get_allow_traffic(allow_traffic_flags)
return f"""[Interface]{"\n# Device: " + interface.device_name if interface.device_name else ""}
PrivateKey = {interface.private_key}
Address = {interface.address}
# {dns_comment(dns_blocklist_flags)}
DNS = {get_dns_server(dns_blocklist_flags)}{killswitch_config if killswitch else ""}
[Peer]
PublicKey = {relay["pubkey"]}
# {traffic[1]}
AllowedIPs = {traffic[0]}{f"\n# Multihop: {multihop["hostname"]} -> {relay["hostname"]}" if multihop else ""}
Endpoint = {get_endpoint(relay, multihop, connect_proto)}"""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment