From 034106918d9e56680e42508644591dfd276d8be8 Mon Sep 17 00:00:00 2001 From: bonamin Date: Mon, 28 Sep 2026 15:46:51 +0300 Subject: [PATCH] fix(install): detect HOST_IP from physical NICs only, never a VPN tunnel `ip route get 1.1.1.1` returns the tunnel address on servers running a full-tunnel WireGuard/Tailscale/ZeroTier client, so phones got a QR code and URL pointing at an address they can't reach. Detection now uses the main-table default route's interface if it is real hardware (/sys/class/net//device), else the first physical NIC with an IPv4, else `hostname -I` as a last resort (the installer shows it for confirmation). .env.example says HOST_IP must be the physical LAN address. Tested with a stubbed `ip` + fake sysfs in Debian: wg-quick full tunnel, tunnel owning the default route, no default route and WiFi-only all pick the physical address; full install.sh scenarios unchanged. Co-Authored-By: Claude Opus 5.5 --- .env.example | 6 ++++-- install.sh | 34 +++++++++++++++++++++++++++------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index 79a2df0..5c3364c 100644 --- a/.env.example +++ b/.env.example @@ -9,8 +9,10 @@ CLOUD_URL=https://xenia-admin.bonamin.gr SECRET_KEY=generate-with-openssl-rand-hex-32 LICENSE_GRACE_HOURS=24 -# This machine's LAN IP — the address phones open (http://) and the -# pairing QR code encodes. install.sh detects it; reserve it in the router's DHCP. +# This machine's LAN IP on the PHYSICAL network (Ethernet/WiFi) — the address +# phones open (http://) and the pairing QR code encodes. Never a VPN +# address (WireGuard/Tailscale/ZeroTier). install.sh detects it; reserve it in +# the router's DHCP. HOST_IP= # Break-glass support account (leave blank to disable) diff --git a/install.sh b/install.sh index d847e91..cea52c6 100644 --- a/install.sh +++ b/install.sh @@ -7,17 +7,37 @@ set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -# LAN IP of this machine (the address phones use: http://). -# `ip route get` picks the interface that actually routes out, which avoids -# docker0 / bridge addresses that `hostname -I` may list first. +# LAN IP of this machine on the PHYSICAL network (the address phones use: +# http://). Must never be a VPN address: with a full-tunnel WireGuard / +# Tailscale / ZeroTier client, "the interface that reaches the internet" is the +# tunnel, which phones on the restaurant WiFi can't reach. +# Real NICs (Ethernet, WiFi) have /sys/class/net//device; VPN tunnels, +# Docker bridges, veths and loopback don't. +# XENIA_SYS_NET overrides the sysfs path (tests only). detect_host_ip() { - local ip="" - if command -v ip >/dev/null 2>&1; then - ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for (i=1;i<=NF;i++) if ($i=="src") {print $(i+1); exit}}') + local sys_net="${XENIA_SYS_NET:-/sys/class/net}" ifc ip="" + command -v ip >/dev/null 2>&1 || { hostname -I 2>/dev/null | awk '{print $1}'; return; } + + first_ipv4() { ip -4 -o addr show dev "$1" 2>/dev/null | awk '{split($4, a, "/"); print a[1]; exit}'; } + + # 1) Interface of the main-table default route, if it's real hardware. + # (wg-quick full-tunnel routing uses policy rules + its own table, so the + # main table's default route still points at the physical uplink.) + ifc=$(ip route show default 2>/dev/null | awk '{for (i=1;i<=NF;i++) if ($i=="dev") {print $(i+1); exit}}') + if [ -n "$ifc" ] && [ -e "$sys_net/$ifc/device" ]; then + ip=$(first_ipv4 "$ifc") fi + # 2) Otherwise the first real-hardware interface that has an IPv4 address. if [ -z "$ip" ]; then - ip=$(hostname -I 2>/dev/null | awk '{print $1}') + for path in "$sys_net"/*; do + ifc=$(basename "$path") + [ -e "$path/device" ] || continue + ip=$(first_ipv4 "$ifc") + [ -n "$ip" ] && break + done fi + # 3) Last resort — the admin confirms it at the prompt anyway. + [ -z "$ip" ] && ip=$(hostname -I 2>/dev/null | awk '{print $1}') echo "$ip" }