feat(public-ip): unify public-IP detection into one privacy-first script
The public IP was previously detected independently in three places,
each contacting a different third party: the Hub (HTTPS echo via
api.ipify.org / ifconfig.me / icanhazip.com on every API call and
background tick), DDNS (myip.opendns.com via OpenDNS), and LiveKit
(embedded STUN). Consolidate into a single detector with one shared
cache so every consumer reads the same value with minimal exposure.
- add modules/core/public-ip.nix: installs /var/lib/sovran/public-ip.py
(pure Python stdlib, no new deps) writing /var/lib/secrets/external-ip
- detection chain (first success wins): explicit pin, fresh cache
(default TTL 300s), STUN binding request over UDP (one packet, no
metadata), DNS myip.opendns.com query, then OPT-IN HTTPS echo
(publicIP.httpsEcho, empty by default — never contacted unless listed)
- privacy: while the cache is fresh zero third parties are contacted;
at most one party learns the IP per refresh interval, via the least
exposing mechanism available
- hub (server.py): _get_external_ip() now reads the shared detector /
cache instead of calling ipify/ifconfig/icanhazip directly
- ddns (njalla.nix): use the shared detector instead of a separate
OpenDNS dig; allow the hardened service to write /var/lib/secrets
- element-calling: livekit-turn-setup falls back to the shared
detector on cold boot; add LiveKit webhooks to lk-jwt-service
(sfu_webhook) so abrupt disconnects are cleaned up immediately;
set LIVEKIT_SANITY_CHECK_INTERVAL_SECONDS=60 as a missed-webhook
guard; drop the dead services.livekit.settings block and set
openFirewall=false (Caddy fronts the SFU; no public 7880/tcp)
- new options: sovran_systemsOS.publicIP.{stunServer,stunPort,
dnsResolver,httpsEcho,cacheTTL}
This commit is contained in:
+17
-4
@@ -33,7 +33,7 @@
|
||||
# /var/lib/njalla/ddns_urls.json.
|
||||
NoNewPrivileges = true;
|
||||
ProtectSystem = "strict";
|
||||
ReadWritePaths = [ "/var/lib/njalla" ];
|
||||
ReadWritePaths = [ "/var/lib/njalla" "/var/lib/secrets" ];
|
||||
ReadOnlyPaths = [ "/etc/sovran" ];
|
||||
ProtectHome = true;
|
||||
PrivateTmp = true;
|
||||
@@ -88,12 +88,15 @@ try:
|
||||
except Exception:
|
||||
sys.exit(0) # no URLs configured — nothing to do
|
||||
|
||||
# Resolve current public IP once
|
||||
# Resolve current public IP via the shared detector — one script, one cache
|
||||
# (STUN -> DNS -> opt-in HTTPS echo; see /var/lib/sovran/public-ip.py).
|
||||
# The detector refreshes /var/lib/secrets/external-ip, which the Hub and
|
||||
# LiveKit read as well, so the whole system shares a single detected value.
|
||||
public_ip = ""
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["dig", "@resolver4.opendns.com", "myip.opendns.com", "+short", "-4"],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
[sys.executable, "/var/lib/sovran/public-ip.py", "check"],
|
||||
capture_output=True, text=True, timeout=20,
|
||||
)
|
||||
raw = r.stdout.strip().splitlines()[0] if r.stdout.strip() else ""
|
||||
ipaddress.ip_address(raw) # validates — raises if not a real IP
|
||||
@@ -101,6 +104,16 @@ try:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not public_ip:
|
||||
# Last resort: the shared cache file, if the detector is unavailable.
|
||||
try:
|
||||
with open("/var/lib/secrets/external-ip") as f:
|
||||
raw = f.read().strip()
|
||||
ipaddress.ip_address(raw)
|
||||
public_ip = raw
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not public_ip:
|
||||
sys.exit(0) # no IP resolved — skip to avoid sending bare ''${IP}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user