Agent Architecture¶
The PowerBeacon Agent is a small, self-contained Go binary. Its purpose is to receive authenticated Wake-on-LAN dispatch commands from the backend and deliver UDP magic packets to devices on the local network.
With the cluster-aware backend, one device wake action can now be dispatched to multiple agents. Each agent still receives its own authenticated /wol request and executes independently.
Why a Separate Agent?¶
Containers running on Docker Desktop (Windows/macOS) cannot send UDP broadcast packets that reliably reach sleeping devices on the physical LAN, because Docker Desktop routes traffic through an internal VM with NAT. The agent runs as a native process or container directly on a Linux host (e.g., a Raspberry Pi, NAS, or mini PC) that has a real NIC on the same broadcast domain as the target devices.
Technology Stack¶
| Concern | Library |
|---|---|
| Language | Go 1.26 |
| HTTP router | gorilla/mux 1.8 |
| WOL packet | stdlib net (UDP) |
| Backend registration | stdlib net/http |
| OS detection | stdlib runtime |
Directory Structure¶
agent/
├── cmd/agent/
│ └── main.go # Entry point: flag parsing, registration, HTTP server
├── internal/
│ ├── api/
│ │ └── wol_handler.go # HTTP handlers: POST /wol, GET /health, GET /info
│ ├── client/
│ │ └── backend.go # BackendClient: register, heartbeat, retry logic
│ ├── network/
│ │ └── broadcast.go # Local IP and hostname discovery helpers
│ └── wol/
│ └── wol.go # Magic packet construction and UDP send
├── build/ # Compiled binaries (darwin-amd64, darwin-arm64, linux-amd64)
└── install/ # install-agent.sh / install-agent.ps1
Startup Sequence¶
flowchart TD
Start["Agent started"]
Flags["Parse CLI flags &<br/>environment variables"]
Validate["Validate advertise IP<br/>if provided"]
Network["Display network interfaces<br/>info log"]
Client["Create BackendClient"]
Goroutine["Launch registration<br/>goroutine"]
Register["Call StartWithRetry<br/>Register with backend"]
Success{"Registered?"}
Retry["Sleep 10s<br/>Retry"]
Heartbeat["Start heartbeat loop<br/>every 30s"]
HTTPServer["Bind HTTP server<br/>0.0.0.0:PORT"]
Routes["Register routes<br/>/wol, /health, /info"]
Serve["Serve HTTP requests"]
Shutdown["Wait for SIGINT/SIGTERM"]
Start --> Flags
Flags --> Validate
Validate --> Network
Network --> Client
Client --> Goroutine
Goroutine --> Register
Register --> Success
Success -->|No| Retry
Retry --> Register
Success -->|Yes| Heartbeat
Heartbeat --> HTTPServer
HTTPServer --> Routes
Routes --> Serve
Serve --> Shutdown
style Start fill:#3b82f6,stroke:#1e40af,color:#fff
style Success fill:#10b981,stroke:#047857,color:#fff
style Serve fill:#8b5cf6,stroke:#6d28d9,color:#fff
Configuration¶
How Configuration Works
The agent uses a two-tier configuration system:
- CLI flags (highest priority) — passed at runtime
- Environment variables (lower priority) — set on the host or container
- Defaults (fallback) — if neither flag nor env var is provided
| Flag | Environment Variable | Default | Description |
|---|---|---|---|
--backend |
BACKEND_URL |
http://localhost:8000 |
Backend base URL |
--port |
AGENT_PORT |
18080 |
Port the agent HTTP API listens on |
--bind |
AGENT_BIND |
0.0.0.0 |
Interface to bind the HTTP server |
--advertise-ip |
AGENT_ADVERTISE_IP |
(auto-detected) | IP the backend should use to reach this agent |
Auto-Detection Gotchas
If --advertise-ip is not set, the agent auto-detects its local IP. This works fine on single-NIC hosts but can pick the wrong interface on multi-homed servers. Always explicitly set --advertise-ip in production or containerized environments.
Registration Protocol¶
On startup the agent calls POST /api/agents/register on the backend with:
{
"hostname": "my-linux-host",
"ip": "192.168.1.50",
"port": 18080,
"os": "linux",
"version": "1.0.0"
}
If an agent with the same hostname already exists in the backend database, its IP, port, OS, version, and last_seen are updated and the existing token is returned. If it is a new agent, a UUID and a new random bearer token are created.
The backend responds with:
The agent stores agent_id and token in memory. Restarts re-register and receive the same token (idempotent by hostname).
Heartbeat Protocol¶
After successful registration, a background goroutine sends a heartbeat every 30 seconds:
The backend updates Agent.last_seen and confirms the agent's status as online. If the backend does not receive a heartbeat within a configurable window, the agent is considered offline (status tracking is done at the backend level).
HTTP API¶
The agent exposes a minimal HTTP API on port 18080 (default):
POST /wol¶
Dispatches a Wake-on-LAN magic packet. Requires Authorization: Bearer {token}.
Request body:
Validation rules:
macmust be a valid MAC address in colon-separated, dash-separated, or bare hex format.broadcastmust be a valid IP address.portdefaults to 9 if not provided.
Response on success:
GET /health¶
Returns HTTP 200 with:
No authentication required. Used by the backend AgentService.check_agent_health().
GET /info¶
Returns agent metadata (hostname, IP, OS, version). No authentication required.
WOL Packet Implementation¶
Magic Packet Standard
The WOL magic packet is defined by the Wake-on-LAN standard as exactly 102 bytes:
- Bytes 0–5: six
0xFFbytes (synchronization stream). - Bytes 6–101: the target MAC address repeated 16 times.
The agent's wol.go implementation follows these steps:
parseMAC()— strips:and-separators, validates that the result is 12 hex characters, and decodes to a 6-byte slice.buildMagicPacket()— allocates a 102-byte slice, writes 6 ×0xFF, then copies the 6-byte MAC 16 times.sendUDPBroadcast()— resolves the broadcast address withnet.ResolveUDPAddr, dials a UDP socket withnet.DialUDP, and callsconn.Write(packet).
Supported MAC address formats:
AA:BB:CC:DD:EE:FF
AA-BB-CC-DD-EE-FF
AABBCCDDEEFF
Token Security¶
!!! danger "Token Compromise" - Each agent receives a unique bearer token at registration time. - If a token is compromised, only that agent is affected; other agents remain secure. - Regenerate compromised tokens by re-registering the agent (it will request a new token). - The token is stored in memory only on the agent process; restarting clears it.
The bearer token used between the backend and agent is:
- Generated randomly by the backend at agent registration.
- Stored in the
agentstable, indexed for fast lookup. - Sent by the agent in every
POST /api/agents/heartbeatrequest. - Sent by the backend in every
POST http://{agent.ip}:18080/wolrequest. - Validated by the agent's
WOLHandlerbefore any WOL packet is sent.
If the agent has not yet completed registration (token is empty), the /wol endpoint returns 503 Service Unavailable, preventing unauthenticated WOL execution.
Installation¶
Easy Installation
Pre-built binaries for common platforms are bundled with the backend and automatically served at standard endpoints.
The install script:
- Downloads the binary for your OS and architecture
- Makes it executable
- Optionally installs as a system service
Supported Platforms¶
| Platform | Architecture | Notes |
|---|---|---|
| Linux | amd64 | Primary deployment target |
| macOS | amd64, arm64 | Development and testing; LAN broadcast works natively |
| Windows | — | Binary not currently shipped; build from source if needed |
Relationship to Backend¶
The agent is intentionally stateless. It does not persist any data locally. All state (agent ID, token, status) lives in the backend database. This means:
- Reinstalling or replacing an agent binary on the same host re-registers by hostname and resumes normal operation.
- Multiple agents can be registered and each device is assigned to exactly one agent via
Device.agent_id. - The backend can reach out to any registered agent using the
ip:portstored at registration time.