mirror of
https://github.com/eduardogsilva/wireguard_webadmin.git
synced 2026-01-01 14:16:18 +00:00
Add cluster worker implementation and configuration files
This commit is contained in:
21
.env.cluster_example
Normal file
21
.env.cluster_example
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Example configuration for Cluster Node
|
||||||
|
# Rename this file to .env before starting
|
||||||
|
|
||||||
|
# Master Server Address (where the API is hosting)
|
||||||
|
# Example: https://wireguard.example.com
|
||||||
|
MASTER_SERVER_ADDRESS=https://wireguard.example.com
|
||||||
|
|
||||||
|
# Timezone
|
||||||
|
TZ=America/Sao_Paulo
|
||||||
|
|
||||||
|
# Cluster Worker Token (Get this from the Master Server)
|
||||||
|
TOKEN=<uuid>
|
||||||
|
|
||||||
|
# Debug Mode (optional, default False)
|
||||||
|
DEBUG_MODE=False
|
||||||
|
|
||||||
|
# Compose Version (Do not change)
|
||||||
|
COMPOSE_VERSION=03r
|
||||||
|
|
||||||
|
# Timezone (duplicate for compatibility if needed, but TZ is standard)
|
||||||
|
TIMEZONE=America/Sao_Paulo
|
||||||
28
cluster_node.yml
Normal file
28
cluster_node.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
wireguard-webadmin-cluster-node:
|
||||||
|
container_name: wireguard-webadmin-cluster-node
|
||||||
|
restart: unless-stopped
|
||||||
|
build:
|
||||||
|
context: containers/cluster_node
|
||||||
|
dockerfile: Dockerfile-cluster_node
|
||||||
|
environment:
|
||||||
|
- MASTER_SERVER_ADDRESS=${MASTER_SERVER_ADDRESS}
|
||||||
|
- DEBUG_MODE=${DEBUG_MODE}
|
||||||
|
- COMPOSE_VERSION=03r
|
||||||
|
- TZ=${TIMEZONE}
|
||||||
|
- TOKEN=${TOKEN}
|
||||||
|
volumes:
|
||||||
|
- cluster_node_wireguard:/etc/wireguard
|
||||||
|
#ports:
|
||||||
|
# Ports for WireGuard instances.
|
||||||
|
#- "51820-51839:51820-51839/udp"
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
- SYS_MODULE
|
||||||
|
sysctls:
|
||||||
|
- net.ipv4.conf.all.src_valid_mark=1
|
||||||
|
- net.ipv4.ip_forward=1
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
cluster_node_wireguard:
|
||||||
33
containers/cluster_node/Dockerfile-cluster_node
Normal file
33
containers/cluster_node/Dockerfile-cluster_node
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Single stage build for Cluster Node
|
||||||
|
FROM python:3.12-slim
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install necessary runtime packages
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
wireguard \
|
||||||
|
iptables \
|
||||||
|
iproute2 \
|
||||||
|
net-tools \
|
||||||
|
inetutils-ping \
|
||||||
|
inetutils-traceroute \
|
||||||
|
procps \
|
||||||
|
curl \
|
||||||
|
nano \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy requirements and install Python packages
|
||||||
|
# Using the specific requirements.txt for this container
|
||||||
|
COPY requirements.txt /app/requirements.txt
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy the application code
|
||||||
|
COPY . /app/
|
||||||
|
|
||||||
|
# Set execution permissions on scripts
|
||||||
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
|
ARG SERVER_ADDRESS
|
||||||
|
ARG DEBUG_MODE
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
|
CMD ["python", "-u", "/app/cluster_worker.py"]
|
||||||
171
containers/cluster_node/cluster_worker.py
Normal file
171
containers/cluster_node/cluster_worker.py
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import glob
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
MASTER_SERVER_ADDRESS = os.environ.get('MASTER_SERVER_ADDRESS')
|
||||||
|
TOKEN = os.environ.get('TOKEN')
|
||||||
|
WIREGUARD_DIR = '/etc/wireguard'
|
||||||
|
WORKER_VERSION = 10
|
||||||
|
REQUEST_TIMEOUT = 10
|
||||||
|
|
||||||
|
class ClusterWorker:
|
||||||
|
def __init__(self):
|
||||||
|
self.config_version = 0
|
||||||
|
self.should_run = True
|
||||||
|
self.session = requests.Session()
|
||||||
|
|
||||||
|
if not MASTER_SERVER_ADDRESS or not TOKEN:
|
||||||
|
logger.error("MASTER_SERVER_ADDRESS or TOKEN not set")
|
||||||
|
self.should_run = False
|
||||||
|
|
||||||
|
self.base_url = f"https://{MASTER_SERVER_ADDRESS}/api/cluster"
|
||||||
|
# Validate URL scheme
|
||||||
|
if not MASTER_SERVER_ADDRESS.startswith(('http://', 'https://')):
|
||||||
|
self.base_url = f"https://{MASTER_SERVER_ADDRESS}/api/cluster"
|
||||||
|
else:
|
||||||
|
self.base_url = f"{MASTER_SERVER_ADDRESS}/api/cluster"
|
||||||
|
|
||||||
|
def cleanup_wireguard(self):
|
||||||
|
logger.info("Cleaning up WireGuard configuration...")
|
||||||
|
# Stop all wireguard interfaces
|
||||||
|
try:
|
||||||
|
files = glob.glob(f"{WIREGUARD_DIR}/*.conf")
|
||||||
|
for f in files:
|
||||||
|
interface = os.path.basename(f).replace('.conf', '')
|
||||||
|
subprocess.run(['wg-quick', 'down', interface], capture_output=True)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error stopping wireguard interfaces: {e}")
|
||||||
|
|
||||||
|
# Remove files
|
||||||
|
try:
|
||||||
|
for f in glob.glob(f"{WIREGUARD_DIR}/*"):
|
||||||
|
os.remove(f)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error cleaning directory: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(['iptables', '-t', 'nat', '-F', 'WGWADM_POSTROUTING'], capture_output=True)
|
||||||
|
subprocess.run(['iptables', '-t', 'nat', '-F', 'WGWADM_PREROUTING'], capture_output=True)
|
||||||
|
subprocess.run(['iptables', '-t', 'filter', '-F', 'WGWADM_FORWARD'], capture_output=True)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error flushing firewall: {e}")
|
||||||
|
|
||||||
|
def get_status(self):
|
||||||
|
params = {
|
||||||
|
'token': TOKEN,
|
||||||
|
'worker_config_version': self.config_version,
|
||||||
|
'worker_version': WORKER_VERSION
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
response = self.session.get(f"{self.base_url}/status/", params=params, timeout=REQUEST_TIMEOUT)
|
||||||
|
return response
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logger.error(f"Connection error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def download_configs(self):
|
||||||
|
params = {
|
||||||
|
'token': TOKEN,
|
||||||
|
'worker_config_version': self.config_version,
|
||||||
|
'worker_version': WORKER_VERSION
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
response = self.session.get(f"{self.base_url}/worker/get_config_files/", params=params, timeout=REQUEST_TIMEOUT)
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
if data.get('status') == 'success':
|
||||||
|
return data
|
||||||
|
return None
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logger.error(f"Error downloading configs: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def apply_configs(self, data):
|
||||||
|
logger.info("Applying new configurations...")
|
||||||
|
files = data.get('files', {})
|
||||||
|
cluster_settings = data.get('cluster_settings', {})
|
||||||
|
new_config_version = cluster_settings.get('config_version', 0)
|
||||||
|
|
||||||
|
# 1. Stop existing interfaces
|
||||||
|
self.cleanup_wireguard()
|
||||||
|
|
||||||
|
# 2. Write new files
|
||||||
|
for filename, content in files.items():
|
||||||
|
filepath = os.path.join(WIREGUARD_DIR, filename)
|
||||||
|
with open(filepath, 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
if filename == 'wg-firewall.sh':
|
||||||
|
os.chmod(filepath, 0o755)
|
||||||
|
|
||||||
|
# Start interfaces
|
||||||
|
conf_files = glob.glob(f"{WIREGUARD_DIR}/*.conf")
|
||||||
|
for conf in conf_files:
|
||||||
|
interface = os.path.basename(conf).replace('.conf', '')
|
||||||
|
logger.info(f"Starting WireGuard interface: {interface}")
|
||||||
|
try:
|
||||||
|
subprocess.run(['wg-quick', 'up', interface], check=True, capture_output=True)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
logger.error(f"Failed to start {interface}: {e.stderr.decode()}")
|
||||||
|
|
||||||
|
# 4. Update config version
|
||||||
|
self.config_version = new_config_version
|
||||||
|
logger.info(f"Configuration updated to version {self.config_version}")
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if not self.should_run:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("Cluster Worker starting...")
|
||||||
|
|
||||||
|
# Initial cleanup
|
||||||
|
self.cleanup_wireguard()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
response = self.get_status()
|
||||||
|
|
||||||
|
if response:
|
||||||
|
if response.status_code == 403:
|
||||||
|
logger.error("Received 403 Forbidden. Shutting down WireGuard and stopping requests.")
|
||||||
|
self.cleanup_wireguard()
|
||||||
|
self.should_run = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
remote_config_version = data.get('cluster_settings', {}).get('config_version', 0)
|
||||||
|
|
||||||
|
if remote_config_version != self.config_version:
|
||||||
|
logger.info(f"Config version mismatch (Local: {self.config_version}, Remote: {remote_config_version}). Updating...")
|
||||||
|
config_data = self.download_configs()
|
||||||
|
if config_data:
|
||||||
|
self.apply_configs(config_data)
|
||||||
|
else:
|
||||||
|
logger.error("Failed to download config files.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error in main loop: {e}")
|
||||||
|
|
||||||
|
time.sleep(60)
|
||||||
|
|
||||||
|
# Final loop state if 403 was received
|
||||||
|
while not self.should_run:
|
||||||
|
logger.info("Worker disabled due to 403 Forbidden. WireGuard is off.")
|
||||||
|
time.sleep(60)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
worker = ClusterWorker()
|
||||||
|
worker.run()
|
||||||
33
containers/cluster_node/entrypoint.sh
Normal file
33
containers/cluster_node/entrypoint.sh
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Set Timezone
|
||||||
|
if [ -n "$TZ" ]; then
|
||||||
|
ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Enable IP forwarding
|
||||||
|
sysctl -w net.ipv4.ip_forward=1 > /dev/null 2>&1 || echo "Warning: Could not set net.ipv4.ip_forward"
|
||||||
|
|
||||||
|
# Check required variables
|
||||||
|
if [ -z "$MASTER_SERVER_ADDRESS" ]; then
|
||||||
|
echo "ERROR: MASTER_SERVER_ADDRESS is not set."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$TOKEN" ]; then
|
||||||
|
echo "ERROR: TOKEN is not set."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check MASTER_SERVER_ADDRESS for HTTPS
|
||||||
|
if [[ "$MASTER_SERVER_ADDRESS" != https://* ]]; then
|
||||||
|
if [[ "$MASTER_SERVER_ADDRESS" == http://192.168.* ]]; then
|
||||||
|
echo "Warning: Using HTTP only for development."
|
||||||
|
else
|
||||||
|
echo "ERROR: MASTER_SERVER_ADDRESS must start with https://. Received: $MASTER_SERVER_ADDRESS"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
5
containers/cluster_node/requirements.txt
Normal file
5
containers/cluster_node/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
certifi==2025.11.12
|
||||||
|
charset-normalizer==3.4.4
|
||||||
|
idna==3.11
|
||||||
|
requests==2.32.5
|
||||||
|
urllib3==2.6.2
|
||||||
@@ -20,7 +20,7 @@ services:
|
|||||||
- rrd_data:/rrd_data/
|
- rrd_data:/rrd_data/
|
||||||
ports:
|
ports:
|
||||||
# Do not directly expose the Django port to the internet, use some kind of reverse proxy with SSL.
|
# Do not directly expose the Django port to the internet, use some kind of reverse proxy with SSL.
|
||||||
- "127.0.0.1:8000:8000"
|
- "8000:8000"
|
||||||
# Warning: Docker will have a hard time handling large amount of ports. Expose only the ports that you need.
|
# Warning: Docker will have a hard time handling large amount of ports. Expose only the ports that you need.
|
||||||
# Ports for multiple WireGuard instances. (Probably, you just need one)
|
# Ports for multiple WireGuard instances. (Probably, you just need one)
|
||||||
- "51820-51839:51820-51839/udp"
|
- "51820-51839:51820-51839/udp"
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ Including another URLconf
|
|||||||
1. Import the include() function: from django.urls import include, path
|
1. Import the include() function: from django.urls import include, path
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
from django.contrib import admin
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from accounts.views import view_create_first_user, view_login, view_logout
|
from accounts.views import view_create_first_user, view_login, view_logout
|
||||||
@@ -40,7 +39,7 @@ from wireguard_peer.views import view_manage_ip_address, view_wireguard_peer_lis
|
|||||||
from wireguard_tools.views import download_config_or_qrcode, export_wireguard_configs, restart_wireguard_interfaces
|
from wireguard_tools.views import download_config_or_qrcode, export_wireguard_configs, restart_wireguard_interfaces
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
# path('admin/', admin.site.urls),
|
||||||
path('', view_apply_db_patches, name='apply_db_patches'),
|
path('', view_apply_db_patches, name='apply_db_patches'),
|
||||||
path('status/', view_wireguard_status, name='wireguard_status'),
|
path('status/', view_wireguard_status, name='wireguard_status'),
|
||||||
path('dns/', view_static_host_list, name='static_host_list'),
|
path('dns/', view_static_host_list, name='static_host_list'),
|
||||||
|
|||||||
Reference in New Issue
Block a user