fix: pacman hooks for hosts

This commit is contained in:
Krzysztof kuhy Rudnicki 2025-10-13 10:21:35 +02:00
parent f830f61392
commit 1e85350d5a
8 changed files with 321 additions and 6 deletions

View File

@ -10,6 +10,7 @@ Components:
- hosts-guard.path (triggers on PathChanged=/etc/hosts)
- hosts-bind-mount.service (bind mounts /etc/hosts read-only after boot)
3. psychological/ directory scripts that add delay + journaling before allowing a maintenance/unlock operation.
4. pacman hooks automatically unlock/re-lock /etc/hosts around package transactions so pacman never fails due to the read-only bind mount.
Install Flow (suggested):
1. After generating /etc/hosts via your existing hosts/install.sh, copy it to /usr/local/share/locked-hosts.
@ -19,6 +20,11 @@ Install Flow (suggested):
systemctl enable --now hosts-guard.path
systemctl enable --now hosts-bind-mount.service
4. (Optional) Use psychological/unlock-hosts.sh as the ONLY sanctioned way to modify hosts (it removes protections temporarily, launches an editor after a delay, and re-enforces on close).
5. Make pacman automatic (recommended):
./install_pacman_hooks.sh
This installs hooks under /etc/pacman.d/hooks that:
- PreTransaction: temporarily disable guard and make /etc/hosts writable
- PostTransaction: re-run enforcement and re-enable guard (bind mount + path watcher)
Limitations:
- A root user can still disable units, remount, remove attributes.

0
hosts/guard/enforce-hosts.sh Normal file → Executable file
View File

View File

@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -euo pipefail
require_root() { if [[ $EUID -ne 0 ]]; then exec sudo -E bash "$0" "$@"; fi }
require_root "$@"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
HOOKS_DIR="/etc/pacman.d/hooks"
install -d -m 755 "$HOOKS_DIR"
# Pre-transaction hook
cat >"$HOOKS_DIR/10-unlock-etc-hosts.hook" <<'HOOK'
[Trigger]
Operation = Upgrade
Operation = Install
Operation = Remove
Type = Package
Target = *
[Action]
Description = Temporarily unlocking /etc/hosts for transaction
When = PreTransaction
Exec = /bin/bash /usr/local/share/hosts-guard/pacman-pre-unlock-hosts.sh
NeedsTargets
HOOK
# Post-transaction hook
cat >"$HOOKS_DIR/90-relock-etc-hosts.hook" <<'HOOK'
[Trigger]
Operation = Upgrade
Operation = Install
Operation = Remove
Type = Package
Target = *
[Action]
Description = Re-locking /etc/hosts after transaction
When = PostTransaction
Exec = /bin/bash /usr/local/share/hosts-guard/pacman-post-relock-hosts.sh
NeedsTargets
HOOK
# Place helper scripts into a shared location
install -d -m 755 /usr/local/share/hosts-guard
install -m 755 "$SCRIPT_DIR/pacman-hooks/pacman-pre-unlock-hosts.sh" /usr/local/share/hosts-guard/
install -m 755 "$SCRIPT_DIR/pacman-hooks/pacman-post-relock-hosts.sh" /usr/local/share/hosts-guard/
echo "Pacman hooks installed into $HOOKS_DIR."

View File

@ -0,0 +1,50 @@
#!/usr/bin/env bash
# Post-transaction hook to re-apply hosts guard protections (single-layer ro bind)
TARGET=/etc/hosts
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 >/devnull 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
}
# 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_mounts
if [[ -x "$ENFORCE" ]]; then
"$ENFORCE" >/dev/null 2>&1 || true
fi
# 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 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
exit 0

View File

@ -0,0 +1,66 @@
#!/usr/bin/env bash
# Non-interactive pre-transaction hook to temporarily unlock /etc/hosts
TARGET=/etc/hosts
LOGTAG=hosts-guard-hook
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; }
is_bind_mount() { findmnt -no OPTIONS -T "$TARGET" 2>/dev/null | grep -qw bind; }
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)
echo "$attrs" | grep -q " i " && chattr -i "$TARGET" >/dev/null 2>&1 || true
echo "$attrs" | grep -q " a " && chattr -a "$TARGET" >/dev/null 2>&1 || true
fi
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
# Always collapse any existing layers; we'll operate on the plain file
cleanup_mount_stacks
# If someone managed a ro single-layer mount, ensure rw by remounting or collapsing again
if is_ro_mount; then
mount -o remount,rw "$TARGET" >/dev/null 2>&1 || cleanup_mount_stacks
fi
logger -t "$LOGTAG" "pre: unlocking /etc/hosts (done)"
exit 0

0
hosts/guard/setup_hosts_guard.sh Normal file → Executable file
View File

View File

