Created
May 24, 2026 11:56
-
-
Save ayufan/d8f3a8914ee5c25434dbc16e4441cd9a to your computer and use it in GitHub Desktop.
Create Wireguard NordVPN config
This file contains hidden or 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 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