from flask import Flask, request, render_template_string, redirect, url_for, session, Response import os, subprocess, shutil, sys, json if os.geteuid() != 0: print("This script needs to be run as root or sudo, try 'sudo python3 wg_helper.py'") sys.exit(1) # ===================== # ADMIN_PASSWORD = "" # ===================== # app = Flask(__name__) app.secret_key = os.urandom(24) wireguard_dir = "/etc/wireguard" config_file = "wg_helper.json" full_config_file_path = os.path.join(wireguard_dir, config_file) SYSCTL_CONF = "/etc/sysctl.conf" default_config = { "server": { "server_network_interface": subprocess.run("ip -o -4 route show to default | awk '{print $5}'", shell=True, capture_output=True, text=True).stdout.strip(), "Endpoint": "", "ListenPort": 51820, "PrivateKey": "", "PublicKey": "" }, "peers": [ ] } # Common CSS for all pages CSS = """ * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; background-color: #f5f7fa; padding: 0; margin: 0; } .container { width: 95%; max-width: 1200px; margin: 0 auto; padding: 20px; } .header { background-color: #2c3e50; color: white; padding: 15px 0; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); } .header-content { display: flex; justify-content: space-between; align-items: center; } .header h1 { margin: 0; font-size: 24px; } .navbar { text-align: right; } .navbar a { color: white; text-decoration: none; padding: 8px 12px; border-radius: 4px; transition: background-color 0.3s; } .navbar a:hover { background-color: rgba(255,255,255,0.1); } .card { background-color: white; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); } .card h2 { color: #2c3e50; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #eee; } .card strong { color: #34495e; } .divider { height: 1px; background-color: #e1e4e8; margin: 20px 0; } input[type="text"], input[type="password"] { width: 100%; padding: 10px; margin: 8px 0; border: 1px solid #ddd; border-radius: 4px; font-size: 16px; } .btn, input[type="submit"] { background-color: #3498db; color: white; border: none; padding: 8px 15px; border-radius: 4px; cursor: pointer; font-size: 14px; margin-right: 5px; transition: background-color 0.3s; } .btn:hover, input[type="submit"]:hover { background-color: #2980b9; } .btn-small { padding: 5px 10px; font-size: 12px; } .btn-danger { background-color: #e74c3c; } .btn-danger:hover { background-color: #c0392b; } .btn-success { background-color: #2ecc71; } .btn-success:hover { background-color: #27ae60; } .status-item { margin-bottom: 10px; display: flex; align-items: center; flex-wrap: wrap; } .status-item form { margin-left: 10px; } .login-form { max-width: 400px; margin: 100px auto; background-color: white; padding: 30px; border-radius: 8px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); } .login-form h2 { text-align: center; margin-bottom: 20px; color: #2c3e50; } .login-form input[type="submit"] { width: 100%; padding: 10px; margin-top: 15px; } .error-message { color: #e74c3c; margin-top: 10px; text-align: center; } .peer-item { background-color: #f8f9fa; border-left: 4px solid #3498db; padding: 15px; margin-bottom: 15px; border-radius: 4px; } .peer-item form { margin-top: 10px; } .peer-actions { margin-top: 10px; display: flex; gap: 5px; } .option-row { display: flex; align-items: center; margin-bottom: 15px; } .option-row strong { min-width: 150px; } .option-row form { flex-grow: 1; display: flex; gap: 10px; } .option-row input[type="text"] { flex-grow: 1; } .success { color: #2ecc71; } .danger { color: #e74c3c; } .warning { color: #f39c12; } @media (max-width: 768px) { .option-row { flex-direction: column; align-items: flex-start; } .option-row strong { margin-bottom: 5px; } .option-row form { width: 100%; } } """ @app.route("/", methods=["GET", "POST"]) def login(): LOGIN_HTML = """ WireGuard Helper - Login

SeaBee's WireGuard Helper

