Single-binary Go replacement for the Outline VPN server stack. Replaces the original 3-process deployment (Node.js shadowbox + outline-ss-server + Prometheus) with one Go binary that uses outline-ss-server as a library.
Optionally includes AmneziaWG support with HTTP/3 QUIC cover traffic on port 443.
go build ./cmd/shroud/
# Start the server
./shroud -c config.example.yaml
# With verbose logging
./shroud -c config.yaml -v
curl -fsSL https://sourcecraft.dev/bigbes/shroud/raw/branch/main/install.sh | sudo bash
The installer builds from source, creates a systemd service, configures firewall rules, and prints the management API URL on completion.
# Server management (no running server required)
./shroud server info -c config.yaml
./shroud server set-port 50000 -c config.yaml
./shroud server set-hostname vpn.example.com -c config.yaml
./shroud server set-name "My VPN" -c config.yaml
# Access key management (no running server required)
./shroud key list -c config.yaml
./shroud key add -n "user1" -c config.yaml
./shroud key add -n "user2" -p 51234 --cipher chacha20-ietf-poly1305 -c config.yaml
./shroud key remove 1 -c config.yaml
./shroud key rename 1 "new-name" -c config.yaml
# Shell completions
./shroud completion bash
./shroud completion zsh
See config.example.yaml for the full configuration reference.
server:
name: "My Outline Server"
hostname: "example.com"
api:
listen_addr: ":8081"
metrics:
listen_addr: "127.0.0.1:8081"
node_exporter_collectors:
- cpu
- meminfo
- loadavg
- filesystem
- diskstats
- netdev
shadowsocks:
default_port: 0 # 0 = pick random available port
default_cipher: chacha20-ietf-poly1305
replay_history: 10000
amneziawg:
enabled: false
listen_port: 443
address: "10.14.0.0/24"
domain: "vpn.example.com" # For automatic Let's Encrypt certs
state_file: state.yaml
All management endpoints live under /<secret>/ prefix. The secret is auto-generated on first run and printed to the log.
| Method | Endpoint | Description |
|---|---|---|
GET |
/<secret>/server |
Server info |
PUT |
/<secret>/name |
Rename server |
GET |
/<secret>/access-keys |
List all keys |
POST |
/<secret>/access-keys |
Create key |
GET |
/<secret>/access-keys/{id} |
Get key |
PUT |
/<secret>/access-keys/{id} |
Upsert key |
DELETE |
/<secret>/access-keys/{id} |
Delete key |
PUT |
/<secret>/access-keys/{id}/name |
Rename key |
PUT |
/<secret>/access-keys/{id}/data-limit |
Set data limit |
DELETE |
/<secret>/access-keys/{id}/data-limit |
Remove data limit |
PUT |
/<secret>/server/port-for-new-access-keys |
Set default port |
PUT |
/<secret>/server/hostname-for-access-keys |
Set hostname |
PUT |
/<secret>/server/access-key-data-limit |
Set default data limit |
DELETE |
/<secret>/server/access-key-data-limit |
Remove default data limit |
GET |
/<secret>/metrics/enabled |
Check metrics status |
PUT |
/<secret>/metrics/enabled |
Toggle metrics |
GET |
/<secret>/metrics/transfer |
Per-key byte transfer |
GET |
/<secret>/access-keys/{id}/awg-config |
Download AmneziaWG config |
Public endpoints (no auth):
| Method | Endpoint | Server | Description |
|---|---|---|---|
GET |
/metrics |
Metrics | Prometheus scrape target |
GET |
/healthz |
Metrics | Health check |
cmd/shroud/main.go CLI entry point, cobra commands, signal handling
|
+-> internal/config/ YAML config loading and validation
+-> internal/store/ YAML file persistence (atomic write via rename)
+-> internal/ssserver/ Wraps outline-ss-server as a library
+-> internal/metrics/ Prometheus registry + TransferTracker
+-> internal/api/ REST API handlers (shadowbox-compatible)
+-> internal/awgserver/ AmneziaWG server + HTTP/3 QUIC cover
The Shadowsocks proxy is imported as a Go library — not run as a subprocess. When keys change, SyncKeys() rebuilds cipher lists and hot-swaps listeners. The integration surface is ~100 lines in internal/ssserver/server.go.
go build -ldflags='-X main.version=1.0.0' ./cmd/shroud/
go test ./...