Fix captive portal detection and restore MOTD

This commit is contained in:
v3d 2026-06-29 17:29:00 +02:00
parent 3b7a16564b
commit 34b4c4b036
9 changed files with 153 additions and 59 deletions

View file

@ -42,3 +42,7 @@ Use `pikey` to save upstream Wi-Fi credentials, then `onion` and reboot to host
through Tor. Use `hotspot` and reboot to run the local captive gallery at
`http://10.1.1.1/`. HTTPS interception is intentionally not attempted because a
local captive portal cannot present valid certificates for arbitrary sites.
If a phone joins but does not open its captive-login window, run
`pivilion-diagnose` on the Pi to verify DHCP/DNS, Apache rewrite handling,
nftables, and the common Android, Apple, and Microsoft HTTP probes.

View file

@ -0,0 +1,12 @@
# Show the Pivilion status banner for the configured owner on interactive Bash
# login shells. Other users and non-interactive sessions remain unchanged.
[ -n "${BASH_VERSION:-}" ] || return 0
case $- in *i*) ;; *) return 0 ;; esac
[ -r /usr/local/lib/pivilion/common.sh ] || return 0
[ "${PIVILION_MOTD_SHOWN:-0}" != 1 ] || return 0
. /usr/local/lib/pivilion/common.sh
pivilion_load_config >/dev/null 2>&1 || return 0
[ "$(id -un)" = "$PIVILION_USER" ] || return 0
pivilion_print_motd
export PIVILION_MOTD_SHOWN=1

View file

@ -112,27 +112,13 @@ if ! shopt -oq posix; then
fi
fi
# info on login
let upSeconds="$(/usr/bin/cut -d. -f1 /proc/uptime)"
let secs=$((${upSeconds}%60))
let mins=$((${upSeconds}/60%60))
let hours=$((${upSeconds}/3600%24))
let days=$((${upSeconds}/86400))
UPTIME=`printf "%d days, %02dh%02dm%02ds" "$days" "$hours" "$mins" "$secs"`
# get the load averages
read one five fifteen rest < /proc/loadavg
echo "
@@@@@@@@@@@@@@@@@@@ `date +"%A, %e %B %Y, %r"`
@@@@@@, &@& *@@@@@@ `uname -srnmo`
@@@@@@@. .@@@@@@@ Uptime.............: ${UPTIME}
@@@@ .@@@. @@@@ Memory.............: `cat /proc/meminfo | grep MemFree | awk {'print $2'}`kB (Free) / `cat /proc/meminfo | grep MemTotal | awk {'print $2'}`kB (Total)
@ &@@@@.@@@@# @ Load Averages......: ${one}, ${five}, ${fifteen} (1, 5, 15 min)
@ @ . #@@@# . @ @ Running Processes..: `ps ax | wc -l | tr -d " "`
@ @@@, # ,@@@ @ Manual / more info.: pivilion.net
@@, @ ,@@ Onion Address......: `cat /home/pi/pivilion/torname`
@@@@@#@@@@@@@#@@@@@ Run "pivilion" for inital setup! / Running in `cat /home/pi/pivilion/mode` mode
"
# Pivilion login banner. The modern installer deploys the same hook through
# /etc/profile.d; this fallback keeps the legacy filesystem overlay usable.
if [ "${PIVILION_MOTD_SHOWN:-0}" != 1 ] && [ -r /usr/local/lib/pivilion/common.sh ]; then
. /usr/local/lib/pivilion/common.sh
if pivilion_load_config >/dev/null 2>&1 && [ "$(id -un)" = "$PIVILION_USER" ]; then
pivilion_print_motd
export PIVILION_MOTD_SHOWN=1
fi
fi
export LC_ALL=C

View file

