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:
commit
e192076414
4 changed files with 453 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
config.ini
|
||||||
|
venv/
|
51
config.dist.ini
Normal file
51
config.dist.ini
Normal 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
8
requirements.txt
Normal 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
392
worker.py
Normal 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)
|
Loading…
Reference in a new issue