diff --git a/core/archipelago/src/bootstrap.rs b/core/archipelago/src/bootstrap.rs index 3670b0ae..6e26ecb7 100644 --- a/core/archipelago/src/bootstrap.rs +++ b/core/archipelago/src/bootstrap.rs @@ -171,6 +171,13 @@ pub async fn ensure_doctor_installed() { Ok(false) => debug!("tor-helper.sh already current"), Err(e) => warn!("tor-helper sync failed (non-fatal): {:#}", e), } + match run_welcome_banner_sync().await { + Ok(true) => info!( + "Console welcome banner synchronized (LAN address + .local name, not the WG tunnel IP)" + ), + Ok(false) => debug!("Console welcome banner already current (or not an ISO node)"), + Err(e) => warn!("Welcome banner sync failed (non-fatal): {:#}", e), + } match run_tor_torrc_repair().await { Ok(true) => info!("Tor healed at boot (torrc rebuilt and/or daemon restarted)"), Ok(false) => debug!("Tor healthy and torrc in sync — no heal needed"), @@ -654,6 +661,44 @@ exit 2 const TOR_HELPER_SH: &str = include_str!("../../../scripts/tor-helper.sh"); const TOR_HELPER_PATH: &str = "/opt/archipelago/scripts/tor-helper.sh"; +/// The console welcome banner, embedded so the OTA can fix it on deployed +/// nodes. `/etc/profile.d/archipelago.sh` is baked by the ISO installer and +/// no OTA path touched it, so every node kept whatever its ISO generation +/// shipped — including banners that print the node's own WireGuard address +/// (10.44.0.1, present on EVERY node) as the "web ui", which is unreachable +/// off-tunnel and actively misleading after a move to a new network +/// (framework-pt, 2026-08-15). Canonical copy: scripts/welcome-banner.sh; +/// the ISO builder inlines the same content for fresh installs. +const WELCOME_BANNER_SH: &str = include_str!("../../../scripts/welcome-banner.sh"); +const WELCOME_BANNER_PATH: &str = "/etc/profile.d/archipelago.sh"; + +async fn run_welcome_banner_sync() -> Result { + let current = tokio::fs::read_to_string(WELCOME_BANNER_PATH) + .await + .unwrap_or_default(); + // Only refresh a banner the installer put there: a dev machine running + // the backend from a checkout has no business growing one in /etc. + if current.is_empty() || current == WELCOME_BANNER_SH { + return Ok(false); + } + let staged = "/var/lib/archipelago/welcome-banner.staged"; + if let Some(dir) = Path::new(staged).parent() { + tokio::fs::create_dir_all(dir).await.ok(); + } + tokio::fs::write(staged, WELCOME_BANNER_SH) + .await + .context("stage welcome banner")?; + let script = format!( + "set -eu\ninstall -m 0755 {staged} {dest}\nexit 0\n", + staged = staged, + dest = WELCOME_BANNER_PATH + ); + host_sudo(&["sh", "-lc", &script]) + .await + .context("install welcome banner")?; + Ok(true) +} + async fn run_tor_helper_sync() -> Result { let current = tokio::fs::read_to_string(TOR_HELPER_PATH) .await diff --git a/image-recipe/_archived/build-auto-installer-iso.sh b/image-recipe/_archived/build-auto-installer-iso.sh index 7605e290..0c0f3765 100755 --- a/image-recipe/_archived/build-auto-installer-iso.sh +++ b/image-recipe/_archived/build-auto-installer-iso.sh @@ -3244,10 +3244,22 @@ esac if [ -t 0 ] && [ -z "$ARCHIPELAGO_WELCOMED" ]; then export ARCHIPELAGO_WELCOMED=1 - # Wait for network (DHCP may not be ready yet on first boot) + # Wait for network (DHCP may not be ready yet on first boot). + # The address shown must be one a LAN user can actually reach: the + # default route's source address. `hostname -I` lists addresses in + # interface order, so a node with WireGuard up advertised 10.44.0.1 — + # its own tunnel address, present on EVERY node — as its "web ui", + # which is unreachable off-tunnel and actively misleading after a + # move to a new network (framework-pt, 2026-08-15). IP="" for i in 1 2 3 4 5; do - IP=$(hostname -I 2>/dev/null | awk '{print $1}') + IP=$(ip -4 route get 1.1.1.1 2>/dev/null | sed -n 's/.*src \([0-9.]*\).*/\1/p' | head -n1) + # Offline LAN (no default route): first address that is not a + # tunnel (10.44/16 WireGuard), CGNAT (100.64/10 Tailscale), or + # loopback one. + [ -n "$IP" ] || IP=$(hostname -I 2>/dev/null | tr ' ' '\n' \ + | grep -vE '^(10\.44\.|100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.|127\.)' | head -n1) + [ -n "$IP" ] || IP=$(hostname -I 2>/dev/null | awk '{print $1}') [ -n "$IP" ] && break sleep 2 done @@ -3269,6 +3281,11 @@ if [ -t 0 ] && [ -z "$ARCHIPELAGO_WELCOMED" ]; then echo -e " ${OD}bitcoin node os${N}" if [ -n "$IP" ]; then echo -e " ${W}web ui http://$IP${N}" + # The mDNS name survives any DHCP change — it is the address to + # give people for a headless box that moves between networks. + if systemctl is-active avahi-daemon >/dev/null 2>&1; then + echo -e " ${W} http://$(hostname).local${N}" + fi echo -e " ${W}ssh archipelago@$IP${N}" echo -e " ${W}password archipelago (SSH)${N}" echo -e " ${OD}web ui asks you to create a password on first visit${N}" diff --git a/scripts/welcome-banner.sh b/scripts/welcome-banner.sh new file mode 100644 index 00000000..9de861b8 --- /dev/null +++ b/scripts/welcome-banner.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# Console welcome banner (/etc/profile.d/archipelago.sh). +# +# CANONICAL COPY. Two consumers keep nodes in lockstep with it: +# - bootstrap::run_welcome_banner_sync embeds it (include_str!) and +# installs it at startup on ISO-installed nodes, so banner fixes +# actually reach the deployed fleet via OTA; +# - the ISO builder (image-recipe/_archived/build-auto-installer-iso.sh, +# PROFILE heredoc) inlines the same content for fresh installs. +# +# Ensure /sbin and /usr/sbin are in PATH (needed for reboot, shutdown, etc.) +case ":$PATH:" in + *:/sbin:*) ;; *) export PATH="$PATH:/sbin:/usr/sbin" ;; +esac +if [ -t 0 ] && [ -z "$ARCHIPELAGO_WELCOMED" ]; then + export ARCHIPELAGO_WELCOMED=1 + + # Wait for network (DHCP may not be ready yet on first boot). + # The address shown must be one a LAN user can actually reach: the + # default route's source address. `hostname -I` lists addresses in + # interface order, so a node with WireGuard up advertised 10.44.0.1 — + # its own tunnel address, present on EVERY node — as its "web ui", + # which is unreachable off-tunnel and actively misleading after a + # move to a new network (framework-pt, 2026-08-15). + IP="" + for i in 1 2 3 4 5; do + IP=$(ip -4 route get 1.1.1.1 2>/dev/null | sed -n 's/.*src \([0-9.]*\).*/\1/p' | head -n1) + # Offline LAN (no default route): first address that is not a + # tunnel (10.44/16 WireGuard), CGNAT (100.64/10 Tailscale), or + # loopback one. + [ -n "$IP" ] || IP=$(hostname -I 2>/dev/null | tr ' ' '\n' \ + | grep -vE '^(10\.44\.|100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.|127\.)' | head -n1) + [ -n "$IP" ] || IP=$(hostname -I 2>/dev/null | awk '{print $1}') + [ -n "$IP" ] && break + sleep 2 + done + + O='\033[38;5;208m' + OD='\033[38;5;130m' + W='\033[1;37m' + N='\033[0m' + + # The logo uses UTF-8 block-drawing glyphs. Switching back from the kiosk + # (Xorg on vt1) can leave the console VT out of UTF-8 mode, which renders + # them as garbage bytes ("sometimes corrupt"). ESC % G forces the VT into + # UTF-8 mode so the logo always draws correctly. + printf '\033%%G' 2>/dev/null || true + clear + echo -e " ${O}▄▀█ █▀▄ █▀▀ █ █ █ █▀█ █▀▀ █ ▄▀█ █▀▀ █▀█${N}" + echo -e " ${O}█▀█ █▀▄ █ █▀█ █ █▀▀ ██▀ █ █▀█ █ █ █ █${N}" + echo -e " ${O}▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀${N}" + echo -e " ${OD}bitcoin node os${N}" + if [ -n "$IP" ]; then + echo -e " ${W}web ui http://$IP${N}" + # The mDNS name survives any DHCP change — it is the address to + # give people for a headless box that moves between networks. + if systemctl is-active avahi-daemon >/dev/null 2>&1; then + echo -e " ${W} http://$(hostname).local${N}" + fi + echo -e " ${W}ssh archipelago@$IP${N}" + echo -e " ${W}password archipelago (SSH)${N}" + echo -e " ${OD}web ui asks you to create a password on first visit${N}" + else + echo -e " ${OD}Waiting for network...${N}" + fi + if [ -b /dev/mapper/archipelago-data ] || [ -b /dev/mapper/archipelago_crypt ]; then + echo -e " ${OD}storage LUKS2 encrypted${N}" + fi + # The kiosk's Xorg runs on vt1 (see archipelago-kiosk-launcher: "Xorg :0 + # vt1"), so Ctrl+Alt+F1 IS the kiosk and a terminal is on another VT (F2, + # where systemd auto-spawns a getty). The old hints had these backwards — + # they sent you to an empty black VT7 and there was no way back to the kiosk. + if systemctl is-active archipelago-kiosk.service >/dev/null 2>&1; then + echo -e " ${OD}display Kiosk active (Ctrl+Alt+F2 for terminal)${N}" + else + echo -e " ${OD}display Console (Ctrl+Alt+F1 for kiosk)${N}" + fi + echo "" +fi