Initial Commit
This commit is contained in:
@@ -1,3 +1,52 @@
|
|||||||
# Port-Forwarding-Services
|
# RelayDock
|
||||||
|
|
||||||
This is a Full Stack Port Forwarding Service. You can run it on a linux machine and forward a port with a extra systemd service.
|
## Pakete installieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y python3-flask socat jq
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dateien kopieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /opt/relaydock-web /etc/relaydock
|
||||||
|
sudo cp -r ./* /opt/relaydock-web/
|
||||||
|
sudo cp /opt/relaydock-web/config.json /etc/relaydock/config.json
|
||||||
|
sudo cp /opt/relaydock-web/relaydock@.service /etc/systemd/system/
|
||||||
|
sudo cp /opt/relaydock-web/relaydock.target /etc/systemd/system/
|
||||||
|
sudo cp /opt/relaydock-web/relaydock-web.service /etc/systemd/system/
|
||||||
|
sudo cp /opt/relaydock-web/relaydock-instance.sh /usr/local/bin/
|
||||||
|
sudo cp /opt/relaydock-web/relaydock-reconcile.sh /usr/local/bin/
|
||||||
|
sudo chmod +x /usr/local/bin/relaydock-instance.sh /usr/local/bin/relaydock-reconcile.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## sudoers für Web-UI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo visudo
|
||||||
|
```
|
||||||
|
|
||||||
|
Dann ergänzen:
|
||||||
|
|
||||||
|
```text
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /bin/systemctl start relaydock@*.service
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /bin/systemctl stop relaydock@*.service
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /bin/systemctl restart relaydock@*.service
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /bin/systemctl daemon-reload
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/local/bin/relaydock-reconcile.sh *
|
||||||
|
```
|
||||||
|
|
||||||
|
Wenn du den Webdienst als root laufen lässt, brauchst du das nicht, sicherer ist aber ein eigener User mit sauberem sudoers-Eintrag.
|
||||||
|
|
||||||
|
## Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now relaydock-web.service
|
||||||
|
sudo /usr/local/bin/relaydock-reconcile.sh /etc/relaydock/config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Aufruf
|
||||||
|
|
||||||
|
Weboberfläche dann auf Port 8080 öffnen.
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
from flask import Flask, render_template, request, redirect, url_for, flash
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
APP_TITLE = "RelayDock"
|
||||||
|
CONFIG_PATH = Path(os.environ.get("RELAYDOCK_CONFIG", "./config.json"))
|
||||||
|
RECONCILE_CMD = os.environ.get("RELAYDOCK_RECONCILE", "./relaydock-reconcile.sh")
|
||||||
|
SERVICE_PREFIX = os.environ.get("RELAYDOCK_PREFIX", "relaydock")
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.secret_key = os.environ.get("FLASK_SECRET_KEY", "change-me")
|
||||||
|
|
||||||
|
NAME_RE = re.compile(r"^[a-zA-Z0-9._-]+$")
|
||||||
|
PROTO_SET = {"udp", "tcp"}
|
||||||
|
ROLE_SET = {"oracle", "pi"}
|
||||||
|
|
||||||
|
|
||||||
|
def default_config():
|
||||||
|
return {"role": "oracle", "rules": []}
|
||||||
|
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
if not CONFIG_PATH.exists():
|
||||||
|
return default_config()
|
||||||
|
with CONFIG_PATH.open("r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def save_config(data):
|
||||||
|
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with CONFIG_PATH.open("w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
f.write("\n")
|
||||||
|
|
||||||
|
|
||||||
|
def validate_rule(rule, config):
|
||||||
|
errors = []
|
||||||
|
name = rule.get("name", "").strip()
|
||||||
|
proto = rule.get("proto", "").strip().lower()
|
||||||
|
target_host = rule.get("target_host", "").strip()
|
||||||
|
try:
|
||||||
|
listen_port = int(rule.get("listen_port", 0))
|
||||||
|
target_port = int(rule.get("target_port", 0))
|
||||||
|
except ValueError:
|
||||||
|
errors.append("Ports müssen Zahlen sein.")
|
||||||
|
return errors
|
||||||
|
|
||||||
|
if not NAME_RE.match(name):
|
||||||
|
errors.append("Name darf nur Buchstaben, Zahlen, Punkt, Unterstrich und Bindestrich enthalten.")
|
||||||
|
if proto not in PROTO_SET:
|
||||||
|
errors.append("Proto muss udp oder tcp sein.")
|
||||||
|
if not (1 <= listen_port <= 65535 and 1 <= target_port <= 65535):
|
||||||
|
errors.append("Ports müssen zwischen 1 und 65535 liegen.")
|
||||||
|
if not target_host:
|
||||||
|
errors.append("Target Host darf nicht leer sein.")
|
||||||
|
|
||||||
|
existing = [r["name"] for r in config.get("rules", [])]
|
||||||
|
if name in existing:
|
||||||
|
errors.append("Name existiert bereits.")
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def run_cmd(cmd):
|
||||||
|
return subprocess.run(cmd, text=True, capture_output=True)
|
||||||
|
|
||||||
|
|
||||||
|
def unit_name(name):
|
||||||
|
return f"{SERVICE_PREFIX}@{name}.service"
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
config = load_config()
|
||||||
|
rules = []
|
||||||
|
for rule in config.get("rules", []):
|
||||||
|
svc = unit_name(rule["name"])
|
||||||
|
state = run_cmd(["systemctl", "is-active", svc])
|
||||||
|
enabled = run_cmd(["systemctl", "is-enabled", svc])
|
||||||
|
rules.append({
|
||||||
|
**rule,
|
||||||
|
"service": svc,
|
||||||
|
"active": state.stdout.strip() if state.returncode == 0 else state.stdout.strip() or "inactive",
|
||||||
|
"enabled": enabled.stdout.strip() if enabled.returncode == 0 else enabled.stdout.strip() or "disabled",
|
||||||
|
})
|
||||||
|
return render_template("index.html", title=APP_TITLE, config=config, rules=rules)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/role")
|
||||||
|
def set_role():
|
||||||
|
config = load_config()
|
||||||
|
role = request.form.get("role", "").strip().lower()
|
||||||
|
if role not in ROLE_SET:
|
||||||
|
flash("Ungültige Rolle.", "error")
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
config["role"] = role
|
||||||
|
save_config(config)
|
||||||
|
flash("Rolle gespeichert.", "success")
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/rules/add")
|
||||||
|
def add_rule():
|
||||||
|
config = load_config()
|
||||||
|
rule = {
|
||||||
|
"name": request.form.get("name", ""),
|
||||||
|
"proto": request.form.get("proto", ""),
|
||||||
|
"listen_port": request.form.get("listen_port", "0"),
|
||||||
|
"target_host": request.form.get("target_host", ""),
|
||||||
|
"target_port": request.form.get("target_port", "0"),
|
||||||
|
}
|
||||||
|
errors = validate_rule(rule, config)
|
||||||
|
if errors:
|
||||||
|
for err in errors:
|
||||||
|
flash(err, "error")
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
rule["listen_port"] = int(rule["listen_port"])
|
||||||
|
rule["target_port"] = int(rule["target_port"])
|
||||||
|
config.setdefault("rules", []).append(rule)
|
||||||
|
save_config(config)
|
||||||
|
flash("Regel hinzugefügt.", "success")
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/rules/<name>/delete")
|
||||||
|
def delete_rule(name):
|
||||||
|
config = load_config()
|
||||||
|
before = len(config.get("rules", []))
|
||||||
|
config["rules"] = [r for r in config.get("rules", []) if r["name"] != name]
|
||||||
|
save_config(config)
|
||||||
|
if len(config["rules"]) < before:
|
||||||
|
flash(f"Regel {name} gelöscht.", "success")
|
||||||
|
else:
|
||||||
|
flash("Regel nicht gefunden.", "error")
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/rules/<name>/toggle")
|
||||||
|
def toggle_rule(name):
|
||||||
|
svc = unit_name(name)
|
||||||
|
state = run_cmd(["systemctl", "is-active", svc])
|
||||||
|
if state.returncode == 0 and state.stdout.strip() == "active":
|
||||||
|
result = run_cmd(["sudo", "systemctl", "stop", svc])
|
||||||
|
flash(result.stderr.strip() or result.stdout.strip() or f"{svc} gestoppt.", "success" if result.returncode == 0 else "error")
|
||||||
|
else:
|
||||||
|
result = run_cmd(["sudo", "systemctl", "start", svc])
|
||||||
|
flash(result.stderr.strip() or result.stdout.strip() or f"{svc} gestartet.", "success" if result.returncode == 0 else "error")
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/rules/<name>/restart")
|
||||||
|
def restart_rule(name):
|
||||||
|
svc = unit_name(name)
|
||||||
|
result = run_cmd(["sudo", "systemctl", "restart", svc])
|
||||||
|
flash(result.stderr.strip() or result.stdout.strip() or f"{svc} neugestartet.", "success" if result.returncode == 0 else "error")
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/reconcile")
|
||||||
|
def reconcile():
|
||||||
|
result = run_cmd(["sudo", RECONCILE_CMD, str(CONFIG_PATH)])
|
||||||
|
flash(result.stderr.strip() or result.stdout.strip() or "Synchronisierung ausgeführt.", "success" if result.returncode == 0 else "error")
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/daemon-reload")
|
||||||
|
def daemon_reload():
|
||||||
|
result = run_cmd(["sudo", "systemctl", "daemon-reload"])
|
||||||
|
flash(result.stderr.strip() or result.stdout.strip() or "systemd daemon-reload ausgeführt.", "success" if result.returncode == 0 else "error")
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host="0.0.0.0", port=8080, debug=True)
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"role": "oracle",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"name": "icarus-game",
|
||||||
|
"proto": "udp",
|
||||||
|
"listen_port": 17777,
|
||||||
|
"target_host": "2001:db8::1234",
|
||||||
|
"target_port": 17777
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "steam-query",
|
||||||
|
"proto": "udp",
|
||||||
|
"listen_port": 27015,
|
||||||
|
"target_host": "2001:db8::1234",
|
||||||
|
"target_port": 27015
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
NAME="${1:?instance name missing}"
|
||||||
|
CONFIG_FILE="${CONFIG_FILE:-/etc/relaydock/config.json}"
|
||||||
|
|
||||||
|
command -v jq >/dev/null 2>&1 || { echo "Fehlt: jq" >&2; exit 1; }
|
||||||
|
command -v socat >/dev/null 2>&1 || { echo "Fehlt: socat" >&2; exit 1; }
|
||||||
|
[[ -f "$CONFIG_FILE" ]] || { echo "Config fehlt: $CONFIG_FILE" >&2; exit 1; }
|
||||||
|
|
||||||
|
ROLE=$(jq -r '.role' "$CONFIG_FILE")
|
||||||
|
RULE=$(jq -e --arg name "$NAME" '.rules[] | select(.name == $name)' "$CONFIG_FILE")
|
||||||
|
PROTO=$(jq -r '.proto' <<<"$RULE")
|
||||||
|
LISTEN_PORT=$(jq -r '.listen_port' <<<"$RULE")
|
||||||
|
TARGET_HOST=$(jq -r '.target_host' <<<"$RULE")
|
||||||
|
TARGET_PORT=$(jq -r '.target_port' <<<"$RULE")
|
||||||
|
|
||||||
|
if [[ "$ROLE" == "oracle" ]]; then
|
||||||
|
if [[ "$PROTO" == "udp" ]]; then
|
||||||
|
exec socat "UDP4-LISTEN:${LISTEN_PORT},fork,reuseaddr" "UDP6:[${TARGET_HOST}]:${TARGET_PORT}"
|
||||||
|
else
|
||||||
|
exec socat "TCP4-LISTEN:${LISTEN_PORT},fork,reuseaddr" "TCP6:[${TARGET_HOST}]:${TARGET_PORT}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [[ "$PROTO" == "udp" ]]; then
|
||||||
|
exec socat "UDP6-LISTEN:${LISTEN_PORT},fork,reuseaddr" "UDP4:${TARGET_HOST}:${TARGET_PORT}"
|
||||||
|
else
|
||||||
|
exec socat "TCP6-LISTEN:${LISTEN_PORT},fork,reuseaddr" "TCP4:${TARGET_HOST}:${TARGET_PORT}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
Executable
+23
@@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CONFIG_FILE="${1:-/etc/relaydock/config.json}"
|
||||||
|
PREFIX="${RELAYDOCK_PREFIX:-relaydock}"
|
||||||
|
|
||||||
|
command -v jq >/dev/null 2>&1 || { echo "Fehlt: jq" >&2; exit 1; }
|
||||||
|
[[ -f "$CONFIG_FILE" ]] || { echo "Config fehlt: $CONFIG_FILE" >&2; exit 1; }
|
||||||
|
|
||||||
|
mapfile -t desired < <(jq -r '.rules[].name' "$CONFIG_FILE" | sort -u)
|
||||||
|
mapfile -t current < <(systemctl list-units --full --all "${PREFIX}@*.service" --no-legend | awk '{print $1}' | sed -E "s/^${PREFIX}@(.*)\\.service$/\\1/" | sort -u)
|
||||||
|
|
||||||
|
for name in "${desired[@]}"; do
|
||||||
|
systemctl enable --now "${PREFIX}@${name}.service"
|
||||||
|
done
|
||||||
|
|
||||||
|
for name in "${current[@]}"; do
|
||||||
|
if ! printf '%s\n' "${desired[@]}" | grep -qx "$name"; then
|
||||||
|
systemctl disable --now "${PREFIX}@${name}.service" || true
|
||||||
|
else
|
||||||
|
systemctl restart "${PREFIX}@${name}.service"
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=RelayDock Web UI
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/opt/relaydock-web
|
||||||
|
Environment=FLASK_SECRET_KEY=change-me
|
||||||
|
Environment=RELAYDOCK_CONFIG=/etc/relaydock/config.json
|
||||||
|
Environment=RELAYDOCK_RECONCILE=/usr/local/bin/relaydock-reconcile.sh
|
||||||
|
Environment=RELAYDOCK_PREFIX=relaydock
|
||||||
|
ExecStart=/usr/bin/python3 /opt/relaydock-web/app.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=2
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=All configured Pterodactyl forward instances
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Pterodactyl forward %I
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
PartOf=relaydock.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/bin/relaydock-instance.sh %i
|
||||||
|
Restart=always
|
||||||
|
RestartSec=2
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=relaydock.target
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--bg:#0b1220;
|
||||||
|
--panel:#121a2b;
|
||||||
|
--panel-2:#182338;
|
||||||
|
--text:#ecf3ff;
|
||||||
|
--muted:#a8b6cf;
|
||||||
|
--line:#263247;
|
||||||
|
--accent:#16c7b7;
|
||||||
|
--danger:#dc5d68;
|
||||||
|
--warn:#e0a648;
|
||||||
|
--ok:#2bb673;
|
||||||
|
}
|
||||||
|
* { box-sizing:border-box; }
|
||||||
|
body { margin:0; font-family:Inter,system-ui,sans-serif; background:linear-gradient(180deg,#09101d,#0e1728); color:var(--text); }
|
||||||
|
.wrap { max-width:1200px; margin:0 auto; padding:24px; }
|
||||||
|
.card { background:rgba(18,26,43,.92); border:1px solid var(--line); border-radius:16px; padding:20px; box-shadow:0 10px 30px rgba(0,0,0,.2); }
|
||||||
|
.hero { display:flex; justify-content:space-between; gap:16px; align-items:flex-start; margin-bottom:20px; }
|
||||||
|
.grid { display:grid; gap:20px; }
|
||||||
|
.grid.two { grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); margin-bottom:20px; }
|
||||||
|
.stack { display:grid; gap:12px; }
|
||||||
|
.actions, .row-actions { display:flex; gap:10px; flex-wrap:wrap; }
|
||||||
|
input, select, button { width:100%; border-radius:12px; border:1px solid var(--line); background:var(--panel-2); color:var(--text); padding:12px 14px; }
|
||||||
|
button { width:auto; cursor:pointer; }
|
||||||
|
button.primary { background:var(--accent); color:#06231f; border-color:transparent; font-weight:700; }
|
||||||
|
button.danger { background:var(--danger); color:white; border-color:transparent; }
|
||||||
|
table { width:100%; border-collapse:collapse; }
|
||||||
|
th, td { text-align:left; padding:12px; border-top:1px solid var(--line); vertical-align:top; }
|
||||||
|
.table-wrap { overflow:auto; }
|
||||||
|
.badge { display:inline-block; padding:6px 10px; border-radius:999px; background:#25324a; border:1px solid var(--line); }
|
||||||
|
.badge.active { background:rgba(43,182,115,.15); color:#92efbf; border-color:rgba(43,182,115,.35); }
|
||||||
|
.badge.inactive, .badge.failed { background:rgba(224,166,72,.12); color:#ffd28b; border-color:rgba(224,166,72,.35); }
|
||||||
|
.badge.muted { color:var(--muted); }
|
||||||
|
.flash { padding:12px 14px; border-radius:12px; margin-bottom:12px; }
|
||||||
|
.flash.success { background:rgba(43,182,115,.14); border:1px solid rgba(43,182,115,.3); }
|
||||||
|
.flash.error { background:rgba(220,93,104,.14); border:1px solid rgba(220,93,104,.3); }
|
||||||
|
p { color:var(--muted); }
|
||||||
|
@media (max-width: 760px) { .hero { flex-direction:column; } th:nth-child(6), td:nth-child(6) { display:none; } }
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="wrap">
|
||||||
|
<header class="hero card">
|
||||||
|
<div>
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
<p>JSON verwalten, systemd-Template-Instanzen synchronisieren und einzelne Weiterleitungen starten oder stoppen.</p>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<form method="post" action="{{ url_for('daemon_reload') }}"><button>systemd reload</button></form>
|
||||||
|
<form method="post" action="{{ url_for('reconcile') }}"><button class="primary">JSON synchronisieren</button></form>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
<section class="stack">
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="flash {{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<section class="grid two">
|
||||||
|
<article class="card">
|
||||||
|
<h2>Rolle</h2>
|
||||||
|
<form method="post" action="{{ url_for('set_role') }}" class="stack">
|
||||||
|
<select name="role">
|
||||||
|
<option value="oracle" {% if config.role == 'oracle' %}selected{% endif %}>oracle</option>
|
||||||
|
<option value="pi" {% if config.role == 'pi' %}selected{% endif %}>pi</option>
|
||||||
|
</select>
|
||||||
|
<button>Rolle speichern</button>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="card">
|
||||||
|
<h2>Neue Regel</h2>
|
||||||
|
<form method="post" action="{{ url_for('add_rule') }}" class="stack">
|
||||||
|
<input name="name" placeholder="icarus-game" required>
|
||||||
|
<select name="proto"><option value="udp">udp</option><option value="tcp">tcp</option></select>
|
||||||
|
<input name="listen_port" placeholder="17777" type="number" min="1" max="65535" required>
|
||||||
|
<input name="target_host" placeholder="2001:db8::1234 oder 192.168.178.50" required>
|
||||||
|
<input name="target_port" placeholder="17777" type="number" min="1" max="65535" required>
|
||||||
|
<button class="primary">Regel hinzufügen</button>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>Weiterleitungen</h2>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Proto</th>
|
||||||
|
<th>Listen</th>
|
||||||
|
<th>Ziel</th>
|
||||||
|
<th>Aktiv</th>
|
||||||
|
<th>Enabled</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for rule in rules %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ rule.name }}</td>
|
||||||
|
<td>{{ rule.proto }}</td>
|
||||||
|
<td>{{ rule.listen_port }}</td>
|
||||||
|
<td>{{ rule.target_host }}:{{ rule.target_port }}</td>
|
||||||
|
<td><span class="badge {{ rule.active }}">{{ rule.active }}</span></td>
|
||||||
|
<td><span class="badge muted">{{ rule.enabled }}</span></td>
|
||||||
|
<td>
|
||||||
|
<div class="row-actions">
|
||||||
|
<form method="post" action="{{ url_for('toggle_rule', name=rule.name) }}"><button>{% if rule.active == 'active' %}Stop{% else %}Start{% endif %}</button></form>
|
||||||
|
<form method="post" action="{{ url_for('restart_rule', name=rule.name) }}"><button>Restart</button></form>
|
||||||
|
<form method="post" action="{{ url_for('delete_rule', name=rule.name) }}" onsubmit="return confirm('Regel wirklich löschen?');"><button class="danger">Löschen</button></form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr><td colspan="7">Noch keine Regeln vorhanden.</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user