Files
Hammerhead/deploy/setup.sh
T
VeillaxandClaude Sonnet 5.5 536284ec75
Release / release (push) Failing after 13m6s
CI / test (push) Failing after 18m11s
Give Hammerhead its own edge with a built-in dashboard route; setup.sh installs it with Blacktip instead of Caddy
HAMMERHEAD_SELF_DOMAIN creates an edge for this machine, writes its token to <data>/self-edge.env
and adds an https route for the dashboard to that edge's config, so no reverse proxy is needed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 15:38:16 -06:00

280 lines
12 KiB
Bash

#!/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://<domain> 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 <domain>: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 ;;
aarch64 | arm64) ARCH=arm64 ;;
armv7* | armv6* | arm) ARCH=arm ;;
*) die "unsupported CPU: $(uname -m)" ;;
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 <app> [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 <file> <KEY> <value>: 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 <file> <PREFIX>: 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 <service>
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 <hammerhead url> <token> <version>: 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 <site 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 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