ops: sync dispatcharr tooling and pause noisy workflows
Some checks failed
secret-guardrails / artifact-secret-scan (push) Has been cancelled
secret-guardrails / gitleaks (push) Has been cancelled

This commit is contained in:
Fizzlepoof
2026-05-24 02:59:51 +00:00
parent a4c5ee7285
commit 5d88f6cf93
19 changed files with 1070 additions and 28 deletions

View File

@@ -28,6 +28,13 @@ UNIFI_PASSWORD=***
UNIFI_SITE=default UNIFI_SITE=default
UNIFI_VERIFY_TLS=false UNIFI_VERIFY_TLS=false
# Pangolin integration API
PANGOLIN_API_URL=https://api.paccoco.com/v1
PANGOLIN_API_KEY=CHANGE_ME
PANGOLIN_ORG_ID=paccoco
PANGOLIN_SITE_ID=4
PANGOLIN_DOMAIN_ID=domain1
# Backup automation scaffolding # Backup automation scaffolding
SERENITY_BACKUP_HOST=root@10.5.30.5 SERENITY_BACKUP_HOST=root@10.5.30.5
SERENITY_BACKUP_ROOT=/mnt/user/backups/plausible-deniability SERENITY_BACKUP_ROOT=/mnt/user/backups/plausible-deniability

View File

