From bbbf80c25f8dd5d9ab82c2f3c9b7ed16890d0640 Mon Sep 17 00:00:00 2001 From: v3d Date: Mon, 29 Jun 2026 23:08:56 +0200 Subject: [PATCH] Move MOTD into TTY login banner --- README.md | 6 ++ etc/profile.d/pivilion-motd.sh | 5 +- .../system/pivilion-boot-greeter.service | 13 ---- .../system/pivilion-login-banner.service | 14 +++++ install.sh | 61 ++++++++++++++++--- tests/run.sh | 10 +++ tests/test-common.sh | 6 ++ usr/local/lib/pivilion/boot-greeter.sh | 6 -- usr/local/lib/pivilion/common.sh | 16 ++++- usr/local/lib/pivilion/issue-banner.sh | 16 +++++ usr/local/lib/pivilion/mode.sh | 13 ++++ 11 files changed, 137 insertions(+), 29 deletions(-) delete mode 100644 etc/systemd/system/pivilion-boot-greeter.service create mode 100644 etc/systemd/system/pivilion-login-banner.service delete mode 100755 usr/local/lib/pivilion/boot-greeter.sh create mode 100755 usr/local/lib/pivilion/issue-banner.sh diff --git a/README.md b/README.md index 3175d28..d3e1317 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,12 @@ 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. diff --git a/etc/profile.d/pivilion-motd.sh b/etc/profile.d/pivilion-motd.sh index fabd70d..655be9d 100644 --- a/etc/profile.d/pivilion-motd.sh +++ b/etc/profile.d/pivilion-motd.sh @@ -1,7 +1,8 @@ -# Show the Pivilion status banner for the configured owner on interactive Bash -# login shells. Other users and non-interactive sessions remain unchanged. +# 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 diff --git a/etc/systemd/system/pivilion-boot-greeter.service b/etc/systemd/system/pivilion-boot-greeter.service deleted file mode 100644 index 75b1e23..0000000 --- a/etc/systemd/system/pivilion-boot-greeter.service +++ /dev/null @@ -1,13 +0,0 @@ -[Unit] -Description=Display the Pivilion boot greeting and system status -After=local-fs.target -Before=getty@tty1.service - -[Service] -Type=oneshot -ExecStart=/usr/local/lib/pivilion/boot-greeter.sh -StandardOutput=journal+console -StandardError=journal+console - -[Install] -WantedBy=multi-user.target diff --git a/etc/systemd/system/pivilion-login-banner.service b/etc/systemd/system/pivilion-login-banner.service new file mode 100644 index 0000000..aa386df --- /dev/null +++ b/etc/systemd/system/pivilion-login-banner.service @@ -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 diff --git a/install.sh b/install.sh index df47c2b..69ea6d6 100755 --- a/install.sh +++ b/install.sh @@ -42,13 +42,14 @@ 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/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 @@ -117,7 +118,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-mode.service pivilion-boot-greeter.service pivilion-dnsmasq.service pivilion-hostapd.service tor.service tor@default.service; do + 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 @@ -146,9 +147,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-mode.service pivilion-boot-greeter.service tor.service tor@default.service 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-dnsmasq.service pivilion-hostapd.service tor.service tor@default.service; do + 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" @@ -171,7 +172,7 @@ deploy_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 0755 "$SOURCE_DIR/usr/local/lib/pivilion/mode.sh" "$SOURCE_DIR/usr/local/lib/pivilion/diagnose.sh" \ - "$SOURCE_DIR/usr/local/lib/pivilion/boot-greeter.sh" /usr/local/lib/pivilion/ + "$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 0644 "$SOURCE_DIR"/etc/systemd/system/*.service /etc/systemd/system/ install -m 0644 "$SOURCE_DIR/etc/tor/torrc.d/pivilion.conf" /etc/tor/torrc.d/pivilion.conf @@ -211,6 +212,43 @@ EOF 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() { local tmp log "Configuring services" @@ -228,10 +266,13 @@ EOF if ! grep -Fqx '%include /etc/tor/torrc.d/*.conf' /etc/tor/torrc; then 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 + migrate_legacy_console_greeter systemctl daemon-reload - systemctl enable apache2 pivilion-mode.service pivilion-boot-greeter.service + systemctl enable apache2 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 + /usr/local/lib/pivilion/issue-banner.sh + command -v agetty >/dev/null 2>&1 && agetty --reload >/dev/null 2>&1 || true } validate_install() { @@ -243,6 +284,9 @@ validate_install() { 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" } @@ -259,7 +303,10 @@ EOF check_install() { local failed=0 file - for file in /etc/pivilion.defaults /var/lib/pivilion/install-state /usr/local/lib/pivilion/common.sh /usr/local/lib/pivilion/mode.sh /usr/local/lib/pivilion/diagnose.sh /etc/systemd/system/pivilion-mode.service; do + 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 diff --git a/tests/run.sh b/tests/run.sh index 1b9ae57..fa1fadb 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -49,6 +49,15 @@ state_contract() { 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" @@ -61,6 +70,7 @@ 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; } diff --git a/tests/test-common.sh b/tests/test-common.sh index aff59bf..bb5cd9f 100755 --- a/tests/test-common.sh +++ b/tests/test-common.sh @@ -17,6 +17,7 @@ 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" @@ -43,6 +44,11 @@ EOF [ "$(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 diff --git a/usr/local/lib/pivilion/boot-greeter.sh b/usr/local/lib/pivilion/boot-greeter.sh deleted file mode 100755 index 466f9f0..0000000 --- a/usr/local/lib/pivilion/boot-greeter.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -set -euo pipefail - -source /usr/local/lib/pivilion/common.sh -pivilion_load_config -pivilion_print_motd "Welcome to Pivilion!" diff --git a/usr/local/lib/pivilion/common.sh b/usr/local/lib/pivilion/common.sh index 85793eb..4a2672b 100644 --- a/usr/local/lib/pivilion/common.sh +++ b/usr/local/lib/pivilion/common.sh @@ -161,6 +161,7 @@ pivilion_service_state() { 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') @@ -191,6 +192,19 @@ pivilion_print_motd() { @@, @ ,@@ Onion Address......: $torname @@@@@#@@@@@@@#@@@@@ Desired / active....: $desired / $active Backend / health...: $backend / $health - IP addresses.......: $addresses 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 } diff --git a/usr/local/lib/pivilion/issue-banner.sh b/usr/local/lib/pivilion/issue-banner.sh new file mode 100755 index 0000000..5e1a0e3 --- /dev/null +++ b/usr/local/lib/pivilion/issue-banner.sh @@ -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 diff --git a/usr/local/lib/pivilion/mode.sh b/usr/local/lib/pivilion/mode.sh index f7e7e0f..5c565a2 100755 --- a/usr/local/lib/pivilion/mode.sh +++ b/usr/local/lib/pivilion/mode.sh @@ -24,10 +24,18 @@ CAPTIVE_LIVE=$PIVILION_WEBROOT/gen/.htaccess BACKEND=legacy pivilion_uses_networkmanager && BACKEND=networkmanager +refresh_login_banner() { + if [ -x /usr/local/lib/pivilion/issue-banner.sh ]; then + /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" } @@ -168,16 +176,19 @@ 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 @@ -203,6 +214,7 @@ start_hotspot() { 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." } @@ -227,6 +239,7 @@ start_onion() { pivilion_write_state onion "$BACKEND" degraded "${failure:-Onion diagnostics failed}" logger -t pivilion-mode "Onion mode active but degraded: ${failure:-diagnostics failed}" || true fi + refresh_login_banner echo "Pivilion onion mode activation completed." }