Multi-interface management, topology view, performance tuning, precompiled CSS

- Interface model with per-interface subnet/port/keys; import/adopt existing wg-quick configs (key-less imported peers, optional key rotation), cascade delete
- Split wireguard.py into a package (keys via cryptography X25519, status, addressing, conf parse/render, sync, host tuning)
- ECharts horizontal topology view (interface -> peers -> site subnets)
- Advanced options: MTU, MSS clamping, FwMark/Table, custom PostUp/PostDown, per-peer keepalive override
- Runtime settings (sample interval/retention, online threshold, UI refresh) with traffic sample pruning; host tuning (UDP buffers, backlog, GRO forwarding)
- Precompiled Tailwind CSS replacing Play CDN runtime (fixes FOUC); stable table layout and diffed polling renders
- Host network mode in compose; NAT/isolation iptables moved into app sync

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
lofyer
2026-07-05 14:01:44 +08:00
co-authored by factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent 192960ad3e
commit 1b227c2470
31 changed files with 2361 additions and 583 deletions
+188
View File
@@ -0,0 +1,188 @@
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from ..config import settings
from ..crypto import decrypt
from ..models import Interface, Peer
from .addressing import server_address
def config_path(name: str) -> Path:
return settings.wg_config_dir / f"{name}.conf"
def discover_configs() -> list[str]:
if not settings.wg_config_dir.exists():
return []
return sorted(p.stem for p in settings.wg_config_dir.glob("*.conf"))
def server_allowed_ips(peer: Peer) -> str:
allowed = peer.address
if peer.extra_allowed_ips:
allowed += f", {peer.extra_allowed_ips}"
return allowed
def render_server_config(iface: Interface, peers: list[Peer]) -> str:
lines = [
"[Interface]",
f"PrivateKey = {decrypt(iface.private_key_enc)}",
f"Address = {server_address(iface.subnet)}",
f"ListenPort = {iface.listen_port}",
]
if iface.mtu:
lines.append(f"MTU = {iface.mtu}")
if iface.fwmark:
lines.append(f"FwMark = {iface.fwmark}")
if iface.route_table:
lines.append(f"Table = {iface.route_table}")
if iface.mss_clamp:
rule = (
"-o %i -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu"
)
lines.append(f"PostUp = iptables -t mangle -A FORWARD {rule}")
lines.append(f"PostDown = iptables -t mangle -D FORWARD {rule} || true")
# wg-quick adds routes for /32 peer addresses automatically; extra
# site subnets need explicit routes so return traffic enters the tunnel.
for peer in peers:
if peer.enabled and peer.extra_allowed_ips:
for subnet in peer.extra_allowed_ips.split(","):
subnet = subnet.strip()
lines.append(f"PostUp = ip route replace {subnet} dev %i")
for command in (iface.post_up or "").splitlines():
command = command.strip()
if command:
lines.append(f"PostUp = {command}")
for command in (iface.post_down or "").splitlines():
command = command.strip()
if command:
lines.append(f"PostDown = {command}")
for peer in peers:
if not peer.enabled:
continue
lines += [
"",
"[Peer]",
f"# {peer.name}",
f"PublicKey = {peer.public_key}",
]
if peer.preshared_key_enc:
lines.append(f"PresharedKey = {decrypt(peer.preshared_key_enc)}")
lines.append(f"AllowedIPs = {server_allowed_ips(peer)}")
return "\n".join(lines) + "\n"
def render_client_config(peer: Peer, iface: Interface) -> str:
if not peer.has_private_key:
raise ValueError(
"Peer has no private key (imported). Rotate keys to generate a config."
)
lines = [
"[Interface]",
f"PrivateKey = {decrypt(peer.private_key_enc)}",
f"Address = {peer.address}",
f"DNS = {peer.dns or iface.dns}",
]
if iface.mtu:
lines.append(f"MTU = {iface.mtu}")
lines += [
"",
"[Peer]",
f"PublicKey = {iface.public_key}",
]
if peer.preshared_key_enc:
lines.append(f"PresharedKey = {decrypt(peer.preshared_key_enc)}")
keepalive = (
peer.persistent_keepalive
if peer.persistent_keepalive is not None
else iface.persistent_keepalive
)
lines += [
f"Endpoint = {iface.host}:{iface.listen_port}",
f"AllowedIPs = {peer.client_allowed_ips or iface.allowed_ips}",
]
if keepalive:
lines.append(f"PersistentKeepalive = {keepalive}")
return "\n".join(lines) + "\n"
def write_server_config(iface: Interface, peers: list[Peer]) -> None:
settings.wg_config_dir.mkdir(parents=True, exist_ok=True)
path = config_path(iface.name)
path.touch(mode=0o600)
path.write_text(render_server_config(iface, peers))
def backup_config(name: str) -> Path | None:
path = config_path(name)
if not path.exists():
return None
stamp = datetime.now().strftime("%Y%m%d%H%M%S")
backup = path.with_name(f"{name}.conf.bak-{stamp}")
backup.write_bytes(path.read_bytes())
backup.chmod(0o600)
return backup
@dataclass
class ParsedPeer:
public_key: str = ""
preshared_key: str = ""
allowed_ips: list[str] = field(default_factory=list)
name: str = ""
@dataclass
class ParsedConfig:
private_key: str = ""
address: str = ""
listen_port: int | None = None
peers: list[ParsedPeer] = field(default_factory=list)
def parse_config(text: str) -> ParsedConfig:
parsed = ParsedConfig()
section = ""
current: ParsedPeer | None = None
pending_comment = ""
for raw in text.splitlines():
line = raw.strip()
if not line:
continue
if line.startswith("#"):
comment = line.lstrip("# ").strip()
if section == "peer" and current is not None and not current.name:
current.name = comment
else:
pending_comment = comment
continue
if line.startswith("["):
section = line.strip("[]").lower()
if section == "peer":
current = ParsedPeer(name=pending_comment)
parsed.peers.append(current)
pending_comment = ""
continue
if "=" not in line:
continue
key, _, value = line.partition("=")
key = key.strip().lower()
value = value.strip()
if section == "interface":
if key == "privatekey":
parsed.private_key = value
elif key == "address":
parsed.address = value.split(",")[0].strip()
elif key == "listenport":
parsed.listen_port = int(value)
elif section == "peer" and current is not None:
if key == "publickey":
current.public_key = value
elif key == "presharedkey":
current.preshared_key = value
elif key == "allowedips":
current.allowed_ips += [v.strip() for v in value.split(",") if v.strip()]
parsed.peers = [p for p in parsed.peers if p.public_key]
return parsed