diff --git a/test_fips.sh b/test_fips.sh new file mode 100755 index 0000000..a079808 --- /dev/null +++ b/test_fips.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +set -euo pipefail + +TEST_URL="http://npub1crpldvy49ef8z34wlacwujnfudy4nd7k96aqdx5wgn6ckztz7z8q9t59ud.fips/index.html" +CURL_TIMEOUT="${CURL_TIMEOUT:-20}" +MIN_BYTES="${MIN_BYTES:-1000}" + +if ! command -v curl >/dev/null 2>&1; then + echo "Error: curl is required." >&2 + exit 1 +fi + +if ! command -v python3 >/dev/null 2>&1; then + echo "Error: python3 is required." >&2 + exit 1 +fi + +get_cmd_output() { + local out_file err_file + out_file="$(mktemp)" + err_file="$(mktemp)" + + if "$@" >"${out_file}" 2>"${err_file}"; then + cat "${out_file}" + rm -f "${out_file}" "${err_file}" + return 0 + fi + + if command -v sudo >/dev/null 2>&1; then + if sudo "$@" >"${out_file}" 2>"${err_file}"; then + cat "${out_file}" + rm -f "${out_file}" "${err_file}" + return 0 + fi + fi + + cat "${err_file}" >&2 || true + rm -f "${out_file}" "${err_file}" + return 1 +} + +json_get() { + local json_input="$1" + local path="$2" + python3 - "$path" "$json_input" <<'PY' +import json +import sys + +path = sys.argv[1].split('.') +obj = json.loads(sys.argv[2]) + +cur = obj +for p in path: + if isinstance(cur, dict) and p in cur: + cur = cur[p] + else: + print("n/a") + raise SystemExit(0) + +if isinstance(cur, (dict, list)): + print(json.dumps(cur, separators=(",", ":"))) +else: + print(cur) +PY +} + +first_connected_peer() { + local json_input="$1" + python3 - "$json_input" <<'PY' +import json +import sys + +obj = json.loads(sys.argv[1]) +peers = obj.get("peers", []) +connected = [p for p in peers if p.get("connectivity") == "connected"] +if not connected: + print("none") + raise SystemExit(0) + +p = connected[0] +name = p.get("display_name") or p.get("npub") or "unknown" +addr = p.get("transport_addr") or "n/a" +print(f"{name} @ {addr}") +PY +} + +echo "==> Checking local FIPS service status" +if command -v systemctl >/dev/null 2>&1; then + if ! systemctl is-active --quiet fips.service; then + echo "Error: fips.service is not active." >&2 + echo "Start it with: sudo systemctl start fips" >&2 + exit 1 + fi +else + echo "Warning: systemctl not found; skipping service-state check." +fi + +if ! command -v fips >/dev/null 2>&1; then + echo "Error: fips binary not found in PATH." >&2 + exit 1 +fi + +if ! command -v fipsctl >/dev/null 2>&1; then + echo "Error: fipsctl binary not found in PATH." >&2 + exit 1 +fi + +FIPS_VERSION="$(fips --version | head -n1)" +FIPSCTL_VERSION="$(fipsctl --version | head -n1)" +STATUS_JSON="$(get_cmd_output fipsctl show status)" +PEERS_JSON="$(get_cmd_output fipsctl show peers)" + +echo "==> Testing .fips URL reachability" +TMP_BODY="$(mktemp)" +trap 'rm -f "${TMP_BODY}"' EXIT + +HTTP_CODE="$(curl -sS --max-time "${CURL_TIMEOUT}" -o "${TMP_BODY}" -w '%{http_code}' "${TEST_URL}")" +BYTES="$(wc -c < "${TMP_BODY}" | tr -d '[:space:]')" + +if [[ "${HTTP_CODE}" != "200" ]]; then + echo "Error: expected HTTP 200, got ${HTTP_CODE}." >&2 + exit 1 +fi + +if (( BYTES < MIN_BYTES )); then + echo "Error: response too small (${BYTES} bytes, expected at least ${MIN_BYTES})." >&2 + exit 1 +fi + +echo "" +echo "FIPS health summary (10 key metrics)" +echo "1) fips_version: ${FIPS_VERSION}" +echo "2) fipsctl_version: ${FIPSCTL_VERSION}" +echo "3) node_state: $(json_get "${STATUS_JSON}" state)" +echo "4) uptime_secs: $(json_get "${STATUS_JSON}" uptime_secs)" +echo "5) npub: $(json_get "${STATUS_JSON}" npub)" +echo "6) ipv6_addr: $(json_get "${STATUS_JSON}" ipv6_addr)" +echo "7) peer_count: $(json_get "${STATUS_JSON}" peer_count)" +echo "8) link_count: $(json_get "${STATUS_JSON}" link_count)" +echo "9) session_count: $(json_get "${STATUS_JSON}" session_count)" +echo "10) connected_peer_example: $(first_connected_peer "${PEERS_JSON}")" + +echo "" +echo "URL test: HTTP=${HTTP_CODE}, bytes=${BYTES}" diff --git a/update_and_deploy_fips.sh b/update_and_deploy_fips.sh new file mode 100755 index 0000000..a396650 --- /dev/null +++ b/update_and_deploy_fips.sh @@ -0,0 +1,421 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Updates local fips/ clone (if upstream has new commits), builds release binaries, +# and installs or updates the local systemd deployment on this x86 host. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +FIPS_DIR="${SCRIPT_DIR}/fips" +PREFERRED_REMOTE_URL="${PREFERRED_REMOTE_URL:-https://git.laantungir.net/laantungir/fips.git}" +DEFAULT_DEPLOY_PEERS=$(cat <<'YAML' +peers: + - npub: "npub1crpldvy49ef8z34wlacwujnfudy4nd7k96aqdx5wgn6ckztz7z8q9t59ud" + alias: "laantungir" + addresses: + - transport: udp + addr: "15.235.3.231:2121" + connect_policy: auto_connect + - npub: "npub1qmc3cvfz0yu2hx96nq3gp55zdan2qclealn7xshgr448d3nh6lks7zel98" + alias: "fips-test-node" + addresses: + - transport: udp + addr: "217.77.8.91:2121" + connect_policy: auto_connect + - npub: "npub1zv58cn7v83mxvttl70w5fwjwuclfmntv9cnmv5wmz2nzz88u5urqvdx96n" + alias: "fips.v0l.io" + addresses: + - transport: udp + addr: "185.18.221.160:2121" + connect_policy: auto_connect +YAML +) + +if [[ ! -d "${FIPS_DIR}" ]]; then + echo "Error: Missing ${FIPS_DIR}" >&2 + exit 1 +fi + +if [[ ! -d "${FIPS_DIR}/.git" ]]; then + echo "Error: ${FIPS_DIR} is not a git repository." >&2 + exit 1 +fi + +if ! command -v git >/dev/null 2>&1; then + echo "Error: git is required." >&2 + exit 1 +fi + +require_sudo() { + if ! command -v sudo >/dev/null 2>&1; then + echo "Error: sudo is required to install missing dependencies." >&2 + exit 1 + fi +} + +install_system_packages() { + local packages=("$@") + + if command -v apt-get >/dev/null 2>&1; then + require_sudo + sudo apt-get update + sudo apt-get install -y "${packages[@]}" + elif command -v dnf >/dev/null 2>&1; then + require_sudo + sudo dnf install -y "${packages[@]}" + elif command -v yum >/dev/null 2>&1; then + require_sudo + sudo yum install -y "${packages[@]}" + elif command -v pacman >/dev/null 2>&1; then + require_sudo + sudo pacman -Sy --needed --noconfirm "${packages[@]}" + elif command -v zypper >/dev/null 2>&1; then + require_sudo + sudo zypper --non-interactive install "${packages[@]}" + elif command -v apk >/dev/null 2>&1; then + require_sudo + sudo apk add --no-cache "${packages[@]}" + else + echo "Error: unsupported package manager. Install dependencies manually." >&2 + exit 1 + fi +} + +ensure_base_build_deps() { + if command -v apt-get >/dev/null 2>&1; then + install_system_packages git curl build-essential pkg-config libssl-dev ca-certificates + elif command -v dnf >/dev/null 2>&1 || command -v yum >/dev/null 2>&1; then + install_system_packages git curl gcc gcc-c++ make pkgconf-pkg-config openssl-devel ca-certificates + elif command -v pacman >/dev/null 2>&1; then + install_system_packages git curl base-devel pkgconf openssl ca-certificates + elif command -v zypper >/dev/null 2>&1; then + install_system_packages git curl gcc gcc-c++ make pkg-config libopenssl-devel ca-certificates + elif command -v apk >/dev/null 2>&1; then + install_system_packages git curl build-base pkgconf openssl-dev ca-certificates + fi +} + +ensure_rust_toolchain() { + if command -v cargo >/dev/null 2>&1 && command -v rustc >/dev/null 2>&1; then + return + fi + + echo "==> Rust toolchain not found; installing rustup + cargo" + + if ! command -v curl >/dev/null 2>&1; then + ensure_base_build_deps + fi + + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + + export PATH="${HOME}/.cargo/bin:${PATH}" + if [[ -f "${HOME}/.cargo/env" ]]; then + # shellcheck disable=SC1090 + source "${HOME}/.cargo/env" + fi + + if ! command -v cargo >/dev/null 2>&1; then + echo "Error: cargo still not available after rustup install." >&2 + exit 1 + fi +} + +ensure_build_toolchain() { + echo "==> Verifying build dependencies" + ensure_base_build_deps + ensure_rust_toolchain +} + +ensure_tun_ready() { + if [[ -e /dev/net/tun ]]; then + return + fi + + echo "==> /dev/net/tun not present; attempting to load tun module" + require_sudo + if ! command -v modprobe >/dev/null 2>&1; then + echo "Error: modprobe not found; cannot load tun module." >&2 + exit 1 + fi + + sudo modprobe tun + if [[ ! -e /dev/net/tun ]]; then + echo "Error: /dev/net/tun is still unavailable after modprobe tun." >&2 + exit 1 + fi +} + +ask_yes_no() { + local prompt="$1" + local default_value="$2" + local default_hint reply + + if [[ "${default_value}" == "yes" ]]; then + default_hint="Y/n" + else + default_hint="y/N" + fi + + if [[ ! -t 0 ]]; then + printf '%s\n' "${default_value}" + return + fi + + while true; do + read -r -p "${prompt} [${default_hint}]: " reply || true + reply="${reply:-}" + + if [[ -z "${reply}" ]]; then + printf '%s\n' "${default_value}" + return + fi + + case "${reply}" in + y|Y|yes|YES) + printf '%s\n' "yes" + return + ;; + n|N|no|NO) + printf '%s\n' "no" + return + ;; + esac + + echo "Please answer yes or no." + done +} + +ensure_build_toolchain +ensure_tun_ready + +cd "${FIPS_DIR}" + +ensure_preferred_remote() { + local current_url + current_url="$(git remote get-url origin 2>/dev/null || true)" + + if [[ -z "${current_url}" ]]; then + echo "==> No origin remote found; adding origin ${PREFERRED_REMOTE_URL}" + git remote add origin "${PREFERRED_REMOTE_URL}" + return + fi + + if [[ "${current_url}" == "${PREFERRED_REMOTE_URL}" ]]; then + return + fi + + if [[ "${current_url}" == git@* || "${current_url}" == ssh://* ]]; then + echo "==> Switching origin remote from SSH to HTTPS to avoid SSH key failures" + echo " old: ${current_url}" + echo " new: ${PREFERRED_REMOTE_URL}" + git remote set-url origin "${PREFERRED_REMOTE_URL}" + fi +} + +ensure_preferred_remote + +echo "==> Checking fips repository for updates" + +UPSTREAM_REF="" +if UPSTREAM_REF="$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null)"; then + git fetch --prune + + LOCAL_SHA="$(git rev-parse HEAD)" + REMOTE_SHA="$(git rev-parse "${UPSTREAM_REF}")" + BASE_SHA="$(git merge-base HEAD "${UPSTREAM_REF}")" + + if [[ "${LOCAL_SHA}" == "${REMOTE_SHA}" ]]; then + echo "Repository already up to date (${LOCAL_SHA:0:12})." + elif [[ "${LOCAL_SHA}" == "${BASE_SHA}" ]]; then + echo "New commits detected on ${UPSTREAM_REF}; pulling fast-forward updates..." + git pull --ff-only + elif [[ "${REMOTE_SHA}" == "${BASE_SHA}" ]]; then + echo "Local repository is ahead of ${UPSTREAM_REF}; not pulling." + else + echo "Error: local and upstream histories have diverged. Resolve manually, then re-run." >&2 + exit 1 + fi +else + echo "No upstream tracking branch configured; skipping update check/pull." +fi + +echo "==> Building release binaries" +cargo build --release + +for bin in fips fipsctl fipstop; do + if [[ ! -x "target/release/${bin}" ]]; then + echo "Error: missing built binary target/release/${bin}" >&2 + exit 1 + fi +done + +INSTALL_MODE="fresh install" +if [[ -x /usr/local/bin/fips || -f /etc/systemd/system/fips.service ]]; then + INSTALL_MODE="update" +fi + +echo "==> Deployment options (press Enter for defaults)" +ENABLE_PERSISTENT_IDENTITY="$(ask_yes_no "Enable persistent identity?" "yes")" +ADD_DEFAULT_PEERS="$(ask_yes_no "Add default static peers (laantungir/fips-test-node/fips.v0l.io)?" "yes")" +ADD_CURRENT_USER_TO_FIPS_GROUP="$(ask_yes_no "Add current user to fips group?" "yes")" +START_SERVICE_NOW="$(ask_yes_no "Start/restart fips service now?" "yes")" + +echo "==> Preparing ${INSTALL_MODE} payload" + +STAGING_ROOT="$(mktemp -d)" +trap 'rm -rf "${STAGING_ROOT}"' EXIT + +mkdir -p "${STAGING_ROOT}/systemd" "${STAGING_ROOT}/common" + +cp target/release/fips "${STAGING_ROOT}/systemd/" +cp target/release/fipsctl "${STAGING_ROOT}/systemd/" +cp target/release/fipstop "${STAGING_ROOT}/systemd/" + +cp packaging/systemd/install.sh "${STAGING_ROOT}/systemd/" +cp packaging/systemd/fips.service "${STAGING_ROOT}/systemd/" +cp packaging/systemd/fips-dns.service "${STAGING_ROOT}/systemd/" +cp packaging/common/fips.yaml "${STAGING_ROOT}/systemd/" +cp packaging/common/hosts "${STAGING_ROOT}/systemd/" + +cp packaging/common/fips-dns-setup "${STAGING_ROOT}/common/" +cp packaging/common/fips-dns-teardown "${STAGING_ROOT}/common/" + +chmod +x "${STAGING_ROOT}/systemd/install.sh" + +echo "==> Installing/updating locally (sudo required)" +INSTALL_OUTPUT="" +if ! INSTALL_OUTPUT="$(sudo "${STAGING_ROOT}/systemd/install.sh" 2>&1)"; then + printf '%s\n' "${INSTALL_OUTPUT}" >&2 + exit 1 +fi + +# Upstream installer prints a long manual checklist that this wrapper now handles. +printf '%s\n' "${INSTALL_OUTPUT}" | awk ' + BEGIN { skip = 0 } + /^=== Installation complete ===/ { skip = 1 } + { if (!skip) print } +' + +CONFIG_FILE="/etc/fips/fips.yaml" +if [[ -f "${CONFIG_FILE}" ]]; then + echo "==> Applying fips.yaml settings" + require_sudo + sudo cp "${CONFIG_FILE}" "${CONFIG_FILE}.bak.$(date +%Y%m%d%H%M%S)" + + echo " - Enforcing transports.udp.mtu: 1280" + TMP_CONFIG="$(mktemp)" + awk ' +function indent_of(s, i,c) { + for (i = 1; i <= length(s); i++) { + c = substr(s, i, 1) + if (c != " ") return i - 1 + } + return length(s) +} +{ + line = $0 + indent = indent_of(line) + + if (in_udp && indent <= udp_indent && line !~ /^[[:space:]]*$/ && line !~ /^[[:space:]]*#/) { + if (!udp_has_mtu) { + print sprintf("%*smtu: 1280", udp_indent + 3, "") + udp_has_mtu = 1 + } + in_udp = 0 + } + + if (in_transports && indent <= transports_indent && line !~ /^[[:space:]]*$/ && line !~ /^[[:space:]]*#/) { + in_transports = 0 + } + + if (line ~ /^[[:space:]]*transports:[[:space:]]*$/) { + in_transports = 1 + transports_indent = indent + } + + if (in_transports && line ~ /^[[:space:]]*udp:[[:space:]]*$/) { + in_udp = 1 + udp_indent = indent + udp_has_mtu = 0 + } + + if (in_udp && line ~ /^[[:space:]]*mtu:[[:space:]]*[0-9]+([[:space:]]*#.*)?$/) { + sub(/mtu:[[:space:]]*[0-9]+/, "mtu: 1280", line) + udp_has_mtu = 1 + } + + print line +} +END { + if (in_udp && !udp_has_mtu) { + print sprintf("%*smtu: 1280", udp_indent + 3, "") + } +} +' "${CONFIG_FILE}" > "${TMP_CONFIG}" + + if ! grep -Eq '^[[:space:]]*transports:[[:space:]]*$' "${TMP_CONFIG}" || ! grep -Eq '^[[:space:]]*udp:[[:space:]]*$' "${TMP_CONFIG}"; then + cat >> "${TMP_CONFIG}" <<'YAML' + +transports: + udp: + mtu: 1280 +YAML + fi + + sudo install -m 0600 "${TMP_CONFIG}" "${CONFIG_FILE}" + rm -f "${TMP_CONFIG}" + + if [[ "${ENABLE_PERSISTENT_IDENTITY}" == "yes" ]]; then + echo " - Enabling node.identity.persistent" + if grep -Eq '^[[:space:]]*#?[[:space:]]*persistent:[[:space:]]*(true|false)?[[:space:]]*$' "${CONFIG_FILE}"; then + sudo sed -Ei '0,/^[[:space:]]*#?[[:space:]]*persistent:[[:space:]]*(true|false)?[[:space:]]*$/s// persistent: true/' "${CONFIG_FILE}" + else + echo "Warning: could not find persistent setting to update; leaving as-is." >&2 + fi + fi + + if [[ "${ADD_DEFAULT_PEERS}" == "yes" ]]; then + echo " - Installing default static peers" + TMP_CONFIG="$(mktemp)" + awk '/^[[:space:]]*peers:[[:space:]]*/{exit} {print}' "${CONFIG_FILE}" > "${TMP_CONFIG}" + printf '\n%s\n' "${DEFAULT_DEPLOY_PEERS}" >> "${TMP_CONFIG}" + sudo install -m 0600 "${TMP_CONFIG}" "${CONFIG_FILE}" + rm -f "${TMP_CONFIG}" + fi +fi + +if [[ "${ADD_CURRENT_USER_TO_FIPS_GROUP}" == "yes" ]]; then + TARGET_USER="${SUDO_USER:-${USER:-}}" + if [[ -n "${TARGET_USER}" && "${TARGET_USER}" != "root" ]] && id -u "${TARGET_USER}" >/dev/null 2>&1; then + echo "==> Adding ${TARGET_USER} to fips group" + require_sudo + sudo usermod -aG fips "${TARGET_USER}" + echo " (log out and back in for group membership to take effect)" + else + echo "==> Skipping group update (no non-root target user detected)" + fi +fi + +echo "==> Cleaning potentially stale fd00::/8 route state" +require_sudo +sudo ip -6 route del fd00::/8 >/dev/null 2>&1 || true + +if [[ "${START_SERVICE_NOW}" == "yes" ]]; then + echo "==> Starting/restarting FIPS services" + sudo systemctl daemon-reload + sudo systemctl restart fips.service + sudo systemctl restart fips-dns.service || true + + echo "==> Running post-deploy health check" + if [[ -x "${SCRIPT_DIR}/test_fips.sh" ]]; then + "${SCRIPT_DIR}/test_fips.sh" + else + echo "Warning: ${SCRIPT_DIR}/test_fips.sh not found or not executable; skipping health check." >&2 + fi +else + echo "==> Service start skipped by user choice" +fi + +echo +echo "Done. fips has been built and ${INSTALL_MODE} has completed." +echo "Check service status with: sudo systemctl status fips --no-pager" +echo "Follow logs with: sudo journalctl -u fips -f"