Flash OpenWrt onto any router.

conwrt is a framework for flashing routers with OpenWrt — AI-assisted discovery for new devices, automated flashing for known ones. 21 devices supported across 7 vendors.

Deployment Wizard → Quick Start ↓ View on GitHub

Quick Start

From clone to flash in three commands.

1

Install

git clone https://github.com/amperstrand/conwrt && cd conwrt && python3 -m venv .venv && source .venv/bin/activate && python -m pip install -e '.[dev]'
2

Connect & Detect

# Plug ethernet to your router, then: python3 scripts/conwrt.py probe # Or auto-detect: python3 scripts/conwrt.py auto
3

Flash

python3 scripts/conwrt.py flash --model-id dlink-covr-x1860-a1 --request-image --wan-ssh

Or use the interactive Wizard → to pick your router and get the exact commands.

What conwrt can do

Six core capabilities that make router flashing reproducible.

Auto-Detect Devices

Probe any ethernet interface to identify connected routers via SSH fingerprint, MAC OUI, or boot-state detection. No manual IP guessing.

21 Supported Devices

D-Link, GL.iNet, Linksys, Ubiquiti, ZyXEL, ASUS, Extreme Networks. Each with tested flash methods and boot signatures.

One-Command Flash

conwrt flash handles the full lifecycle: detect recovery mode → upload firmware → verify SHA-256 → confirm SSH → collect inventory.

21 Pre-configured Presets

USB tethering, SQM bufferbloat fix, WireGuard VPN, AdGuard, mesh networking, travelmate captive portal — all baked into the firmware image.

Custom ASU Builds

Request firmware from the OpenWrt ASU builder with your SSH key, password, WiFi config, and use cases baked in. Flash and forget.

Router-to-Router

Install conwrt on an OpenWrt router to flash other routers. Multicast flash (zycast) for provisioning many devices at once.

Who uses conwrt?

Real-world deployment scenarios.

The Traveler

USB tether + VPN + SQM on a pocket-sized GL.iNet Beryl AX.

Read full story →

The Homelab Operator

AdGuard + guest WiFi + WireGuard remote access on a Linksys MX4200.

Read full story →

The Mesh Builder

802.11s mesh network from repurposed consumer routers.

Read full story →

The Developer

Python API, model definitions, CLI scripting for full infrastructure-as-code.

Read full story →

The Device Rescuer

Recovery-mode flashing for bricked or stock-firmware-locked routers.

Read full story →
tested Verified on real hardware
defined Model defined, not yet tested
blocked Known to not work

Flash Methods

Vendor Device SoC RAM Flash oem
http
sysupgrade recovery
http
uboot
http
dlink
hnap
edgeos
kernel
swap
tftp serial
tftp
openwrt
mtd
write
zycast Caps
ASUS Lyra MAP-AC2200 tri-band WiFi mesh node (IPQ4019 + QCA9886)
asus-lyra-map-ac2200
Qualcomm IPQ4019 (quad-core Cortex-A7) @ 717MHz 256MB DDR3 (Nanya NT5CC128M16IP-DI) NAND 128MB (Macronix) defined ethernet wifi
COMFAST CF-WR632AX WiFi 6 travel router (MT7981B Filogic 830, 2.5G WAN)
comfast-cf-wr632ax
MT7981B (Filogic 830) dual-core Cortex-A53 @ 1.3GHz 512MB DDR3 SPI-NAND 128MB (Winbond W25N01GV) defined defined defined ethernet wifi usb
D-Link COVR-X1860 A1 mesh WiFi
dlink-covr-x1860-a1
MT7621AT 256MB NAND 128MB tested tested blocked ethernet wifi
Extreme Networks WS-AP3915i / AP391x Qualcomm IPQ4029 access point — no-serial TFTP flash via rdwr_boot_cfg
extreme-networks-ws-ap3915i
Qualcomm IPQ4029 (4x Cortex-A7 @ 717 MHz) 512MB DDR3 SPI-NOR 32MB (Macronix MX25L25635E, verified from DTS and hardware) tested tested ethernet
GL.iNet AR150 Mini Router (all variants)
glinet-gl-ar150
AR9331 (MIPS 24Kc) @ 400MHz 64MB DDR2 NOR 16MB defined ethernet wifi usb
GL.iNet AR300M-Lite / AR300M16 / AR300M16-Ext (NOR only, no switch, no NAND)
glinet-gl-ar300m-lite
QCA9533 v2 (MIPS 24Kc) @ 650MHz 128MB DDR2 NOR 16MB (Winbond W25Q128) only — no NAND defined defined ethernet wifi usb
GL.iNet AR300M / AR300M-Ext / AR300MD (NAND boot, switch LEFT)
glinet-gl-ar300m-nand
QCA9533 v2 (MIPS 24Kc) @ 650MHz 128MB DDR2 NAND 128MB (GigaDevice SPI NAND) + NOR 16MB (Winbond W25Q128) defined tested ethernet wifi usb
GL.iNet AR300M / AR300M-Ext (NOR boot). NOT recommended for general use — NAND is 128MB vs NOR's 16MB. NOR is useful as automatic failsafe recovery partition. NOR boot via switch RIGHT (bootm 0x9f050000) or bootcount method (fw_setenv bootcount 3). Switch-based NOR boot may not work on all U-Boot versions.
glinet-gl-ar300m-nor
QCA9533 v2 (MIPS 24Kc) @ 650MHz 128MB DDR2 NAND 128MB (GigaDevice SPI NAND) + NOR 16MB (Winbond W25Q128) defined defined ethernet wifi usb
GL.iNet Mudi (GL-E750) portable 4G LTE router — QCA9533 2.4GHz + QCA9887 5GHz, battery powered
glinet-gl-e750
QCA9533 ver 2 rev 0 (MIPS 24Kc V7.4) 128MB NAND 128MB (ubi mtd5) + kernel mtd3 4MB defined ethernet wifi usb
GL.iNet Beryl AX (GL-MT3000) WiFi 6 travel router
glinet-mt3000
MT7981B (Filogic 820) dual-core Cortex-A53 @ 1.3GHz 512MB DDR4 NAND 256MB tested tested ethernet wifi usb
GL.iNet Flint 2 (GL-MT6000) WiFi 6 router
glinet-mt6000
MT7986A (Filogic 830) quad-core Cortex-A53 @ 1.8GHz 1024MB DDR4 NAND 1024MB defined defined ethernet wifi usb
Linksys Velop AX4200 WiFi 6 System V1 — CVE-2019-16340 auth bypass for initial flash
linksys-mx4200-v1
IPQ8174 (quad-core Cortex-A53) @ 1.0GHz 512MB DDR3 NAND 256MB defined ethernet wifi
Linksys Velop Tri-Band V1 (WHW03) — U-Boot HTTP recovery signatures needed
linksys-whw03-v1
IPQ4019 (quad-core Cortex-A7) @ 716MHz 512MB DDR3 eMMC 4GB defined ethernet wifi
Linksys Velop Tri-Band V2 (WHW03 V2) — U-Boot HTTP recovery signatures needed
linksys-whw03-v2
IPQ4019 (quad-core Cortex-A7) @ 716MHz 512MB DDR3 NAND 512MB defined ethernet wifi
Raspberry Pi Ltd Raspberry Pi 4 Model B (BCM2711) single-board computer, 1-8GB RAM, gigabit ethernet, USB3, microSD3.0. Runs OpenWrt, Ubuntu Server, Raspberry Pi OS. Boots from SD, USB mass storage, or network (EEPROM bootloader).
raspberrypi-4b
Broadcom BCM2711 quad-core Cortex-A72 @ 1.5GHz (BCM2711C0) 1/2/4/8 GB LPDDR4 SDRAM ethernet wifi usb
Ubiquiti EdgeRouter 6P (ER-e300) — Cavium Octeon III, 5x GbE + SFP, no WiFi
ubnt-edgerouter-6p
Cavium CN7130 (Octeon III) 1024MB eMMC 4GB + SPI NOR 8MB defined tested defined ethernet usb
Ubiquiti EdgeRouter X (ER-X) — MediaTek MT7621A, 5x GbE, no WiFi
ubnt-edgerouter-x
MediaTek MT7621A (MIPS) 256MB DDR3 256MB SPI NAND defined ethernet
QEMU Virtual x86_64 OpenWrt instance (QEMU/KVM) for CI testing
virtual-x86-64
virtual defined ethernet
Zyxel EX5700 (Telenor) — MediaTek MT7986B, WiFi 6 tri-band
zyxel-ex5700-telenor
MT7986B (Filogic 830) quad-core Cortex-A53 1GB DDR3 SPI NAND ~486MB (UBI) defined ethernet wifi
Zyxel GS1900-24E 24-port managed gigabit switch, Realtek RTL8382M
zyxel-gs1900-24e
Realtek RTL8382M MIPS 4KEc @ 500MHz 128 MiB DDR2 SDRAM 16 MiB SPI NOR (Macronix MX25L12835F) tested
bbaf09c
tested
bbaf09c
defined tested
bbaf09c
ethernet
Zyxel GS1900-8HP A1 (formerly v2) 8-port PoE+ managed gigabit switch, Realtek RTL8380M, 70W PoE budget
zyxel-gs1900-8hp-a1
Realtek RTL8380M MIPS 4KEc @ 500MHz 128 MiB DDR3 SDRAM (Samsung K4B1G0846G) 16 MiB SPI NOR (Macronix MX25L12835F) tested tested tested defined ethernet
Zyxel GS1900-8HP B1 (formerly v1) 8-port PoE+ managed gigabit switch, Realtek RTL8380M, 70W PoE budget
zyxel-gs1900-8hp-b1
Realtek RTL8380M MIPS 4KEc @ 500MHz 128 MiB DDR2 SDRAM (Nanya NT5TU128M8HE-AC) 16 MiB SPI NOR (Macronix MX25L12835F) defined defined defined defined ethernet
Zyxel GS1920-24 24-port managed gigabit switch (non-PoE), Realtek RTL8392M
zyxel-gs1920-24
Realtek RTL8392M MIPS 4KEc @ 500MHz 128 MiB DDR2 SDRAM 16 MiB SPI NOR (dual-slot, 8 MiB per slot) defined defined ethernet
Zyxel NR7101 — 5G/NR outdoor router, MediaTek MT7621
zyxel-nr7101
MT7621A (MediaTek) dual-core MIPS 1004Kc @ 880MHz 256MB DDR3 SPI NAND 256MB tested
5b45579
tested ethernet wifi cellular
Zyxel WX5600-T0 (AX6000) — Altibox/Lyse ISP build, MediaTek MT7986B
zyxel-wx5600-t0
MT7986B (Filogic 830) quad-core Cortex-A53 @ 1.6GHz 512MB DDR3 SPI NAND 512MB (Micron MT29F4G01ABAFD, NMBM enabled) defined defined ethernet wifi

Device Testing Log

Per-device tested capabilities with commit links.