{% if error %}
{{ error }}
{% endif %}
""" if request.method == "POST": if request.form.get("password") == ADMIN_PASSWORD: session["logged_in"] = True return redirect(url_for("dashboard")) else: return render_template_string(LOGIN_HTML, error="Incorrect password, please try again") return render_template_string(LOGIN_HTML, error=None) @app.route("/dashboard") def dashboard(): ensure_logged_in() config_data = load_config() # One liners is_wg_installed = shutil.which("wg") is not None is_ufw_installed = ufw_get_path() is_iptables_installed = iptables_get_path() server_network_interface = config_data.get("server", {}).get("server_network_interface", "") # Status indicators wg_installed_status = "Installed" if is_wg_installed else "Not installed" wg_installed_class = "success" if is_wg_installed else "danger" install_wg_button = "" if not is_wg_installed: install_wg_button = """
""" ufw_allow_port_button = """
""" ufw_installed_status = "Installed" if is_ufw_installed else "Not installed" ufw_installed_class = "success" if is_ufw_installed else "warning" if is_ufw_installed: ufw_path = ufw_get_path() ufw_enabled_status = subprocess.run(f"{ufw_path} status | awk '/Status:/ {{print $2}}'", shell=True, capture_output=True, text=True).stdout.strip() if ufw_enabled_status == "active": ufw_enabled_status = "active" ufw_enabled_class = "success" else: ufw_enabled_status = "disabled" ufw_enabled_class = "warning" else: ufw_enabled_status = "" ufw_enabled_class = "" iptables_installed_status = "Installed" if is_iptables_installed else "Not installed" iptables_installed_class = "success" if is_iptables_installed else "danger" if not is_iptables_installed: iptables_install_button = """
""" else: iptables_install_button = "" wg_running_status = "" wg_running_class = "" wg_running_button = "" if is_wg_installed: wg_running_check = subprocess.run("systemctl is-active wg-quick@wg0", shell=True, capture_output=True, text=True).stdout.strip() config_data = load_config() server_priv_key = config_data.get("server", {}).get("PrivateKey", "") if server_priv_key == "": wg_running_status = "Server keys need to be generated first" wg_running_class = "warning" else: if wg_running_check == "failed": wg_running_status = "Not running" wg_running_class = "danger" wg_running_button = """
""" else: wg_running_status = "Running" wg_running_class = "success" else: wg_running_status = "Not running, waiting for install" wg_running_class = "warning" wg_enabled_at_boot_status = "" wg_enabled_at_boot_class = "" wg_enabled_at_boot_button = "" wg_enabled_at_boot_check = subprocess.run(f"systemctl is-enabled wg-quick@wg0", shell=True, capture_output=True, text=True).stdout.strip() if wg_enabled_at_boot_check == "enabled": wg_enabled_at_boot_status = "Enabled" wg_enabled_at_boot_class = "success" else: wg_enabled_at_boot_status = "DISABLED" wg_enabled_at_boot_class = "danger" wg_enabled_at_boot_button = """
""" ufw_port_status = "" ufw_port_class = "" if is_ufw_installed and ufw_enabled_status == "active": ufw_path = ufw_get_path() ufw_wg_port_check = subprocess.run(f"{ufw_path} status | awk '/51820/ {{print $2; exit}}'", shell=True, capture_output=True, text=True).stdout.strip() if ufw_wg_port_check == "": ufw_port_status = "Not added" ufw_port_class = "danger" ufw_port_button = ufw_allow_port_button else: ufw_port_status = "Allowed" if ufw_wg_port_check == "ALLOW" else "Denied" ufw_port_class = "success" if ufw_wg_port_check == "ALLOW" else "danger" ufw_port_button = "" if ufw_wg_port_check == "ALLOW" else ufw_allow_port_button else: ufw_port_status = "N/A" ufw_port_class = "warning" ufw_port_button = "" # Endpoint data server_endpoint_data = config_data.get("server", {}).get("Endpoint", "") if server_endpoint_data == "": server_endpoint_form = """
""" else: server_endpoint_form = f""" {server_endpoint_data}
""" # Server public key server_public_key = config_data.get("server", {}).get("PublicKey", "") if server_public_key == "": server_public_key_form = f""" No Keys set
""" else: server_public_key_form = f""" {server_public_key}
""" # Peers list peers_list = "" peer_config = config_data.get("peers", []) for peer in peer_config: peer_id = peer["id"] peer_name = peer["name"] peer_public_key = peer["PublicKey"] peers_list += f"""
ID: {peer_id}
Name: {peer_name}
Public Key: {peer_public_key}
""" html = f""" SeaBee's WG Helper

SeaBee's Wireguard Helper v0.2

System Status

Wireguard software:  {wg_installed_status} {install_wg_button}
Wireguard autostart at boot:  {wg_enabled_at_boot_status} {wg_enabled_at_boot_button}
UFW:  {ufw_installed_status} {'  &  ' + '' + ufw_enabled_status + '' if ufw_enabled_status else ''}
{f'''
UFW Wireguard Port:  {ufw_port_status} {ufw_port_button}
''' if ufw_port_status else ''}
iptables:  {iptables_installed_status} {iptables_install_button}
Server network interface:  {server_network_interface}
Wireguard status:  {wg_running_status} {wg_running_button}

Server Configuration

Server endpoint:
{server_endpoint_form}
Server Public key:
{server_public_key_form}

