# client2server — Project Memory > Bi-directional event forwarder: OpenWrt routers → Go server → Redpanda → LuIS backend. ## Purpose - Routers push events (DHCP leases, WiFi connects/disconnects, WAN state) to a central server. - Two delivery paths: WebSocket long-lived connection for command roundtrips, and direct HTTP POST for hotplug-triggered events. - Server fans out via Redpanda topics; LuIS backend consumes. - Server can also push commands back to routers (uci_set, shell, reboot, wifi_restart, status). ## Stack | Layer | Tech | |-------|------| | Router client | Lua (OpenWrt), `client2server-unified.lua` | | Hotplug paths | Shell + curl, `/etc/hotplug.d/{wireless,dhcp}/*` | | Transport | WebSocket :3843 (Caddy LB → 2× Go servers) | | API | HTTP :3844 (Caddy → server1) | | Event bus | Redpanda (Kafka-compatible) :9092 | | Server | Go (`nhooyr.io/websocket`, `redpanda-data/redpanda-sdk-go`) | ## Repo Layout (current) ``` client2server/ ├── ARCHITECTURE.md # Mermaid diagrams, full spec ├── README.md # User-facing docs ├── MEMORY.md # ← you are here ├── Caddyfile # LB + reverse proxy ├── docker-compose.yml # redpanda + 2× server + caddy ├── package/ │ ├── Makefile # IPK build (includes hotplug) │ ├── src/ │ │ └── client2server-unified.lua # CANONICAL Lua client │ ├── files/ │ │ ├── etc/config/client2server # UCI defaults │ │ └── etc/init.d/client2server # Procd init script (exports UCI → env) │ └── hotplug/ │ ├── 01-wifi # wireless hotplug → wifi_connected/disconnected │ └── 02-dhcp # dhcp hotplug → dhcp_lease_new/expire └── server/ ├── main.go # WS handler + Redpanda producer/consumer ├── go.mod └── Dockerfile ``` **Removed in 2026-06-09 cleanup:** `package/src/{minimal,ws}.lua`, legacy `etc/` and `usr/` (predecessor `event-forwarder`), `server/{index,server-ws}.js` (Node fallback). All in git history if needed. ## Event Types (router → server) | Event | Source | Payload | |-------|--------|---------| | `dhcp_lease_new` | dnsmasq (luv timer + hotplug) | mac, ip, hostname | | `dhcp_lease_expire` | dnsmasq (luv timer + hotplug) | mac, old_ip | | `wan_link_up` | /sys/class/net/* | device | | `wan_link_down` | /sys/class/net/* | device | | `wan_dhcp_new` | ubus | new_ip | | `wan_dhcp_changed` | ubus | old_ip, new_ip | | `wifi_connected` | hostapd hotplug | mac, interface | | `wifi_disconnected` | hostapd hotplug | mac, interface | ✅ **All event names are now consistent** (hotplug scripts renamed to match unified.lua vocabulary on 2026-06-09). ## Commands (server → router) | Command | Args | |---------|------| | `uci_set` | config, section, option, value | | `shell` | command | | `reboot` | — | | `wifi_restart` | — | | `status` | — | ## Ports | Service | Port | Notes | |---------|------|-------| | WebSocket LB | 3843 | Routers connect here | | HTTP API | 3844 | REST + health | | Redpanda Kafka | 9092 | Internal | | Redpanda REST | 8082 | Schema/management | | Redpanda Schema | 8081 | Schema registry | ## Quick Run ```bash # Full stack cd /root/.openclaw/workspace/client2server TOKEN=*** docker-compose up -d # Server only cd server && go build -o server . && \ REDPANDA_BROKERS=localhost:9092 TOKEN=*** ./server # Install on router (manual) scp package/src/client2server-unified.lua root@router:/usr/sbin/ scp package/files/etc/init.d/client2server root@router:/etc/init.d/ scp package/files/etc/config/client2server root@router:/etc/config/ scp package/hotplug/01-wifi root@router:/etc/hotplug.d/wireless/ scp package/hotplug/02-dhcp root@router:/etc/hotplug.d/dhcp/ ssh root@router "chmod +x /usr/sbin/client2server-unified.lua /etc/init.d/client2server /etc/hotplug.d/wireless/01-wifi /etc/hotplug.d/dhcp/02-dhcp" ssh root@router "/etc/init.d/client2server enable && /etc/init.d/client2server start" # Or build IPK make package/client2server-unified/ipk ``` ## Architecture: Hybrid Event Delivery The router has **two parallel event paths** to the server: 1. **Hotplug path (instant)** — kernel fires, shell runs, curl POSTs - `01-wifi` for WiFi connect/disconnect - `02-dhcp` for DHCP lease add/del - Latency: ~10ms 2. **Lua state-diff path (≤1s)** — luv async loop polls state, sends diffs - SSID name changes - `wan_link_up/down` - `wan_dhcp_new/changed` - `dhcp_lease_new/expire` (fallback / redundancy with hotplug) Both paths post to the same `POST /api/events` endpoint, so server-side sees one event stream. ## Environment Wiring (init.d → hotplug) - `init.d/client2server` reads UCI on `start()` and writes `/var/run/client2server.env` - Exports `SERVER_URL`, `TOKEN`, `ROUTER_ID` to Lua's environment - Hotplug scripts also `uci get` directly as fallback (in case called outside init.d context) - Env file removed on `stop()` ## Client Evolution The Lua client went through ~6 architectural rewrites in 2 days (Jun 7 2026) trying to kill polling. Final settled state = hybrid hotplug + luv state-diff. See git log `9e248cd..0b6d8ff` for the iteration history. ## Known Issues / TODO - ⚠️ Caddyfile email is commented out — set real value if enabling `auto_https` - ⚠️ UCI default `wss://your-server.com/ws` is a placeholder — must be edited per-deployment - ⚠️ Docker-compose TOKEN is the literal string `***` — override via env or `.env` file - `unified.lua` is 759 lines — worth splitting into modules (DHCP/WiFi/WAN/WS/CMD) but functional as-is - No tests for the Go server; would benefit from integration tests using a mock WebSocket client - `command_id` idempotency + per-router command queue are server-side features (per commits 4134845, 7c85b33) — verify they work end-to-end ## Author Luis Rosales — MIT License 2026