Merge pull request #2 from outis1one/claude/avaya-switch-manager-DxZs5
Claude/avaya switch manager dx zs5
This commit is contained in:
+248
-67
File diff suppressed because one or more lines are too long
+1063
-30
File diff suppressed because it is too large
Load Diff
+473
-1
@@ -195,6 +195,7 @@ _RE_DIR = re.compile(r'^(in|out)$')
|
||||
_RE_PROTO = re.compile(r'^(ip|tcp|udp|icmp)$')
|
||||
_RE_ACTION = re.compile(r'^(permit|deny)$')
|
||||
_RE_DESC = re.compile(r'^[a-zA-Z0-9\-_ ]{0,64}$')
|
||||
_RE_IP = re.compile(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||
|
||||
def _san(v: str, pat: re.Pattern, field: str) -> str:
|
||||
"""Reject shell-injection characters and check value against an allow-list regex."""
|
||||
@@ -549,6 +550,7 @@ class AclRule(BaseModel):
|
||||
dst_mask: Optional[str] = "0.0.0.255"
|
||||
dst_any: Optional[bool] = True
|
||||
port: Optional[str] = ""
|
||||
port_end: Optional[str] = "" # when set, generates "range port port_end"
|
||||
@field_validator("action")
|
||||
@classmethod
|
||||
def ca(cls, v): return _san(v, _RE_ACTION, "action")
|
||||
@@ -639,7 +641,12 @@ def build_acl(acl: AclCreate) -> list[str]:
|
||||
for i, r in enumerate(acl.rules):
|
||||
src = "any" if r.src_any else f"{r.src} {r.src_mask}"
|
||||
dst = "any" if r.dst_any else f"{r.dst} {r.dst_mask}"
|
||||
port_str = f" eq {r.port}" if r.port else ""
|
||||
if r.port and r.port_end:
|
||||
port_str = f" range {r.port} {r.port_end}"
|
||||
elif r.port:
|
||||
port_str = f" eq {r.port}"
|
||||
else:
|
||||
port_str = ""
|
||||
cmds.append(f" {i+1} {r.action} {r.proto} {src} {dst}{port_str}")
|
||||
vid = san_vid(acl.apply_vlan, "apply_vlan")
|
||||
cmds += [f"interface vlan {vid}",
|
||||
@@ -1345,6 +1352,32 @@ def _get_switch_dhcp_status() -> dict:
|
||||
except Exception:
|
||||
return {"running": False, "vlans": []}
|
||||
|
||||
def _get_relay_status() -> dict:
|
||||
"""Read current DHCP relay (ip helper-address) config from each VLAN interface."""
|
||||
import re as _re
|
||||
try:
|
||||
raw = read_cmd("show ip helper-address")
|
||||
configured = {}
|
||||
for line in raw.splitlines():
|
||||
# Typical output: " 10 192.168.99.1"
|
||||
m = _re.match(r'\s*(\d+)\s+(\d+\.\d+\.\d+\.\d+)', line)
|
||||
if m:
|
||||
configured[int(m.group(1))] = m.group(2)
|
||||
return {"vlans": configured, "ok": True}
|
||||
except Exception as e:
|
||||
log.warning(f"Relay status fetch failed: {e}")
|
||||
return {"vlans": {}, "ok": False}
|
||||
|
||||
def _build_relay_cmds(opnsense_ip: str, vlan_ids: list) -> list:
|
||||
"""Generate ERS 5952 CLI to set ip helper-address on the specified VLANs."""
|
||||
cmds = []
|
||||
for vid in vlan_ids:
|
||||
cmds += [
|
||||
f"interface vlan {vid}",
|
||||
f" ip helper-address {opnsense_ip}",
|
||||
]
|
||||
return cmds
|
||||
|
||||
def _find_conflicts(switch_res: list, opnsense_res: list) -> list:
|
||||
"""
|
||||
Find same MAC in both switch and OPNsense.
|
||||
@@ -1386,6 +1419,14 @@ class SyncRequest(BaseModel):
|
||||
mac: str
|
||||
direction: str # "to_switch" | "to_opnsense" | "remove_switch" | "remove_opnsense"
|
||||
|
||||
class RelayConfig(BaseModel):
|
||||
token: str
|
||||
opnsense_ip: str
|
||||
vlans: list = [10, 20, 30, 40, 50] # VLANs to relay; 99 is always local
|
||||
@field_validator("opnsense_ip")
|
||||
@classmethod
|
||||
def cip(cls, v): return _san(v, _RE_IP, "opnsense_ip")
|
||||
|
||||
# ── DHCP endpoints ─────────────────────────────────────────────────────────
|
||||
|
||||
@app.get("/api/dhcp/overview")
|
||||
@@ -1421,6 +1462,7 @@ def dhcp_overview():
|
||||
opnsense_res = _get_opnsense_reservations(cfg) if cfg.get("key") else []
|
||||
opnsense_leases = _get_opnsense_leases(cfg) if cfg.get("key") else []
|
||||
conflicts = _find_conflicts(switch_res, opnsense_res)
|
||||
relay_status = _get_relay_status()
|
||||
|
||||
# Which VLANs have switch DHCP vs OPNsense
|
||||
# Switch DHCP VLANs from status, OPNsense VLANs inferred from interface names
|
||||
@@ -1439,6 +1481,7 @@ def dhcp_overview():
|
||||
"leases": opnsense_leases,
|
||||
"interfaces": opnsense_ifaces,
|
||||
},
|
||||
"relay": relay_status,
|
||||
"conflicts": conflicts,
|
||||
"has_conflicts": len(conflicts) > 0,
|
||||
}
|
||||
@@ -1568,6 +1611,25 @@ def sync_reservation(body: SyncRequest):
|
||||
|
||||
raise HTTPException(400, f"Unknown direction: {body.direction}")
|
||||
|
||||
@app.get("/api/dhcp/relay/status")
|
||||
def relay_status_endpoint():
|
||||
"""Return current ip helper-address config from the switch per VLAN."""
|
||||
return _get_relay_status()
|
||||
|
||||
@app.post("/api/dhcp/relay/configure")
|
||||
def configure_relay(body: RelayConfig):
|
||||
"""
|
||||
Push ip helper-address to each non-management VLAN so the switch relays
|
||||
DHCP requests to OPNsense. VLAN 99 is never relayed — it stays local
|
||||
as the management / recovery path.
|
||||
"""
|
||||
require_session(body.token)
|
||||
safe_vlans = [int(v) for v in body.vlans if int(v) != 99]
|
||||
if not safe_vlans:
|
||||
raise HTTPException(400, "No VLANs to configure (VLAN 99 is excluded)")
|
||||
cmds = _build_relay_cmds(body.opnsense_ip, safe_vlans)
|
||||
return push_one_by_one(cmds)
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# CONTROL D / ctrld DNS MANAGEMENT
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
@@ -2313,3 +2375,413 @@ def save_local_hostnames(body: LocalHostnamesUpdate):
|
||||
"then run: docker compose up -d dnsmasq"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# OPNSENSE WIREGUARD — ROUTER-LEVEL VPN WITH PER-VLAN ACCESS CONTROL
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# Moves WireGuard from the management computer onto OPNsense so any
|
||||
# device on any VLAN can VPN home without touching the management PC.
|
||||
# Each peer is granted access only to the VLANs you choose.
|
||||
#
|
||||
# Architecture:
|
||||
# OPNsense wg1 interface (10.99.2.0/24 — separate from local wg0)
|
||||
# Peer Alice → tunnel IP 10.99.2.2 → allowed VLAN 10 + VLAN 20
|
||||
# Peer Bob → tunnel IP 10.99.2.3 → allowed VLAN 10 only
|
||||
# Private keys are generated here and stored only on the mgmt PC.
|
||||
# OPNsense receives only the public key (standard WireGuard practice).
|
||||
|
||||
OPN_WG_FILE = _Path("/etc/switch-manager/opnsense_wg.json")
|
||||
|
||||
|
||||
def _load_opnsense_wg() -> dict:
|
||||
if OPN_WG_FILE.exists():
|
||||
try:
|
||||
return _json.loads(OPN_WG_FILE.read_text())
|
||||
except Exception:
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
||||
def _save_opnsense_wg(cfg: dict):
|
||||
OPN_WG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
OPN_WG_FILE.write_text(_json.dumps(cfg, indent=2))
|
||||
OPN_WG_FILE.chmod(0o600)
|
||||
|
||||
|
||||
class OPNWGServerSetup(BaseModel):
|
||||
token: str
|
||||
server_name: str = "switch-mgmt-vpn"
|
||||
listen_port: int = 51820
|
||||
tunnel_subnet: str = "10.99.2.0/24"
|
||||
public_endpoint: str = "" # public IP or DDNS hostname for client configs
|
||||
|
||||
|
||||
class OPNWGAddPeer(BaseModel):
|
||||
token: str
|
||||
name: str
|
||||
allowed_vlans: list # list of VLAN IDs: [10, 20, 30]
|
||||
vlan_subnets: dict # {10: "192.168.10.0/24", 20: "192.168.20.0/24", ...}
|
||||
|
||||
|
||||
@app.get("/api/opnsense/wireguard/status")
|
||||
def opnsense_wg_status():
|
||||
"""Check OPNsense WireGuard plugin, server, and peer state."""
|
||||
opn_cfg = _load_opnsense_cfg()
|
||||
if not opn_cfg:
|
||||
return {"opnsense_configured": False}
|
||||
|
||||
wg = _load_opnsense_wg()
|
||||
|
||||
# Probe for the WireGuard plugin — a 404 means the plugin isn't installed
|
||||
try:
|
||||
_opnsense_request(opn_cfg, "wireguard/server/searchServer")
|
||||
plugin_ok = True
|
||||
except ValueError as e:
|
||||
msg = str(e)
|
||||
# 404 → plugin absent; other errors → reachability / auth issue
|
||||
plugin_ok = False
|
||||
return {
|
||||
"opnsense_configured": True,
|
||||
"plugin_installed": False,
|
||||
"error": msg,
|
||||
"server": None,
|
||||
"peers": [],
|
||||
}
|
||||
|
||||
# If we have a saved server UUID, fetch live info
|
||||
server_info = None
|
||||
if wg.get("server_uuid"):
|
||||
try:
|
||||
s = _opnsense_request(opn_cfg, f"wireguard/server/getServer/{wg['server_uuid']}")
|
||||
srv = s.get("server", {})
|
||||
server_info = {
|
||||
"uuid": wg["server_uuid"],
|
||||
"name": srv.get("name", wg.get("server_name","")),
|
||||
"pubkey": srv.get("pubkey", wg.get("server_pubkey","")),
|
||||
"tunnel_ip": wg.get("server_tunnel_ip",""),
|
||||
"listen_port": wg.get("listen_port", 51820),
|
||||
"public_endpoint": wg.get("public_endpoint",""),
|
||||
}
|
||||
except Exception:
|
||||
# Server UUID no longer valid (e.g. OPNsense was reset)
|
||||
server_info = None
|
||||
|
||||
# Merge OPNsense peer list with local metadata (which holds allowed_vlans)
|
||||
local_peers = {p["name"]: p for p in wg.get("peers", [])}
|
||||
opn_peers = []
|
||||
try:
|
||||
resp = _opnsense_request(opn_cfg, "wireguard/client/searchClient")
|
||||
opn_peers = resp.get("rows", [])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
merged = []
|
||||
for p in opn_peers:
|
||||
name = p.get("name", "")
|
||||
loc = local_peers.get(name, {})
|
||||
merged.append({
|
||||
"uuid": p.get("uuid", ""),
|
||||
"name": name,
|
||||
"enabled": p.get("enabled", "0") == "1",
|
||||
"tunnel_ip": p.get("tunneladdress", ""),
|
||||
"allowed_vlans": loc.get("allowed_vlans", []),
|
||||
})
|
||||
|
||||
return {
|
||||
"opnsense_configured": True,
|
||||
"plugin_installed": plugin_ok,
|
||||
"server": server_info,
|
||||
"peers": merged,
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/opnsense/wireguard/setup-server")
|
||||
def opnsense_wg_setup_server(body: OPNWGServerSetup):
|
||||
"""Create (or replace) a WireGuard server on OPNsense via its API."""
|
||||
require_session(body.token)
|
||||
opn_cfg = _load_opnsense_cfg()
|
||||
if not opn_cfg:
|
||||
raise HTTPException(400, "OPNsense not configured — connect it in the DHCP tab first")
|
||||
|
||||
import ipaddress as _ip, time as _time
|
||||
|
||||
try:
|
||||
net = _ip.ip_network(body.tunnel_subnet, strict=False)
|
||||
except Exception:
|
||||
raise HTTPException(400, "Invalid tunnel_subnet — use CIDR notation e.g. 10.99.2.0/24")
|
||||
|
||||
server_tunnel_ip = f"{list(net.hosts())[0]}/{net.prefixlen}"
|
||||
|
||||
wg = _load_opnsense_wg()
|
||||
|
||||
# Tear down any pre-existing server so we start clean
|
||||
if wg.get("server_uuid"):
|
||||
try:
|
||||
_opnsense_request(opn_cfg,
|
||||
f"wireguard/server/delServer/{wg['server_uuid']}", method="POST")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
payload = {
|
||||
"server": {
|
||||
"enabled": "1",
|
||||
"name": body.server_name,
|
||||
"instance": "1", # creates wg1 — leaves wg0 (local) untouched
|
||||
"port": str(body.listen_port),
|
||||
"tunneladdress": server_tunnel_ip,
|
||||
"dns": "",
|
||||
"peers": "",
|
||||
}
|
||||
}
|
||||
try:
|
||||
result = _opnsense_request(opn_cfg, "wireguard/server/addServer",
|
||||
method="POST", body=payload)
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"OPNsense rejected server creation: {e}")
|
||||
|
||||
server_uuid = result.get("uuid","")
|
||||
if not server_uuid:
|
||||
raise HTTPException(500, "OPNsense did not return a server UUID")
|
||||
|
||||
# Apply so OPNsense generates the keypair, then read it back
|
||||
try:
|
||||
_opnsense_request(opn_cfg, "wireguard/service/reconfigure", method="POST")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_time.sleep(1.5) # give the daemon a moment to generate keys
|
||||
server_pubkey = ""
|
||||
try:
|
||||
s = _opnsense_request(opn_cfg, f"wireguard/server/getServer/{server_uuid}")
|
||||
server_pubkey = s.get("server", {}).get("pubkey", "")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
wg = {
|
||||
"server_uuid": server_uuid,
|
||||
"server_name": body.server_name,
|
||||
"listen_port": body.listen_port,
|
||||
"tunnel_subnet": body.tunnel_subnet,
|
||||
"server_tunnel_ip": server_tunnel_ip,
|
||||
"server_pubkey": server_pubkey,
|
||||
"public_endpoint": body.public_endpoint,
|
||||
"peers": [],
|
||||
}
|
||||
_save_opnsense_wg(wg)
|
||||
|
||||
log.info(f"OPNsense WG server created: {body.server_name} uuid={server_uuid}")
|
||||
return {
|
||||
"success": True,
|
||||
"server_uuid": server_uuid,
|
||||
"server_pubkey": server_pubkey,
|
||||
"server_tunnel_ip": server_tunnel_ip,
|
||||
"listen_port": body.listen_port,
|
||||
}
|
||||
|
||||
|
||||
@app.delete("/api/opnsense/wireguard/server")
|
||||
def opnsense_wg_delete_server(token: str):
|
||||
"""Remove the WireGuard server from OPNsense and clear local state."""
|
||||
require_session(token)
|
||||
opn_cfg = _load_opnsense_cfg()
|
||||
if not opn_cfg:
|
||||
raise HTTPException(400, "OPNsense not configured")
|
||||
wg = _load_opnsense_wg()
|
||||
if not wg.get("server_uuid"):
|
||||
raise HTTPException(404, "No server is configured")
|
||||
try:
|
||||
_opnsense_request(opn_cfg,
|
||||
f"wireguard/server/delServer/{wg['server_uuid']}", method="POST")
|
||||
_opnsense_request(opn_cfg, "wireguard/service/reconfigure", method="POST")
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"Failed to delete server: {e}")
|
||||
_save_opnsense_wg({})
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@app.post("/api/opnsense/wireguard/add-peer")
|
||||
def opnsense_wg_add_peer(body: OPNWGAddPeer):
|
||||
"""
|
||||
Generate a WireGuard keypair, register the peer on OPNsense,
|
||||
link it to the server, and return a ready-to-use .conf for the client.
|
||||
The private key is stored only on the management PC (never sent to OPNsense).
|
||||
"""
|
||||
require_session(body.token)
|
||||
opn_cfg = _load_opnsense_cfg()
|
||||
if not opn_cfg:
|
||||
raise HTTPException(400, "OPNsense not configured")
|
||||
wg = _load_opnsense_wg()
|
||||
if not wg.get("server_uuid"):
|
||||
raise HTTPException(400, "Set up the WireGuard server on OPNsense first")
|
||||
|
||||
import ipaddress as _ip, re as _re
|
||||
|
||||
# ── Allocate next free IP in the tunnel subnet ────────────────────
|
||||
net = _ip.ip_network(wg["tunnel_subnet"], strict=False)
|
||||
hosts = list(net.hosts())
|
||||
used = set()
|
||||
# Reserve the server's own tunnel IP
|
||||
m = _re.match(r'(\S+)/\d+', wg.get("server_tunnel_ip", ""))
|
||||
if m:
|
||||
used.add(m.group(1))
|
||||
for p in wg.get("peers", []):
|
||||
m2 = _re.match(r'(\S+)/\d+', p.get("tunnel_ip", ""))
|
||||
if m2:
|
||||
used.add(m2.group(1))
|
||||
|
||||
peer_ip_obj = next((h for h in hosts if str(h) not in used), None)
|
||||
if not peer_ip_obj:
|
||||
raise HTTPException(400, "Tunnel subnet is full — no IPs available for new peer")
|
||||
peer_ip = f"{peer_ip_obj}/{net.prefixlen}"
|
||||
|
||||
# ── Build the AllowedIPs list from chosen VLANs ───────────────────
|
||||
vlan_cidrs = []
|
||||
for vid in body.allowed_vlans:
|
||||
subnet = (body.vlan_subnets.get(str(vid))
|
||||
or body.vlan_subnets.get(int(vid))
|
||||
or f"192.168.{vid}.0/24")
|
||||
vlan_cidrs.append(subnet)
|
||||
# Always include the tunnel subnet so the client can reach the server
|
||||
allowed_ips = ", ".join([str(net)] + vlan_cidrs) if vlan_cidrs else str(net)
|
||||
|
||||
# ── Generate keypair (private key stays on mgmt PC only) ─────────
|
||||
c_priv, c_pub = _wg_genkey_api()
|
||||
|
||||
# ── Register peer (client) on OPNsense ───────────────────────────
|
||||
peer_payload = {
|
||||
"client": {
|
||||
"enabled": "1",
|
||||
"name": body.name,
|
||||
"pubkey": c_pub,
|
||||
"psk": "",
|
||||
"tunneladdress": peer_ip,
|
||||
"serveraddress": "",
|
||||
"serverport": "",
|
||||
"keepalive": "25",
|
||||
}
|
||||
}
|
||||
try:
|
||||
result = _opnsense_request(opn_cfg, "wireguard/client/addClient",
|
||||
method="POST", body=peer_payload)
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"OPNsense rejected peer creation: {e}")
|
||||
|
||||
peer_uuid = result.get("uuid", "")
|
||||
if not peer_uuid:
|
||||
raise HTTPException(500, "OPNsense did not return a peer UUID")
|
||||
|
||||
# ── Link peer to server (append to server's peers list) ──────────
|
||||
try:
|
||||
s = _opnsense_request(opn_cfg,
|
||||
f"wireguard/server/getServer/{wg['server_uuid']}")
|
||||
srv = s.get("server", {})
|
||||
existing = srv.get("peers", "")
|
||||
new_peers = f"{existing},{peer_uuid}" if existing else peer_uuid
|
||||
_opnsense_request(opn_cfg,
|
||||
f"wireguard/server/setServer/{wg['server_uuid']}", method="POST",
|
||||
body={"server": {**srv, "peers": new_peers}})
|
||||
except Exception as e:
|
||||
log.warning(f"Could not link peer to server (peer still registered): {e}")
|
||||
|
||||
# Apply config
|
||||
try:
|
||||
_opnsense_request(opn_cfg, "wireguard/service/reconfigure", method="POST")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── Build client .conf ────────────────────────────────────────────
|
||||
server_pubkey = wg.get("server_pubkey", "")
|
||||
endpoint_host = wg.get("public_endpoint", "") or "<YOUR-OPNSENSE-PUBLIC-IP>"
|
||||
endpoint_port = wg.get("listen_port", 51820)
|
||||
tunnel_gw = wg.get("server_tunnel_ip", "").split("/")[0]
|
||||
|
||||
client_conf = (
|
||||
f"[Interface]\n"
|
||||
f"PrivateKey = {c_priv}\n"
|
||||
f"Address = {peer_ip}\n"
|
||||
f"DNS = {tunnel_gw}\n\n"
|
||||
f"[Peer]\n"
|
||||
f"PublicKey = {server_pubkey or '<SERVER_PUBKEY>'}\n"
|
||||
f"Endpoint = {endpoint_host}:{endpoint_port}\n"
|
||||
f"AllowedIPs = {allowed_ips}\n"
|
||||
f"PersistentKeepalive = 25\n"
|
||||
)
|
||||
|
||||
# ── Persist peer metadata locally ────────────────────────────────
|
||||
peer_meta = {
|
||||
"uuid": peer_uuid,
|
||||
"name": body.name,
|
||||
"pub_key": c_pub,
|
||||
"priv_key": c_priv, # NEVER sent to OPNsense
|
||||
"tunnel_ip": peer_ip,
|
||||
"allowed_vlans": body.allowed_vlans,
|
||||
"allowed_ips": allowed_ips,
|
||||
"config": client_conf,
|
||||
}
|
||||
peers = [p for p in wg.get("peers", []) if p.get("name") != body.name]
|
||||
peers.append(peer_meta)
|
||||
wg["peers"] = peers
|
||||
_save_opnsense_wg(wg)
|
||||
|
||||
log.info(f"OPNsense WG peer added: {body.name} → {peer_ip} VLANs={body.allowed_vlans}")
|
||||
return {
|
||||
"success": True,
|
||||
"uuid": peer_uuid,
|
||||
"name": body.name,
|
||||
"tunnel_ip": peer_ip,
|
||||
"allowed_vlans": body.allowed_vlans,
|
||||
"config": client_conf,
|
||||
}
|
||||
|
||||
|
||||
@app.delete("/api/opnsense/wireguard/peer/{uuid}")
|
||||
def opnsense_wg_remove_peer(uuid: str, token: str):
|
||||
"""Remove a peer from OPNsense and from local metadata."""
|
||||
require_session(token)
|
||||
opn_cfg = _load_opnsense_cfg()
|
||||
if not opn_cfg:
|
||||
raise HTTPException(400, "OPNsense not configured")
|
||||
wg = _load_opnsense_wg()
|
||||
|
||||
# Remove from OPNsense
|
||||
try:
|
||||
_opnsense_request(opn_cfg,
|
||||
f"wireguard/client/delClient/{uuid}", method="POST")
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"Failed to remove peer from OPNsense: {e}")
|
||||
|
||||
# Unlink from server peers list
|
||||
if wg.get("server_uuid"):
|
||||
try:
|
||||
s = _opnsense_request(opn_cfg,
|
||||
f"wireguard/server/getServer/{wg['server_uuid']}")
|
||||
srv = s.get("server", {})
|
||||
existing = srv.get("peers", "")
|
||||
updated = ",".join(p for p in existing.split(",") if p and p != uuid)
|
||||
_opnsense_request(opn_cfg,
|
||||
f"wireguard/server/setServer/{wg['server_uuid']}", method="POST",
|
||||
body={"server": {**srv, "peers": updated}})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Apply
|
||||
try:
|
||||
_opnsense_request(opn_cfg, "wireguard/service/reconfigure", method="POST")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
wg["peers"] = [p for p in wg.get("peers", []) if p.get("uuid") != uuid]
|
||||
_save_opnsense_wg(wg)
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@app.get("/api/opnsense/wireguard/peer-config/{name}")
|
||||
def opnsense_wg_peer_config(name: str):
|
||||
"""Return the saved .conf text for a named peer (includes private key)."""
|
||||
wg = _load_opnsense_wg()
|
||||
peer = next((p for p in wg.get("peers", []) if p["name"] == name), None)
|
||||
if not peer:
|
||||
raise HTTPException(404, f"Peer '{name}' not found in local store")
|
||||
return {"name": name, "config": peer.get("config", "")}
|
||||
|
||||
Reference in New Issue
Block a user