EN

How the nodes are built: Dell DW5821e-eSIM and DW5829e-eSIM on Linux

Every Freedom Checker node is an ordinary Linux box with a wired connection and a Dell LTE modem whose built-in eUICC (eSIM) holds the profiles of several operators. Switching between operators is done in software: the node enables another eSIM profile, brings up a data session, moves it into its own network namespace and runs the checks from there.

Everything below has been in production since May 2026 and is published in the open repository dell-dw5821e-dw5829e-esim-linux (project page): scripts, udev rules, systemd units and a write-up of every failure mode. No Dell firmware or proprietary binaries are redistributed — the kernel already has the drivers; the repository is the userspace that was missing. This page is a digest; the details and the current scripts live in the repository.

The two modems

Both are Qualcomm Snapdragon X20 modules with a built-in eUICC and no physical SIM slot. They differ in how Linux talks to the eUICC and how the data session is brought up.

DW5829e-eSIM · DW5821e-eSIM
USB ID413c:81e4 · 413c:81e0 (81d7 is the non-eSIM DW5821e)
ModuleSnapdragon X20 · Snapdragon X20, Foxconn T77W968, firmware T77W968.F1.0.0.5.2.GC.013
Kernel driversqmi_wwan, cdc_mbim, option — both
USB configurationDW5829e must be in configuration 1 (QMI), default is 2 (MBIM) — one udev rule · DW5821e runs in its default configuration
eUICC accessQMI via /dev/cdc-wdm0 and qmi-proxy, UIM slot 2 · AT+CSIM over the AT serial port
Data pathQMI WDS, wwan0 in raw-IP mode · pppd + ATD*99# over the AT port
Profile switchlpac enable → DMS reset (qmicli --dms-set-operating-mode=reset) · lpac enable → AT+CFUN=0 / AT+CFUN=1
Tested onUbuntu 24.04, kernel 6.8, libqmi 1.36, libmbim 1.32, pppd 2.4.9

eSIM profiles: lpac

Profiles are managed with lpac, an open-source LPA client (AGPL-3.0, not included in the repository). It talks to the eUICC through an APDU backend and to the operator's SM-DP+ through an HTTP backend (curl). The two modems need two builds: for the DW5829e the qmi backend behind qmi-proxy, so the control device can be shared with qmicli; for the DW5821e the at_csim backend on the AT port, which must be free of pppd at that moment.

export LPAC_APDU=at_csim LPAC_APDU_AT_DEVICE=/dev/dw5821e-at LPAC_HTTP=curl
lpac profile list
lpac profile download -s <SM-DP+ host> -m <matching id>
lpac profile enable <AID>
lpac notification process -a

A profile is identified by its ISD-P AID (a0000005591010ffffffff89000011xx), never by ICCID; a node's uplink declaration is a list of "AID → APN" pairs. Activation codes and matching IDs stay out of version control: a matching ID is a one-time credential for downloading the profile. The first thing to do on either machine is to disable and mask ModemManager: it grabs the QMI device and the AT ports the moment the modem enumerates, and lpac then fails in ways that look like eUICC faults.

The full operator-switch cycle

Enabling another profile is not enough — the modem has to re-read the eUICC. Each modem has its own ritual, and both are spelled out in full because any skipped step produces a failure that looks like a carrier problem.

DW5829e (QMI), 40–60 seconds

  1. stop the WDS session;
  2. USB unbind/rebind so nothing holds the control device;
  3. lpac profile disable the current one, enable the target;
  4. DMS reset — the modem re-enumerates on USB; wait ≈30 s instead of polling a device node that is about to disappear, and do not call lpac while the reset is in flight;
  5. rebind again, then start the data session;
  6. routing and the network namespace.

DW5821e (PPP), ≈80 seconds

  1. stop pppd, free the AT port, close stale logical channels on the eUICC;
  2. check AT+CFUN? before touching lpac. If a previous cycle died between CFUN=0 and CFUN=1, the radio is off, the eUICC is unpowered and lpac returns nothing; the script sends AT+CFUN=1 and waits for +CPIN: READY;
  3. lpac profile list → disable current → enable target;
  4. AT+CFUN=0, two seconds, AT+CFUN=1; poll AT+CREG? for network registration;
  5. verify the APN (esim-ensure-apn.sh) — see the pitfalls below;
  6. generate the PPP peer and chatscript for the APN, pppd call, wait for ppp0 to get an address, routing table 100 plus rule, namespace.

The data session

QMI WDS. wwan0 is put into raw-IP mode, qmicli --wds-start-network="apn=…,ip-type=4" starts the session and --wds-get-current-settings returns the address, gateway, DNS and MTU. The default route through the modem goes into a dedicated routing table (we use 100) with an ip rule from <wwan address> lookup 100, never into main: the box keeps its wired default. The session state is written to a file under /run, where the other scripts and the namespace helper find it.

PPP. The peer file and chatscript are regenerated on every bring-up: a hand-edited peer with a wrong device path once sent the recovery ladder through seven escalations for nothing. Could not determine remote IP address: defaulting to 10.64.64.64 and IPV6CP: timeout sending Config-Requests in the pppd log are both harmless; PAP succeeds with empty credentials on the Russian operators we use. The modem's serial ports get stable names from udev by USB interface number — /dev/dw5821e-at (AT commands and PPP), /dev/dw5821e-at2 (a second AT port for diagnostics while PPP holds the first), -gps, -dm — so scripts never guess a ttyUSB.

