WG helper script
This commit is contained in:
BIN
Divers/wireguard_helper/Screenshot 2025-04-12 122809.png
Normal file
BIN
Divers/wireguard_helper/Screenshot 2025-04-12 122809.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
53
Divers/wireguard_helper/readme.md
Normal file
53
Divers/wireguard_helper/readme.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# SeaBee's WireGuard Helper Script
|
||||||
|
|
||||||
|
This Python script simplifies the installation and management of a WireGuard VPN server, including client configuration.
|
||||||
|
|
||||||
|
## 🔧 Features
|
||||||
|
|
||||||
|
- Installs WireGuard and required dependencies
|
||||||
|
- Generates server and peer key pairs
|
||||||
|
- Edits and manages the WireGuard config file (`wg0.conf`)
|
||||||
|
- Stores configuration data in a JSON file for easy reuse
|
||||||
|
- Allows editing the server endpoint
|
||||||
|
- Starts a temporary web admin panel for WireGuard management
|
||||||
|
- Automatically opens port `51820` using UFW (if enabled)
|
||||||
|
|
||||||
|
## 📦 Requirements
|
||||||
|
|
||||||
|
- Python 3
|
||||||
|
- Flask (`sudo apt install python3-flask` **or** `pip install flask`)
|
||||||
|
- A Linux-based system
|
||||||
|
- Internet connection
|
||||||
|
|
||||||
|
> ⚠️ Note: This script has been tested on debain and may not work on your machine
|
||||||
|
|
||||||
|
## 🚀 Installation & Usage
|
||||||
|
|
||||||
|
1. Ensure you have **all** dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install curl python3-flask -y
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Download and run the script
|
||||||
|
```bash
|
||||||
|
curl -L -o wg_helper.py https://raw.githubusercontent.com/seabee33/wireguard_helper/main/wg_helper.py
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Set the admin password** on **line 10** of the file: `ADMIN_PASSWORD = "your_secure_password_here"`
|
||||||
|
|
||||||
|
4. **Run the script with sudo or as root** `sudo python3 wg_helper.py`
|
||||||
|
|
||||||
|
5. Access the web panel: Open your browser and go to: `http://<SERVER_IP>:5050` and go through the easy setup steps:
|
||||||
|
|
||||||
|
|
||||||
|
## Setup once web panel loaded
|
||||||
|
1. Install and activate necessary software in **system status** (Install wireguard, autostart wireguard at boot, install iptables, Open Port 51820 *if ufw is installed*)
|
||||||
|
2. Fill in endpoint in options
|
||||||
|
3. generate keys
|
||||||
|
4. Add peers with a unique name
|
||||||
|
5. download peer config and connect on client device
|
||||||
|
6. Don't forget to port forward your server on your modem! (port 51820)
|
||||||
|
|
||||||
|
Preview:
|
||||||
|

