Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
"words": [
"daltonbr",
"dylib",
"godotenv",
"joho",
"wolserver",
"wolserver"
]
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

# Build outputs
/wolserver
wol-server

# Go module / workspace
go.work
Expand Down
197 changes: 134 additions & 63 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,95 +1,166 @@
# WOL-Server

🔧 Project Overview
A lightweight HTTP server that sends Wake-on-LAN (WOL) magic packets to power on remote machines on your local network. Supports multiple devices through YAML configuration or direct API parameters.

This is a lightweight Go HTTP server that sends a Wake-on-LAN (WOL) magic packet to power on a remote machine on your local network.
## Quick Start

📦 How It Works
### Docker (Recommended)

- The server exposes an HTTP endpoint:
```yml
services:
wol-server:
image: ghcr.io/daltonbr/wol-server:latest
network_mode: "host" # Required for WOL broadcast on Linux
environment:
WOL_CONFIG: |
devices:
desktop:
mac: "00:11:22:33:44:55"
ip: "192.168.1.255"
port: 9
broadcast: true

laptop:
mac: "aa:bb:cc:dd:ee:ff"
ip: "192.168.1.100"
restart: unless-stopped

# macOS users: network_mode: "host" doesn't work on Docker Desktop
# Use port mapping instead and comment out network_mode above:
# ports:
# - "5000:5000"

# Alternative: Mount a config file instead of using WOL_CONFIG env var
# volumes:
# - type: bind
# source: ~/.config/wol-server/config.yaml
# target: /root/.config/wol-server/config.yaml
# read_only: true
```

### Native Binary

```bash
GET /wake
```
# Install config
mkdir -p ~/.config/wol-server
cp config.example.yaml ~/.config/wol-server/config.yaml
# Edit config.yaml with your device details

- When accessed, it reads environment variables to determine:
- The target MAC address
- The destination IP and port
- Whether to use broadcast
- It then sends a WOL magic packet to wake the device
```bash
Comment thread
daltonbr marked this conversation as resolved.
# Run
go run .
```

🌱 Environment Variables
## Usage

Set these before running the server:
**Wake a configured device:**

| Variable | Example | Description |
| -------- | ------- | ----------- |
| WOL_MAC | 00:11:22:33:44:55 | Target machine's MAC address (required) |
| WOL_IP | 192.168.1.255 | Target machine's IP or broadcast (optional, default: 192.168.1.255) |
| WOL_PORT | 9 | UDP port for WOL (optional, default: 9) |
| WOL_BROADCAST | `true` or `false` | Use broadcast or direct IP (optional) |
```bash
curl http://localhost:5000/wake?device=desktop
```

▶️ Running
**Wake using direct parameters (no config needed):**

## Docker
```bash
curl "http://localhost:5000/wake?mac=00:11:22:33:44:55&ip=192.168.1.255"
```

`docker-compose.yml`
**Check server status:**

```yml
services:
wol-server:
image: ghcr.io/daltonbr/wol-server:latest
network_mode: "host"
environment:
ENV: production
WOL_MAC: "00:11:22:33:44:55" # Replace with your actual MAC address
WOL_IP: "192.168.1.255" # Your broadcast IP or direct device
WOL_PORT: "9" # Usually 7 or 9
WOL_BROADCAST: "true" # Set false if using directed IP (not supported yet)
restart: unless-stopped
```bash
curl http://localhost:5000/status
```

> [!TIP]
> Docker’s default bridge network does not allow broadcast e.g.`(192.168.1.255)` to go through - that is why we need `network-mode` set to `host`.
## Configuration

## How to use
### Option 1: Environment Variable (Docker)

You need to enable wake-on-lan on your Network card and probably enable this on your BIOS setup (usually is disabled to save energy)
[Check this guide for more details](https://www.windowscentral.com/software-apps/windows-11/how-to-enable-wake-on-lan-on-windows-11).
Set `WOL_CONFIG` with YAML content (see docker-compose example above).

Turn on PC - send the magical packet
### Option 2: Config File (Bare Metal)

```http
http://localhost:5000/wake
Create `~/.config/wol-server/config.yaml`:

```yaml
devices:
desktop:
mac: "00:11:22:33:44:55"
ip: "192.168.1.255"
port: 9 # optional, default: 9
broadcast: true # optional, default: false

laptop:
mac: "11:22:33:44:55:66"
ip: "192.168.1.255"
```

Check status
See [`config.example.yaml`](config.example.yaml) for more examples.

```http
http://localhost:5000/status
```
### Configuration Fields

📤 Publishing (GHCR)
| Field | Type | Description | Default |
| ----- | ---- | ----------- | ------- |
| mac | string | Target machine's MAC address (required) | - |
| ip | string | Target IP or broadcast address (required) | - |
| port | integer | UDP port for WOL | 9 |
| broadcast | boolean | Use broadcast mode | false |
Comment thread
daltonbr marked this conversation as resolved.
Outdated

To build and publish a multi-arch Docker image (specifically `linux/amd64`) to GitHub Container Registry (GHCR):
## API Reference

```bash
# Build for linux/amd64
docker build --platform linux/amd64 -t wol-server .
### Endpoints

**GET /wake**

Wake a device using either a configured device name or direct parameters.

**Query Parameters:**

Using configured device:

- `device` - Device name from config (required)

Using direct parameters:

- `mac` - MAC address in colon format (required)
- `ip` - Target IP or broadcast address (required)
- `port` - UDP port (optional, default: 9)
- `broadcast` - "true" or "false" (optional, default: true)

**Examples:**

# Tag with GHCR path and version
docker tag wol-server ghcr.io/daltonbr/wol-server:latest
docker tag wol-server ghcr.io/daltonbr/wol-server:<version>
```bash
# Using device name
GET /wake?device=desktop

