Skip to content

Instantly share code, notes, and snippets.

@ayufan
Created May 24, 2026 11:56
Show Gist options
  • Select an option

  • Save ayufan/d8f3a8914ee5c25434dbc16e4441cd9a to your computer and use it in GitHub Desktop.

Select an option

Save ayufan/d8f3a8914ee5c25434dbc16e4441cd9a to your computer and use it in GitHub Desktop.
Create Wireguard NordVPN config
#!/usr/bin/env python3
import argparse
import getpass
import json
import sys
import urllib.request
import urllib.error
from pathlib import Path
CREDENTIALS_FILE = Path(__file__).parent / ".nordvpn_private_key"
API_BASE = "https://api.nordvpn.com/v1"
def fetch_private_key(token: str) -> str:
req = urllib.request.Request(
f"{API_BASE}/users/services/credentials",
headers={"Authorization": f"Basic {__import__('base64').b64encode(f'token:{token}'.encode()).decode()}"},
)
try:
with urllib.request.urlopen(req) as resp:
data = json.load(resp)
key = data.get("nordlynx_private_key")
if not key:
print("Error: nordlynx_private_key not found in response.", file=sys.stderr)
sys.exit(1)
return key
except urllib.error.HTTPError as e:
print(f"Error fetching credentials: {e.code} {e.reason}", file=sys.stderr)
sys.exit(1)
def load_private_key() -> str:
if not CREDENTIALS_FILE.exists():
if sys.stdin.isatty():
print("Get your token at: https://my.nordaccount.com/dashboard/nordvpn/manual-configuration/")
token = getpass.getpass("NordVPN access token: ").strip()
if not token:
print("Error: no token provided.", file=sys.stderr)
sys.exit(1)
key = fetch_private_key(token)
CREDENTIALS_FILE.write_text(key + "\n")
CREDENTIALS_FILE.chmod(0o600)
print(f"Private key saved to {CREDENTIALS_FILE}")
return key
print("Error: no private key found. Run with --token <access-token> first.", file=sys.stderr)
print("Get your token at: https://my.nordaccount.com/dashboard/nordvpn/manual-configuration/", file=sys.stderr)
sys.exit(1)
return CREDENTIALS_FILE.read_text().strip()
def fetch_countries() -> list:
url = f"{API_BASE}/servers/countries"
with urllib.request.urlopen(url) as resp:
return json.load(resp)
def get_country_id(country: str) -> str:
for c in fetch_countries():
if c.get("code", "").lower() == country.lower() or c.get("name", "").lower() == country.lower():
return str(c["id"])
print(f"Error: country '{country}' not found.", file=sys.stderr)
sys.exit(1)
def get_server(country: str | None, index: int) -> dict:
url = (
f"{API_BASE}/servers/recommendations"
f"?filters[servers_technologies][identifier]=wireguard_udp&limit=50"
)
if country:
country_id = get_country_id(country)
url += f"&filters[country_id]={country_id}"
with urllib.request.urlopen(url) as resp:
servers = json.load(resp)
if not servers or index >= len(servers):
print(f"Error: no server found at index {index}.", file=sys.stderr)
sys.exit(1)
return servers[index]
def extract_wg_info(server: dict) -> tuple[str, str, str]:
hostname = server.get("hostname", "")
public_key = ""
port = "51820"
for tech in server.get("technologies", []):
if tech.get("identifier") == "wireguard_udp":
for meta in tech.get("metadata", []):
if meta.get("name") == "public_key":
public_key = meta.get("value", "")
elif meta.get("name") == "port":
port = meta.get("value", "51820")
return hostname, public_key, port
def generate_conf(private_key: str, hostname: str, public_key: str, port: str) -> str:
return f"""\
[Interface]
PrivateKey = {private_key}
Address = 10.5.0.2/32
DNS = 103.86.96.100
[Peer]
PublicKey = {public_key}
Endpoint = {hostname}:{port}
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
"""
def main():
parser = argparse.ArgumentParser(description="Generate a NordVPN WireGuard configuration.")
parser.add_argument("output", nargs="?", help="Path to write the .conf file")
parser.add_argument("--token", metavar="ACCESS_TOKEN", help="NordVPN access token (first time setup)")
parser.add_argument("--country", metavar="COUNTRY", help="Country code or name (e.g. US, Germany)")
parser.add_argument("--index", type=int, default=0, metavar="N", help="Server index from recommendations (default: 0)")
parser.add_argument("--country-list", action="store_true", help="List all available country codes and names")
args = parser.parse_args()
if args.country_list:
for c in sorted(fetch_countries(), key=lambda x: x.get("code", "")):
print(f"{c.get('code', ''):4} {c.get('name', '')}")
return
if not args.output:
parser.error("output is required unless --country-list is used")
if args.token:
print("Fetching private key...")
private_key = fetch_private_key(args.token)
CREDENTIALS_FILE.write_text(private_key + "\n")
CREDENTIALS_FILE.chmod(0o600)
print(f"Private key saved to {CREDENTIALS_FILE}")
private_key = load_private_key()
print(f"Fetching server recommendations{f' for {args.country}' if args.country else ''}...")
server = get_server(args.country, args.index)
hostname, public_key, port = extract_wg_info(server)
if not hostname or not public_key:
print("Error: could not extract WireGuard info from server.", file=sys.stderr)
sys.exit(1)
print(f"Selected server: {hostname}")
conf = generate_conf(private_key, hostname, public_key, port)
output = Path(args.output)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(conf)
output.chmod(0o600)
print(f"Configuration written to {output}")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment