commit 4d3e204a2d2d59299df571124450507c99743086 Author: Patrick Neff Date: Thu May 2 11:43:19 2024 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3608aee --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.conf +*.png +*.ini diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..e9a19ab --- /dev/null +++ b/flake.lock @@ -0,0 +1,64 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": [ + "systems" + ] + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1713537308, + "narHash": "sha256-XtTSSIB2DA6tOv+l0FhvfDMiyCmhoRbNB+0SeInZkbk=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "5c24cf2f0a12ad855f444c30b2421d044120c66f", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "systems": "systems" + } + }, + "systems": { + "locked": { + "lastModified": 1680978846, + "narHash": "sha256-Gtqg8b/v49BFDpDetjclCYXm8mAnTrUzR0JnE2nv5aw=", + "owner": "nix-systems", + "repo": "x86_64-linux", + "rev": "2ecfcac5e15790ba6ce360ceccddb15ad16d08a8", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "x86_64-linux", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..f12efe6 --- /dev/null +++ b/flake.nix @@ -0,0 +1,29 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + systems.url = "github:nix-systems/x86_64-linux"; + flake-utils = { + url = "github:numtide/flake-utils"; + inputs.systems.follows = "systems"; + }; + }; + outputs = { + nixpkgs, + flake-utils, + ... + }: + flake-utils.lib.eachDefaultSystem (system: let + pkgs = import nixpkgs { + inherit system; + }; + in { + devShells.default = pkgs.mkShellNoCC { + name = "wireguard config"; + packages = with pkgs; [ + wireguard-tools + qrencode + python3 + ]; + }; + }); +} diff --git a/generate_config.py b/generate_config.py new file mode 100755 index 0000000..bf07806 --- /dev/null +++ b/generate_config.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python + +import subprocess +import argparse +import configparser +import sys + +from pathlib import Path +from string import Template +from typing import Iterable, TypedDict, NamedTuple + +TEMPLATE_STR = """[Interface] +PrivateKey = $private_key +Address = $address +DNS = $dns + +[Peer] +PublicKey = $remote_public_key +PresharedKey = $psk +AllowedIPs = $allowed_ips +Endpoint = $endpoint""" +TEMPLATE = Template(TEMPLATE_STR) +ENDPOINT = "127.0.0.1:51820" +PUBLIC_KEY = "REPLACE_WITH_SERVER_PUBLIC_KEY" +DNS = "127.0.0.1" + + +class ConfigArgs(TypedDict): + remote_public_key: str + public_key: str + private_key: str + psk: str + address: str + allowed_ips: str + endpoint: str + dns: str + + +class Config(NamedTuple): + public_key: str + psk: str + config: str + + +def main(): + # Query for connection info (Public key from server, Address, AllowedIPs, etc...) + args = parse_args() + config = load_config(args.config) + + if args.outfile and args.outfile.exists(): + print("The file {args.outfile} already exists. Do you want to overwrite? y/N ", end="") + overwrite_input = input() + overwrite = overwrite_input.strip()[0].lower() == 'y' + if not overwrite: + sys.exit(0) + + address = args.address + allowed_ips = args.allowed_ips + public_key = args.public_key or config["wireguard"]["public_key"] + endpoint = args.endpoint or config["wireguard"]["endpoint"] + dns = args.dns or config["wireguard"]["dns"] + psk = args.psk + public_key, psk, config = generate_config( + address, + allowed_ips, + public_key, + endpoint, + dns, + psk, + ) + if not args.quiet: + print_config(public_key, psk, config) + if args.outfile: + save_config_file(args.outfile, config) + + +def print_config(public_key: str, psk: str, config: str): + print(f"### Public Key:\n{public_key}", end="\n\n") + print(f"### PSK:\n{psk}", end="\n\n") + print(f"### Wireguard Config:\n{config}", end="\n\n") + + +def generate_config( + address: str, + allowed_ips: Iterable[str], + remote_public_key: str, + endpoint: str, + dns: str, + psk: str | None, +) -> Config: + allowed_ips = ", ".join(allowed_ips) + private_key = get_private_key() + public_key = get_public_key(private_key) + psk = psk or get_psk() + config = { + "private_key": private_key, + "public_key": public_key, + "remote_public_key": remote_public_key, + "psk": psk, + "address": address, + "allowed_ips": allowed_ips, + "endpoint": endpoint, + "dns": dns, + } + config_string = TEMPLATE.substitute(**config) + return Config(public_key, psk, config_string) + + +def get_private_key() -> str: + result = subprocess.run(["wg", "genkey"], stdout=subprocess.PIPE) + private_key = result.stdout.strip() + return private_key.decode("utf-8") + + +def get_public_key(private_key: str) -> str: + process = subprocess.Popen( + ["wg", "pubkey"], stdin=subprocess.PIPE, stdout=subprocess.PIPE + ) + result = process.communicate(input=private_key.encode("utf-8")) + public_key = result[0].strip() + return public_key.decode("utf-8") + + +def get_psk() -> str: + result = subprocess.run(["wg", "genpsk"], stdout=subprocess.PIPE) + psk = result.stdout.strip() + return psk.decode("utf-8") + + +def save_config_file(filename: Path, config: str): + filename.touch() + filename.chmod(0o600) + filename.write_text(config) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="WireGuard Config File Generator") + parser.add_argument("address", help="The remote wireguard server hostname or address") + parser.add_argument("allowed_ips", nargs="+", help="The list of allowed IP ranges for the routed network") + parser.add_argument("-c", "--config", default="config.ini", type=Path, help="The config file to use for server configuration") + parser.add_argument("-o", "--outfile", type=Path, help="The file to write to") + parser.add_argument("-q", "--quiet", default=False, action="store_true", help="Suppress all output") + parser.add_argument("-p", "--psk", help="The Pre-Shared Key to use") + parser.add_argument("-e", "--endpoint", help="The Wireguard server to use") + parser.add_argument("-d", "--dns", help="The DNS server to use") + parser.add_argument("-P", "--public-key", help="The Servers public key to use") + return parser.parse_args() + + +def load_config(filename: Path) -> configparser.ConfigParser: + config = configparser.ConfigParser() + if not filename.exists(): + config["wireguard"] = { + "endpoint": ENDPOINT, + "public_key": PUBLIC_KEY, + "dns": DNS, + } + with filename.open("w") as fp: + config.write(fp) + + config.read(filename) + return config + + +if __name__ == "__main__": + main() diff --git a/generate_qr_code.py b/generate_qr_code.py new file mode 100755 index 0000000..2ffd699 --- /dev/null +++ b/generate_qr_code.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python + +import subprocess +import argparse +import sys + +from pathlib import Path + + +def main(): + args = parse_args() + config_file = Path(args.config) + config = config_file.read_text().strip() + + if args.outfile and args.outfile.exists(): + print("The file {args.outfile} already exists. Do you want to overwrite? y/N ", end="") + overwrite_input = input() + overwrite = overwrite_input.strip()[0].lower() == 'y' + if not overwrite: + sys.exit(0) + + if not args.quiet: + print_qr_code(config) + + if args.outfile: + save_qr_code_image(args.outfile, config) + + +def print_qr_code(config: str): + print("### QR Code:") + print(get_qr_code(config)) + +def get_qr_code(config: str) -> str: + process = subprocess.Popen( + ["qrencode", "-t", "utf8"], stdin=subprocess.PIPE, stdout=subprocess.PIPE + ) + result = process.communicate(input=config.encode("utf-8")) + return result[0].decode("utf-8") + + +def save_qr_code_image(filename: Path, config: str): + process = subprocess.Popen( + ["qrencode", "-t", "png", "-o", filename], stdin=subprocess.PIPE + ) + process.communicate(input=config.encode("utf-8")) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="WireGuard QR Code Generator") + parser.add_argument("config", help="The WireGuard config file to generate a qr code for") + parser.add_argument("-o", "--outfile", type=Path, help="The ouput file for the generated PNG image") + parser.add_argument("-q", "--quiet", action="store_true", help="Suppress the output of the UTF-8 QR Code") + return parser.parse_args() + + +if __name__ == "__main__": + main()