→ Device Support Matrix — supported devices, flash methods, and tested hardware.
A framework for flashing routers with OpenWrt, with a 2-stage workflow:
- AI-assisted discovery (
prompts/): Encounter an unknown router, identify it, fingerprint its attack surface, plan a capture strategy, and analyze pcap artifacts to determine flash methods and boot signatures. This is how new router models get onboarded into conwrt. - Automated flashing (
scripts/+models/): Once a model is defined with its flash method and boot signatures, subsequent flashes of that model are fully automated -- detect recovery mode, upload firmware, verify, and collect inventory.
DISCLAIMER: conwrt flashes firmware onto real hardware. You must have legal authority to modify any device you use it on. Verify model definitions and firmware images before every flash. The authors accept no liability for bricked devices or data loss.
- macOS (uses
sayfor voice guidance,ifconfigfor interface detection) tcpdumpandsudofor pcap capture and network monitoring- Python 3.10+
- An ethernet cable and a router to flash
Hardware-safe development and CI do not require a router:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e '.[dev]'
make ciUseful targets:
| Target | What it does | Hardware safe |
|---|---|---|
make lint |
Ruff plus shell syntax checks | Yes |
make typecheck |
Pyright static checks | Yes |
make validate-models |
Validate every models/*.json against schemas/model.schema.json |
Yes |
make test |
Python unit tests plus the existing smoke test | Yes |
make ci |
lint + typecheck + schemas + models + tests | Yes |
make ipk |
Build conwrt as an arch-independent OpenWrt ipk package | Yes |
Do not run real flashing, sysupgrade, SSH, SCP, TFTP, tcpdump, serial, ASU, or other network-mutating commands from tests. Use mocks/stubs only. Commands such as python3 scripts/conwrt.py ..., scripts/tftp-server.py, and router SSH/SCP helpers can mutate real devices and should only be run intentionally against hardware you control.
| Method | How it works | Requires | Speed |
|---|---|---|---|
| sysupgrade | SSH + SCP, runs sysupgrade -n | Running OpenWrt + SSH | ~3 min |
| recovery-http | Reset button, uboot HTTP server | Physical access + reset pin | ~2 min |
| dlink-hnap | HNAP SOAP API upload via stock firmware web UI | Stock D-Link firmware, network access | ❌ validation blocks flash |
| tftp | TFTP server for uboot network boot | Serial or uboot access | varies |
| extreme-rdwr-tftp | SSH to stock → rdwr_boot_cfg → TFTP boot initramfs → sysupgrade | Stock Extreme firmware, SSH access | ~5 min |
| zycast | Multicast to many devices simultaneously | Network broadcast domain | varies |
See device support matrix — auto-generated from models/*.json (the authoritative single source of truth).
Tested devices are tracked in each model's tested_hardware field. To mark a device as tested, add a tested_hardware entry to its JSON:
"tested_hardware": {
"recovery-http": { "tested": true, "date": "2026-05-16", "notes": "..." },
"wifi_sta_ap": { "tested": true, "date": "2026-05-16", "notes": "..." }
}conwrt/
├── models/ # Device model definitions (species-level, static, in git)
│ └── *.json # See device matrix for full list
├── scripts/ # Automated flashing and management scripts
│ ├── conwrt.py # Main flasher (auto-detect, sysupgrade, U-Boot recovery)
│ ├── router-fingerprint.py # SSH-based device fingerprinting and inventory
│ ├── firmware-manager.py # ASU firmware build/download/cache management
│ ├── model_loader.py # Shared model registry reader
│ ├── router-probe.py # Device boot state detection (off/uboot/openwrt)
│ ├── inventory.py # Inventory utilities
│ ├── extreme_ap391x_analyze.py # Extreme AP391x firmware image analysis
│ └── use_cases/ # Use case presets (auto-discovered plugins)
├── data/ # Runtime data (gitignored)
│ ├── inventory.jsonl # Append-only device inventory (specimen-level)
│ └── *.bin # Cached firmware images
├── captures/ # Pcap captures (gitignored)
├── images/ # ASU firmware cache (gitignored)
├── recipes/ # Device-specific procedures and notes
├── prompts/ # AI-assisted discovery step templates
│ ├── step-01-identify-device.md
│ ├── step-02-fingerprint-surface.md
│ ├── step-03-plan-capture.md
│ └── step-04-analyze-artifacts.md
├── docs/ # Process and documentation
├── examples/ # Example artifacts (redacted)
└── README.md
conwrt has two stages. Stage 1 is for new devices not yet in models/. Stage 2 is for known devices.
When you encounter a router that isn't in the models directory, walk through the prompt templates in prompts/ with an LLM:
- Identify device (
step-01-identify-device.md) -- determine make, model, hardware revision - Fingerprint surface (
step-02-fingerprint-surface.md) -- map exposed services, boot behavior, recovery interfaces - Plan capture (
step-03-plan-capture.md) -- design a pcap capture strategy to observe the boot sequence - Analyze artifacts (
step-04-analyze-artifacts.md) -- extract boot signatures, recovery mode patterns, and flash method from captures
The output of this process is a model JSON for models/ and recipe notes for recipes/. Once committed, that device model moves to Stage 2.
For any device already defined in models/, the scripts handle everything end-to-end:
# Auto-detect device and flash with custom ASU image
python3 scripts/conwrt.py --request-image --wan-sshpython3 scripts/conwrt.py --model-id dlink-covr-x1860-a1 --image firmware.binpython3 scripts/conwrt.py --model-id dlink-covr-x1860-a1 \
--request-image --ssh-key ~/.ssh/id_ed25519.pub --no-passwordpython3 scripts/conwrt.py --model-id dlink-covr-x1860-a1 \
--request-image --no-password --wan-sshpython3 scripts/conwrt.py --model-id dlink-covr-x1860-a1 \
--image fw.bin --no-uploadPreview operator profile from config.toml without changing a router:
python3 scripts/conwrt.py profile plan --model-id dlink-covr-x1860-a1
python3 scripts/conwrt.py configure --dry-run --ip 192.168.1.1
python3 scripts/firmware-manager.py request --profile dlink_covr-x1860-a1 --dry-runUse cases and packages are defined once in scripts/use_cases/ and applied via both ASU builds and post-install SSH.
python3 scripts/conwrt.py list# List all cached builds
python3 scripts/conwrt.py cache list
# Remove old builds, keep latest per model
python3 scripts/conwrt.py cache clean --keep-latest
# Remove all builds for a specific model
python3 scripts/conwrt.py cache clean --model-id dlink_covr-x1860-a1# Auto-detect router at default gateway
python3 scripts/router-fingerprint.py
# Target a specific IP
python3 scripts/router-fingerprint.py --ip 192.168.1.1
# Save to file
python3 scripts/router-fingerprint.py --ip 192.168.1.1 --output fingerprint.json| Option | Description |
|---|---|
--model-id ID |
Model ID from models/ (auto-detected if device is running OpenWrt) |
--image PATH |
Path to firmware image |
--request-image |
Request custom image from ASU with baked-in settings |
--ssh-key PATH |
SSH public key to embed (auto-detected: id_ed25519.pub or id_rsa.pub) |
--password PASS |
Set root password (default: random, printed once, saved in inventory) |
--no-password |
Skip password, key-only auth |
--wan-ssh |
Open SSH on WAN interface (disables password login on WAN) |
--force-uboot |
Force U-Boot recovery even if OpenWrt is running |
--no-voice |
Disable voice guidance |
--no-upload |
Dry run, detect only |
--interface IFACE |
Ethernet interface (auto-detected) |
--capture PATH |
Save pcap capture |
--transport ssh|ubus |
Transport for configure command: SSH shell or ubus HTTP |
--version |
Print version and exit |
models/ holds species-level data: what a COVR-X1860 is, how to flash it, what its boot signatures look like. Static, checked into git.
data/inventory.jsonl holds specimen-level data: this specific unit's MAC address, serial number, SSH key fingerprint, and full flash timeline. Gitignored, append-only, stays local.
Each model JSON contains vendor info, OpenWrt target/device/arch, hardware specs (SoC, flash, RAM, ports, Wi-Fi), MAC OUI prefixes for device identification, flash method definitions, and boot milestone patterns from pcap analysis.
- Event-driven state machine with real-time pcap monitoring
- Auto-detects device state (OpenWrt, U-Boot, offline) and picks sysupgrade or U-Boot recovery
- Optional model ID — auto-detected from SSH fingerprint when device is running
- Voice guidance via macOS
say(user actions and milestones only) - Full timeline tracking: power off through SSH verification
- SHA-256 firmware verification
- SSH verification and inventory collection after flash
- ASU integration for custom firmware builds with baked-in SSH keys, passwords, WAN SSH
- Use case presets — flash OpenWrt pre-configured for tethering, SQM, VPN, guest WiFi, etc.
- Transport-agnostic ops pipeline — each use case generates typed operations that render to shell or ubus HTTP
- Firmware cache management (
conwrt cache list/clean) - Automatic ethernet interface detection
- Link monitoring survives pcap writer death during reboot
- conwrt ipk package — install on OpenWrt routers for router-to-router flashing
conwrt doesn't just install OpenWrt — it installs OpenWrt pre-configured for a specific use case. Instead of flashing a stock image and then reading wiki docs to manually configure your router, you declare what you want in config.toml and the firmware arrives ready to go.
Status: tether tested on hardware (GL.iNet MT3000, Android RNDIS). All other presets are untested — the uci commands and package lists are based on OpenWrt wiki documentation and community guides. They need real-device validation before being considered production-ready.
Each preset is a single Python file in scripts/use_cases/ that declares:
- Packages to include in the ASU firmware build
- A shell script of uci commands that runs on first boot
- Required hardware capabilities (auto-skipped if your device lacks USB, WiFi, etc.)
Enable them in config.toml:
[use_cases]
enabled = ["tether-android", "sqm"]
[use_cases.sqm]
download_kbps = 340000
upload_kbps = 19000Or discover what's available:
python3 scripts/conwrt.py list-use-cases
python3 scripts/conwrt.py list-use-cases --model-id glinet-mt3000These use cases are the most immediately useful because they require almost no user configuration:
| Preset | What it does | User provides |
|---|---|---|
| tether | USB WAN from Android or iPhone (auto-detects) | Plug in USB cable |
| sqm | Smart Queue Management with CAKE — eliminates bufferbloat | Download/upload speeds in Kbit/s |
| travelmate | Auto-connect to hotel/airport WiFi with captive portal detection | Nothing (auto-scans) |
These are the "flash and forget" cases — no wiki reading, no manual uci editing. Flash the image, plug in your phone or connect to upstream WiFi, and it works.
| Preset | Description | Post-flash? |
|---|---|---|
tether |
Auto-detect Android or iPhone USB WAN. Android gets ADB auto-enable. | No |
tether-android |
USB WAN from Android. Enable tethering manually on the phone. | No |
tether-android-adb |
USB WAN from Android + ADB auto-enable. Confirm on phone, auto-activates. | No |
tether-ios |
USB WAN from iPhone. Enable Personal Hotspot manually on the phone. | No |
sqm |
Bufferbloat fix via CAKE/fq_codel (manual speeds) | No |
auto-sqm |
Auto-measure WAN speed + configure SQM (experimental) | No |
guest-wifi |
Isolated guest WiFi with separate subnet, DHCP, and firewall zone | No |
doh |
DNS-over-HTTPS via https-dns-proxy for encrypted DNS | No |
mwan3 |
Multi-WAN failover or load balancing | No |
travelmate |
Auto-connect to captive portal WiFi | No |
tollgate |
Bitcoin/Lightning payment gateway (ipk from GitHub CI) | Yes (ipk deploy) |
wireguard-client |
VPN tunnel (auto-generates keys per device) | Auto (registration) |
wireguard-server |
VPN server for remote access | Yes (QR codes, peers) |
adguard |
Network-wide ad blocking | Yes (web setup wizard) |
openclash |
Transparent proxy for censorship bypass | Yes (subscription import) |
Presets requiring post-flash setup need SSH access after first boot to complete configuration (importing VPN configs, running setup wizards, etc.).
Status: Tested on hardware (D-Link COVR-X1860 A1). WireGuard client, key generation, and post-flash registration all verified.
conwrt handles WireGuard in two stages: firmware build (use case preset) and post-flash registration (automatic). Each device auto-generates its own Curve25519 keypair on first boot — no private keys ever exist in the firmware image.
Enable the wireguard-client use case in config.toml. This bakes wireguard-tools and a first-boot uci script into the firmware image. On first boot, OpenWrt generates a unique private key and configures the wg0 interface with your server's endpoint.
The firmware includes:
wg0WireGuard interface withprivate_key='generate'(unique per device)- Peer config (server public key, endpoint, allowed IPs)
- Firewall
vpnzone + LAN→VPN forwarding - Optional kill switch (block all traffic if VPN drops)
After the router boots, conwrt automatically registers the device with your VPN server. This requires a [wireguard] section in config.toml with an SSH alias to your VPN server:
[wireguard]
registration_server = "my-vpn-server" # SSH alias from ~/.ssh/config
wg_interface = "wg0" # WireGuard interface on the serverThe registration flow:
- SSH to router → read the auto-generated public key via
wg show wg0 public-key - SSH to VPN server → run
wg set wg0 peer <pubkey> allowed-ips <address>(live registration) - Append
[Peer]block to/etc/wireguard/wg0.confon the server (persistence) - Save public key to device inventory
The router's SSH key must be in the VPN server's authorized_keys. conwrt uses BatchMode=yes (key-only auth) for the server connection.
[use_cases]
enabled = ["wireguard-client"]
[use_cases.wireguard-client]
peer_public_key = "SERVER_PUBLIC_KEY"
endpoint_host = "vpn.example.com"
endpoint_port = 51820
address = "10.0.0.2/32"
allowed_ips = "10.0.0.0/24" # only management subnet through tunnel
kill_switch = false
[wireguard]
registration_server = "my-vpn-server"
wg_interface = "wg0"[use_cases]
enabled = ["wireguard-client"]
[use_cases.wireguard-client]
peer_public_key = "SERVER_PUBLIC_KEY"
endpoint_host = "vpn.example.com"
address = "10.0.0.2/32"
kill_switch = true # block all traffic if VPN drops
[wireguard]
registration_server = "my-vpn-server"
wg_interface = "wg0"- Same firmware image works for all devices — each generates unique keys on first boot
- Private key never leaves the router (generated on-device, stored in UCI overlay)
- Keys survive
sysupgrade(UCI overlay preserved by default) - Public key recorded in inventory for auditing and re-registration
- No secrets committed to git —
config.tomlis gitignored
wg-setup.py can apply WireGuard config post-flash from pre-generated server peer configs, without going through the full conwrt flash flow:
python3 scripts/wg-setup.py --peer 3 --server my-vpn-hostThe long-term vision is an interactive menu system that interviews the user before flashing: "Do you want USB tethering? SQM? A VPN?" — then builds a firmware image with everything pre-configured. The current config.toml approach is the declarative foundation for that interactive layer.
conwrt automatically configures WiFi after flashing based on [network.sta] and [network.ap] in config.toml. No manual uci editing needed.
Status: Tested on hardware (D-Link COVR-X1860 A1). Radio auto-detection verified: radio0=2.4GHz, radio1=5GHz.
- Post-flash SSH (default): After flashing and SSH verification, conwrt detects the correct radio for each band via
uci get wireless.radioN.bandand applies STA/AP uci commands over SSH. - ASU first-boot (via
--request-image): The same uci commands are baked into the firmware's first-boot script, so WiFi is configured before first SSH.
Both flows use the same radio detection logic — iterate radio0..radio3, check band option, match to the configured band.
[network.sta]
band = "5ghz"
ssid = "UpstreamNetwork"
encryption = "psk2"
key = "passphrase"The router connects to the upstream network on the 5GHz radio (auto-detected) and uses it as WAN. Firewall zone is wan.
[network.sta]
band = "5ghz"
ssid = "UpstreamNetwork"
encryption = "psk2"
key = "passphrase"
[network.ap]
band = "2.4ghz"
ssid = "MyNetwork"
encryption = "psk2"
key = "network-password"STA on 5GHz for upstream WAN, AP on 2.4GHz for local clients. Both radios configured independently.
| Config value | OpenWrt band | Notes |
|---|---|---|
2.4ghz |
2g |
802.11b/g/n/ax |
5ghz |
5g |
802.11a/n/ac/ax |
5ghz-low |
5g |
Lower 5GHz channels (UNII-1) |
5ghz-high |
5g |
Upper 5GHz channels (UNII-3) |
6ghz |
6g |
WiFi 6E (if hardware supports) |
conwrt can flash D-Link routers running stock firmware without entering recovery mode, directly through the manufacturer's web UI API.
How it works: The HNAP (Home Network Administration Protocol) SOAP API accepts firmware uploads when properly authenticated. conwrt performs a challenge-response login (HMAC-MD5 + custom AES), uploads the OpenWrt factory image via multipart POST, and triggers the flash via GetFirmwareValidation.
GetFirmwareValidation returns IsValid: false for OpenWrt images. The firmware upload API accepts the binary (returns OK), and the device reboots, but the bootloader-level validation rejects non-D-Link firmware and boots back to stock. The GPL RSA signing key (password: 12345678) is a test key — production devices use different keys. No router has ever been successfully flashed via HNAP. Use recovery-http (U-Boot) for reliable flashing.
Advantages over recovery-http (when it works):
- No physical reset button press needed
- Works remotely over the network
- Faster setup (no recovery mode dance)
Requirements:
- Router running D-Link stock firmware with known admin password
- Network access to the router's web UI (HTTP)
- OpenWrt factory image (not sysupgrade)
Usage:
python3 scripts/conwrt.py --model-id dlink-covr-x1860-a1 \
--image firmware.bin --flash-method dlink-hnapThe default password ("password") and API endpoints are defined in the model JSON.
conwrt can run FROM an OpenWrt router to flash another router, or from macOS/Linux, with multiple flash methods. Router-to-router provisioning and stock-firmware flashing are both supported.
Status: Tested. x1860 to x1860 recovery-http verified, WiFi STA/AP post-flash config verified. HNAP auth + upload API verified working but does NOT produce a successful flash — stock firmware validation blocks OpenWrt images.
-
Flash the host router with OpenWrt (via conwrt from macOS/Linux). Configure
[network.sta]in config.toml so it gets WiFi WAN after flashing. -
Install dependencies on the host OpenWrt router:
opkg update opkg install python3-base python3-light python3-urllib python3-json \ python3-codecs python3-ctypes python3-email python3-logging \ python3-openssl python3-struct python3-fcntl curl
See
scripts/openwrt-requirements.txtfor the full list. -
Copy conwrt to the router:
scp -O -r scripts/ root@<host-ip>:/tmp/conwrt/ scp -O models/<model>.json root@<host-ip>:/tmp/conwrt/models/ scp -O firmware.bin root@<host-ip>:/tmp/conwrt/
-
Run conwrt from the router:
ssh root@<host-ip> cd /tmp/conwrt/scripts python3 conwrt.py --model-id dlink-covr-x1860-a1 \ --image /tmp/conwrt/firmware.bin --no-pcap --no-voice
The interface is auto-detected as
br-lanon OpenWrt.
The host router's LAN interface gets a temporary IP alias on the recovery subnet (e.g. 192.168.0.10/24). The target router's uboot recovery HTTP server is detected via curl, firmware is uploaded via HTTP POST, and boot completion is verified via SSH polling.
Important: The SSH session to the host router will drop when the interface is reconfigured. Run in background:
python3 conwrt.py --model-id dlink-covr-x1860-a1 \
--image /tmp/conwrt/firmware.bin --no-pcap --no-voice \
> /tmp/conwrt/flash.log 2>&1 &
disown %1| Method | Status | Notes |
|---|---|---|
| sysupgrade | Supported | SSH/SCP via Dropbear, works with any sysupgrade-capable model |
| recovery-http | Tested | Tested x1860 to x1860, polling-only mode |
| dlink-hnap | ❌ upload OK, flash fails | HNAP auth + upload works, but GetFirmwareValidation rejects OpenWrt images — no successful flash ever recorded |
| tftp | Untested | Uses bundled scripts/tftp-server.py (no dnsmasq dependency) |
| extreme-rdwr-tftp | Untested | SSH to stock → rdwr_boot_cfg writes U-Boot vars → TFTP boot initramfs → backup → sysupgrade |
| zycast (multicast) | Untested | Pure Python fallback when C binary unavailable (OpenWrt/MIPS) |
| serial | Not yet | Requires USB-serial adapter |
| Mode | Requires | Events Detected |
|---|---|---|
| scapy (full) | python3-scapy | All: ARP, HTTP, ICMPv6, UDP |
| tcpdump (events) | tcpdump only | All: parsed from tcpdump output |
| polling-only | curl + ssh | Limited: link state + SSH availability |
On OpenWrt, tcpdump event monitoring is recommended — install via opkg install tcpdump.
Use --no-pcap for polling-only mode (no tcpdump needed).
All captures, images, and data directories are gitignored. SSH key user@host comments are stripped before embedding in firmware. Inventory stays local. No personal data in model definitions.
Standard PR-based workflow. Device contributions should include:
- Model JSON in
models/ - Recipe notes in
recipes/ - Boot signatures from pcap analysis
- Tested on real hardware
MIT. See LICENSE.