M .gitignore => .gitignore +2 -2
@@ 1,5 1,5 @@
-# Binary (root only, not cmd/outline-distro/)
-/outline-distro
+# Binary (root only, not cmd/shroud/)
+/shroud
# State/runtime
state.yaml
A README.md => README.md +157 -0
@@ 0,0 1,157 @@
+# shroud
+
+Single-binary Go replacement for the [Outline VPN](https://getoutline.org/) server stack. Replaces the original 3-process deployment (Node.js shadowbox + outline-ss-server + Prometheus) with one Go binary that uses [outline-ss-server](https://github.com/Jigsaw-Code/outline-ss-server) as a library.
+
+Optionally includes [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) support with HTTP/3 QUIC cover traffic on port 443.
+
+## Features
+
+- **Single binary** — no Node.js, no child processes, no sidecar Prometheus
+- **Shadowbox-compatible REST API** — drop-in replacement for existing Outline clients
+- **Hot-reload** — adding/removing keys swaps cipher lists and listeners without downtime
+- **AmneziaWG** — obfuscated WireGuard VPN with automatic TLS certificates and HTTP/3 cover server
+- **Built-in metrics** — Prometheus endpoint with optional node_exporter collectors (Linux)
+- **CLI management** — manage keys and server config without a running server
+- **Atomic state persistence** — single YAML file with safe write-via-rename
+
+## Quick Start
+
+### Build from source
+
+```bash
+go build ./cmd/shroud/
+```
+
+### Run
+
+```bash
+# Start the server
+./shroud -c config.example.yaml
+
+# With verbose logging
+./shroud -c config.yaml -v
+```
+
+### Install on Ubuntu/Debian
+
+```bash
+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.
+
+## CLI Usage
+
+```bash
+# 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
+```
+
+## Configuration
+
+See [`config.example.yaml`](config.example.yaml) for the full configuration reference.
+
+```yaml
+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
+```
+
+## REST API
+
+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 |
+
+## Architecture
+
+```
+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`.
+
+## Building with version info
+
+```bash
+go build -ldflags='-X main.version=1.0.0' ./cmd/shroud/
+```
+
+## Testing
+
+```bash
+go test ./...
+```
R cmd/outline-distro/main.go => cmd/shroud/main.go +17 -17
@@ 25,12 25,12 @@ import (
"golang.org/x/term"
"gopkg.in/yaml.v3"
- "sourcecraft.dev/bigbes/outline-distro/internal/api"
- "sourcecraft.dev/bigbes/outline-distro/internal/awgserver"
- "sourcecraft.dev/bigbes/outline-distro/internal/config"
- "sourcecraft.dev/bigbes/outline-distro/internal/metrics"
- "sourcecraft.dev/bigbes/outline-distro/internal/ssserver"
- "sourcecraft.dev/bigbes/outline-distro/internal/store"
+ "sourcecraft.dev/bigbes/shroud/internal/api"
+ "sourcecraft.dev/bigbes/shroud/internal/awgserver"
+ "sourcecraft.dev/bigbes/shroud/internal/config"
+ "sourcecraft.dev/bigbes/shroud/internal/metrics"
+ "sourcecraft.dev/bigbes/shroud/internal/ssserver"
+ "sourcecraft.dev/bigbes/shroud/internal/store"
)
var version = "dev"
@@ 48,8 48,8 @@ func newRootCmd() *cobra.Command {
)
root := &cobra.Command{
- Use: "outline-distro",
- Short: "Single-binary Outline Shadowsocks server with Prometheus metrics",
+ Use: "shroud",
+ Short: "Shadowsocks + AmneziaWG VPN server",
Version: version,
RunE: func(cmd *cobra.Command, args []string) error {
return runServe(configFile, verbose)
@@ 77,27 77,27 @@ func newCompletionCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate shell completion scripts",
- Long: `Generate shell completion scripts for outline-distro.
+ Long: `Generate shell completion scripts for shroud.
To load completions:
Bash:
- $ source <(outline-distro completion bash)
+ $ source <(shroud completion bash)
# To install permanently:
- $ outline-distro completion bash > /etc/bash_completion.d/outline-distro
+ $ shroud completion bash > /etc/bash_completion.d/shroud
Zsh:
- $ source <(outline-distro completion zsh)
+ $ source <(shroud completion zsh)
# To install permanently:
- $ outline-distro completion zsh > "${fpath[1]}/_outline-distro"
+ $ shroud completion zsh > "${fpath[1]}/_shroud"
Fish:
- $ outline-distro completion fish | source
+ $ shroud completion fish | source
# To install permanently:
- $ outline-distro completion fish > ~/.config/fish/completions/outline-distro.fish
+ $ shroud completion fish > ~/.config/fish/completions/shroud.fish
PowerShell:
- PS> outline-distro completion powershell | Out-String | Invoke-Expression
+ PS> shroud completion powershell | Out-String | Invoke-Expression
`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
@@ 689,7 689,7 @@ func runServe(configFile string, verbose bool) error {
logger.Info("Access info written.", "file", infoFile)
}
- logger.Info("outline-distro started.",
+ logger.Info("shroud started.",
"version", version,
"api", cfg.API.ListenAddr,
"metrics", cfg.Metrics.ListenAddr,
M config.example.yaml => config.example.yaml +1 -1
@@ 36,7 36,7 @@ amneziawg:
dns: "1.1.1.1, 8.8.8.8"
# HTTP/3 cover for DPI resistance (requires domain)
domain: "" # e.g., vpn.example.com
- cert_cache: /var/lib/outline-distro/certs
+ cert_cache: /var/lib/shroud/certs
acme_http_port: 80
# Obfuscation parameters (must match client config)
jc: 4
R dist/outline-distro.service => dist/shroud.service +9 -9
@@ 1,29 1,29 @@
[Unit]
-Description=Outline Distro — Shadowsocks + AmneziaWG VPN Server
-Documentation=https://sourcecraft.dev/bigbes/outline-distro
+Description=Shroud — Shadowsocks + AmneziaWG VPN Server
+Documentation=https://sourcecraft.dev/bigbes/shroud
After=network-online.target nss-lookup.target
Wants=network-online.target
[Service]
Type=simple
-ExecStart=/usr/local/bin/outline-distro -c /etc/outline-distro/config.yaml
+ExecStart=/usr/local/bin/shroud -c /etc/shroud/config.yaml
Restart=on-failure
RestartSec=5
WatchdogSec=60
# Logging — stdout/stderr go straight to journald.
-# View with: journalctl -u outline-distro -f
-# Filter errors: journalctl -u outline-distro -p err
+# View with: journalctl -u shroud -f
+# Filter errors: journalctl -u shroud -p err
StandardOutput=journal
StandardError=journal
-SyslogIdentifier=outline-distro
+SyslogIdentifier=shroud
# File descriptors
LimitNOFILE=65536
# Run as dedicated user (created by install script)
-User=outline-distro
-Group=outline-distro
+User=shroud
+Group=shroud
# Capabilities — needed for:
# CAP_NET_BIND_SERVICE — bind to ports < 1024 (AWG on 443, ACME on 80)
@@ 50,7 50,7 @@ RemoveIPC=yes
SystemCallArchitectures=native
# Writable paths for state, certs, and TUN device
-ReadWritePaths=/var/lib/outline-distro /etc/outline-distro /dev/net/tun
+ReadWritePaths=/var/lib/shroud /etc/shroud /dev/net/tun
# Allow /dev/net/tun access for AWG
DeviceAllow=/dev/net/tun rw
M go.mod => go.mod +1 -1
@@ 1,4 1,4 @@
-module sourcecraft.dev/bigbes/outline-distro
+module sourcecraft.dev/bigbes/shroud
go 1.26.1
M install.sh => install.sh +19 -19
@@ 1,8 1,8 @@
#!/usr/bin/env bash
#
-# outline-distro installer (Ubuntu)
+# shroud installer (Ubuntu)
#
-# Builds from source and installs outline-distro as a systemd service
+# Builds from source and installs shroud as a systemd service
# with Shadowsocks + optional AmneziaWG (HTTP/3 mux for DPI resistance).
#
# Usage:
@@ 16,14 16,14 @@ set -euo pipefail
# --- Paths ---
-INSTALL_DIR="/opt/outline-distro"
-CONFIG_DIR="/etc/outline-distro"
-DATA_DIR="/var/lib/outline-distro"
-BINARY="/usr/local/bin/outline-distro"
-SERVICE_NAME="outline-distro"
-SERVICE_USER="outline-distro"
+INSTALL_DIR="/opt/shroud"
+CONFIG_DIR="/etc/shroud"
+DATA_DIR="/var/lib/shroud"
+BINARY="/usr/local/bin/shroud"
+SERVICE_NAME="shroud"
+SERVICE_USER="shroud"
UNIT_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
-REPO_URL="https://sourcecraft.dev/bigbes/outline-distro.git"
+REPO_URL="https://sourcecraft.dev/bigbes/shroud.git"
# --- Config defaults ---
@@ 110,7 110,7 @@ fi
if ! id "$SERVICE_USER" &>/dev/null; then
info "Creating system user $SERVICE_USER..."
useradd --system --home-dir "$DATA_DIR" --shell /usr/sbin/nologin \
- --comment "Outline Distro VPN" "$SERVICE_USER"
+ --comment "Shroud VPN" "$SERVICE_USER"
ok "User $SERVICE_USER created"
fi
@@ 129,7 129,7 @@ fi
VERSION=$(git describe --tags --always 2>/dev/null || echo "dev")
info "Building $VERSION..."
CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=$VERSION" \
- -o "$BINARY" ./cmd/outline-distro/
+ -o "$BINARY" ./cmd/shroud/
ok "Binary: $BINARY ($VERSION)"
# --- Directories ---
@@ 217,14 217,14 @@ fi
info "Installing systemd unit..."
# Use the bundled unit file if present, otherwise generate one.
-BUNDLED_UNIT="$INSTALL_DIR/dist/outline-distro.service"
+BUNDLED_UNIT="$INSTALL_DIR/dist/shroud.service"
if [[ -f "$BUNDLED_UNIT" ]]; then
cp "$BUNDLED_UNIT" "$UNIT_FILE"
else
cat > "$UNIT_FILE" <<UNIT
[Unit]
-Description=Outline Distro — Shadowsocks + AmneziaWG VPN Server
-Documentation=https://sourcecraft.dev/bigbes/outline-distro
+Description=Shroud — Shadowsocks + AmneziaWG VPN Server
+Documentation=https://sourcecraft.dev/bigbes/shroud
After=network-online.target nss-lookup.target
Wants=network-online.target
@@ 273,10 273,10 @@ ok "Systemd unit installed and enabled"
if command -v ufw >/dev/null 2>&1 && ufw status | grep -q "active"; then
info "Configuring ufw firewall rules..."
- ufw allow "$API_PORT"/tcp comment "outline-distro API" >/dev/null 2>&1 || true
+ ufw allow "$API_PORT"/tcp comment "shroud API" >/dev/null 2>&1 || true
if [[ "$AWG_ENABLED" == "true" ]]; then
- ufw allow "$AWG_PORT"/udp comment "outline-distro AWG+HTTP3" >/dev/null 2>&1 || true
- ufw allow 80/tcp comment "outline-distro ACME" >/dev/null 2>&1 || true
+ ufw allow "$AWG_PORT"/udp comment "shroud AWG+HTTP3" >/dev/null 2>&1 || true
+ ufw allow 80/tcp comment "shroud ACME" >/dev/null 2>&1 || true
fi
ok "Firewall rules added"
else
@@ 291,7 291,7 @@ if [[ "$AWG_ENABLED" == "true" ]]; then
info "Enabling IPv4 forwarding..."
sysctl -q -w net.ipv4.ip_forward=1
# Persist via sysctl.d drop-in (idempotent).
- cat > /etc/sysctl.d/99-outline-distro.conf <<'SYSCTL'
+ cat > /etc/sysctl.d/99-shroud.conf <<'SYSCTL'
# Enable IPv4 forwarding for AmneziaWG VPN
net.ipv4.ip_forward=1
SYSCTL
@@ 330,7 330,7 @@ API_SECRET_DISPLAY=$(grep -oP 'secret:\s*"\K[^"]+' "$CONFIG_FILE" 2>/dev/null ||
cat <<EOF
=============================================
- outline-distro installed successfully
+ shroud installed successfully
=============================================
Config: ${CONFIG_FILE}
M internal/api/handlers.go => internal/api/handlers.go +2 -2
@@ 5,8 5,8 @@ import (
"net/http"
"strings"
- "sourcecraft.dev/bigbes/outline-distro/internal/awgserver"
- "sourcecraft.dev/bigbes/outline-distro/internal/store"
+ "sourcecraft.dev/bigbes/shroud/internal/awgserver"
+ "sourcecraft.dev/bigbes/shroud/internal/store"
)
func (h *Handler) getServer(w http.ResponseWriter, r *http.Request) {
M internal/api/router.go => internal/api/router.go +5 -5
@@ 7,11 7,11 @@ import (
"log/slog"
"net/http"
- "sourcecraft.dev/bigbes/outline-distro/internal/awgserver"
- "sourcecraft.dev/bigbes/outline-distro/internal/config"
- "sourcecraft.dev/bigbes/outline-distro/internal/metrics"
- "sourcecraft.dev/bigbes/outline-distro/internal/ssserver"
- "sourcecraft.dev/bigbes/outline-distro/internal/store"
+ "sourcecraft.dev/bigbes/shroud/internal/awgserver"
+ "sourcecraft.dev/bigbes/shroud/internal/config"
+ "sourcecraft.dev/bigbes/shroud/internal/metrics"
+ "sourcecraft.dev/bigbes/shroud/internal/ssserver"
+ "sourcecraft.dev/bigbes/shroud/internal/store"
)
type Handler struct {
M internal/awgserver/server.go => internal/awgserver/server.go +1 -1
@@ 11,7 11,7 @@ import (
"github.com/amnezia-vpn/amneziawg-go/tun"
"github.com/quic-go/quic-go/http3"
- "sourcecraft.dev/bigbes/outline-distro/internal/store"
+ "sourcecraft.dev/bigbes/shroud/internal/store"
)
// Config holds all settings for the AmneziaWG server.
M internal/config/config.go => internal/config/config.go +1 -1
@@ 160,7 160,7 @@ func (c *Config) setDefaults() {
c.AmneziaWG.MTU = 1420
}
if c.AmneziaWG.CertCache == "" {
- c.AmneziaWG.CertCache = "/var/lib/outline-distro/certs"
+ c.AmneziaWG.CertCache = "/var/lib/shroud/certs"
}
if c.AmneziaWG.ACMEHTTPPort == 0 {
c.AmneziaWG.ACMEHTTPPort = 80
M internal/metrics/collector.go => internal/metrics/collector.go +1 -1
@@ 30,7 30,7 @@ func NewServerMetrics() *ServerMetrics {
return &ServerMetrics{
buildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "build_info",
- Help: "Information on the outline-distro build",
+ Help: "Information on the shroud build",
}, []string{"version"}),
accessKeys: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "keys",
M internal/ssserver/server.go => internal/ssserver/server.go +2 -2
@@ 15,8 15,8 @@ import (
onet "github.com/Jigsaw-Code/outline-ss-server/net"
"github.com/Jigsaw-Code/outline-ss-server/service"
- "sourcecraft.dev/bigbes/outline-distro/internal/metrics"
- "sourcecraft.dev/bigbes/outline-distro/internal/store"
+ "sourcecraft.dev/bigbes/shroud/internal/metrics"
+ "sourcecraft.dev/bigbes/shroud/internal/store"
)
const tcpReadTimeout = 59 * time.Second