fix(ansible): resolve LTE failover data-plane bug on BroadMobi BM806C
ci/woodpecker/push/flux-reconcile-source Pipeline was successful
ci/woodpecker/cron/renovate Pipeline was successful

The embedded BroadMobi BM806C modem (Qualcomm MDM9225, firmware
M1.2.0_E1.0.1_A1.1.8) in the D-Link DWR-921 C1 has two independent
firmware bugs that together break the QMI data plane:

1.  Modem accepts 802.3 framing but its 802.3 path is buggy — downlink
    frames never reach the host kernel. raw-ip framing works.

2.  qmish calls uqmi --start-network --apn <foo>, which triggers
    FS#1363: the modem establishes a phantom bearer that gets IP
    addresses but has no working data path. Using --start-network
    --profile <N> (referencing a pre-configured NVRAM profile with
    the same APN) works correctly.

Fixes applied:

- qmish patches (3x community.openwrt.lineinfile):
  * Replace --set-data-format 802.3 with raw-ip
  * Replace --wda-set-data-format 802.3 with raw-ip
  * Bracket raw_ip sysfs write with ip link down/up (kernel rejects
    write with -EBUSY when wwan0 is already up)
- Modem NVRAM: create/modify profile 2 (internetipv6, ipv6) for the
  IPv6 APN — profile 1 is already managed by qmish's --modify-profile
- UCI wwan: add profile=1 and v6profile=2 so qmish uses --start-network
  --profile instead of --apn on both the v4 and v6 legs
- Firewall: add wwan zone (input REJECT, output ACCEPT, forward REJECT)
  and Allow-ICMPv6-wwan rule
- main.yml: reorder — packages (including usb-modeswitch) now run
  before wwan setup, so the modem is out of EDL mode when wwan.yml
  queries it for profile creation

