From 0238f3a53142df75009894b31f2ef281b379917f Mon Sep 17 00:00:00 2001 From: v3d Date: Thu, 2 Jul 2026 21:20:47 +0200 Subject: [PATCH] Make Pivilion the exclusive dnsmasq owner --- install.sh | 9 ++++---- tests/run.sh | 18 ++++++++++++++- usr/local/lib/pivilion/mode.sh | 41 ++++++++++++++++++++++++++++++++-- 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/install.sh b/install.sh index d36984b..fc488fe 100755 --- a/install.sh +++ b/install.sh @@ -121,7 +121,7 @@ backup_managed_files() { BACKUP_DIR=$(mktemp -d /var/backups/pivilion/install-$(date +%Y%m%d-%H%M%S).XXXXXX) install -d -m 0700 "$BACKUP_DIR/files" : > "$BACKUP_DIR/present"; : > "$BACKUP_DIR/missing"; : > "$BACKUP_DIR/enabled-services"; : > "$BACKUP_DIR/active-services" - for service in apache2 pivilion-config.service pivilion-mode.service pivilion-boot-greeter.service pivilion-login-banner.service pivilion-dnsmasq.service pivilion-hostapd.service tor.service tor@default.service; do + for service in apache2 dnsmasq.service pivilion-config.service pivilion-mode.service pivilion-boot-greeter.service pivilion-login-banner.service pivilion-dnsmasq.service pivilion-hostapd.service tor.service tor@default.service; do systemctl is-enabled --quiet "$service" 2>/dev/null && printf '%s\n' "$service" >> "$BACKUP_DIR/enabled-services" || true systemctl is-active --quiet "$service" 2>/dev/null && printf '%s\n' "$service" >> "$BACKUP_DIR/active-services" || true done @@ -151,9 +151,9 @@ rollback() { relative=${path#/}; rm -rf -- "$path"; install -d -m 0755 "$(dirname "$path")"; cp -a "$BACKUP_DIR/files/$relative" "$path" done < "$BACKUP_DIR/present" systemctl daemon-reload 2>/dev/null || true - systemctl disable apache2 pivilion-config.service pivilion-mode.service pivilion-boot-greeter.service pivilion-login-banner.service tor.service tor@default.service 2>/dev/null || true + systemctl disable apache2 dnsmasq.service pivilion-config.service pivilion-mode.service pivilion-boot-greeter.service pivilion-login-banner.service tor.service tor@default.service 2>/dev/null || true while IFS= read -r path; do [ -n "$path" ] && systemctl enable "$path" 2>/dev/null || true; done < "$BACKUP_DIR/enabled-services" - for service in apache2 pivilion-config.service pivilion-mode.service pivilion-boot-greeter.service pivilion-login-banner.service pivilion-dnsmasq.service pivilion-hostapd.service tor.service tor@default.service; do + for service in apache2 dnsmasq.service pivilion-config.service pivilion-mode.service pivilion-boot-greeter.service pivilion-login-banner.service pivilion-dnsmasq.service pivilion-hostapd.service tor.service tor@default.service; do grep -Fqx "$service" "$BACKUP_DIR/active-services" || systemctl stop "$service" 2>/dev/null || true done while IFS= read -r service; do [ -n "$service" ] && systemctl start "$service" 2>/dev/null || true; done < "$BACKUP_DIR/active-services" @@ -281,9 +281,10 @@ EOF fi migrate_legacy_console_greeter systemctl daemon-reload + systemctl disable --now dnsmasq.service systemctl enable apache2 pivilion-config.service pivilion-mode.service pivilion-login-banner.service systemctl enable tor@default.service 2>/dev/null || systemctl enable tor.service - systemctl disable pivilion-dnsmasq.service pivilion-hostapd.service 2>/dev/null || true + systemctl disable pivilion-dnsmasq.service pivilion-hostapd.service /usr/local/lib/pivilion/issue-banner.sh command -v agetty >/dev/null 2>&1 && agetty --reload >/dev/null 2>&1 || true } diff --git a/tests/run.sh b/tests/run.sh index 85af2d6..715a63a 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -41,7 +41,8 @@ installer_safety() { grep -q -- '--repair' "$ROOT/install.sh" && grep -q 'backup_managed_files' "$ROOT/install.sh" && grep -q 'rollback()' "$ROOT/install.sh" && - ! grep -Eq 'disable( --now)? dnsmasq' "$ROOT/install.sh" && + grep -Fq 'systemctl disable --now dnsmasq.service' "$ROOT/install.sh" && + grep -Fq 'systemctl disable pivilion-dnsmasq.service pivilion-hostapd.service' "$ROOT/install.sh" && ! grep -q 'rm -f.*CAPTIVE\|rm -f.*\.htaccess' "$ROOT/usr/local/bin/onion" } @@ -88,6 +89,20 @@ networkmanager_startup_contract() { grep -q 'nmcli connection up "$connection" ifname "$iface"' "$ROOT/usr/local/lib/pivilion/networkmanager.sh" } +dnsmasq_ownership_contract() { + [ -f "$ROOT/etc/systemd/system/pivilion-dnsmasq.service" ] && + ! find "$ROOT/etc/systemd/system" -type l \( -name 'dnsmasq.service' -o -name 'pivilion-dnsmasq.service' \) -print -quit | grep -q . && + grep -Fq 'systemctl stop dnsmasq.service pivilion-dnsmasq.service' "$ROOT/usr/local/lib/pivilion/mode.sh" && + grep -Fq 'systemctl stop pivilion-dnsmasq.service' "$ROOT/usr/local/lib/pivilion/mode.sh" && + grep -q 'start_pivilion_dnsmasq' "$ROOT/usr/local/lib/pivilion/mode.sh" && + grep -q 'systemctl is-active --quiet pivilion-dnsmasq.service' "$ROOT/usr/local/lib/pivilion/mode.sh" && + grep -Fq 'address=/#/10.1.1.1' "$ROOT/usr/local/lib/pivilion/mode.sh" && + grep -q 'android_probe.*302' "$ROOT/usr/local/lib/pivilion/diagnose.sh" && + grep -q 'apple_probe.*302' "$ROOT/usr/local/lib/pivilion/diagnose.sh" && + grep -q 'microsoft_probe.*302' "$ROOT/usr/local/lib/pivilion/diagnose.sh" && + grep -Fqx 'RewriteCond %{REQUEST_FILENAME} -f [OR]' "$ROOT/home/pi/pivilion/config/html/.htaccess" +} + shellcheck_scripts() { command -v shellcheck >/dev/null 2>&1 || return 0 shellcheck "$ROOT/install.sh" "$ROOT"/usr/local/lib/pivilion/*.sh "$ROOT"/usr/local/bin/pivilion* "$ROOT/usr/local/bin/hotspot" "$ROOT/usr/local/bin/onion" "$ROOT/usr/local/bin/pikey" @@ -106,6 +121,7 @@ run "canonical boot configuration contract" boot_config_contract run "same-boot hotspot and onion activation contract" same_boot_mode_contract run "NetworkManager software-radio and delayed-device handling" "$ROOT/tests/test-networkmanager.sh" run "NetworkManager hotspot startup contract" networkmanager_startup_contract +run "exclusive captive dnsmasq ownership contract" dnsmasq_ownership_contract run "ShellCheck (when installed)" shellcheck_scripts [ "$failures" -eq 0 ] || { echo "$failures test group(s) failed." >&2; exit 1; } diff --git a/usr/local/lib/pivilion/mode.sh b/usr/local/lib/pivilion/mode.sh index f3c614d..db1f9d4 100755 --- a/usr/local/lib/pivilion/mode.sh +++ b/usr/local/lib/pivilion/mode.sh @@ -90,7 +90,7 @@ validate_ap_support() { } cleanup_hotspot() { - systemctl stop pivilion-dnsmasq.service pivilion-hostapd.service 2>/dev/null || true + systemctl stop dnsmasq.service pivilion-dnsmasq.service pivilion-hostapd.service 2>/dev/null || true rm -f "$DNSMASQ_CONF" "$HOSTAPD_CONF" nft delete table inet pivilion 2>/dev/null || true if command -v nmcli >/dev/null 2>&1; then @@ -115,6 +115,41 @@ check_dns_conflict() { fi } +log_dnsmasq_failure() { + { + echo "Pivilion dnsmasq diagnostics:" + echo "-- TCP/UDP port 53 listeners --" + ss -H -lntup 'sport = :53' || true + echo "-- pivilion-dnsmasq.service status --" + systemctl status pivilion-dnsmasq.service --no-pager -l || true + echo "-- generic dnsmasq.service status --" + systemctl status dnsmasq.service --no-pager -l || true + echo "-- recent pivilion-dnsmasq.service journal --" + journalctl -u pivilion-dnsmasq.service -n 50 --no-pager || true + } >&2 +} + +start_pivilion_dnsmasq() { + local i stable=0 + if ! systemctl start pivilion-dnsmasq.service; then + echo "Could not start pivilion-dnsmasq.service" >&2 + log_dnsmasq_failure + return 1 + fi + for i in $(seq 1 10); do + if systemctl is-active --quiet pivilion-dnsmasq.service; then + stable=$((stable + 1)) + [ "$stable" -ge 3 ] && return 0 + else + stable=0 + fi + sleep 1 + done + echo "pivilion-dnsmasq.service did not remain active" >&2 + log_dnsmasq_failure + return 1 +} + check_hostapd_conflict() { if systemctl is-active --quiet hostapd.service 2>/dev/null; then echo "The system hostapd service already owns a wireless interface." >&2 @@ -225,6 +260,7 @@ start_hotspot() { fi if ! check_dns_conflict; then pivilion_write_state none "$BACKEND" failed "Port 53 conflict" + log_dnsmasq_failure refresh_login_banner exit "$PIVILION_E_CONFLICT" fi @@ -245,7 +281,7 @@ start_hotspot() { ip link set "$IFACE" down; ip address flush dev "$IFACE"; ip address add 10.1.1.1/24 dev "$IFACE"; ip link set "$IFACE" up systemctl restart pivilion-hostapd.service fi - systemctl restart pivilion-dnsmasq.service + start_pivilion_dnsmasq apply_firewall if wait_for_diagnostics hotspot; then pivilion_write_state hotspot "$BACKEND" healthy none @@ -264,6 +300,7 @@ start_onion() { pivilion_write_state none "$BACKEND" activating none if [ "$BACKEND" = networkmanager ]; then pivilion_nm_prepare_interface "$IFACE"; fi cleanup_hotspot + systemctl stop pivilion-dnsmasq.service 2>/dev/null || true rm -f "$CAPTIVE_LIVE" if [ "$BACKEND" = networkmanager ]; then nmcli connection up pivilion-onion >/dev/null 2>&1 || {