Last active
March 6, 2024 02:15
-
-
Save e9x/2677b1c284c9e46bee36452aed884d31 to your computer and use it in GitHub Desktop.
mullvad config doohickey
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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("]") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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