# Push to GHCR
docker push ghcr.io/daltonbr/wol-server:latest
docker push ghcr.io/daltonbr/wol-server:<version>
# Using direct parameters
GET /wake?mac=00:11:22:33:44:55&ip=192.168.1.255&port=9&broadcast=true
```

> 💡 You must be logged in to GHCR with a Personal Access Token (PAT) that has `write:packages` permission:
**GET /status**

Check if the server is running.

**Response:** `WOL service is running`

## Prerequisites

Your target machine must have:

- Wake-on-LAN enabled in BIOS/UEFI
- Wake-on-LAN enabled in network adapter settings
- Connected to power (or battery with WOL support)

[Guide: How to enable Wake-on-LAN on Windows 11](https://www.windowscentral.com/software-apps/windows-11/how-to-enable-wake-on-lan-on-windows-11)

## Alternative: Volume Mount Config

Instead of using `WOL_CONFIG` env var, you can mount a config file (see commented example in docker-compose above).

## Development

See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for:

> 💡 You must be logged in to GHCR using your GitHub username and a Personal Access Token (PAT) with `write:packages` permission:
>
> ```bash
> docker login ghcr.io
> ```
- Building from source (requires Go 1.25.5+)
- Running tests
- Cross-compilation
- Publishing releases
21 changes: 21 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Example WOL Server Configuration
# Copy this to ~/.config/wol-server/config.yaml or use WOL_CONFIG env var

devices:
desktop:
mac: "00:11:22:33:44:55"
ip: "192.168.1.255"
port: 9
broadcast: true

gaming-pc:
mac: "aa:bb:cc:dd:ee:ff"
ip: "192.168.1.100"
port: 9
broadcast: false

laptop:
mac: "11:22:33:44:55:66"
ip: "192.168.1.255"
# port defaults to 9 if not specified
# broadcast defaults to false if not specified
Comment thread
daltonbr marked this conversation as resolved.
Outdated
87 changes: 60 additions & 27 deletions config.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
package main

import (
"errors"
"fmt"
"os"
"strconv"
"path/filepath"

"gopkg.in/yaml.v3"
)

// DeviceConfig defines the configuration for a single device.
type DeviceConfig struct {
MAC string `yaml:"mac"`
IP string `yaml:"ip"`
Port int `yaml:"port"`
Broadcast bool `yaml:"broadcast"`
}

// Config defines the YAML structure for all devices.
type Config struct {
Devices map[string]DeviceConfig `yaml:"devices"`
}

// WOLConfig defines the configuration for sending a Wake-on-LAN magic packet.
type WOLConfig struct {
// MACAddress is the target device's MAC address in colon format (e.g. "00:11:22:33:44:55").
Expand All @@ -22,43 +37,61 @@ type WOLConfig struct {

// Broadcast indicates whether to use UDP broadcast (true) or direct IP (false).
Broadcast bool
}

// Alias is an optional name for the device (e.g. "Glados").
// Useful if managing multiple configs later.
// Optional: not used in logic yet.
// Alias string
var globalConfig Config
Comment thread
daltonbr marked this conversation as resolved.

// SendPing indicates whether a ping should be sent after the WOL packet
// to check if the device is responding. Optional feature for future use.
// SendPing bool
// loadConfig loads configuration from WOL_CONFIG env var or config file
func loadConfig() error {
// Try loading from WOL_CONFIG environment variable first
if configYaml := os.Getenv("WOL_CONFIG"); configYaml != "" {
if err := yaml.Unmarshal([]byte(configYaml), &globalConfig); err != nil {
return fmt.Errorf("failed to parse WOL_CONFIG env var: %w", err)
}
return nil
}

// PingTimeout defines the timeout in seconds when SendPing is enabled.
// PingTimeout int
}
// Fall back to config file
configDir := os.Getenv("XDG_CONFIG_HOME")
if configDir == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}
configDir = filepath.Join(homeDir, ".config")
}

configPath := filepath.Join(configDir, "wol-server", "config.yaml")

func loadConfig() (WOLConfig, error) {
mac := os.Getenv("WOL_MAC")
ip := os.Getenv("WOL_IP")
portStr := os.Getenv("WOL_PORT")
broadcast := os.Getenv("WOL_BROADCAST") == "true"
data, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("failed to read config file %s: %w", configPath, err)
}

if mac == "" {
return WOLConfig{}, errors.New("WOL_MAC environment variable is required")
if err := yaml.Unmarshal(data, &globalConfig); err != nil {
return fmt.Errorf("failed to parse config file: %w", err)
}

if ip == "" {
return WOLConfig{}, errors.New("WOL_IP environment variable is required")
return nil
}

// getDeviceConfig retrieves a device configuration by name
func getDeviceConfig(deviceName string) (WOLConfig, error) {
device, exists := globalConfig.Devices[deviceName]
if !exists {
return WOLConfig{}, fmt.Errorf("device '%s' not found in config", deviceName)
}

port := 9 // default
if p, err := strconv.Atoi(portStr); err == nil {
port = p
// Apply defaults
port := device.Port
if port == 0 {
port = 9
}

Comment thread
daltonbr marked this conversation as resolved.
return WOLConfig{
MACAddress: mac,
IPAddress: ip,
MACAddress: device.MAC,
IPAddress: device.IP,
Port: port,
Broadcast: broadcast,
Broadcast: device.Broadcast,
}, nil
Comment thread
daltonbr marked this conversation as resolved.
}
Loading