- dl_content no longer runs with no timeout at all: every attempt carries --connect-timeout, and a content transfer that connects then crawls below 1 KiB/s for 30s is aborted so the next try can take over (a slow but moving download is still never cut off by a total cap) - verify the release binary by looking up its SHA256SUMS entry explicitly instead of `sha256sum -c --ignore-missing`, which quietly succeeded when the manifest held no line for the arch - copy the local vps-prebuild instead of moving it, so a later failure cannot destroy the only snapshot along with the temp dir - fail loudly when lib-download.sh is missing instead of degrading silently - drop the unused dl_handshake helper, accept binary-mode `*asset` lines, and lower-case v4_forward in 30-haproxy so True/TRUE match like 00-ip-ask
379 lines
18 KiB
Bash
379 lines
18 KiB
Bash
#!/usr/bin/env bash
|
|
# 30-haproxy.sh — install the HAProxy domain proxy, migrate away from Traefik.
|
|
#
|
|
# WHAT THIS SCRIPT OWNS
|
|
# 1. The HAProxy binary, pinned to the release BRANCH below (official HAProxy
|
|
# performance packages; the branch is the only thing a future major
|
|
# upgrade has to change).
|
|
# 2. The static entry configuration /etc/haproxy/haproxy.cfg. The panel
|
|
# publishes per-generation configs and repoints /etc/haproxy/current at
|
|
# them; this file only includes the current one.
|
|
# 3. The systemd unit (overriding any unit the distribution package ships).
|
|
# 4. The one-way migration OFF Traefik (see "TRAEFIK REMOVAL" below).
|
|
#
|
|
# ---------------------------------------------------------------------------
|
|
# UPGRADING FROM A TRAEFIK-ERA INSTALL (read before touching this script)
|
|
# ---------------------------------------------------------------------------
|
|
# vpsmgr used to front user domains with Traefik. An operator may run an old
|
|
# version for a long time before upgrading, so this script must converge a
|
|
# Traefik host onto HAProxy in ONE run, and never leave both proxies on the box:
|
|
#
|
|
# * net.traefik (the config key) was RENAMED to net.haproxy. Values are
|
|
# unchanged. The Go side reads BOTH (see cfg.Load / cfg.FillAuto) so an old
|
|
# config.yaml or an old installer exporting VPSMGR_TRAEFIK still works.
|
|
# * Traefik's program, unit and service account are REMOVED here. Only its
|
|
# config FILE /etc/traefik/traefik.yaml is kept (harmless, no longer read).
|
|
# * The old /etc/traefik/dynamic directory is REMOVED: it holds per-domain
|
|
# YAML that nothing reads any more, and keeping it would invite confusion
|
|
# about which proxy is live.
|
|
# * The pre-rename marker /etc/vpsmgr/.install-traefik-off is still honored
|
|
# (it records "80/443 was taken, so keep the proxy installed but down"),
|
|
# and is deleted once the new state has been recorded.
|
|
# * Ordering is deliberate: install + validate the new proxy FIRST, stop
|
|
# Traefik, start HAProxy, and only then delete the Traefik files. A failure
|
|
# before the start leaves the box on Traefik and aborts the install loudly.
|
|
#
|
|
# Do NOT add a "keep Traefik as a fallback" branch: a fallback would mean two
|
|
# proxies fighting for 80/443 after a reboot.
|
|
# ---------------------------------------------------------------------------
|
|
set -uo pipefail
|
|
|
|
log(){ echo "[30] $*"; }
|
|
die(){ echo "[30] error: $*" >&2; exit 1; }
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. Pinned version: BRANCH is the contract, the patch level floats within it.
|
|
# ---------------------------------------------------------------------------
|
|
# Changing HAPROXY_BRANCH is the ONLY edit needed for a future major upgrade.
|
|
# It must stay in sync with cfg.HaproxyBranch in the panel (the panel refuses
|
|
# to manage a binary from a different branch) and with the two places below:
|
|
# HAPROXY_REPO_SLUG -> the apt source path (…/performance/<distro>/ha<XY>)
|
|
# HAPROXY_BRANCH_RE -> the version assertion in verify_version
|
|
# A wrong slug makes `apt-get update` 404 and the install fails loudly; a wrong
|
|
# regex makes verify_version fail. Neither can silently install the wrong proxy.
|
|
HAPROXY_BRANCH="3.4"
|
|
HAPROXY_BRANCH_RE='^3\.4\.'
|
|
HAPROXY_REPO_SLUG="ha34"
|
|
HAPROXY_KEY_URL="https://pks.haproxy.com/linux/community/RPM-GPG-KEY-HAProxy"
|
|
HAPROXY_KEY="/usr/share/keyrings/HAPROXY-key-community.asc"
|
|
HAPROXY_PKG="haproxy-awslc"
|
|
|
|
# The published layout root. Must match cfg.DefaultProxyDir.
|
|
HAPROXY_DIR="/etc/haproxy"
|
|
ENTRY_CFG="$HAPROXY_DIR/haproxy.cfg"
|
|
CURRENT_LINK="$HAPROXY_DIR/current"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. Distribution keys for the apt source. The vendor repo is per-distribution
|
|
# AND per-branch, so both have to be resolved before writing the source.
|
|
# ---------------------------------------------------------------------------
|
|
OS_ID=""
|
|
OS_CODENAME=""
|
|
if [[ -r /etc/os-release ]]; then
|
|
# shellcheck disable=SC1091
|
|
. /etc/os-release
|
|
OS_ID="${ID:-}"
|
|
OS_CODENAME="${VERSION_CODENAME:-}"
|
|
fi
|
|
case "$OS_ID" in
|
|
debian|ubuntu) ;;
|
|
*) die "unsupported distribution '${OS_ID:-unknown}' for the HAProxy package repository (need debian or ubuntu)" ;;
|
|
esac
|
|
[[ -n "$OS_CODENAME" ]] || die "could not determine VERSION_CODENAME from /etc/os-release"
|
|
|
|
# Surface any previous HAProxy install early. An OLDER branch (e.g. 3.2) is
|
|
# upgraded in place by the branch switch below, which is expected; the version
|
|
# assertion at the end is what proves the switch worked.
|
|
if [[ -x /usr/sbin/haproxy ]]; then
|
|
log "existing haproxy: $(/usr/sbin/haproxy -v 2>/dev/null | head -1)"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. Install the pinned branch from the official vendor repository.
|
|
# ---------------------------------------------------------------------------
|
|
install_repo(){
|
|
# gpg + curl + ca-certificates are needed for the key and the repo; the
|
|
# installer's 00-check step normally provides them, this covers a standalone
|
|
# run of this script on a bare host.
|
|
for p in curl ca-certificates gpg; do
|
|
dpkg -s "$p" >/dev/null 2>&1 || {
|
|
log "installing $p (needed for the HAProxy repository)"
|
|
apt-get update -qq
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "$p"
|
|
}
|
|
done
|
|
|
|
if [[ ! -s "$HAPROXY_KEY" ]]; then
|
|
# download_keyring: the vendor key endpoint is plain HTTPS (no TLS client
|
|
# auth), so a fetch failure is fatal — never continue without the key, or
|
|
# apt would trust an unverified repository.
|
|
log "fetching the HAProxy repository key"
|
|
mkdir -p /usr/share/keyrings
|
|
curl -fsSL --max-time 60 -o "$HAPROXY_KEY.tmp" "$HAPROXY_KEY_URL" \
|
|
|| die "could not download the HAProxy repository key"
|
|
mv "$HAPROXY_KEY.tmp" "$HAPROXY_KEY"
|
|
chmod 0644 "$HAPROXY_KEY"
|
|
fi
|
|
|
|
local src="/etc/apt/sources.list.d/haproxy.list"
|
|
local want="deb [signed-by=$HAPROXY_KEY] https://www.haproxy.com/download/haproxy/performance/$OS_ID/$HAPROXY_REPO_SLUG $OS_CODENAME main"
|
|
if [[ ! -f "$src" ]] || [[ "$(cat "$src" 2>/dev/null)" != "$want" ]]; then
|
|
# Any older source line (a previous branch) is replaced wholesale: leaving
|
|
# both would let apt pick whichever branch has the higher version.
|
|
printf '%s\n' "$want" > "$src"
|
|
chmod 0644 "$src"
|
|
log "configured the HAProxy $HAPROXY_BRANCH repository for $OS_ID/$OS_CODENAME"
|
|
# A stale package list from the previous branch must not be reused.
|
|
rm -f /var/lib/apt/lists/*haproxy* 2>/dev/null || true
|
|
fi
|
|
apt-get update -qq || die "apt-get update failed (check the HAProxy repository)"
|
|
}
|
|
|
|
verify_version(){
|
|
local bin="${1:-/usr/sbin/haproxy}"
|
|
[[ -x "$bin" ]] || die "HAProxy binary not found at $bin"
|
|
local out
|
|
out="$("$bin" -v 2>/dev/null | head -1)"
|
|
local ver
|
|
ver="$(awk '{print $3}' <<<"$out")"
|
|
# The vendor package reports "HAProxy version 3.4.4-0+ha34+deb13u1 2026/08/27".
|
|
if [[ ! "$ver" =~ $HAPROXY_BRANCH_RE ]]; then
|
|
die "installed HAProxy is '$ver', which is not on the pinned branch $HAPROXY_BRANCH ($out)
|
|
a newer/older branch source is still configured — fix /etc/apt/sources.list.d/haproxy.list"
|
|
fi
|
|
log "haproxy $ver (branch $HAPROXY_BRANCH)"
|
|
}
|
|
|
|
install_repo
|
|
if ! dpkg -s "$HAPROXY_PKG" >/dev/null 2>&1; then
|
|
log "installing $HAPROXY_PKG (HAProxy $HAPROXY_BRANCH, AWS-LC build)"
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "$HAPROXY_PKG" \
|
|
|| die "could not install $HAPROXY_PKG"
|
|
else
|
|
# Already present: make sure it is the newest patch of the pinned branch.
|
|
# `--only-upgrade` never installs anything new, so a no-op stays a no-op.
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --only-upgrade "$HAPROXY_PKG" >/dev/null 2>&1 || true
|
|
fi
|
|
verify_version /usr/sbin/haproxy
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. Directories. The panel (user 'vps') owns the layout root and publishes
|
|
# generations into it; HAProxy only READS them, so the dirs stay 0755.
|
|
# ---------------------------------------------------------------------------
|
|
mkdir -p "$HAPROXY_DIR" /run/haproxy
|
|
# Keep any configuration an operator already had before we overwrite it. This
|
|
# is a one-time courtesy for the very first install of this script on a host
|
|
# that ran the distribution's haproxy package.
|
|
if [[ -f "$ENTRY_CFG" ]] && ! grep -q "Managed by vpsmgr" "$ENTRY_CFG" 2>/dev/null; then
|
|
cp -a "$ENTRY_CFG" "$ENTRY_CFG.before-vpsmgr" 2>/dev/null || true
|
|
log "kept the previous $ENTRY_CFG as $ENTRY_CFG.before-vpsmgr"
|
|
fi
|
|
cp "$ROOT/configs/haproxy/haproxy.cfg" "$ENTRY_CFG"
|
|
chmod 0644 "$ENTRY_CFG"
|
|
|
|
# A generation must exist before the unit is ever started, because the entry
|
|
# config resolves its map paths through "$CURRENT_LINK/maps/" and HAProxy
|
|
# refuses to start when a map cannot be opened. The panel republishes this from
|
|
# the DB at startup; this is only the bootstrap (empty maps = every request
|
|
# falls through to the reject backends).
|
|
# The unit loads BOTH the entry config and this file, so "the generation exists"
|
|
# means domains.cfg plus both maps.
|
|
if [[ ! -f "$CURRENT_LINK/domains.cfg" ]] || [[ ! -f "$CURRENT_LINK/maps/http.map" ]]; then
|
|
log "bootstrapping an empty HAProxy generation"
|
|
gen="$HAPROXY_DIR/releases/000001"
|
|
mkdir -p "$gen/maps"
|
|
cp "$ROOT/configs/haproxy/domains.cfg" "$gen/domains.cfg"
|
|
cp "$ROOT/configs/haproxy/maps/http.map" "$gen/maps/http.map"
|
|
cp "$ROOT/configs/haproxy/maps/sni.map" "$gen/maps/sni.map"
|
|
printf 'generation: 1\ndomains: 0\n' > "$gen/manifest"
|
|
printf '1\n' > "$HAPROXY_DIR/generation"
|
|
# Atomic switch: a temp link renamed over the real one, so a reader (or a
|
|
# concurrent panel publish) never observes a missing link.
|
|
ln -sfn "releases/000001" "$CURRENT_LINK.tmp" && mv -Tf "$CURRENT_LINK.tmp" "$CURRENT_LINK"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5. The unit. Debian's haproxy package ships /lib/systemd/system/haproxy.service
|
|
# plus an /etc/default/haproxy conffile; ours lives in /etc/systemd/system,
|
|
# which takes precedence, so exactly one process can ever own 80/443.
|
|
# ---------------------------------------------------------------------------
|
|
if [[ -f /lib/systemd/system/haproxy.service ]] || [[ -f /usr/lib/systemd/system/haproxy.service ]]; then
|
|
log "distribution haproxy unit present — overridden by /etc/systemd/system/haproxy.service"
|
|
fi
|
|
cp "$ROOT/configs/systemd/haproxy.service" /etc/systemd/system/haproxy.service
|
|
chmod 0644 /etc/systemd/system/haproxy.service
|
|
systemctl daemon-reload
|
|
|
|
# The haproxy account is created by the package's maintainer scripts. If a
|
|
# future package drops it (or we ever switch to a hand-built binary), create it
|
|
# here; the config's "user haproxy" would otherwise fail to start.
|
|
if ! id -u haproxy >/dev/null 2>&1; then
|
|
useradd --system --no-create-home --home-dir /nonexistent --shell /usr/sbin/nologin haproxy
|
|
log "created the 'haproxy' service account"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 6. Effective enablement: an IPv4 domain-proxy-eligible policy AND the
|
|
# domain-proxy switch. web-only qualifies; false does not.
|
|
# ---------------------------------------------------------------------------
|
|
V4_PROXY_ALLOWED=1
|
|
V4_FWD_EFFECTIVE="${VPSMGR_V4_FORWARD:-1}"
|
|
case "${V4_FWD_EFFECTIVE,,}" in
|
|
1|true|web-only) V4_PROXY_ALLOWED=1 ;;
|
|
*) V4_PROXY_ALLOWED=0 ;;
|
|
esac
|
|
|
|
# Proxy switch: installer override first (VPSMGR_HAPROXY, or the legacy
|
|
# VPSMGR_TRAEFIK an old installer exported), then an adopted config file
|
|
# (net.haproxy, or the pre-rename net.traefik key), then the default (enabled).
|
|
PROXY_CFG=""
|
|
pick_bool(){
|
|
case "${1,,}" in
|
|
1|true) PROXY_CFG=true ;;
|
|
0|false) PROXY_CFG=false ;;
|
|
*) die "$2 must be 1/0/true/false (got '$1')" ;;
|
|
esac
|
|
}
|
|
if [[ -n "${VPSMGR_HAPROXY:-}" ]]; then
|
|
pick_bool "$VPSMGR_HAPROXY" VPSMGR_HAPROXY
|
|
elif [[ -n "${VPSMGR_TRAEFIK:-}" ]]; then
|
|
pick_bool "$VPSMGR_TRAEFIK" VPSMGR_TRAEFIK
|
|
elif [[ -f /etc/vpsmgr/config.yaml ]]; then
|
|
# Read the new key when present, else the legacy one. Both are plain "true"/
|
|
# "false" values under the net: section.
|
|
v="$(grep -E '^\s+haproxy:' /etc/vpsmgr/config.yaml 2>/dev/null | awk -F': ' '{print $2}' | tr -d '"')"
|
|
if [[ -z "$v" ]]; then
|
|
v="$(grep -E '^\s+traefik:' /etc/vpsmgr/config.yaml 2>/dev/null | awk -F': ' '{print $2}' | tr -d '"')"
|
|
fi
|
|
[[ -n "$v" ]] && pick_bool "$v" config
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 7. Port check BEFORE touching Traefik, so a conflict can never leave the box
|
|
# with no proxy at all. A Traefik/HAProxy listener we already own is not a
|
|
# conflict (that is the normal upgrade path).
|
|
# ---------------------------------------------------------------------------
|
|
ports_busy(){
|
|
local port="$1" proc
|
|
while read -r line; do
|
|
[[ -z "$line" ]] && continue
|
|
proc="$(sed -n 's/.*users:(("\([^"]*\)".*/\1/p' <<<"$line")"
|
|
case "${proc##*/}" in
|
|
haproxy|traefik|vpsmgr) continue ;; # ours
|
|
esac
|
|
return 0
|
|
done < <(ss -H -tlnp 2>/dev/null | awk -v p=":$port" '$4 ~ p"$"')
|
|
return 1
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 8. TRAEFIK REMOVAL — one-way, after the new proxy is proven startable.
|
|
# ---------------------------------------------------------------------------
|
|
traefik_present(){
|
|
[[ -x /usr/local/bin/traefik ]] \
|
|
|| [[ -f /etc/systemd/system/traefik.service ]] \
|
|
|| [[ -d /etc/traefik ]] \
|
|
|| id -u traefik >/dev/null 2>&1
|
|
}
|
|
|
|
remove_traefik(){
|
|
traefik_present || return 0
|
|
log "removing the Traefik era install (one-way: HAProxy is the only proxy now)"
|
|
|
|
# Stop and disable FIRST: a disabled unit must not come back on reboot and
|
|
# race HAProxy for 80/443.
|
|
systemctl disable --now traefik.service >/dev/null 2>&1 || true
|
|
rm -f /etc/systemd/system/traefik.service
|
|
systemctl daemon-reload >/dev/null 2>&1 || true
|
|
|
|
# Program and service account.
|
|
rm -f /usr/local/bin/traefik
|
|
if id -u traefik >/dev/null 2>&1; then
|
|
userdel traefik >/dev/null 2>&1 || true
|
|
fi
|
|
# userdel leaves the same-named primary group behind on some systems; a
|
|
# stale group makes a later `useradd traefik` fail with exit 9.
|
|
if getent group traefik >/dev/null 2>&1; then
|
|
groupdel traefik >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
# Per-domain YAML from the old proxy. Nothing reads it now; leaving it would
|
|
# only confuse an operator debugging "which proxy is serving this domain".
|
|
rm -rf /etc/traefik/dynamic
|
|
# The config FILE is kept deliberately: it documents the previous setup and
|
|
# removing it would discard host-specific provider settings. It is inert.
|
|
[[ -f /etc/traefik/traefik.yaml ]] && log "kept /etc/traefik/traefik.yaml (no longer read)"
|
|
|
|
log "traefik removed: binary, unit, service account and dynamic rules"
|
|
}
|
|
|
|
# The marker records "80/443 was already taken when this host was installed, so
|
|
# keep the domain proxy installed but down". It predates the rename; honor it
|
|
# and then retire it (the state it describes now lives in net.haproxy).
|
|
if [[ -f /etc/vpsmgr/.install-traefik-off ]]; then
|
|
log "found .install-traefik-off — keeping the domain proxy installed but disabled"
|
|
PROXY_CFG=false
|
|
fi
|
|
[[ -n "$PROXY_CFG" ]] || PROXY_CFG=true
|
|
|
|
CONFLICT=""
|
|
if ports_busy 80; then CONFLICT="80"; fi
|
|
if ports_busy 443; then CONFLICT="${CONFLICT:+$CONFLICT and }443"; fi
|
|
|
|
want_start=1
|
|
if [[ "$V4_PROXY_ALLOWED" -eq 0 ]]; then
|
|
want_start=0
|
|
log "IPv4 inbound fully off — HAProxy installed but disabled (domains kept)"
|
|
elif [[ "$PROXY_CFG" != "true" ]]; then
|
|
want_start=0
|
|
log "net.haproxy is false — HAProxy installed but disabled (not started/autostarted; domains kept)"
|
|
elif [[ -n "$CONFLICT" ]]; then
|
|
want_start=0
|
|
log "port $CONFLICT is in use by another process — HAProxy installed but disabled"
|
|
log "free the port, then run: vps config set net.haproxy true"
|
|
fi
|
|
|
|
# The candidate configuration must be valid BEFORE we touch the running proxy:
|
|
# a broken config would otherwise take the domain service down with it.
|
|
# Validate EXACTLY the command line the unit is about to run (see
|
|
# configs/systemd/haproxy.service): the entry config alone is not the
|
|
# configuration, it only declares the frontends, the reject backends and the
|
|
# timeouts. The per-domain backends and the routing maps live in the
|
|
# generation, so checking one file would happily pass a setup that answers 404
|
|
# on :80 and closes every :443 connection.
|
|
/usr/sbin/haproxy -c -q -f "$ENTRY_CFG" -f "$CURRENT_LINK/domains.cfg" 2>/dev/null \
|
|
|| { /usr/sbin/haproxy -c -f "$ENTRY_CFG" -f "$CURRENT_LINK/domains.cfg";
|
|
die "the generated HAProxy configuration is invalid"; }
|
|
log "configuration check passed (entry config + generation)"
|
|
|
|
if [[ "$want_start" -eq 1 ]]; then
|
|
systemctl enable --now haproxy.service >/dev/null 2>&1 || true
|
|
sleep 1
|
|
if ! systemctl is-active --quiet haproxy.service; then
|
|
# Only NOW is a message about the still-running Traefik accurate.
|
|
if systemctl is-active --quiet traefik.service 2>/dev/null; then
|
|
systemctl status haproxy.service --no-pager | tail -5
|
|
die "HAProxy failed to start while Traefik is still running — free 80/443 or disable it, then re-run the installer"
|
|
fi
|
|
systemctl status haproxy.service --no-pager | tail -5
|
|
die "HAProxy failed to start"
|
|
fi
|
|
log "HAProxy running"
|
|
# Only after HAProxy is confirmed active do we tear Traefik down, so a failed
|
|
# start leaves the host exactly as it was.
|
|
remove_traefik
|
|
else
|
|
# Not starting: disable boot autostart so a reboot cannot bring up a proxy
|
|
# the operator asked to keep off.
|
|
systemctl disable --now haproxy.service >/dev/null 2>&1 || true
|
|
# Traefik must still go: it must never be the process serving domains after
|
|
# this script runs, even when HAProxy stays down.
|
|
remove_traefik
|
|
fi
|
|
|
|
rm -f /etc/vpsmgr/.install-traefik-off 2>/dev/null || true
|
|
log "domain proxy: $( [[ "$want_start" -eq 1 ]] && echo "running (HAProxy $HAPROXY_BRANCH)" || echo "installed but disabled" )"
|
|
echo "[30] haproxy ready"
|