@ -12,6 +12,36 @@ BOLD='\033[1m'
NC='\033[0m' # No Color
PACMAN_BIN="/usr/bin/pacman"
# Determine if this invocation may perform a transaction (upgrade/install/remove)
needs_unlock() {
# If args include -S (install/upgrade), -U (local install), or -R (remove), we unlock
# Also include -Su/-Syu/-Syuu when -S is part of the combined flag
for arg in "$@"; do
case "$arg" in
-S*|-U|-R|--sync|--upgrade|--remove)
return 0 ;;
esac
done
return 1
}
# Run pre/post hooks for /etc/hosts guard if present
pre_unlock_hosts() {
local pre="/usr/local/share/hosts-guard/pacman-pre-unlock-hosts.sh"
if [[ -x "$pre" ]]; then
echo -e "${CYAN}[hosts-guard] Preparing /etc/hosts for transaction...${NC}" >&2
/bin/bash "$pre" || true
fi
}
post_relock_hosts() {
local post="/usr/local/share/hosts-guard/pacman-post-relock-hosts.sh"
if [[ -x "$post" ]]; then
/bin/bash "$post" || true
echo -e "${CYAN}[hosts-guard] Protections re-applied to /etc/hosts.${NC}" >&2
fi
}
# Ensure periodic system services (timer/monitor) are set up; if not, trigger setup
ensure_periodic_maintenance() {
@ -155,7 +185,7 @@ function has_noconfirm_flag() {
# Cleanup: remove any installed blocked packages (in addition to the queued operation)
function remove_installed_blocked_packages() {
local user_args=("$@")
# args not used; kept for future policy extension
# List installed package names
mapfile -t installed_names < <("$PACMAN_BIN" -Qq 2>/dev/null)
local to_remove=()
@ -223,8 +253,10 @@ function check_for_steam() {
# Function to check if current day is a weekday (after 4PM Friday until midnight Sunday)
function is_weekday() {
local day_of_week=$(date +%u) # %u gives 1-7 (Monday is 1, Sunday is 7)
local hour=$(date +%H) # %H gives hour in 24-hour format (00-23)
local day_of_week
day_of_week=$(date +%u) # %u gives 1-7 (Monday is 1, Sunday is 7)
local hour
hour=$(date +%H) # %H gives hour in 24-hour format (00-23)
# Monday through Thursday are always weekdays
if [[ $day_of_week -ge 1 && $day_of_week -le 4 ]]; then
@ -248,7 +280,8 @@ function prompt_for_steam_challenge() {
# Check if it's a weekday and block completely
if is_weekday; then
local day_name=$(date +%A)
local day_name
day_name=$(date +%A)
echo -e "${RED}Steam installation BLOCKED: Steam cannot be installed on weekdays.${NC}"
echo -e "${RED}Today is $day_name. Please try again on the weekend (Saturday or Sunday).${NC}"
return 1
@ -522,15 +555,23 @@ fi
display_operation "$1"
# Echo the command that's about to be executed
echo -e "${GREEN}Executing:${NC} $PACMAN_BIN $@" >&2
echo -e "${GREEN}Executing:${NC} $PACMAN_BIN $*" >&2
# Record start time for statistics
start_time=$(date +%s)
# Execute the real pacman command
# Execute the real pacman command (with /etc/hosts guard handling)
if needs_unlock "$@"; then
pre_unlock_hosts
fi
"$PACMAN_BIN" "$@"
exit_code=$?
if needs_unlock "$@"; then
post_relock_hosts
fi
# Record end time for statistics
end_time=$(date +%s)
duration=$((end_time - start_time))

103
scripts/toggle_window_manager.sh Executable file
View File

@ -0,0 +1,103 @@
#!/bin/bash
set -euo pipefail
# Configuration -----------------------------------------------------------------
TARGET_SESSION_NAME="Xfce Session"
TARGET_PACKAGES=(
xfwm4 # Compositing window manager with XFCE integration
xfce4-session # Provides the Xfce session entry for display managers
xfce4-panel # Panel with system tray support
xfce4-settings # Settings daemon (enables compositing toggle, theming, etc.)
xfce4-terminal # Handy default terminal for the new environment
)
# Utility functions --------------------------------------------------------------
info() { echo "[INFO] $*"; }
warn() { echo "[WARN] $*" >&2; }
error() { echo "[ERROR] $*" >&2; exit 1; }
require_command() {
local cmd="$1" pkg_hint="${2:-}"
if ! command -v "$cmd" >/dev/null 2>&1; then
if [[ -n "$pkg_hint" ]]; then
warn "Install '$pkg_hint' to obtain the '$cmd' command."
fi
error "Required command '$cmd' not found."
fi
}
ensure_pacman() {
require_command pacman "pacman"
if ! grep -qi "arch" /etc/os-release 2>/dev/null; then
warn "This script was designed for Arch Linux; continuing anyway."
fi
}
install_packages() {
local missing=()
for pkg in "${TARGET_PACKAGES[@]}"; do
if ! pacman -Qi "$pkg" >/dev/null 2>&1; then
missing+=("$pkg")
fi
done
if [[ ${#missing[@]} -eq 0 ]]; then
info "All target packages are already installed."
return
fi
if ! command -v sudo >/dev/null 2>&1; then
error "sudo is required to install packages. Install sudo or run this script as root."
fi
info "Installing missing packages: ${missing[*]}"
sudo pacman -S --needed --noconfirm "${missing[@]}"
}
print_post_install_tips() {
cat <<EOF
------------------------------------------------------------------------
XFCE session installed.
• i3 remains your default window manager. We did not modify ~/.xinitrc,
display manager defaults, or systemd targets.
• At your next graphical login, pick "${TARGET_SESSION_NAME}" (or "Xfce"),
then log in to enjoy compositing via xfwm4.
• Once you are done testing Unity, simply log out and choose i3 again.
We'll log you out now so you can switch sessions safely.
------------------------------------------------------------------------
EOF
}
logout_user() {
local session_id="${XDG_SESSION_ID:-}"
if [[ -n "$session_id" ]] && loginctl show-session "$session_id" >/dev/null 2>&1; then
info "Terminating current session (ID: $session_id) via loginctl."
loginctl terminate-session "$session_id"
return
fi
if loginctl list-sessions 2>/dev/null | awk '{print $1" "$3}' | grep -q " $USER$"; then
info "Terminating all sessions for user '$USER' via loginctl."
loginctl terminate-user "$USER"
return
fi
warn "loginctl could not terminate the session; attempting fallback logout."
pkill -KILL -u "$USER" || error "Failed to terminate user sessions. Please log out manually."
}
main() {
ensure_pacman
install_packages
print_post_install_tips
# Give the user a moment to read the instructions before logging out.
logout_user
}
main "$@"