diff --git a/ers5952-manager.jsx b/ers5952-manager.jsx index 6545996..e2790ff 100644 --- a/ers5952-manager.jsx +++ b/ers5952-manager.jsx @@ -5099,12 +5099,14 @@ function ServicesTab({ vlans, session, onNeedAuth, backendOk }) { const [deploying, setDeploying] = useState(false); const [deployResult, setDeployResult] = useState(null); const [actionLoading, setActionLoading] = useState(""); + const [caddyIpInput, setCaddyIpInput] = useState(""); const load = async () => { try { const [svc, st] = await Promise.all([API("/services"), API("/services/status")]); setServices(svc.services || []); setStatus(st); + if (st.caddy_ip && !caddyIpInput) setCaddyIpInput(st.caddy_ip); } catch(e) { console.error(e); } }; useEffect(() => { if (backendOk) load(); }, [backendOk]); @@ -5125,6 +5127,17 @@ function ServicesTab({ vlans, session, onNeedAuth, backendOk }) { await load(); }; + const saveCaddyIp = async () => { + if (!session) { onNeedAuth(); return; } + if (!caddyIpInput.trim()) return; + try { + await API("/services/config", { method:"POST", body:{ + token: session.token, config: { caddy_ip: caddyIpInput.trim() } + }}); + await load(); + } catch(e) { alert("Failed: " + e.message); } + }; + const enableNatReflection = async () => { if (!session) { onNeedAuth(); return; } setActionLoading("nat"); @@ -5141,7 +5154,7 @@ function ServicesTab({ vlans, session, onNeedAuth, backendOk }) { setActionLoading("pf"); try { const r = await API("/services/create-port-forward", { method:"POST", - body:{ token: session.token, mgmt_ip: status?.mgmt_ip } }); + body:{ token: session.token, caddy_ip: status?.caddy_ip } }); if (r.note) alert(r.note); await load(); } catch(e) { alert("Failed: " + e.message); } @@ -5153,7 +5166,7 @@ function ServicesTab({ vlans, session, onNeedAuth, backendOk }) { setDeploying(true); setDeployResult(null); try { const r = await API("/services/deploy", { method:"POST", - body:{ token: session.token, mgmt_ip: status?.mgmt_ip } }); + body:{ token: session.token, caddy_ip: status?.caddy_ip } }); setDeployResult(r); await load(); } catch(e) { setDeployResult({ success: false, errors: [e.message] }); } @@ -5203,10 +5216,32 @@ function ServicesTab({ vlans, session, onNeedAuth, backendOk }) { + {/* Services Host Config */} +
+
Services Host (Caddy Computer)
+
+
+ The computer running Caddy and your services (Plex, etc.) — on LAN, separate from the + VLAN 99 management computer. +
+
+
+ + setCaddyIpInput(e.target.value)} + placeholder="192.168.1.50"/> +
+ +
+
+
+ {/* Status Checklist */}
Setup Checklist
+ 0} label={`${services.length} service${services.length !== 1 ? "s" : ""} configured`} /> - {status?.mgmt_ip && ( -
- Management computer IP: {status.mgmt_ip} -
- )}
diff --git a/switch_backend.py b/switch_backend.py index a96571c..db3dc21 100644 --- a/switch_backend.py +++ b/switch_backend.py @@ -4515,8 +4515,12 @@ def push_policy(body: dict): # ══════════════════════════════════════════════════════════════════════ # # Architecture: -# Caddy runs on the LAN management computer. It is the reverse proxy for -# all services — only port 443 is forwarded from WAN, and Caddy routes +# Caddy runs on a SEPARATE LAN services computer, NOT the VLAN 99 +# management computer. Management box only runs this tool + SSH keys. +# Services box (LAN) runs Caddy, Plex, Docker, etc. +# +# WAN port forward 443 → services box LAN IP (Caddy). +# Caddy routes # by hostname (SNI) to the correct backend. Service ports (32400, 8123, # etc.) are NEVER exposed on WAN. # @@ -4530,8 +4534,25 @@ def push_policy(body: dict): # IoT = untrusted = same access as someone on the internet. SERVICES_FILE = _Path("/etc/switch-manager/service-proxies.json") +SERVICES_CONFIG_FILE = _Path("/etc/switch-manager/services-config.json") CADDYFILE_EXTRA = _Path("/etc/switch-manager/Caddyfile.services") +def _load_services_config() -> dict: + """Load services/Caddy host config: caddy_ip, etc. + This is the LAN services box, NOT the VLAN 99 management computer.""" + if SERVICES_CONFIG_FILE.exists(): + try: return _json.loads(SERVICES_CONFIG_FILE.read_text()) + except: pass + return {} + +def _save_services_config(cfg: dict): + SERVICES_CONFIG_FILE.write_text(_json.dumps(cfg, indent=2)) + SERVICES_CONFIG_FILE.chmod(0o600) + +def _get_caddy_ip() -> str: + """Return the Caddy/services box LAN IP.""" + return _load_services_config().get("caddy_ip", "") + def _load_services() -> list: if SERVICES_FILE.exists(): try: return _json.loads(SERVICES_FILE.read_text()) @@ -4622,13 +4643,32 @@ def _get_mgmt_ip() -> str: return "" +@app.get("/api/services/config") +def get_services_config(): + """Return services host configuration.""" + return _load_services_config() + +@app.post("/api/services/config") +def save_services_config_endpoint(body: dict): + """Save services host configuration (Caddy box LAN IP).""" + require_session(body.get("token", "")) + cfg = body.get("config", {}) + if not cfg.get("caddy_ip"): + raise HTTPException(400, "caddy_ip required — the LAN IP of your services/Caddy computer") + _save_services_config(cfg) + return {"success": True, "config": cfg} + @app.get("/api/services/status") def services_status(): - """Full status check: Caddy import, NAT reflection, port forward, services.""" + """Full status check: Caddy host, NAT reflection, port forward, services.""" services = _load_services() + svc_cfg = _load_services_config() cfg = _load_opnsense_cfg() + caddy_ip = svc_cfg.get("caddy_ip", "") result = { "services": services, + "caddy_ip": caddy_ip, + "caddy_configured": bool(caddy_ip), "mgmt_ip": _get_mgmt_ip(), "caddy_file_exists": CADDYFILE_EXTRA.exists(), "nat_reflection": None, @@ -4731,19 +4771,19 @@ def create_wan_port_forward(body: dict): if not cfg.get("key"): raise HTTPException(503, "OPNsense API not configured") - mgmt_ip = body.get("mgmt_ip", _get_mgmt_ip()) - if not mgmt_ip: - raise HTTPException(400, "Cannot determine management computer IP — provide mgmt_ip") + caddy_ip = body.get("caddy_ip", _get_caddy_ip()) + if not caddy_ip: + raise HTTPException(400, "Caddy/services box IP not configured — set it in the Services tab") tracked = _load_service_rules() if tracked.get("wan_443_uuid"): return {"success": True, "already_exists": True, "uuid": tracked["wan_443_uuid"], - "mgmt_ip": mgmt_ip} + "caddy_ip": caddy_ip} backup = _pre_change_backup(reason="pre-WAN-port-forward-443") try: - # Create NAT port forward rule: WAN TCP 443 → mgmt_ip:443 + # Create NAT port forward rule: WAN TCP 443 → caddy_ip:443 (services box on LAN) r = _opnsense_request(cfg, "firewall/source_nat/addRule", "POST", { "rule": { "enabled": "1", @@ -4751,8 +4791,8 @@ def create_wan_port_forward(body: dict): "protocol": "tcp", "source": {"any": "1"}, "destination": {"any": "1", "port": "443"}, - "target": {"address": mgmt_ip, "port": "443"}, - "descr": "switch-manager: WAN 443 → Caddy reverse proxy", + "target": {"address": caddy_ip, "port": "443"}, + "descr": f"switch-manager: WAN 443 → Caddy ({caddy_ip})", "nordr": "0", } }) @@ -4770,8 +4810,8 @@ def create_wan_port_forward(body: dict): "ipprotocol": "inet", "protocol": "tcp", "source": {"any": "1"}, - "destination": {"address": mgmt_ip, "port": "443"}, - "descr": "switch-manager: allow WAN → Caddy:443 (pair with NAT rule)", + "destination": {"address": caddy_ip, "port": "443"}, + "descr": f"switch-manager: allow WAN → Caddy ({caddy_ip}:443)", } }) uuid = r.get("uuid", "") @@ -4780,10 +4820,10 @@ def create_wan_port_forward(body: dict): _opnsense_request(cfg, "firewall/filter/apply", "POST") tracked["wan_443_uuid"] = uuid - tracked["mgmt_ip"] = mgmt_ip + tracked["caddy_ip"] = caddy_ip _save_service_rules(tracked) - return {"success": True, "uuid": uuid, "mgmt_ip": mgmt_ip, "backup": backup, + return {"success": True, "uuid": uuid, "caddy_ip": caddy_ip, "backup": backup, "note": "If this is the first time, also verify in OPNsense UI: " "Firewall > NAT > Port Forward that the rule looks correct. " "The OPNsense NAT API varies between versions."} @@ -4816,7 +4856,7 @@ def deploy_services(body: dict): backup = _pre_change_backup(reason="pre-service-deploy") - mgmt_ip = body.get("mgmt_ip", _get_mgmt_ip()) + caddy_ip = body.get("caddy_ip", _get_caddy_ip()) or _get_mgmt_ip() # ── Step 1: Write Caddyfile.services ───────────────────────────── caddy_content = _generate_caddyfile_services(services) @@ -4891,11 +4931,11 @@ def deploy_services(body: dict): # ── Step 4: WAN port forward ───────────────────────────────────── tracked = _load_service_rules() if tracked.get("wan_443_uuid"): - steps_done.append(f"WAN port forward 443 → {tracked.get('mgmt_ip', mgmt_ip)}:443 (tracked)") + steps_done.append(f"WAN port forward 443 → {tracked.get('caddy_ip', caddy_ip)}:443 (tracked)") else: pending_steps.append( f"Create WAN port forward: OPNsense > Firewall > NAT > Port Forward — " - f"WAN TCP 443 → {mgmt_ip}:443 (Caddy). " + f"WAN TCP 443 → {caddy_ip}:443 (services box). " f"Or use the 'Create Port Forward' button above.") return { @@ -4905,7 +4945,7 @@ def deploy_services(body: dict): "errors": errors, "backup": backup, "caddy_content": caddy_content, - "mgmt_ip": mgmt_ip, + "caddy_ip": caddy_ip, "nat_reflection_enabled": nat_status, }