Isolation: one namespace per uplink

The helper creates namespace esim with a veth pair, a default route through the host side, SNAT on the host to the modem address and its own resolv.conf. Anything run with ip netns exec esim … uses the mobile uplink and nothing else — the clean way to measure "what does this operator pass" without touching the host's routing.

The rule we paid for: measure from the namespace or do not measure. If a switch fails and the measurement runs anyway, it silently runs over the wired uplink and gets recorded under the operator's name. So a failed switch must raise, and the namespace is checked before a mobile task is dispatched. Before that rule we found weeks of "mobile" successes that were wire.

Recovery

Mobile operators drop idle sessions (NAT, PDP expiry), and the node has to notice on its own. A health probe makes an HTTPS request from inside the namespace every 60 seconds with an 8-second timeout; three consecutive failures start the ladder: bearer refresh → profile re-switch → modem reset, with per-rung cooldowns of 60 s / 5 min / 15 min, at most three modem resets per hour and then an hour of silence. While an uplink is recovering, no work is dispatched on it. "Modem absent from USB" counts only after four checks over ~30 seconds — a single check lands inside the modem's own rebind window.

With a carrier-side block the ladder cannot help and will reset the modem three times an hour indefinitely; such an uplink has to be capped or disabled.

Pitfalls that cost weeks

  • ModemManager. Grabs /dev/cdc-wdm0 and the AT ports on enumeration. Disable and mask it; both modems run fine without it.
  • The DW5829e enumerates as MBIM. Configuration 2 by default, the QMI drivers need 1. One udev line.
  • Radio left off after an interrupted CFUN=0/1 (DW5821e). +CFUN: 0, lpac profile list is empty, AT+CPIN? → +CME ERROR: 13, the chatscript aborts on ATH → ERROR. Because the documented CFUN cycle sits after lpac, it is never reached and the node loops on "profile not found" forever — ours did for 30 hours. Fixed by checking AT+CFUN? before lpac. Rate-limit CFUN cycles: 170 of them in two hours once put the firmware into a state where the modem stayed on the bus but answered nothing.
  • LTE attach with the previous operator's APN (DW5821e). The modem rejects AT+CGDCONT while CFUN=0 and, at CFUN=1, attaches with whatever APN context 1 held before. The classic chatscript sets CGDCONT=1 only right before ATD*99#, after the attach, and the bearer keeps the stale APN. From outside it looks like a carrier problem: registered, PDP active, IP assigned, zero payload — one operator black-holes everything, one answers with RST, one redirects every HTTP request to its "wrong APN / balance" portal. Diagnose with AT+CGCONTRDP=1 on the second AT port. The fix is esim-ensure-apn.sh: CGDCONT=1 with the radio on, compare with the active context, AT+CGATT=0 / AT+CGATT=1 on mismatch (≈14 s). The bug hid for months because switches to the other operators kept failing at lpac and the working operator's APN stayed in place; the first successful foreign switch broke every operator at once.
  • A "bearer refresh" that is really a USB unbind/rebind (DW5829e). The modem vanishes from /dev for 10–20 seconds. A nervous ladder tuned to "one failed 4-second probe every 20 s → refresh" produced exactly that: tasks in flight died in the rebind window, the next probe hit the next window, operator switches could not complete inside their timeout, and a one-shot "is the modem on the bus?" check landed inside a rebind and opened a one-hour circuit breaker on a healthy modem. Settled values: 60 s / 8 s / three consecutive failures.
  • Carrier-side subscriber states look exactly like modem faults. +CEREG: 0,1, +CGACT: 1,1, IP assigned, PPP up, zero bytes. Before touching the modem: fetch the operator's own portal from inside the namespace — if login.<operator> or balance.<operator> answers while the outside world does not, the subscriber is in a walled state (cooling period, captcha, suspended service) and the modem is innocent; check the APN with +CGCONTRDP=1; read the SMS inbox (AT+CMGL="ALL") — operators often text the reason; try a USSD balance request (a +CUSD: 5 refusal is itself a hint); the same operators working on another node with other SIMs rules out an operator-wide outage.
  • Small things that cost a deploy each. A stale /var/lock/LCK..ttyUSB0 after a dead pppd fails the next dial; lpac left holding the AT port starves pppd; a peer file with the old device path after the udev rename gives "unrecognized option".

What is in the repository

docs/one document per modem (QMI and PPP), a note on building lpac for both backends and the RPATH layout, and pitfalls.md — every failure: how it looks from outside and what fixed it
scripts/dw5829e-qmi/profile switch, bearer up/down/refresh, bootstrap for the QMI modem
scripts/dw5821e-ppp/the same for the PPP modem plus esim-ensure-apn.sh and the peer template
scripts/common/per-uplink network namespace with SNAT, lpac symlink setup, profile download helper
udev/, systemd/stable port names for the DW5821e, the USB configuration pin for the DW5829e, one-shot units that recreate the lpac layout at boot
examples/how uplinks (profiles × APNs) are declared for the bootstrap script

Scripts and documentation are MIT-licensed. Contributions are welcome, especially other Snapdragon X20 modules (the Foxconn T77W968 appears under several OEM names) and the DW5821e in MBIM mode, which we did not need and did not test.

The data these nodes collect is open: the open data package with a daily archive, field schema and a CC BY 4.0 licence.

← How we measure · Back to the availability table