diff --git a/README.md b/README.md index 0b6317b..975b30d 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,18 @@ ports. Then open ports on the **Resources** page. Releases are built by Gitea Actions. Pushing a tag such as `v0.2.0` runs `.gitea/workflows/release.yml`, which tests the code, builds Hammerhead for Linux (amd64, arm64, armv7), Windows (amd64) and macOS (amd64, arm64), publishes the archives with a `SHA256SUMS` file on the Releases page, and pushes a multi-architecture container image to this server's registry (`git.veillax.com/veillax/hammerhead`). Every push and pull request runs `.gitea/workflows/ci.yml`: formatting, vet, tests, and a check that every release platform still compiles. +**Fastest: one script on a fresh VPS** + +`deploy/setup.sh` (also inside the Linux archives) installs everything for you on a systemd Linux: the service, a generated master key, the firewall rules, and optionally HTTPS. + +``` +sudo sh setup.sh --domain hammerhead.example.com # Hammerhead + Caddy for HTTPS +sudo sh setup.sh edge --hammerhead https://hammerhead.example.com --token --ports 25565,30000-30100 +sudo sh setup.sh agent --hammerhead https://hammerhead.example.com --token +``` + +`edge` installs Blacktip (with its bundled Traefik) and lists every IP address on the machine that ports can be opened on; `agent` installs Bamboo on the machine that hosts your services. All three can run on one machine or on three. + **On a Linux server (systemd)** ``` diff --git a/deploy/build.sh b/deploy/build.sh index dfa7965..b79a22f 100644 --- a/deploy/build.sh +++ b/deploy/build.sh @@ -36,7 +36,7 @@ for t in $TARGETS; do [ -f "$f" ] && cp "$f" "$stage/" done if [ "$os" = linux ]; then - cp deploy/install.sh "deploy/$NAME.service" deploy/docker-compose.yml "$stage/" + cp deploy/install.sh deploy/setup.sh "deploy/$NAME.service" deploy/docker-compose.yml "$stage/" chmod +x "$stage/install.sh" fi if [ "$os" = windows ] && command -v zip >/dev/null 2>&1; then diff --git a/deploy/setup.sh b/deploy/setup.sh new file mode 100644 index 0000000..33e912f --- /dev/null +++ b/deploy/setup.sh @@ -0,0 +1,215 @@ +#!/bin/sh +# One-shot setup for a VPS. Run as root. +# +# sh setup.sh [--domain hh.example.com] [--version v0.1.0] Hammerhead (dashboard and API) +# sh setup.sh edge --hammerhead URL --token TOKEN [--ports 25565,30000-30100] [--version V] +# sh setup.sh agent --hammerhead URL --token TOKEN [--version V] +# +# Hammerhead: installs it as a systemd service (with a generated master key, kept in +# /etc/hammerhead/hammerhead.env), and with --domain also Caddy, which gets and renews +# the HTTPS certificate for that name. DNS for the name must already point here. +# edge: installs Blacktip, with its bundled Traefik, on this machine (or any other VPS), +# opens the firewall and lists every IP address it can open ports on. +# agent: installs Bamboo, the site agent, on the machine that hosts your services. +# +# Edge and agent need a token, which only exists once Hammerhead is up: 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= HH= TOKEN= PORTS= +while [ $# -gt 0 ]; do + case "$1" in + --domain) DOMAIN=$2; shift 2 ;; + --version) VERSION=$2; shift 2 ;; + --hammerhead) HH=$2; shift 2 ;; + --token) TOKEN=$2; shift 2 ;; + --ports) PORTS=$2; shift 2 ;; + -h | --help) sed -n '2,16p' "$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 : downloads that app's release for this CPU, verifies it and runs its install.sh +fetch_install() { + app=$1 + ver=${VERSION:-$(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 +} + +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"; } +} + +case $MODE in +hammerhead) + fetch_install hammerhead + ENVF=/etc/hammerhead/hammerhead.env + if ! grep -q '^HAMMERHEAD_MASTER_KEY=.' "$ENVF"; then + say "generating the master key" + set_env "$ENVF" HAMMERHEAD_MASTER_KEY "$(/usr/local/bin/hammerhead -genkey)" + echo "Saved in $ENVF. Back that file up: without the key the encrypted secrets in the database can't be read." + fi + if [ -n "$DOMAIN" ]; then + set_env "$ENVF" HAMMERHEAD_URL "https://$DOMAIN" + # only Caddy needs to reach it + sed -i 's|-listen :8080|-listen 127.0.0.1:8080|' /etc/systemd/system/hammerhead.service + 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 + 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 restart hammerhead + wait_active hammerhead + say "Hammerhead is running" + echo "Open $URL to create the first administrator." + echo + echo "Next, in the dashboard: Server -> Edges -> add an edge, then on the machine that has your public IPs run" + echo " sh setup.sh edge --hammerhead $URL --token " + echo "and on each machine that hosts your services (home server, game server...) run" + echo " sh setup.sh agent --hammerhead $URL --token " + echo "Either can be this same machine." + ;; +edge) + [ -n "$HH" ] && [ -n "$TOKEN" ] || die "edge needs --hammerhead URL and --token TOKEN (Server -> Edges -> Show command)" + fetch_install blacktip + ENVF=/etc/blacktip/blacktip.env + set_env "$ENVF" BLACKTIP_HAMMERHEAD "$HH" + set_env "$ENVF" BLACKTIP_TOKEN "$TOKEN" + 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." + 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 + ENVF=/etc/bamboo/bamboo.env + set_env "$ENVF" BAMBOO_HAMMERHEAD "$HH" + set_env "$ENVF" BAMBOO_TOKEN "$TOKEN" + 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