#!/usr/bin/env bash
#
# outline-distro installer (Ubuntu)
#
# Builds from source and installs outline-distro 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/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"
UNIT_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
REPO_URL="https://sourcecraft.dev/bigbes/outline-distro.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"
# --- Parse arguments ---
while [[ $# -gt 0 ]]; do
case "$1" in
--awg) AWG_ENABLED="true"; shift ;;
--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)
--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 "Outline Distro 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/outline-distro/
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)
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}"
domain: "${AWG_DOMAIN}"
cert_cache: ${DATA_DIR}/certs
acme_http_port: 80
jc: 4
jmin: 64
jmax: 256
s1: 32
s2: 28
s3: 20
s4: 25
h1: "50000-100000"
h2: "150000-200000"
h3: "250000-300000"
h4: "350000-400000"
state_file: ${DATA_DIR}/state.yaml
YAML
chown "$SERVICE_USER:$SERVICE_USER" "$CONFIG_FILE"
chmod 640 "$CONFIG_FILE"
ok "Config: $CONFIG_FILE"
fi
# --- Install systemd unit ---
info "Installing systemd unit..."
# Use the bundled unit file if present, otherwise generate one.
BUNDLED_UNIT="$INSTALL_DIR/dist/outline-distro.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
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 "outline-distro 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
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)"
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-outline-distro.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
=============================================
outline-distro 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
echo " Open the Shadowsocks port once keys are created (see 'key list')."
echo ""