# client2server > **Bi-directional event forwarder + Apple-style dashboard for OpenWrt router fleets** [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) OpenWrt event forwarder with a real-time web dashboard, command queue, and persistent history. Routers stream DHCP, WiFi, and WAN events over WebSocket; the Go server fans them out to Redpanda and persists them to SQLite; a React/TypeScript SPA gives you live visibility and command control from any browser. ## Overview ``` ┌────────────────────────────────────────────────────────────────┐ │ client2server Architecture │ ├────────────────────────────────────────────────────────────────┤ │ │ │ OpenWrt Routers │ │ │ WebSocket :3843 + Hotplug (shared buffer) │ │ ▼ │ │ ┌──────────────┐ │ │ │ Caddy LB │ ────────────────┐ │ │ │ :80/443 UI │ :3843 API/WS │ │ │ └──────┬───────┘ └──────┬────────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Dashboard │ │ server1 │ │ server2 │ │ │ │ (React) │ │ (Go) │ │ (Go) │ │ │ │ /login │ │ SQLite │ │ SQLite │ │ │ │ /routers │ │ JWT │ │ JWT │ │ │ │ /events │ │ SSE │ │ SSE │ │ │ │ /commands │ │ WebSocket │ │ WebSocket │ │ │ │ /alerts │ └──────┬──────┘ └──────┬──────┘ │ │ └─────────────┘ │ │ │ │ └────────┬─────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────┐ │ │ │ Redpanda │ Kafka protocol │ │ │ :9092 │ Persisted │ │ └─────────────────┘ │ │ │ └────────────────────────────────────────────────────────────────┘ ``` ## Features ### Server (Go) - ✅ **WebSocket** connection with auto-reconnect from routers - ✅ **HTTP POST** fallback for hotplug scripts - ✅ **Redpanda** (Kafka) event fan-out for downstream consumers - ✅ **SQLite** persistence (events, commands, alerts, users) - ✅ **JWT auth** with role-based access (system_admin / project_admin / user) - ✅ **Server-Sent Events** for real-time dashboard feed - ✅ **Time-series metrics** (1-min buckets, 24h retention) - ✅ **Alerts** for offline routers (auto-clear on reconnect) - ✅ **Command queue** for offline routers (auto-flush on reconnect) - ✅ **Idempotency** (5-min TTL on command IDs) - ✅ **Load balanced** behind Caddy (2× Go servers, health checks) ### Router client (Lua) - ✅ **Hotplug-driven** WiFi connect/disconnect + DHCP lease events (instant, no polling) - ✅ **luv-based** async state-diff for WAN link, DHCP renew, IP changes (≤1s latency) - ✅ **UCI-configurable** server URL, token, check intervals - ✅ **Unified event buffer** (`/var/run/client2server/buffer`) — shared by hotplug + Lua, survives internet outages - ✅ **Wake-file signaling** — hotplug events nudge the agent for ~1s latency instead of waiting for the 30s flush cycle - ✅ **Bidirectional** command execution (uci_set, shell, reboot, wifi_restart, status) ### Dashboard (React) - ✅ **Apple-style** dark theme (frosted glass surfaces, gradient hero, tabular stat numbers) - ✅ **Live event feed** via Server-Sent Events - ✅ **Real-time charts** of events and commands over time - ✅ **Router list** with online/offline status and queued command count - ✅ **Command console** with target router picker and one-click reboot/wifi_restart/status - ✅ **Alerts inbox** with one-click acknowledgement - ✅ **Event history** with router-id and event-type filters ## Quick Start ### Option 1: Docker Compose (Recommended) ```bash # Clone and run git clone https://git3.techno-world.net/lrosales/client2server.git cd client2server # Set secrets (use any 32+ byte strings) cat > .env <" \ -d '{"router_id":"router1","command":"reboot"}' # UCI set curl -X POST http://localhost:3843/api/command \ -H "Authorization: Bearer " \ -d '{"router_id":"router1","command":"uci_set","args":{"config":"network","section":"lan","option":"ipaddr","value":"192.168.1.1"}}' # Shell curl -X POST http://localhost:3843/api/command \ -H "Authorization: Bearer " \ -d '{"router_id":"router1","command":"shell","args":{"command":"uptime"}}' ``` The token can be either the legacy `TOKEN` shared secret (used by routers and hotplug scripts) or a JWT obtained from `POST /api/auth/login` (used by the dashboard). The router doesn't need a JWT — it sends the shared `TOKEN` directly. ### Available Commands | Command | Description | Arguments | Dangerous | |---------|-------------|-----------|-----------| | `reboot` | Reboot router | — | ⚠️ | | `wifi_restart` | Restart WiFi | — | | | `status` | Get router status | — | | | `shell` | Run shell command | `command` | ⚠️ | | `uci_set` | Set UCI config value | `config,section,option,value` | ⚠️ | ## Events Router sends these events to server (via the shared on-disk buffer): | Event | Source | Payload | |-------|--------|---------| | `dhcp_lease_new` | dnsmasq (luv + hotplug) | `mac`, `ip`, `hostname` | | `dhcp_lease_expire` | dnsmasq (luv + 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` | ## Offline Behavior Both event paths (hotplug scripts + Lua agent) share a single on-disk buffer at `/var/run/client2server/buffer` (NDJSON, max 1000 events). When the internet is down: 1. Hotplug scripts append events to the buffer + touch a wake file. 2. The Lua agent polls the wake file on every loop iteration (~1s). 3. The Lua agent tries to flush on reconnect; on failure, events stay in the buffer and reconnect logic kicks in (30s→5min backoff, 10s when buffer >80% full). 4. When the buffer overflows, the **oldest** events are dropped (FIFO). The buffer survives agent restarts and internet outages; it does NOT survive router reboots (it's in tmpfs), which is acceptable since network events from a rebooting router are stale anyway. ## Project Structure ``` client2server/ ├── ARCHITECTURE.md # Architecture docs ├── README.md # This file ├── MEMORY.md # Project memory (AI/agent context) ├── Caddyfile # Caddy load balancer ├── docker-compose.yml # Full stack ├── package/ # OpenWrt IPK build │ ├── Makefile │ ├── src/ │ │ └── client2server-unified.lua # Router script (canonical) │ ├── files/ │ │ ├── etc/init.d/client2server │ │ └── etc/config/client2server │ └── hotplug/ │ ├── 01-wifi # /etc/hotplug.d/wireless/ - instant WiFi events │ ├── 02-dhcp # /etc/hotplug.d/dhcp/ - instant DHCP events │ └── _lib.sh # /usr/share/client2server/hotplug-lib.sh (shared buffer/spool) ├── server/ # Go server │ ├── main.go # WS handler + HTTP API + ingestEvent │ ├── auth.go # JWT + scrypt + login + middleware │ ├── consumer.go # Redpanda → SQLite consumer │ ├── metrics.go # 1-min buckets, 24h retention │ ├── sse.go # SSE broadcaster │ ├── store.go # SQLite (users, events, commands, alerts) │ ├── go.mod │ └── Dockerfile └── dashboard/ # React SPA (Apple-style) ├── src/ │ ├── App.tsx # Routes + auth gate │ ├── styles.css # Apple design tokens │ ├── lib/{api,types,sse}.ts │ ├── components/{Shell,ui}.tsx │ └── pages/{Login,Overview,Routers,Events,Commands,Alerts}.tsx ├── screenshots/ # Visual reference (6 retina PNGs) ├── Dockerfile # Node builder + Caddy runtime └── Caddyfile.production # /api + /ws proxy, SPA fallback ``` ## Building IPK ```bash # With OpenWrt SDK ./scripts/feeds update -a ./scripts/feeds install client2server-unified make package/client2server-unified/compile make package/client2server-unified/ipk # Output # bin/packages/*/client2server-unified_*.ipk ``` ## Building Dashboard ```bash cd dashboard npm install npm run build # → dist/ (static files) # Or development server: VITE_API_TARGET=http://localhost:3843 npm run dev ``` ## Monitoring ```bash # Server logs docker-compose logs -f server1 # Router logs (on OpenWrt) logread -f -e client2server # Redpanda docker-compose logs -f redpanda # API curl http://localhost:3843/api/routers curl http://localhost:3843/api/events ``` ## Default Credentials The Go server creates a default admin user on first run: - **Username:** `admin` - **Password:** `admin` **Change this immediately in production.** Connect to the SQLite database and replace the password hash, or extend the `/api/auth` endpoints to expose a user-management UI. ## Security - **JWT auth** for dashboard access (HS256, 24h tokens) - **Legacy shared token** for router/hotplug access (simpler on minimal OpenWrt) - **Use TLS** (`wss://`, `https://`) in production - **Firewalls**: Only allow port 3843 from router network - **Redpanda**: Enable auth for production - **JWT_SECRET**: Must be 32+ random bytes in production - **Bind Caddy to a reverse proxy** (Cloudflare, nginx) for HTTPS termination ## License MIT - Luis Rosales 2026