From e192076414f1d3ac5186deef314b11c5ece929e7 Mon Sep 17 00:00:00 2001 From: Kumi Date: Thu, 18 Jul 2024 17:24:24 +0200 Subject: [PATCH] feat: setup WireGuard VPN server automation across providers Introduce a new feature to automate the creation and configuration of WireGuard VPN servers across multiple cloud providers (Hetzner, AWS, DigitalOcean, Azure). Changes include: - Added a `.gitignore` file to exclude `config.ini` and `venv/`. - Provided `config.dist.ini` with configuration templates for supported providers. - Created a `requirements.txt` listing all necessary dependencies. - Developed `worker.py` to handle server creation, WireGuard setup, and configuration management. This enhancement simplifies and standardizes VPN server deployment, improving operational efficiency and consistency. --- .gitignore | 2 + config.dist.ini | 51 ++++++ requirements.txt | 8 + worker.py | 392 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 453 insertions(+) create mode 100644 .gitignore create mode 100644 config.dist.ini create mode 100644 requirements.txt create mode 100644 worker.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8628898 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +config.ini +venv/ \ No newline at end of file diff --git a/config.dist.ini b/config.dist.ini new file mode 100644 index 0000000..3f15976 --- /dev/null +++ b/config.dist.ini @@ -0,0 +1,51 @@ +[wireguard] +# WireGuard configuration for the VPN server + +address = 10.123.123.2, fdfd:fdfd:1234::2 +listen_port = 1234 + +# Peer configuration + +peer_public_key = public key of the peer +peer_allowed_ips = 10.123.123.1, fdfd:fdfd:1234::1 +peer_endpoint = 1.2.3.4:1234 +peer_persistent_keepalive = 25 + +# IP addresses that should be routed through the VPN - this is returned in the WireGuard configuration generated +# The default value below routes all Google IP addresses as well as https://icanhazip.com as used in Invidious' smart-ipv6-rotator +# The addresses of the server itself are automatically routed through the VPN +# +# To route all traffic through the VPN, use: +# routed_addresses = 0.0.0.0/0, ::/0 +routed_addresses = 2001:4860:4000::/36, 2404:6800:4000::/36, 2607:f8b0:4000::/36, 2800:3f0:4000::/36, 2a00:1450:4000::/36, 2c0f:fb50:4000::/36, 2606:4700::6812:7261 + +[hetzner] +api_token = your_hetzner_api_token +location = nbg1 +server_type = cx22 +image = debian-12 + +[aws] +access_key = your_aws_access_key +secret_key = your_aws_secret_key +region = us-east-1 +instance_type = t2.micro +ami_name = debian-12-amd64-20240702-1796 +key_pair = your_aws_key_pair + +[digitalocean] +api_token = your_digitalocean_api_token +region = nyc3 +server_type = s-1vcpu-1gb +image = debian-12-x64 + +[azure] +subscription_id = your_azure_subscription_id +client_id = your_azure_client_id +client_secret = your_azure_client_secret +tenant_id = your_azure_tenant_id +location = eastus +vm_size = Standard_B1s +image_publisher = Debian +image_offer = debian-11 +image_sku = 11-backports-gen2 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..89b7249 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +paramiko +hcloud +boto3 +python-digitalocean +azure-mgmt-compute +azure-mgmt-network +azure-mgmt-resource +azure-identity \ No newline at end of file diff --git a/worker.py b/worker.py new file mode 100644 index 0000000..f10e3c8 --- /dev/null +++ b/worker.py @@ -0,0 +1,392 @@ +import os +import paramiko +import subprocess +import time +import logging +import random +import string +import argparse +import configparser +import boto3 +from digitalocean import Manager, Droplet +from hcloud import Client +from hcloud.server_types.client import ServerType +from hcloud.images.client import Image +from hcloud.locations.client import LocationsClient +from hcloud.ssh_keys.client import SSHKeysClient +from hcloud.servers.domain import ServerCreatePublicNetwork +from azure.identity import ClientSecretCredential +from azure.mgmt.compute import ComputeManagementClient +from azure.mgmt.network import NetworkManagementClient +from azure.mgmt.resource import ResourceManagementClient + +# Set up logging +logging.basicConfig( + format="[%(asctime)s] %(levelname)s: %(message)s", + level=logging.INFO, +) + +# Read configuration +config = configparser.ConfigParser() +config.read("config.ini") + +# Argument parser +parser = argparse.ArgumentParser(description="Create and configure a Wireguard server.") +parser.add_argument( + "--provider", + type=str, + choices=["hetzner", "aws", "digitalocean", "azure"], + default="hetzner", + help="Cloud provider (default: hetzner)", +) +parser.add_argument("--location", type=str, help="Server location") +parser.add_argument("--server_type", type=str, help="Server type") +args = parser.parse_args() + +random_string = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) + + +# Function to create a server on Hetzner Cloud +def create_hetzner_server(location, server_type): + api_token = config["hetzner"]["api_token"] + client = Client(token=api_token) + server_type = ServerType(name=server_type) + image_name = config["hetzner"]["image"] + image = Image(name=image_name) + location = LocationsClient(client).get_by_name(location) + ssh_key = SSHKeysClient(client).get_all()[0] + public_network = ServerCreatePublicNetwork(enable_ipv4=False, enable_ipv6=True) + + logging.info("Creating Hetzner server...") + + server = client.servers.create( + name="wireguard-vps-" + random_string, + server_type=server_type, + image=image, + location=location, + ssh_keys=[ssh_key], + public_net=public_network, + ) + + while not server.server.status == "running": + time.sleep(5) + server.server.reload() + + return server.server + + +# Function to get the latest AMI ID by name +def get_ami_id_by_name(ec2_client, ami_name): + response = ec2_client.describe_images( + Filters=[ + {"Name": "name", "Values": [ami_name]}, + {"Name": "state", "Values": ["available"]}, + ], + Owners=["self", "amazon"], + ) + images = sorted(response["Images"], key=lambda x: x["CreationDate"], reverse=True) + if images: + return images[0]["ImageId"] + else: + raise ValueError(f"No AMI found with name pattern {ami_name}") + + +# Function to create a server on AWS +def create_aws_server(location, server_type): + access_key = config["aws"]["access_key"] + secret_key = config["aws"]["secret_key"] + region = config["aws"]["region"] + ami_name = config["aws"]["ami_name"] + key_pair = config["aws"]["key_pair"] + + ec2_client = boto3.client( + "ec2", + aws_access_key_id=access_key, + aws_secret_access_key=secret_key, + region_name=region, + ) + + ami_id = get_ami_id_by_name(ec2_client, ami_name) + + ec2 = boto3.resource( + "ec2", + aws_access_key_id=access_key, + aws_secret_access_key=secret_key, + region_name=region, + ) + + logging.info("Creating AWS server...") + + instances = ec2.create_instances( + ImageId=ami_id, + MinCount=1, + MaxCount=1, + InstanceType=server_type, + KeyName=key_pair, + ) + + return instances[0] + + +# Function to create a server on DigitalOcean +def create_digitalocean_server(location, server_type): + api_token = config["digitalocean"]["api_token"] + manager = Manager(token=api_token) + + logging.info("Creating DigitalOcean server...") + + droplet = Droplet( + token=api_token, + name="wireguard-vps-" + random_string, + region=location, + image="debian-12-x64", + size_slug=server_type, + ssh_keys=manager.get_all_sshkeys(), + ) + + droplet.create() + return droplet + + +# Function to create a server on Azure +def create_azure_server(location, server_type): + subscription_id = config["azure"]["subscription_id"] + client_id = config["azure"]["client_id"] + client_secret = config["azure"]["client_secret"] + tenant_id = config["azure"]["tenant_id"] + vm_size = server_type + image_publisher = config["azure"]["image_publisher"] + image_offer = config["azure"]["image_offer"] + image_sku = config["azure"]["image_sku"] + + credential = ClientSecretCredential( + client_id=client_id, client_secret=client_secret, tenant_id=tenant_id + ) + resource_client = ResourceManagementClient(credential, subscription_id) + compute_client = ComputeManagementClient(credential, subscription_id) + network_client = NetworkManagementClient(credential, subscription_id) + + resource_group_name = f"rg-{random_string}" + network_name = f"vnet-{random_string}" + subnet_name = f"subnet-{random_string}" + ip_name = f"ip-{random_string}" + nic_name = f"nic-{random_string}" + vm_name = f"vm-{random_string}" + + logging.info("Creating Azure resource group...") + resource_client.resource_groups.create_or_update( + location=location, resource_group_name=resource_group_name + ) + + logging.info("Creating Azure virtual network...") + network_client.virtual_networks.begin_create_or_update( + resource_group_name, + network_name, + {"location": location, "address_space": {"address_prefixes": ["10.0.0.0/16"]}}, + ).result() + + logging.info("Creating Azure subnet...") + network_client.subnets.begin_create_or_update( + resource_group_name, + network_name, + subnet_name, + {"address_prefix": "10.0.0.0/24"}, + ).result() + + logging.info("Creating Azure public IP address...") + ip_address = network_client.public_ip_addresses.begin_create_or_update( + resource_group_name, + ip_name, + {"location": location, "public_ip_allocation_method": "Dynamic"}, + ).result() + + logging.info("Creating Azure network interface...") + nic = network_client.network_interfaces.begin_create_or_update( + resource_group_name, + nic_name, + { + "location": location, + "ip_configurations": [ + { + "name": "ipconfig1", + "public_ip_address": ip_address, + "subnet": { + "id": f"/subscriptions/{subscription_id}/resourceGroups/{resource_group_name}/providers/Microsoft.Network/virtualNetworks/{network_name}/subnets/{subnet_name}" + }, + } + ], + }, + ).result() + + logging.info("Creating Azure virtual machine...") + vm = compute_client.virtual_machines.begin_create_or_update( + resource_group_name, + vm_name, + { + "location": location, + "storage_profile": { + "image_reference": { + "publisher": image_publisher, + "offer": image_offer, + "sku": image_sku, + "version": "latest", + } + }, + "hardware_profile": {"vm_size": vm_size}, + "os_profile": { + "computer_name": vm_name, + "admin_username": "azureuser", + "admin_password": "P@ssw0rd!", + }, + "network_profile": {"network_interfaces": [{"id": nic.id}]}, + }, + ).result() + + return vm, ip_address.ip_address + + +# Function to execute commands on the server +def ssh_execute_command(ip, command): + logging.info(f"Executing command on {ip}: {command}") + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(ip, username="root", key_filename=os.path.expanduser("~/.ssh/id_rsa")) + stdin, stdout, stderr = ssh.exec_command(command) + output = stdout.read().decode() + error = stderr.read().decode() + ssh.close() + return output, error + + +# Function to generate a random private key +def generate_private_key(): + return subprocess.check_output("wg genkey", shell=True).decode().strip() + + +# Function to convert private key to public key +def private_to_public_key(private_key): + return ( + subprocess.check_output(f"echo {private_key} | wg pubkey", shell=True) + .decode() + .strip() + ) + + +# Function to generate a random preshared key +def generate_preshared_key(): + return subprocess.check_output("wg genpsk", shell=True).decode().strip() + + +# Function to generate Wireguard keys +def generate_wireguard_keys(): + logging.info("Generating Wireguard keys...") + private_key = generate_private_key() + public_key = private_to_public_key(private_key) + preshared_key = generate_preshared_key() + return private_key, public_key, preshared_key + + +# Function to configure Wireguard on the VPS +def configure_wireguard(server_ip, private_key, public_key, preshared_key): + address = config["wireguard"]["address"] + listen_port = config["wireguard"]["listen_port"] + + peer_public_key = config["wireguard"]["peer_public_key"] + allowed_ips = config["wireguard"]["peer_allowed_ips"] + endpoint = config["wireguard"]["peer_endpoint"] + persistent_keepalive = config["wireguard"]["peer_persistent_keepalive"] + + wg_config = f""" +[Interface] +Address = {address} +PrivateKey = {private_key} +ListenPort = {listen_port} + +[Peer] +PublicKey = {peer_public_key} +PresharedKey = {preshared_key} +AllowedIPs = {allowed_ips} +Endpoint = {endpoint} +PersistentKeepalive = {persistent_keepalive} +""" + + ssh_execute_command(server_ip, f"echo '{wg_config}' > /etc/wireguard/wg0.conf") + ssh_execute_command(server_ip, "wg-quick up wg0") + + # Configure ip6tables + ip6tables_rules = [ + "ip6tables -A FORWARD -i wg0 -j ACCEPT", + "ip6tables -A FORWARD -o wg0 -j ACCEPT", + "ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE", + ] + + for rule in ip6tables_rules: + ssh_execute_command(server_ip, rule) + + +# Main function to create and configure the server +def main(provider, location, server_type): + if provider == "hetzner": + location = location or config["hetzner"]["location"] + server_type = server_type or config["hetzner"]["server_type"] + server = create_hetzner_server(location, server_type) + server_ip = server.public_net.ipv6.ip.split("/")[0] + "1" + elif provider == "aws": + location = location or config["aws"]["region"] + server_type = server_type or config["aws"]["instance_type"] + server = create_aws_server(location, server_type) + server_ip = server.public_ip_address + elif provider == "digitalocean": + location = location or config["digitalocean"]["region"] + server_type = server_type or config["digitalocean"]["server_type"] + server = create_digitalocean_server(location, server_type) + server.load() + server_ip = server.ip_address + elif provider == "azure": + location = location or config["azure"]["location"] + server_type = server_type or config["azure"]["vm_size"] + server, server_ip = create_azure_server(location, server_type) + else: + raise ValueError("Unsupported provider") + + logging.info(f"Server IP: {server_ip}") + + # Giving server time to boot up + logging.info("Waiting for server to boot up...") + time.sleep(30) + + # Install Wireguard and configure it + commands = [ + "apt update", + "apt install -y wireguard", + "echo 'net.ipv6.conf.all.forwarding=1' >> /etc/sysctl.conf", + "sysctl -p", + ] + + for command in commands: + ssh_execute_command(server_ip, command) + + private_key, public_key, preshared_key = generate_wireguard_keys() + configure_wireguard(server_ip, private_key, public_key, preshared_key) + + # Generate client configuration for Chimpman + wireguard_address = config["wireguard"]["address"] + routed_addresses = config["wireguard"]["routed_addresses"] + listen_port = config["wireguard"]["listen_port"] + persistent_keepalive = config["wireguard"]["peer_persistent_keepalive"] + + chimpman_config = f""" +[Peer] +PublicKey = {public_key} +PresharedKey = {preshared_key} +AllowedIPs = {wireguard_address}, {routed_addresses} +Endpoint = [{server_ip}]:{listen_port} +PersistentKeepalive = {persistent_keepalive} +""" + + print("Chimpman Wireguard Configuration:") + print(chimpman_config) + + +# Run the main function with parsed arguments +main(args.provider, args.location, args.server_type)