See docs/wwan-bm806c-qmi-workaround.md for the full diagnosis
(what we ruled out, how we confirmed, manual setup steps, component
versions, future upstreaming).
This commit is contained in:
2026-05-16 19:54:46 +02:00
parent 92447996fc
commit 2d69cc6569
6 changed files with 757 additions and 4 deletions
+18
View File
@@ -20,6 +20,11 @@
# output: ACCEPT (AP itself initiates outbound — opkg, NTP, etc.)
# forward: REJECT (AP does not route client traffic through uplink)
#
# wwan — LTE modem uplink (Orange PL, /dev/cdc-wdm0, disabled by default)
# input: REJECT (no inbound from LTE)
# output: ACCEPT (AP itself uses LTE for outbound when uplink unavailable)
# forward: REJECT (no client traffic through LTE)
#
# No forwarding rules between zones — all inter-zone policy is on MikroTik.
- name: Configure firewall
@@ -64,10 +69,23 @@
option output 'ACCEPT'
option forward 'REJECT'
config zone
option name 'wwan'
list network 'wwan'
option input 'REJECT'
option output 'ACCEPT'
option forward 'REJECT'
config rule
option name 'Allow-ICMPv6-uplink'
option src 'uplink'
option proto 'icmpv6'
option target 'ACCEPT'
config rule
option name 'Allow-ICMPv6-wwan'
option src 'wwan'
option proto 'icmpv6'
option target 'ACCEPT'
notify: Reload firewall
+10 -4
View File
@@ -5,9 +5,19 @@
- name: System configuration
ansible.builtin.import_tasks: system.yml
# Packages must be installed before wwan.yml — usb-modeswitch is what triggers
# the modem out of EDL mode (05c6:9008 → 2020:2033 QMI), and uqmi/luci-proto-qmi
# provide the tools used downstream.
- name: Package management
ansible.builtin.import_tasks: packages.yml
when: openwrt_packages | length > 0
- name: Network configuration
ansible.builtin.import_tasks: network.yml
- name: WWAN modem configuration
ansible.builtin.import_tasks: wwan.yml
- name: Firewall configuration
ansible.builtin.import_tasks: firewall.yml
@@ -16,7 +26,3 @@
- name: LED configuration
ansible.builtin.import_tasks: led.yml
- name: Package management
ansible.builtin.import_tasks: packages.yml
when: openwrt_packages | length > 0
+34
View File
@@ -14,6 +14,8 @@
# lan — bridge (br-lan) on eth0.2, LAN clients via LAN ports
# iot — bridge (br-iot) on eth0.5, IoT clients via wifi only
# uplink — static 192.168.6.2/24 + 2001:470:61a3:600::2/64 on eth0.6, internet access for opkg
# wwan — QMI LTE modem (/dev/cdc-wdm0), Orange PL dual-stack failover (APNs: internet + internetipv6)
# Manual ifup only (option auto '0'); modem-specific quirks handled in wwan.yml.
- name: Configure network
community.openwrt.uci:
@@ -126,6 +128,38 @@
option device 'br-iot'
option proto 'none'
# LTE failover via embedded BroadMobi BM806C (Qualcomm MDM9225, fw M1.2.0_E1.0.1_A1.1.8).
# This modem has a firmware bug: when QMI --start-network is invoked with --apn
# (a WDS TLV), the modem establishes a phantom bearer that gets assigned IP
# addresses but cannot pass downlink data — TX packets egress, zero replies arrive.
# See https://forum.openwrt.org/t/problem-with-bm806u-e1-dwr-921-c3/130094 and
# https://github.com/openwrt/openwrt/issues/6295 (FS#1363). Workaround: configure
# the APN via NVRAM profile (uqmi --modify-profile, done by qmi.sh) and reference
# the profile via --start-network --profile, NOT --apn. qmi.sh already supports
# passing --profile when UCI option 'profile' is set — and 'apn' is kept because
# qmi.sh's --modify-profile call (line 314) still needs it to write the profile.
# qmi.sh only writes profile 1; profile 2 (used for the IPv6 v6apn) is created by
# the wwan role task.
#
# The BM806C also requires raw-ip framing (kernel qmi_wwan driver mode) to
# work properly. qmi.sh defaults to 802.3 mode; a patch in the wwan role task
# changes this to raw-ip for our setup.
config interface 'wwan'
option device '/dev/cdc-wdm0'
option proto 'qmi'
option apn 'internet'
option v6apn 'internetipv6'
option profile '1'
option v6profile '2'
option auth 'pap'
option username 'internet'
option password 'internet'
option pdptype 'ipv4v6'
option dhcp '0'
option dhcpv6 '0'
option metric '100'
option auto '0'
config interface 'uplink'
option device 'eth0.6'
option proto 'static'
+105
View File
@@ -0,0 +1,105 @@
---
# Configures the embedded BroadMobi BM806C LTE modem for QMI use.
#
# Two workarounds are applied here for documented bugs in this modem's firmware
# (M1.2.0_E1.0.1_A1.1.8, no public updates available):
#
# 1. raw-ip framing. The modem advertises 802.3 support but the 802.3 firmware
# path is buggy — downlink frames don't reach the host. raw-ip works.
# See bmork's kernel commit "net: qmi_wwan: support 'raw IP' mode":
# 'newer generations of QMI hardware and firmware have moved towards
# defaulting to raw IP mode instead, followed by an increasing number of
# bugs in the already buggy 802.3 firmware implementation'.
# qmi.sh hardcodes 802.3; we patch it in-place to use raw-ip.
#
# 2. --profile instead of --apn on --start-network. With --apn, the modem
# establishes a phantom bearer that has no working downlink data path.
# With --profile referencing a pre-configured NVRAM profile, data flows.
# See https://forum.openwrt.org/t/problem-with-bm806u-e1-dwr-921-c3/130094
# and https://github.com/openwrt/openwrt/issues/6295 (FS#1363).
# qmi.sh already supports the 'profile' UCI option; we set it in the
# wwan interface config (profile=1 for IPv4, v6profile=2 for IPv6).
# qmi.sh's --modify-profile call writes profile 1; profile 2 is bootstrapped
# here for the IPv6-specific APN.
# Patch qmi.sh to request raw-ip framing instead of 802.3. Two distinct uqmi
# calls in the upstream proto handler request the data format — both must be
# patched for the readback to return raw-ip and trigger the kernel driver's
# sysfs raw_ip=Y flip. Idempotent: lineinfile is a no-op once the patterns
# no longer match.
# Diff output is suppressed for these tasks: the full qmi.sh file (~13 KB) is
# included in both before/after diff payloads, and ansible's SSH stdout parser
# truncates the resulting JSON, causing 'Module result deserialization failed:
# No end of json char found'. The change is self-evident from the task name.
- name: Patch qmi.sh — set-data-format to raw-ip
community.openwrt.lineinfile:
path: /lib/netifd/proto/qmi.sh
regex: '^(\s*uqmi .* --set-data-format) 802\.3(.*)$'
line: '\1 raw-ip\2'
backrefs: true
diff: false
- name: Patch qmi.sh — wda-set-data-format to raw-ip
community.openwrt.lineinfile:
path: /lib/netifd/proto/qmi.sh
regex: '^(\s*uqmi .* --wda-set-data-format) 802\.3(.*)$'
line: '\1 raw-ip\2'
backrefs: true
diff: false
# The kernel rejects writes to /sys/class/net/wwan0/qmi/raw_ip while the netdev
# is up: 'Cannot change a running device' (-EBUSY). qmi.sh tries to flip it
# while the interface is up — this works in 802.3 mode (no-op when already N),
# but with our raw-ip patch above, the flip is mandatory and must succeed.
# We bracket the sysfs write with ip link down/up.
- name: Patch qmi.sh — bracket raw_ip sysfs write with ip link down/up
community.openwrt.lineinfile:
path: /lib/netifd/proto/qmi.sh
regex: '^(\s*)echo "Y" > /sys/class/net/\$ifname/qmi/raw_ip$'
line: '\1ip link set $ifname down; echo "Y" > /sys/class/net/$ifname/qmi/raw_ip; ip link set $ifname up'
backrefs: true
diff: false
# Profile 2 in modem NVRAM holds the IPv6 APN. qmi.sh only manages profile 1
# (the v4 APN via --modify-profile, line 314); profile 2 is our responsibility.
# These steps are skipped if the modem isn't enumerated yet (fresh boot before
# usb-modeswitch completes, or modem in a fault state).
- name: Check if QMI device is available
community.openwrt.stat:
path: /dev/cdc-wdm0
register: wwan_cdc_wdm
- name: Query QMI profile list
community.openwrt.command:
cmd: uqmi -t 3000 -d /dev/cdc-wdm0 --get-profile-list 3gpp
register: wwan_profile_list
changed_when: false
failed_when: false
when: wwan_cdc_wdm.stat.exists | default(false)
- name: Configure IPv6 APN profile 2
when:
- wwan_cdc_wdm.stat.exists | default(false)
- wwan_profile_list.rc | default(1) == 0
- wwan_profile_list.stdout | default('') | trim | length > 0
- wwan_profile_list.stdout | default('') | trim is match('^\\{')
block:
- name: Parse profile indexes
ansible.builtin.set_fact:
wwan_profile_indexes: >-
{{ (wwan_profile_list.stdout | from_json).profiles
| default([]) | map(attribute='index') | list }}
- name: Create profile 2 for IPv6 APN if missing
community.openwrt.command:
cmd: uqmi -t 3000 -d /dev/cdc-wdm0 --create-profile 3gpp --apn internetipv6 --pdp-type ipv6
when: 2 not in wwan_profile_indexes
# --modify-profile is idempotent at the modem level. We can't detect
# whether values changed (uqmi doesn't return diff info), so we always
# report 'ok' (changed_when: false) to keep play output clean. The cost
# of always calling this is one QMI roundtrip.
- name: Ensure profile 2 settings are current
community.openwrt.command:
cmd: uqmi -t 3000 -d /dev/cdc-wdm0 --modify-profile 3gpp,2 --apn internetipv6 --pdp-type ipv6
changed_when: false
+1
View File
@@ -7,3 +7,4 @@ openbao_fields:
iot_wifi:
path: openwrt_iot_wifi
password_key: password