#!/usr/bin/env bash set -euo pipefail # ============================================================================== # Configuration (may be overridden by unattended.sh) # ============================================================================== ZEROTIER_VERSION="${ZEROTIER_VERSION:-1.16.2}" SOFTWARE_ROOT="${SOFTWARE_ROOT:-/usr/local/software}" SYSOP_DATA_ROOT="${SYSOP_DATA_ROOT:-/media/sysop}" REQUIRED_SWAP_MIB="${REQUIRED_SWAP_MIB:-2048}" ROOTFS_BUILD_SIZE="${ROOTFS_BUILD_SIZE:-1400M}" MAKE_JOBS="${MAKE_JOBS:-1}" ENABLE_SSO="${ENABLE_SSO:-0}" PREBUILT_BINARY="${PREBUILT_BINARY:-}" ZEROTIER_ROOT="$SOFTWARE_ROOT/zerotier" ZEROTIER_VERSIONS="$ZEROTIER_ROOT/versions" ZEROTIER_BUILD_PROFILE="${ZEROTIER_VERSION}-linux-musl-aarch64-sso${ENABLE_SSO}" ZEROTIER_INSTALL="$ZEROTIER_VERSIONS/$ZEROTIER_BUILD_PROFILE" ZEROTIER_STATE="$SYSOP_DATA_ROOT/zerotier" BUILD_DIRECTORY="$SOFTWARE_ROOT/.zerotier-build-$ZEROTIER_BUILD_PROFILE" SOURCE_DIRECTORY="$BUILD_DIRECTORY/ZeroTierOne" BUILD_DEPS=( build-base linux-headers openssl-dev pkgconf ) if [[ "$ENABLE_SSO" == "1" ]]; then BUILD_DEPS+=(cargo) fi # ============================================================================== # Helpers and cleanup # ============================================================================== BUILD_DEPS_INSTALLED=0 BUILD_REQUIRED=1 PREBUILT_AVAILABLE=0 STATE_WAS_PRESENT=0 die() { echo "ERROR: $*" >&2 exit 1 } safe_remove_build_directory() { case "$BUILD_DIRECTORY" in "$SOFTWARE_ROOT"/.zerotier-build-*) rm -rf -- "$BUILD_DIRECTORY" ;; *) echo "WARNING: Refusing to remove unexpected path: $BUILD_DIRECTORY" >&2 ;; esac } release_build_resources() { local result=0 if (( BUILD_DEPS_INSTALLED == 1 )); then echo "=== Removing ZeroTier build dependencies ===" if apk del .zerotier-builddeps; then BUILD_DEPS_INSTALLED=0 else result=1 fi fi safe_remove_build_directory return "$result" } cleanup_on_exit() { local status=$? trap - EXIT set +e release_build_resources exit "$status" } migrate_directory_to_state() { local source="$1" if [[ -L "$source" ]]; then rm -f -- "$source" return 0 fi if [[ -d "$source" ]]; then cp -a "$source/." "$ZEROTIER_STATE/" 2>/dev/null || true rm -rf -- "$source" elif [[ -e "$source" ]]; then die "Refusing to replace non-directory path: $source" fi } trap cleanup_on_exit EXIT trap 'exit 129' HUP trap 'exit 130' INT trap 'exit 143' TERM # ============================================================================== # Validate the Alpine diskless/ZFS environment # ============================================================================== [[ $EUID -eq 0 ]] || die "This installer must be run as root." for command in apk git lbu mount rc-update do command -v "$command" >/dev/null 2>&1 || die "Missing required command: $command" done [[ -d "$SOFTWARE_ROOT" ]] || die "Missing software dataset mount: $SOFTWARE_ROOT" [[ -d "$SYSOP_DATA_ROOT" ]] || die "Missing sysop dataset mount: $SYSOP_DATA_ROOT" SOFTWARE_FILESYSTEM="$({ awk -v path="$SOFTWARE_ROOT" '$2 == path { print $3; exit }' /proc/mounts } || true)" [[ "$SOFTWARE_FILESYSTEM" == "zfs" ]] || die "$SOFTWARE_ROOT must be a mounted ZFS dataset, not '${SOFTWARE_FILESYSTEM:-unknown}'." case "$REQUIRED_SWAP_MIB" in ''|*[!0-9]*) die "REQUIRED_SWAP_MIB must be a positive integer." ;; esac case "$MAKE_JOBS" in ''|*[!0-9]*) die "MAKE_JOBS must be a positive integer." ;; esac case "$ENABLE_SSO" in 0|1) ;; *) die "ENABLE_SSO must be either 0 or 1." ;; esac (( REQUIRED_SWAP_MIB > 0 )) || die "REQUIRED_SWAP_MIB must be greater than zero." (( MAKE_JOBS > 0 )) || die "MAKE_JOBS must be greater than zero." if [[ -x "$ZEROTIER_INSTALL/zerotier-one" ]]; then EXISTING_VERSION="$( "$ZEROTIER_INSTALL/zerotier-one" -v 2>/dev/null || true )" if [[ "$EXISTING_VERSION" == "$ZEROTIER_VERSION" ]]; then BUILD_REQUIRED=0 else echo "WARNING: Existing ZeroTier binary reports version '${EXISTING_VERSION:-unknown}'; replacing it." fi fi if (( BUILD_REQUIRED == 1 )) && [[ -n "$PREBUILT_BINARY" && -s "$PREBUILT_BINARY" ]] then PREBUILT_AVAILABLE=1 BUILD_REQUIRED=0 fi # ============================================================================== # Check swap and free space when compilation is needed # ============================================================================== if (( BUILD_REQUIRED == 1 )); then ACTIVE_SWAP_KB="$(awk 'NR > 1 { total += $3 } END { print total + 0 }' /proc/swaps)" REQUIRED_SWAP_KB=$((REQUIRED_SWAP_MIB * 1024)) (( ACTIVE_SWAP_KB >= REQUIRED_SWAP_KB )) || die "ZeroTier compilation requires at least ${REQUIRED_SWAP_MIB} MiB active swap." AVAILABLE_KB="$(df -Pk "$SOFTWARE_ROOT" | awk 'NR == 2 { print $4 }')" REQUIRED_FREE_KB=$((1200 * 1024)) (( AVAILABLE_KB >= REQUIRED_FREE_KB )) || die "At least 1200 MB must be free on $SOFTWARE_ROOT for the ZeroTier build." echo echo "=== Reusing all active swap devices for compilation ===" grep -E 'MemTotal|SwapTotal|SwapFree' /proc/meminfo elif (( PREBUILT_AVAILABLE == 1 )); then echo echo "=== Using precompiled ZeroTier ${ZEROTIER_VERSION} binary ===" echo "Source: $PREBUILT_BINARY" else echo echo "=== Reusing the existing ZeroTier ${ZEROTIER_VERSION} binary ===" fi # ============================================================================== # Enlarge the temporary RAM root filesystem # ============================================================================== if (( BUILD_REQUIRED == 1 )); then ROOT_FILESYSTEM="$(awk '$2 == "/" { print $3; exit }' /proc/mounts)" if [[ "$ROOT_FILESYSTEM" == "tmpfs" ]]; then echo echo "=== Enlarging temporary rootfs limit to $ROOTFS_BUILD_SIZE ===" mount -o "remount,size=$ROOTFS_BUILD_SIZE" / fi fi # ============================================================================== # Install build-only packages # ============================================================================== if (( BUILD_REQUIRED == 1 )); then echo echo "=== Installing ZeroTier build dependencies ===" if [[ "$ENABLE_SSO" == "1" ]]; then echo "ZeroTier SSO is enabled; Rust/Cargo will be installed." else echo "ZeroTier SSO is disabled; Rust/Cargo are not required." fi # --no-cache keeps the compiler toolchain out of the persistent APK cache. # Retry transient Wi-Fi/download failures, cleaning a partial virtual # package before each new attempt. APK_ADD_SUCCEEDED=0 for attempt in 1 2 3; do if apk add \ --no-cache \ --virtual .zerotier-builddeps \ "${BUILD_DEPS[@]}" then APK_ADD_SUCCEEDED=1 break fi echo "WARNING: Build dependency installation attempt $attempt failed." apk del .zerotier-builddeps >/dev/null 2>&1 || true if (( attempt < 3 )); then echo "Retrying after $((attempt * 10)) seconds..." sleep $((attempt * 10)) fi done (( APK_ADD_SUCCEEDED == 1 )) || die "Could not install ZeroTier build dependencies after 3 attempts." BUILD_DEPS_INSTALLED=1 for command in gcc g++ make pkgconf strip do command -v "$command" >/dev/null 2>&1 || die "Build dependency did not provide required command: $command" done if [[ "$ENABLE_SSO" == "1" ]]; then command -v cargo >/dev/null 2>&1 || die "SSO build dependency did not provide cargo." fi fi # ============================================================================== # Download the exact pinned ZeroTier source tag # ============================================================================== if (( BUILD_REQUIRED == 1 )); then echo echo "=== Downloading ZeroTier ${ZEROTIER_VERSION} source ===" safe_remove_build_directory mkdir -p "$BUILD_DIRECTORY" git init "$SOURCE_DIRECTORY" git -C "$SOURCE_DIRECTORY" remote add \ origin \ https://github.com/zerotier/ZeroTierOne.git # Fetch the exact tag ref. This avoids both the earlier git-describe error # on a shallow checkout and ambiguity between branch and tag names. git -C "$SOURCE_DIRECTORY" fetch \ --depth 1 \ origin \ "refs/tags/$ZEROTIER_VERSION:refs/tags/$ZEROTIER_VERSION" git -C "$SOURCE_DIRECTORY" checkout \ --detach \ "refs/tags/$ZEROTIER_VERSION" ACTUAL_COMMIT="$(git -C "$SOURCE_DIRECTORY" rev-parse HEAD)" TAG_COMMIT="$( git -C "$SOURCE_DIRECTORY" \ rev-parse --verify "refs/tags/$ZEROTIER_VERSION^{commit}" )" [[ "$ACTUAL_COMMIT" == "$TAG_COMMIT" ]] || die "Checked-out commit does not match tag $ZEROTIER_VERSION." fi # ============================================================================== # Build conservatively for the Raspberry Pi Zero 2W # ============================================================================== if (( BUILD_REQUIRED == 1 )); then echo echo "=== Building ZeroTier with ${MAKE_JOBS} make job(s) ===" ( cd "$SOURCE_DIRECTORY" # BCM2710A1 does not provide the optional ARMv8 crypto extensions that # ZeroTier's aarch64 makefile otherwise enables by default. sed -i \ 's/-march=armv8-a+crypto/-march=armv8-a/g' \ make-linux.mk if grep -q -- '-march=armv8-a+crypto' make-linux.mk; then die "Could not disable ARM crypto-extension instructions." fi export TMPDIR="$BUILD_DIRECTORY/tmp" if [[ "$ENABLE_SSO" == "1" ]]; then export CARGO_BUILD_JOBS=1 export CARGO_INCREMENTAL=0 export CARGO_HOME="$BUILD_DIRECTORY/cargo-home" mkdir -p "$CARGO_HOME" fi mkdir -p "$TMPDIR" make \ -j "$MAKE_JOBS" \ CC=gcc \ CXX=g++ \ ZT_CONTROLLER=0 \ ZT_SSO_SUPPORTED="$ENABLE_SSO" \ one strip zerotier-one ) fi # ============================================================================== # Install the binary on rpool/software # ============================================================================== if (( PREBUILT_AVAILABLE == 1 )); then echo echo "=== Installing precompiled ZeroTier on rpool/software ===" mkdir -p "$ZEROTIER_INSTALL" install -m 0755 \ "$PREBUILT_BINARY" \ "$ZEROTIER_INSTALL/zerotier-one" elif (( BUILD_REQUIRED == 1 )); then echo echo "=== Installing ZeroTier on rpool/software ===" mkdir -p "$ZEROTIER_INSTALL" install -m 0755 \ "$SOURCE_DIRECTORY/zerotier-one" \ "$ZEROTIER_INSTALL/zerotier-one" fi mkdir -p "$ZEROTIER_ROOT" ln -sfn "versions/$ZEROTIER_BUILD_PROFILE" "$ZEROTIER_ROOT/current" rm -f \ /usr/sbin/zerotier-one \ /usr/sbin/zerotier-cli \ /usr/sbin/zerotier-idtool ln -s "$ZEROTIER_ROOT/current/zerotier-one" /usr/sbin/zerotier-one ln -s zerotier-one /usr/sbin/zerotier-cli ln -s zerotier-one /usr/sbin/zerotier-idtool # ============================================================================== # Keep identity and future network state under sysop's ZFS-backed area # ============================================================================== if [[ -d "$ZEROTIER_STATE" ]] && find "$ZEROTIER_STATE" -mindepth 1 -print -quit | grep -q . then STATE_WAS_PRESENT=1 fi mkdir -p "$ZEROTIER_STATE" /sysop /var/lib chmod 0700 "$ZEROTIER_STATE" if id sysop >/dev/null 2>&1; then chown sysop:sysop "$ZEROTIER_STATE" fi migrate_directory_to_state /sysop/zerotier migrate_directory_to_state /var/lib/zerotier-one ln -s "$ZEROTIER_STATE" /sysop/zerotier ln -s /sysop/zerotier /var/lib/zerotier-one # ============================================================================== # OpenRC service # ============================================================================== cat >/etc/init.d/zerotier-one <<'OPENRC' #!/sbin/openrc-run description="ZeroTier One network virtualization service" command="/usr/sbin/zerotier-one" command_background="yes" pidfile="/run/${RC_SVCNAME}.pid" depend() { need zfs-mount use net logger after networking } start_pre() { if [ ! -d /var/lib/zerotier-one ]; then eerror "Persistent ZeroTier state is unavailable" return 1 fi modprobe tun >/dev/null 2>&1 || true } OPENRC chmod 0755 /etc/init.d/zerotier-one grep -qxF tun /etc/modules 2>/dev/null || echo tun >>/etc/modules rc-update add zerotier-one default # ============================================================================== # Persist diskless symlinks and service files # ============================================================================== for path in \ /usr/sbin/zerotier-one \ /usr/sbin/zerotier-cli \ /usr/sbin/zerotier-idtool \ /sysop/zerotier \ /var/lib/zerotier-one \ /etc/init.d/zerotier-one do lbu include "$path" done # ============================================================================== # Verify without joining a network # ============================================================================== echo echo "=== Verifying the ZeroTier installation ===" if ! INSTALLED_VERSION="$(/usr/sbin/zerotier-one -v 2>/dev/null)"; then die "The ZeroTier binary cannot run on this Raspberry Pi/Alpine system." fi echo "$INSTALLED_VERSION" [[ "$INSTALLED_VERSION" == "$ZEROTIER_VERSION" ]] || die "ZeroTier reports version '$INSTALLED_VERSION', expected '$ZEROTIER_VERSION'." if ldd /usr/sbin/zerotier-one 2>&1 | grep -q 'not found'; then ldd /usr/sbin/zerotier-one >&2 || true die "The ZeroTier binary has missing runtime libraries." fi if (( STATE_WAS_PRESENT == 0 )) && [[ -d "$ZEROTIER_STATE/networks.d" ]]; then die "A fresh installation unexpectedly contains joined-network configuration." fi # ============================================================================== # Remove toolchain and source tree # ============================================================================== echo release_build_resources trap - EXIT HUP INT TERM echo echo "ZeroTier ${ZEROTIER_VERSION} is installed and enabled for the next boot." echo "Existing identity/configuration is preserved; a fresh install is not joined."