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.
This commit is contained in:
Kumi 2024-07-18 17:24:24 +02:00
commit e192076414
Signed by: kumi
GPG key ID: ECBCC9082395383F
4 changed files with 453 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
config.ini
venv/

51
config.dist.ini Normal file
View file

@ -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

8
requirements.txt Normal file
View file

@ -0,0 +1,8 @@
paramiko
hcloud
boto3
python-digitalocean
azure-mgmt-compute
azure-mgmt-network
azure-mgmt-resource
azure-identity

392
worker.py Normal file
View file

@ -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)