|
||||||
986
Divers/wireguard_helper/wg_helper.py
Normal file
986
Divers/wireguard_helper/wg_helper.py
Normal file
@@ -0,0 +1,986 @@
|
|||||||
|
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 = """
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WireGuard Helper - Login</title>
|
||||||
|
<style>
|
||||||
|
""" + CSS + """
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="login-form">
|
||||||
|
<h2>SeaBee's WireGuard Helper</h2>
|
||||||
|
<form method="POST">
|
||||||
|
<div>
|
||||||
|
<input type="password" name="password" placeholder="Enter admin password" required autofocus>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="submit" value="Login" class="btn">
|
||||||
|
</div>
|
||||||
|
{% if error %}
|
||||||
|
<div class="error-message">{{ error }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 = """
|
||||||
|
<form action="/install_wireguard" method="POST" style='display:inline'>
|
||||||
|
<input type="submit" value="Install WireGuard" class="btn btn-success">
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
|
|
||||||
|
ufw_allow_port_button = """
|
||||||
|
<form action='/ufw_open_port' method='POST' style='display:inline'>
|
||||||
|
<input type='submit' value='Open Port 51820' class="btn btn-success">
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 = """
|
||||||
|
<form action="/install_iptables" method="POST" style='display:inline'>
|
||||||
|
<input type="submit" value="Install iptables" class="btn btn-success">
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
|
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 = """
|
||||||
|
<form action='/start_wg' method='POST' style='display:inline'>
|
||||||
|
<input type='submit' value='Start wireguard' class="btn btn-success">
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
|
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 = """
|
||||||
|
<form action='/autostart_wg_on_boot' method='POST' style='display:inline'>
|
||||||
|
<input type='submit' value='Autostart Wireguard at boot' class="btn btn-success">
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 = """
|
||||||
|
<form action="/update_server_endpoint" method="POST" class="option-form">
|
||||||
|
<input type="text" placeholder="Set server endpoint" name='endpoint' required>
|
||||||
|
<input type="submit" value="Set endpoint" class="btn btn-success">
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
server_endpoint_form = f"""
|
||||||
|
<span>{server_endpoint_data}</span>
|
||||||
|
<form action="/update_server_endpoint" method="POST" class="option-form">
|
||||||
|
<input type="text" placeholder='Enter new endpoint' name='endpoint' required>
|
||||||
|
<input type="submit" value="Update endpoint" class="btn">
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Server public key
|
||||||
|
server_public_key = config_data.get("server", {}).get("PublicKey", "")
|
||||||
|
if server_public_key == "":
|
||||||
|
server_public_key_form = f"""
|
||||||
|
<span class="warning">No Keys set</span>
|
||||||
|
<form action="/regenerate_server_keys" method="POST" class="option-form">
|
||||||
|
<input type="submit" value="Generate server keys" class="btn btn-success">
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
server_public_key_form = f"""
|
||||||
|
<code class="success">{server_public_key}</code>
|
||||||
|
<form action="/regenerate_server_keys" method="POST" class="option-form">
|
||||||
|
<input type="submit" value="Regenerate server keys" class="btn">
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 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"""
|
||||||
|
<div class="peer-item">
|
||||||
|
<div><strong>ID:</strong> {peer_id}</div>
|
||||||
|
<div><strong>Name:</strong> {peer_name}</div>
|
||||||
|
<div><strong>Public Key:</strong> <code>{peer_public_key}</code></div>
|
||||||
|
<div class="peer-actions">
|
||||||
|
<form action='/delete_peer' method='POST'>
|
||||||
|
<input type='hidden' name='peer_id_to_delete' value='{peer_id}'>
|
||||||
|
<input type='submit' value='Delete peer' class="btn btn-danger">
|
||||||
|
</form>
|
||||||
|
<form action='/download_peer_config' method='POST'>
|
||||||
|
<input type='hidden' name='peer_id' value='{peer_id}'>
|
||||||
|
<input type='submit' value='Download config file' class="btn">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>"""
|
||||||
|
|
||||||
|
html = f"""
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>SeaBee's WG Helper</title>
|
||||||
|
<style>
|
||||||
|
{CSS}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<div class="container header-content">
|
||||||
|
<h1>SeaBee's Wireguard Helper v0.2</h1>
|
||||||
|
<div class="navbar">
|
||||||
|
<a href="/logout">Logout</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="card">
|
||||||
|
<h2>System Status</h2>
|
||||||
|
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>Wireguard software:</strong>
|
||||||
|
<span class="{wg_installed_class}">{wg_installed_status}</span>
|
||||||
|
{install_wg_button}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>Wireguard autostart at boot:</strong>
|
||||||
|
<span class="{wg_enabled_at_boot_class}">{wg_enabled_at_boot_status}</span>
|
||||||
|
{wg_enabled_at_boot_button}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>UFW:</strong>
|
||||||
|
<span class="{ufw_installed_class}">{ufw_installed_status}</span>
|
||||||
|
{' & ' + '<span class="' + ufw_enabled_class + '">' + ufw_enabled_status + '</span>' if ufw_enabled_status else ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{f'''
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>UFW Wireguard Port:</strong>
|
||||||
|
<span class="{ufw_port_class}">{ufw_port_status}</span>
|
||||||
|
{ufw_port_button}
|
||||||
|
</div>
|
||||||
|
''' if ufw_port_status else ''}
|
||||||
|
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>iptables:</strong>
|
||||||
|
<span class="{iptables_installed_class}">{iptables_installed_status}</span>
|
||||||
|
{iptables_install_button}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>Server network interface:</strong>
|
||||||
|
<span>{server_network_interface}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>Wireguard status:</strong>
|
||||||
|
<span class="{wg_running_class}">{wg_running_status}</span>
|
||||||
|
{wg_running_button}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Server Configuration</h2>
|
||||||
|
|
||||||
|
<div class="option-row">
|
||||||
|
<strong>Server endpoint:</strong>
|
||||||
|
<div>{server_endpoint_form}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option-row">
|
||||||
|
<strong>Server Public key:</strong>
|
||||||
|
<div>{server_public_key_form}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Peer Management</h2>
|
||||||
|
|
||||||
|
<div class="status-item" style="margin-bottom: 20px;">
|
||||||
|
<strong>Create new peer:</strong>
|
||||||
|
<form action="/create_new_peer" method="POST" style="display:flex; gap:10px; margin-left:10px;">
|
||||||
|
<input type="text" placeholder="Peer name" name="peer_name" required>
|
||||||
|
<input type="submit" value="Create" class="btn btn-success">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{peers_list if peers_list else '<p>No peers configured yet. Create your first peer above.</p>'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
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"""
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Installation Error</title>
|
||||||
|
<style>{CSS}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Installation Failed</h2>
|
||||||
|
<p class="danger">Error: {e}</p>
|
||||||
|
<a href="/dashboard" class="btn">Back to Dashboard</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
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"""
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Installation Error</title>
|
||||||
|
<style>{CSS}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Installation Failed</h2>
|
||||||
|
<p class="danger">Error: {e}</p>
|
||||||
|
<a href="/dashboard" class="btn">Back to Dashboard</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
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"""
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>UFW Error</title>
|
||||||
|
<style>{CSS}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="card">
|
||||||
|
<h2>UFW Configuration Failed</h2>
|
||||||
|
<p class="danger">Error: {e}</p>
|
||||||
|
<a href="/dashboard" class="btn">Back to Dashboard</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
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)
|
||||||
Reference in New Issue
Block a user