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.
NAND 128MB (GigaDevice SPI NAND) + NOR 16MB (Winbond W25Q128)
defined
tested
ethernetwifiusb
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)
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
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
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-ostested2026-10-10
netboot-rescuetested2026-10-08
usb-imagetested2026-10-08
eeprom-update-netboottested2026-10-08
Ubiquiti
EdgeRouter 6P (ER-e300) — Cavium Octeon III, 5x GbE + SFP, no WiFi ubnt-edgerouter-6p
cellular_data_esimtestedsilent.link 9eSIM (Telia roaming profile), bench cold boot through a PoE switch2026-11-05
cellular_data_remote_saptestedAndroid phone SIM over Bluetooth SAP (wwand + wwand-rsim), bench cold boot through a PoE switch2026-11-05
wifi_aptested2026-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
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
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.
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)
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.
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.
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.
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)
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.
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
# --- 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"
# --- 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"
# --- 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)"
# --- 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"
# --- 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
# --- 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"