A GitHub Actions pipeline that builds an OpenWrt image for every IPQ807x
device in the target — all 39 of them: Qualcomm NSS hardware offload running
on OpenWrt main's upstream qca_edma / qca_ppe ethernet drivers
(PR #22381) — not the vendor
qca-nss-dp / qca-ssdk stack every other NSS build uses. Built from
openwrt-nss-edma and
nss-packages.
New to NSS offload? The wiki page NSS Offload Explained covers the concept from the ground up. Architecture, runtime model and measured results are in the full wiki.
Every release carries one sysupgrade image per device, named after its OpenWrt
profile id. Grab ...-<your device>-squashfs-sysupgrade.bin from the newest
edma-nss-*
release
and flash it:
sysupgrade -n /tmp/openwrt-qualcommax-ipq807x-xiaomi_ax3600-squashfs-sysupgrade.binOr via LuCI: System → Backup / Flash Firmware, upload, uncheck "Keep settings" for a first-time flash.
Each build also publishes an edma-nss-mesh-* release with the same images
built for 802.11s mesh offload. Mesh interfaces are only accepted by NSS
firmware 11.4.0.5, the last line that supports them, so that release carries
11.4 in place of the 12.5 firmware the default images ship. Take it if you run
802.11s; take edma-nss-* otherwise.
These are sysupgrade images only — there is no factory image. You must already be running OpenWrt on the device; if you are on stock vendor firmware, install stock OpenWrt first (your device's OpenWrt device page covers that), then flash this over it.
Devices built — all 39 in qualcommax/ipq807x
The split is the ath11k memory profile, which is compile-time and image-wide, so each group of boards that shares a profile shares a build.
| Group | RAM | Devices |
|---|---|---|
xiaomi_ax3600 |
512 MB | Xiaomi AX3600 (own build: adds the board's wireless defaults and SQM template) |
ipq807x-1g |
1 GB+ | Aliyun AP8220, Arcadyan AW1000, Asus RT-AX89X, Buffalo WXR-5950AX12, Dynalink DL-WRX36, Edgecore EAP102, Linksys HomeWRK, Linksys MX4200 v2, Linksys MX4300, Linksys MX5300, Linksys MX8500, Netgear RAX120v2, Netgear RBR750, Netgear RBS750, Netgear SXR80, Netgear SXS80, Netgear WAX620, Netgear WAX630, prpl Haze, QNAP 301w, Spectrum SAX1V1K, TCL LINKHUB HH500V, TP-Link Deco X80-5G, TP-Link EAP620 HD v1, TP-Link EAP660 HD v1, Xiaomi AX9000, Yuncore AX880, Zbtlink ZBT-Z800AX, Zyxel NBG7815, Zyxel NWA110AX, Zyxel NWA210AX |
ipq807x-512m |
512 MB | CMCC RM2-6, Compex WPQ873, Edimax CAX1800, Linksys MX4200 v1, Redmi AX6, ZTE MF269 |
ipq807x-256m |
256 MB | Netgear WAX218 |
The AX3600 is the board every change is validated on; the rest carry the same data path and the same NSS device-tree nodes, and are built so a bug report starts from a known image instead of a hand-rolled config.
Runtime model: the nss service (/etc/init.d/nss) makes the data-path
decision once, early in boot: it arms the NSS data plane, boots the firmware,
and loads Wi-Fi with offload already selected — the radios come up directly
on the wifili path, with no later rebind. Its /usr/sbin/nss-up instance
then layers ECM (and SQM, once you have configured it) on top as the network
comes up; output lands in the system log (logread -e nss). If arming or
the Wi-Fi offload registration fails, the boot falls back to host-mode Wi-Fi
and the host stack on its own. The universal recovery path is the uci
flag: uci set nss.general.enabled='0'; uci commit nss (survives
sysupgrade) — with it set, every boot is a stock host-only system.
Check plane health any time with nss-status over ssh, or in LuCI under
Status → NSS Offload.
Wi-Fi ships disabled, like any OpenWrt image with no factory credentials — no image can carry a working password without publishing it here. Connect a cable, set an SSID and key in LuCI → Network → Wireless, and enable the radios. On the AX3600 the radio paths, band, channel and 802.11k/v options come preconfigured, so there is no first-boot detection race; the other images use OpenWrt's own first-boot radio detection.
The edma-nss image enables the full offload stack plus a hardened, batteries-
included desktop-router config:
| Area | What's on |
|---|---|
| NSS data plane | kmod-qca-nss-drv + the kmod-qca-ppe-nss glue |
| Connection offload | ECM (kmod-qca-nss-ecm), PPPoE manager (kmod-qca-nss-drv-pppoe) — IPv4 NAT, IPv6 routing, PPPoE-over-VLAN |
| Bridge offload | kmod-qca-nss-drv-bridge-mgr — wired LAN bridging in hardware |
| Multicast | kmod-qca-mcs — same-subnet multicast hardware-bridged to snooped members |
| SQM | NSS qdiscs (-qdisc/-igs) + sqm-scripts-nss (nss-edma.qos, DSCP fast lane both directions) + luci-app-sqm. Ships as a disabled template: set download/upload to ~90-95 % of your measured line rate and enable it (LuCI Network → SQM or uci) — there is no safe universal default rate |
| QoS marking | nssqos + luci-app-nssqos — DSCP marking & fast-lane prioritization rules (CLI /etc/config/nssqos, LuCI Network → QoS Marking (NSS)), effective on accelerated flows; the applied class shows per flow in the DSCP column of Status → Realtime → Connections |
| Wi-Fi | ath11k NSS offload (wifili) on both radios (CONFIG_ATH11K_NSS_SUPPORT); radio paths, band, channel and 802.11k/v preconfigured, interfaces ship disabled with no key — set SSID/key over the LAN port and enable them |
| Diagnostics | nss-status CLI health report (now incl. fast-lane counters) + LuCI Status → NSS Offload page + per-station firmware Wi-Fi counters (/sys/kernel/debug/ieee80211/phy*/netdev:*/stations/<mac>/nss_stats: A-MSDU aggregation, MPDU retries) |
| Firmware/profile | NSS.FW.12.5-210-HK.R; NSS memory profile matched to the board's RAM (HIGH / MEDIUM / LOW) |
| Security | OpenSSH only (post-quantum KEX, AEAD/ETM, RSA ≥ 3072), PKG_* hardening (ASLR/PIE, stack protector, FORTIFY_3, RELRO, seccomp), WAN DROP + BCP38, HTTPS redirect, OQS provider in OpenSSL |
| Toolchain | GCC 15 + Graphite, Binutils 2.46, Mold linker, LTO, -mcpu=cortex-a53+crc+crypto; ccache off |
| Userland | LuCI (SSL), htop, iperf3, curl, BBR |
Toolchain and package pins live in
devices/common/config, shared by every image;
devices/<group>/config adds only the device list and the memory profiles.
These are build-verified and wired in code, but off by default because the
reference network does not use them. Add the package to
devices/common/config and rebuild:
| Feature | Add to config | Notes |
|---|---|---|
| Routed L3 multicast (IPTV WAN→LAN) | CONFIG_PACKAGE_igmpproxy=y (or smcroute) |
ECM offloads each kernel MFC entry to the PPE; needs a real WAN multicast source and a two-VIF topology. See docs/CUSTOMIZE.md. |
| MAP-T / 464XLAT | CONFIG_PACKAGE_kmod-nat46=y |
nat46 headers + QCA MAP-T exports are staged in the tree. |
| VXLAN | CONFIG_PACKAGE_kmod-vxlan=y |
fdb/age-update offload via kernel patch 0972. |
| MACVLAN | CONFIG_PACKAGE_kmod-macvlan=y |
ECM support via kernel patch 0962. |
| GRE | CONFIG_PACKAGE_kmod-gre=y |
ECM GRE support builds. |
Not available on this platform/firmware: IPsec (ESP) offload, TLS/DTLS, and
CoDel ECN marking. (Wi-Fi mesh offload works on an 11.4-firmware build —
NSS_FIRMWARE_VERSION_11_4 + ATH11K_NSS_MESH_SUPPORT; only the default 12.5
firmware blocks it.) See the
Limitations
page.
AX3600 (IPQ8071A, 512 MB), NSS.FW.12.5-210, kernel 6.18 — details in the
wiki:
| Metric | Host path | NSS offload |
|---|---|---|
| 311 Mbit/s PPPoE NAT | ~42 % of one core (softirq) | ~99.7 % CPU idle |
| SQM at 285 Mbit ingress | CPU-bound | 258 Mbit goodput, ~99 % idle |
| RTT under shaped load | bufferbloat | 16 ms avg vs 20 idle — flat |
| Wi-Fi data path | mac80211/ath11k on the CPU | wifili on the NSS cores |
Everything is parameterized in the env: block of
.github/workflows/build.yml — fork the repo,
edit, and the pipeline builds on push. Or build locally:
git clone --branch nss-edma-rework https://github.com/JuliusBairaktaris/openwrt-nss-edma openwrt
cd openwrt
cp feeds.conf.default feeds.conf
echo "src-git nss https://github.com/JuliusBairaktaris/nss-packages.git;edma-nss" >> feeds.conf
./scripts/feeds update -a && ./scripts/feeds install -a
B=../Qualcommax_NSS_Builder/devices
cat "$B/common/config" "$B/xiaomi_ax3600/config" > .config # or any devices/<group>
make defconfig && make -j"$(nproc)"Important
Updating an existing checkout: both branches (nss-edma-rework and the
edma-nss feed) are periodically rebased — fixes are folded into the
commits that own them, so history gets rewritten. A plain git pull will
fail or produce a broken merge, and ./scripts/feeds update (which runs
git pull --ff-only inside feeds/nss) fails quietly and leaves the feed
stale — a common source of build errors that don't reproduce upstream.
Update like this instead:
git fetch origin && git reset --hard origin/nss-edma-rework
rm -rf feeds/nss package/feeds/nss
./scripts/feeds update -a && ./scripts/feeds install -a
make defconfigBefore reporting a build error, verify you are current: git log --oneline -1
in the tree and in feeds/nss must match the tips of
nss-edma-rework
and edma-nss.
The NSS runtime tools (nss-up, nss-status, the nss boot service, the
QoS marking CLI/UI) ship as regular packages from the openwrt fork
(nss-tools, nssqos, luci-app-nss, luci-app-nssqos) — plain fork
checkouts get them by selecting the packages, no builder needed. The few
remaining overlay files (SSH config and QoL defaults in devices/common/files*/,
the AX3600's wireless defaults and SQM template in devices/xiaomi_ax3600/files*/)
are copied into the image with a
files/ directory or the builder pipeline. See docs/CUSTOMIZE.md
for the full customization guide and docs/ARCHITECTURE.md
for how the pipeline works.
devices/common/ # shared by every image
config # the .config bulk (toolchain, hardening, NSS packages)
files/ # base rootfs overlay (sshd_config, QoL uci-defaults)
files.edma-nss/ # edma-nss overlay (rc.local)
devices/xiaomi_ax3600/ # one directory per build: config + optional overlays
devices/ipq807x-{1g,512m,256m}/
scripts/ # check-updates, prepare-build, prune-releases (tested, linted)
docs/ # CUSTOMIZE.md, ARCHITECTURE.md
.github/workflows/ # build.yml (check → release → build → publish → prune), lint.yml
Each devices/<name>/config is concatenated onto devices/common/config and
resolved with make defconfig, which is verified to have kept every requested
symbol — a silently reduced image fails the build instead of shipping.
The pipeline runs check → release → build → publish → prune: check resolves
the upstream/NSS ref to a SHA and skips a scheduled build when nothing changed;
release opens a draft release; build runs once per device group in
parallel, applying the config + overlays, compiling, and uploading its images
into that draft; publish makes it public once every group succeeded, and
discards it otherwise, so a release is never missing devices; prune keeps the
newest KEEP releases. Builds are uncached (fresh runner, reproducible
SOURCE_DATE_EPOCH) and the pipeline is linted (actionlint, shellcheck,
yamllint) on every PR.
Issues and PRs welcome — see CONTRIBUTING.md.
- Ansuel (Christian Marangi) — the EDMA rework this stack builds on
- qosmio — NSS development, the openwrt-ipq tree, and the Wi-Fi offload patch lineage
- rodriguezst — original ipq807x-openwrt-builder
- OpenWrt community — the IPQ807x NSS Build thread
This is an unpaid, single-maintainer effort. If this work is useful to you, consider chipping in — it goes toward IPQ807x development and hardware to start looking into IPQ50xx and IPQ60xx next.
- GitHub Sponsors — zero-fee, GitHub-native
- PayPal — one-off donations
Thank you!
GPL-2.0, consistent with OpenWrt.