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" }