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>
280 lines
12 KiB
Bash
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
|