Vendor Device Tested Capabilities
ASUS Lyra MAP-AC2200 tri-band WiFi mesh node (IPQ4019 + QCA9886)
asus-lyra-map-ac2200
stock-ssh-mtd-batch1 tested 2026-05-26
stock-ssh-mtd-batch2 tested 2026-05-27
COMFAST CF-WR632AX WiFi 6 travel router (MT7981B Filogic 830, 2.5G WAN)
comfast-cf-wr632ax
stock_webui_openwrt_25_12_5 tested 2026-10-25
D-Link COVR-X1860 A1 mesh WiFi
dlink-covr-x1860-a1
recovery-http tested 2026-05-24
dlink-hnap defined
sysupgrade tested 2026-05-18
recovery-http-25.12.5 tested 2026-10-08
vpn-nordvpn-wireguard tested 2026-10-08
vpn-nordvpn-token-and-vpn-ap tested 2026-10-08
wifi_sta_ap tested 2026-05-15
vpn-nordvpn-openvpn tested 2026-10-08
vpn-nordvpn-ikev2 defined 2026-10-08
Extreme Networks WS-AP3915i / AP391x Qualcomm IPQ4029 access point — no-serial TFTP flash via rdwr_boot_cfg
extreme-networks-ws-ap3915i
extreme-rdwr-tftp-initramfs tested 2026-05-23
sysupgrade tested 2026-09-22
tftp tested 2026-09-21
serial-bridge tested 2026-09-23
GL.iNet AR300M / AR300M-Ext / AR300MD (NAND boot, switch LEFT)
glinet-gl-ar300m-nand
recovery-http tested 2026-05-16
nand_boot tested 2026-05-17
nor_setup tested 2026-05-17
nor_boot_switch_method tested 2026-05-17
nor_boot_bootcount_method tested 2026-05-17
nand_boot_after_nor tested 2026-05-17
tether-android-adb tested 2026-05-17
sqm tested 2026-05-17
GL.iNet AR300M / AR300M-Ext (NOR boot). NOT recommended for general use — NAND is 128MB vs NOR's 16MB. NOR is useful as automatic failsafe recovery partition. NOR boot via switch RIGHT (bootm 0x9f050000) or bootcount method (fw_setenv bootcount 3). Switch-based NOR boot may not work on all U-Boot versions.
glinet-gl-ar300m-nor
nor_boot_bootcount tested 2026-05-17
nor_boot_switch tested 2026-05-17
nand_boot_after_nor tested 2026-05-17
GL.iNet Beryl AX (GL-MT3000) WiFi 6 travel router
glinet-mt3000
uboot-http tested 2026-05-16
sysupgrade tested 2026-05-16
usb_tether_android tested 2026-05-16
Raspberry Pi Ltd Raspberry Pi 4 Model B (BCM2711) single-board computer, 1-8GB RAM, gigabit ethernet, USB3, microSD3.0. Runs OpenWrt, Ubuntu Server, Raspberry Pi OS. Boots from SD, USB mass storage, or network (EEPROM bootloader).
raspberrypi-4b
netboot-os tested 2026-10-10
netboot-rescue tested 2026-10-08
usb-image tested 2026-10-08
eeprom-update-netboot tested 2026-10-08
Ubiquiti EdgeRouter 6P (ER-e300) — Cavium Octeon III, 5x GbE + SFP, no WiFi
ubnt-edgerouter-6p
edgeos-kernel-swap tested 2026-05-19
Zyxel GS1900-24E 24-port managed gigabit switch, Realtek RTL8382M
zyxel-gs1900-24e
oem-http tested @bbaf09c Uploaded initramfs via Playwright, device booted into OpenWrt initramfs in ~90s 2026-05-18
sysupgrade tested @bbaf09c sysupgrade -n completed, SSH access after ~120s boot 2026-05-18
mtd-write tested @bbaf09c mtd -r write succeeded, device booted into 25.12.1 initramfs 2026-05-18
serial-tftp-openwrt defined
uboot_env_modification tested @bbaf09c fw_setenv + fw_printenv confirmed serverip change 2026-05-18
Zyxel GS1900-8HP A1 (formerly v2) 8-port PoE+ managed gigabit switch, Realtek RTL8380M, 70W PoE budget
zyxel-gs1900-8hp-a1
oem-stock-upgrade tested V2.00(AAHI.2) → V2.90(AAHI.0) via Playwright HTTP upload, cmd=5903, partition=0 (Active) 2026-05-21
oem-http tested Uploaded OpenWrt 25.12.1 initramfs via OEM HTTP to V2.90 OEM firmware, device booted into initramfs in ~24s 2026-05-21
sysupgrade tested sysupgrade -n from 25.12.1 initramfs to 25.12.1 squashfs permanent install, SSH access after ~105s boot 2026-05-21
serial-tftp-openwrt tested conwrt flash-rescue (profile-driven, PR #545): BREAK ladder -> rtk network on -> bundled-TFTP tftpboot 5289055 B -> iminfo -> bootm -> initramfs shell -> wget 6554382 B -> on-box sha gate -> sysupgrade -n 2026-10-25
Zyxel NR7101 — 5G/NR outdoor router, MediaTek MT7621
zyxel-nr7101
sysupgrade tested @5b45579 sysupgrade -n 24.10.5→25.12.4, SSH key auth after ~60s boot 2026-05-19
zycast tested Go zycast binary on MIPS OpenWrt switch, NR7101 Telenor (MAC 78:c5:7d:xx:xx:xx — OUI kept, unit redacted #290), PoE power cycle, initramfs-recovery image 2026-05-26
modem_inventory tested @5b45579 AT commands on /dev/ttyUSB2: ATI, AT+GMR, AT+QCCID all responded 2026-05-19
cellular_data tested @b68de06 SIM card (ice+), ping 8.8.8.8 via LTE, 56Mbps down, 43ms latency 2026-05-19
cellular_data_esim tested silent.link 9eSIM (Telia roaming profile), bench cold boot through a PoE switch 2026-11-05
cellular_data_remote_sap tested Android phone SIM over Bluetooth SAP (wwand + wwand-rsim), bench cold boot through a PoE switch 2026-11-05
wifi_ap tested 2026-10-24

Use Case Presets

Name Description Status Requires Packages Commands
adguard Network-wide ad blocking via AdGuard Home (post-flash setup) tested
ops characterization + transport parity
none adguardhome, luci-app-adguardhome
ap-nostr-id Generate a Nostr keypair per access point. Writes npub as hostapd nas_identifier so RADIUS requests self-identify the AP, and saves nsec to /etc/tollgate/ap-nsec for future use. untested ethernet
auto-sqm Auto-measure WAN speed and configure SQM to eliminate bufferbloat tested
Verified on OpenWrt 24.10.2 QEMU (SHC). Config generation + hotplug script.
none sqm-scripts, luci-app-sqm, jsonfilter, iperf3
cashu-vpn-client Cashu VPN client — full-tunnel WireGuard client for the vps-on-demand exit experimental ethernet wireguard-tools, wireguard-kmod, luci-proto-wireguard
configurationwizzard net4sats admin UI + captive portal (Preact). Builds and deploys portal to NDS htdocs, admin to /www/net4sats/, installs rpcd plugin for backend API bridge. Run deploy_configurationwizzard() post-flash to build + deploy. tested
Full E2E on COVR-X1860: captive portal, Lightning payment, internet access
none
doh DNS-over-HTTPS via https-dns-proxy for encrypted DNS resolution tested
ops characterization + transport parity
none https-dns-proxy, luci-app-https-dns-proxy
fips-bluetooth-rfcomm FIPS mesh networking over Bluetooth RFCOMM (star topology with TUN) experimental
ASUS Lyra MAP-AC2200, star topology, RFCOMM transport
bluetooth bluez-libs, bluez-utils, bluez-utils-extra, kmod-tun
fleet_identity Deterministic per-device identity: MAC-derived root password, - hostname, rogue-DHCP disarm. Host twin: scripts/fleet_secrets.py untested
derivation parity unit-tested; on-device application (incl. AP block) pending NR7101 batch — verify unit #1 manually per AGENTS before batch
none requires configuration
guest-wifi Isolated guest WiFi network with separate subnet, DHCP, and firewall zone tested
ops characterization + transport parity
wifi
maas-portable-lab Minimal MAAS for portable home labs. Creates a wireless-uplink MAAS island: - x1860 flashed with OpenWrt, bridged ports, WiFi STA uplink - Pi 4B boots Ubuntu Server from USB SSD - MAAS region+rack controller with DHCP on bench subnet - PXE boot capability for managed servers Serves 1-5 managed machines comfortably on 4GB RAM. untested
Not yet exercised on hardware; scripts reviewed only.
none requires configuration
mesh11sd 802.11s mesh networking with auto-config via mesh11sd tested
ops characterization + transport parity
wifi mesh11sd, ip-full, kmod-nft-bridge, vxlan requires configuration
mptcp-bonding MPTCP connection bonding via BondingShouldBeFree (true aggregation, needs OpenWrt 25.12+ or SNAPSHOT) tested
BSBF server installed on SHC VM 1077 (66.92.204.237). xray active on :16384, client UUID registered. VPS kernel 6.12.90 has CONFIG_MPTCP=y, subflows=8. iperf3 localhost baseline: 28.4 Gbps. OpenWrt 25.12+ has CONFIG_MPTCP=y by default (kernel 6.12.71). OpenWrt 24.10 does NOT have MPTCP (CONFIG_MPTCP=n, kernel 6.6). Verified on SNAPSHOT r35226 (kernel 6.18.37).
none requires configuration
mwan3 Multi-WAN failover or load balancing via mwan3 tested
ops characterization + transport parity. Verified on OpenWrt 24.10.2 with iptables-nft compat in QEMU/KVM. mwan3 status shows 'online and tracking is active'. See pbr use case for nftables-native alternative.
none mwan3, iptables-nft, ip6tables-nft, iptables-mod-conntrack-extra
net-roles Interface role assignment for constrained devices (NR7101 class): ordered uplinks (sim/wifi/ethernet) rendered as netifd metrics for passive failover, optional tagged-VLAN ethernet uplink or mgmt door on the single port, and single-radio STA/AP exclusivity. Stack mwan3 for health-checked failover. Enable AFTER tollgate-cellular (metric pass must run last). experimental
renders validated (multi-mode sh -n); hardware run pending next TollGate build
none
nodns Local DNS cache of nodns records via dnsmasq. Polls the nodns bot zone export API and writes A/AAAA records to /etc/dnsmasq.d/nodns.hosts (dnsmasq addn-hosts). No extra packages required -- uses only curl and sh from base OpenWrt. tested
Verified on OpenWrt 24.10.2 QEMU (SHC). dnsmasq config + DNS resolution.
none
openclash Transparent proxy via Clash/Mihomo for censorship bypass (post-flash config) tested
ops characterization + transport parity
none luci-app-openclash, coreutils-nohup, bash, iptables, dnsmasq-full +4 more
openperf_client WiFi performance test client — discovers openperf AP via SSID, runs test matrix on boot untested wifi python3, iperf3
openperf_server Open WiFi performance testing server (JSON-RPC on port 7777, supports HaHalow + 2.4/5 GHz) untested wifi python3, iperf3
pbr nftables-native policy-based routing (pbr package) tested
nftables-native. Verified on OpenWrt 24.10.2 in QEMU/KVM. pbr creates fw4 include file at /usr/share/nftables.d/ruleset-post/30-pbr.nft. For multi-WAN load balancing, use mwan3 with iptables-nft until mwan4 ships.
none pbr, luci-app-pbr
rescue-watchdog Self-healing reboot when the management target goes unreachable (bench images) untested
Design born from the 2026-10-08 wedges; verify the grace period and reboot behavior on hardware before trusting it unattended.
none
sqm Smart Queue Management with CAKE to eliminate bufferbloat tested
Verified on OpenWrt 24.10.2 QEMU (SHC). CAKE qdisc confirmed via tc.
none sqm-scripts, luci-app-sqm
ssh-hardening Optional Dropbear hardening: disable password auth, idle timeout, rate limit tested
ops unit tests, UciSet validation
none
ssl Trusted HTTPS certificates for LAN devices. Downloads real Let's Encrypt certs from lancert.dev or qip.sh — no domain ownership required. Configures uhttpd HTTPS and opens port 443. tested
Cert download + uhttpd config verified on COVR-X1860. Lancert/qip providers tested.
none libustream-wolfssl, ca-bundle
tether Auto-detect Android or iPhone USB WAN. For Android, includes ADB auto-enable. tested
GL.iNet MT3000, Android RNDIS + CDC-NCM (Pixel 10)
usb kmod-usb-net, kmod-usb-net-rndis, kmod-usb-net-cdc-ether, kmod-usb-net-cdc-ncm, kmod-usb2 +9 more
tether-android USB WAN from Android phone. Enable tethering manually on the phone. tested
GL.iNet MT3000
usb kmod-usb-net, kmod-usb-net-rndis, kmod-usb-net-cdc-ether, kmod-usb-net-cdc-ncm, kmod-usb2 +1 more
tether-android-adb USB WAN from Android phone with ADB auto-enable (legacy svc switch + Android 14+ UI-automation toggle). Confirm on phone, tethering activates automatically. tested
GL.iNet MT3000, Pixel 10 (Android 15) via UI toggle
usb kmod-usb-net, kmod-usb-net-rndis, kmod-usb-net-cdc-ether, kmod-usb-net-cdc-ncm, kmod-usb2 +2 more
tether-ios USB WAN from iPhone. Enable Personal Hotspot manually on the phone. experimental
wiki-based; needs hardware validation
usb kmod-usb-net, kmod-usb-net-ipheth, kmod-usb-net-cdc-ether, usbmuxd, libimobiledevice +2 more
tollgate OpenTollGate Bitcoin/Lightning payment gateway. Downloads ipk via nostr (Blossom) with GitHub CI fallback, deploys post-flash via SCP + opkg. Configures nodogsplash captive portal, hotplug for WiFi STA, and optional gateway branding. tested
ops characterization + resolve tests + transport parity + physical router verification
none nodogsplash, libustream-wolfssl, ca-bundle, ca-certificates
tollgate-cellular Cellular upstream for the TollGate: brings the Quectel RG502Q-EA-class 5G modem up as an ECM WAN (auto-dial inside the modem), switches the active SIM slot, sets the APN, and stages sim.json + sim-balance (provider usage API + modem counters + NDS clients in one command). Pairs with the 'tollgate' preset — enable both. ECM chosen over QMI from bench evidence: stock uqmi hangs against this modem family and lacks QMAP aggregation. tested
provision end-to-end on bench NR7101 2026-10-24 (ECM+slot+APN+tooling); see conwrt-bench sessions/2026-10-23-nr7101-a6e4-sim-bringup
none kmod-usb-net-cdc-ether
tollgate-security RFC 1918 isolation: block public WiFi from upstream LAN + client isolation tested
ops unit tests, UciSet validation
wifi
travelmate Travel router auto-connect for captive portal WiFi (hotels, airports) tested
Config generation verified. WiFi scanning needs physical hardware.
wifi travelmate, luci-app-travelmate, ca-bundle, ca-certificates
vpn-ivpn IVPN WireGuard VPN — account ID auth + key registration untested none wireguard-tools, luci-proto-wireguard requires configuration
vpn-mullvad Mullvad WireGuard VPN — API key registration + server auto-selection untested none wireguard-tools, luci-proto-wireguard requires configuration
vpn-node Self-announcing WireGuard VPN server (publishes Europa listing on Nostr) untested none wireguard-tools, luci-proto-wireguard requires configuration
vpn-nordvpn NordVPN WireGuard VPN (NordLynx) — access token or static config, optional VPN-exit AP tested
Tested on hardware 2026-10-08 (D-Link COVR-X1860 A1, OpenWrt 25.12.5): config_file mode with 5GHz STA uplink, full-tunnel WireGuard to NordVPN DE exit verified from LAN client (handshake, NAT, DNS via tunnel, kill switch). Token mode + 2.4GHz VPN-exit AP tested same day after reflash.
none wireguard-tools, luci-proto-wireguard
vpn-nordvpn-ikev2 NordVPN IKEv2/IPsec (strongSwan, EAP-MSCHAPv2) — optional EIP-93 hw accel untested
IKE_SA proven manually 2026-10-08 (EAP-MSCHAPv2, AES-CBC-256/SHA256); CHILD_SA blocked by SAD EINVAL with eip93 loaded — hw_accel=false is the supported path pending the driver verdict.
none strongswan, strongswan-charon, strongswan-swanctl, strongswan-mod-aes, strongswan-mod-sha1 +19 more
vpn-nordvpn-openvpn NordVPN OpenVPN (udp/tcp) — service credentials resolved from access token tested
Tested on hardware 2026-10-08 (COVR-X1860 A1, OpenWrt 25.12.5): OpenVPN UDP to a DE server with service credentials from the access token, exit verified.
none openvpn-openssl
vpn-openvpn Generic OpenVPN VPN client — for ExpressVPN, ProtonVPN, CyberGhost, TunnelBear, and any provider offering .ovpn config files untested none openvpn-openssl, luci-app-openvpn requires configuration
vpn-pia Private Internet Access WireGuard VPN — full API integration (token gen + key registration + kill switch + DNS) tested
OpenWrt 24.10.4, MediaTek Filogic — PIA Czechia region
none wireguard-tools, luci-proto-wireguard requires configuration
vpn-pia-openvpn Private Internet Access OpenVPN — validated static recipe (RSA-4096 CA, UDP 8080 pool + remote-random, IPv6 pull-filters, no persist-tun) untested
Recipe validated manually on GL-MT3000 / OpenWrt 25.12.5 (recipes/glinet/mt3000/notes.md) — provider op-path not yet exercised on hardware
none openvpn-openssl
vpn-surfshark Surfshark WireGuard VPN — token auth + key registration + server auto-selection untested none wireguard-tools, luci-proto-wireguard requires configuration
wireguard-client WireGuard VPN client — route traffic through a VPN tunnel tested
D-Link COVR-X1860 A1
none kmod-wireguard, wireguard-tools, luci-proto-wireguard
wireguard-server WireGuard VPN server — remote access to home network tested
ops characterization + transport parity
none kmod-wireguard, wireguard-tools, luci-proto-wireguard, qrencode

Documentation

Source files and design docs on GitHub.

READMEFull feature reference, CLI options, use case docs ContributingPR-based workflow, device contribution guide AI Discovery ProcessStage 1 onboarding for unknown routers Device GotchasPer-model quirks and known issues Testing StatusHardware verification log and test matrix Ops Pipeline DesignTransport-agnostic operation rendering Per-Device RecipesStep-by-step procedures and field notes Safety Rules (AGENTS.md)Hardware-safety constraints for automated agents
# --- AdGuard Home --- uci set adguardhome.adguardhome=adguardhome uci set adguardhome.adguardhome.enabled='1' uci set adguardhome.adguardhome.http_address='0.0.0.0:3000' uci set adguardhome.adguardhome.dns_port='5353' uci commit adguardhome /etc/init.d/adguardhome enable /etc/init.d/adguardhome start uci set dhcp.@dnsmasq[0].noresolv='1' uci add_list dhcp.@dnsmasq[0].server='127.0.0.1#5353' uci commit dhcp /etc/init.d/dnsmasq restart echo "AdGuard Home configured: DNS on port 5353, web UI at 0.0.0.0:3000"
# --- AP Nostr identity --- mkdir -p /etc/tollgate [ -f /etc/tollgate/ap-nsec ] || printf '%s\n' 'nsec1ezecvgzv9x45qxppdsl7dw7ysqpuuhz6a3ugsqsdcnxfjfx202dszyq538' > /etc/tollgate/ap-nsec [ -f /etc/tollgate/ap-nsec ] || chmod 600 /etc/tollgate/ap-nsec [ -f /etc/tollgate/ap-npub ] || printf '%s\n' 'npub1q0sqk7tflq7gg9zyk3fjkk6vaqmu6mp2l53wn8dudjv5yx52qc5aknn8964' > /etc/tollgate/ap-npub [ -f /etc/tollgate/ap-npub ] || chmod 644 /etc/tollgate/ap-npub # --- Set nas_identifier on all wifi-iface sections --- for _s in $(uci show wireless 2>/dev/null | grep '=wifi-iface' | cut -d. -f2 | cut -d= -f1); do uci set wireless.$_s.nas_identifier='npub1q0sqk7tflq7gg9zyk3fjkk6vaqmu6mp2l53wn8dudjv5yx52qc5aknn8964'; done uci commit wireless wifi reload 2>/dev/null || true echo "AP Nostr identity configured: npub=npub1q0sqk7tflq7gg9zyk3f..."
# --- Auto-SQM --- touch /etc/config/auto_sqm uci set auto_sqm.config=auto_sqm uci set auto_sqm.config.mode='static' uci set auto_sqm.config.interface='wan' uci set auto_sqm.config.device='eth0' uci set auto_sqm.config.target_percent='90' uci set auto_sqm.config.qdisc='cake' uci set auto_sqm.config.script='piece_of_cake.qos' uci set auto_sqm.config.measurement_mode='wget' uci set auto_sqm.config.iperf3_host='' uci set auto_sqm.config.iperf3_port='5201' uci set auto_sqm.config.test_url='http://speedtest.tele2.net/10MB.zip' uci set auto_sqm.config.download_kbps='0' uci set auto_sqm.config.upload_kbps='0' uci set auto_sqm.config.upload_fallback_kbps='5000' uci set auto_sqm.config.hysteresis_percent='10' uci set auto_sqm.config.dynamic_interval_hours='4' uci commit auto_sqm cat <<'AUTO_SQM_EOF' > /usr/sbin/auto-sqm #!/bin/sh # auto-sqm — measure WAN link speed and configure SQM MODE=$(uci -q get auto_sqm.config.mode || echo "static") INTERFACE=$(uci -q get auto_sqm.config.interface || echo "wan") DEVICE=$(uci -q get auto_sqm.config.device || echo "eth0") TARGET_PCT=$(uci -q get auto_sqm.config.target_percent || echo "90") QDISC=$(uci -q get auto_sqm.config.qdisc || echo "cake") QOS_SCRIPT=$(uci -q get auto_sqm.config.script || echo "piece_of_cake.qos") MEAS_MODE=$(uci -q get auto_sqm.config.measurement_mode || echo "wget") IPERF3_HOST=$(uci -q get auto_sqm.config.iperf3_host || echo "") IPERF3_PORT=$(uci -q get auto_sqm.config.iperf3_port || echo "5201") TEST_URL=$(uci -q get auto_sqm.config.test_url || echo "") UPLOAD_FALLBACK=$(uci -q get auto_sqm.config.upload_fallback_kbps || echo "5000") HYSTERESIS=$(uci -q get auto_sqm.config.hysteresis_percent || echo "10") DL_SPEED=0 UL_SPEED=0 measure_wget() { [ -z "$DEVICE" ] && return 1 [ -z "$TEST_URL" ] && return 1 bytes_before=$(cat /sys/class/net/"$DEVICE"/statistics/rx_bytes 2>/dev/null) || return 1 t1=$(date +%s) wget -O /dev/null -T 30 "$TEST_URL" 2>/dev/null || return 1 t2=$(date +%s) bytes_after=$(cat /sys/class/net/"$DEVICE"/statistics/rx_bytes 2>/dev/null) || return 1 elapsed=$((t2 - t1)) [ "$elapsed" -eq 0 ] && elapsed=1 speed_bps=$(( (bytes_after - bytes_before) * 8 / elapsed )) DL_SPEED=$((speed_bps / 1000)) DL_SPEED=$((DL_SPEED * TARGET_PCT / 100)) } measure_iperf3() { [ -z "$IPERF3_HOST" ] && return 1 command -v iperf3 >/dev/null 2>&1 || return 1 json_out=$(iperf3 -c "$IPERF3_HOST" -p "$IPERF3_PORT" -t 5 -J 2>/dev/null) || return 1 speed_bps=$(echo "$json_out" | jsonfilter -e "@.end.sum_received.bits_per_second" 2>/dev/null) || return 1 speed_bps=$(echo "$speed_bps" | cut -d. -f1) DL_SPEED=$((speed_bps / 1000)) DL_SPEED=$((DL_SPEED * TARGET_PCT / 100)) } apply_sqm() { dl=$1 ul=$2 [ "$dl" -eq 0 ] && return 1 cur_dl=$(uci -q get sqm."$INTERFACE".download || echo "0") cur_ul=$(uci -q get sqm."$INTERFACE".upload || echo "0") if [ "$cur_dl" -gt 0 ] && [ "$cur_ul" -gt 0 ]; then lo=$((100 - HYSTERESIS)) hi=$((100 + HYSTERESIS)) pct_dl=$((cur_dl * 100 / dl)) pct_ul=$((cur_ul * 100 / ul)) if [ "$pct_dl" -ge "$lo" ] && [ "$pct_dl" -le "$hi" ] && [ "$pct_ul" -ge "$lo" ] && [ "$pct_ul" -le "$hi" ]; then echo "auto-sqm: change within hysteresis, skipping" return 0 fi fi # clean baseline first: /tmp/.uci staging debris from an aborted # earlier run must not merge into this commit (#470, AGENTS uci rule) uci revert sqm 2>/dev/null || true touch /etc/config/sqm uci set sqm.$INTERFACE=queue # SQM must bind the actual device, not the UCI network name (#470 — # the wan lesson: names break on WiFi-STA/alias setups where # device != name). Resolve via the network section, then the # configured DEVICE, then fall back to the name. _sqm_dev=$(uci -q get network."$INTERFACE".device 2>/dev/null) [ -z "$_sqm_dev" ] && _sqm_dev="$DEVICE" [ -z "$_sqm_dev" ] && _sqm_dev="$INTERFACE" uci set sqm.$INTERFACE.interface="$_sqm_dev" uci set sqm.$INTERFACE.enabled='1' uci set sqm.$INTERFACE.script="$QOS_SCRIPT" uci set sqm.$INTERFACE.qdisc="$QDISC" uci set sqm.$INTERFACE.linklayer='none' uci set sqm.$INTERFACE.overhead='0' uci set sqm.$INTERFACE.linklayer_adaptation_mechanism='default' uci set sqm.$INTERFACE.debug_logging='0' uci set sqm.$INTERFACE.verbosity='5' uci set sqm.$INTERFACE.download="$dl" uci set sqm.$INTERFACE.upload="$ul" uci commit sqm /etc/init.d/sqm enable /etc/init.d/sqm restart 2>/dev/null || true echo "auto-sqm: SQM applied ${dl}/${ul} kbit/s (${QDISC})" } STATIC_DL=$(uci -q get auto_sqm.config.download_kbps || echo "0") STATIC_UL=$(uci -q get auto_sqm.config.upload_kbps || echo "0") if [ "$MODE" = "static" ] && [ "$STATIC_DL" -gt 0 ]; then DL_SPEED=$((STATIC_DL * TARGET_PCT / 100)) if [ "$STATIC_UL" -gt 0 ]; then UL_SPEED=$((STATIC_UL * TARGET_PCT / 100)) else UL_SPEED=$UPLOAD_FALLBACK fi else if [ "$MEAS_MODE" = "iperf3" ] && [ -n "$IPERF3_HOST" ]; then measure_iperf3 || measure_wget else measure_wget || measure_iperf3 fi if [ "$DL_SPEED" -eq 0 ]; then echo "auto-sqm: measurement failed, will retry on next trigger" >&2 exit 1 fi if [ "$STATIC_UL" -gt 0 ]; then UL_SPEED=$((STATIC_UL * TARGET_PCT / 100)) else UL_SPEED=$UPLOAD_FALLBACK fi fi apply_sqm "$DL_SPEED" "$UL_SPEED" AUTO_SQM_EOF chmod +x /usr/sbin/auto-sqm mkdir -p /etc/hotplug.d/iface cat <<'HOTPLUG_EOF' > /etc/hotplug.d/iface/10-sqm-autotune #!/bin/sh [ "$ACTION" = "ifup" ] && [ "$INTERFACE" = "wan" ] && /usr/sbin/auto-sqm & HOTPLUG_EOF chmod +x /etc/hotplug.d/iface/10-sqm-autotune echo 'auto-sqm: will measure and configure on WAN ifup' echo 'Auto-SQM configured'
# --- Cashu VPN client (vps-on-demand WireGuard exit) --- # Tunnel address and peer key come from POST /peer on the vps-on-demand service. uci set network.wg0=interface uci set network.wg0.proto='wireguard' uci set network.wg0.private_key='generate' uci set network.wg0.addresses='' uci add_list network.wg0.dns='1.1.1.1' uci set network.wg0_peer=wireguard_wg0 uci set network.wg0_peer.public_key='' uci set network.wg0_peer.endpoint_host='' uci set network.wg0_peer.endpoint_port='51820' uci set network.wg0_peer.allowed_ips='0.0.0.0/0' uci set network.wg0_peer.route_allowed_ips='1' uci set network.wg0_peer.persistent_keepalive='25' # --- Cleanup stale anonymous WireGuard firewall sections --- while uci show firewall 2>/dev/null | grep -q "name='vpn'"; do _s=$(uci show firewall 2>/dev/null | grep "name='vpn'" | head -1 | cut -d. -f2 | cut -d= -f1); uci delete "firewall.$_s" 2>/dev/null; done; true while uci show firewall 2>/dev/null | grep -q "dest='vpn'"; do _s=$(uci show firewall 2>/dev/null | grep "dest='vpn'" | head -1 | cut -d. -f2 | cut -d= -f1); uci delete "firewall.$_s" 2>/dev/null; done; true while uci show firewall 2>/dev/null | grep -q "name='KillSwitch-Reject-NonVPN'"; do _s=$(uci show firewall 2>/dev/null | grep "name='KillSwitch-Reject-NonVPN'" | head -1 | cut -d. -f2 | cut -d= -f1); uci delete "firewall.$_s" 2>/dev/null; done; true # --- WireGuard firewall zone + LAN forwarding --- uci set firewall.cashu_vpn_zone=zone uci set firewall.cashu_vpn_zone.name='vpn' uci set firewall.cashu_vpn_zone.input='REJECT' uci set firewall.cashu_vpn_zone.output='ACCEPT' uci set firewall.cashu_vpn_zone.forward='REJECT' uci set firewall.cashu_vpn_zone.masq='1' uci set firewall.cashu_vpn_zone.mtu_fix='1' uci set firewall.cashu_vpn_zone.network='wg0' uci set firewall.cashu_vpn_fwd=forwarding uci set firewall.cashu_vpn_fwd.src='lan' uci set firewall.cashu_vpn_fwd.dest='vpn' # --- Kill switch: drop LAN→WAN when the tunnel is down --- uci set firewall.cashu_vpn_killswitch=rule uci set firewall.cashu_vpn_killswitch.name='KillSwitch-Reject-NonVPN' uci set firewall.cashu_vpn_killswitch.src='lan' uci set firewall.cashu_vpn_killswitch.dest='wan' uci set firewall.cashu_vpn_killswitch.proto='all' uci set firewall.cashu_vpn_killswitch.target='REJECT' # --- Commit + restart --- uci commit network uci commit firewall /etc/init.d/network restart echo 'Cashu VPN client configured: :51820 (peer_ip=)' i=0; while [ $i -lt 30 ]; do if wg show wg0 2>/dev/null | grep -q 'latest handshake'; then echo 'Cashu VPN kill-switch: tunnel verified'; exit 0; fi; sleep 1; i=$((i+1)); done; echo 'Cashu VPN kill-switch: tunnel failed — removing kill switch to restore WAN'; uci delete firewall.cashu_vpn_killswitch; uci commit firewall; fw4 restart
# --- configurationwizzard: deployed via deploy_configurationwizzard() --- # Admin: http://router/ (uhttpd port 80) # Portal: served by NDS on port 2050 # rpcd plugin bridges ubus → tollgate CLI
# --- DNS-over-HTTPS (cloudflare) --- uci -q delete https-dns-proxy >/dev/null 2>&1 || true uci set https-dns-proxy.main=https_dns_proxy uci set https-dns-proxy.main.bootstrap_dns='1.1.1.1,8.8.8.8' uci set https-dns-proxy.main.listen_addr='127.0.0.1' uci set https-dns-proxy.main.listen_port='5053' uci set https-dns-proxy.main.resolver_url='https://cloudflare-dns.com/dns-query' uci commit https-dns-proxy /etc/init.d/https-dns-proxy enable /etc/init.d/https-dns-proxy restart # --- Point dnsmasq to DoH proxy on port 5053 --- uci set dhcp.@dnsmasq[0].noresolv='1' uci delete dhcp.@dnsmasq[0].server uci add_list dhcp.@dnsmasq[0].server='127.0.0.1#5053' uci commit dhcp /etc/init.d/dnsmasq restart echo "DoH configured: cloudflare on port 5053"
mkdir -p /etc/fips echo 'node:' > /etc/fips/fips.yaml echo ' identity:' >> /etc/fips/fips.yaml echo ' persistent: true' >> /etc/fips/fips.yaml echo '' >> /etc/fips/fips.yaml echo 'tun:' >> /etc/fips/fips.yaml echo ' enabled: true' >> /etc/fips/fips.yaml echo '' >> /etc/fips/fips.yaml echo 'dns:' >> /etc/fips/fips.yaml echo ' enabled: true' >> /etc/fips/fips.yaml echo '' >> /etc/fips/fips.yaml echo 'transports:' >> /etc/fips/fips.yaml echo ' rfcomm:' >> /etc/fips/fips.yaml echo ' mode: "client"' >> /etc/fips/fips.yaml echo ' channel: 1' >> /etc/fips/fips.yaml echo ' mtu: 1000' >> /etc/fips/fips.yaml echo ' auto_connect: true' >> /etc/fips/fips.yaml echo '' >> /etc/fips/fips.yaml echo 'peers: []' >> /etc/fips/fips.yaml echo '' >> /etc/fips/fips.yaml echo 'gateway:' >> /etc/fips/fips.yaml echo ' enabled: true' >> /etc/fips/fips.yaml echo ' pool: "fd01::/112"' >> /etc/fips/fips.yaml echo ' lan_interface: "br-lan"' >> /etc/fips/fips.yaml echo ' dns:' >> /etc/fips/fips.yaml echo ' listen: "[::1]:5353"' >> /etc/fips/fips.yaml echo ' upstream: "[::1]:5354"' >> /etc/fips/fips.yaml echo ' ttl: 60' >> /etc/fips/fips.yaml echo '0000' > /etc/fips/bt-pin echo '#!/bin/sh /etc/rc.common' > /etc/init.d/bt-agent echo 'START=98' >> /etc/init.d/bt-agent echo 'STOP=10' >> /etc/init.d/bt-agent echo 'start() {' >> /etc/init.d/bt-agent echo ' hciconfig hci0 up' >> /etc/init.d/bt-agent echo ' command -v bt-agent >/dev/null 2>&1 && bt-agent -c NoInputNoOutput -p /etc/fips/bt-pin &' >> /etc/init.d/bt-agent echo '}' >> /etc/init.d/bt-agent echo 'stop() {' >> /etc/init.d/bt-agent echo ' killall bt-agent 2>/dev/null' >> /etc/init.d/bt-agent echo '}' >> /etc/init.d/bt-agent chmod +x /etc/init.d/bt-agent /etc/init.d/bt-agent enable echo '#!/bin/sh /etc/rc.common' > /etc/init.d/fips echo 'START=99' >> /etc/init.d/fips echo 'STOP=10' >> /etc/init.d/fips echo 'start() {' >> /etc/init.d/fips echo ' fips -c /etc/fips/fips.yaml &' >> /etc/init.d/fips echo '}' >> /etc/init.d/fips echo 'stop() {' >> /etc/init.d/fips echo ' killall fips 2>/dev/null' >> /etc/init.d/fips echo '}' >> /etc/init.d/fips chmod +x /etc/init.d/fips /etc/init.d/fips enable echo 'FIPS Bluetooth RFCOMM configured (client, channel 1, 0 peer(s))'
# --- Guest WiFi: 'Guest' --- _lan_ip=$(uci get network.lan.ipaddr 2>/dev/null || echo '192.168.1.1') _guest_o3=$(echo "$_lan_ip" | cut -d. -f3) _guest_o3=$((_guest_o3 + 1)) [ "$_guest_o3" -gt 255 ] && _guest_o3=0 _guest_prefix=$(echo "$_lan_ip" | cut -d. -f1-2).$_guest_o3 _radio=""; for _r in radio0 radio1 radio2 radio3; do _b=$(uci get wireless.${_r}.band 2>/dev/null || true); if [ "$_b" = "2g" ]; then _radio=$_r; break; fi; done; if [ -z "$_radio" ]; then echo 'guest-wifi: no 2g radio found — aborting before any config changes' >&2; exit 1; fi uci set network.guest=interface uci set network.guest.ipaddr="$_guest_prefix.1" uci set network.guest.proto='static' uci set network.guest.netmask='255.255.255.0' uci commit network uci set dhcp.guest=dhcp uci set dhcp.guest.interface='guest' uci set dhcp.guest.start='100' uci set dhcp.guest.limit='50' uci set dhcp.guest.leasetime='1h' uci commit dhcp uci set firewall.guest=zone uci set firewall.guest.name='guest' uci set firewall.guest.network='guest' uci set firewall.guest.input='REJECT' uci set firewall.guest.output='ACCEPT' uci set firewall.guest.forward='REJECT' uci set firewall.guest_fwd=forwarding uci set firewall.guest_fwd.src='guest' uci set firewall.guest_fwd.dest='wan' uci set firewall.guest_dns=rule uci set firewall.guest_dns.name='Allow-DNS-guest' uci set firewall.guest_dns.src='guest' uci set firewall.guest_dns.dest_port='53' uci set firewall.guest_dns.proto='tcpudp' uci set firewall.guest_dns.target='ACCEPT' uci set firewall.guest_dhcp=rule uci set firewall.guest_dhcp.name='Allow-DHCP-guest' uci set firewall.guest_dhcp.src='guest' uci set firewall.guest_dhcp.dest_port='67-68' uci set firewall.guest_dhcp.proto='udp' uci set firewall.guest_dhcp.target='ACCEPT' uci commit firewall while uci show wireless 2>/dev/null | grep -q "network='guest'"; do _s=$(uci show wireless 2>/dev/null | grep "network='guest'" | head -1 | cut -d. -f2 | cut -d= -f1); uci delete "wireless.$_s" 2>/dev/null; done; true uci set wireless.guest_ap=wifi-iface; uci set wireless.guest_ap.device=$_radio; uci set wireless.guest_ap.mode=ap; uci set wireless.guest_ap.ssid='Guest'; uci set wireless.guest_ap.encryption=psk2; uci set wireless.guest_ap.network=guest; uci set wireless.guest_ap.isolate=1 uci commit wireless /etc/init.d/network restart /etc/init.d/firewall restart echo "Guest WiFi configured: SSID='Guest' network=guest isolated=1"
# --- mwan3 multi-WAN --- uci -q delete mwan3 >/dev/null 2>&1 || true uci set mwan3.wan=interface uci set mwan3.wan.enabled='1' uci add_list mwan3.wan.track_ip='1.0.0.1' uci add_list mwan3.wan.track_ip='1.1.1.1' uci add_list mwan3.wan.track_ip='8.8.8.8' uci add_list mwan3.wan.track_ip='8.8.4.4' uci set mwan3.wan.family='ipv4' uci set mwan3.wan.reliability='1' uci set mwan3.wan.count='1' uci set mwan3.wan.timeout='2' uci set mwan3.wan.interval='5' uci set mwan3.wan.down='3' uci set mwan3.wan.up='8' uci set mwan3.usbwan=interface uci set mwan3.usbwan.enabled='1' uci add_list mwan3.usbwan.track_ip='1.0.0.1' uci add_list mwan3.usbwan.track_ip='1.1.1.1' uci add_list mwan3.usbwan.track_ip='8.8.8.8' uci add_list mwan3.usbwan.track_ip='8.8.4.4' uci set mwan3.usbwan.family='ipv4' uci set mwan3.usbwan.reliability='1' uci set mwan3.usbwan.count='1' uci set mwan3.usbwan.timeout='2' uci set mwan3.usbwan.interval='5' uci set mwan3.usbwan.down='3' uci set mwan3.usbwan.up='8' uci set mwan3.wan_m1_w1=member uci set mwan3.wan_m1_w1.interface='wan' uci set mwan3.wan_m1_w1.metric='1' uci set mwan3.wan_m1_w1.weight='1' uci set mwan3.usbwan_m2_w1=member uci set mwan3.usbwan_m2_w1.interface='usbwan' uci set mwan3.usbwan_m2_w1.metric='2' uci set mwan3.usbwan_m2_w1.weight='1' uci set mwan3.wan_policy=policy uci add_list mwan3.wan_policy.use_member='wan_m1_w1' uci add_list mwan3.wan_policy.use_member='usbwan_m2_w1' uci set mwan3.wan_policy.last_resort='default' uci set mwan3.default_rule_v4=rule uci set mwan3.default_rule_v4.dest_ip='0.0.0.0/0' uci set mwan3.default_rule_v4.use_policy='wan_policy' uci set mwan3.default_rule_v4.family='ipv4' uci set mwan3.https_rule=rule uci set mwan3.https_rule.dest_port='443' uci set mwan3.https_rule.proto='tcp' uci set mwan3.https_rule.sticky='1' uci set mwan3.https_rule.use_policy='wan_policy' uci commit mwan3 /etc/init.d/mwan3 restart echo "mwan3 configured: wan (primary) + usbwan (failover)"
# --- net-roles: uplinks=none serve_eth=True serve_wifi=True mgmt_vlan=off --- # --- net-roles: readbacks (gate the commits — staged values are visible to uci get) --- uci commit network uci commit firewall /etc/init.d/network reload echo "net-roles applied: uplinks=0 serve_eth=True serve_wifi=True mgmt=lan"
# --- nodns DNS bridge: local zone cache via dnsmasq --- mkdir -p /etc/dnsmasq.d touch /etc/dnsmasq.d/nodns.hosts # --- procd service: /etc/init.d/nodns-bridge --- cat > /etc/init.d/nodns-bridge << 'NODNS_INIT_EOF' #!/bin/sh /etc/rc.common START=99 USE_PROCD=1 start_service() { procd_open_instance procd_set_param command /usr/bin/nodns-poll.sh procd_set_param respawn procd_set_param stdout 1 procd_set_param stderr 1 procd_close_instance } NODNS_INIT_EOF chmod +x /etc/init.d/nodns-bridge # --- poll script: /usr/bin/nodns-poll.sh --- cat > /usr/bin/nodns-poll.sh << 'NODNS_POLL_EOF' #!/bin/sh # nodns DNS bridge -- polls zone export API, updates dnsmasq addn-hosts cache. # Auto-generated by conwrt use_cases/nodns. ZONE="nodns.shop" API_URL="https://nodns.shop" POLL_INTERVAL=300 HOSTS_FILE="/etc/dnsmasq.d/nodns.hosts" TMP_FILE="/tmp/nodns-zone.txt" mkdir -p /etc/dnsmasq.d while true; do # Fetch BIND zone file from the nodns bot API. if curl -s --connect-timeout 10 --max-time 30 \ "${API_URL}/api/zone/${ZONE}/export" \ -o "$TMP_FILE" 2>/dev/null; then # Parse: extract A/AAAA records -> "ip<TAB>fqdn" lines for addn-hosts. awk -v z="$ZONE" ' BEGIN { OFS = "\t" } { rtype = toupper($3) if (rtype != "A" && rtype != "AAAA") next name = $1 ip = $NF if (name == "@") name = z else if (name ~ /\.$/) sub(/\.$/, "", name) else name = name "." z print ip, name }' "$TMP_FILE" > "$HOSTS_FILE.new" # Atomic swap only when we got valid data. if [ -s "$HOSTS_FILE.new" ]; then mv "$HOSTS_FILE.new" "$HOSTS_FILE" killall -HUP dnsmasq 2>/dev/null else rm -f "$HOSTS_FILE.new" fi fi sleep "$POLL_INTERVAL" done NODNS_POLL_EOF chmod +x /usr/bin/nodns-poll.sh # --- Register addn-hosts with dnsmasq --- uci -q del_list dhcp.@dnsmasq[0].addnhosts='/etc/dnsmasq.d/nodns.hosts'; true uci add_list dhcp.@dnsmasq[0].addnhosts='/etc/dnsmasq.d/nodns.hosts' uci commit dhcp /etc/init.d/dnsmasq restart # --- Enable and start nodns-bridge --- /etc/init.d/nodns-bridge enable /etc/init.d/nodns-bridge start echo "nodns bridge configured: zone=nodns.shop interval=300s url=https://nodns.shop"
# --- OpenClash transparent proxy --- mkdir -p /etc/openclash/config uci -q revert openclash 2>/dev/null || true uci set openclash.config.enable='1' uci set openclash.config.config_path='/etc/openclash/config/config.yaml' uci set openclash.config.proxy_type='ss' uci set openclash.config.core_type='Meta' uci set openclash.config.dashboard_type='Official' uci set openclash.config.dns_mode='redir-host' uci set openclash.config.operation_mode='redir-host' uci commit openclash /etc/init.d/openclash enable echo "OpenClash configured: Meta core, ss proxy" echo "Post-flash: import your subscription config via the LuCI web UI"
# --- openperf-client: WiFi Performance Test Client --- # Create directories mkdir -p /root/openperf/init.d # Write STA autostart procd service cat > /etc/init.d/openperf-autostart << 'OPENPERF_EOF' #!/bin/sh /etc/rc.common USE_PROCD=1 START=98 STOP=1 start_service() { procd_open_instance procd_set_param respawn 3600 5 0 procd_set_param env AP_IP= procd_set_param command /usr/bin/python3 /root/openperf/sta_autostart.py procd_set_param stdout 1 procd_set_param stderr 1 procd_close_instance } OPENPERF_EOF chmod +x /etc/init.d/openperf-autostart # Configure 2.4 GHz STA to scan for openperf AP for iface in sta_radio0 phy0_sta; do if uci -q get wireless.$iface >/dev/null 2>&1; then uci set wireless.$iface.ssid='openperf' uci set wireless.$iface.encryption='none' uci set wireless.$iface.network='wan' break fi done # Configure HaHalow STA if present for iface in default_radio1; do if uci -q get wireless.$iface >/dev/null 2>&1; then uci set wireless.$iface.ssid='openperf-halow' uci set wireless.$iface.encryption='sae' uci set wireless.$iface.key='openperf-test' break fi done uci commit wireless # Enable autostart service /etc/init.d/openperf-autostart enable # NOTE: Python files must be pushed to /root/openperf/ via install-openperf.sh. # The autostart loop will wait for AP discovery via default gateway.
# --- openperf-server: Open WiFi Performance Testing --- # Create directories mkdir -p /root/openperf/init.d # Write procd init script cat > /etc/init.d/openperf-server << 'OPENPERF_EOF' #!/bin/sh /etc/rc.common USE_PROCD=1 START=99 STOP=1 start_service() { procd_open_instance procd_set_param respawn 3600 5 0 procd_set_param command /usr/bin/python3 /root/openperf/server.py procd_set_param stdout 1 procd_set_param stderr 1 procd_set_param env PORT=7777 procd_close_instance } OPENPERF_EOF chmod +x /etc/init.d/openperf-server # Set management SSID on first 2.4 GHz AP interface for iface in phy0-ap0 default_radio0; do if uci -q get wireless.$iface >/dev/null 2>&1; then uci set wireless.$iface.ssid='openperf' uci set wireless.$iface.encryption='none' break fi done # Set HaHalow test SSID if present for iface in default_radio1; do if uci -q get wireless.$iface >/dev/null 2>&1; then uci set wireless.$iface.ssid='openperf-halow' uci set wireless.$iface.encryption='sae' uci set wireless.$iface.key='openperf-test' break fi done uci commit wireless # Enable and start service /etc/init.d/openperf-server enable # NOTE: Python files (server.py, protocol.py, radio_hal.py, radio_hal_iw.py) # must be pushed to /root/openperf/ via install-openperf.sh or SCP. # The service will fail to start until files are present, but procd will respawn.
# --- pbr (nftables-native policy routing) --- uci set pbr.config=pbr uci set pbr.config.enabled='1' uci set pbr.config.verbosity='2' uci set pbr.config.strict_enforcement='1' uci set pbr.config.resolver_set='none' uci set pbr.config.nft_file_helper='1' uci set pbr.config.netifd_interface_default=wan uci add_list pbr.config.netifd_interface_local=lan # Example: default policy routes all traffic via WAN uci commit pbr /etc/init.d/pbr enable /etc/init.d/pbr restart echo "pbr configured. Verify with: nft list table inet pbr 2>/dev/null" echo "Check status: /etc/init.d/pbr status"
# --- rescue-watchdog (reboot if 192.168.1.254 unreachable 3x1min) --- cat > /etc/init.d/rescue-watchdog << 'WD_EOF' #!/bin/sh /etc/rc.common START=99 USE_PROCD=1 start_service() { procd_open_instance procd_set_param command /bin/sh /usr/bin/rescue-watchdog-loop procd_set_param respawn 3600 5 0 procd_set_param stdout 1 procd_set_param stderr 1 procd_close_instance } WD_EOF chmod +x /etc/init.d/rescue-watchdog /usr/bin/rescue-watchdog-loop << 'WDL_EOF' #!/bin/sh TARGET="${WATCHDOG_TARGET:-192.168.1.254}" GRACE=600 FAILS=0 while :; do UPTIME=$(cut -d. -f1 /proc/uptime) if [ "$UPTIME" -lt "$GRACE" ]; then FAILS=0 elif ping -c1 -W3 "$TARGET" >/dev/null 2>&1; then FAILS=0 else FAILS=$((FAILS+1)) logger -t rescue-watchdog "target $TARGET unreachable ($FAILS/3)" if [ "$FAILS" -ge 3 ]; then logger -t rescue-watchdog "networking wedged - rebooting" sync reboot fi fi sleep 60 done WDL_EOF chmod +x /usr/bin/rescue-watchdog-loop /etc/init.d/rescue-watchdog enable /etc/init.d/rescue-watchdog restart
# --- SQM Smart Queue Management --- for _s in $(uci -q show sqm 2>/dev/null | grep '=queue' | cut -d. -f2 | cut -d= -f1); do uci -q delete sqm.$_s; done; true _sqm_dev=$(uci get network.wan.device 2>/dev/null); if [ -z "$_sqm_dev" ] || ! ip addr show "$_sqm_dev" 2>/dev/null | grep -q "inet "; then _sqm_dev=$(ip route show default 0.0.0.0/0 2>/dev/null | awk '{print $5}' | head -1); fi; if [ -z "$_sqm_dev" ]; then _sqm_dev=wan; fi uci set sqm.wan=queue uci set sqm.wan.interface="$_sqm_dev" uci set sqm.wan.enabled='1' uci set sqm.wan.script='piece_of_cake.qos' uci set sqm.wan.qdisc='cake' uci set sqm.wan.linklayer='none' uci set sqm.wan.overhead='0' uci set sqm.wan.download='10000' uci set sqm.wan.upload='5000' uci set sqm.wan.linklayer_adaptation_mechanism='default' uci set sqm.wan.debug_logging='0' uci set sqm.wan.verbosity='5' uci commit sqm /etc/init.d/sqm enable /etc/init.d/sqm restart echo "SQM configured: 10000/5000 kbit/s (cake) on $_sqm_dev"
# --- SSH hardening --- uci set dropbear.@dropbear[0].PasswordAuth='off' uci set dropbear.@dropbear[0].RootPasswordAuth='off' uci set dropbear.@dropbear[0].Port='22' uci set dropbear.@dropbear[0].IdleTimeout='300' uci set dropbear.@dropbear[0].MaxAuthTries='3' uci set dropbear.@dropbear[0].GatewayPorts='no' uci commit dropbear /etc/init.d/dropbear restart echo "SSH hardened: password_auth=off idle=300s max_tries=3 port=22"
# --- SSL: trusted HTTPS via lancert --- # -- uhttpd HTTPS (admin interface) -- if [ -f /etc/tollgate/ssl/server.crt ] && [ -f /etc/tollgate/ssl/server.key ]; then (uci -q revert uhttpd 2>/dev/null || true) && uci set uhttpd.main.cert='/etc/tollgate/ssl/server.crt' && uci set uhttpd.main.key='/etc/tollgate/ssl/server.key' && (uci -q delete uhttpd.main.listen_https || true) && uci add_list uhttpd.main.listen_https='0.0.0.0:443' && uci add_list uhttpd.main.listen_https='[::]:443' && uci commit uhttpd && uci -q get uhttpd.main.listen_https | grep -q '0.0.0.0:443' && /etc/init.d/uhttpd restart && echo 'ssl: uhttpd HTTPS wired on :443'; else echo 'ssl: /etc/tollgate/ssl certs not present yet — skipping uhttpd HTTPS wiring (deploy certs, then re-run)' >&2; fi # -- Allow port 443 through nodogsplash -- if uci -q show nodogsplash >/dev/null 2>&1; then (uci -q revert nodogsplash 2>/dev/null || true) && (uci -q del_list nodogsplash.@nodogsplash[0].users_to_router='allow tcp port 443' || true) && uci add_list nodogsplash.@nodogsplash[0].users_to_router='allow tcp port 443' && uci commit nodogsplash && uci -q get nodogsplash.@nodogsplash[0].users_to_router | grep -q 'allow tcp port 443' && /etc/init.d/nodogsplash restart && echo 'ssl: nodogsplash allows tcp/443 to the router'; fi
# --- USB tethering (auto-detect) --- /usr/sbin/usbmuxd -v -U -f >/dev/null 2>&1 & USB_DEV="" for _ in $(seq 1 45); do for dev in /sys/class/net/*; do name=$(basename "$dev") case "$name" in lo|br-lan|eth0|wlan*) continue ;; esac driver=$(readlink "$dev/device/driver" 2>/dev/null || true) if echo "$driver" | grep -qi "rndis\|cdc_ether\|cdc_ncm\|usbnet\|ipheth"; then USB_DEV="$name" break 2 fi iface_desc="" [ -e "$dev/device/interface" ] && iface_desc=$(cat "$dev/device/interface" 2>/dev/null || true) if echo "$iface_desc" | grep -qi "apple\|iphone"; then USB_DEV="$name" break 2 fi done sleep 1 done if [ -z "$USB_DEV" ]; then for cand in usb0 usb1; do [ -e "/sys/class/net/$cand" ] && USB_DEV="$cand" && break done fi if [ -n "$USB_DEV" ]; then uci set network.usbwan=interface uci set network.usbwan.proto='dhcp' uci set network.usbwan.device="$USB_DEV" uci set network.usbwan.metric='20' for _z in $(uci show firewall 2>/dev/null | grep '=zone' | cut -d. -f2 | cut -d= -f1 || true); do if [ "$(uci -q get firewall."$_z".name)" = 'wan' ]; then uci -q del_list firewall."$_z".network='usbwan' 2>/dev/null; uci add_list firewall."$_z".network='usbwan'; else uci -q del_list firewall."$_z".network='usbwan' 2>/dev/null; fi; done; true uci commit network uci commit firewall ifup usbwan 2>/dev/null || true echo "USB tethering enabled on $USB_DEV -> usbwan" else echo "USB tethering: no USB network device found, skipping" >&2 fi \ mkdir -p /etc/hotplug.d/usb cat > /etc/hotplug.d/usb/99-usb-tether-adb << 'HOTPLUG_EOF' [ "$ACTION" = "bind" ] || exit 0 [ "$PRODUCT" ] || exit 0 ( HOME=/root export HOME command -v adb >/dev/null 2>&1 || exit 0 DELAY=1 attempt=0 while [ $attempt -lt 7 ]; do attempt=$((attempt + 1)) sleep $DELAY if ip addr show usb0 2>/dev/null | grep -q "inet "; then exit 0 fi STATE=$(adb get-state 2>/dev/null) if [ "$STATE" = "device" ]; then adb shell svc usb setFunctions rndis 2>/dev/null sleep 3 if ip addr show usb0 2>/dev/null | grep -q "inet "; then logger -t usb-tether "ADB svc enabled tethering (attempt $attempt)" exit 0 fi adb shell input keyevent KEYCODE_WAKEUP 2>/dev/null sleep 1 adb shell am start -a android.settings.TETHER_SETTINGS 2>/dev/null sleep 3 adb shell uiautomator dump /sdcard/ui.xml 2>/dev/null UI=$(adb shell cat /sdcard/ui.xml 2>/dev/null) USB_ROW=$(echo "$UI" | grep -oE 'USB.tethering[^>]*bounds="\[[0-9]+,[0-9]+' | grep -oE '[0-9]+,[0-9]+' | head -1) USB_Y=$(echo "$USB_ROW" | cut -d, -f2) if [ -n "$USB_Y" ]; then USB_Y_CENTER=$((USB_Y + 28)) SWITCH=$(echo "$UI" | grep -oE "Switch[^>]*bounds=\"\[[0-9]+,${USB_Y_CENTER}" | grep -oE '\[[0-9]+,' | grep -oE '[0-9]+' | head -1) if [ -n "$SWITCH" ]; then SWITCH_CENTER=$((SWITCH + 65)) adb shell input tap $SWITCH_CENTER $USB_Y_CENTER 2>/dev/null sleep 3 if ip addr show usb0 2>/dev/null | grep -q "inet "; then logger -t usb-tether "ADB UI toggle enabled tethering (attempt $attempt)" exit 0 fi fi fi fi DELAY=$((DELAY * 2)) done logger -t usb-tether "ADB tethering gave up after $attempt attempts" ) & HOTPLUG_EOF
# --- Android USB tethering (manual) --- USB_DEV="" for _ in $(seq 1 45); do for dev in /sys/class/net/*; do name=$(basename "$dev") case "$name" in lo|br-lan|eth0|wlan*) continue ;; esac driver=$(readlink "$dev/device/driver" 2>/dev/null || true) if echo "$driver" | grep -qi "rndis\|cdc_ether\|cdc_ncm\|usbnet"; then USB_DEV="$name" break 2 fi done sleep 1 done if [ -z "$USB_DEV" ]; then for cand in usb0 usb1; do [ -e "/sys/class/net/$cand" ] && USB_DEV="$cand" && break done fi if [ -n "$USB_DEV" ]; then uci set network.usbwan=interface uci set network.usbwan.proto='dhcp' uci set network.usbwan.device="$USB_DEV" uci set network.usbwan.metric='20' for _z in $(uci show firewall 2>/dev/null | grep '=zone' | cut -d. -f2 | cut -d= -f1 || true); do if [ "$(uci -q get firewall."$_z".name)" = 'wan' ]; then uci -q del_list firewall."$_z".network='usbwan' 2>/dev/null; uci add_list firewall."$_z".network='usbwan'; else uci -q del_list firewall."$_z".network='usbwan' 2>/dev/null; fi; done; true uci commit network uci commit firewall ifup usbwan 2>/dev/null || true echo "USB tethering enabled on $USB_DEV -> usbwan" else echo "USB tethering: no USB network device found, skipping" >&2 fi
# --- Android USB tethering (ADB auto-enable) --- USB_DEV="" for _ in $(seq 1 45); do for dev in /sys/class/net/*; do name=$(basename "$dev") case "$name" in lo|br-lan|eth0|wlan*) continue ;; esac driver=$(readlink "$dev/device/driver" 2>/dev/null || true) if echo "$driver" | grep -qi "rndis\|cdc_ether\|cdc_ncm\|usbnet"; then USB_DEV="$name" break 2 fi done sleep 1 done if [ -z "$USB_DEV" ]; then for cand in usb0 usb1; do [ -e "/sys/class/net/$cand" ] && USB_DEV="$cand" && break done fi if [ -n "$USB_DEV" ]; then uci set network.usbwan=interface uci set network.usbwan.proto='dhcp' uci set network.usbwan.device="$USB_DEV" uci set network.usbwan.metric='20' for _z in $(uci show firewall 2>/dev/null | grep '=zone' | cut -d. -f2 | cut -d= -f1 || true); do if [ "$(uci -q get firewall."$_z".name)" = 'wan' ]; then uci -q del_list firewall."$_z".network='usbwan' 2>/dev/null; uci add_list firewall."$_z".network='usbwan'; else uci -q del_list firewall."$_z".network='usbwan' 2>/dev/null; fi; done; true uci commit network uci commit firewall ifup usbwan 2>/dev/null || true echo "USB tethering enabled on $USB_DEV -> usbwan" else echo "USB tethering: no USB network device found, skipping" >&2 fi \ mkdir -p /etc/hotplug.d/usb cat > /etc/hotplug.d/usb/99-usb-tether-adb << 'HOTPLUG_EOF' [ "$ACTION" = "bind" ] || exit 0 [ "$PRODUCT" ] || exit 0 ( HOME=/root export HOME command -v adb >/dev/null 2>&1 || exit 0 DELAY=1 attempt=0 while [ $attempt -lt 7 ]; do attempt=$((attempt + 1)) sleep $DELAY if ip addr show usb0 2>/dev/null | grep -q "inet "; then exit 0 fi STATE=$(adb get-state 2>/dev/null) if [ "$STATE" = "device" ]; then adb shell svc usb setFunctions rndis 2>/dev/null sleep 3 if ip addr show usb0 2>/dev/null | grep -q "inet "; then logger -t usb-tether "ADB svc enabled tethering (attempt $attempt)" exit 0 fi adb shell input keyevent KEYCODE_WAKEUP 2>/dev/null sleep 1 adb shell am start -a android.settings.TETHER_SETTINGS 2>/dev/null sleep 3 adb shell uiautomator dump /sdcard/ui.xml 2>/dev/null UI=$(adb shell cat /sdcard/ui.xml 2>/dev/null) USB_ROW=$(echo "$UI" | grep -oE 'USB.tethering[^>]*bounds="\[[0-9]+,[0-9]+' | grep -oE '[0-9]+,[0-9]+' | head -1) USB_Y=$(echo "$USB_ROW" | cut -d, -f2) if [ -n "$USB_Y" ]; then USB_Y_CENTER=$((USB_Y + 28)) SWITCH=$(echo "$UI" | grep -oE "Switch[^>]*bounds=\"\[[0-9]+,${USB_Y_CENTER}" | grep -oE '\[[0-9]+,' | grep -oE '[0-9]+' | head -1) if [ -n "$SWITCH" ]; then SWITCH_CENTER=$((SWITCH + 65)) adb shell input tap $SWITCH_CENTER $USB_Y_CENTER 2>/dev/null sleep 3 if ip addr show usb0 2>/dev/null | grep -q "inet "; then logger -t usb-tether "ADB UI toggle enabled tethering (attempt $attempt)" exit 0 fi fi fi fi DELAY=$((DELAY * 2)) done logger -t usb-tether "ADB tethering gave up after $attempt attempts" ) & HOTPLUG_EOF
# --- iPhone USB tethering --- /usr/sbin/usbmuxd -v -U -f >/dev/null 2>&1 & USB_DEV="" for _ in $(seq 1 45); do for dev in /sys/class/net/*; do name=$(basename "$dev") case "$name" in lo|br-lan|eth0|wlan*) continue ;; esac driver=$(readlink "$dev/device/driver" 2>/dev/null || true) if echo "$driver" | grep -qi "ipheth"; then USB_DEV="$name" break 2 fi iface_desc="" [ -e "$dev/device/interface" ] && iface_desc=$(cat "$dev/device/interface" 2>/dev/null || true) if echo "$iface_desc" | grep -qi "apple\|iphone"; then USB_DEV="$name" break 2 fi done sleep 1 done if [ -z "$USB_DEV" ]; then for cand in usb0 usb1; do [ -e "/sys/class/net/$cand" ] && USB_DEV="$cand" && break done fi if [ -n "$USB_DEV" ]; then uci set network.usbwan=interface uci set network.usbwan.proto='dhcp' uci set network.usbwan.device="$USB_DEV" uci set network.usbwan.metric='20' for _z in $(uci show firewall 2>/dev/null | grep '=zone' | cut -d. -f2 | cut -d= -f1 || true); do if [ "$(uci -q get firewall."$_z".name)" = 'wan' ]; then uci -q del_list firewall."$_z".network='usbwan' 2>/dev/null; uci add_list firewall."$_z".network='usbwan'; else uci -q del_list firewall."$_z".network='usbwan' 2>/dev/null; fi; done; true uci commit network uci commit firewall ifup usbwan 2>/dev/null || true echo "USB tethering enabled on $USB_DEV -> usbwan" else echo "USB tethering: no USB network device found, skipping" >&2 fi
# --- TollGate: nodogsplash captive portal --- uci set nodogsplash.@nodogsplash[0].enabled='1' uci set nodogsplash.@nodogsplash[0].clientid='mac' uci -q del_list nodogsplash.@nodogsplash[0].users_to_router='allow tcp port 2121' 2>/dev/null || true uci add_list nodogsplash.@nodogsplash[0].users_to_router='allow tcp port 2121' uci -q del_list nodogsplash.@nodogsplash[0].users_to_router='allow tcp port 2050' 2>/dev/null || true uci add_list nodogsplash.@nodogsplash[0].users_to_router='allow tcp port 2050' uci commit nodogsplash /etc/init.d/nodogsplash enable /etc/init.d/nodogsplash restart # --- TollGate: hotplug (wwan ifup \u2192 tollgate restart) --- mkdir -p /etc/hotplug.d/iface && printf '%s' '#!/bin/sh [ "$ACTION" = "ifup" ] || exit 0 [ "$INTERFACE" = "wwan" ] || exit 0 logger -t tollgate-hotplug "wwan up \u2014 restarting tollgate" /etc/init.d/tollgate-wrt restart & ' > /etc/hotplug.d/iface/95-tollgate-restart && chmod +x /etc/hotplug.d/iface/95-tollgate-restart # --- TollGate: ipk installed separately via deploy_tollgate_post_flash() ---
# --- cellular: WAN (wan) = modem ECM interface (dhcp from modem) --- uci -q revert network.wan || true uci set network.wan.proto='dhcp' uci set network.wan.device='usb0' uci set network.wan.metric='5' uci commit network G1="$(uci -q get network.wan.proto)"; G2="$(uci -q get network.wan.device)"; [ "$G1" = dhcp ] && [ "$G2" = usb0 ] || { echo "cellular: readback FAILED (proto=$G1 device=$G2)"; exit 1; } # --- cellular: modem one-shot (ECM + slot + APN; detached, idempotent) --- mkdir -p /usr/bin && printf '%s' '\ #!/bin/sh # Cellular modem bring-up (one-shot, idempotent). Validated 2026-10-24. # Timings: USB enum ~35-50s after CFUN; roaming registration up to ~135s. # NEVER substitute uqmi status calls — the uqmi hang class hangs SSH # sessions against this modem family; the AT port is the truth. MARKER=/etc/cellular-modem.done [ -f "$MARKER" ] && exit 0 ( N=0 while [ $N -lt 30 ]; do for p in /dev/ttyUSB0 /dev/ttyUSB1 /dev/ttyUSB2 /dev/ttyUSB3; do [ -c "$p" ] || continue f=/tmp/atdet.$$ cat "$p" > "$f" & cp=$! sleep 1 printf '\''AT\r'\'' > "$p" sleep 2 kill $cp 2>/dev/null grep -q OK "$f" 2>/dev/null && { ATPORT=$p; rm -f "$f"; break; } rm -f "$f" done [ -n "${ATPORT:-}" ] && break N=$((N+1)); sleep 5 done [ -z "${ATPORT:-}" ] && { logger -t cellular-setup "no AT port"; exit 1; } at() { f=/tmp/atcmd.$$; # watchdog-bounded read (L20/L21): never leak a reader, never block forever ( stty -F $ATPORT 115200 raw -echo min 1 time 5 2>/dev/null; dd bs=1 count=400 <$ATPORT >$f 2>/dev/null ) & c=$!; sleep 1; printf '\''%s\r'\'' "$1" > $ATPORT; sleep 4; kill $c 2>/dev/null; wait $c 2>/dev/null; grep -qE '\''^\+|^OK'\'' $f 2>/dev/null; rc=$?; rm -f $f; return $rc; } FAIL=0 # ECM mode: auto-dial + DHCP inside the modem; persists in modem NVM. at '\''AT+QCFG="usbnet",1'\'' || { logger -t cellular-setup "usbnet set failed"; FAIL=1; } # SIM slot: dual-slot modems must point at the card with the profile. if [ -n "" ]; then at "AT+QUIMSLOT=" || { logger -t cellular-setup "slot set failed"; FAIL=1; }; fi # APN on cid 1 (matches the modem auto-dial in ECM mode). at '\''AT+CGDCONT=1,"IPV4V6",""'\'' || { logger -t cellular-setup "apn set failed"; FAIL=1; } # Reset applies mode+slot; USB re-enumerates (~40s); registration <=135s. at '\''AT+CFUN=1,1'\'' || { logger -t cellular-setup "cfun reset failed"; FAIL=1; } sleep 45 ifup wan 2>/dev/null # Marker only on full success — a failed setup must retry on next boot. if [ "$FAIL" = 0 ]; then touch "$MARKER" logger -t cellular-setup "modem setup complete (at=$ATPORT)" else logger -t cellular-setup "modem setup FAILED — will retry on next boot" fi ) & exit 0 ' > /usr/bin/cellular-setup && chmod +x /usr/bin/cellular-setup && ( /usr/bin/cellular-setup || true ) # --- cellular: hotplug re-up (usb0 ifup -> wan) --- mkdir -p /etc/hotplug.d/iface && printf '%s' '#!/bin/sh [ "$ACTION" = ifup ] || exit 0 [ "$INTERFACE" = "usb0" ] || exit 0 ifup wan 2>/dev/null ' > /etc/hotplug.d/iface/96-cellular-up && chmod +x /etc/hotplug.d/iface/96-cellular-up /etc/init.d/network reload
# --- TollGate Security: RFC 1918 isolation --- uci set firewall.Block-LAN-To-RFC1918-10=rule uci set firewall.Block-LAN-To-RFC1918-10.name='Block-LAN-To-RFC1918-10' uci set firewall.Block-LAN-To-RFC1918-10.src='lan' uci set firewall.Block-LAN-To-RFC1918-10.dest='wan' uci set firewall.Block-LAN-To-RFC1918-10.dest_ip='10.0.0.0/8' uci set firewall.Block-LAN-To-RFC1918-10.proto='all' uci set firewall.Block-LAN-To-RFC1918-10.family='ipv4' uci set firewall.Block-LAN-To-RFC1918-10.target='DROP' # Block-LAN-To-RFC1918-10: RFC 1918 private (class A) uci set firewall.Block-LAN-To-RFC1918-172=rule uci set firewall.Block-LAN-To-RFC1918-172.name='Block-LAN-To-RFC1918-172' uci set firewall.Block-LAN-To-RFC1918-172.src='lan' uci set firewall.Block-LAN-To-RFC1918-172.dest='wan' uci set firewall.Block-LAN-To-RFC1918-172.dest_ip='172.16.0.0/12' uci set firewall.Block-LAN-To-RFC1918-172.proto='all' uci set firewall.Block-LAN-To-RFC1918-172.family='ipv4' uci set firewall.Block-LAN-To-RFC1918-172.target='DROP' # Block-LAN-To-RFC1918-172: RFC 1918 private (class B) uci set firewall.Block-LAN-To-RFC1918-192=rule uci set firewall.Block-LAN-To-RFC1918-192.name='Block-LAN-To-RFC1918-192' uci set firewall.Block-LAN-To-RFC1918-192.src='lan' uci set firewall.Block-LAN-To-RFC1918-192.dest='wan' uci set firewall.Block-LAN-To-RFC1918-192.dest_ip='192.168.0.0/16' uci set firewall.Block-LAN-To-RFC1918-192.proto='all' uci set firewall.Block-LAN-To-RFC1918-192.family='ipv4' uci set firewall.Block-LAN-To-RFC1918-192.target='DROP' # Block-LAN-To-RFC1918-192: RFC 1918 private (class C — ISP router LAN) uci set firewall.Block-LAN-To-RFC1918-LL=rule uci set firewall.Block-LAN-To-RFC1918-LL.name='Block-LAN-To-RFC1918-LL' uci set firewall.Block-LAN-To-RFC1918-LL.src='lan' uci set firewall.Block-LAN-To-RFC1918-LL.dest='wan' uci set firewall.Block-LAN-To-RFC1918-LL.dest_ip='169.254.0.0/16' uci set firewall.Block-LAN-To-RFC1918-LL.proto='all' uci set firewall.Block-LAN-To-RFC1918-LL.family='ipv4' uci set firewall.Block-LAN-To-RFC1918-LL.target='DROP' # Block-LAN-To-RFC1918-LL: RFC 3927 link-local (mDNS, SSDP) uci commit firewall /etc/init.d/firewall restart # --- WiFi client isolation --- for _s in $(uci show wireless 2>/dev/null | grep '=wifi-iface' | cut -d. -f2 | cut -d= -f1); do uci set wireless.$_s.isolate='1'; done; true uci commit wireless wifi reload
# --- Travelmate travel router --- uci set travelmate.global.trm_enabled='1' uci set travelmate.global.trm_automatic='1' uci set travelmate.global.trm_captive='1' uci set travelmate.global.trm_timeout='60' uci set travelmate.global.trm_retry='5' uci set travelmate.global.trm_radio='radio0' uci commit travelmate /etc/init.d/travelmate enable /etc/init.d/travelmate restart echo "Travelmate configured: auto-connect on radio0"
# vpn-nordvpn: NOT CONFIGURED — set [use_cases.vpn-nordvpn] config_file (path to a WireGuard .conf) or access_token in config.toml
# vpn-nordvpn-ikev2: NOT CONFIGURED — set access_token in config.toml [use_cases.vpn-nordvpn-ikev2] (EAP credentials are derived from it at build time), or pass resolved statics (username + password + station)
# vpn-nordvpn: openvpn technologies require access_token (service credentials are fetched from it at build time)
# vpn-pia-openvpn: NOT CONFIGURED — set username (p-number) and password in config.toml [use_cases.vpn-pia-openvpn]
# --- WireGuard VPN client --- uci set network.wg0=interface uci set network.wg0.proto='wireguard' uci set network.wg0.private_key='generate' uci set network.wg0.addresses='10.67.0.2/32' uci set network.wg0_peer=wireguard_wg0 uci set network.wg0_peer.public_key='' uci set network.wg0_peer.endpoint_host='' uci set network.wg0_peer.endpoint_port='51820' uci set network.wg0_peer.allowed_ips='0.0.0.0/0, ::/0' uci set network.wg0_peer.route_allowed_ips='1' uci set network.wg0_peer.persistent_keepalive='25' # --- Cleanup stale anonymous WireGuard firewall sections --- while uci show firewall 2>/dev/null | grep -q "name='vpn'"; do _s=$(uci show firewall 2>/dev/null | grep "name='vpn'" | head -1 | cut -d. -f2 | cut -d= -f1); uci delete "firewall.$_s" 2>/dev/null; done; true while uci show firewall 2>/dev/null | grep -q "dest='vpn'"; do _s=$(uci show firewall 2>/dev/null | grep "dest='vpn'" | head -1 | cut -d. -f2 | cut -d= -f1); uci delete "firewall.$_s" 2>/dev/null; done; true while uci show firewall 2>/dev/null | grep -q "name='KillSwitch-Reject-NonVPN'"; do _s=$(uci show firewall 2>/dev/null | grep "name='KillSwitch-Reject-NonVPN'" | head -1 | cut -d. -f2 | cut -d= -f1); uci delete "firewall.$_s" 2>/dev/null; done; true # --- WireGuard firewall zone + forwarding --- uci set firewall.wg_client_vpn=zone uci set firewall.wg_client_vpn.name='vpn' uci set firewall.wg_client_vpn.input='REJECT' uci set firewall.wg_client_vpn.output='ACCEPT' uci set firewall.wg_client_vpn.forward='REJECT' uci set firewall.wg_client_vpn.masq='1' uci set firewall.wg_client_vpn.mtu_fix='1' uci set firewall.wg_client_vpn.network='wg0' uci set firewall.wg_client_fwd=forwarding uci set firewall.wg_client_fwd.src='lan' uci set firewall.wg_client_fwd.dest='vpn' uci set firewall.wg_client_killswitch=rule uci set firewall.wg_client_killswitch.name='KillSwitch-Reject-NonVPN' uci set firewall.wg_client_killswitch.src='lan' uci set firewall.wg_client_killswitch.dest='wan' uci set firewall.wg_client_killswitch.proto='all' uci set firewall.wg_client_killswitch.target='REJECT' uci commit network uci commit firewall /etc/init.d/network restart echo 'WireGuard client configured: :51820' i=0; while [ $i -lt 30 ]; do if wg show wg0 2>/dev/null | grep -q 'latest handshake'; then echo 'WG kill-switch: tunnel verified'; exit 0; fi; sleep 1; i=$((i+1)); done; echo 'WG kill-switch: tunnel failed — removing kill switch to restore WAN'; uci delete firewall.wg_client_killswitch; uci commit firewall; fw4 restart
# --- WireGuard VPN server --- uci set network.wg0=interface uci set network.wg0.proto='wireguard' uci set network.wg0.private_key='generate' uci set network.wg0.listen_port='51820' uci add_list network.wg0.addresses='10.1.99.1/24' mkdir -p /etc/wireguard && wg show wg0 public-key > /etc/wireguard/server_public_key 2>/dev/null || true while uci show firewall 2>/dev/null | grep -q "name='vpn'"; do _s=$(uci show firewall 2>/dev/null | grep "name='vpn'" | head -1 | cut -d. -f2 | cut -d= -f1); uci delete "firewall.$_s" 2>/dev/null; done; true while uci show firewall 2>/dev/null | grep -q "name='Allow-WireGuard'"; do _s=$(uci show firewall 2>/dev/null | grep "name='Allow-WireGuard'" | head -1 | cut -d. -f2 | cut -d= -f1); uci delete "firewall.$_s" 2>/dev/null; done; true uci set firewall.wg_server_vpn=zone uci set firewall.wg_server_vpn.name='vpn' uci set firewall.wg_server_vpn.input='ACCEPT' uci set firewall.wg_server_vpn.output='ACCEPT' uci set firewall.wg_server_vpn.forward='REJECT' uci set firewall.wg_server_vpn.masq='1' uci set firewall.wg_server_vpn.mtu_fix='1' uci set firewall.wg_server_vpn.network='wg0' uci set firewall.wg_server_fwd_lan=forwarding uci set firewall.wg_server_fwd_lan.src='vpn' uci set firewall.wg_server_fwd_lan.dest='lan' uci set firewall.wg_server_fwd_wan=forwarding uci set firewall.wg_server_fwd_wan.src='vpn' uci set firewall.wg_server_fwd_wan.dest='wan' uci set firewall.wg_server_allow=rule uci set firewall.wg_server_allow.name='Allow-WireGuard' uci set firewall.wg_server_allow.src='wan' uci set firewall.wg_server_allow.dest_port='51820' uci set firewall.wg_server_allow.proto='udp' uci set firewall.wg_server_allow.target='ACCEPT' uci commit network uci commit firewall /etc/init.d/network restart echo "WireGuard server configured: port 51820, subnet 10.1.99.1/24"