feat(backend): runtime LAN-IP detection (netinfo helper) + manual override
The backend's bridge network can't see the host's NICs, so HOST_IP from install.sh went stale silently after a DHCP change. Now: - services/netinfo_helper.py runs as a new `netinfo` service (same backend image, network_mode: host): every 60s it picks the PHYSICAL LAN address (main-table default-route NIC if real hardware, else first real NIC with IPv4; never WireGuard/Tailscale/ZeroTier/bridges/veths, ignores 169.254) and writes it to the shared `netinfo` volume. Stdlib only. On Docker Desktop (linuxkit/WSL2 kernel) it reports "unsupported" instead of the VM's meaningless address. - services/lan_ip.py: one resolver used by /api/system/status (lan_ip + lan_ip_info), the pairing QR and the cloud heartbeat's local_ip: override (pos_settings network.lan_ip_override) → live detection (ignored when older than 5 min) → HOST_IP. Flags `mismatch` when a pinned address is no longer on any of the machine's NICs. - PUT /api/system/lan-ip-override (manager): set a private IPv4 or null to return to automatic; public/loopback/link-local/IPv6 rejected (422). - cloud_sync._get_local_ip uses the resolver (no more socket trick that returned the container IP). Tested: helper selection on a fake sysfs/route table (8 cases incl. VPN default routes) + real ioctl/route parsing on a Linux kernel; resolver priority/staleness/mismatch/validation (18 cases); isolated full stack: HOST_IP fallback on Docker Desktop, override save/validate/auth, simulated Linux detection incl. DHCP change and dead helper, heartbeat IP. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,6 @@ import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
@@ -80,18 +79,18 @@ def _compute_expiry_fields(expires_at_str: str | None) -> dict:
|
||||
|
||||
|
||||
def _get_local_ip() -> str | None:
|
||||
"""Best-effort detection of the host machine's LAN IP address.
|
||||
When running inside Docker the socket trick returns the container/bridge IP,
|
||||
so we honour HOST_IP if it is explicitly provided via the environment."""
|
||||
import os
|
||||
if host_ip := os.environ.get("HOST_IP", "").strip():
|
||||
return host_ip
|
||||
"""The server's LAN IP as phones see it — same resolver as the manager's
|
||||
pairing QR (override → live detection → HOST_IP), see services/lan_ip.py.
|
||||
No socket tricks: inside Docker they return the unreachable bridge IP."""
|
||||
from database import SessionLocal
|
||||
from services.lan_ip import resolve_lan_ip
|
||||
db = SessionLocal()
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||
s.connect(("8.8.8.8", 80))
|
||||
return s.getsockname()[0]
|
||||
return resolve_lan_ip(db)["effective"]
|
||||
except Exception:
|
||||
return None
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
async def _sync_once():
|
||||
|
||||
Reference in New Issue
Block a user