From a4575808afabdf1fcf32af74854692ec02114b40 Mon Sep 17 00:00:00 2001 From: Carlo Szelinsky Date: Sat, 22 Aug 2026 10:22:52 +0200 Subject: [PATCH] comgt: ncm: wait for the data link to settle before configuring it Some modems raise carrier as soon as the data call is enabled and drop it again briefly while the connection is finalised. On a Quectel EG060W the drop is about a second long and lands about two seconds after AT+QNETDEVCTL, with no configuration command involved at all. Not every modem does it: a Fibocom FG621-EA sampled at ~90 Hz over three cycles never dropped carrier. netifd turns that transient into a permanent failure: the DHCP sub-interface is dynamic and created only once during setup, and a dynamic interface is removed on carrier loss (IFC_REMOVE in interface_check_state()), so nothing ever recreates it. udhcpc is usually still sending discovers when the drop kills it, and the interface is left with no address and no route. If the drop lands after a lease was granted the lease is stale instead, and a renew does not recover that either because it unicasts to a server that no longer exists. Wait for the carrier to stay up for a few seconds before handing the interface to netifd, so the drop falls outside the window netifd is watching. directip.sh already waits for carrier for the same class of reason ("wait for carrier to avoid firmware stability bugs") and qmi.sh sleeps unconditionally while waiting for registration; unlike those this is tunable, and linksettle 0 keeps the previous behaviour. The wait gives up after three times linksettle, floored at 30s so it always outlasts the settle window, and continues anyway. Profiles that only reach data state at finalize, further down, have nothing to wait for here and skip it; mtk1 is marked with linkatfinalize for that. Measured on an EG060W (Cudy LT700 Outdoor), three runs each: with linksettle 0 udhcpc never obtains a lease, the dynamic interface is torn down and the link stays dead; with the 3s default the lease, route and traffic come up every time, and netifd logs no carrier loss at all. On a modem without the drop the wait only adds linksettle seconds to the bring-up (14s vs 17s on the FG621-EA). It stays on by default anyway: a missed drop leaves the link permanently dead rather than slow, and it is not known in advance which modems do it. linksettle 0 turns it off where it is not needed. Signed-off-by: Carlo Szelinsky Tested-by: Fredrik Hallin Link: https://github.com/openwrt/openwrt/pull/24689 Signed-off-by: Jonas Jelonek --- package/network/utils/comgt/files/ncm.json | 1 + package/network/utils/comgt/files/ncm.sh | 45 ++++++++++++++++++++-- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/package/network/utils/comgt/files/ncm.json b/package/network/utils/comgt/files/ncm.json index 5f68b134967..3aafcd06e00 100644 --- a/package/network/utils/comgt/files/ncm.json +++ b/package/network/utils/comgt/files/ncm.json @@ -72,6 +72,7 @@ "AT+CGDCONT=${profile},\\\"${pdptype}\\\",\\\"${apn}\\\",0,0" ], "connect": "AT+CGACT=1,${profile}", + "linkatfinalize": "1", "finalize": "AT+CGDATA=\\\"M-MBIM\\\",${profile},1", "disconnect": "AT+CGACT=0,${profile}" }, diff --git a/package/network/utils/comgt/files/ncm.sh b/package/network/utils/comgt/files/ncm.sh index 73dd1f8b6d1..20d1c84f25c 100644 --- a/package/network/utils/comgt/files/ncm.sh +++ b/package/network/utils/comgt/files/ncm.sh @@ -17,6 +17,7 @@ proto_ncm_init_config() { proto_config_add_string password proto_config_add_string pincode proto_config_add_string delay + proto_config_add_int linksettle proto_config_add_string mode proto_config_add_string pdptype proto_config_add_boolean sourcefilter @@ -26,16 +27,44 @@ proto_ncm_init_config() { proto_config_add_defaults } +# Wait for carrier to settle after connecting: modems often drop it again +# briefly while finalising, and the PDP address can change across that flap. +ncm_wait_link() { + local ifname="$1" settle="$2" + local stable=0 waited=0 carrier timeout + + # must outlast settle + timeout=$((settle * 3)) + [ "$timeout" -lt 30 ] && timeout=30 + + /sbin/ip link set dev "$ifname" up 2>/dev/null + + while [ "$waited" -lt "$timeout" ]; do + # unreadable (netdev down, no carrier support): nothing to wait for + carrier=$(cat "/sys/class/net/$ifname/carrier" 2>/dev/null) || return 0 + + [ "$carrier" = "1" ] && stable=$((stable + 1)) || stable=0 + + sleep 1 + waited=$((waited + 1)) + + # after the sleep, so settle counts seconds + [ "$stable" -ge "$settle" ] && return 0 + done + + return 1 +} + proto_ncm_setup() { local interface="$1" - local connect context_type devname devpath finalize ifpath initialize manufacturer setmode + local connect context_type devname devpath finalize ifpath initialize linkatfinalize manufacturer setmode local delegate ip4table ip6table mtu sourcefilter $PROTO_DEFAULT_OPTIONS json_get_vars delegate ip4table ip6table mtu sourcefilter $PROTO_DEFAULT_OPTIONS - local apn auth delay device ifname mode password pdptype pincode profile username - json_get_vars apn auth delay device ifname mode password pdptype pincode profile username + local apn auth delay device ifname linksettle mode password pdptype pincode profile username + json_get_vars apn auth delay device ifname linksettle mode password pdptype pincode profile username [ "$metric" = "" ] && metric="0" @@ -176,7 +205,15 @@ proto_ncm_setup() { } } - json_get_vars finalize + json_get_vars finalize linkatfinalize + + # nothing to wait for yet when the profile only enters data state at + # finalize, further down + [ -n "$linksettle" ] || linksettle=3 + [ "$linksettle" -gt 0 ] && [ -z "$linkatfinalize" ] && { + ncm_wait_link "$ifname" "$linksettle" || \ + echo "Link did not settle, continuing anyway" + } echo "Setting up $ifname" proto_init_update "$ifname" 1