fix(ansible): resolve LTE failover data-plane bug on BroadMobi BM806C
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
@@ -7,3 +7,4 @@ openbao_fields:
|
||||
iot_wifi:
|
||||
path: openwrt_iot_wifi
|
||||
password_key: password
|
||||
|
||||
|
||||
Reference in New Issue
Block a user