@ -1,17 +1,11 @@
RewriteEngine on
RewriteCond %{REQUEST_URI} !(\/index\.php)$
RewriteCond %{REQUEST_URI} !(\/pi-logo_128\.png)$
RewriteCond %{REQUEST_URI} !(\/content\/.*)$
RewriteCond %{REQUEST_URI} !(\/index\.php)$
RewriteCond %{REQUEST_URI} !(\/pi-logo_128\.png)$
RewriteCond %{REQUEST_URI} !(\/content\/.*)$
RewriteCond %{REQUEST_URI} !(\/css\/.*)$
RewriteCond %{REQUEST_URI} !(\/doc\/.*)$
RewriteCond %{REQUEST_URI} !(\/docker\/.*)$
RewriteCond %{REQUEST_URI} !(\/img\/.*)$
RewriteCond %{REQUEST_URI} !(\/js\/.*)$
RewriteCond %{REQUEST_URI} !(\/modules\/.*)$
RewriteCond %{REQUEST_URI} !(\/tests\/.*)$
RewriteCond %{REQUEST_URI} !(\/*.php)$
RewriteCond %{REQUEST_URI} !(\/*.head.*)$
RewriteRule ^(.*)$ http://10.1.1.1/index.php [L,R=301]
# Captive-network probes use otherwise unknown paths such as /generate_204,
# /hotspot-detect.html, and /connecttest.txt. Redirect those temporarily so
# clients open their login assistant, but let the gallery serve its own PHP and
# asset paths without a loop.
RewriteCond %{REQUEST_URI} !\.php$ [NC]
RewriteCond %{REQUEST_URI} !\.head(?:\.|$) [NC]
RewriteCond %{REQUEST_URI} !^/(?:content|css|doc|docker|img|images|js|modules|tests|upload)(?:/|$) [NC]
RewriteCond %{REQUEST_URI} !^/pi-logo_128\.png$ [NC]
RewriteRule ^ http://10.1.1.1/index.php [L,R=302,NE]

View file

@ -1,15 +1,16 @@
#!/bin/bash
set -euo pipefail
SOURCE_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
source "$SOURCE_DIR/usr/local/lib/pivilion/common.sh"
pivilion_load_config
pivilion_print_motd "Pivilion installer"
if [ "$(id -u)" -ne 0 ]; then
echo "Run this installer with sudo: sudo ./install.sh" >&2
exit 1
fi
SOURCE_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
source "$SOURCE_DIR/usr/local/lib/pivilion/common.sh"
pivilion_load_config
export DEBIAN_FRONTEND=noninteractive
is_networkmanager_system() {
@ -20,7 +21,7 @@ is_networkmanager_system() {
}
install_packages() {
local packages=(apache2 libapache2-mod-php php php-cli tor dnsmasq nftables iproute2 iw rfkill wget unzip wpasupplicant)
local packages=(apache2 libapache2-mod-php php php-cli tor dnsmasq dnsutils nftables iproute2 iw rfkill curl wget unzip wpasupplicant)
apt-get update
apt-get install -y "${packages[@]}"
if is_networkmanager_system; then
@ -37,13 +38,14 @@ install_packages() {
deploy_files() {
local php_version
install -d -m 0755 /usr/local/lib/pivilion /usr/local/bin /etc/systemd/system /etc/tor/torrc.d
install -d -m 0755 /usr/local/lib/pivilion /usr/local/bin /etc/systemd/system /etc/tor/torrc.d /etc/profile.d
install -m 0644 "$SOURCE_DIR/usr/local/lib/pivilion/common.sh" /usr/local/lib/pivilion/common.sh
install -m 0755 "$SOURCE_DIR/usr/local/lib/pivilion/mode.sh" /usr/local/lib/pivilion/mode.sh
install -m 0755 "$SOURCE_DIR"/usr/local/bin/* /usr/local/bin/
install -m 0644 "$SOURCE_DIR/etc/systemd/system/pivilion-mode.service" /etc/systemd/system/pivilion-mode.service
install -m 0644 "$SOURCE_DIR/etc/systemd/system/pivilion-dnsmasq.service" /etc/systemd/system/pivilion-dnsmasq.service
install -m 0644 "$SOURCE_DIR/etc/tor/torrc.d/pivilion.conf" /etc/tor/torrc.d/pivilion.conf
install -m 0644 "$SOURCE_DIR/etc/profile.d/pivilion-motd.sh" /etc/profile.d/pivilion-motd.sh
cat > /etc/pivilion.defaults <<EOF
PIVILION_USER=$PIVILION_USER
@ -122,7 +124,7 @@ validate_install() {
dnsmasq --test
bash -n /usr/local/lib/pivilion/common.sh /usr/local/lib/pivilion/mode.sh /usr/local/bin/pivilion \
/usr/local/bin/hotspot /usr/local/bin/onion /usr/local/bin/pikey /usr/local/bin/generator \
/usr/local/bin/static /usr/local/bin/hotglue /usr/local/bin/htaccess
/usr/local/bin/static /usr/local/bin/hotglue /usr/local/bin/htaccess /usr/local/bin/pivilion-diagnose
}
install_packages

View file

@ -3,21 +3,11 @@ set -euo pipefail
source /usr/local/lib/pivilion/common.sh
pivilion_load_config
mode=$(pivilion_current_mode)
torname="not generated yet"
[ ! -s "$PIVILION_DIR/torname" ] || torname=$(tr -d '\r\n' < "$PIVILION_DIR/torname")
uptime_text=$(uptime -p 2>/dev/null || true)
pivilion_print_motd
cat <<EOF
Welcome to Pivilion!
Host...............: $(uname -n)
System.............: $(uname -srmo)
Uptime.............: ${uptime_text:-unknown}
Mode after reboot..: $mode
Onion address......: $torname
Web root...........: $PIVILION_WEBROOT
Web root: $PIVILION_WEBROOT
Commands:
pivilion show this status
@ -28,4 +18,5 @@ Commands:
static convert the current gallery to static files
hotglue install or restore Hotglue
htaccess reset or remove captive redirect rules
pivilion-diagnose check captive DNS, HTTP redirects, and services
EOF

View file

@ -0,0 +1,4 @@
#!/bin/bash
set -euo pipefail
[ "$(id -u)" -eq 0 ] || exec sudo "$0" "$@"
exec /usr/local/lib/pivilion/mode.sh diagnose

View file

@ -111,3 +111,30 @@ pivilion_current_mode() {
pivilion_require_root() {
[ "$(id -u)" -eq 0 ] || { echo "This operation must run as root (use sudo)." >&2; return 1; }
}
pivilion_print_motd() {
local heading=${1:-Welcome to Pivilion!}
local uptime_text memory_free memory_total load_one load_five load_fifteen processes torname mode
uptime_text=$(uptime -p 2>/dev/null || printf 'unknown')
memory_free=$(awk '/^MemAvailable:/ {print $2; exit}' /proc/meminfo 2>/dev/null)
memory_total=$(awk '/^MemTotal:/ {print $2; exit}' /proc/meminfo 2>/dev/null)
read -r load_one load_five load_fifteen _ < /proc/loadavg
processes=$(ps ax --no-headers 2>/dev/null | wc -l)
torname=not-generated-yet
[ ! -s "$PIVILION_DIR/torname" ] || torname=$(tr -d '\r\n' < "$PIVILION_DIR/torname")
mode=$(pivilion_current_mode)
printf '\n%s\n' "$heading"
cat <<EOF
@@@@@@@@@@@@@@@@@@@ $(date '+%A, %e %B %Y, %r')
@@@@@@, &@& *@@@@@@ $(uname -srnmo)
@@@@@@@. .@@@@@@@ Uptime.............: $uptime_text
@@@@ .@@@. @@@@ Memory.............: ${memory_free:-unknown}kB (Available) / ${memory_total:-unknown}kB (Total)
@ &@@@@.@@@@# @ Load Averages......: $load_one, $load_five, $load_fifteen (1, 5, 15 min)
@ @ . #@@@# . @ @ Running Processes..: $processes
@ @@@, # ,@@@ @ Manual / more info.: pivilion.net
@@, @ ,@@ Onion Address......: $torname
@@@@@#@@@@@@@#@@@@@ Running in $mode mode
EOF
}

View file

@ -11,6 +11,77 @@ IFACE=$(pivilion_wifi_interface)
RUN_DIR=/run/pivilion
DNSMASQ_CONF=/etc/dnsmasq.d/pivilion-runtime.conf
NM_HOTSPOT=pivilion-hotspot
CAPTIVE_TEMPLATE=$PIVILION_DIR/config/html/.htaccess
CAPTIVE_LIVE=$PIVILION_WEBROOT/gen/.htaccess
install_captive_redirect() {
local tmp
[ -r "$CAPTIVE_TEMPLATE" ] || {
echo "Captive redirect template is missing: $CAPTIVE_TEMPLATE" >&2
return 1
}
[ -d "$PIVILION_WEBROOT/gen" ] || {
echo "Gallery webroot is missing: $PIVILION_WEBROOT/gen" >&2
return 1
}
tmp=$(mktemp "$PIVILION_WEBROOT/gen/.htaccess.XXXXXX")
if ! install -m 0644 -o root -g www-data "$CAPTIVE_TEMPLATE" "$tmp"; then
rm -f "$tmp"
return 1
fi
mv -f "$tmp" "$CAPTIVE_LIVE"
}
diagnostic_result() {
local label=$1
shift
if "$@"; then
printf ' [ok] %s\n' "$label"
return 0
fi
printf ' [fail] %s\n' "$label" >&2
return 1
}
probe_redirect() {
local host=$1 path=$2 headers
headers=$(curl --silent --show-error --max-time 5 --dump-header - --output /dev/null \
--header "Host: $host" "http://10.1.1.1$path") || return 1
grep -Eq '^HTTP/[0-9.]+ 302([[:space:]]|$)' <<<"$headers" &&
grep -Eiq '^Location:[[:space:]]*http://10\.1\.1\.1/index\.php\r?$' <<<"$headers"
}
dns_points_to_portal() {
local host=$1 answer
answer=$(dig +short A @10.1.1.1 "$host" 2>/dev/null | tail -n1)
[ "$answer" = 10.1.1.1 ]
}
apache_rewrite_enabled() {
apache2ctl -M 2>/dev/null | grep -q 'rewrite_module'
}
nft_portal_table_active() {
nft list table inet pivilion >/dev/null 2>&1
}
hotspot_diagnostics() {
local failures=0
echo "Pivilion hotspot diagnostics:"
diagnostic_result "Apache is active" systemctl is-active --quiet apache2 || failures=$((failures + 1))
diagnostic_result "Apache rewrite module is enabled" apache_rewrite_enabled || failures=$((failures + 1))
diagnostic_result "live captive redirect matches its template" cmp -s "$CAPTIVE_TEMPLATE" "$CAPTIVE_LIVE" || failures=$((failures + 1))
diagnostic_result "Pivilion dnsmasq is active" systemctl is-active --quiet pivilion-dnsmasq.service || failures=$((failures + 1))
diagnostic_result "captive DNS resolves Android probe" dns_points_to_portal connectivitycheck.gstatic.com || failures=$((failures + 1))
diagnostic_result "Pivilion nftables table is active" nft_portal_table_active || failures=$((failures + 1))
diagnostic_result "Android probe receives portal redirect" probe_redirect connectivitycheck.gstatic.com /generate_204 || failures=$((failures + 1))
diagnostic_result "Apple probe receives portal redirect" probe_redirect captive.apple.com /hotspot-detect.html || failures=$((failures + 1))
diagnostic_result "Microsoft probe receives portal redirect" probe_redirect www.msftconnecttest.com /connecttest.txt || failures=$((failures + 1))
[ "$failures" -eq 0 ] || {
echo "$failures hotspot diagnostic check(s) failed." >&2
return 1
}
}
stop_hotspot() {
systemctl stop pivilion-dnsmasq.service 2>/dev/null || true
@ -56,6 +127,7 @@ EOF
start_hotspot() {
stop_hotspot
install_captive_redirect
rfkill unblock wifi 2>/dev/null || true
systemctl start apache2
if pivilion_uses_networkmanager; then
@ -87,6 +159,7 @@ EOF
dnsmasq --test --conf-file="$DNSMASQ_CONF"
systemctl restart pivilion-dnsmasq.service
start_firewall
hotspot_diagnostics
echo "Pivilion hotspot '$PIVILION_HOTSPOT_SSID' is active on $IFACE (10.1.1.1)."
}
@ -120,7 +193,7 @@ start_onion() {
systemctl restart dhcpcd 2>/dev/null || true
systemctl restart "wpa_supplicant@$IFACE.service" 2>/dev/null || systemctl restart wpa_supplicant 2>/dev/null || true
fi
rm -f "$PIVILION_WEBROOT/gen/.htaccess"
rm -f "$CAPTIVE_LIVE"
systemctl start apache2
if systemctl cat tor@default.service >/dev/null 2>&1; then
systemctl restart tor@default.service
@ -134,5 +207,6 @@ start_onion() {
case "$MODE" in
hotspot) start_hotspot ;;
onion) start_onion ;;
diagnose) hotspot_diagnostics ;;
*) echo "Unknown Pivilion mode: $MODE" >&2; exit 2 ;;
esac