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