@@ -0,0 +1,251 @@
#!/usr/bin/env python3
"""Create or update the Pangolin resource/target for Dispatcharr.
Expected env vars:
- PANGOLIN_API_URL e.g. https://api.paccoco.com/v1
- PANGOLIN_API_KEY bearer token for Pangolin integration API
- PANGOLIN_ORG_ID e.g. paccoco
Optional env defaults:
- PANGOLIN_SITE_ID default 4 (Plausible Deniability)
- PANGOLIN_DOMAIN_ID default domain1
Default desired resource:
- host/domain: tv.paccoco.com
- site: Plausible Deniability (siteId 4)
- target: dispatcharr:9191
- Pangolin auth: disabled (sso=false)
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from typing import Any
from urllib import error, parse, request
DEFAULT_API_URL = "https://api.paccoco.com/v1"
DEFAULT_ORG_ID = "paccoco"
DEFAULT_DOMAIN_ID = "domain1"
DEFAULT_SITE_ID = 4
DEFAULT_NAME = "dispatcharr"
DEFAULT_SUBDOMAIN = "tv"
DEFAULT_TARGET_IP = "dispatcharr"
DEFAULT_TARGET_PORT = 9191
USER_AGENT = "doris-pangolin-dispatcharr/1.0"
def env(name: str, default: str | None = None) -> str | None:
value = os.environ.get(name)
if value is None or value == "":
return default
return value
def api_request(base_url: str, api_key: str, method: str, path: str, body: dict[str, Any] | None = None) -> tuple[int, Any]:
url = base_url.rstrip("/") + path
headers = {
"Authorization": f"Bearer {api_key}",
"Accept": "application/json",
"User-Agent": USER_AGENT,
}
data = None
if body is not None:
data = json.dumps(body).encode("utf-8")
headers["Content-Type"] = "application/json"
req = request.Request(url, data=data, headers=headers, method=method.upper())
try:
with request.urlopen(req, timeout=30) as resp:
raw = resp.read().decode("utf-8", "replace")
return resp.status, json.loads(raw) if raw else None
except error.HTTPError as exc:
raw = exc.read().decode("utf-8", "replace")
try:
payload = json.loads(raw)
except Exception:
payload = raw
return exc.code, payload
def fail(msg: str, payload: Any = None) -> int:
print(msg, file=sys.stderr)
if payload is not None:
if isinstance(payload, (dict, list)):
print(json.dumps(payload, indent=2, sort_keys=True), file=sys.stderr)
else:
print(payload, file=sys.stderr)
return 1
def find_resource(resources: list[dict[str, Any]], *, name: str, full_domain: str) -> dict[str, Any] | None:
for resource in resources:
if resource.get("fullDomain") == full_domain:
return resource
for resource in resources:
if resource.get("name") == name:
return resource
return None
def main() -> int:
parser = argparse.ArgumentParser(description="Upsert Pangolin Dispatcharr resource")
parser.add_argument("--api-url", default=env("PANGOLIN_API_URL", DEFAULT_API_URL))
parser.add_argument("--api-key", default=env("PANGOLIN_API_KEY"))
parser.add_argument("--org-id", default=env("PANGOLIN_ORG_ID", DEFAULT_ORG_ID))
site_id_default = env("PANGOLIN_SITE_ID", str(DEFAULT_SITE_ID)) or str(DEFAULT_SITE_ID)
parser.add_argument("--site-id", type=int, default=int(site_id_default))
parser.add_argument("--domain-id", default=env("PANGOLIN_DOMAIN_ID", DEFAULT_DOMAIN_ID))
parser.add_argument("--name", default=DEFAULT_NAME)
parser.add_argument("--subdomain", default=DEFAULT_SUBDOMAIN)
parser.add_argument("--target-ip", default=DEFAULT_TARGET_IP)
parser.add_argument("--target-port", type=int, default=DEFAULT_TARGET_PORT)
parser.add_argument("--hc-path", default="/")
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()
if not args.api_key:
return fail("Missing PANGOLIN_API_KEY / --api-key")
full_domain = f"{args.subdomain}.paccoco.com"
status, resources_resp = api_request(
args.api_url,
args.api_key,
"GET",
f"/org/{parse.quote(args.org_id)}/resources?pageSize=200",
)
if status != 200:
return fail("Failed to list Pangolin resources", resources_resp)
resources = resources_resp.get("data", {}).get("resources", [])
resource = find_resource(resources, name=args.name, full_domain=full_domain)
create_body = {
"name": args.name,
"subdomain": args.subdomain,
"http": True,
"protocol": "tcp",
"domainId": args.domain_id,
}
if resource is None:
if args.dry_run:
print(json.dumps({"action": "create_resource", "body": create_body}, indent=2))
resource_id = None
else:
status, create_resp = api_request(
args.api_url,
args.api_key,
"PUT",
f"/org/{parse.quote(args.org_id)}/resource",
create_body,
)
if status not in (200, 201):
return fail("Failed to create Pangolin resource", create_resp)
resource_id = create_resp.get("data", {}).get("resourceId") or create_resp.get("data", {}).get("resource", {}).get("resourceId")
if not resource_id:
return fail("Create resource succeeded but resourceId was missing", create_resp)
status, resource_resp = api_request(args.api_url, args.api_key, "GET", f"/resource/{resource_id}")
if status != 200:
return fail("Failed to read back created resource", resource_resp)
resource = resource_resp.get("data", {}).get("resource") or resource_resp.get("data") or {}
else:
resource_id = resource["resourceId"]
resource_update = {
"name": args.name,
"niceId": resource.get("niceId") if resource else None,
"subdomain": args.subdomain,
"ssl": True,
"sso": False,
"blockAccess": False,
"emailWhitelistEnabled": False,
"applyRules": False,
"domainId": args.domain_id,
"enabled": True,
"stickySession": False,
"tlsServerName": None,
"setHostHeader": None,
"skipToIdpId": None,
"headers": None,
"maintenanceModeEnabled": False,
"maintenanceModeType": "forced",
"maintenanceTitle": None,
"maintenanceMessage": None,
"maintenanceEstimatedTime": None,
"postAuthPath": None,
}
target_body = {
"siteId": args.site_id,
"ip": args.target_ip,
"port": args.target_port,
"enabled": True,
"hcEnabled": True,
"hcPath": args.hc_path,
"hcScheme": "http",
"hcMode": "http",
"hcHostname": args.target_ip,
"hcPort": args.target_port,
"hcInterval": 30,
"hcUnhealthyInterval": 30,
"hcTimeout": 10,
"hcHeaders": None,
"hcFollowRedirects": True,
"hcMethod": "GET",
"hcStatus": 200,
"hcTlsServerName": None,
"hcHealthyThreshold": 1,
"hcUnhealthyThreshold": 3,
"path": None,
"pathMatchType": None,
"rewritePath": None,
"rewritePathType": None,
}
existing_targets = resource.get("targets", []) if resource else []
matching_target = None
for target in existing_targets:
if target.get("siteId") == args.site_id:
matching_target = target
break
if args.dry_run:
print(json.dumps({
"resource_id": resource_id,
"full_domain": full_domain,
"resource_update": resource_update,
"target_action": "update" if matching_target else "create",
"target_id": matching_target.get("targetId") if matching_target else None,
"target_body": target_body,
}, indent=2))
return 0
if resource_id is not None:
status, update_resp = api_request(args.api_url, args.api_key, "POST", f"/resource/{resource_id}", resource_update)
if status not in (200, 201):
return fail("Failed to update Pangolin resource", update_resp)
if matching_target:
target_id = matching_target["targetId"]
status, target_resp = api_request(args.api_url, args.api_key, "POST", f"/target/{target_id}", target_body)
if status not in (200, 201):
return fail("Failed to update Pangolin target", target_resp)
else:
status, target_resp = api_request(args.api_url, args.api_key, "PUT", f"/resource/{resource_id}/target", target_body)
if status not in (200, 201):
return fail("Failed to create Pangolin target", target_resp)
status, final_resp = api_request(args.api_url, args.api_key, "GET", f"/resource/{resource_id}")
if status != 200:
return fail("Failed to read back final Pangolin resource", final_resp)
payload = final_resp.get("data", {}).get("resource") or final_resp.get("data") or final_resp
print(json.dumps(payload, indent=2))
return 0
return fail("Unexpected state: resource_id is missing")
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -15,9 +15,11 @@ sudo -n docker compose --env-file .env up -d
Access: Access:
- Direct LAN URL: `http://10.5.30.6:9191` - Direct LAN URL: `http://10.5.30.6:9191`
- Intended public URL: `https://tv.paccoco.com` via Pangolin with Pangolin auth disabled
Notes: Notes:
- Uses Dispatcharr's recommended all-in-one container (`DISPATCHARR_ENV=aio`). - Uses Dispatcharr's recommended all-in-one container (`DISPATCHARR_ENV=aio`).
- Data is persisted at `/mnt/docker-ssd/docker/appdata/dispatcharr`. - Data is persisted at `/mnt/docker-ssd/docker/appdata/dispatcharr`.
- First-run setup creates the initial local Dispatcharr user in the web UI. - First-run setup creates the initial local Dispatcharr user in the web UI.
- IPTV credentials and EPG sources are added later through the UI. - IPTV credentials and EPG sources are added later through the UI.
- Pangolin automation helper: `automation/bin/pangolin_upsert_dispatcharr.py` (requires Pangolin integration API key).

View File

@@ -0,0 +1,358 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Paccoco VLAN Policy Lanes — 2026-05-23</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #020617;
--panel: rgba(15, 23, 42, 0.84);
--panel-soft: rgba(15, 23, 42, 0.72);
--border: #1e293b;
--text: #e2e8f0;
--muted: #94a3b8;
--cyan: #22d3ee;
--emerald: #34d399;
--amber: #fbbf24;
--rose: #fb7185;
--violet: #a78bfa;
--slate: #94a3b8;
}
body {
font-family: 'JetBrains Mono', monospace;
background: var(--bg);
color: var(--text);
min-height: 100vh;
padding: 24px;
background-image:
linear-gradient(rgba(30,41,59,0.45) 1px, transparent 1px),
linear-gradient(90deg, rgba(30,41,59,0.45) 1px, transparent 1px);
background-size: 40px 40px;
}
.container { max-width: 1560px; margin: 0 auto; }
.header {
margin-bottom: 18px;
padding: 18px 20px;
border: 1px solid var(--border);
border-radius: 18px;
background: linear-gradient(180deg, rgba(15,23,42,0.92), rgba(15,23,42,0.70));
box-shadow: 0 24px 60px rgba(0,0,0,0.28);
}
.header-top {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
flex-wrap: wrap;
}
.pulse-dot {
width: 12px;
height: 12px;
border-radius: 999px;
background: var(--emerald);
box-shadow: 0 0 18px rgba(52, 211, 153, 0.5);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(0.9); opacity: 0.58; }
}
h1 {
font-size: 28px;
color: #f8fafc;
line-height: 1.15;
}
.subtitle {
color: var(--muted);
font-size: 13px;
line-height: 1.6;
max-width: 1200px;
}
.badge-row {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 12px;
}
.badge {
border: 1px solid var(--border);
border-radius: 999px;
padding: 6px 10px;
font-size: 11px;
color: var(--muted);
background: rgba(2,6,23,0.7);
}
.shell {
background: var(--panel-soft);
border: 1px solid var(--border);
border-radius: 20px;
padding: 18px;
overflow-x: auto;
box-shadow: 0 24px 60px rgba(0,0,0,0.28);
}
svg {
width: 100%;
min-width: 1460px;
display: block;
}
.cards {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
margin-top: 18px;
}
.card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 16px;
padding: 16px 18px;
}
.card h3 {
font-size: 13px;
color: #f8fafc;
margin-bottom: 10px;
}
.card ul {
list-style: none;
color: var(--muted);
font-size: 12px;
line-height: 1.6;
}
.card li { margin-bottom: 6px; }
.tip, .footer {
text-align: center;
color: #64748b;
font-size: 11px;
margin-top: 12px;
}
@media (max-width: 1100px) {
.cards { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="header-top">
<div class="pulse-dot"></div>
<h1>Paccoco VLAN Policy Lanes</h1>
</div>
<div class="subtitle">
Simplified operator view focused on VLAN identity and firewall intent as of 2026-05-23. This intentionally strips most host-by-host detail so the important story is obvious: which lanes are internal, which are untrusted, where DNS now lands, and which paths are allowed, blocked, or preserved only as narrow exceptions.
</div>
<div class="badge-row">
<div class="badge">Internal zone: Management + Trusted + Servers</div>
<div class="badge">Untrusted zone: IoT + Camera + Old IoT</div>
<div class="badge">Hotspot: Guest internet-only</div>
<div class="badge">Restricted-lane DNS target: 10.5.30.53</div>
</div>
</div>
<div class="shell">
<svg viewBox="0 0 1500 980" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="VLAN policy lanes diagram">
<defs>
<marker id="arrow-cyan" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#22d3ee" />
</marker>
<marker id="arrow-emerald" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#34d399" />
</marker>
<marker id="arrow-amber" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#fbbf24" />
</marker>
<marker id="arrow-rose" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#fb7185" />
</marker>
</defs>
<rect width="1500" height="980" fill="#020617" rx="18" />
<rect x="30" y="24" width="1440" height="120" rx="18" fill="rgba(8,51,68,0.08)" stroke="#22d3ee" stroke-width="1.2" stroke-dasharray="8,4" />
<text x="54" y="50" fill="#22d3ee" font-size="12" font-weight="700">Control plane and zone definitions</text>
<rect x="54" y="68" width="280" height="52" rx="10" fill="#0f172a" />
<rect x="54" y="68" width="280" height="52" rx="10" fill="rgba(136,19,55,0.38)" stroke="#fb7185" stroke-width="1.4" />
<text x="194" y="89" fill="#ffffff" font-size="13" font-weight="700" text-anchor="middle">Gateway / UDM Pro</text>
<text x="194" y="108" fill="#94a3b8" font-size="10" text-anchor="middle">Mgmt IP 10.5.0.1 • control surface lives only in Management</text>
<rect x="374" y="68" width="320" height="52" rx="10" fill="#0f172a" />
<rect x="374" y="68" width="320" height="52" rx="10" fill="rgba(6,78,59,0.38)" stroke="#34d399" stroke-width="1.4" />
<text x="534" y="89" fill="#ffffff" font-size="13" font-weight="700" text-anchor="middle">Internal Zone</text>
<text x="534" y="108" fill="#94a3b8" font-size="10" text-anchor="middle">Management + Trusted + Servers can initiate where needed</text>
<rect x="734" y="68" width="320" height="52" rx="10" fill="#0f172a" />
<rect x="734" y="68" width="320" height="52" rx="10" fill="rgba(120,53,15,0.28)" stroke="#fbbf24" stroke-width="1.4" />
<text x="894" y="89" fill="#ffffff" font-size="13" font-weight="700" text-anchor="middle">Untrusted Zone</text>
<text x="894" y="108" fill="#94a3b8" font-size="10" text-anchor="middle">IoT + Camera + Old IoT get DNS help but no broad gateway reach</text>
<rect x="1094" y="68" width="352" height="52" rx="10" fill="#0f172a" />
<rect x="1094" y="68" width="352" height="52" rx="10" fill="rgba(76,29,149,0.36)" stroke="#a78bfa" stroke-width="1.4" />
<text x="1270" y="89" fill="#ffffff" font-size="13" font-weight="700" text-anchor="middle">Shared DNS Anchor</text>
<text x="1270" y="108" fill="#94a3b8" font-size="10" text-anchor="middle">Pi-hole HA VIP 10.5.30.53 advertised to restricted lanes by DHCP</text>
<rect x="30" y="168" width="670" height="650" rx="18" fill="rgba(6,78,59,0.10)" stroke="#34d399" stroke-width="1.2" stroke-dasharray="8,4" />
<text x="54" y="194" fill="#34d399" font-size="12" font-weight="700">Internal lanes</text>
<rect x="730" y="168" width="740" height="650" rx="18" fill="rgba(120,53,15,0.08)" stroke="#fbbf24" stroke-width="1.2" stroke-dasharray="8,4" />
<text x="754" y="194" fill="#fbbf24" font-size="12" font-weight="700">Restricted / edge lanes</text>
<line x1="700" y1="492" x2="730" y2="492" stroke="#34d399" stroke-width="3" marker-end="url(#arrow-emerald)" />
<text x="715" y="480" fill="#34d399" font-size="9" text-anchor="middle">policy edge</text>
<!-- Internal column -->
<rect x="60" y="224" width="190" height="235" rx="16" fill="rgba(136,19,55,0.16)" stroke="#fb7185" stroke-width="1.4" />
<text x="155" y="254" fill="#ffffff" font-size="15" font-weight="700" text-anchor="middle">Management</text>
<text x="155" y="274" fill="#94a3b8" font-size="10" text-anchor="middle">10.5.0.0/24</text>
<text x="155" y="292" fill="#94a3b8" font-size="10" text-anchor="middle">control plane</text>
<text x="155" y="334" fill="#fb7185" font-size="11" font-weight="700" text-anchor="middle">Lives here</text>
<text x="155" y="354" fill="#94a3b8" font-size="9" text-anchor="middle">UDM Pro 10.5.0.1</text>
<text x="155" y="370" fill="#94a3b8" font-size="9" text-anchor="middle">switches, AP management</text>
<text x="155" y="408" fill="#34d399" font-size="10" text-anchor="middle">Operator-only admin surface</text>
<rect x="275" y="224" width="200" height="235" rx="16" fill="rgba(8,51,68,0.16)" stroke="#22d3ee" stroke-width="1.4" />
<text x="375" y="254" fill="#ffffff" font-size="15" font-weight="700" text-anchor="middle">Trusted</text>
<text x="375" y="274" fill="#94a3b8" font-size="10" text-anchor="middle">10.5.1.0/24 • VLAN 51</text>
<text x="375" y="292" fill="#94a3b8" font-size="10" text-anchor="middle">people + daily drivers</text>
<text x="375" y="334" fill="#ffffff" font-size="11" font-weight="700" text-anchor="middle">Purpose</text>
<text x="375" y="354" fill="#94a3b8" font-size="9" text-anchor="middle">phones, laptops, tablets</text>
<text x="375" y="370" fill="#94a3b8" font-size="9" text-anchor="middle">normal admin origin points</text>
<text x="375" y="408" fill="#34d399" font-size="10" text-anchor="middle">Can initiate toward Servers and Untrusted</text>
<rect x="500" y="224" width="170" height="235" rx="16" fill="rgba(6,78,59,0.16)" stroke="#34d399" stroke-width="1.4" />
<text x="585" y="254" fill="#ffffff" font-size="15" font-weight="700" text-anchor="middle">Servers</text>
<text x="585" y="274" fill="#94a3b8" font-size="10" text-anchor="middle">10.5.30.0/24 • VLAN 30</text>
<text x="585" y="292" fill="#94a3b8" font-size="10" text-anchor="middle">core services</text>
<text x="585" y="334" fill="#ffffff" font-size="11" font-weight="700" text-anchor="middle">Anchor services</text>
<text x="585" y="354" fill="#94a3b8" font-size="9" text-anchor="middle">PD .6 • Serenity .5</text>
<text x="585" y="370" fill="#94a3b8" font-size="9" text-anchor="middle">Nomad .7 • Rocinante .112</text>
<text x="585" y="386" fill="#a78bfa" font-size="9" text-anchor="middle">Pi-hole VIP 10.5.30.53</text>
<text x="585" y="408" fill="#34d399" font-size="10" text-anchor="middle">Service destination for the rest of the network</text>
<!-- Internal policy notes -->
<rect x="60" y="500" width="610" height="276" rx="16" fill="rgba(15,23,42,0.84)" stroke="#1e293b" stroke-width="1.2" />
<text x="86" y="530" fill="#f8fafc" font-size="13" font-weight="700">Internal policy summary</text>
<line x1="210" y1="610" x2="500" y2="610" stroke="#34d399" stroke-width="2.4" marker-end="url(#arrow-emerald)" />
<text x="355" y="596" fill="#34d399" font-size="10" text-anchor="middle">Trusted/admin workflows can reach Servers</text>
<line x1="400" y1="646" x2="740" y2="646" stroke="#34d399" stroke-width="2.4" marker-end="url(#arrow-emerald)" />
<text x="566" y="632" fill="#34d399" font-size="10" text-anchor="middle">Internal can initiate into restricted lanes when needed</text>
<line x1="585" y1="680" x2="1130" y2="680" stroke="#a78bfa" stroke-width="2.2" marker-end="url(#arrow-emerald)" />
<text x="855" y="666" fill="#a78bfa" font-size="10" text-anchor="middle">Servers host shared DNS target used by restricted lanes</text>
<text x="86" y="718" fill="#94a3b8" font-size="10">• Management remains the only rightful home of gateway administration.</text>
<text x="86" y="740" fill="#94a3b8" font-size="10">• Trusted is where John/admin clients live and where most deliberate control starts.</text>
<text x="86" y="762" fill="#94a3b8" font-size="10">• Servers is the service center, not a client junk drawer.</text>
<!-- Restricted columns -->
<rect x="760" y="224" width="190" height="250" rx="16" fill="rgba(120,53,15,0.16)" stroke="#fbbf24" stroke-width="1.4" />
<text x="855" y="254" fill="#ffffff" font-size="15" font-weight="700" text-anchor="middle">IoT</text>
<text x="855" y="274" fill="#94a3b8" font-size="10" text-anchor="middle">10.5.10.0/24 • VLAN 510</text>
<text x="855" y="292" fill="#94a3b8" font-size="10" text-anchor="middle">appliances + smart-home gear</text>
<text x="855" y="334" fill="#ffffff" font-size="11" font-weight="700" text-anchor="middle">Kept</text>
<text x="855" y="354" fill="#a78bfa" font-size="9" text-anchor="middle">DNS -> 10.5.30.53</text>
<text x="855" y="370" fill="#94a3b8" font-size="9" text-anchor="middle">DHCP preserved</text>
<text x="855" y="386" fill="#94a3b8" font-size="9" text-anchor="middle">mDNS preserved where needed</text>
<text x="855" y="426" fill="#fb7185" font-size="10" text-anchor="middle">No broad gateway reach</text>
<rect x="980" y="224" width="190" height="250" rx="16" fill="rgba(120,53,15,0.16)" stroke="#fbbf24" stroke-width="1.4" />
<text x="1075" y="254" fill="#ffffff" font-size="15" font-weight="700" text-anchor="middle">Camera</text>
<text x="1075" y="274" fill="#94a3b8" font-size="10" text-anchor="middle">10.5.20.0/24 • VLAN 520</text>
<text x="1075" y="292" fill="#94a3b8" font-size="10" text-anchor="middle">Protect chimes + camera-adjacent gear</text>
<text x="1075" y="334" fill="#ffffff" font-size="11" font-weight="700" text-anchor="middle">Kept</text>
<text x="1075" y="354" fill="#a78bfa" font-size="9" text-anchor="middle">DNS -> 10.5.30.53</text>
<text x="1075" y="370" fill="#94a3b8" font-size="9" text-anchor="middle">DHCP preserved</text>
<text x="1075" y="386" fill="#94a3b8" font-size="9" text-anchor="middle">same shield posture as IoT</text>
<text x="1075" y="426" fill="#fb7185" font-size="10" text-anchor="middle">No broad gateway reach</text>
<rect x="1200" y="224" width="230" height="250" rx="16" fill="rgba(136,19,55,0.16)" stroke="#fb7185" stroke-width="1.4" />
<text x="1315" y="254" fill="#ffffff" font-size="15" font-weight="700" text-anchor="middle">Old IoT / Legacy</text>
<text x="1315" y="274" fill="#94a3b8" font-size="10" text-anchor="middle">192.168.1.0/24 • VLAN 2</text>
<text x="1315" y="292" fill="#94a3b8" font-size="10" text-anchor="middle">shrinking-only containment lane</text>
<text x="1315" y="334" fill="#ffffff" font-size="11" font-weight="700" text-anchor="middle">Still tolerated</text>
<text x="1315" y="354" fill="#a78bfa" font-size="9" text-anchor="middle">DNS -> 10.5.30.53</text>
<text x="1315" y="370" fill="#94a3b8" font-size="9" text-anchor="middle">same shield posture as other untrusted lanes</text>
<text x="1315" y="386" fill="#fbbf24" font-size="9" text-anchor="middle">no new joins if avoidable</text>
<text x="1315" y="426" fill="#fb7185" font-size="10" text-anchor="middle">Exception-only future</text>
<rect x="870" y="522" width="450" height="100" rx="16" fill="rgba(30,41,59,0.26)" stroke="#94a3b8" stroke-width="1.3" />
<text x="1095" y="552" fill="#ffffff" font-size="15" font-weight="700" text-anchor="middle">Guest / Hotspot</text>
<text x="1095" y="572" fill="#94a3b8" font-size="10" text-anchor="middle">10.5.90.0/24 • VLAN 590</text>
<text x="1095" y="592" fill="#94a3b8" font-size="10" text-anchor="middle">internet-only lane • kept separate from the internal story</text>
<!-- Policy flows -->
<rect x="760" y="656" width="670" height="120" rx="16" fill="rgba(15,23,42,0.84)" stroke="#1e293b" stroke-width="1.2" />
<text x="786" y="686" fill="#f8fafc" font-size="13" font-weight="700">Firewall intent</text>
<line x1="815" y1="720" x2="1035" y2="720" stroke="#a78bfa" stroke-width="2.2" marker-end="url(#arrow-emerald)" />
<text x="925" y="708" fill="#a78bfa" font-size="10" text-anchor="middle">Restricted lanes get DNS service path</text>
<line x1="1060" y1="742" x2="240" y2="120" stroke="#fb7185" stroke-width="2" stroke-dasharray="6,5" marker-end="url(#arrow-rose)" />
<text x="802" y="550" fill="#fb7185" font-size="10" text-anchor="middle">Blocked: Untrusted -> Gateway admin / broad gateway access</text>
<line x1="1310" y1="470" x2="1180" y2="620" stroke="#fbbf24" stroke-width="2" stroke-dasharray="4,4" marker-end="url(#arrow-amber)" />
<text x="1250" y="560" fill="#fbbf24" font-size="10" text-anchor="middle">Only proven exceptions survive</text>
<!-- Legend -->
<rect x="30" y="850" width="1440" height="100" rx="18" fill="rgba(15,23,42,0.88)" stroke="#1e293b" stroke-width="1.2" />
<text x="56" y="880" fill="#ffffff" font-size="12" font-weight="700">Legend</text>
<rect x="56" y="898" width="16" height="10" rx="2" fill="rgba(8,51,68,0.40)" stroke="#22d3ee" stroke-width="1" />
<text x="82" y="907" fill="#94a3b8" font-size="9">trusted / operator lane</text>
<rect x="242" y="898" width="16" height="10" rx="2" fill="rgba(6,78,59,0.40)" stroke="#34d399" stroke-width="1" />
<text x="268" y="907" fill="#94a3b8" font-size="9">servers / service core</text>
<rect x="432" y="898" width="16" height="10" rx="2" fill="rgba(120,53,15,0.30)" stroke="#fbbf24" stroke-width="1" />
<text x="458" y="907" fill="#94a3b8" font-size="9">restricted edge lane</text>
<rect x="632" y="898" width="16" height="10" rx="2" fill="rgba(136,19,55,0.40)" stroke="#fb7185" stroke-width="1" />
<text x="658" y="907" fill="#94a3b8" font-size="9">management / sensitive control surface</text>
<line x1="930" y1="903" x2="972" y2="903" stroke="#34d399" stroke-width="2.2" marker-end="url(#arrow-emerald)" />
<text x="986" y="907" fill="#94a3b8" font-size="9">allowed path</text>
<line x1="1102" y1="903" x2="1144" y2="903" stroke="#fb7185" stroke-width="2.2" stroke-dasharray="6,5" marker-end="url(#arrow-rose)" />
<text x="1158" y="907" fill="#94a3b8" font-size="9">blocked path</text>
<line x1="1274" y1="903" x2="1316" y2="903" stroke="#fbbf24" stroke-width="2.2" stroke-dasharray="4,4" marker-end="url(#arrow-amber)" />
<text x="1330" y="907" fill="#94a3b8" font-size="9">narrow preserved exception</text>
</svg>
</div>
<div class="cards">
<div class="card">
<h3>Read this diagram as policy, not inventory</h3>
<ul>
<li>• Left side is where control and deliberate administration belong.</li>
<li>• Right side is where devices live when they need containment more than trust.</li>
<li>• The important service export from Servers is shared DNS at 10.5.30.53.</li>
</ul>
</div>
<div class="card">
<h3>The two rules that matter most</h3>
<ul>
<li>• Internal can initiate outward when needed.</li>
<li>• Untrusted does not get broad gateway access back in, especially not UDM admin TCP.</li>
<li>• Any exception has to earn its keep with a real failing device.</li>
</ul>
</div>
<div class="card">
<h3>What still deserves live validation</h3>
<ul>
<li>• Renew a DHCP lease on one client from each restricted lane.</li>
<li>• Confirm that client resolves through 10.5.30.53.</li>
<li>• Confirm gateway UI/admin ports still fail from those lanes.</li>
</ul>
</div>
</div>
<div class="tip">Open locally with: xdg-open /home/fizzlepoof/repos/truenas-stacks/docs/architecture/network-policy-lanes-2026-05-23.html</div>
<div class="footer">Focused simplification derived from the broader network topology document created on 2026-05-23.</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,424 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Paccoco Homelab Network Topology — 2026-05-23</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'JetBrains Mono', monospace;
background: #020617;
color: #e2e8f0;
min-height: 100vh;
padding: 24px;
}
.container {
max-width: 1480px;
margin: 0 auto;
}
.header {
margin-bottom: 20px;
}
.header-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.pulse-dot {
width: 12px;
height: 12px;
border-radius: 999px;
background: #22d3ee;
animation: pulse 2s infinite;
box-shadow: 0 0 18px rgba(34, 211, 238, 0.55);
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.55; transform: scale(0.92); }
}
h1 {
font-size: 26px;
font-weight: 700;
color: #f8fafc;
}
.subtitle {
color: #94a3b8;
font-size: 13px;
margin-left: 24px;
line-height: 1.5;
}
.diagram-shell {
background: rgba(15, 23, 42, 0.72);
border: 1px solid #1e293b;
border-radius: 18px;
padding: 18px;
overflow-x: auto;
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.28);
}
svg {
width: 100%;
min-width: 1380px;
display: block;
}
.cards {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
margin-top: 18px;
}
.card {
background: rgba(15, 23, 42, 0.72);
border: 1px solid #1e293b;
border-radius: 14px;
padding: 16px 18px;
}
.card-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.card-dot {
width: 8px;
height: 8px;
border-radius: 999px;
}
.cyan { background: #22d3ee; }
.emerald { background: #34d399; }
.rose { background: #fb7185; }
.amber { background: #fbbf24; }
.violet { background: #a78bfa; }
.slate { background: #94a3b8; }
.card h3 {
font-size: 13px;
color: #f8fafc;
}
.card ul {
list-style: none;
color: #94a3b8;
font-size: 12px;
line-height: 1.55;
}
.card li { margin-bottom: 6px; }
.footer {
text-align: center;
color: #64748b;
font-size: 11px;
margin-top: 14px;
}
.tip {
margin-top: 10px;
color: #94a3b8;
font-size: 12px;
}
@media (max-width: 1100px) {
.cards { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="header-row">
<div class="pulse-dot"></div>
<h1>Paccoco Homelab Network Topology</h1>
</div>
<div class="subtitle">
Current-state operator map as of 2026-05-23. Reflects live UniFi segmentation, the new Servers VLAN,
restricted-lane DNS cutover to 10.5.30.53, and the broader Untrusted → Gateway shield with DHCP/mDNS preservation.
</div>
</div>
<div class="diagram-shell">
<svg viewBox="0 0 1500 1080" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Homelab network topology diagram">
<defs>
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="#1e293b" stroke-width="0.6" />
</pattern>
<marker id="arrow-cyan" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#22d3ee" />
</marker>
<marker id="arrow-emerald" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#34d399" />
</marker>
<marker id="arrow-amber" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#fbbf24" />
</marker>
<marker id="arrow-rose" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#fb7185" />
</marker>
</defs>
<rect width="1500" height="1080" fill="#020617" />
<rect width="1500" height="1080" fill="url(#grid)" />
<!-- Top boundaries -->
<rect x="30" y="24" width="1440" height="130" rx="16" fill="rgba(120,53,15,0.10)" stroke="#fbbf24" stroke-width="1.2" stroke-dasharray="8,4" />
<text x="50" y="48" fill="#fbbf24" font-size="12" font-weight="700">External access + WAN edge</text>
<rect x="30" y="176" width="1440" height="214" rx="16" fill="rgba(8,51,68,0.08)" stroke="#22d3ee" stroke-width="1.2" stroke-dasharray="8,4" />
<text x="50" y="200" fill="#22d3ee" font-size="12" font-weight="700">UniFi control plane + switching fabric</text>
<rect x="30" y="420" width="1440" height="520" rx="16" fill="rgba(30,41,59,0.14)" stroke="#64748b" stroke-width="1.2" stroke-dasharray="8,4" />
<text x="50" y="444" fill="#cbd5e1" font-size="12" font-weight="700">Segmentation lanes</text>
<!-- External row -->
<rect x="70" y="62" width="190" height="62" rx="8" fill="#0f172a" />
<rect x="70" y="62" width="190" height="62" rx="8" fill="rgba(30,41,59,0.55)" stroke="#94a3b8" stroke-width="1.5" />
<text x="165" y="87" fill="#ffffff" font-size="13" font-weight="700" text-anchor="middle">Internet / Clients</text>
<text x="165" y="106" fill="#94a3b8" font-size="10" text-anchor="middle">trusted users • phones • browsers</text>
<rect x="350" y="62" width="220" height="62" rx="8" fill="#0f172a" />
<rect x="350" y="62" width="220" height="62" rx="8" fill="rgba(120,53,15,0.30)" stroke="#fbbf24" stroke-width="1.5" />
<text x="460" y="87" fill="#ffffff" font-size="13" font-weight="700" text-anchor="middle">Pangolin VPS + Newt</text>
<text x="460" y="106" fill="#94a3b8" font-size="10" text-anchor="middle">reverse proxy for public subdomains</text>
<rect x="660" y="62" width="180" height="62" rx="8" fill="#0f172a" />
<rect x="660" y="62" width="180" height="62" rx="8" fill="rgba(136,19,55,0.40)" stroke="#fb7185" stroke-width="1.5" />
<text x="750" y="87" fill="#ffffff" font-size="13" font-weight="700" text-anchor="middle">Tailscale</text>
<text x="750" y="106" fill="#94a3b8" font-size="10" text-anchor="middle">Serenity: 100.94.87.79</text>
<rect x="950" y="62" width="250" height="62" rx="8" fill="#0f172a" />
<rect x="950" y="62" width="250" height="62" rx="8" fill="rgba(30,41,59,0.55)" stroke="#94a3b8" stroke-width="1.5" />
<text x="1075" y="87" fill="#ffffff" font-size="13" font-weight="700" text-anchor="middle">Cloudflare DNS</text>
<text x="1075" y="106" fill="#94a3b8" font-size="10" text-anchor="middle">paccoco.com records point to Pangolin VPS</text>
<!-- Core row -->
<rect x="100" y="242" width="220" height="92" rx="10" fill="#0f172a" />
<rect x="100" y="242" width="220" height="92" rx="10" fill="rgba(136,19,55,0.40)" stroke="#fb7185" stroke-width="1.6" />
<text x="210" y="268" fill="#ffffff" font-size="14" font-weight="700" text-anchor="middle">UDM Pro</text>
<text x="210" y="288" fill="#94a3b8" font-size="10" text-anchor="middle">gateway + UniFi controller</text>
<text x="210" y="305" fill="#94a3b8" font-size="10" text-anchor="middle">Mgmt IP: 10.5.0.1</text>
<text x="210" y="322" fill="#fb7185" font-size="9" text-anchor="middle">Admin TCP blocked from Untrusted</text>
<rect x="410" y="230" width="250" height="116" rx="10" fill="#0f172a" />
<rect x="410" y="230" width="250" height="116" rx="10" fill="rgba(120,53,15,0.28)" stroke="#fbbf24" stroke-width="1.6" />
<text x="535" y="256" fill="#ffffff" font-size="14" font-weight="700" text-anchor="middle">Switch Fabric</text>
<text x="535" y="278" fill="#94a3b8" font-size="10" text-anchor="middle">USW-24-PoE — 10.5.0.10</text>
<text x="535" y="296" fill="#94a3b8" font-size="10" text-anchor="middle">USW Pro HD 24 — 10.5.0.135</text>
<text x="535" y="314" fill="#fbbf24" font-size="9" text-anchor="middle">10G links for PD + Serenity • 2.5G lanes for Nomad/FlyingDutchman/Rocinante</text>
<rect x="760" y="230" width="250" height="116" rx="10" fill="#0f172a" />
<rect x="760" y="230" width="250" height="116" rx="10" fill="rgba(8,51,68,0.42)" stroke="#22d3ee" stroke-width="1.6" />
<text x="885" y="256" fill="#ffffff" font-size="14" font-weight="700" text-anchor="middle">Wi-Fi Access Points</text>
<text x="885" y="278" fill="#94a3b8" font-size="10" text-anchor="middle">U7 Pro — 10.5.0.21 — active</text>
<text x="885" y="296" fill="#94a3b8" font-size="10" text-anchor="middle">U6 LR — 10.5.0.20</text>
<text x="885" y="314" fill="#fb7185" font-size="9" text-anchor="middle">currently disconnected in UniFi</text>
<rect x="1110" y="230" width="280" height="116" rx="10" fill="#0f172a" />
<rect x="1110" y="230" width="280" height="116" rx="10" fill="rgba(6,78,59,0.40)" stroke="#34d399" stroke-width="1.6" />
<text x="1250" y="256" fill="#ffffff" font-size="14" font-weight="700" text-anchor="middle">Policy Zones</text>
<text x="1250" y="278" fill="#94a3b8" font-size="10" text-anchor="middle">Internal = Mgmt + Trusted + Servers</text>
<text x="1250" y="296" fill="#94a3b8" font-size="10" text-anchor="middle">Untrusted = IoT + Camera + Old IoT</text>
<text x="1250" y="314" fill="#94a3b8" font-size="10" text-anchor="middle">Hotspot = Guest</text>
<!-- Arrows top/core -->
<line x1="260" y1="93" x2="348" y2="93" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#arrow-amber)" />
<line x1="570" y1="93" x2="658" y2="93" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#arrow-rose)" />
<line x1="838" y1="93" x2="948" y2="93" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#arrow-amber)" />
<line x1="210" y1="124" x2="210" y2="240" stroke="#22d3ee" stroke-width="1.8" marker-end="url(#arrow-cyan)" />
<line x1="535" y1="346" x2="535" y2="438" stroke="#22d3ee" stroke-width="1.8" marker-end="url(#arrow-cyan)" />
<line x1="885" y1="346" x2="885" y2="438" stroke="#22d3ee" stroke-width="1.8" marker-end="url(#arrow-cyan)" />
<line x1="210" y1="334" x2="410" y2="288" stroke="#22d3ee" stroke-width="1.6" marker-end="url(#arrow-cyan)" />
<line x1="660" y1="288" x2="760" y2="288" stroke="#22d3ee" stroke-width="1.6" marker-end="url(#arrow-cyan)" />
<line x1="1010" y1="288" x2="1110" y2="288" stroke="#22d3ee" stroke-width="1.6" marker-end="url(#arrow-cyan)" />
<!-- VLAN columns -->
<!-- Management -->
<rect x="56" y="482" width="250" height="390" rx="14" fill="rgba(136,19,55,0.16)" stroke="#fb7185" stroke-width="1.3" />
<text x="181" y="512" fill="#ffffff" font-size="15" font-weight="700" text-anchor="middle">Management</text>
<text x="181" y="531" fill="#94a3b8" font-size="10" text-anchor="middle">10.5.0.0/24</text>
<text x="181" y="548" fill="#94a3b8" font-size="10" text-anchor="middle">UDM Pro • switches • AP management</text>
<rect x="82" y="578" width="198" height="78" rx="8" fill="#0f172a" />
<rect x="82" y="578" width="198" height="78" rx="8" fill="rgba(136,19,55,0.40)" stroke="#fb7185" stroke-width="1.4" />
<text x="181" y="604" fill="#ffffff" font-size="12" font-weight="700" text-anchor="middle">Core Infra</text>
<text x="181" y="623" fill="#94a3b8" font-size="9" text-anchor="middle">UDM Pro 10.5.0.1</text>
<text x="181" y="639" fill="#94a3b8" font-size="9" text-anchor="middle">USW-24-PoE 10.5.0.10 • USW Pro HD 10.5.0.135</text>
<rect x="82" y="690" width="198" height="82" rx="8" fill="#0f172a" />
<rect x="82" y="690" width="198" height="82" rx="8" fill="rgba(8,51,68,0.40)" stroke="#22d3ee" stroke-width="1.4" />
<text x="181" y="716" fill="#ffffff" font-size="12" font-weight="700" text-anchor="middle">AP Control</text>
<text x="181" y="735" fill="#94a3b8" font-size="9" text-anchor="middle">U7 Pro active</text>
<text x="181" y="751" fill="#94a3b8" font-size="9" text-anchor="middle">U6 LR disconnected but still modeled</text>
<text x="181" y="812" fill="#fb7185" font-size="10" text-anchor="middle">Management stays operator-only</text>
<!-- Trusted -->
<rect x="336" y="482" width="250" height="390" rx="14" fill="rgba(8,51,68,0.16)" stroke="#22d3ee" stroke-width="1.3" />
<text x="461" y="512" fill="#ffffff" font-size="15" font-weight="700" text-anchor="middle">Trusted</text>
<text x="461" y="531" fill="#94a3b8" font-size="10" text-anchor="middle">10.5.1.0/24 • VLAN 51</text>
<text x="461" y="548" fill="#94a3b8" font-size="10" text-anchor="middle">laptops • phones • tablets • daily-driver clients</text>
<rect x="362" y="578" width="198" height="82" rx="8" fill="#0f172a" />
<rect x="362" y="578" width="198" height="82" rx="8" fill="rgba(8,51,68,0.40)" stroke="#22d3ee" stroke-width="1.4" />
<text x="461" y="604" fill="#ffffff" font-size="12" font-weight="700" text-anchor="middle">User Devices</text>
<text x="461" y="623" fill="#94a3b8" font-size="9" text-anchor="middle">operator/admin endpoints live here</text>
<text x="461" y="639" fill="#94a3b8" font-size="9" text-anchor="middle">can initiate into servers + untrusted lanes</text>
<rect x="362" y="690" width="198" height="82" rx="8" fill="#0f172a" />
<rect x="362" y="690" width="198" height="82" rx="8" fill="rgba(30,41,59,0.55)" stroke="#94a3b8" stroke-width="1.4" />
<text x="461" y="716" fill="#ffffff" font-size="12" font-weight="700" text-anchor="middle">Wi-Fi SSID</text>
<text x="461" y="735" fill="#94a3b8" font-size="9" text-anchor="middle">primary trusted network</text>
<text x="461" y="751" fill="#94a3b8" font-size="9" text-anchor="middle">legacy compatibility cleanup still ongoing</text>
<text x="461" y="812" fill="#34d399" font-size="10" text-anchor="middle">Allowed to initiate toward Untrusted</text>
<!-- Servers -->
<rect x="616" y="482" width="290" height="390" rx="14" fill="rgba(6,78,59,0.16)" stroke="#34d399" stroke-width="1.3" />
<text x="761" y="512" fill="#ffffff" font-size="15" font-weight="700" text-anchor="middle">Servers</text>
<text x="761" y="531" fill="#94a3b8" font-size="10" text-anchor="middle">10.5.30.0/24 • VLAN 30</text>
<text x="761" y="548" fill="#94a3b8" font-size="10" text-anchor="middle">core services + app stacks + AI + storage-adjacent hosts</text>
<rect x="642" y="572" width="238" height="112" rx="8" fill="#0f172a" />
<rect x="642" y="572" width="238" height="112" rx="8" fill="rgba(6,78,59,0.40)" stroke="#34d399" stroke-width="1.4" />
<text x="761" y="598" fill="#ffffff" font-size="12" font-weight="700" text-anchor="middle">Core Hosts</text>
<text x="761" y="617" fill="#94a3b8" font-size="9" text-anchor="middle">PlausibleDeniability — 10.5.30.6</text>
<text x="761" y="633" fill="#94a3b8" font-size="9" text-anchor="middle">Serenity — 10.5.30.5</text>
<text x="761" y="649" fill="#94a3b8" font-size="9" text-anchor="middle">N.O.M.A.D. — 10.5.30.7</text>
<text x="761" y="665" fill="#94a3b8" font-size="9" text-anchor="middle">Rocinante — 10.5.30.112</text>
<rect x="642" y="706" width="238" height="98" rx="8" fill="#0f172a" />
<rect x="642" y="706" width="238" height="98" rx="8" fill="rgba(76,29,149,0.40)" stroke="#a78bfa" stroke-width="1.4" />
<text x="761" y="732" fill="#ffffff" font-size="12" font-weight="700" text-anchor="middle">Shared Services</text>
<text x="761" y="751" fill="#94a3b8" font-size="9" text-anchor="middle">Pi-hole HA VIP / DNS: 10.5.30.53</text>
<text x="761" y="767" fill="#94a3b8" font-size="9" text-anchor="middle">Gitea • Home Assistant • n8n • OpenWebUI • Qdrant</text>
<text x="761" y="783" fill="#94a3b8" font-size="9" text-anchor="middle">LiteLLM • monitoring • media • productivity stacks</text>
<text x="761" y="832" fill="#34d399" font-size="10" text-anchor="middle">Internal services reachable by trusted/admin workflows</text>
<!-- IoT -->
<rect x="936" y="482" width="250" height="390" rx="14" fill="rgba(120,53,15,0.16)" stroke="#fbbf24" stroke-width="1.3" />
<text x="1061" y="512" fill="#ffffff" font-size="15" font-weight="700" text-anchor="middle">IoT</text>
<text x="1061" y="531" fill="#94a3b8" font-size="10" text-anchor="middle">10.5.10.0/24 • VLAN 510</text>
<text x="1061" y="548" fill="#94a3b8" font-size="10" text-anchor="middle">ecobees • MyQ • fridge • smart appliances</text>
<rect x="962" y="584" width="198" height="84" rx="8" fill="#0f172a" />
<rect x="962" y="584" width="198" height="84" rx="8" fill="rgba(120,53,15,0.30)" stroke="#fbbf24" stroke-width="1.4" />
<text x="1061" y="610" fill="#ffffff" font-size="12" font-weight="700" text-anchor="middle">DHCP DNS</text>
<text x="1061" y="629" fill="#94a3b8" font-size="9" text-anchor="middle">hands out 10.5.30.53</text>
<text x="1061" y="645" fill="#94a3b8" font-size="9" text-anchor="middle">no longer depends on gateway-default DNS</text>
<rect x="962" y="696" width="198" height="92" rx="8" fill="#0f172a" />
<rect x="962" y="696" width="198" height="92" rx="8" fill="rgba(136,19,55,0.40)" stroke="#fb7185" stroke-width="1.4" />
<text x="1061" y="722" fill="#ffffff" font-size="12" font-weight="700" text-anchor="middle">Gateway Access</text>
<text x="1061" y="741" fill="#94a3b8" font-size="9" text-anchor="middle">general gateway access blocked</text>
<text x="1061" y="757" fill="#94a3b8" font-size="9" text-anchor="middle">DHCP + mDNS preserved</text>
<text x="1061" y="773" fill="#94a3b8" font-size="9" text-anchor="middle">UDM admin TCP explicitly blocked</text>
<!-- Camera -->
<rect x="1216" y="482" width="250" height="180" rx="14" fill="rgba(120,53,15,0.16)" stroke="#fbbf24" stroke-width="1.3" />
<text x="1341" y="512" fill="#ffffff" font-size="15" font-weight="700" text-anchor="middle">Camera</text>
<text x="1341" y="531" fill="#94a3b8" font-size="10" text-anchor="middle">10.5.20.0/24 • VLAN 520</text>
<text x="1341" y="548" fill="#94a3b8" font-size="10" text-anchor="middle">Protect chimes + camera-adjacent gear</text>
<text x="1341" y="582" fill="#94a3b8" font-size="9" text-anchor="middle">Same shield posture as IoT</text>
<text x="1341" y="599" fill="#94a3b8" font-size="9" text-anchor="middle">DNS -> 10.5.30.53 • DHCP/mDNS preserved</text>
<text x="1341" y="616" fill="#94a3b8" font-size="9" text-anchor="middle">general gateway access blocked</text>
<!-- Guest -->
<rect x="1216" y="686" width="250" height="86" rx="14" fill="rgba(30,41,59,0.26)" stroke="#94a3b8" stroke-width="1.3" />
<text x="1341" y="715" fill="#ffffff" font-size="15" font-weight="700" text-anchor="middle">Guest / Hotspot</text>
<text x="1341" y="734" fill="#94a3b8" font-size="10" text-anchor="middle">10.5.90.0/24 • VLAN 590 • internet-only lane</text>
<!-- Old IoT -->
<rect x="1216" y="794" width="250" height="96" rx="14" fill="rgba(136,19,55,0.18)" stroke="#fb7185" stroke-width="1.3" />
<text x="1341" y="823" fill="#ffffff" font-size="15" font-weight="700" text-anchor="middle">Old IoT / Legacy Quarantine</text>
<text x="1341" y="842" fill="#94a3b8" font-size="10" text-anchor="middle">192.168.1.0/24 • VLAN 2</text>
<text x="1341" y="859" fill="#94a3b8" font-size="9" text-anchor="middle">shrinking-only containment lane</text>
<text x="1341" y="875" fill="#94a3b8" font-size="9" text-anchor="middle">same DNS/gateway shield pattern as IoT/Camera</text>
<!-- Inter-lane arrows / policy cues -->
<line x1="510" y1="636" x2="935" y2="636" stroke="#34d399" stroke-width="1.8" marker-end="url(#arrow-emerald)" />
<text x="726" y="626" fill="#34d399" font-size="9" text-anchor="middle">Allow Internal → Untrusted</text>
<line x1="1061" y1="584" x2="761" y2="756" stroke="#a78bfa" stroke-width="1.8" marker-end="url(#arrow-emerald)" />
<text x="930" y="699" fill="#a78bfa" font-size="9" text-anchor="middle">DNS only → 10.5.30.53</text>
<line x1="1061" y1="696" x2="210" y2="334" stroke="#fb7185" stroke-width="1.6" stroke-dasharray="6,5" marker-end="url(#arrow-rose)" />
<text x="705" y="520" fill="#fb7185" font-size="9" text-anchor="middle">Blocked: general Untrusted → Gateway</text>
<line x1="1341" y1="572" x2="210" y2="306" stroke="#fb7185" stroke-width="1.4" stroke-dasharray="5,5" marker-end="url(#arrow-rose)" />
<text x="770" y="394" fill="#fb7185" font-size="9" text-anchor="middle">Blocked: UDM admin TCP from Camera / IoT / Old IoT</text>
<line x1="1061" y1="788" x2="761" y2="786" stroke="#fbbf24" stroke-width="1.5" stroke-dasharray="4,4" marker-end="url(#arrow-amber)" />
<text x="908" y="779" fill="#fbbf24" font-size="9" text-anchor="middle">Only proven exceptions stay</text>
<!-- Legend -->
<rect x="52" y="958" width="1396" height="90" rx="12" fill="rgba(15,23,42,0.85)" stroke="#1e293b" stroke-width="1.2" />
<text x="78" y="986" fill="#ffffff" font-size="12" font-weight="700">Legend</text>
<rect x="78" y="1000" width="16" height="10" rx="2" fill="rgba(8,51,68,0.40)" stroke="#22d3ee" stroke-width="1" />
<text x="103" y="1009" fill="#94a3b8" font-size="9">Trusted / operator lanes</text>
<rect x="270" y="1000" width="16" height="10" rx="2" fill="rgba(6,78,59,0.40)" stroke="#34d399" stroke-width="1" />
<text x="295" y="1009" fill="#94a3b8" font-size="9">Servers / core services</text>
<rect x="468" y="1000" width="16" height="10" rx="2" fill="rgba(120,53,15,0.30)" stroke="#fbbf24" stroke-width="1" />
<text x="493" y="1009" fill="#94a3b8" font-size="9">IoT / camera lanes</text>
<rect x="640" y="1000" width="16" height="10" rx="2" fill="rgba(136,19,55,0.40)" stroke="#fb7185" stroke-width="1" />
<text x="665" y="1009" fill="#94a3b8" font-size="9">Management / blocked security surfaces</text>
<line x1="910" y1="1005" x2="948" y2="1005" stroke="#34d399" stroke-width="1.8" marker-end="url(#arrow-emerald)" />
<text x="960" y="1009" fill="#94a3b8" font-size="9">allowed path</text>
<line x1="1088" y1="1005" x2="1126" y2="1005" stroke="#fb7185" stroke-width="1.8" stroke-dasharray="5,5" marker-end="url(#arrow-rose)" />
<text x="1138" y="1009" fill="#94a3b8" font-size="9">blocked path</text>
<line x1="1262" y1="1005" x2="1300" y2="1005" stroke="#fbbf24" stroke-width="1.6" stroke-dasharray="4,4" marker-end="url(#arrow-amber)" />
<text x="1312" y="1009" fill="#94a3b8" font-size="9">narrow preserved exception</text>
</svg>
</div>
<div class="cards">
<div class="card">
<div class="card-header">
<div class="card-dot emerald"></div>
<h3>Core hosts + routing facts</h3>
</div>
<ul>
<li>• PD 10.5.30.6 is the main Docker/control host.</li>
<li>• Serenity 10.5.30.5 handles NAS + ARR + Tailscale reachability.</li>
<li>• N.O.M.A.D. 10.5.30.7 runs local services and game/server workloads.</li>
<li>• Rocinante 10.5.30.112 carries heavy Ollama inference.</li>
<li>• UDM Pro stays at 10.5.0.1 on the management subnet.</li>
</ul>
</div>
<div class="card">
<div class="card-header">
<div class="card-dot amber"></div>
<h3>What changed recently</h3>
</div>
<ul>
<li>• Servers VLAN is live at 10.5.30.0/24 (VLAN 30).</li>
<li>• IoT, Camera, and Old IoT now DHCP-advertise 10.5.30.53 for DNS.</li>
<li>• Broader Untrusted → Gateway block is live.</li>
<li>• DHCP and mDNS are still preserved for restricted lanes.</li>
<li>• UDM Pro admin TCP ports are explicitly blocked from Untrusted.</li>
</ul>
</div>
<div class="card">
<div class="card-header">
<div class="card-dot rose"></div>
<h3>Still worth validating on real clients</h3>
</div>
<ul>
<li>• Renew one DHCP lease per restricted lane.</li>
<li>• Confirm DNS resolution works through 10.5.30.53.</li>
<li>• Confirm gateway admin/UI access fails from those lanes.</li>
<li>• Keep Google/cast validation as a separate laptop-present test wave.</li>
<li>• Treat any new exception as earned only by a real failing device.</li>
</ul>
</div>
</div>
<div class="tip">Open locally with: xdg-open /home/fizzlepoof/repos/truenas-stacks/docs/architecture/network-topology-2026-05-23.html</div>
<div class="footer">Generated from current repo docs + live UniFi changes documented on 2026-05-23.</div>
</div>
</body>
</html>

View File

@@ -3,8 +3,8 @@
"createdAt": "2026-05-09T19:58:19.095Z", "createdAt": "2026-05-09T19:58:19.095Z",
"id": "fmywVBIanfxOfDlI", "id": "fmywVBIanfxOfDlI",
"name": "Grafana Alert → AI → Gotify v1.4", "name": "Grafana Alert → AI → Gotify v1.4",
"description": null, "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
"active": true, "active": false,
"isArchived": false, "isArchived": false,
"nodes": [ "nodes": [
{ {

View File

@@ -3,8 +3,8 @@
"createdAt": "2026-05-06T23:25:29.005Z", "createdAt": "2026-05-06T23:25:29.005Z",
"id": "gYyOggC4J98keLbj", "id": "gYyOggC4J98keLbj",
"name": "RAG Pipeline v1.0", "name": "RAG Pipeline v1.0",
"description": null, "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
"active": true, "active": false,
"isArchived": false, "isArchived": false,
"nodes": [ "nodes": [
{ {

View File

@@ -3,8 +3,8 @@
"createdAt": "2026-05-06T23:25:29.005Z", "createdAt": "2026-05-06T23:25:29.005Z",
"id": "gYyOggC4J98keLbj", "id": "gYyOggC4J98keLbj",
"name": "RAG Pipeline — Ingest & Query", "name": "RAG Pipeline — Ingest & Query",
"description": null, "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
"active": true, "active": false,
"isArchived": false, "isArchived": false,
"nodes": [ "nodes": [
{ {

View File

@@ -3,8 +3,8 @@
"createdAt": "2026-05-07T20:24:40.153Z", "createdAt": "2026-05-07T20:24:40.153Z",
"id": "dQ1d5deV4mF0s5eP", "id": "dQ1d5deV4mF0s5eP",
"name": "Paperless AI Processing v1.0", "name": "Paperless AI Processing v1.0",
"description": null, "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
"active": true, "active": false,
"isArchived": false, "isArchived": false,
"nodes": [ "nodes": [
{ {

View File

@@ -3,8 +3,8 @@
"createdAt": "2026-05-07T20:24:40.153Z", "createdAt": "2026-05-07T20:24:40.153Z",
"id": "dQ1d5deV4mF0s5eP", "id": "dQ1d5deV4mF0s5eP",
"name": "Paperless AI Processing v1.0", "name": "Paperless AI Processing v1.0",
"description": null, "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
"active": true, "active": false,
"isArchived": false, "isArchived": false,
"nodes": [ "nodes": [
{ {

View File

@@ -3,8 +3,8 @@
"createdAt": "2026-05-09T22:31:02.480Z", "createdAt": "2026-05-09T22:31:02.480Z",
"id": "9qfbZJJmSJqmg5sX", "id": "9qfbZJJmSJqmg5sX",
"name": "Whisper Audio Transcription", "name": "Whisper Audio Transcription",
"description": null, "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
"active": true, "active": false,
"isArchived": false, "isArchived": false,
"nodes": [ "nodes": [
{ {

View File

@@ -3,8 +3,8 @@
"createdAt": "2026-05-07T00:48:49.355Z", "createdAt": "2026-05-07T00:48:49.355Z",
"id": "4YKnRQ9rOiQCeYEU", "id": "4YKnRQ9rOiQCeYEU",
"name": "Paperless \u2192 RAG v1.0", "name": "Paperless \u2192 RAG v1.0",
"description": null, "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
"active": true, "active": false,
"isArchived": false, "isArchived": false,
"nodes": [ "nodes": [
{ {

View File

@@ -3,8 +3,8 @@
"createdAt": "2026-05-07T00:48:49.355Z", "createdAt": "2026-05-07T00:48:49.355Z",
"id": "4YKnRQ9rOiQCeYEU", "id": "4YKnRQ9rOiQCeYEU",
"name": "Paperless \u2192 RAG Ingest", "name": "Paperless \u2192 RAG Ingest",
"description": null, "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
"active": true, "active": false,
"isArchived": false, "isArchived": false,
"nodes": [ "nodes": [
{ {

View File

@@ -3,8 +3,8 @@
"createdAt": "2026-05-07T00:49:43.226Z", "createdAt": "2026-05-07T00:49:43.226Z",
"id": "CFYWbBRx9RPf4HjS", "id": "CFYWbBRx9RPf4HjS",
"name": "Git Commit → AI Summary → Gotify v1.0", "name": "Git Commit → AI Summary → Gotify v1.0",
"description": null, "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
"active": true, "active": false,
"isArchived": false, "isArchived": false,
"nodes": [ "nodes": [
{ {

View File

@@ -3,8 +3,8 @@
"createdAt": "2026-05-07T00:49:43.226Z", "createdAt": "2026-05-07T00:49:43.226Z",
"id": "CFYWbBRx9RPf4HjS", "id": "CFYWbBRx9RPf4HjS",
"name": "Git Commit \u2192 AI Summary \u2192 Gotify", "name": "Git Commit \u2192 AI Summary \u2192 Gotify",
"description": null, "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
"active": true, "active": false,
"isArchived": false, "isArchived": false,
"nodes": [ "nodes": [
{ {

View File

@@ -3,8 +3,8 @@
"createdAt": "2026-05-10T02:04:29.809Z", "createdAt": "2026-05-10T02:04:29.809Z",
"id": "g9JRtBA5lR0OAwVo", "id": "g9JRtBA5lR0OAwVo",
"name": "Class Recording → Transcribe → RAG", "name": "Class Recording → Transcribe → RAG",
"description": null, "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
"active": true, "active": false,
"isArchived": false, "isArchived": false,
"nodes": [ "nodes": [
{ {

View File

@@ -3,8 +3,8 @@
"createdAt": "2026-05-10T02:04:47.115Z", "createdAt": "2026-05-10T02:04:47.115Z",
"id": "sg7nRrhim0omh8oQ", "id": "sg7nRrhim0omh8oQ",
"name": "Class Transcript Ingest v1.1", "name": "Class Transcript Ingest v1.1",
"description": null, "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
"active": true, "active": false,
"isArchived": false, "isArchived": false,
"nodes": [ "nodes": [
{ {

View File

@@ -3,8 +3,8 @@
"createdAt": "2026-05-13T16:06:39.571Z", "createdAt": "2026-05-13T16:06:39.571Z",
"id": "FIhrfDfL1NgakDwl", "id": "FIhrfDfL1NgakDwl",
"name": "Paperless Intake Triage → Doris Review Queue", "name": "Paperless Intake Triage → Doris Review Queue",
"description": null, "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
"active": true, "active": false,
"isArchived": false, "isArchived": false,
"nodes": [ "nodes": [
{ {

View File

@@ -3,8 +3,8 @@
"createdAt": "2026-05-13T21:26:54.606Z", "createdAt": "2026-05-13T21:26:54.606Z",
"id": "7MlM3DjAMf4eSl0s", "id": "7MlM3DjAMf4eSl0s",
"name": "Paperless School Intake Metadata Enrichment v1.1", "name": "Paperless School Intake Metadata Enrichment v1.1",
"description": null, "description": "Disabled 2026-05-24 after PD Ollama/LiteLLM hammering destabilized PlausibleDeniability. Needs redesign before re-enable.",
"active": true, "active": false,
"isArchived": false, "isArchived": false,
"nodes": [ "nodes": [
{ {