MEMORY.md 7.5 KB

client2server — Project Memory

Bi-directional event forwarder: OpenWrt routers → Go server → Redpanda (Kafka) → 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 + HTTP (unified port) via Caddy LB
WebSocket lib github.com/coder/websocket v1.8.13 (fork of nhooyr.io)
Event bus Redpanda (Kafka-compatible) :9092
Kafka client github.com/twmb/franz-go v1.18.0 + pkg/kadm v1.14.0
Server Go 1.22

Repo Layout (current)

client2server/
├── ARCHITECTURE.md      # Mermaid diagrams, full spec
├── README.md            # User-facing docs
├── MEMORY.md            # ← you are here
├── Caddyfile            # LB + reverse proxy (single port :3843)
├── 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 + HTTP API + Redpanda producer
    ├── go.mod
    ├── go.sum
    └── Dockerfile       # Go 1.22 builder + alpine runtime

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

Commands (server → router)

Command Args
uci_set config, section, option, value
shell command
reboot
wifi_restart
status

Ports

Service Port Notes
Caddy LB (WS + HTTP) 3843 Routers connect here, API served here too
Go server (×2) 3843 (internal) Behind Caddy, not directly exposed
Redpanda Kafka 9092 Internal Docker network
Redpanda REST 8082 Schema/management
Redpanda Schema 8081 Schema registry

HTTP API

All endpoints on :3843, behind Caddy LB.

Method Path Auth Purpose
GET /health none Liveness + router stats
POST /api/events Bearer Event ingestion (hotplug + lua)
GET /api/routers none (recommended: add Bearer) List known routers
POST /api/command Bearer Send command to router; awaits result
GET /ws query ?token= or Authorization: Bearer WebSocket upgrade

Quick Run

# Full stack
cd /root/.openclaw/workspace/client2server
TOKEN=*** docker-compose up -d

# Server only (Go 1.22+ required)
cd server && go build -o server . && \
  REDPANDA_BROKERS=localhost:9092 TOKEN=*** PORT=3843 ./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.

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()

Server Architecture (Go)

  • Single main.go (~600 lines) — WS handler, HTTP handlers, Redpanda producer
  • In-memory state: routers, routerQueues (per-router offline queue), pendingCmds (awaiting result), executedCmds (idempotency)
  • Background janitor: cleans executedCmds after idempotencyTTL (5 min)
  • Publish timeout: 3s per kcl.ProduceSync() call — if Redpanda is down, HTTP returns 502 instead of hanging
  • WebSocket auth: ?token= query param or Authorization: Bearer header
  • HTTP API auth: Authorization: Bearer or raw token
  • Single port (3843) — mux routes /ws to WS handler, /api/* to REST, /health to liveness

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.

The Go server went through a dependency overhaul on 2026-06-09:

  • nhooyr.io/websocketgithub.com/coder/websocket (nhooyr archived, coder is the maintained fork; same API)
  • github.com/redpanda-data/redpanda-sdk-go (which never existed as a public module) → github.com/twmb/franz-go + kadm (real, fast, pure Go Kafka client — Redpanda speaks Kafka wire protocol natively)
  • Unified WebSocket and HTTP on port 3843 (Caddy handles routing)

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
  • ⚠️ /api/routers currently has no auth — should require Bearer (intentional for monitoring, but flag it)
  • unified.lua is 759 lines — worth splitting into modules (DHCP/WiFi/WAN/WS/CMD) but functional as-is
  • No automated tests for the Go server; manual smoke test confirms endpoints work (build, /health 200, /api/events 401/502, /api/command queues while offline)
  • Redpanda topic creation is best-effort (relies on AUTO_CREATE_TOPICS=true in dev); production should manage topics explicitly
  • command_id idempotency is server-side only — clients should pass id in RouterCommand for replay safety

Author

Luis Rosales — MIT License 2026