testsAndMisc/linux_configuration/fixes/migrate_hosts_guard_to_guard_lib.sh
Krzysztof kuhy Rudnicki f72455662c Extract the hosts blocker into its own repo
linux_configuration/periodic_background/hosts/ is now
github.com/kuhyx/hosts-blocker, cloned at ~/hosts-blocker. Clears 5 of the 46
directory-depth violations; the remaining 41 are all digital_wellbeing, so
--warn stays on the depth hook until that unit moves too.

History was filtered across ALL FOUR paths the unit has lived at (hosts/,
linux_configuration/hosts/, linux_configuration/scripts/periodic_background/
hosts/, and the current one). Filtering on the current path alone would have
preserved 1 commit out of 51 -- the flatten moved it recently, so a naive
single --path silently truncates the history to the flatten commit and still
looks fine in `git log | head`.

No source line changed: all 15 files are byte-identical to the copies removed
here. install.sh already resolved its lib/ via BASH_SOURCE.

The six callers now resolve the checkout through a new shared helper,
linux_configuration/lib/extracted_repos.sh, which reads the invoking user's
home from SUDO_USER's passwd entry rather than $HOME -- several of these
scripts re-exec under sudo, where $HOME is /root. A missing checkout fails
loudly with a clone instruction; it is never a silent skip, which is the
failure mode that left screen-locker's self-repair dead for five months after
the 2026-05-15 reorg.

The live blocker is untouched: /etc/hosts keeps its chattr +i, and the guards
load their plugins from /usr/local/share/guard-lib-plugins/ (byte-identical by
md5), not from a repo path.

Verified: 25 tests pass from ~/hosts-blocker; shellcheck clean on the helper
and all six callers; sudo resolution prints /home/kuhy/hosts-blocker;
test_setup_dns_blocker.sh passes; report_live_state.sh --check is byte-identical
before and after, so no new stale references.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 20:30:18 +02:00

164 lines
4.8 KiB
Bash
Executable File

#!/bin/bash
# ============================================================================
# migrate_hosts_guard_to_guard_lib.sh
#
# Completes the guard-lib migration that commit 66c4698 started but never
# finished on this machine.
#
# State this repairs: guard-lib is installed (guardctl, lib/, systemd templates,
# and BOTH generic pacman hooks) with /etc/guard-lib/targets completely empty,
# while the pre-guard-lib hosts-guard implementation still does all the real
# work through its own per-file pacman hooks and systemd units. So every pacman
# transaction runs two PreTransaction hooks where one would do, and the
# guard-lib pair - the one that runs FIRST, and the one present for all four
# recorded hook stalls - does literally nothing.
#
# After this runs there is one guard system: guardctl file-guard instances for
# hosts, nsswitch and resolved, driven by guard-lib's generic unlock-all /
# relock-all hooks. The legacy hooks and units are retired.
#
# NOT migrated here: the "shutdown-schedule" instance that
# setup_midnight_shutdown.sh registers. Its target /etc/shutdown-schedule.conf
# does not exist on this machine, so registering it would mean installing a
# different feature, not finishing this migration. Run that script if you want
# the midnight-shutdown guard.
#
# Every step is idempotent, and --rollback restores the legacy layer.
# ============================================================================
set -euo pipefail
# shellcheck source=lib/hosts_guard_migrate.sh
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/hosts_guard_migrate.sh"
# shellcheck source=lib/hosts_guard_rollback.sh
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/hosts_guard_rollback.sh"
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
readonly GUARDCTL="/usr/local/bin/guardctl"
readonly TARGETS_DIR="/etc/guard-lib/targets"
readonly STATE_DIR="/var/lib/guard-lib-migration"
readonly HOOKS_DIR="/etc/pacman.d/hooks"
# Plugins must live somewhere permanent: guardctl records an ABSOLUTE plugin
# path in the instance conf, so pointing it at a repo checkout (or worse, a git
# worktree) silently breaks enforcement the day that directory moves.
readonly PLUGIN_INSTALL_DIR="/usr/local/share/guard-lib-plugins"
readonly LEGACY_HOOKS=(
"10-unlock-etc-hosts.hook"
"90-relock-etc-hosts.hook"
)
readonly LEGACY_UNITS=(
"hosts-guard.path"
"hosts-guard.service"
"hosts-bind-mount.service"
"nsswitch-guard.path"
"nsswitch-guard.service"
"resolved-guard.path"
"resolved-guard.service"
)
DRY_RUN=0
MODE="migrate"
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
readonly SCRIPT_DIR
# The guard plugins moved with the EXTRACTED hosts-blocker repo
# (github.com/kuhyx/hosts-blocker).
# shellcheck source=../lib/extracted_repos.sh
source "$SCRIPT_DIR/../lib/extracted_repos.sh"
PLUGIN_SRC_DIR="$(readlink -f "$(extracted_repo_dir hosts-blocker)/guard/plugins")"
readonly PLUGIN_SRC_DIR
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m'
msg() { printf "${GREEN}[✓]${NC} %s\n" "$*"; }
note() { printf "${BLUE}[i]${NC} %s\n" "$*"; }
warn() { printf "${YELLOW}[!]${NC} %s\n" "$*"; }
err() { printf "${RED}[✗]${NC} %s\n" "$*" >&2; }
usage() {
cat <<EOF
Usage: sudo $SCRIPT_NAME [OPTIONS]
Finishes the guard-lib migration for /etc/hosts, /etc/nsswitch.conf and
/etc/systemd/resolved.conf, then retires the legacy hosts-guard layer.
Options:
--rollback Undo: uninstall the guard-lib instances, restore the legacy
pacman hooks and re-enable the legacy units.
--status Show what is currently registered; change nothing.
--dry-run Print what would happen; change nothing.
-h, --help Show this help
EOF
exit 0
}
run() {
if ((DRY_RUN == 1)); then
printf "${YELLOW}DRY-RUN:${NC} %s\n" "$*"
return 0
fi
"$@"
}
require_root() {
if [[ $EUID -ne 0 ]]; then
exec sudo -E bash "$0" "$@"
fi
}
# ----------------------------------------------------------------------------
# Instance table: name | target | bind_mount | plugin basename | also_watch
# ----------------------------------------------------------------------------
instance_spec() { # <name> -> echoes "target|bind|plugin|also_watch"
case "$1" in
hosts) echo "/etc/hosts|yes||" ;;
nsswitch) echo "/etc/nsswitch.conf|no|nsswitch-plugin.sh|" ;;
resolved) echo "/etc/systemd/resolved.conf|no|resolved-plugin.sh|/etc/systemd/resolved.conf.d" ;;
*) return 1 ;;
esac
}
readonly INSTANCES=(hosts nsswitch resolved)
instance_registered() { [[ -f "$TARGETS_DIR/$1.conf" ]]; }
# Parse arguments
require_root "$@"
while [[ $# -gt 0 ]]; do
case "$1" in
--rollback)
MODE="rollback"
shift
;;
--status)
MODE="status"
shift
;;
--dry-run)
DRY_RUN=1
shift
;;
-h | --help)
usage
;;
*)
err "Unknown option: $1"
exit 1
;;
esac
done
case "$MODE" in
migrate) do_migrate ;;
rollback) do_rollback ;;
status) show_status ;;
esac