failover: resolve MBIM/QMI modems to their wwan netdev
NM reports a gsm connection's device as the control port (cdc-wdm0), but IP + routes live on the wwan netdev (wwu1u2i4 on the Quectel EC25-AF), so probes and route enforcement silently saw nothing (cellular stuck "down", no metric). Map connection-keyed WANs through GENERAL.IP-IFACE, and let the gateway fallback query the connection since the netdev isn't an NM device. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
7c36757d35
commit
fe9e6d3b30
+24
-9
@@ -68,7 +68,19 @@ def resolve(wan, actives):
|
||||
return dev, conn
|
||||
conn = wan.get("connection")
|
||||
dev = next((a["device"] for a in actives if a["name"] == conn), None)
|
||||
return dev, conn
|
||||
return ip_iface(dev), conn
|
||||
|
||||
|
||||
def ip_iface(dev):
|
||||
"""The routed netdev for an NM device. For MBIM/QMI modems NM's device is the
|
||||
control port (cdc-wdm0) while IP/routes live on the wwan netdev — probing and
|
||||
`ip route` must use the latter."""
|
||||
if not dev:
|
||||
return dev
|
||||
r = sh(["nmcli", "-g", "GENERAL.IP-IFACE", "device", "show", dev])
|
||||
if r and r.returncode == 0 and r.stdout.strip():
|
||||
return r.stdout.strip()
|
||||
return dev
|
||||
|
||||
|
||||
def probe_one(dev, url, timeout):
|
||||
@@ -105,7 +117,7 @@ def routes_on(dev):
|
||||
return out
|
||||
|
||||
|
||||
def enforce_route(dev, metric):
|
||||
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
|
||||
a failover flap. This is a pure routing change (carrier-safe, verified)."""
|
||||
@@ -120,7 +132,7 @@ def enforce_route(dev, metric):
|
||||
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 device_gateway(dev)
|
||||
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"])
|
||||
@@ -134,11 +146,14 @@ def enforce_route(dev, metric):
|
||||
sh(["ip", "route", "del", "default", "via", old["gw"], "dev", dev, "metric", str(old["metric"])])
|
||||
|
||||
|
||||
def device_gateway(dev):
|
||||
"""NM's gateway for a device — available even when its default route is missing."""
|
||||
r = sh(["nmcli", "-g", "IP4.GATEWAY", "device", "show", dev])
|
||||
if r and r.returncode == 0:
|
||||
return r.stdout.strip() or None
|
||||
def nm_gateway(dev, conn):
|
||||
"""NM's gateway — available even when the default route is missing. Try the device,
|
||||
then the connection (a wwan netdev is not an NM device, but its connection is active)."""
|
||||
for kind, name in (("device", dev), ("connection", conn)):
|
||||
if name:
|
||||
r = sh(["nmcli", "-g", "IP4.GATEWAY", kind, "show", name])
|
||||
if r and r.returncode == 0 and r.stdout.strip():
|
||||
return r.stdout.strip()
|
||||
return None
|
||||
|
||||
|
||||
@@ -224,7 +239,7 @@ def main():
|
||||
desired = base if s["up"] else base + PENALTY
|
||||
# Enforce the live route every loop (carrier-safe, corrects any NM drift);
|
||||
# update the NM profile only on an actual state change.
|
||||
enforce_route(dev, desired)
|
||||
enforce_route(dev, desired, conn)
|
||||
if s["applied"] != desired:
|
||||
print(f"{name}: {'UP' if s['up'] else 'DOWN'} -> metric {desired}", flush=True)
|
||||
set_profile_metric(conn, desired)
|
||||
|
||||
Reference in New Issue
Block a user