TOML structural fix (critical):
- Remove wrapper [listener]/[network]/[upstream] headers; use flat
dotted-key notation ([listener.0], [network.0], etc.) that ctrld's
Go TOML v2 parser requires — the old nested style triggered a table
redefinition panic at startup
- Add 'name' field to every [upstream.N] section (required by ctrld)
- Add [listener.0.policy] name field
DoH3 and protocol support:
- CtrldVlanProfile gains protocol (default "doh3") and endpoint_url
fields; endpoint_url overrides the ControlD resolver_id URL if set
- Upstream type now uses the profile's protocol instead of hardcoded
"doh" — enables DoH3 connection-pool reuse added in ctrld 2025
Endpoint pre-flight validation:
- New _validate_doh_endpoint(): sends RFC 8484 DoH GET query over
plain HTTPS (works for DoH3 URLs too — ControlD serves both) and
measures latency; no ctrld binary or Docker required
- New POST /api/ctrld/validate-endpoints: tests all profile endpoints,
validates TOML syntax via tomllib (Python 3.11+), returns per-profile
results + toml_preview
- ctrld_save_config now runs validation before writing anything and
returns HTTP 400 with per-profile probe results on failure — configs
are never pushed with a broken endpoint
OPNsense plugin verdict: documented in code — the os-controld plugin
kills Unbound and breaks OPNsense DNS advertisement; SSH-based deploy
with our own TOML remains the correct path
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
- Add POST /api/vlan/provision: end-to-end VLAN wizard that creates the
switch VLAN, OPNsense VLAN tag, DHCP scope, and allow-outbound firewall
rule in one call; returns pending_steps for anything needing manual
OPNsense UI finish (interface assignment when opnsense_if not provided)
- Rewire POST /api/devices/push-reservation to target OPNsense DHCP when
configured (no Advanced License required); falls back to switch CLI only
if OPNsense is not set up; uses stored VLAN→interface map for iface lookup
- Rewrite POST /api/devices/push-pinhole to use OPNsense firewall/filter
API instead of switch ACLs; stores rule UUIDs in pinholes.json for clean
removal; no longer requires Advanced License
- Remove dead relay endpoints (GET/POST /api/dhcp/relay/*), RelayConfig
model, and helpers (_get_relay_status, _get_vlan_ips, _build_relay_cmds);
relay config is irrelevant when OPNsense is the DHCP server
- Add VLAN_IF_MAP_FILE and PINHOLE_FILE with load/save helpers to persist
the VLAN→OPNsense interface mapping and pinhole rule UUIDs across restarts
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
Probes the switch using read-only show commands to detect whether the
Advanced Software License is installed. Base Software rejects ACL and
L3 VLAN interface commands with 'Invalid input detected'.
- GET /api/switch/capabilities: non-destructive probe (show ip access-list,
show interface vlan 1), returns acl/l3_vlan/dhcp_relay_config/
management_pinholes/dns_enforce_acls flags and license_tier. Cached 5 min.
- _require_advanced_license(): guard helper that raises HTTP 402 with a
clear message before attempting any ACL push to the switch.
- Applied guard to: POST /api/switch/acl, /api/devices/push-pinhole,
/api/dhcp/relay/configure, /api/ctrld/dns-enforce-acls.
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
The original commands were missing the 'ip' prefix. Correct ACLI syntax:
show dhcp-server -> show ip dhcp-server
show dhcp-server leases -> show ip dhcp-server leases
show dhcp-server static-binding -> show ip dhcp-server static-binding
The ERS 59100GTS-PWR+ has a DHCP server but it may need to be enabled
first ('ip dhcp-server enable' in config mode) or may require an
Advanced License. All DHCP server calls now have try/except so device
discovery falls back to ARP if the feature is not yet active.
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
- show ip route default → show ip route (parse 0.0.0.0 row for gateway)
- show ip helper-address → show ip dhcp-relay fwd-path + update parser
- ip helper-address → ip dhcp-relay fwd-path <vlan-ip> <server-ip>
(add _get_vlan_ips() to resolve VLAN interface IPs before building cmds)
- Remove all show dhcp-server / show dhcp-server leases / show dhcp-server
static-binding calls — switch has no DHCP server (show ip dhcp ? only
shows 'client'). Device discovery now uses show arp only.
- push-reservation, sync to_switch/remove_switch → 501 Not Implemented
- _get_switch_reservations() / _get_switch_dhcp_status() return empty/false
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
- Table of all read and push commands with confirmed/not-tested/invalid status
- Note that save config caused a reboot on first run
- screen scrollback: Ctrl-A [ to enter copy mode, defscrollback in .screenrc
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
Per the ERS 59100 Quick Install Guide, the switch defaults to 192.168.1.1/24
on VLAN 1 if DHCP fails. Method 3 now notes that on a factory/reset switch
you can TFTP without configuring anything via console first.
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
- Method 3: laptop with console + Ethernet can act as temporary TFTP server
- Method 4: XMODEM over console cable when USB broken and no Ethernet available
- Note 3-hour transfer time warning for XMODEM at 9600 baud
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
- Document current firmware versions (BOSS 7.9.6.015 / Diag 7.5.0.4)
- Note upgrade rules: one version at a time, diag first, no read-only USB files
- USB method via download command and boot menu option 4
- SFTP and TFTP methods via CLI
- Link to Extreme portal and announcements page for latest version
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
- Add boot menu contents (options 1-9, A-C) with explanation of each
- Method 1: boot menu option 5 (works without knowing password)
- Method 2: CLI method (requires working login)
- Note 60-second window and how to continue normal boot if entered by accident
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
- Document BOSS v7.9.6 as the switch firmware in Requirements section
- Add factory reset procedure (boot config flags factory) to Troubleshooting
- Note when a wipe is and is not necessary
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
- Fix save command: copy running-config nvram:config.cfg -> save config
- Fix port format note: remove incorrect 1/1 slot prefix, both models use bare numbers
- Add note explaining why 192.168.1.1 conflict is not a problem (management VLAN 99 is separate subnet)
- Add console commands to change VLAN 1 IP if needed before VLAN 99 is configured
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
exec_command runs in user mode on BOSS v7.9.6; most show commands
(show vlan, show sys-info, show poe-main-status, show config, etc.)
require enable mode. Switch to invoke_shell per read command, sending
terminal length 0 and enable before each command.
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
- Replace show poe-port-status with show poe-port status ALL
- Replace show vlan members with show vlan
- Replace show running-config with show config
- Fix VLAN port format from 1/{p} to {p} (BOSS uses bare port numbers)
- Fix interface naming from GigabitEthernet 1/{p} to GigabitEthernet {p}
- Add terminal length 0 to push session setup to prevent pagination
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
Replace ERS 5952 (48+4 port) config with ERS 59100GTS-PWR+:
- Port validation extended to 1–100
- All interfaces now use GigabitEthernet 1/{p} slot notation
- PoE boundary moved from port 48 to port 96
- VLAN commands updated to use 1/{p} port notation
- Key path, TOTP name, and app title updated
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
Documents the full first-time hardware setup flow:
- Where the console/service port is on the 59100 and 5952 front panel
- What console cable to buy and how to connect it
- What the Ctrl-Y boot screen means (don't press it, just wait)
- Default login credentials
- Bootstrap VLAN 99 commands including 59100 port numbering (1/1 format)
- Management computer static IP setup
- Consolidates and removes duplicate console/bootstrap sections
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
New ACL template: "IoT VLAN — isolated + access local services via FQDN"
- Permits DNS to the configured resolver (so FQDNs resolve to LAN IPs)
- Permits traffic to the servers VLAN subnet only (NAS, Home Assistant, etc.)
- Blocks all other RFC1918 — users, cameras, management stay isolated
- Permits internet
- serverSubnet param auto-suggested from the vlans list if a VLAN named
"Servers" exists; otherwise user enters it
Template description explains the full picture:
- How FQDN access works through the ACL (DNS → LAN IP → ACL permits it)
- Offline resilience: ctrld must be on OPNsense (not management PC) to
survive internet outages; split-horizon in DNS tab makes *.lan resolve
from local dnsmasq without any internet dependency
- Servers VLAN security: lock down servers VLAN inbound ACL by port
so IoT can only reach specific service ports, not all server traffic
Strict IoT template description tightened — now clearly says "zero LAN
access" so users pick the right template for their use case.
Modal: serverSubnet field added alongside existing ctrldIp/nvrIp/pbxIp
params; preview correctly passes serverSubnet to the build function.
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
Dashboard tab (now default landing page):
- Service status cards: Switch / OPNsense / ctrld DNS / WireGuard
each clickable to navigate to the relevant tab
- Setup checklist with progress bar: 8 steps from switch connection
through WireGuard, each uncompleted step is clickable and navigates
to the tab where that step is configured
- VLAN health grid: per-VLAN card showing device count (from DHCP
leases), Relay / DNS / ACL status dots, and inline "+ add" nudges
for anything not yet configured
- Quick Access buttons to all tabs
- All data loaded in parallel from existing API endpoints — no new
backend endpoints needed
ACL template fixes:
- VoIP template label updated: "local desk phones" clarifies when to use it
- VoIP description now explains: ACL is subnet-based (MAC randomization
irrelevant), switch IS the L3 enforcement point (no OPNsense inter-VLAN
rules needed), and gives the correct mobile-softphone recommendation:
configure with public FQDN via OPNsense port forwarding + NAT reflection
- IoT template label/description updated to explicitly list smart TVs,
printers, IoT as targets and mention it blocks PBX + all LAN services
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
ACL backend:
- AclRule gains optional port_end field; build_acl generates
"range X Y" when both port and port_end are set (needed for RTP)
New ACL template — "SIP Phone VLAN — Asterisk / FreePBX access":
- Permits SIP signaling UDP/TCP 5060 to PBX IP
- Permits SIP/TLS TCP 5061 to PBX IP
- Permits RTP audio UDP range 10000-20000 to PBX IP (uses new range syntax)
- Blocks management VLAN 99
- Permits internet and all other traffic
- Requires entering the Asterisk server IP (restricts SIP/RTP to that
exact host, not the whole VLAN subnet)
Template description explains:
- Why OPNsense firewall rules are also needed (inter-VLAN routing)
- Exactly which OPNsense rules to add (including return RTP)
- Remote access options: WebRTC via Caddy reverse proxy (recommended)
and SIP/TLS with fail2ban for traditional SIP clients
Template modal:
- New pbxIp param field shown for VoIP template
- Description box scrollable for longer template descriptions
- Preview renders "range X Y" for port range rules
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
When OPNsense WG plugin is detected:
- Show recommendation banner at top explaining why router-level VPN
is better than running it on the management PC
- OPNsense WG section appears first (recommended path)
- Local WG panels relabeled as "backup / this machine only"
- Local WG description changes to explain the limitation (VLAN 99 only)
When OPNsense is not connected:
- Local WG shown normally as the primary option
- Tip nudge added pointing to DHCP tab to unlock router-level VPN
- OPNsense WG section shown below as a setup prompt
In both cases:
- SSH Tunnel renamed to "Emergency Fallback" with clearer description
- Pre-select servers VLAN when OPNsense WG server becomes available
(most users want remote access to services, not IoT/cameras)
- Connected Peers / Clients panels prefixed "Local WG —" for clarity
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
Moves WireGuard off the management computer and onto OPNsense so any
device can VPN home without touching the management PC. Each peer is
restricted to only the VLANs you select (e.g. phone gets VLAN 10 only,
laptop gets VLAN 10 + 20). Private keys are generated on the mgmt PC
and never sent to OPNsense — only the public key is registered.
Backend (switch_backend.py):
- /api/opnsense/wireguard/status — check plugin, server, peers
- /api/opnsense/wireguard/setup-server — create wg1 on OPNsense via API
- DELETE /api/opnsense/wireguard/server — tear down server
- /api/opnsense/wireguard/add-peer — generate keypair, register peer,
link to server, return .conf
- DELETE /api/opnsense/wireguard/peer/{uuid} — revoke peer
- /api/opnsense/wireguard/peer-config/{name} — fetch saved .conf
Frontend (ers5952-manager.jsx):
- New OPNsenseWGSection component added to VPN tab below local WireGuard
- Progressive UI: not configured → plugin missing → server setup →
peer management (VLAN checkboxes) → QR/.conf download
- Firewall rules guidance panel auto-generated from active peers showing
exactly which OPNsense rules to add per VLAN
- vlans prop threaded through to WireGuardTab so VLAN names/colors
appear on peer badges and in the VLAN selector
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
- Backend: _get_relay_status() reads current ip helper-address per VLAN
- Backend: _build_relay_cmds() generates ERS 5952 relay CLI commands
- Backend: /api/dhcp/relay/status and /api/dhcp/relay/configure endpoints
- Backend: dhcp_overview now includes relay status in response
- Frontend: VLAN_MAP + VlanBadge + vlanFromIp() helpers for consistent labelling
- Frontend: RelayPanel shows per-VLAN relay status grid with push button;
VLAN 99 always shown as locked/local, VLANs 10/20/30/40/50 show live
relay target and purpose note
- Frontend: Reservations table gains VLAN column and inline purpose note
(from descr/notes or VLAN_MAP fallback)
VLAN 99 is excluded from relay at both backend and UI level — it is the
switch management / OPNsense recovery path.
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
- setup_ctrld() now asks for each VLAN's actual subnet (was hardcoded
as 192.168.{id}.0/24, wrong for any non-default addressing)
- Asks for local domain suffix (default: lan) with explanation of what
it is and where to find it in OPNsense
- Asks for local resolver address (default: 127.0.0.1:5353) with a
full plain-English explanation of WHY Unbound must move off port 53,
why both .lan and .local rules are needed, and what breaks without them
- _ctrld_opnsense_setup() now walks through the Unbound port change
step-by-step with exact OPNsense UI paths, pausing for confirmation
at each stage before proceeding
- All three install paths (_local, _opnsense, _manual) now pass
local_domain and local_resolver through to _build_toml()
- Intro text rewritten for someone unfamiliar with DNS filtering,
VLANs, or Resolver IDs — explains what each thing is before asking
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
Features added:
- Port 53 conflict resolution: auto-detect/fix systemd-resolved stub listener
on Linux; instructions for OPNsense Unbound (ctrld auto-terminates it)
- DNS enforcement ACLs: generate ERS 5952 ACL commands that permit DNS only
to ctrld IP and block all other port 53/853 traffic per VLAN
- Inter-VLAN routing ACL templates: Staff, IoT, Guest, Camera profiles with
live preview and parameter inputs (ctrld IP, NVR IP, subnet)
- Local hostname resolution: dnsmasq Docker service for .lan split-horizon DNS;
manage hostname→IP mappings via UI; generates dnsmasq.conf and ctrld.toml
upstream.local block
- Fix ctrld.toml format: correct [listener.0], [network.N], [upstream.N] table
notation (was using wrong [[array]] notation); matches official docs format
- Backend docstrings: added docstrings to all previously undocumented functions
- README: new sections for port 53 conflict resolution, DNS enforcement ACLs,
ACL templates, and local hostname resolution (dnsmasq)
- Fix Python 3.11 f-string syntax errors in Avaya_5952_setup.py (backslash
in f-string expressions, same-type quote in dict access); embed now succeeds
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6