|
|
1 month ago | |
|---|---|---|
| dashboard | 2 months ago | |
| docs | 1 month ago | |
| package | 2 months ago | |
| server | 2 months ago | |
| .gitignore | 2 months ago | |
| ARCHITECTURE.md | 2 months ago | |
| Caddyfile | 2 months ago | |
| MEMORY.md | 2 months ago | |
| README.md | 2 months ago | |
| client2server.dlog | 1 month ago | |
| docker-compose.yml | 2 months ago |
Bi-directional event forwarder + Apple-style dashboard for OpenWrt router fleets
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.
┌────────────────────────────────────────────────────────────────┐
│ 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 │
│ └─────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘
/var/run/client2server/buffer) — shared by hotplug + Lua, survives internet outages# Clone and run
git clone https://git3.techno-world.net/lrosales/client2server.git
cd client2server
# Set secrets (use any 32+ byte strings)
cat > .env <<EOF
TOKEN=replace-me
JWT_SECRET=$(openssl rand -hex 32)
EOF
# Start all services
docker-compose up -d
# Open the dashboard
open http://localhost/
# Login: admin / admin (CHANGE IN PRODUCTION)
# Check status
docker-compose ps
# 1. Go server (with Redpanda running locally)
cd server
go build -o server .
REDPANDA_BROKERS=localhost:9092 \
TOKEN=replace-me \
JWT_SECRET=$(openssl rand -hex 32) \
PORT=3843 \
./server
# 2. Dashboard (Vite dev server)
cd dashboard
npm install
VITE_API_TARGET=http://localhost:3843 npm run dev
# → http://localhost:5173
# 3. On OpenWrt router (copy all files)
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/
scp package/hotplug/_lib.sh root@router:/usr/share/client2server/hotplug-lib.sh
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 /usr/share/client2server/hotplug-lib.sh"
ssh root@router "/etc/init.d/client2server enable && /etc/init.d/client2server start"
| Service | Port | Protocol |
|---|---|---|
| Dashboard (web UI) | 80 / 443 | HTTP(S) |
| WebSocket + HTTP API | 3843 | WS + HTTP (unified) |
| Redpanda Kafka | 9092 | Kafka (Redpanda) |
| Dashboard dev (Vite) | 5173 | HTTP (local dev only) |
TOKEN=replace-me # Legacy router/hotplug shared token
JWT_SECRET=replace-me-32+bytes # JWT signing key for dashboard auth (32+ random bytes)
REDPANDA_BROKERS=redpanda:9092 # Comma-separated Redpanda addresses
PORT=3843 # Server listen port
DB_PATH=/var/lib/c2s/client2server.db # SQLite path
# /etc/config/client2server
config client2server 'general'
option enabled '1'
config server
option url 'wss://your-server.com:3843'
option token 'replace-me'
config wan
option interface 'wan'
option device 'eth0'
All endpoints on :3843. The dashboard uses JWT (from /api/auth/login); routers use the legacy shared TOKEN directly.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /health |
none | Liveness + router stats |
| POST | /api/auth/login |
none | {username,password} → JWT |
| GET | /api/auth/me |
JWT | Current user info |
| POST | /api/events |
token or JWT | Ingest event from router/hotplug |
| GET | /api/events/list |
JWT | Historical events (filterable) |
| GET | /api/events/stream |
token or JWT | Server-Sent Events live feed |
| GET | /api/routers |
none | Known routers |
| POST | /api/command |
token or JWT | Send command, awaits result |
| GET | /api/commands |
JWT | Command history |
| GET | /api/metrics?since=1h |
JWT | Time-series metrics |
| GET | /api/alerts?unack=1 |
JWT | Alerts list |
| POST | /api/alerts/{id}/ack |
JWT | Acknowledge alert |
| GET | /ws |
token | WebSocket from router |
Send from server to router via WebSocket or HTTP API:
# Reboot
curl -X POST http://localhost:3843/api/command \
-H "Authorization: Bearer <your-token-here>" \
-d '{"router_id":"router1","command":"reboot"}'
# UCI set
curl -X POST http://localhost:3843/api/command \
-H "Authorization: Bearer <your-token-here>" \
-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 <your-token-here>" \
-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.
| 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 |
⚠️ |
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 |
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:
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.
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
# 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
cd dashboard
npm install
npm run build # → dist/ (static files)
# Or development server:
VITE_API_TARGET=http://localhost:3843 npm run dev
# 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
The Go server creates a default admin user on first run:
adminadminChange 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.
wss://, https://) in productionMIT - Luis Rosales 2026