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 <github@szelinsky.de>
Tested-by: Fredrik Hallin <fredappsdev@outlook.com>
Link: https://github.com/openwrt/openwrt/pull/24689
Signed-off-by: Jonas Jelonek <jonas@jonasjelonek.de>
This commit is contained in:
Carlo Szelinsky authored and Jonas Jelonek committed 2026-09-17 18:19:17 +02:00
1 parent d0df2ab604
commit a4575808af
2 files changed
+42 -4

No files matched your search

@@ -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}"
},
+41 -4
View File
@@ -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