# DECT Relay Agent Bridges the CollabSupport bot (public cloud) to Cisco DBS-210 DECT base stations on the private `10.0.0.0/8` corporate network. ## Why it exists The bot process runs in the public cloud and can't reach `10.x`. This agent runs inside the data center, dials outbound over WSS to the bot, and executes any DECT command (collect status, reboot, factory-reset, etc.) the bot pushes to it. Only one agent is expected to run at a time. If a second agent connects, the bot assumes it's a legitimate restart, closes the old socket, and adopts the new one. ## Prerequisites - Node.js ≥ 20 - Route from the agent host to `10.0.0.0/8` on TCP 443 - Route from the agent host to the bot's public HTTPS endpoint - The DECT serviceability password (Control Hub → Calling → Features → DECT Networks → Manage → Manage DECT serviceability password) ## Deploy paths There are two ways to run this. Pick one based on where you're deploying. ### 1. Docker container in the data center (recommended for production) The DC host makes **zero network calls** during install — the image is built on your dev machine, saved as a tarball, and shipped inside a self-contained ZIP. This sidesteps the TLS-interception problem that breaks `apk add` and `npm install` inside containers on corporate networks. **On your dev machine (with Docker Desktop / internet access):** ```bash # From the repo root — script is self-locating: ./dect-relay-agent/bundle.sh # → writes dect-relay-agent-bundle-.zip (~40-60MB) ``` Optional overrides: ```bash ./dect-relay-agent/bundle.sh --tag 0.2.0 # override version ./dect-relay-agent/bundle.sh --platform linux/arm64 # if the DC is ARM ``` **On the DC host** (once you've transferred the ZIP): ```bash unzip dect-relay-agent-bundle-*.zip cd dect-relay-agent-bundle-* cp .env.example .env $EDITOR .env # set BOT_URL + AGENT_TOKEN + ADMIN_PASSWORD ./install.sh ``` `install.sh` is idempotent — re-run it after transferring a newer bundle to upgrade. It: 1. `docker load`s the image tarball 2. Pins the loaded tag into `.env` (so compose never falls back to a stale local image) 3. Validates required env values are set (not still placeholder strings) 4. `docker compose up -d` 5. Tails the last 40 log lines so you can see the "Connected — sending hello" message **What the runtime container looks like:** - Non-root `node` user (uid 1000) - Read-only root filesystem, 16MB tmpfs at `/tmp` - All Linux capabilities dropped, `no-new-privileges` - Host networking (so it can reach `10.x/8` without userland proxy translation) - No listening ports — outbound-only WSS to the bot - Log rotation: 10MB × 5 files max ### 2. Direct node process (dev + local iteration) ```bash cd dect-relay-agent npm install ``` `ws`, `axios`, and `dotenv` are the only runtime dependencies. The agent imports the shared `integrations/cisco-dect/` modules from the parent repo via relative paths, so the parent workspace must be present on disk. ## Configure ```bash cp .env.example .env $EDITOR .env ``` Required values: | Var | Meaning | |---|---| | `DECT_RELAY_BOT_URL` | Full WSS URL to the bot's DECT relay endpoint (`wss://your-bot-host/dect-relay/ws`) | | `DECT_RELAY_AGENT_TOKEN` | Shared bearer token — MUST match the bot's `DECT_RELAY_AGENT_TOKEN` exactly | | `DECT_ADMIN_USER` | Usually `admin` | | `DECT_ADMIN_PASSWORD` | Fleet-wide serviceability password | Generate a fresh token: `openssl rand -hex 32`. Rotate on both sides at once — the bot compares tokens with `timingSafeEqual` and will reject any drift with a 401 on the WSS upgrade. ## Run ```bash npm start ``` You should see: ``` [startup] dect-relay-agent v0.1.0 — hostname=..., bot=wss://... [connect] Dialing wss://.../dect-relay/ws [connect] Connected — sending hello ``` And on the bot side: ``` [dect:relay-hub] Agent connected from ... [dect:relay-hub] Agent hello: version=0.1.0 host=... caps=collect,reboot,... ``` ## Wire protocol All frames are JSON, one per WebSocket message. **Agent → Bot on connect:** ```json { "type": "hello", "agentVersion": "0.1.0", "hostname": "dc-dect-relay-01", "capabilities": ["collect","collect-raw","reboot","force-reboot","reboot-chain", "force-reboot-chain","factory-reset","reconfigure-tree"] } ``` **Bot → Agent (collect with raw XML for fixture capture):** ```json { "id": "cmd_", "type": "collect", "baseIp": "10.4.11.87", "includeRaw": true } ``` Legacy agents may also accept `{ "type": "collect-raw", "baseIp": "..." }`. **Agent → Bot (collect + includeRaw reply):** ```json { "id": "cmd_", "ok": true, "elapsedMs": 812, "result": { "rawXml": "...", "byteLength": 12345, "sectionInventory": [...], "parsed": { ... }, "verdict": { ... } } } ``` **Capture script (from repo root, bot running with relay connected):** ```bash node scripts/fetchDectStatusXml.js 782 --save-dir tests/fixtures/dect/ # Optional: --base 10.4.11.87 --url https://your-bot-host/CollabSupport ``` **Bot → Agent (command):** ```json { "id": "cmd_", "type": "collect", "baseIp": "10.4.11.87" } { "id": "cmd_", "type": "reboot", "baseIp": "10.4.11.87" } ``` **Agent → Bot (reply):** ```json { "id": "cmd_", "ok": true, "elapsedMs": 812, "result": { "parsed": { ... }, "verdict": { "healthy": true, ... } } } { "id": "cmd_", "ok": false, "error": { "code": "DIGEST_401", "message": "Base rejected credentials" } } ``` **Heartbeat (both directions, every 30s):** ```json { "type": "ping", "at": 1720000000000 } { "type": "pong", "at": 1720000000000 } ``` The bot terminates the socket if no `pong` arrives within 90s; the agent auto-reconnects with exponential backoff (1s / 2s / 4s / … capped at 30s + 0-1000ms jitter). ## Safety guarantees - DECT admin credentials NEVER leave this agent. The bot only knows the WSS bearer token. - All mutating actions (reboot, factory-reset, reconfigure-tree) are only executed when the bot explicitly issues the corresponding command frame. The agent has no autonomous logic. - The agent enforces no policy — the bot decides who can reboot what. See the bot's audit log for the full record of actions taken (`igmp:audit` style scopes in daily log files). - The agent quarantines mutating actions from probes via the exact same safety model as the CLI tool (`integrations/cisco-dect/probes.js` — GET-triggered actions are only reachable via explicit `trigger*` helpers, never via a generic path fetcher). ## What's in this folder | File | Purpose | |---|---| | `index.js` | Agent entrypoint — WSS client + command dispatcher | | `package.json` | Deps: `ws`, `axios`, `dotenv` | | `.env.example` | Annotated env template | | `README.md` | This file | | `Dockerfile` | Multi-stage Alpine build. No `apk add`, no runtime `npm install`. | | `Dockerfile.dockerignore` | Per-Dockerfile ignore (BuildKit ≥ 23.0). Whitelist-based; keeps build context ~50KB. | | `docker-compose.yml` | DC-side runtime shape (read-only rootfs, host net, capability drop, log rotation). | | `bundle.sh` | Dev-machine packager: `docker build` → `docker save` → `zip`. Runs on your machine. | | `install.sh` | DC-host installer: `docker load` → validate `.env` → `docker compose up -d`. Ships inside the bundle. | **Important**: the `Dockerfile` is designed to be built from the **repo root**, not from this folder, because it needs `../integrations/cisco-dect/*` and `../utils/httpDigestAuth.js` in the build context. `bundle.sh` does this correctly: ```bash docker build -f dect-relay-agent/Dockerfile -t ... . # note the trailing `.` ``` Building with `docker build dect-relay-agent/` will fail (missing shared modules) — always use `bundle.sh`, or invoke `docker build` from the repo root with `-f dect-relay-agent/Dockerfile`. ## Troubleshooting **`WARNING: fetching … TLS: server certificate not trusted` during build** You're building inside a TLS-intercepting corporate network. Don't — build on your dev machine and ship the tarball via `bundle.sh`. That's the whole point of this workflow. **`docker: permission denied while trying to connect to the Docker daemon socket`** The user running `install.sh` needs to be in the `docker` group. Either `sudo usermod -aG docker $USER` (log out/in after) or `sudo ./install.sh`. **Agent connects then immediately disconnects with 401** Bearer token mismatch. `DECT_RELAY_AGENT_TOKEN` on the bot side must match the agent's `.env` exactly. Rotate both together. **Agent connects but every `collect` returns `DIGEST_401`** DBS-210 admin password is wrong. Verify in Control Hub → Calling → Features → DECT Networks → Manage → Manage DECT serviceability password, update `DECT_ADMIN_PASSWORD` in `.env`, then `docker compose restart dect-relay-agent`.