#!/usr/bin/env bash
#
# shroud installer (Ubuntu)
#
# Builds from source and installs shroud as a systemd service
# with Shadowsocks + optional AmneziaWG (HTTP/3 mux for DPI resistance).
#
# Usage:
# curl -sSL <url>/install.sh | bash # SS only
# curl -sSL <url>/install.sh | bash -s -- --awg # SS + AmneziaWG
# curl -sSL <url>/install.sh | bash -s -- --awg --domain vpn.example.com
#
# Requirements: Go 1.22+, root, git, Ubuntu 20.04+
#
set -euo pipefail
# --- Paths ---
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/shroud.git"
# --- Config defaults ---
SERVER_NAME="Outline Server"
HOSTNAME=""
API_PORT="8081"
METRICS_PORT="9091"
SS_PORT="0"
SS_CIPHER="chacha20-ietf-poly1305"
AWG_ENABLED="false"
AWG_PORT="443"
AWG_SUBNET="10.14.0.0/24"
AWG_DNS="1.1.1.1, 8.8.8.8"
AWG_DOMAIN=""
AWG_MTU="1420"
AWG_TUN="awg0"
VLESS_ENABLED="false"
VLESS_PORT="443"
# --- Parse arguments ---
while [[ $# -gt 0 ]]; do
case "$1" in
--awg) AWG_ENABLED="true"; shift ;;
--vless) VLESS_ENABLED="true"; shift ;;
--vless-port) VLESS_PORT="$2"; shift 2 ;;
--domain) AWG_DOMAIN="$2"; shift 2 ;;
--hostname) HOSTNAME="$2"; shift 2 ;;
--name) SERVER_NAME="$2"; shift 2 ;;
--ss-port) SS_PORT="$2"; shift 2 ;;
--awg-port) AWG_PORT="$2"; shift 2 ;;
--awg-subnet) AWG_SUBNET="$2"; shift 2 ;;
--awg-dns) AWG_DNS="$2"; shift 2 ;;
--api-port) API_PORT="$2"; shift 2 ;;
--metrics-port) METRICS_PORT="$2"; shift 2 ;;
--help|-h)
cat <<'USAGE'
Usage: install.sh [OPTIONS]
Options:
--awg Enable AmneziaWG protocol (with HTTP/3 mux)
--vless Enable VLESS+REALITY transport (auto-detects decoy server)
--vless-port PORT VLESS+REALITY listen port (default: 443)
--domain DOMAIN Domain for Let's Encrypt (enables HTTP/3 cover)
--hostname HOST Server hostname/IP for access key URLs
--name NAME Server display name
--ss-port PORT Shadowsocks port (0 = random)
--awg-port PORT AmneziaWG + HTTP/3 listen port (default: 443)
--awg-subnet CIDR AWG peer subnet (default: 10.14.0.0/24)
--awg-dns DNS DNS for AWG clients (default: 1.1.1.1, 8.8.8.8)
--api-port PORT Management API port (default: 8081)
--metrics-port PORT Prometheus metrics port (default: 9091)
USAGE
exit 0
;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
# --- Helpers ---
info() { echo -e "\033[1;34m==>\033[0m $*"; }
ok() { echo -e "\033[1;32m==>\033[0m $*"; }
warn() { echo -e "\033[1;33mWARN:\033[0m $*"; }
fail() { echo -e "\033[1;31mERROR:\033[0m $*" >&2; exit 1; }
# --- Preflight ---
[[ $EUID -eq 0 ]] || fail "This script must be run as root."
. /etc/os-release 2>/dev/null || true
if [[ "${ID:-}" != "ubuntu" && "${ID_LIKE:-}" != *"ubuntu"* && "${ID_LIKE:-}" != *"debian"* ]]; then
warn "This script is designed for Ubuntu. Proceeding anyway..."
fi
command -v go >/dev/null 2>&1 || fail "Go is not installed. Install Go 1.22+: https://go.dev/dl/"
command -v git >/dev/null 2>&1 || fail "git is not installed: apt install git"
GO_VER=$(go version | grep -oP 'go\K[0-9]+\.[0-9]+')
info "Go $GO_VER detected"
if [[ -z "$HOSTNAME" ]]; then
HOSTNAME=$(curl -s -4 --connect-timeout 5 ifconfig.me 2>/dev/null || hostname -f 2>/dev/null || echo "localhost")
info "Auto-detected hostname: $HOSTNAME"
fi
# --- Create system user ---
if ! id "$SERVICE_USER" &>/dev/null; then
info "Creating system user $SERVICE_USER..."
useradd --system --home-dir "$DATA_DIR" --shell /usr/sbin/nologin \
--comment "Shroud VPN" "$SERVICE_USER"
ok "User $SERVICE_USER created"
fi
# --- Build from source ---
info "Cloning/updating repository..."
if [[ -d "$INSTALL_DIR/.git" ]]; then
cd "$INSTALL_DIR"
git pull --ff-only 2>/dev/null || git fetch --all
else
rm -rf "$INSTALL_DIR"
git clone "$REPO_URL" "$INSTALL_DIR"
cd "$INSTALL_DIR"
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/shroud/
ok "Binary: $BINARY ($VERSION)"
# --- Directories ---
mkdir -p "$CONFIG_DIR" "$DATA_DIR/certs"
chown "$SERVICE_USER:$SERVICE_USER" "$DATA_DIR" "$DATA_DIR/certs"
chmod 750 "$DATA_DIR"
# Ensure TUN device node exists (needed inside PrivateDevices=no).
if [[ ! -c /dev/net/tun ]]; then
mkdir -p /dev/net
mknod /dev/net/tun c 10 200
chmod 0666 /dev/net/tun
fi
# --- Generate config ---
CONFIG_FILE="$CONFIG_DIR/config.yaml"
if [[ -f "$CONFIG_FILE" ]]; then
warn "Config exists: $CONFIG_FILE — skipping generation."
else
info "Generating config..."
API_SECRET=$(head -c 16 /dev/urandom | xxd -p)
# Generate random AmneziaWG obfuscation parameters.
# Each install gets unique values to resist DPI fingerprinting.
AWG_JC=$(( RANDOM % 8 + 3 )) # 3..10
AWG_JMIN=$(( RANDOM % 100 + 50 )) # 50..149
AWG_JMAX=$(( AWG_JMIN + RANDOM % 500 + 100 )) # jmin+100..jmin+599
AWG_S1=$(( RANDOM % 500 + 10 )) # 10..509
# Ensure 148+s1 != 92+s2 (packet sizes must differ).
while true; do
AWG_S2=$(( RANDOM % 500 + 10 ))
[[ $(( 148 + AWG_S1 )) -ne $(( 92 + AWG_S2 )) ]] && break
done
AWG_S3=$(( RANDOM % 500 + 10 ))
AWG_S4=$(( RANDOM % 500 + 10 ))
# Generate 4 unique random header magic values (>4, full uint32 range).
rand_u32() { od -An -tu4 -N4 /dev/urandom | tr -d ' '; }
declare -A _hdr_seen=()
AWG_HEADERS=()
while [[ ${#AWG_HEADERS[@]} -lt 4 ]]; do
v=$(rand_u32)
[[ $v -le 4 ]] && continue
[[ -n "${_hdr_seen[$v]:-}" ]] && continue
_hdr_seen[$v]=1
AWG_HEADERS+=("$v")
done
AWG_H1=${AWG_HEADERS[0]}
AWG_H2=${AWG_HEADERS[1]}
AWG_H3=${AWG_HEADERS[2]}
AWG_H4=${AWG_HEADERS[3]}
cat > "$CONFIG_FILE" <<YAML
server:
name: "${SERVER_NAME}"
hostname: "${HOSTNAME}"
api:
listen_addr: ":${API_PORT}"
secret: "${API_SECRET}"
metrics:
listen_addr: "127.0.0.1:${METRICS_PORT}"
node_exporter_collectors:
- cpu
- meminfo
- loadavg
- uname
- filesystem
- diskstats
- netdev
shadowsocks:
default_port: ${SS_PORT}
default_cipher: ${SS_CIPHER}
nat_timeout: 5m
replay_history: 10000
amneziawg:
enabled: ${AWG_ENABLED}
listen_port: ${AWG_PORT}
tun_name: ${AWG_TUN}
address: "${AWG_SUBNET}"
mtu: ${AWG_MTU}
dns: "${AWG_DNS}"
hostname: ""
domain: "${AWG_DOMAIN}"
jc: ${AWG_JC}
jmin: ${AWG_JMIN}
jmax: ${AWG_JMAX}
s1: ${AWG_S1}
s2: ${AWG_S2}
s3: ${AWG_S3}
s4: ${AWG_S4}
h1: "${AWG_H1}"
h2: "${AWG_H2}"
h3: "${AWG_H3}"
h4: "${AWG_H4}"
vless:
enabled: ${VLESS_ENABLED}
listen_addr: ":${VLESS_PORT}"
server_names: []
dest: ""
acme:
cert_cache: ${DATA_DIR}/certs
http_port: 80
state_file: ${DATA_DIR}/state.yaml
YAML
chown "$SERVICE_USER:$SERVICE_USER" "$CONFIG_FILE"
chmod 640 "$CONFIG_FILE"
ok "Config: $CONFIG_FILE"
fi
# --- VLESS autodetect ---
if [[ "$VLESS_ENABLED" == "true" ]]; then
info "Auto-detecting best REALITY decoy server (scanning /24 subnet)..."
if "$BINARY" vless autodetect --ip "$HOSTNAME" --write -c "$CONFIG_FILE" 2>&1; then
ok "REALITY decoy server configured automatically"
else
warn "Auto-detection failed. You can run it manually later:"
warn " $BINARY vless autodetect --write -c $CONFIG_FILE"
fi
fi
# --- Install systemd unit ---
info "Installing systemd unit..."
# Use the bundled unit file if present, otherwise generate one.
BUNDLED_UNIT="$INSTALL_DIR/dist/shroud.service"
if [[ -f "$BUNDLED_UNIT" ]]; then
cp "$BUNDLED_UNIT" "$UNIT_FILE"
else
cat > "$UNIT_FILE" <<UNIT
[Unit]
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=${BINARY} -c ${CONFIG_FILE}
Restart=on-failure
RestartSec=5
WatchdogSec=60
StandardOutput=journal
StandardError=journal
SyslogIdentifier=${SERVICE_NAME}
LimitNOFILE=65536
User=${SERVICE_USER}
Group=${SERVICE_USER}
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_ADMIN CAP_NET_RAW
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_ADMIN CAP_NET_RAW
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=no
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
RestrictNamespaces=yes
RestrictRealtime=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
RemoveIPC=yes
SystemCallArchitectures=native
ReadWritePaths=${DATA_DIR} ${CONFIG_DIR} /dev/net/tun
DeviceAllow=/dev/net/tun rw
[Install]
WantedBy=multi-user.target
UNIT
fi
systemctl daemon-reload
systemctl enable "$SERVICE_NAME" --quiet
ok "Systemd unit installed and enabled"
# --- Firewall (ufw on Ubuntu) ---
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 "shroud API" >/dev/null 2>&1 || true
if [[ "$AWG_ENABLED" == "true" ]]; then
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
if [[ "$VLESS_ENABLED" == "true" ]]; then
ufw allow "$VLESS_PORT"/tcp comment "shroud VLESS+REALITY" >/dev/null 2>&1 || true
fi
ok "Firewall rules added"
else
warn "ufw is not active. Ensure these ports are open:"
warn " TCP ${API_PORT} (management API)"
[[ "$AWG_ENABLED" == "true" ]] && warn " UDP ${AWG_PORT} (AWG + HTTP/3) | TCP 80 (ACME)"
[[ "$VLESS_ENABLED" == "true" ]] && warn " TCP ${VLESS_PORT} (VLESS+REALITY)"
fi
# --- IP forwarding for AWG ---
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-shroud.conf <<'SYSCTL'
# Enable IPv4 forwarding for AmneziaWG VPN
net.ipv4.ip_forward=1
SYSCTL
ok "IPv4 forwarding enabled"
fi
# --- Start service ---
info "Starting ${SERVICE_NAME}..."
# Stop if already running (upgrade case).
systemctl stop "$SERVICE_NAME" 2>/dev/null || true
systemctl start "$SERVICE_NAME"
# Wait for journal to confirm startup.
for i in 1 2 3 4 5; do
if systemctl is-active --quiet "$SERVICE_NAME"; then
break
fi
sleep 1
done
if systemctl is-active --quiet "$SERVICE_NAME"; then
ok "Service is running"
else
echo ""
echo "Service failed to start. Recent logs:"
journalctl -u "$SERVICE_NAME" -n 20 --no-pager
fail "Check full logs: journalctl -u $SERVICE_NAME"
fi
# --- Summary ---
API_SECRET_DISPLAY=$(grep -oP 'secret:\s*"\K[^"]+' "$CONFIG_FILE" 2>/dev/null || echo "<see config>")
cat <<EOF
=============================================
shroud installed successfully
=============================================
Config: ${CONFIG_FILE}
State: ${DATA_DIR}/state.yaml
Binary: ${BINARY}
User: ${SERVICE_USER}
Service management:
systemctl start ${SERVICE_NAME}
systemctl stop ${SERVICE_NAME}
systemctl restart ${SERVICE_NAME}
systemctl status ${SERVICE_NAME}
Logs (journald):
journalctl -u ${SERVICE_NAME} -f # follow
journalctl -u ${SERVICE_NAME} --since today # today's logs
journalctl -u ${SERVICE_NAME} -p err # errors only
API: http://${HOSTNAME}:${API_PORT}/${API_SECRET_DISPLAY}/server
Quick start:
${BINARY} key add -n user1 -c ${CONFIG_FILE}
${BINARY} key list -c ${CONFIG_FILE}
EOF
if [[ "$AWG_ENABLED" == "true" ]]; then
cat <<EOF
AmneziaWG: UDP :${AWG_PORT} (+ HTTP/3 mux)
EOF
[[ -n "$AWG_DOMAIN" ]] && echo " HTTP/3: https://${AWG_DOMAIN}:${AWG_PORT}/"
cat <<EOF
Download client config:
curl -s http://127.0.0.1:${API_PORT}/${API_SECRET_DISPLAY}/access-keys/<ID>/awg-config
EOF
fi
if [[ "$VLESS_ENABLED" == "true" ]]; then
cat <<EOF
VLESS+REALITY: TCP :${VLESS_PORT}
Generate share link:
${BINARY} vless share <key-id> -c ${CONFIG_FILE}
Re-run auto-detection:
${BINARY} vless autodetect --write -c ${CONFIG_FILE}
EOF
fi
echo " Open the Shadowsocks port once keys are created (see 'key list')."
echo ""