#!/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 /install.sh | bash # SS only # curl -sSL /install.sh | bash -s -- --awg # SS + AmneziaWG # curl -sSL /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" <&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" </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 "") cat </awg-config EOF fi if [[ "$VLESS_ENABLED" == "true" ]]; then cat < -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 ""