403 lines
15 KiB
Markdown
403 lines
15 KiB
Markdown
# Avaya ERS 5952 Switch Manager — Project Handoff
|
|
|
|
## What This Is
|
|
|
|
A browser-based management interface for the Avaya / Extreme Networks ERS 5952
|
|
enterprise switch. The switch has no REST API — everything works via SSH
|
|
screen-scraping: the backend opens SSH sessions, sends CLI commands one at a time,
|
|
checks each response for errors, and stops on first failure. Config is only saved
|
|
to NVRAM if every command succeeds.
|
|
|
|
The entire project ships as a single Python file: `Avaya_5952_setup.py`
|
|
This file contains three embedded Python string constants:
|
|
- BACKEND_SRC — the FastAPI backend (Python, ~1800 lines)
|
|
- JSX_SRC — the React frontend (JSX, ~2600 lines)
|
|
- README_SRC — the README (Markdown, ~400 lines)
|
|
|
|
Running `python Avaya_5952_setup.py` extracts and runs 21 setup steps that
|
|
install everything, configure SSH keys, set up TOTP, build the React app,
|
|
install Docker + Caddy or native systemd, set up WireGuard VPN, and optionally
|
|
install Control D DNS filtering via ctrld.
|
|
|
|
The attached file IS the project. Everything is in it.
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
```
|
|
Browser (any device on VLAN 99 or WireGuard VPN)
|
|
↓ https://switch.mgmt.lan (Caddy) or http://IP:8765
|
|
FastAPI backend (Python, runs on management computer)
|
|
↓ ed25519 SSH key (never leaves management computer)
|
|
ERS 5952 switch (192.168.99.1)
|
|
```
|
|
|
|
Management computer = Raspberry Pi, HP T620 thin client, or any always-on Linux box.
|
|
Runs Docker (recommended) with Caddy for HTTPS, or native Python with systemd.
|
|
|
|
---
|
|
|
|
## Current State — What Is Built and Working
|
|
|
|
### Setup script (21 steps)
|
|
1. Writes project files from embedded strings
|
|
2. Installs system packages (picocom, qrencode, wireguard-tools)
|
|
3. Adds user to dialout group (serial port access)
|
|
4. Installs Python dependencies (fastapi, uvicorn, paramiko, pyotp)
|
|
5. Creates /etc/switch-manager/ config directory
|
|
6. Interactive static IP setter (detects interfaces, netplan or interfaces file)
|
|
7. Docker vs native Python choice — explained before asking, Docker is default
|
|
8. Console cable guidance (OS-specific, software download links)
|
|
9. SSH keypair generation (ed25519)
|
|
10. Loads public key onto switch (console step — only manual step)
|
|
11. SSH connectivity test with retry
|
|
12. Pins switch host key (MITM protection)
|
|
13. TOTP setup (qrencode or Python qrcode, terminal QR display, verification)
|
|
14. Patches backend config with switch IP/port/user
|
|
15. Builds React frontend (Vite)
|
|
16. Docker setup (official installer, docker group, compose plugin, Dockerfile,
|
|
docker-compose.yml with Caddy, Caddyfile, CA cert installation)
|
|
OR native systemd service
|
|
17-19. Start service
|
|
20. WireGuard VPN (local server fully automated, OPNsense semi-automated,
|
|
per-client QR codes and .conf files, split tunnel)
|
|
21. Control D ctrld DNS filtering (all 3 options with explanations)
|
|
|
|
### UI — 8 tabs
|
|
- **Port Map** — visual 48+4 chassis, colour-coded by VLAN, live status,
|
|
click-to-configure panel (mode/VLAN/PoE), connection banner
|
|
- **VLANs** — create/rename/delete, port counts
|
|
- **ACL Builder** — visual rule builder (action/proto/src/dst/port),
|
|
assigned to VLAN interface with direction
|
|
- **Review & Push** — annotated CLI table (every command with plain-English
|
|
explanation), batch or step-by-step push, danger check, TOTP gate,
|
|
per-command results with switch error output
|
|
- **Device Access** — MAC randomization warnings (iOS/Android/macOS/Windows),
|
|
DHCP reservation push, ACL pinhole push, live DHCP lease discovery
|
|
- **DHCP** — unified switch + OPNsense view, conflict detection (same MAC in
|
|
both places), one-click sync in 4 directions, OPNsense API integration
|
|
- **DNS Filtering** — Control D ctrld setup, all 3 options with full
|
|
explanations, per-VLAN Resolver IDs, ctrld.toml generation, DoH3 upstreams,
|
|
source-IP-based VLAN routing
|
|
- **VPN** — WireGuard peer management, add clients (QR + .conf), revoke,
|
|
live peer status, SSH tunnel documentation
|
|
|
|
### Security model
|
|
- VLAN 99 isolation — management network unreachable from other VLANs
|
|
- ed25519 SSH key on management computer only — never transmitted
|
|
- TOTP gates every push session — one code, one push batch, auto-locks
|
|
- Hard-blocked commands (removing VLAN 99, disabling SSH) refused entirely
|
|
- Warning-level commands shown for review before push
|
|
- Per-command execution — stops on first switch error
|
|
- Config saved to NVRAM only on full success
|
|
- Connection pool (25s lifetime) — respects switch idle timeout
|
|
|
|
### API — 34 endpoints
|
|
Auth: /api/auth/verify, /api/auth/check, /api/auth/revoke
|
|
Switch: /api/switch/push, /api/switch/port, /api/switch/vlan (GET/POST/DELETE),
|
|
/api/switch/acl, /api/switch/config
|
|
Live: /api/status, /api/live, /api/heartbeat, /api/check/danger
|
|
Devices: /api/devices, /api/devices/save, /api/devices/delete,
|
|
/api/devices/push-reservation, /api/devices/push-pinhole
|
|
DHCP: /api/dhcp/overview, /api/dhcp/detect-opnsense,
|
|
/api/dhcp/configure-opnsense (POST/DELETE),
|
|
/api/dhcp/push-to-opnsense, /api/dhcp/sync
|
|
WireGuard: /api/wireguard/status, /api/wireguard/clients,
|
|
/api/wireguard/add-client, /api/wireguard/revoke-client,
|
|
/api/wireguard/client-qr/{name}
|
|
ctrld: /api/ctrld/status, /api/ctrld/toml-preview, /api/ctrld/save-config,
|
|
/api/ctrld/update-profiles, /api/ctrld/uninstall
|
|
|
|
---
|
|
|
|
## What Still Needs to Be Built
|
|
|
|
### 1. Port 53 conflict resolution (IMPORTANT — blocks ctrld from working)
|
|
|
|
On Linux (Ubuntu/Debian), systemd-resolved holds port 53.
|
|
Fix: disable the stub listener, not the service itself.
|
|
|
|
```bash
|
|
# Add to /etc/systemd/resolved.conf
|
|
[Resolve]
|
|
DNSStubListener=no
|
|
# Then: sudo systemctl restart systemd-resolved
|
|
```
|
|
|
|
The setup script should detect this and fix it automatically before starting ctrld
|
|
(Option A — local install). Add to _ctrld_install_local_setup() in the logic section.
|
|
|
|
On OPNsense (FreeBSD), Unbound holds port 53.
|
|
The RIGHT approach (Option C in ctrld terms, not our Option C):
|
|
- Keep Unbound running — it handles .lan hostnames and local DNS
|
|
- Move Unbound to listen on 127.0.0.1:5353 instead of :53
|
|
- Run ctrld on :53
|
|
- Configure ctrld to forward *.lan, *.local queries to 127.0.0.1:5353
|
|
- ctrld handles all other queries via DoH3 to Control D
|
|
|
|
The ctrld.toml for this split-horizon setup:
|
|
|
|
```toml
|
|
[service]
|
|
name = "ctrld"
|
|
|
|
[[listener]]
|
|
ip = "0.0.0.0"
|
|
port = 53
|
|
tag = "all-vlans"
|
|
|
|
# Forward internal names to local Unbound
|
|
[[upstream]]
|
|
id = "local"
|
|
type = "doh" # actually plain DNS to local resolver
|
|
endpoint = "sdns://AAAA..." # or just use type="legacy" with bootstrap
|
|
tag = "local"
|
|
|
|
# Per-VLAN upstreams
|
|
[[upstream]]
|
|
id = "vlan10"
|
|
type = "doh3"
|
|
endpoint = "https://dns.controld.com/RESOLVER_ID_VLAN10"
|
|
tag = "vlan10"
|
|
|
|
# ... etc per VLAN ...
|
|
|
|
# Route internal domains to local resolver
|
|
[[rule]]
|
|
listener = "all-vlans"
|
|
match_domains = ["lan", "local", "mgmt.lan"]
|
|
upstream = "local"
|
|
|
|
# Route each VLAN to its profile
|
|
[[rule]]
|
|
listener = "all-vlans"
|
|
source_ip = "192.168.10.0/24"
|
|
upstream = "vlan10"
|
|
```
|
|
|
|
The DNS tab and setup script need to:
|
|
a) Detect systemd-resolved conflict and fix it (Linux)
|
|
b) Show OPNsense-specific instructions for moving Unbound to port 5353
|
|
c) Offer to add local hostname forwarding rules to ctrld.toml
|
|
d) Ask for the local domain (default: lan) to forward internally
|
|
|
|
### 2. DNS enforcement ACLs (IMPORTANT — without this ctrld is bypassable)
|
|
|
|
Devices can ignore DHCP-assigned DNS and use 8.8.8.8 directly.
|
|
The 5952 must block this via ACLs on each VLAN interface.
|
|
|
|
For each VLAN (except VLAN 99):
|
|
```
|
|
ip access-list extended DNS-ENFORCE-VLAN10
|
|
permit udp 192.168.10.0 0.0.0.255 host [ctrld-ip] eq 53
|
|
permit tcp 192.168.10.0 0.0.0.255 host [ctrld-ip] eq 53
|
|
deny udp 192.168.10.0 0.0.0.255 any eq 53
|
|
deny tcp 192.168.10.0 0.0.0.255 any eq 53
|
|
deny tcp 192.168.10.0 0.0.0.255 any eq 853
|
|
permit ip any any
|
|
interface vlan 10
|
|
ip access-group DNS-ENFORCE-VLAN10 in
|
|
```
|
|
|
|
The DNS tab should have a "Enforce DNS on Switch" button that:
|
|
- Takes the ctrld IP (from status or manual entry)
|
|
- Generates these ACLs for all configured VLANs
|
|
- Shows them for review
|
|
- Pushes via existing TOTP-gated push mechanism
|
|
|
|
### 3. Inter-VLAN routing ACL templates
|
|
|
|
Pre-built templates in the ACL Builder tab. Currently the ACL builder
|
|
is blank — user builds rules manually. Add a "Templates" section:
|
|
|
|
Templates to build (parameterised by VLAN subnets):
|
|
|
|
**Staff VLAN (full access except management):**
|
|
```
|
|
deny ip [staff-subnet] 192.168.99.0/24 ← no management access
|
|
permit ip any any ← everything else allowed
|
|
```
|
|
|
|
**IoT VLAN (internet only):**
|
|
```
|
|
deny ip [iot-subnet] 192.168.0.0/16 ← no RFC1918
|
|
deny ip [iot-subnet] 10.0.0.0/8
|
|
deny ip [iot-subnet] 172.16.0.0/12
|
|
permit ip any any ← internet allowed
|
|
```
|
|
|
|
**Guest VLAN (internet only, stricter):**
|
|
```
|
|
permit udp [guest-subnet] host [ctrld-ip] eq 53 ← DNS must work first
|
|
deny ip [guest-subnet] 192.168.0.0/16
|
|
deny ip [guest-subnet] 10.0.0.0/8
|
|
deny ip [guest-subnet] 172.16.0.0/12
|
|
permit ip any any
|
|
```
|
|
|
|
**Camera VLAN (NVR only):**
|
|
```
|
|
permit ip [camera-subnet] host [nvr-ip] ← only NVR
|
|
deny ip [camera-subnet] any ← nothing else
|
|
```
|
|
|
|
**DNS enforcement (per VLAN, as above)**
|
|
|
|
The UI should show a "Templates" dropdown in the ACL Builder that
|
|
pre-fills the rule table for common patterns, then lets the user
|
|
adjust IPs before pushing.
|
|
|
|
### 4. Local hostname resolution (optional but useful)
|
|
|
|
Add dnsmasq as an optional Docker service for .lan resolution.
|
|
Minimal config:
|
|
|
|
```
|
|
# docker-compose.yml addition
|
|
dnsmasq:
|
|
image: andyshinn/dnsmasq:latest
|
|
ports:
|
|
- "5353:53/udp"
|
|
- "5353:53/tcp"
|
|
volumes:
|
|
- ./dnsmasq.conf:/etc/dnsmasq.conf
|
|
restart: unless-stopped
|
|
cap_add:
|
|
- NET_ADMIN
|
|
```
|
|
|
|
```
|
|
# dnsmasq.conf (generated by setup)
|
|
address=/switch.mgmt.lan/192.168.99.50
|
|
address=/management.lan/192.168.99.50
|
|
# Add more as needed
|
|
```
|
|
|
|
ctrld.toml then forwards *.lan to 127.0.0.1:5353 (dnsmasq).
|
|
|
|
The DNS tab should have a "Local Hostnames" section where the user
|
|
adds name → IP mappings. The site generates the dnsmasq.conf and
|
|
updates ctrld.toml split-horizon rules.
|
|
|
|
### 5. Comments pass
|
|
|
|
The backend (BACKEND_SRC) needs docstrings and inline comments.
|
|
Every function needs a one-line docstring. Non-obvious ACL patterns,
|
|
the connection pool logic, session management, and the ctrld.toml
|
|
builder all need explanatory comments. Currently comment density
|
|
is inconsistent.
|
|
|
|
### 6. README updates needed
|
|
|
|
Add sections for:
|
|
- Port 53 conflict resolution (systemd-resolved and OPNsense/Unbound)
|
|
- ctrld on OPNsense vs management computer — tradeoffs
|
|
- DNS enforcement ACLs — why they matter, what they do
|
|
- Inter-VLAN routing — the ACL templates, what each VLAN can reach
|
|
- Local hostname resolution — the dnsmasq option
|
|
- ACL templates in the UI
|
|
|
|
---
|
|
|
|
## Key Files and Paths
|
|
|
|
All sensitive files in /etc/switch-manager/ (chmod 600 where appropriate):
|
|
- ers5952_key — SSH private key
|
|
- ers5952_key.pub — SSH public key (loaded on switch)
|
|
- known_hosts — pinned switch host key
|
|
- totp_secret — TOTP seed (BACK THIS UP)
|
|
- devices.json — registered devices
|
|
- opnsense.json — OPNsense API credentials
|
|
- ctrld.json — Control D / ctrld config
|
|
- wg_server_private — WireGuard server private key
|
|
- wg_server_public — WireGuard server public key
|
|
- clients/ — WireGuard client .conf files
|
|
- fqdn — Caddy hostname (e.g. switch.mgmt.lan)
|
|
|
|
Project folder (same dir as Avaya_5952_setup.py):
|
|
- switch_backend.py — FastAPI backend (written by setup)
|
|
- ers5952-manager.jsx — React source (written by setup)
|
|
- README.md — this README (written by setup)
|
|
- Dockerfile — Docker image
|
|
- docker-compose.yml — Caddy + switch-manager services
|
|
- Caddyfile — HTTPS reverse proxy config
|
|
- ctrld.toml — ctrld per-VLAN config (if configured)
|
|
- frontend/dist/ — built React app
|
|
|
|
---
|
|
|
|
## How to Extract/Rebuild the Embedded Files
|
|
|
|
```python
|
|
# Extract any embedded constant from Avaya_5952_setup.py
|
|
def extract(src, name):
|
|
idx = src.find(f'\n{name} = ')
|
|
start = idx + len(f'\n{name} = ')
|
|
qc = src[start]
|
|
end = start + 1
|
|
while end < len(src):
|
|
if src[end] == qc and src[end-1] != '\\': end += 1; break
|
|
end += 1
|
|
return eval(src[start:end])
|
|
|
|
setup = open('Avaya_5952_setup.py').read()
|
|
backend = extract(setup, 'BACKEND_SRC')
|
|
jsx = extract(setup, 'JSX_SRC')
|
|
readme = extract(setup, 'README_SRC')
|
|
|
|
# Rebuild after modifying
|
|
header_end = setup.find('\nBACKEND_SRC = ')
|
|
logic_start = setup.find('\n# ═══\n# SETUP STEPS')
|
|
header = setup[:header_end]
|
|
logic = setup[logic_start:]
|
|
|
|
new_file = (
|
|
header
|
|
+ '\nBACKEND_SRC = ' + repr(backend) + '\n\n'
|
|
+ 'JSX_SRC = ' + repr(jsx) + '\n\n'
|
|
+ 'README_SRC = ' + repr(readme) + '\n'
|
|
+ logic
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## Design Principles (carry these forward)
|
|
|
|
1. **Never write CLI** — user clicks, tool generates, user inspects, user pushes
|
|
2. **Explain before asking** — every choice is explained before the prompt
|
|
3. **One file** — Avaya_5952_setup.py contains everything, no other files to copy
|
|
4. **Stop on first error** — per-command execution, never push blindly
|
|
5. **Hard block lethal commands** — removing VLAN 99, disabling SSH = refused, run at console
|
|
6. **Session-scoped auth** — TOTP unlocks one push batch, auto-locks after
|
|
7. **Self-healing** — per-poll SSH connections, connection pool with liveness check
|
|
8. **Docker first** — explained as recommended, bare metal available with reasons
|
|
9. **No OPNsense creep** — OPNsense integration is optional and additive, never required
|
|
10. **Pi references** — always "management computer (Raspberry Pi, HP T620 thin client,
|
|
old computer or laptop)" on first mention, then just "management computer"
|
|
|
|
---
|
|
|
|
## Known Issues / Gotchas
|
|
|
|
- The ctrld.toml "local" upstream type needs verification against actual ctrld docs
|
|
for plain DNS forwarding syntax. Use `type = "legacy"` with `bootstrap_ip` for
|
|
forwarding to a plain DNS server (Unbound/dnsmasq on localhost).
|
|
|
|
- WireGuard `wg syncconf` command in the add-client endpoint may not work on all
|
|
systems. Fallback: `systemctl reload wg-quick@wg0`. Both should be tried.
|
|
|
|
- The OPNsense DHCP API endpoint changed between OPNsense versions. The current
|
|
code uses `dhcpv4/leases/searchReservation` which is correct for OPNsense 23+.
|
|
Older versions use a different path.
|
|
|
|
- Docker compose file uses named volumes for Caddy data. The Caddy CA cert
|
|
extraction in _install_caddy_ca() uses `docker exec` with a glob for the
|
|
container name which may fail if the compose project name differs. Should use
|
|
`docker compose exec caddy` instead.
|
|
|
|
- The ctrld binary path `/usr/local/bin/ctrld` is the Linux default. On OPNsense
|
|
(FreeBSD) it installs to a different path. The backend CTRLD_BIN constant
|
|
should be detected at runtime, not hardcoded.
|