Wire CP-78xx probe discovery, relay phone-probe commands, and a chat follow-up message so store desk phones get registration, switch, and provisioning detail alongside DECT and WAN diagnostics. Co-authored-by: Cursor <cursoragent@cursor.com>
246 lines
10 KiB
Markdown
246 lines
10 KiB
Markdown
# 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-<YYYYMMDD-HHMMSS>.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_<uuid>", "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_<uuid>", "ok": true, "elapsedMs": 812,
|
||
"result": { "rawXml": "<Status>...</Status>", "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_<uuid>", "type": "collect", "baseIp": "10.4.11.87" }
|
||
{ "id": "cmd_<uuid>", "type": "reboot", "baseIp": "10.4.11.87" }
|
||
```
|
||
|
||
**Agent → Bot (reply):**
|
||
```json
|
||
{ "id": "cmd_<uuid>", "ok": true, "elapsedMs": 812,
|
||
"result": { "parsed": { ... }, "verdict": { "healthy": true, ... } } }
|
||
|
||
{ "id": "cmd_<uuid>", "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).
|
||
|
||
## MPP desk phones (CP-78xx, Webex Calling)
|
||
|
||
The same relay agent can probe Cisco MPP desk phones registered directly with Webex. Phones use **HTTPS on 443** with a self-signed certificate (the agent accepts untrusted TLS, same as DBS-210).
|
||
|
||
### Manual prerequisite (per phone)
|
||
|
||
Before `phone-probe` can return data:
|
||
|
||
1. On the phone: **Settings → Security → Web Access** — enable **Web Server** (Admin Access optional for read-only probes).
|
||
2. From a host on `10.x`, verify JSON is reachable without credentials (typical for Webex-provisioned MPP):
|
||
```bash
|
||
curl -k https://<phone_ip>/Status.json
|
||
```
|
||
3. **No admin password is required** for read-only status on Webex Calling MPP phones — `/Status.json` is served by the user web UI. Set `PHONE_ADMIN_PASSWORD` only if your site locks down admin paths (`/admin/*`) and you have a local password.
|
||
|
||
Lab store for fixture capture: **782**.
|
||
|
||
### Relay commands
|
||
|
||
**Bot → Agent (phone probe):**
|
||
```json
|
||
{ "id": "cmd_<uuid>", "type": "phone-probe", "targetIp": "10.4.11.50" }
|
||
{ "id": "cmd_<uuid>", "type": "phone-probe-raw", "targetIp": "10.4.11.50" }
|
||
```
|
||
|
||
`phone-probe-raw` includes full response bodies for fixture capture. `baseIp` is accepted as an alias for `targetIp`.
|
||
|
||
**Capture script (from repo root, bot + relay running):**
|
||
```bash
|
||
node scripts/probePhone.js raw 782 --save-dir tests/fixtures/mpp/
|
||
```
|
||
|
||
Or via HTTP API: `GET /api/phone/probe/782?ip=10.x.x.x`
|
||
|
||
## 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`.
|