diff --git a/hosts/guard/pacman-hooks/hosts-guard-common.sh b/hosts/guard/pacman-hooks/hosts-guard-common.sh new file mode 100644 index 0000000..9872cc3 --- /dev/null +++ b/hosts/guard/pacman-hooks/hosts-guard-common.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# Shared functions for hosts-guard pacman hooks +# This file is sourced by pacman-pre-unlock-hosts.sh and pacman-post-relock-hosts.sh + +TARGET=/etc/hosts +LOGTAG=hosts-guard-hook + +# Check if target has a read-only mount +is_ro_mount() { + findmnt -no OPTIONS -T "$TARGET" 2>/dev/null | grep -qw ro +} + +# Count mount layers for the target +mount_layers_count() { + awk '$5=="/etc/hosts"{c++} END{print c+0}' /proc/self/mountinfo 2>/dev/null || echo 0 +} + +# Collapse all bind mount layers +collapse_mounts() { + local i=0 + if command -v mountpoint >/dev/null 2>&1; then + while mountpoint -q "$TARGET"; do + umount -l "$TARGET" >/dev/null 2>&1 || break + i=$((i + 1)) + ((i > 20)) && break + done + else + local cnt + cnt=$(mount_layers_count) + while ((cnt > 1)); do + umount -l "$TARGET" >/dev/null 2>&1 || break + i=$((i + 1)) + ((i > 20)) && break + cnt=$(mount_layers_count) + done + fi +} + +# Stop systemd units related to hosts guard +stop_units_if_present() { + local units=(hosts-bind-mount.service hosts-guard.path) + for u in "${units[@]}"; do + if command -v systemctl >/dev/null 2>&1; then + if systemctl list-unit-files 2>/dev/null | grep -q "^$u"; then + systemctl stop "$u" >/dev/null 2>&1 || true + fi + fi + done +} + +# Remove immutable/append-only attributes +remove_host_attrs() { + if command -v lsattr >/dev/null 2>&1; then + local attrs + attrs=$(lsattr -d "$TARGET" 2>/dev/null || true) + if echo "$attrs" | grep -q " i "; then + chattr -i "$TARGET" >/dev/null 2>&1 || true + fi + if echo "$attrs" | grep -q " a "; then + chattr -a "$TARGET" >/dev/null 2>&1 || true + fi + fi +} + +# Apply immutable attribute +apply_immutable() { + if command -v chattr >/dev/null 2>&1; then + chattr +i "$TARGET" >/dev/null 2>&1 || true + fi +} + +# Apply a single read-only bind mount layer +apply_ro_bind_mount() { + mount --bind "$TARGET" "$TARGET" >/dev/null 2>&1 || true + mount -o remount,ro,bind "$TARGET" >/dev/null 2>&1 || true +} + +# Start the path watcher service +start_path_watcher() { + if command -v systemctl >/dev/null 2>&1; then + systemctl start hosts-guard.path >/dev/null 2>&1 || true + fi +} + +# Log to system logger and run log file +log_hook() { + local phase="$1" + local state="$2" + logger -t "$LOGTAG" "$phase: $state" + echo "$(date -Is) $phase-$state" >>/run/hosts-guard-hook.log 2>/dev/null || true +} diff --git a/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh b/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh index abc0bf0..9dc25ca 100644 --- a/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh +++ b/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh @@ -3,54 +3,30 @@ set -euo pipefail -TARGET=/etc/hosts +# Source shared functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=hosts-guard-common.sh +source "$SCRIPT_DIR/hosts-guard-common.sh" + ENFORCE=/usr/local/sbin/enforce-hosts.sh -LOGTAG=hosts-guard-hook -mount_layers_count() { awk '$5=="/etc/hosts"{c++} END{print c+0}' /proc/self/mountinfo 2>/dev/null || echo 0; } -collapse_mounts() { - local i=0 - if command -v mountpoint >/dev/null 2>&1; then - while mountpoint -q "$TARGET"; do - umount -l "$TARGET" >/dev/null 2>&1 || break - i=$((i + 1)) - ((i > 20)) && break - done - else - local cnt - cnt=$(mount_layers_count) - while ((cnt > 1)); do - umount -l "$TARGET" >/dev/null 2>&1 || break - i=$((i + 1)) - ((i > 20)) && break - cnt=$(mount_layers_count) - done - fi -} +log_hook "post" "relocking(start)" -# Ensure we end with a single read-only bind mount layer -logger -t "$LOGTAG" "post: relocking /etc/hosts (starting)" -echo "$(date -Is) post-relock(start)" >>/run/hosts-guard-hook.log 2>/dev/null || true +# Collapse any stacked mounts first collapse_mounts +# Run enforcement script if available if [[ -x $ENFORCE ]]; then "$ENFORCE" >/dev/null 2>&1 || true fi -if command -v chattr >/dev/null 2>&1; then - chattr +i "$TARGET" >/dev/null 2>&1 || true -fi +# Apply protections +apply_immutable +apply_ro_bind_mount -# Apply exactly one ro bind layer -mount --bind "$TARGET" "$TARGET" >/dev/null 2>&1 || true -mount -o remount,ro,bind "$TARGET" >/dev/null 2>&1 || true +# Start the path watcher +start_path_watcher -# Start only the path watcher; avoid bind-mount service (we already bound once) -if command -v systemctl >/dev/null 2>&1; then - systemctl start hosts-guard.path >/dev/null 2>&1 || true -fi - -logger -t "$LOGTAG" "post: relocking /etc/hosts (done)" -echo "$(date -Is) post-relock(done)" >>/run/hosts-guard-hook.log 2>/dev/null || true +log_hook "post" "relocking(done)" exit 0 diff --git a/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh b/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh index e407961..d6cfdf2 100644 --- a/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh +++ b/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh @@ -3,69 +3,27 @@ set -euo pipefail -TARGET=/etc/hosts -LOGTAG=hosts-guard-hook +# Source shared functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=hosts-guard-common.sh +source "$SCRIPT_DIR/hosts-guard-common.sh" -stop_units_if_present() { - local units=(hosts-bind-mount.service hosts-guard.path) - for u in "${units[@]}"; do - if command -v systemctl >/dev/null 2>&1; then - if systemctl list-unit-files 2>/dev/null | grep -q "^$u"; then - systemctl stop "$u" >/dev/null 2>&1 || true - fi - fi - done -} - -is_ro_mount() { findmnt -no OPTIONS -T "$TARGET" 2>/dev/null | grep -qw ro; } - -mount_layers_count() { awk '$5=="/etc/hosts"{c++} END{print c+0}' /proc/self/mountinfo 2>/dev/null || echo 0; } -cleanup_mount_stacks() { - local i=0 - # Unmount bind layers until /etc/hosts is no longer a mountpoint - if command -v mountpoint >/dev/null 2>&1; then - while mountpoint -q "$TARGET"; do - umount -l "$TARGET" >/dev/null 2>&1 || break - i=$((i + 1)) - ((i > 20)) && break - done - else - # Fallback to best-effort using mountinfo count - local cnt - cnt=$(mount_layers_count) - while ((cnt > 1)); do - umount -l "$TARGET" >/dev/null 2>&1 || break - i=$((i + 1)) - ((i > 20)) && break - cnt=$(mount_layers_count) - done - fi -} - -# Drop protective attributes if present -if command -v lsattr >/dev/null 2>&1; then - attrs=$(lsattr -d "$TARGET" 2>/dev/null || true) - if echo "$attrs" | grep -q " i "; then - chattr -i "$TARGET" >/dev/null 2>&1 || true - fi - if echo "$attrs" | grep -q " a "; then - chattr -a "$TARGET" >/dev/null 2>&1 || true - fi -fi +# Remove protective attributes +remove_host_attrs +# Stop guard services stop_units_if_present -logger -t "$LOGTAG" "pre: unlocking /etc/hosts (starting)" -echo "$(date -Is) pre-unlock" >>/run/hosts-guard-hook.log 2>/dev/null || true +log_hook "pre" "unlocking(start)" -# Always collapse any existing layers; we'll operate on the plain file -cleanup_mount_stacks +# Collapse any existing mount layers +collapse_mounts -# If someone managed a ro single-layer mount, ensure rw by remounting or collapsing again +# Ensure writable by remounting if still read-only if is_ro_mount; then - mount -o remount,rw "$TARGET" >/dev/null 2>&1 || cleanup_mount_stacks + mount -o remount,rw "$TARGET" >/dev/null 2>&1 || collapse_mounts fi -logger -t "$LOGTAG" "pre: unlocking /etc/hosts (done)" +log_hook "pre" "unlocking(done)" exit 0