#!/bin/sh # One-shot setup for a VPS. Run as root. # # sh setup.sh --domain hh.example.com [options] Hammerhead and a Blacktip edge on this machine # sh setup.sh edge --hammerhead URL --token TOKEN [options] another edge # sh setup.sh agent --hammerhead URL --token TOKEN [options] a site agent # # With --domain, this installs Hammerhead (the dashboard) and a Blacktip edge on the same # machine. The edge serves the dashboard itself at https:// with an automatic # certificate, so no separate web server is needed. DNS for the domain must already point here, # and ports 80 and 443 must be free (stop Pangolin, Caddy, nginx... first). # # Options: # --version V release of Hammerhead (or of the edge/agent for those modes); default newest # --edge-version V release of the Blacktip installed next to Hammerhead; default newest # --ports 25565,30000-30100 also open these (TCP and UDP) in the firewall # --tunnel HOST:PORT the edge's public WireGuard address; default :51820 # --caddy old style: Hammerhead alone, with Caddy providing HTTPS (no edge here) # --no-auto-update don't let the programs update themselves # # Without --domain, Hammerhead alone is installed on port 8080 over plain HTTP. # Edge and agent need a token: create the edge under Server -> Edges (or the site under Sites) # in the dashboard and copy the values from "Show command". # Safe to run again: it upgrades and keeps your settings and data. set -eu BASE=${SHARKS_BASE:-https://git.veillax.com/Veillax} MODE=hammerhead case "${1:-}" in edge | agent) MODE=$1; shift ;; esac DOMAIN= VERSION= EDGE_VERSION= HH= TOKEN= PORTS= TUNNEL= CADDY=0 AUTOUP=1 while [ $# -gt 0 ]; do case "$1" in --domain) DOMAIN=$2; shift 2 ;; --version) VERSION=$2; shift 2 ;; --edge-version) EDGE_VERSION=$2; shift 2 ;; --hammerhead) HH=$2; shift 2 ;; --token) TOKEN=$2; shift 2 ;; --ports) PORTS=$2; shift 2 ;; --tunnel) TUNNEL=$2; shift 2 ;; --caddy) CADDY=1; shift ;; --no-auto-update) AUTOUP=0; shift ;; -h | --help) sed -n '2,24p' "$0"; exit 0 ;; *) echo "unknown option $1 (see --help)" >&2; exit 2 ;; esac done say() { printf '\n== %s\n' "$*"; } die() { echo "error: $*" >&2; exit 1; } [ "$(id -u)" = 0 ] || die "run as root: sudo sh $0 $*" command -v systemctl > /dev/null 2>&1 || die "this needs a systemd-based Linux (Debian, Ubuntu, Fedora, RHEL, Arch...)" case "$(uname -m)" in x86_64 | amd64) ARCH=amd64 ;; *) die "releases are published for x86-64 Linux only, not $(uname -m); build it yourself with: TARGETS=linux/arm64 sh deploy/build.sh" ;; esac pkg_install() { # installs packages with whichever package manager there is if command -v apt-get > /dev/null 2>&1; then apt-get update -qq || true DEBIAN_FRONTEND=noninteractive apt-get install -y "$@" elif command -v dnf > /dev/null 2>&1; then dnf install -y "$@" elif command -v yum > /dev/null 2>&1; then yum install -y "$@" elif command -v pacman > /dev/null 2>&1; then pacman -S --noconfirm --needed "$@" else die "install these yourself and run again: $*" fi } need() { command -v "$1" > /dev/null 2>&1 || { say "installing $1"; pkg_install "$1"; }; } need curl need tar need git latest() { # newest release tag of repo $1 (read from git, so no API token is needed) git ls-remote --tags --refs "$BASE/$1" 2> /dev/null | sed 's|.*refs/tags/||' | grep -E '^v[0-9]' | sort -V | tail -n 1 } # fetch_install [version]: downloads that app's release for this CPU, verifies it and runs its install.sh fetch_install() { app=$1 ver=${2:-$(latest "$app")} [ -n "$ver" ] || die "couldn't find a release of $app at $BASE/$app (use --version, or SHARKS_BASE for a mirror)" dir=$(mktemp -d) say "downloading $app $ver ($ARCH)" name="${app}_${ver}_linux_${ARCH}" curl -fsSL -o "$dir/$name.tar.gz" "$BASE/$app/releases/download/$ver/$name.tar.gz" || die "no $app $ver release for linux/$ARCH" curl -fsSL -o "$dir/SHA256SUMS" "$BASE/$app/releases/download/$ver/SHA256SUMS" || die "couldn't download the checksums" (cd "$dir" && grep " $name.tar.gz\$" SHA256SUMS | sha256sum -c -) || die "checksum mismatch: not installing" tar -xzf "$dir/$name.tar.gz" -C "$dir" (cd "$dir/$name" && sh ./install.sh) rm -rf "$dir" } # set_env : sets KEY=value, replacing the example line or an older value set_env() { f=$1 k=$2 v=$3 if grep -q "^#\?$k=" "$f"; then tmp=$(mktemp) awk -v k="$k" -v v="$v" 'BEGIN{done=0} { if (!done && ($0 ~ "^#?"k"=")) { print k"="v; done=1 } else print }' "$f" > "$tmp" cat "$tmp" > "$f" rm -f "$tmp" else echo "$k=$v" >> "$f" fi } # auto_update : lets the program update itself unless --no-auto-update was given auto_update() { [ "$AUTOUP" = 1 ] && set_env "$1" "$2_AUTO_UPDATE" 1 return 0 } firewall_open() { # firewall_open 80/tcp 443/tcp ... if command -v ufw > /dev/null 2>&1 && ufw status 2> /dev/null | grep -q "Status: active"; then for p in "$@"; do ufw allow "$p" > /dev/null; done echo "opened in ufw: $*" elif command -v firewall-cmd > /dev/null 2>&1 && firewall-cmd --state > /dev/null 2>&1; then for p in "$@"; do firewall-cmd --permanent --add-port="$p" > /dev/null; done firewall-cmd --reload > /dev/null echo "opened in firewalld: $*" else echo "No active ufw or firewalld found. If this machine has a firewall, or your host has a cloud firewall / security group, open: $*" fi } public_ips() { # global addresses actually configured on this machine ip -o addr show scope global 2> /dev/null | awk '{print $4}' | cut -d/ -f1 || true } wait_active() { # wait_active sleep 2 systemctl is-active --quiet "$1" || { journalctl -u "$1" -n 20 --no-pager; die "$1 didn't start"; } } # listeners on 80/443 that would stop an edge serving websites port_users() { ss -H -ltnp '( sport = :80 or sport = :443 )' 2> /dev/null | grep -v '"blacktip"\|"traefik"' || true } # edge_setup : Blacktip with its bundled Traefik, configured but not started edge_setup() { fetch_install blacktip "$3" ENVF=/etc/blacktip/blacktip.env set_env "$ENVF" BLACKTIP_HAMMERHEAD "$1" set_env "$ENVF" BLACKTIP_TOKEN "$2" auto_update "$ENVF" BLACKTIP chmod 600 "$ENVF" say "network" # WireGuard moves a lot of UDP; the stock socket buffers are small printf 'net.core.rmem_max=7500000\nnet.core.wmem_max=7500000\n' > /etc/sysctl.d/90-blacktip.conf sysctl -q -p /etc/sysctl.d/90-blacktip.conf > /dev/null 2>&1 || true list="80/tcp 443/tcp 51820/udp" if [ -n "$PORTS" ]; then # 25565,30000-30100: each is opened for TCP and UDP (the dashboard decides which one is used) for p in $(echo "$PORTS" | tr ',' ' '); do list="$list $p/tcp $p/udp" done fi # ufw wants a range as 30000:30100, firewalld as 30000-30100 if command -v ufw > /dev/null 2>&1; then list=$(echo "$list" | tr '-' ':'); fi # shellcheck disable=SC2086 firewall_open $list ips=$(public_ips) echo echo "Blacktip can open ports on these addresses of this machine:" echo "$ips" | sed 's/^/ /' if [ "$(echo "$ips" | grep -c .)" -lt 2 ]; then echo "Only one address found. Extra IPs must be added to this machine's network configuration first." echo "A provider's floating or 1:1 NAT address that isn't listed above can't be bound by Blacktip." fi echo "In the dashboard each port picks which of these addresses it listens on." echo "Ports you open later in the dashboard must also be allowed in the firewall: run this again with --ports." } case $MODE in hammerhead) fetch_install hammerhead "$VERSION" ENVF=/etc/hammerhead/hammerhead.env auto_update "$ENVF" HAMMERHEAD if ! grep -q '^HAMMERHEAD_MASTER_KEY=.' "$ENVF"; then say "generating the master key" set_env "$ENVF" HAMMERHEAD_MASTER_KEY "$(/usr/local/lib/hammerhead/hammerhead -genkey)" echo "Saved in $ENVF. Back that file up: without the key the encrypted secrets in the database can't be read." fi UNIT=/etc/systemd/system/hammerhead.service if [ -n "$DOMAIN" ]; then set_env "$ENVF" HAMMERHEAD_URL "https://$DOMAIN" # only something on this machine needs to reach it sed -i 's|-listen :8080|-listen 127.0.0.1:8080|' "$UNIT" if [ "$CADDY" = 1 ]; then systemctl daemon-reload if ! command -v caddy > /dev/null 2>&1; then say "installing Caddy (HTTPS certificates for $DOMAIN)" pkg_install caddy || die "couldn't install Caddy from this distribution; see https://caddyserver.com/docs/install and proxy $DOMAIN to 127.0.0.1:8080" fi mkdir -p /etc/caddy printf '%s {\n\treverse_proxy 127.0.0.1:8080\n}\n' "$DOMAIN" > /etc/caddy/Caddyfile systemctl enable caddy > /dev/null 2>&1 || true systemctl restart caddy firewall_open 80/tcp 443/tcp URL="https://$DOMAIN" else # the dashboard is served by the edge on this machine, so Hammerhead gets its own edge set_env "$ENVF" HAMMERHEAD_SELF_DOMAIN "$DOMAIN" [ -z "$TUNNEL" ] || set_env "$ENVF" HAMMERHEAD_SELF_TUNNEL "$TUNNEL" URL="https://$DOMAIN" fi else firewall_open 8080/tcp ip=$(public_ips | head -n 1) URL="http://${ip:-this-server}:8080" echo "No --domain given, so the dashboard is plain HTTP. Put it behind HTTPS before you create real logins (run this again with --domain)." fi systemctl daemon-reload systemctl restart hammerhead wait_active hammerhead say "Hammerhead is running" if [ -n "$DOMAIN" ] && [ "$CADDY" = 0 ]; then TOKENFILE=/var/lib/hammerhead/self-edge.env for i in 1 2 3 4 5 6 7 8 9 10; do [ -f "$TOKENFILE" ] && break; sleep 1; done [ -f "$TOKENFILE" ] || die "Hammerhead didn't create $TOKENFILE; check: journalctl -u hammerhead" edge_token=$(sed -n 's/^BLACKTIP_TOKEN=//p' "$TOKENFILE") edge_setup http://127.0.0.1:8080 "$edge_token" "$EDGE_VERSION" # something else on 80/443 would stop the dashboard route from coming up if systemctl is-active --quiet caddy 2> /dev/null; then say "stopping Caddy" echo "Caddy was serving this site and holds ports 80/443, which the edge needs now." systemctl disable --now caddy > /dev/null 2>&1 || true fi others=$(port_users) if [ -n "$others" ]; then echo "$others" >&2 die "something else is listening on port 80 or 443 (above). Stop it (for Pangolin: docker compose stop), then run: systemctl restart blacktip" fi systemctl restart blacktip wait_active blacktip say "Blacktip is running" echo "The dashboard is served by the edge on this machine at $URL once the certificate is issued" echo "(a few seconds; $DOMAIN must point at this server). Until then, from your own computer:" echo " ssh -L 8080:127.0.0.1:8080 you@this-server then open http://localhost:8080" fi echo echo "Open $URL to create the first administrator." echo echo "Next: on each machine that hosts your services (home server, game server...) run" echo " sh setup.sh agent --hammerhead $URL --token " echo "with the token from Sites -> add a site. For more public servers add edges under Server -> Edges and run" echo " sh setup.sh edge --hammerhead $URL --token " ;; edge) [ -n "$HH" ] && [ -n "$TOKEN" ] || die "edge needs --hammerhead URL and --token TOKEN (Server -> Edges -> Show command)" edge_setup "$HH" "$TOKEN" "$VERSION" systemctl restart blacktip wait_active blacktip say "Blacktip is running" ;; agent) [ -n "$HH" ] && [ -n "$TOKEN" ] || die "agent needs --hammerhead URL and --token TOKEN (Sites -> Show command)" fetch_install bamboo "$VERSION" ENVF=/etc/bamboo/bamboo.env set_env "$ENVF" BAMBOO_HAMMERHEAD "$HH" set_env "$ENVF" BAMBOO_TOKEN "$TOKEN" auto_update "$ENVF" BAMBOO chmod 600 "$ENVF" if command -v docker > /dev/null 2>&1 && getent group docker > /dev/null 2>&1; then usermod -aG docker bamboo set_env "$ENVF" BAMBOO_DOCKER 1 echo "Docker found: Bamboo will list your containers as targets." fi systemctl restart bamboo wait_active bamboo say "Bamboo is running (it only connects outward, so there is nothing to open)" ;; esac