Compare commits

..

3 commits

17 changed files with 925 additions and 188 deletions

View file

@ -32,6 +32,16 @@ sudo ./install.sh
sudo reboot sudo reboot
``` ```
The installer performs preflight checks, backs up managed configuration under
`/var/backups/pivilion`, validates the deployed services, and restores the
previous configuration if deployment fails. Additional maintenance modes are:
```sh
./install.sh --dry-run # read-only preflight and proposed actions
sudo ./install.sh --check # validate an existing installation
sudo ./install.sh --repair # repeat deployment without replacing gallery content
```
The installer defaults to the user that invoked `sudo`, not to a hard-coded The installer defaults to the user that invoked `sudo`, not to a hard-coded
`pi` account. To customize paths, startup mode, hotspot SSID, or Wi-Fi `pi` account. To customize paths, startup mode, hotspot SSID, or Wi-Fi
interface, copy `pivilion.conf.example` to `/boot/firmware/pivilion.conf` (or interface, copy `pivilion.conf.example` to `/boot/firmware/pivilion.conf` (or
@ -42,3 +52,22 @@ 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 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 `http://10.1.1.1/`. HTTPS interception is intentionally not attempted because a
local captive portal cannot present valid certificates for arbitrary sites. 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.
`pivilion status` distinguishes the requested next-boot mode from the last
verified active mode and reports degraded activation. `pivilion diagnose
--json` provides the same health checks for monitoring. The legacy
`pivilion-diagnose` command remains an alias.
On the physical TTY, Pivilion appends its current IP, ASCII logo, and system
status through `/etc/issue.d/90-pivilion.issue`; Raspberry Pi OS retains its
normal Debian/hostname/TTY heading. The banner appears before `login:` rather
than among systemd boot messages. SSH sessions receive the same status once
after authentication.
Diagnostic exit codes are stable: `10` invalid configuration, `11` missing or
unsupported hardware, `12` service/port conflict, `13` core service failure,
`14` captive portal failure, `15` Tor/onion failure, and `20` installer failure.

View file

@ -0,0 +1,13 @@
# TTY consoles receive the banner from agetty before login. Keep the
# post-authentication banner only for interactive SSH sessions.
[ -n "${BASH_VERSION:-}" ] || return 0
case $- in *i*) ;; *) return 0 ;; esac
[ -n "${SSH_CONNECTION:-}" ] || return 0
[ -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

@ -0,0 +1,11 @@
[Unit]
Description=Pivilion legacy Wi-Fi access point
After=network-pre.target
[Service]
Type=simple
ExecStart=/usr/sbin/hostapd /run/pivilion/hostapd.conf
Restart=on-failure
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,14 @@
[Unit]
Description=Generate the Pivilion TTY login banner
After=local-fs.target
Before=getty@tty1.service
[Service]
Type=oneshot
ExecStart=/usr/local/lib/pivilion/issue-banner.sh
TimeoutStartSec=5
StandardOutput=null
StandardError=journal
[Install]
WantedBy=multi-user.target

View file

@ -7,7 +7,7 @@ Wants=network-pre.target
Type=oneshot Type=oneshot
ExecStart=/usr/local/lib/pivilion/mode.sh ExecStart=/usr/local/lib/pivilion/mode.sh
RemainAfterExit=yes RemainAfterExit=yes
TimeoutStartSec=90 TimeoutStartSec=120
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View file

@ -112,27 +112,13 @@ if ! shopt -oq posix; then
fi fi
fi fi
# info on login # Pivilion login banner. The modern installer deploys the same hook through
# /etc/profile.d; this fallback keeps the legacy filesystem overlay usable.
let upSeconds="$(/usr/bin/cut -d. -f1 /proc/uptime)" if [ "${PIVILION_MOTD_SHOWN:-0}" != 1 ] && [ -r /usr/local/lib/pivilion/common.sh ]; then
let secs=$((${upSeconds}%60)) . /usr/local/lib/pivilion/common.sh
let mins=$((${upSeconds}/60%60)) if pivilion_load_config >/dev/null 2>&1 && [ "$(id -un)" = "$PIVILION_USER" ]; then
let hours=$((${upSeconds}/3600%24)) pivilion_print_motd
let days=$((${upSeconds}/86400)) export PIVILION_MOTD_SHOWN=1
UPTIME=`printf "%d days, %02dh%02dm%02ds" "$days" "$hours" "$mins" "$secs"` fi
fi
# 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
"
export LC_ALL=C export LC_ALL=C

View file

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

View file

@ -1,51 +1,184 @@
#!/bin/bash #!/bin/bash
set -euo pipefail set -Eeuo pipefail
if [ "$(id -u)" -ne 0 ]; then INSTALL_VERSION=2
echo "Run this installer with sudo: sudo ./install.sh" >&2 ACTION=install
exit 1 case "${1:-}" in
fi '') ;;
--check) ACTION=check ;;
--dry-run) ACTION=dry-run ;;
--repair) ACTION=repair ;;
-h|--help) echo "Usage: sudo ./install.sh [--check|--dry-run|--repair]"; exit 0 ;;
*) echo "Usage: sudo ./install.sh [--check|--dry-run|--repair]" >&2; exit 2 ;;
esac
[ "$#" -le 1 ] || { echo "Only one installer action may be specified." >&2; exit 2; }
SOURCE_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) SOURCE_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
source "$SOURCE_DIR/usr/local/lib/pivilion/common.sh" source "$SOURCE_DIR/usr/local/lib/pivilion/common.sh"
pivilion_load_config pivilion_load_config || exit "$PIVILION_E_CONFIG"
cat <<'PIVILION_LOGO'
Welcome to Pivilion!
@@@@@@@@@@@@@@@@@@@
@@@@@@, &@& *@@@@@@
@@@@@@@. .@@@@@@@
@@@@ .@@@. @@@@
@ &@@@@.@@@@# @
@ @ . #@@@# . @ @
@ @@@, # ,@@@ @
@@, @ ,@@
@@@@@#@@@@@@@#@@@@@
PIVILION_LOGO
if [ "$ACTION" != dry-run ] && [ "$(id -u)" -ne 0 ]; then
echo "Run this installer with sudo: sudo ./install.sh ${1:-}" >&2
exit "$PIVILION_E_INSTALL"
fi
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
BACKUP_DIR=""
TRANSACTION_ACTIVE=0
declare -a MANAGED_PATHS=(
/etc/pivilion.defaults /etc/pivilion.php /etc/profile.d/pivilion-motd.sh /etc/issue.d/90-pivilion.issue
/etc/apache2/ports.conf /etc/apache2/conf-available/pivilion.conf
/etc/apache2/sites-available/000-default.conf /etc/apache2/sites-available/000-upload.conf
/etc/apache2/conf-enabled/pivilion.conf /etc/apache2/sites-enabled/000-default.conf
/etc/tor/torrc /etc/tor/torrc.d/pivilion.conf
/etc/systemd/system/pivilion-mode.service /etc/systemd/system/pivilion-dnsmasq.service
/etc/systemd/system/pivilion-hostapd.service /etc/systemd/system/pivilion-boot-greeter.service
/etc/systemd/system/pivilion-login-banner.service /etc/rc.local
/usr/local/lib/pivilion /usr/local/bin/pivilion /usr/local/bin/pivilion-diagnose
/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
/var/lib/pivilion/install-state
/etc/php/7.4/apache2/conf.d/99-pivilion.ini
/etc/php/8.2/apache2/conf.d/99-pivilion.ini
/etc/php/8.4/apache2/conf.d/99-pivilion.ini
)
log() { printf '==> %s\n' "$*"; }
warn() { printf 'WARNING: %s\n' "$*" >&2; }
preflight() {
local codename free_kb iface package
log "Running preflight checks"
[ -r /etc/os-release ] || { echo "Cannot identify the operating system." >&2; return "$PIVILION_E_INSTALL"; }
. /etc/os-release
codename=${VERSION_CODENAME:-unknown}
case "$codename" in bullseye|bookworm|trixie) ;; *) echo "Unsupported OS codename: $codename" >&2; return "$PIVILION_E_INSTALL" ;; esac
getent passwd "$PIVILION_USER" >/dev/null || return "$PIVILION_E_CONFIG"
[ -d "$PIVILION_HOME" ] || { echo "Configured home does not exist: $PIVILION_HOME" >&2; return "$PIVILION_E_CONFIG"; }
free_kb=$(df -Pk / | awk 'NR == 2 {print $4}')
[ "${free_kb:-0}" -ge 524288 ] || { echo "At least 512 MiB free space is required." >&2; return "$PIVILION_E_INSTALL"; }
iface=$(pivilion_wifi_interface)
[ -n "$iface" ] || { echo "No Wi-Fi interface found." >&2; return "$PIVILION_E_HARDWARE"; }
[ -d "/sys/class/net/$iface" ] || { echo "Configured Wi-Fi interface does not exist: $iface" >&2; return "$PIVILION_E_HARDWARE"; }
if command -v iw >/dev/null 2>&1; then
local phy
phy=$(iw dev "$iface" info 2>/dev/null | awk '$1 == "wiphy" {print "phy" $2; exit}')
[ -z "$phy" ] || iw phy "$phy" info 2>/dev/null | grep -Eq '^[[:space:]]*\* AP$' || warn "$iface does not advertise AP mode; hotspot activation will fail safely."
fi
if ss -H -ltn 'sport = :80' 2>/dev/null | grep -q .; then warn "Port 80 is currently occupied; this is expected only when Apache is already installed."; fi
if ss -H -lntu 'sport = :53' 2>/dev/null | grep -Eq '(0\.0\.0\.0|\*):53'; then warn "A wildcard DNS listener may conflict with captive mode."; fi
if command -v apt-cache >/dev/null 2>&1; then
for package in apache2 php tor dnsmasq hostapd nftables network-manager; do
apt-cache show "$package" >/dev/null 2>&1 || warn "Package metadata is unavailable for $package; apt-get update may be required."
done
fi
log "Preflight passed for $codename using interface $iface"
}
is_networkmanager_system() { is_networkmanager_system() {
command -v nmcli >/dev/null 2>&1 && { command -v nmcli >/dev/null 2>&1 && { systemctl is-enabled --quiet NetworkManager 2>/dev/null || systemctl is-active --quiet NetworkManager 2>/dev/null; }
systemctl is-enabled --quiet NetworkManager 2>/dev/null ||
systemctl is-active --quiet NetworkManager 2>/dev/null
}
} }
install_packages() { 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 hostapd nftables iproute2 iw rfkill curl wget unzip wpasupplicant)
log "Installing required packages"
apt-get update apt-get update
apt-get install -y "${packages[@]}" apt-get install -y "${packages[@]}"
if is_networkmanager_system; then if is_networkmanager_system; then
echo "Using the installed NetworkManager backend." log "Using NetworkManager"
elif command -v dhcpcd >/dev/null 2>&1 || dpkg-query -W dhcpcd5 >/dev/null 2>&1; then elif command -v dhcpcd >/dev/null 2>&1 || dpkg-query -W dhcpcd5 >/dev/null 2>&1; then
apt-get install -y hostapd dhcpcd5 apt-get install -y dhcpcd5
echo "Using the installed dhcpcd/hostapd backend." log "Using dhcpcd with dedicated Pivilion hostapd"
else else
apt-get install -y network-manager apt-get install -y network-manager
systemctl enable NetworkManager systemctl enable NetworkManager
echo "No active networking backend was found; installed NetworkManager." log "Installed NetworkManager because no supported backend was active"
fi fi
} }
backup_managed_files() {
local path relative
install -d -m 0700 /var/backups/pivilion
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-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
for path in "${MANAGED_PATHS[@]}"; do
relative=${path#/}
if [ -e "$path" ] || [ -L "$path" ]; then
printf '%s\n' "$path" >> "$BACKUP_DIR/present"
install -d -m 0700 "$BACKUP_DIR/files/$(dirname "$relative")"
cp -a "$path" "$BACKUP_DIR/files/$relative"
else
printf '%s\n' "$path" >> "$BACKUP_DIR/missing"
fi
done
TRANSACTION_ACTIVE=1
log "Configuration backup created at $BACKUP_DIR"
}
rollback() {
local rc=$? path relative service
trap - ERR INT TERM
[ "$TRANSACTION_ACTIVE" -eq 1 ] || exit "$rc"
warn "Installation failed; restoring managed configuration from $BACKUP_DIR"
while IFS= read -r path; do [ -n "$path" ] && rm -rf -- "$path"; done < "$BACKUP_DIR/missing"
while IFS= read -r path; do
[ -n "$path" ] || continue
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-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-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"
apache2ctl configtest >/dev/null 2>&1 && systemctl reload apache2 2>/dev/null || true
warn "Rollback complete. Package installations were retained."
exit "$rc"
}
trap rollback ERR INT TERM
atomic_from_stdin() {
local destination=$1 mode=${2:-0644} tmp
install -d -m 0755 "$(dirname "$destination")"
tmp=$(mktemp "$(dirname "$destination")/.pivilion.XXXXXX")
cat > "$tmp"; chmod "$mode" "$tmp"; mv -f "$tmp" "$destination"
}
deploy_files() { deploy_files() {
local php_version local php_version
install -d -m 0755 /usr/local/lib/pivilion /usr/local/bin /etc/systemd/system /etc/tor/torrc.d log "Deploying Pivilion files"
install -d -m 0755 /usr/local/lib/pivilion /usr/local/bin /etc/systemd/system /etc/tor/torrc.d /etc/profile.d /var/lib/pivilion
install -m 0644 "$SOURCE_DIR/usr/local/lib/pivilion/common.sh" /usr/local/lib/pivilion/common.sh 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/lib/pivilion/mode.sh" "$SOURCE_DIR/usr/local/lib/pivilion/diagnose.sh" \
"$SOURCE_DIR/usr/local/lib/pivilion/issue-banner.sh" /usr/local/lib/pivilion/
install -m 0755 "$SOURCE_DIR"/usr/local/bin/* /usr/local/bin/ 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/*.service /etc/systemd/system/
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/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 atomic_from_stdin /etc/pivilion.defaults <<EOF
PIVILION_USER=$PIVILION_USER PIVILION_USER=$PIVILION_USER
PIVILION_HOME=$PIVILION_HOME PIVILION_HOME=$PIVILION_HOME
PIVILION_DIR=$PIVILION_DIR PIVILION_DIR=$PIVILION_DIR
@ -54,80 +187,150 @@ PIVILION_MODE=$PIVILION_MODE
PIVILION_HOTSPOT_SSID=$PIVILION_HOTSPOT_SSID PIVILION_HOTSPOT_SSID=$PIVILION_HOTSPOT_SSID
PIVILION_WIFI_INTERFACE=$PIVILION_WIFI_INTERFACE PIVILION_WIFI_INTERFACE=$PIVILION_WIFI_INTERFACE
EOF EOF
chmod 0644 /etc/pivilion.defaults
install -d -m 0755 -o "$PIVILION_USER" -g "$PIVILION_GROUP" "$PIVILION_DIR" "$PIVILION_DIR/config" install -d -m 0755 -o "$PIVILION_USER" -g "$PIVILION_GROUP" "$PIVILION_DIR" "$PIVILION_DIR/config"
cp -a "$SOURCE_DIR/home/pi/pivilion/config/." "$PIVILION_DIR/config/" cp -a "$SOURCE_DIR/home/pi/pivilion/config/." "$PIVILION_DIR/config/"
chown -R "$PIVILION_USER:$PIVILION_GROUP" "$PIVILION_DIR/config" chown -R "$PIVILION_USER:$PIVILION_GROUP" "$PIVILION_DIR/config"
install -d -m 0755 -o "$PIVILION_USER" -g "$PIVILION_GROUP" \ install -d -m 0755 -o "$PIVILION_USER" -g "$PIVILION_GROUP" "$PIVILION_DIR/static" "$PIVILION_DIR/hotglue" "$PIVILION_DIR/hotglue/backup"
"$PIVILION_DIR/static" "$PIVILION_DIR/hotglue" "$PIVILION_DIR/hotglue/backup"
[ -s "$PIVILION_DIR/mode" ] || pivilion_set_mode "$PIVILION_MODE" [ -s "$PIVILION_DIR/mode" ] || pivilion_set_mode "$PIVILION_MODE"
[ -e "$PIVILION_DIR/torname" ] || install -m 0644 -o "$PIVILION_USER" -g "$PIVILION_GROUP" /dev/null "$PIVILION_DIR/torname" [ -e "$PIVILION_DIR/torname" ] || install -m 0644 -o "$PIVILION_USER" -g "$PIVILION_GROUP" /dev/null "$PIVILION_DIR/torname"
install -d -m 2775 -o www-data -g www-data "$PIVILION_WEBROOT" "$PIVILION_WEBROOT/gen" install -d -m 2775 -o www-data -g www-data "$PIVILION_WEBROOT" "$PIVILION_WEBROOT/gen"
cp -a "$SOURCE_DIR/var/www/html/pivilion/scripts" "$PIVILION_WEBROOT/" cp -a "$SOURCE_DIR/var/www/html/pivilion/scripts" "$SOURCE_DIR/var/www/html/pivilion/images" "$SOURCE_DIR/var/www/html/pivilion/skeleton" "$PIVILION_WEBROOT/"
cp -a "$SOURCE_DIR/var/www/html/pivilion/images" "$PIVILION_WEBROOT/"
cp -a "$SOURCE_DIR/var/www/html/pivilion/skeleton" "$PIVILION_WEBROOT/"
install -m 0664 -o www-data -g www-data "$SOURCE_DIR/var/www/html/pivilion/gen.php" "$PIVILION_WEBROOT/gen.php" install -m 0664 -o www-data -g www-data "$SOURCE_DIR/var/www/html/pivilion/gen.php" "$PIVILION_WEBROOT/gen.php"
install -m 0644 -o www-data -g www-data "$SOURCE_DIR/var/www/html/pivilion/index.html" "$PIVILION_WEBROOT/index.html" install -m 0644 -o www-data -g www-data "$SOURCE_DIR/var/www/html/pivilion/index.html" "$SOURCE_DIR/var/www/html/pivilion/pi-logo_128.png" "$PIVILION_WEBROOT/"
install -m 0644 -o www-data -g www-data "$SOURCE_DIR/var/www/html/pivilion/pi-logo_128.png" "$PIVILION_WEBROOT/pi-logo_128.png" if [ ! -e "$PIVILION_WEBROOT/gen/index.php" ]; then cp -a "$SOURCE_DIR/var/www/html/pivilion/gen/." "$PIVILION_WEBROOT/gen/"; fi
if [ ! -e "$PIVILION_WEBROOT/gen/index.php" ]; then chown -R www-data:www-data "$PIVILION_WEBROOT/scripts" "$PIVILION_WEBROOT/images" "$PIVILION_WEBROOT/skeleton" "$PIVILION_WEBROOT/gen"
cp -a "$SOURCE_DIR/var/www/html/pivilion/gen/." "$PIVILION_WEBROOT/gen/"
fi
chown -R www-data:www-data "$PIVILION_WEBROOT/scripts" "$PIVILION_WEBROOT/images" "$PIVILION_WEBROOT/skeleton"
chown -R www-data:www-data "$PIVILION_WEBROOT/gen"
find "$PIVILION_WEBROOT" -type d -exec chmod 2775 {} +
find "$PIVILION_WEBROOT" -type f -exec chmod 0664 {} +
cat > /etc/pivilion.php <<EOF atomic_from_stdin /etc/pivilion.php <<EOF
<?php <?php
define('PIVILION_DIR', ${PIVILION_DIR@Q}); define('PIVILION_DIR', ${PIVILION_DIR@Q});
define('PIVILION_WEBROOT', ${PIVILION_WEBROOT@Q}); define('PIVILION_WEBROOT', ${PIVILION_WEBROOT@Q});
EOF EOF
chmod 0644 /etc/pivilion.php
php_version=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;') php_version=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')
install -d -m 0755 "/etc/php/$php_version/apache2/conf.d" install -d -m 0755 "/etc/php/$php_version/apache2/conf.d"
install -m 0644 "$SOURCE_DIR/etc/php/pivilion.ini" "/etc/php/$php_version/apache2/conf.d/99-pivilion.ini" install -m 0644 "$SOURCE_DIR/etc/php/pivilion.ini" "/etc/php/$php_version/apache2/conf.d/99-pivilion.ini"
} }
migrate_legacy_console_greeter() {
local tmp signatures=0
systemctl disable --now pivilion-boot-greeter.service 2>/dev/null || true
rm -f /etc/systemd/system/pivilion-boot-greeter.service /usr/local/lib/pivilion/boot-greeter.sh
[ -f /etc/rc.local ] || return 0
grep -q 'hostapd -B' /etc/rc.local && signatures=$((signatures + 1)) || true
grep -q 'ifconfig br0' /etc/rc.local && signatures=$((signatures + 1)) || true
grep -q 'iptables -t nat' /etc/rc.local && signatures=$((signatures + 1)) || true
grep -q 'service dnsmasq start' /etc/rc.local && signatures=$((signatures + 1)) || true
grep -q '/usr/local/lib/pivilion/mode.sh' /etc/rc.local && signatures=4 || true
if [ "$signatures" -ge 4 ]; then
cp -a /etc/rc.local "$BACKUP_DIR/legacy-rc.local"
atomic_from_stdin /etc/rc.local 0755 <<'EOF'
#!/bin/sh -e
# Pivilion startup is managed by pivilion-mode.service.
exit 0
EOF
log "Migrated legacy Pivilion rc.local; original saved in $BACKUP_DIR/legacy-rc.local"
elif grep -q '/var/lib/tor/hidden_service/hostname.*home/pi/torname' /etc/rc.local; then
cp -a /etc/rc.local "$BACKUP_DIR/legacy-rc.local"
if grep -Fq '_IP=$(hostname -I)' /etc/rc.local && grep -Fq 'My IP address is' /etc/rc.local; then
atomic_from_stdin /etc/rc.local 0755 <<'EOF'
#!/bin/sh -e
# Pivilion startup and the TTY banner are managed by systemd and agetty.
exit 0
EOF
else
tmp=$(mktemp)
grep -v '/var/lib/tor/hidden_service/hostname.*home/pi/torname' /etc/rc.local > "$tmp"
install -m 0755 "$tmp" /etc/rc.local
rm -f "$tmp"
fi
log "Migrated legacy Pivilion onion rc.local; original saved in $BACKUP_DIR/legacy-rc.local"
fi
}
configure_services() { configure_services() {
sed "s|/var/www/html/pivilion|$PIVILION_WEBROOT|g" "$SOURCE_DIR/etc/apache2/sites-available/000-default.conf" > /etc/apache2/sites-available/000-default.conf local tmp
sed "s|/var/www/html/pivilion|$PIVILION_WEBROOT|g" "$SOURCE_DIR/etc/apache2/sites-available/000-upload.conf" > /etc/apache2/sites-available/000-upload.conf log "Configuring services"
tmp=$(mktemp); sed "s|/var/www/html/pivilion|$PIVILION_WEBROOT|g" "$SOURCE_DIR/etc/apache2/sites-available/000-default.conf" > "$tmp"; install -m 0644 "$tmp" /etc/apache2/sites-available/000-default.conf; rm -f "$tmp"
tmp=$(mktemp); sed "s|/var/www/html/pivilion|$PIVILION_WEBROOT|g" "$SOURCE_DIR/etc/apache2/sites-available/000-upload.conf" > "$tmp"; install -m 0644 "$tmp" /etc/apache2/sites-available/000-upload.conf; rm -f "$tmp"
install -m 0644 "$SOURCE_DIR/etc/apache2/ports.conf" /etc/apache2/ports.conf install -m 0644 "$SOURCE_DIR/etc/apache2/ports.conf" /etc/apache2/ports.conf
cat > /etc/apache2/conf-available/pivilion.conf <<EOF atomic_from_stdin /etc/apache2/conf-available/pivilion.conf <<EOF
<Directory "$PIVILION_WEBROOT"> <Directory "$PIVILION_WEBROOT">
Options Indexes FollowSymLinks Options Indexes FollowSymLinks
AllowOverride All AllowOverride All
Require all granted Require all granted
</Directory> </Directory>
EOF EOF
a2enmod rewrite a2enmod rewrite; a2enconf pivilion; a2ensite 000-default.conf
a2enconf pivilion
a2ensite 000-default.conf
if ! grep -Fqx '%include /etc/tor/torrc.d/*.conf' /etc/tor/torrc; then if ! grep -Fqx '%include /etc/tor/torrc.d/*.conf' /etc/tor/torrc; then
printf '\n%%include /etc/tor/torrc.d/*.conf\n' >> /etc/tor/torrc tmp=$(mktemp); cp /etc/tor/torrc "$tmp"; printf '\n%%include /etc/tor/torrc.d/*.conf\n' >> "$tmp"; install -m 0644 "$tmp" /etc/tor/torrc; rm -f "$tmp"
fi fi
migrate_legacy_console_greeter
systemctl daemon-reload systemctl daemon-reload
systemctl enable apache2 pivilion-mode.service systemctl enable apache2 pivilion-mode.service pivilion-login-banner.service
systemctl enable tor@default.service 2>/dev/null || systemctl enable tor.service systemctl enable tor@default.service 2>/dev/null || systemctl enable tor.service
systemctl disable --now dnsmasq 2>/dev/null || true systemctl disable pivilion-dnsmasq.service pivilion-hostapd.service 2>/dev/null || true
systemctl disable pivilion-dnsmasq.service 2>/dev/null || true /usr/local/lib/pivilion/issue-banner.sh
command -v agetty >/dev/null 2>&1 && agetty --reload >/dev/null 2>&1 || true
} }
validate_install() { validate_install() {
apache2ctl configtest local failed=0
tor --verify-config -f /etc/tor/torrc log "Validating installation"
dnsmasq --test bash -n /usr/local/lib/pivilion/*.sh /usr/local/bin/pivilion /usr/local/bin/pivilion-diagnose /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 || failed=1
bash -n /usr/local/lib/pivilion/common.sh /usr/local/lib/pivilion/mode.sh /usr/local/bin/pivilion \ apache2ctl configtest || failed=1
/usr/local/bin/hotspot /usr/local/bin/onion /usr/local/bin/pikey /usr/local/bin/generator \ tor --verify-config -f /etc/tor/torrc || failed=1
/usr/local/bin/static /usr/local/bin/hotglue /usr/local/bin/htaccess dnsmasq --test || failed=1
php -l "$PIVILION_WEBROOT/gen.php" || failed=1
systemd-analyze verify /etc/systemd/system/pivilion-*.service || failed=1
grep -q '^My IP address is ' /etc/issue.d/90-pivilion.issue || failed=1
grep -q '^@@@@@@@@@@@@@@@@@@@' /etc/issue.d/90-pivilion.issue || failed=1
grep -q 'StandardOutput=null' /etc/systemd/system/pivilion-login-banner.service || failed=1
if command -v shellcheck >/dev/null 2>&1; then shellcheck /usr/local/lib/pivilion/*.sh /usr/local/bin/pivilion* /usr/local/bin/hotspot /usr/local/bin/onion /usr/local/bin/pikey || failed=1; fi
[ "$failed" -eq 0 ] || return "$PIVILION_E_INSTALL"
} }
write_install_state() {
atomic_from_stdin /var/lib/pivilion/install-state <<EOF
version=$INSTALL_VERSION
installed_at=$(date --iso-8601=seconds)
user=$PIVILION_USER
webroot=$PIVILION_WEBROOT
backup=$BACKUP_DIR
EOF
}
check_install() {
local failed=0 file
for file in /etc/pivilion.defaults /var/lib/pivilion/install-state /etc/issue.d/90-pivilion.issue \
/usr/local/lib/pivilion/common.sh /usr/local/lib/pivilion/mode.sh /usr/local/lib/pivilion/diagnose.sh \
/usr/local/lib/pivilion/issue-banner.sh /etc/systemd/system/pivilion-mode.service \
/etc/systemd/system/pivilion-login-banner.service; do
if [ ! -e "$file" ]; then echo "Missing: $file" >&2; failed=1; fi
done
if [ "$failed" -eq 0 ]; then validate_install || failed=1; fi
if [ "$failed" -eq 0 ]; then log "Installed files and service configuration are valid"; return 0; fi
echo "Installation check failed. Run sudo ./install.sh --repair" >&2
return "$PIVILION_E_INSTALL"
}
preflight
case "$ACTION" in
dry-run)
log "Dry run complete; installation would install packages, back up managed configuration, deploy files, validate, and enable services."
exit 0 ;;
check) check_install; exit $? ;;
esac
backup_managed_files
install_packages install_packages
deploy_files deploy_files
configure_services configure_services
validate_install validate_install
write_install_state
systemctl is-active --quiet apache2 2>/dev/null && systemctl reload apache2 || true
TRANSACTION_ACTIVE=0
trap - ERR INT TERM
echo "Pivilion installed for $PIVILION_USER. Reboot to enter $(pivilion_current_mode) mode." log "Pivilion $ACTION completed for $PIVILION_USER. Reboot to enter $(pivilion_current_mode) mode."

77
tests/run.sh Executable file
View file

@ -0,0 +1,77 @@
#!/bin/bash
set -euo pipefail
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
failures=0
run() {
local label=$1
shift
if "$@"; then printf '[pass] %s\n' "$label"; else printf '[fail] %s\n' "$label" >&2; failures=$((failures + 1)); fi
}
shell_syntax() {
bash -n "$ROOT/install.sh" "$ROOT"/usr/local/lib/pivilion/*.sh "$ROOT"/usr/local/bin/* "$ROOT/etc/profile.d/pivilion-motd.sh"
}
php_syntax() {
command -v php >/dev/null 2>&1 || return 0
while IFS= read -r file; do php -l "$file" >/dev/null || return 1; done < <(find "$ROOT/var" "$ROOT/home" -name '*.php' -type f)
}
systemd_syntax() {
local output
command -v systemd-analyze >/dev/null 2>&1 || return 0
output=$(systemd-analyze verify "$ROOT"/etc/systemd/system/*.service 2>&1) || true
output=$(printf '%s\n' "$output" | grep -Ev 'SO_PASS(RIGHTS|CRED)|handoff timestamp|user lookup socket' || true)
[ -z "$output" ]
}
captive_rules() {
grep -q 'R=302' "$ROOT/home/pi/pivilion/config/html/.htaccess" &&
! grep -q 'R=301' "$ROOT/home/pi/pivilion/config/html/.htaccess" &&
! grep -Eq 'dport[[:space:]]+443|redirect to :443' "$ROOT/usr/local/lib/pivilion/mode.sh"
}
installer_safety() {
grep -q -- '--dry-run' "$ROOT/install.sh" &&
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 -q 'rm -f.*CAPTIVE\|rm -f.*\.htaccess' "$ROOT/usr/local/bin/onion"
}
state_contract() {
grep -q "printf 'desired_mode" "$ROOT/usr/local/lib/pivilion/common.sh" &&
grep -q "printf 'active_mode" "$ROOT/usr/local/lib/pivilion/common.sh" &&
grep -q "printf 'health" "$ROOT/usr/local/lib/pivilion/common.sh" &&
grep -q 'degraded' "$ROOT/usr/local/lib/pivilion/mode.sh"
}
tty_banner_contract() {
grep -q 'StandardOutput=null' "$ROOT/etc/systemd/system/pivilion-login-banner.service" &&
! grep -q 'journal+console' "$ROOT/etc/systemd/system/pivilion-login-banner.service" &&
grep -q '/etc/issue.d/90-pivilion.issue' "$ROOT/usr/local/lib/pivilion/issue-banner.sh" &&
grep -q 'SSH_CONNECTION' "$ROOT/etc/profile.d/pivilion-motd.sh" &&
grep -q 'agetty --reload' "$ROOT/usr/local/lib/pivilion/mode.sh" &&
! grep -Eq '(^|[[:space:]])/etc/issue([[:space:]]|$)' "$ROOT/install.sh"
}
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"
}
run "configuration and state fixtures" "$ROOT/tests/test-common.sh"
run "shell syntax" shell_syntax
run "PHP syntax" php_syntax
run "systemd unit syntax" systemd_syntax
run "temporary HTTP-only captive rules" captive_rules
run "transactional installer safety contract" installer_safety
run "truthful runtime state contract" state_contract
run "agetty login-banner contract" tty_banner_contract
run "ShellCheck (when installed)" shellcheck_scripts
[ "$failures" -eq 0 ] || { echo "$failures test group(s) failed." >&2; exit 1; }
echo "All available tests passed."

68
tests/test-common.sh Executable file
View file

@ -0,0 +1,68 @@
#!/bin/bash
set -euo pipefail
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
USER_NAME=$(id -un)
cat > "$TMP/primary.conf" <<EOF
PIVILION_USER=$USER_NAME
PIVILION_MODE=hotspot
EOF
cat > "$TMP/fallback.conf" <<EOF
PIVILION_USER=$USER_NAME
PIVILION_HOME=$HOME
PIVILION_DIR=$TMP/pivilion
PIVILION_WEBROOT=/var/www/html/pivilion
PIVILION_MODE=onion
PIVILION_HOTSPOT_SSID=fixture-portal
PIVILION_WIFI_INTERFACE=wlan-test
EOF
export PIVILION_CONFIG_PATHS="$TMP/primary.conf $TMP/fallback.conf"
export PIVILION_STATE_DIR="$TMP/run"
export PIVILION_STATE_FILE="$TMP/run/status.tsv"
source "$ROOT/usr/local/lib/pivilion/common.sh"
pivilion_load_config
[ "$PIVILION_MODE" = hotspot ]
[ "$PIVILION_HOTSPOT_SSID" = fixture-portal ]
[ "$PIVILION_DIR" = "$TMP/pivilion" ]
install -d "$PIVILION_DIR" "$PIVILION_STATE_DIR"
printf 'onion\n' > "$PIVILION_DIR/mode"
[ "$(pivilion_current_mode)" = onion ]
cat > "$PIVILION_STATE_FILE" <<EOF
desired_mode onion
active_mode hotspot
backend networkmanager
health degraded
failure probe failed
EOF
[ "$(pivilion_state_value active_mode)" = hotspot ]
[ "$(pivilion_state_value health)" = degraded ]
[ "$(pivilion_state_value missing fallback)" = fallback ]
pivilion_print_issue > "$TMP/pivilion.issue"
grep -Fqx 'My IP address is \4{wlan-test}' "$TMP/pivilion.issue"
grep -q '^Welcome to Pivilion!$' "$TMP/pivilion.issue"
grep -q '^@@@@@@@@@@@@@@@@@@@' "$TMP/pivilion.issue"
mkdir "$TMP/bin"
cat > "$TMP/bin/nmcli" <<'EOF'
#!/bin/sh
exit 0
EOF
cat > "$TMP/bin/systemctl" <<'EOF'
#!/bin/sh
[ "${MOCK_NM_ACTIVE:-0}" = 1 ]
EOF
chmod +x "$TMP/bin/nmcli" "$TMP/bin/systemctl"
PATH="$TMP/bin:$PATH" MOCK_NM_ACTIVE=1 pivilion_uses_networkmanager
if PATH="$TMP/bin:$PATH" MOCK_NM_ACTIVE=0 pivilion_uses_networkmanager; then exit 1; fi
sed 's/PIVILION_MODE=hotspot/PIVILION_MODE=invalid/' "$TMP/primary.conf" > "$TMP/invalid.conf"
PIVILION_CONFIG_PATHS="$TMP/invalid.conf $TMP/fallback.conf"
export PIVILION_CONFIG_PATHS
if pivilion_load_config >/dev/null 2>&1; then exit 1; fi

View file

@ -4,7 +4,6 @@ source /usr/local/lib/pivilion/common.sh
[ "$(id -u)" -eq 0 ] || exec sudo "$0" "$@" [ "$(id -u)" -eq 0 ] || exec sudo "$0" "$@"
pivilion_load_config pivilion_load_config
pivilion_set_mode onion pivilion_set_mode onion
rm -f "$PIVILION_WEBROOT/gen/.htaccess"
echo "Pivilion will start in onion mode after the next reboot." echo "Pivilion will start in onion mode after the next reboot."
if [ -s "$PIVILION_DIR/torname" ]; then if [ -s "$PIVILION_DIR/torname" ]; then
printf 'Your onion domain is %s\n' "$(tr -d '\r\n' < "$PIVILION_DIR/torname")" printf 'Your onion domain is %s\n' "$(tr -d '\r\n' < "$PIVILION_DIR/torname")"

View file

@ -3,21 +3,22 @@ set -euo pipefail
source /usr/local/lib/pivilion/common.sh source /usr/local/lib/pivilion/common.sh
pivilion_load_config pivilion_load_config
mode=$(pivilion_current_mode) case "${1:-status}" in
torname="not generated yet" status) [ "$#" -le 1 ] || { echo "Usage: pivilion [status|diagnose [--json]]" >&2; exit 2; } ;;
[ ! -s "$PIVILION_DIR/torname" ] || torname=$(tr -d '\r\n' < "$PIVILION_DIR/torname") diagnose) shift; exec /usr/local/lib/pivilion/diagnose.sh "$@" ;;
uptime_text=$(uptime -p 2>/dev/null || true) *) echo "Usage: pivilion [status|diagnose [--json]]" >&2; exit 2 ;;
esac
pivilion_print_motd
failure=$(pivilion_state_value failure none)
cat <<EOF cat <<EOF
Welcome to Pivilion! Web root: $PIVILION_WEBROOT
Apache: $(pivilion_service_state apache2)
Host...............: $(uname -n) Tor: $(if systemctl cat tor@default.service >/dev/null 2>&1; then pivilion_service_state tor@default.service; else pivilion_service_state tor.service; fi)
System.............: $(uname -srmo) Last activation failure: $failure
Uptime.............: ${uptime_text:-unknown}
Mode after reboot..: $mode
Onion address......: $torname
Web root...........: $PIVILION_WEBROOT
Commands: Commands:
pivilion show this status pivilion show this status
@ -28,4 +29,6 @@ Commands:
static convert the current gallery to static files static convert the current gallery to static files
hotglue install or restore Hotglue hotglue install or restore Hotglue
htaccess reset or remove captive redirect rules htaccess reset or remove captive redirect rules
pivilion diagnose [--json] check networking, redirects, and services
pivilion-diagnose compatibility alias for diagnostics
EOF EOF

View file

@ -0,0 +1,3 @@
#!/bin/bash
set -euo pipefail
exec /usr/local/lib/pivilion/diagnose.sh "$@"

View file

@ -3,6 +3,18 @@
# Shared configuration and small helpers for the Pivilion commands. # Shared configuration and small helpers for the Pivilion commands.
PIVILION_CONFIG="" PIVILION_CONFIG=""
PIVILION_STATE_DIR=${PIVILION_STATE_DIR:-/run/pivilion}
PIVILION_STATE_FILE=${PIVILION_STATE_FILE:-$PIVILION_STATE_DIR/status.tsv}
# Stable command exit codes. Keep these public so scripts and monitoring can
# distinguish configuration, hardware, service, captive portal, and Tor faults.
PIVILION_E_CONFIG=10
PIVILION_E_HARDWARE=11
PIVILION_E_CONFLICT=12
PIVILION_E_SERVICE=13
PIVILION_E_CAPTIVE=14
PIVILION_E_TOR=15
PIVILION_E_INSTALL=20
_pivilion_trim() { _pivilion_trim() {
local value=$1 local value=$1
@ -12,8 +24,9 @@ _pivilion_trim() {
} }
_pivilion_config_value() { _pivilion_config_value() {
local key=$1 file line name value local key=$1 file line name value config_paths
for file in /boot/firmware/pivilion.conf /boot/pivilion.conf /etc/pivilion.conf /etc/pivilion.defaults; do config_paths=${PIVILION_CONFIG_PATHS:-/boot/firmware/pivilion.conf /boot/pivilion.conf /etc/pivilion.conf /etc/pivilion.defaults}
for file in $config_paths; do
[ -r "$file" ] || continue [ -r "$file" ] || continue
while IFS= read -r line || [ -n "$line" ]; do while IFS= read -r line || [ -n "$line" ]; do
line=$(_pivilion_trim "$line") line=$(_pivilion_trim "$line")
@ -111,3 +124,87 @@ pivilion_current_mode() {
pivilion_require_root() { pivilion_require_root() {
[ "$(id -u)" -eq 0 ] || { echo "This operation must run as root (use sudo)." >&2; return 1; } [ "$(id -u)" -eq 0 ] || { echo "This operation must run as root (use sudo)." >&2; return 1; }
} }
pivilion_state_value() {
local key=$1 fallback=${2:-unknown} value
value=$(awk -F '\t' -v key="$key" '$1 == key {sub(/^[^\t]*\t/, ""); print; exit}' "$PIVILION_STATE_FILE" 2>/dev/null || true)
printf '%s\n' "${value:-$fallback}"
}
pivilion_write_state() {
local active=$1 backend=$2 health=$3 failure=${4:-} tmp desired addresses
pivilion_require_root
install -d -m 0755 "$PIVILION_STATE_DIR"
desired=$(pivilion_current_mode)
addresses=$(hostname -I 2>/dev/null | xargs || true)
failure=${failure//$'\n'/ }
failure=${failure//$'\t'/ }
tmp=$(mktemp "$PIVILION_STATE_DIR/status.XXXXXX")
{
printf 'desired_mode\t%s\n' "$desired"
printf 'active_mode\t%s\n' "$active"
printf 'backend\t%s\n' "$backend"
printf 'health\t%s\n' "$health"
printf 'activated_at\t%s\n' "$(date --iso-8601=seconds)"
printf 'addresses\t%s\n' "${addresses:-none}"
printf 'failure\t%s\n' "${failure:-none}"
} > "$tmp"
chmod 0644 "$tmp"
mv -f "$tmp" "$PIVILION_STATE_FILE"
}
pivilion_service_state() {
local state
state=$(systemctl is-active "$1" 2>/dev/null || true)
printf '%s\n' "${state:-unknown}"
}
pivilion_print_motd() {
local heading=${1:-Welcome to Pivilion!}
local show_addresses=${2:-yes}
local uptime_text memory_free memory_total load_one load_five load_fifteen processes torname desired active backend health addresses
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)
if ! read -r load_one load_five load_fifteen _ < /proc/loadavg 2>/dev/null; then
load_one=unknown; load_five=unknown; load_fifteen=unknown
fi
processes=$({ ps ax --no-headers 2>/dev/null || true; } | wc -l)
[ "$processes" -gt 0 ] 2>/dev/null || processes=unknown
torname=not-generated-yet
[ ! -s "$PIVILION_DIR/torname" ] || torname=$(tr -d '\r\n' < "$PIVILION_DIR/torname")
desired=$(pivilion_current_mode)
active=$(pivilion_state_value active_mode not-verified)
backend=$(pivilion_state_value backend unknown)
health=$(pivilion_state_value health unknown)
addresses=$(pivilion_state_value addresses none)
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
@@@@@#@@@@@@@#@@@@@ Desired / active....: $desired / $active
Backend / health...: $backend / $health
EOF
if [ "$show_addresses" = yes ]; then
printf ' IP addresses.......: %s\n' "$addresses"
fi
}
pivilion_print_issue() {
local iface
iface=$(pivilion_wifi_interface)
if [ -n "$iface" ]; then
printf 'My IP address is \\4{%s}\n' "$iface"
else
printf 'My IP address is unknown\n'
fi
pivilion_print_motd "Welcome to Pivilion!" no
}

View file

@ -0,0 +1,116 @@
#!/bin/bash
set -uo pipefail
source /usr/local/lib/pivilion/common.sh
[ "$(id -u)" -eq 0 ] || exec sudo /usr/local/lib/pivilion/diagnose.sh "$@"
OUTPUT=text
QUIET=0
MODE=""
while [ "$#" -gt 0 ]; do
case "$1" in
--json) OUTPUT=json ;;
--quiet) QUIET=1 ;;
--mode) shift; MODE=${1:-} ;;
*) echo "Usage: pivilion diagnose [--json] [--mode hotspot|onion]" >&2; exit 2 ;;
esac
shift
done
pivilion_load_config || exit "$PIVILION_E_CONFIG"
DESIRED=$(pivilion_current_mode)
ACTIVE=$(pivilion_state_value active_mode not-verified)
BACKEND=$(pivilion_state_value backend unknown)
RECORDED_HEALTH=$(pivilion_state_value health unknown)
ADDRESSES=$(pivilion_state_value addresses none)
FAILURE_REASON=$(pivilion_state_value failure none)
MODE=${MODE:-$ACTIVE}
case "$MODE" in not-verified|none|unknown) MODE=$DESIRED ;; esac
case "$MODE" in hotspot|onion) ;; *) MODE=$(pivilion_current_mode) ;; esac
declare -a CHECK_NAMES=() CHECK_VALUES=() CHECK_DETAILS=()
FAILURES=0
SERVICE_FAILURE=0
MODE_FAILURE=0
HARDWARE_FAILURE=0
record_check() {
local name=$1 value=$2 detail=${3:-}
CHECK_NAMES+=("$name")
CHECK_VALUES+=("$value")
CHECK_DETAILS+=("$detail")
if [ "$value" != pass ]; then FAILURES=$((FAILURES + 1)); fi
}
service_active() { systemctl is-active --quiet "$1"; }
apache_rewrite() { apache2ctl -M 2>/dev/null | grep -q rewrite_module; }
live_redirect() { cmp -s "$PIVILION_DIR/config/html/.htaccess" "$PIVILION_WEBROOT/gen/.htaccess"; }
dns_answer() { [ "$(dig +short A @10.1.1.1 "$1" 2>/dev/null | tail -n1)" = 10.1.1.1 ]; }
nft_active() { nft list table inet pivilion >/dev/null 2>&1; }
probe_redirect() {
local host=$1 path=$2 headers
headers=$(curl -sS --max-time 5 -D - -o /dev/null -H "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"
}
gallery_works() { curl -fsS --max-time 5 -o /dev/null http://10.1.1.1/index.php; }
default_route() { ip route show default | grep -q '^default'; }
valid_torname() {
local hostname
hostname=$(tr -d '\r\n' < "$PIVILION_DIR/torname" 2>/dev/null || true)
[[ "$hostname" =~ ^[a-z2-7]{56}\.onion$ ]]
}
tor_active() {
if systemctl cat tor@default.service >/dev/null 2>&1; then service_active tor@default.service; else service_active tor.service; fi
}
IFACE=$(pivilion_wifi_interface)
if [ -n "$IFACE" ] && [ -d "/sys/class/net/$IFACE" ]; then record_check wifi_interface pass "$IFACE"; else record_check wifi_interface fail missing; HARDWARE_FAILURE=1; fi
if service_active apache2; then record_check apache pass active; else record_check apache fail inactive; SERVICE_FAILURE=1; fi
if apache_rewrite; then record_check apache_rewrite pass enabled; else record_check apache_rewrite fail disabled; SERVICE_FAILURE=1; fi
if [ "$MODE" = hotspot ]; then
if live_redirect; then record_check captive_redirect pass installed; else record_check captive_redirect fail missing_or_stale; MODE_FAILURE=1; fi
if service_active pivilion-dnsmasq.service; then record_check dnsmasq pass active; else record_check dnsmasq fail inactive; SERVICE_FAILURE=1; fi
if nft_active; then record_check nftables pass active; else record_check nftables fail missing; MODE_FAILURE=1; fi
if dns_answer connectivitycheck.gstatic.com; then record_check captive_dns pass 10.1.1.1; else record_check captive_dns fail wrong_answer; MODE_FAILURE=1; fi
if probe_redirect connectivitycheck.gstatic.com /generate_204; then record_check android_probe pass 302; else record_check android_probe fail no_redirect; MODE_FAILURE=1; fi
if probe_redirect captive.apple.com /hotspot-detect.html; then record_check apple_probe pass 302; else record_check apple_probe fail no_redirect; MODE_FAILURE=1; fi
if probe_redirect www.msftconnecttest.com /connecttest.txt; then record_check microsoft_probe pass 302; else record_check microsoft_probe fail no_redirect; MODE_FAILURE=1; fi
if gallery_works; then record_check gallery pass reachable; else record_check gallery fail unreachable; MODE_FAILURE=1; fi
else
if default_route; then record_check upstream_route pass present; else record_check upstream_route fail missing; MODE_FAILURE=1; fi
if tor_active; then record_check tor pass active; else record_check tor fail inactive; MODE_FAILURE=1; fi
if valid_torname; then record_check onion_hostname pass valid_v3; else record_check onion_hostname fail missing_or_invalid; MODE_FAILURE=1; fi
fi
json_escape() {
local value=$1
value=${value//\\/\\\\}; value=${value//\"/\\\"}; value=${value//$'\n'/\\n}; value=${value//$'\r'/\\r}; value=${value//$'\t'/\\t}
printf '%s' "$value"
}
if [ "$OUTPUT" = json ]; then
printf '{"desired_mode":"%s","active_mode":"%s","diagnosed_mode":"%s","backend":"%s","recorded_health":"%s","addresses":"%s","failure":"%s","healthy":%s,"checks":{' \
"$(json_escape "$DESIRED")" "$(json_escape "$ACTIVE")" "$(json_escape "$MODE")" "$(json_escape "$BACKEND")" \
"$(json_escape "$RECORDED_HEALTH")" "$(json_escape "$ADDRESSES")" "$(json_escape "$FAILURE_REASON")" \
"$([ "$FAILURES" -eq 0 ] && printf true || printf false)"
for ((i=0; i<${#CHECK_NAMES[@]}; i++)); do
[ "$i" -eq 0 ] || printf ','
printf '"%s":{"status":"%s","detail":"%s"}' "$(json_escape "${CHECK_NAMES[$i]}")" "${CHECK_VALUES[$i]}" "$(json_escape "${CHECK_DETAILS[$i]}")"
done
printf '}}\n'
elif [ "$QUIET" -eq 0 ]; then
printf 'Pivilion diagnostics (desired=%s, active=%s, checking=%s, backend=%s, recorded-health=%s):\n' \
"$DESIRED" "$ACTIVE" "$MODE" "$BACKEND" "$RECORDED_HEALTH"
for ((i=0; i<${#CHECK_NAMES[@]}; i++)); do
printf ' [%s] %-20s %s\n' "${CHECK_VALUES[$i]}" "${CHECK_NAMES[$i]}" "${CHECK_DETAILS[$i]}"
done
fi
[ "$FAILURES" -eq 0 ] && exit 0
[ "$HARDWARE_FAILURE" -eq 0 ] || exit "$PIVILION_E_HARDWARE"
[ "$SERVICE_FAILURE" -eq 0 ] || exit "$PIVILION_E_SERVICE"
[ "$MODE" != hotspot ] || exit "$PIVILION_E_CAPTIVE"
exit "$PIVILION_E_TOR"

View file

@ -0,0 +1,16 @@
#!/bin/bash
set -euo pipefail
source /usr/local/lib/pivilion/common.sh
pivilion_require_root
pivilion_load_config
ISSUE_FILE=/etc/issue.d/90-pivilion.issue
ISSUE_DIR=$(dirname "$ISSUE_FILE")
install -d -m 0755 "$ISSUE_DIR"
tmp=$(mktemp "$ISSUE_DIR/.90-pivilion.XXXXXX")
trap 'rm -f "$tmp"' EXIT
pivilion_print_issue > "$tmp"
chmod 0644 "$tmp"
mv -f "$tmp" "$ISSUE_FILE"
trap - EXIT

View file

@ -1,33 +1,105 @@
#!/bin/bash #!/bin/bash
set -euo pipefail set -Eeuo pipefail
source /usr/local/lib/pivilion/common.sh source /usr/local/lib/pivilion/common.sh
pivilion_require_root pivilion_require_root
pivilion_load_config pivilion_load_config || exit "$PIVILION_E_CONFIG"
MODE=${1:-$(pivilion_current_mode)} MODE=${1:-$(pivilion_current_mode)}
case "$MODE" in hotspot|onion) ;; *) echo "Unknown Pivilion mode: $MODE" >&2; exit 2 ;; esac
IFACE=$(pivilion_wifi_interface) IFACE=$(pivilion_wifi_interface)
[ -n "$IFACE" ] || { echo "No Wi-Fi interface found; set PIVILION_WIFI_INTERFACE" >&2; exit 1; } [ -n "$IFACE" ] && [ -d "/sys/class/net/$IFACE" ] || {
pivilion_write_state none none failed "Wi-Fi interface not found"
echo "No Wi-Fi interface found; set PIVILION_WIFI_INTERFACE" >&2
exit "$PIVILION_E_HARDWARE"
}
RUN_DIR=/run/pivilion RUN_DIR=/run/pivilion
DNSMASQ_CONF=/etc/dnsmasq.d/pivilion-runtime.conf DNSMASQ_CONF=/etc/dnsmasq.d/pivilion-runtime.conf
HOSTAPD_CONF=$RUN_DIR/hostapd.conf
NFT_CONF=$RUN_DIR/pivilion.nft
NM_HOTSPOT=pivilion-hotspot NM_HOTSPOT=pivilion-hotspot
CAPTIVE_TEMPLATE=$PIVILION_DIR/config/html/.htaccess
CAPTIVE_LIVE=$PIVILION_WEBROOT/gen/.htaccess
BACKEND=legacy
pivilion_uses_networkmanager && BACKEND=networkmanager
stop_hotspot() { refresh_login_banner() {
systemctl stop pivilion-dnsmasq.service 2>/dev/null || true if [ -x /usr/local/lib/pivilion/issue-banner.sh ]; then
rm -f "$DNSMASQ_CONF" /usr/local/lib/pivilion/issue-banner.sh || logger -t pivilion-mode "Could not refresh TTY login banner"
fi
command -v agetty >/dev/null 2>&1 && agetty --reload >/dev/null 2>&1 || true
}
activation_error() {
local rc=$? line=${BASH_LINENO[0]:-unknown}
trap - ERR
pivilion_write_state none "$BACKEND" failed "Activation failed at line $line (exit $rc)" || true
refresh_login_banner || true
logger -t pivilion-mode "Failed to activate $MODE mode at line $line (exit $rc)" 2>/dev/null || true
exit "$rc"
}
trap activation_error ERR
atomic_install() {
local source=$1 destination=$2 owner=${3:-root} group=${4:-root} mode=${5:-0644} tmp
install -d -m 0755 "$(dirname "$destination")"
tmp=$(mktemp "$(dirname "$destination")/.pivilion.XXXXXX")
if ! install -m "$mode" -o "$owner" -g "$group" "$source" "$tmp"; then rm -f "$tmp"; return 1; fi
mv -f "$tmp" "$destination"
}
install_captive_redirect() {
[ -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; }
atomic_install "$CAPTIVE_TEMPLATE" "$CAPTIVE_LIVE" root www-data 0644
}
validate_ap_support() {
local phy
command -v iw >/dev/null 2>&1 || return 1
phy=$(iw dev "$IFACE" info 2>/dev/null | awk '$1 == "wiphy" {print "phy" $2; exit}')
[ -n "$phy" ] && iw phy "$phy" info 2>/dev/null | grep -Eq '^[[:space:]]*\* AP$'
}
cleanup_hotspot() {
systemctl stop 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 nft delete table inet pivilion 2>/dev/null || true
if pivilion_uses_networkmanager; then if command -v nmcli >/dev/null 2>&1; then
nmcli connection down "$NM_HOTSPOT" 2>/dev/null || true nmcli connection down "$NM_HOTSPOT" >/dev/null 2>&1 || true
nmcli connection delete "$NM_HOTSPOT" 2>/dev/null || true nmcli connection delete "$NM_HOTSPOT" >/dev/null 2>&1 || true
else fi
systemctl stop hostapd 2>/dev/null || true if ip -4 address show dev "$IFACE" | grep -q '10\.1\.1\.1/24'; then
ip address flush dev "$IFACE" 2>/dev/null || true ip address del 10.1.1.1/24 dev "$IFACE" 2>/dev/null || true
fi
if systemctl is-active --quiet hostapd.service 2>/dev/null && grep -Fqx "ssid=$PIVILION_HOTSPOT_SSID" /etc/hostapd/hostapd.conf 2>/dev/null; then
systemctl stop hostapd.service
fi fi
} }
write_dnsmasq_config() { check_dns_conflict() {
install -d -m 0755 /etc/dnsmasq.d local listeners
cat > "$DNSMASQ_CONF" <<EOF listeners=$(ss -H -lntu 'sport = :53' 2>/dev/null || true)
[ -z "$listeners" ] && return 0
if grep -Eq '(0\.0\.0\.0|\*|10\.1\.1\.1):53([[:space:]]|$)' <<<"$listeners"; then
echo "Port 53 is already occupied by a non-Pivilion service." >&2
return 1
fi
}
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
return 1
fi
}
write_hotspot_configs() {
local dns_tmp hostapd_tmp nft_tmp
install -d -m 0755 "$RUN_DIR" /etc/dnsmasq.d
dns_tmp=$(mktemp "$RUN_DIR/dnsmasq.XXXXXX")
cat > "$dns_tmp" <<EOF
interface=$IFACE interface=$IFACE
bind-dynamic bind-dynamic
listen-address=10.1.1.1 listen-address=10.1.1.1
@ -36,11 +108,25 @@ dhcp-option=option:router,10.1.1.1
dhcp-authoritative dhcp-authoritative
address=/#/10.1.1.1 address=/#/10.1.1.1
EOF EOF
} dnsmasq --test --conf-file="$dns_tmp"
atomic_install "$dns_tmp" "$DNSMASQ_CONF"
rm -f "$dns_tmp"
start_firewall() { hostapd_tmp=$(mktemp "$RUN_DIR/hostapd.XXXXXX")
install -d -m 0755 "$RUN_DIR" cat > "$hostapd_tmp" <<EOF
cat > "$RUN_DIR/pivilion.nft" <<EOF interface=$IFACE
driver=nl80211
ssid=$PIVILION_HOTSPOT_SSID
hw_mode=g
channel=6
auth_algs=1
wmm_enabled=0
EOF
atomic_install "$hostapd_tmp" "$HOSTAPD_CONF"
rm -f "$hostapd_tmp"
nft_tmp=$(mktemp "$RUN_DIR/nft.XXXXXX")
cat > "$nft_tmp" <<EOF
table inet pivilion { table inet pivilion {
chain prerouting { chain prerouting {
type nat hook prerouting priority dstnat; policy accept; type nat hook prerouting priority dstnat; policy accept;
@ -50,44 +136,24 @@ table inet pivilion {
} }
} }
EOF EOF
nft delete table inet pivilion 2>/dev/null || true nft -c -f "$nft_tmp"
nft -f "$RUN_DIR/pivilion.nft" atomic_install "$nft_tmp" "$NFT_CONF"
rm -f "$nft_tmp"
} }
start_hotspot() { apply_firewall() {
stop_hotspot nft delete table inet pivilion 2>/dev/null || true
rfkill unblock wifi 2>/dev/null || true nft -f "$NFT_CONF"
systemctl start apache2 }
if pivilion_uses_networkmanager; then
nmcli connection add type wifi ifname "$IFACE" con-name "$NM_HOTSPOT" ssid "$PIVILION_HOTSPOT_SSID" wait_for_default_route() {
nmcli connection modify "$NM_HOTSPOT" 802-11-wireless.mode ap 802-11-wireless.band bg \ local i
ipv4.method manual ipv4.addresses 10.1.1.1/24 ipv4.never-default yes ipv6.method disabled for i in $(seq 1 30); do ip route show default | grep -q '^default' && return 0; sleep 1; done
nmcli connection up "$NM_HOTSPOT" return 1
else }
systemctl stop "wpa_supplicant@$IFACE.service" 2>/dev/null || true
command -v dhcpcd >/dev/null 2>&1 && dhcpcd -k "$IFACE" 2>/dev/null || true restart_tor() {
ip link set "$IFACE" down if systemctl cat tor@default.service >/dev/null 2>&1; then systemctl restart tor@default.service; else systemctl restart tor.service; fi
ip address flush dev "$IFACE"
ip address add 10.1.1.1/24 dev "$IFACE"
ip link set "$IFACE" up
install -d -m 0755 /etc/hostapd
cat > /etc/hostapd/hostapd.conf <<EOF
interface=$IFACE
driver=nl80211
ssid=$PIVILION_HOTSPOT_SSID
hw_mode=g
channel=6
auth_algs=1
wmm_enabled=0
EOF
systemctl unmask hostapd 2>/dev/null || true
systemctl restart hostapd
fi
write_dnsmasq_config
dnsmasq --test --conf-file="$DNSMASQ_CONF"
systemctl restart pivilion-dnsmasq.service
start_firewall
echo "Pivilion hotspot '$PIVILION_HOTSPOT_SSID' is active on $IFACE (10.1.1.1)."
} }
update_torname() { update_torname() {
@ -95,44 +161,86 @@ update_torname() {
for i in $(seq 1 30); do for i in $(seq 1 30); do
if [ -s "$source" ]; then if [ -s "$source" ]; then
hostname=$(tr -d '\r\n' < "$source") hostname=$(tr -d '\r\n' < "$source")
if [[ ! "$hostname" =~ ^[a-z2-7]{56}\.onion$ ]]; then [[ "$hostname" =~ ^[a-z2-7]{56}\.onion$ ]] || return 1
echo "Tor produced an invalid v3 onion hostname." >&2
return 1
fi
tmp=$(mktemp "$PIVILION_DIR/.torname.XXXXXX") tmp=$(mktemp "$PIVILION_DIR/.torname.XXXXXX")
printf '%s\n' "$hostname" > "$tmp" printf '%s\n' "$hostname" > "$tmp"
chown "$PIVILION_USER:$PIVILION_GROUP" "$tmp" chown "$PIVILION_USER:$PIVILION_GROUP" "$tmp"; chmod 0644 "$tmp"; mv -f "$tmp" "$PIVILION_DIR/torname"
chmod 0644 "$tmp"
mv -f "$tmp" "$PIVILION_DIR/torname"
return 0 return 0
fi fi
sleep 1 sleep 1
done done
echo "Tor started, but no onion hostname was generated within 30 seconds." >&2
return 1 return 1
} }
start_hotspot() {
pivilion_write_state none "$BACKEND" activating none
validate_ap_support || {
pivilion_write_state none "$BACKEND" failed "$IFACE does not advertise Wi-Fi AP support"
refresh_login_banner
echo "$IFACE does not advertise Wi-Fi AP support." >&2
exit "$PIVILION_E_HARDWARE"
}
cleanup_hotspot
if ! check_hostapd_conflict; then
pivilion_write_state none "$BACKEND" failed "System hostapd conflict"
refresh_login_banner
exit "$PIVILION_E_CONFLICT"
fi
if ! check_dns_conflict; then
pivilion_write_state none "$BACKEND" failed "Port 53 conflict"
refresh_login_banner
exit "$PIVILION_E_CONFLICT"
fi
install_captive_redirect
write_hotspot_configs
rfkill unblock wifi 2>/dev/null || true
systemctl start apache2
if [ "$BACKEND" = networkmanager ]; then
nmcli connection add type wifi ifname "$IFACE" con-name "$NM_HOTSPOT" ssid "$PIVILION_HOTSPOT_SSID"
nmcli connection modify "$NM_HOTSPOT" 802-11-wireless.mode ap 802-11-wireless.band bg \
ipv4.method manual ipv4.addresses 10.1.1.1/24 ipv4.never-default yes ipv6.method disabled connection.autoconnect no
nmcli connection up "$NM_HOTSPOT"
else
command -v dhcpcd >/dev/null 2>&1 && dhcpcd -k "$IFACE" 2>/dev/null || true
systemctl stop "wpa_supplicant@$IFACE.service" 2>/dev/null || true
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
apply_firewall
if /usr/local/lib/pivilion/diagnose.sh --mode hotspot --quiet; then
pivilion_write_state hotspot "$BACKEND" healthy none
else
pivilion_write_state hotspot "$BACKEND" degraded "Post-start captive portal diagnostics failed"
logger -t pivilion-mode "Hotspot active but degraded; run pivilion diagnose" || true
fi
refresh_login_banner
echo "Pivilion hotspot '$PIVILION_HOTSPOT_SSID' is active on $IFACE."
}
start_onion() { start_onion() {
stop_hotspot local failure=""
if pivilion_uses_networkmanager; then pivilion_write_state none "$BACKEND" activating none
nmcli connection up pivilion-onion 2>/dev/null || echo "No saved Pivilion Wi-Fi connection; run pikey." >&2 cleanup_hotspot
rm -f "$CAPTIVE_LIVE"
if [ "$BACKEND" = networkmanager ]; then
nmcli connection up pivilion-onion >/dev/null 2>&1 || failure="No saved Pivilion Wi-Fi connection; run pikey"
else else
systemctl restart dhcpcd 2>/dev/null || true systemctl restart dhcpcd 2>/dev/null || true
systemctl restart "wpa_supplicant@$IFACE.service" 2>/dev/null || systemctl restart wpa_supplicant 2>/dev/null || true systemctl restart "wpa_supplicant@$IFACE.service" 2>/dev/null || systemctl restart wpa_supplicant 2>/dev/null || true
fi fi
rm -f "$PIVILION_WEBROOT/gen/.htaccess" wait_for_default_route || failure="${failure:+$failure; }No upstream default route after 30 seconds"
systemctl start apache2 systemctl start apache2
if systemctl cat tor@default.service >/dev/null 2>&1; then restart_tor
systemctl restart tor@default.service update_torname || failure="${failure:+$failure; }Tor did not produce a valid hostname; retained previous value"
if [ -z "$failure" ] && /usr/local/lib/pivilion/diagnose.sh --mode onion --quiet; then
pivilion_write_state onion "$BACKEND" healthy none
else else
systemctl restart tor.service pivilion_write_state onion "$BACKEND" degraded "${failure:-Onion diagnostics failed}"
logger -t pivilion-mode "Onion mode active but degraded: ${failure:-diagnostics failed}" || true
fi fi
update_torname || true refresh_login_banner
echo "Pivilion onion mode is active." echo "Pivilion onion mode activation completed."
} }
case "$MODE" in case "$MODE" in hotspot) start_hotspot ;; onion) start_onion ;; esac
hotspot) start_hotspot ;;
onion) start_onion ;;
*) echo "Unknown Pivilion mode: $MODE" >&2; exit 2 ;;
esac