2 Commits
Author SHA1 Message Date
VeillaxandClaude Sonnet 5.5 7db686eeb0 Retry PROXY e2e reads while the tunnel handshake completes
CI / test (push) Successful in 7m37s
Release / release (push) Successful in 8m25s
The first connection through the tunnel can exceed the read deadline on a slow CI runner.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 12:22:12 -06:00
VeillaxandClaude Sonnet 5.5 58d1cfe9a3 Add deploy/setup.sh for one-command VPS setup
CI / test (push) Successful in 1m41s
Release / release (push) Failing after 3m16s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 04:25:54 -06:00
4 changed files with 232 additions and 5 deletions

No files matched your search

+12
View File
@@ -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 <edge token> --ports 25565,30000-30100
sudo sh setup.sh agent --hammerhead https://hammerhead.example.com --token <site 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)**
```
+1 -1
View File
@@ -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
+215
View File
@@ -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 <app>: 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 <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
}
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"; }
}
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 <edge token>"
echo "and on each machine that hosts your services (home server, game server...) run"
echo " sh setup.sh agent --hammerhead $URL --token <site 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
+4 -4
View File
@@ -91,7 +91,10 @@ func proxyBackend(t *testing.T, seen chan<- string) int {
func askFrom(t *testing.T, from, addr, send string) string {
t.Helper()
var last error
for i := 0; i < 50; i++ {
// The first connection through the tunnel can wait for the WireGuard handshake, which is
// slow on a busy CI runner, so a read timeout is retried until the overall deadline.
deadline := time.Now().Add(30 * time.Second)
for time.Now().Before(deadline) {
d := net.Dialer{LocalAddr: &net.TCPAddr{IP: net.ParseIP(from)}, Timeout: time.Second}
c, err := d.Dial("tcp", addr)
if err == nil {
@@ -102,9 +105,6 @@ func askFrom(t *testing.T, from, addr, send string) string {
if rerr == nil {
return strings.TrimSpace(line)
}
if ne, ok := rerr.(net.Error); ok && ne.Timeout() {
t.Fatalf("%s accepted the connection but never answered: %v", addr, rerr) // a real hang, not a startup race
}
last = rerr
} else {
last = err