Peer Management

Create new peer:
{peers_list if peers_list else '

No peers configured yet. Create your first peer above.

'}
""" return html @app.route("/logout") def logout(): session.clear() return redirect(url_for("login")) def ensure_logged_in(): if not session.get("logged_in"): return redirect(url_for("login")) @app.route("/install_wireguard", methods=["POST"]) def install_wireguard(): ensure_logged_in() try: subprocess.check_call(["sudo", "apt", "install", "-y", "wireguard"]) return redirect(url_for("dashboard")) except subprocess.CalledProcessError as e: error_html = f""" Installation Error

Installation Failed

Error: {e}

Back to Dashboard
""" return error_html @app.route("/install_iptables", methods=["POST"]) def install_iptables(): ensure_logged_in() try: subprocess.check_call(["sudo", "apt", "install", "-y", "iptables"]) return redirect(url_for("dashboard")) except subprocess.CalledProcessError as e: error_html = f""" Installation Error

Installation Failed

Error: {e}

Back to Dashboard
""" return error_html @app.route("/update_server_endpoint", methods=["POST"]) def update_server_endpoint(): ensure_logged_in() new_endpoint = request.form.get("endpoint", "").strip() config_data = load_config() config_data.setdefault("server", {})["Endpoint"] = new_endpoint update_config(config_data) return redirect(url_for("dashboard")) @app.route("/ufw_open_port", methods=["POST"]) def ufw_open_port(): ensure_logged_in() ufw_path = ufw_get_path() try: subprocess.check_call([ufw_path, "allow", "51820", "comment", '"Wireguard"']) except subprocess.CalledProcessError as e: error_html = f""" UFW Error

UFW Configuration Failed

Error: {e}

Back to Dashboard
""" return error_html return redirect(url_for("dashboard")) @app.route("/regenerate_server_keys", methods=["POST"]) def regenerate_server_keys(): private_key = subprocess.check_output(["wg", "genkey"], text=True).strip() public_key = subprocess.check_output(["wg", "pubkey"], input=private_key, text=True).strip() config_data = load_config() config_data.setdefault("server", {})["PrivateKey"] = private_key config_data.setdefault("server", {})["PublicKey"] = public_key update_config(config_data) return redirect(url_for("dashboard")) @app.route("/create_new_peer", methods=["POST"]) def create_new_peer(): new_peer_name = request.form.get("peer_name", "").strip() config_data = load_config() peers = config_data.get("peers", []) existing_ids = sorted(peer["id"] for peer in peers) # Find available ID by checking gaps existing_ids = sorted(peer["id"] for peer in peers) # Get all assigned IDs, sorted # Start from 2, find the first missing number new_id = 2 for id in existing_ids: if id == new_id: new_id += 1 else: break # Found a gap, use it # Generate new keys private_key = subprocess.check_output(["wg", "genkey"], text=True).strip() public_key = subprocess.check_output(["wg", "pubkey"], input=private_key, text=True).strip() # Create new peer info new_peer = { "id": new_id, "name": new_peer_name, "PrivateKey": private_key, "PublicKey": public_key } peers.append(new_peer) peers = sorted(peers, key=lambda peer: peer.get("id", 0)) config_data["peers"] = peers update_config(config_data) return redirect(url_for("dashboard")) @app.route("/delete_peer", methods=["POST"]) def delete_peer(): peer_id_to_delete = request.form.get("peer_id_to_delete", "").strip() peer_id_to_delete = int(peer_id_to_delete) config_data = load_config() peer_config = config_data.get("peers", []) # Literally just set the peers to itself minus the peer ID requested new_peer_config = [peer for peer in peer_config if peer["id"] != peer_id_to_delete] new_peer_config = sorted(new_peer_config, key=lambda peer: peer.get("id", 0)) config_data["peers"] = new_peer_config update_config(config_data) return redirect(url_for("dashboard")) @app.route("/download_peer_config", methods=["POST"]) def download_peer_config(): peer_id_to_download = request.form.get("peer_id", "").strip() peer_id_to_download = int(peer_id_to_download) config_data = load_config() peer_config = config_data.get("peers", []) selected_peer = None for peer in peer_config: if peer.get("id") == peer_id_to_download: selected_peer = peer break if not selected_peer: return "Peer not found", 404 peer_name = selected_peer["name"] peer_pub_key = selected_peer["PublicKey"] peer_priv_key = selected_peer["PrivateKey"] server_pub_key = config_data["server"]["PublicKey"] endpoint = config_data["server"]["Endpoint"] listen_port = config_data["server"]["ListenPort"] interface = config_data["server"]["server_network_interface"] # Compose the WireGuard peer config config_text = f"""[Interface] PrivateKey = {peer_priv_key} Address = 10.0.0.{peer_id_to_download}/32 DNS = 1.1.1.1 [Peer] PublicKey = {server_pub_key} Endpoint = {endpoint}:{listen_port} AllowedIPs = 0.0.0.0/0 PersistentKeepalive = 25 """ # Create response with downloadable config file filename = f"peer_{peer_name}.conf" return Response( config_text, mimetype="text/plain", headers={"Content-Disposition": f"attachment; filename={filename}"} ) @app.route("/start_wg", methods=["POST"]) def start_wg(): subprocess.run(["wg-quick", "up", "wg0"], text=True) return redirect(url_for("dashboard")) @app.route("/autostart_wg_on_boot", methods=["POST"]) def enable_wg_at_boot(): subprocess.run(["systemctl", "enable", "wg-quick@wg0"], text=True) return redirect(url_for("dashboard")) def ufw_get_path(): ufw_dirs = ["/bin", "/sbin", "/usr/bin", "/usr/sbin", "/usr/local/bin", "/usr/local/sbin"] for dir in ufw_dirs: path = os.path.join(dir, "ufw") if os.path.isfile(path) and os.access(path, os.X_OK): return path return None def iptables_get_path(): ufw_dirs = ["/sbin", "/usr/sbin", "/bin", "/usr/bin", "/usr/local/sbin", "/usr/local/bin"] for dir in ufw_dirs: path = os.path.join(dir, "iptables") if os.path.isfile(path) and os.access(path, os.X_OK): return path return None def run_first_install_setup(): # Create /etc/wireguard folder if not os.path.exists(wireguard_dir): os.mkdir(wireguard_dir) # Create config json if not os.path.isfile(full_config_file_path): with open(full_config_file_path, "w") as f: json.dump(default_config, f, indent=4) # Create wg0.conf if not os.path.isfile("/etc/wireguard/wg0.conf"): update_config(load_config()) # Port forward update_sysctl() def update_sysctl(): try: with open(SYSCTL_CONF, "r") as file: lines = file.readlines() modified = False found = False for i in range(len(lines)): if "net.ipv4.ip_forward=" in lines[i]: found = True if lines[i].strip().startswith("#"): # If commented, uncomment lines[i] = lines[i].lstrip("#") # Remove leading # modified = True if lines[i].strip() != "net.ipv4.ip_forward=1": # Ensure it's set to 1 lines[i] = "net.ipv4.ip_forward=1\n" modified = True if not found: # If not found, append it lines.append("\nnet.ipv4.ip_forward=1\n") modified = True if modified: with open(SYSCTL_CONF, "w") as file: file.writelines(lines) os.system("sudo sysctl -p") # Apply changes print("Updated sysctl.conf and applied changes.") else: print("No changes needed.") except Exception as e: print(f"Error: {e}") def load_config(): try: with open(full_config_file_path, "r") as f: return json.load(f) except: return(0) def update_config(config_data): with open(full_config_file_path, "w") as f: json.dump(config_data, f, indent=4) peers = config_data.get("peers", []) server_priv_key = config_data.get("server", {}).get("PrivateKey", "") server_pub_key = config_data.get("server", {}).get("PublicKey", "") server_endpoint = config_data.get("server", {}).get("Endpoint", "") server_network_interface = config_data.get("server", {}).get("server_network_interface", "") wg_config_content = f"""[Interface] Address = 10.0.0.1/24 SaveConfig = true PostUp = {iptables_get_path()} -A FORWARD -i %i -j ACCEPT; {iptables_get_path()} -t nat -A POSTROUTING -o {server_network_interface} -j MASQUERADE PostDown = {iptables_get_path()} -D FORWARD -i %i -j ACCEPT; {iptables_get_path()} -t nat -D POSTROUTING -o {server_network_interface} -j MASQUERADE ListenPort = 51820 PrivateKey = {server_priv_key} """ for peer in peers: peer_id = peer["id"] peer_name = peer["name"] peer_pub_key = peer["PublicKey"] peer_priv_key = peer["PrivateKey"] wg_config_content += f""" [Peer] PublicKey = {peer_pub_key} AllowedIPs = 10.0.0.{peer_id}/32 """ subprocess.run(["systemctl", "stop", "wg-quick@wg0"], text=True) with open("/etc/wireguard/wg0.conf", "w") as f: f.write(wg_config_content) print("Server config updated") print("restarting wireguard service") subprocess.run(["systemctl", "start", "wg-quick@wg0"], text=True) if __name__ == "__main__": run_first_install_setup() app.run(host="0.0.0.0", port=5050)