failover: fix enforce_route() to handle gateway-less WANs (cellular)
The EC25/Koodo GSM connection is QMI raw-ip with an on-link /29 and no gateway at all (nh 0.0.0.0), not a normal DHCP WAN with a temporarily unknown gateway. enforce_route() required a gw and silently gave up when none was found, so cellular could be reported up/healthy/preferred in state.json while never actually getting a default route (Prefer button had no effect since it only touches metric, not this bailout). Now falls back to installing a gateway-less `ip route ... dev DEV` default when the device genuinely has an IPv4 address (carrier truly up), mirroring what NM itself would install without never-default. Also fixes stale-route pruning to handle gateway-less old routes, which the previous gw-only check skipped. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
6d5184e2e7
commit
b0283f6083
+32
-7
@@ -117,6 +117,28 @@ def routes_on(dev):
|
||||
return out
|
||||
|
||||
|
||||
def _del_default(dev, r):
|
||||
args = ["ip", "route", "del", "default", "dev", dev, "metric", str(r["metric"])]
|
||||
if r["gw"]:
|
||||
args[4:4] = ["via", r["gw"]]
|
||||
sh(args)
|
||||
|
||||
|
||||
def has_ipv4(dev):
|
||||
"""True if dev currently carries an IPv4 address (carrier genuinely up), used as the
|
||||
signal to install a gateway-less default route for point-to-point/on-link WANs (e.g.
|
||||
the EC25 modem's QMI raw-ip /29, which has no gateway at all — nh 0.0.0.0 — unlike
|
||||
a normal DHCP WAN whose gateway is just temporarily unknown)."""
|
||||
r = sh(["ip", "-4", "-j", "addr", "show", "dev", dev])
|
||||
if not (r and r.stdout.strip()):
|
||||
return False
|
||||
try:
|
||||
data = json.loads(r.stdout)
|
||||
return bool(data and data[0].get("addr_info"))
|
||||
except (json.JSONDecodeError, IndexError):
|
||||
return False
|
||||
|
||||
|
||||
def enforce_route(dev, metric, conn=None):
|
||||
"""Ensure exactly one default route on dev at the desired metric, via `ip route`.
|
||||
NEVER use `nmcli device reapply` — it resets r8152 USB-ethernet carriers and causes
|
||||
@@ -127,23 +149,26 @@ def enforce_route(dev, metric, conn=None):
|
||||
if any(r["metric"] == metric for r in rts):
|
||||
# desired metric already present; just prune any stale others
|
||||
for r in rts:
|
||||
if r["metric"] != metric and r["gw"]:
|
||||
sh(["ip", "route", "del", "default", "via", r["gw"], "dev", dev, "metric", str(r["metric"])])
|
||||
if r["metric"] != metric:
|
||||
_del_default(dev, r)
|
||||
return
|
||||
# Prefer the gw from an existing default route; fall back to NM's known gateway so we can
|
||||
# also *restore* a route that went missing while the carrier is still up (not just rebase one).
|
||||
gw = next((r["gw"] for r in rts if r["gw"]), None) or nm_gateway(dev, conn)
|
||||
if not gw:
|
||||
return # no gateway known (carrier down); profile metric still set
|
||||
r = sh(["ip", "route", "add", "default", "via", gw, "dev", dev, "metric", str(metric), "proto", "static"])
|
||||
args = ["ip", "route", "add", "default", "dev", dev, "metric", str(metric), "proto", "static"]
|
||||
if gw:
|
||||
args[4:4] = ["via", gw]
|
||||
elif not has_ipv4(dev):
|
||||
return # no gateway known and carrier not actually up; nothing to route via
|
||||
r = sh(args)
|
||||
if not (r and r.returncode == 0):
|
||||
# Add failed — most likely another dev transiently holds this exact metric during a
|
||||
# preference swap. Leave the existing route intact and retry next loop; do NOT prune,
|
||||
# or we'd strand this dev with no default route at all.
|
||||
return
|
||||
for old in rts:
|
||||
if old["metric"] != metric and old["gw"]:
|
||||
sh(["ip", "route", "del", "default", "via", old["gw"], "dev", dev, "metric", str(old["metric"])])
|
||||
if old["metric"] != metric:
|
||||
_del_default(dev, old)
|
||||
|
||||
|
||||
def nm_gateway(dev, conn):
|
||||
|
||||
Reference in New Issue
Block a user