fix(banner): console banner shows the reachable LAN address, not the WG tunnel IP

The welcome banner picked its address with 'hostname -I | awk {print $1}',
so a node with WireGuard up advertised 10.44.0.1 — its own tunnel address,
present on EVERY node — as its web ui / ssh address. Off-tunnel that is
unreachable, and after a headless box moves to a new network it is exactly
the wrong thing to trust (framework-pt, 2026-08-15).

- Pick the default route's source address; fall back to the first address
  that is not WireGuard 10.44/16, CGNAT 100.64/10, or loopback.
- Also print http://<hostname>.local when avahi is up — the one address
  that survives any DHCP change, which is the real answer for headless
  boxes that move between networks.
- scripts/welcome-banner.sh is the new canonical copy, embedded in the
  binary (tor-helper pattern): bootstrap::run_welcome_banner_sync rewrites
  /etc/profile.d/archipelago.sh on ISO-installed nodes at startup, so the
  fix reaches the deployed fleet with the next OTA instead of only fresh
  ISOs. Machines without an installer-baked banner are left untouched.
- Same fix inlined in the live ISO builder's PROFILE heredoc
  (image-recipe/_archived/build-auto-installer-iso.sh).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-15 08:37:13 -04:00
co-authored by Claude Fable 5
parent 56d6396142
commit 1587853ce2
3 changed files with 143 additions and 2 deletions
+45
View File
@@ -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<bool> {
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<bool> {
let current = tokio::fs::read_to_string(TOR_HELPER_PATH)
.await
@@ -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}"
+79
View File
@@ -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