English · Русский
An OpenWrt-native routing manager for the mihomo proxy.
OpenWrt decides the route. Mihomo handles the proxy.
PureWRT keeps routing decisions in the firewall where they belong: dnsmasq forwards DNS to mihomo's
resolver, gets real-IP answers, and populates nftables sets; nftables matches destination IPs and
TPROXYs only selected traffic into per-section mihomo listeners. Everything else stays direct (or
under mwan3). Rule providers expand into nftset/nftables rather than mihomo's rules: engine — so the
hot path is the kernel, not the proxy.
LAN client
│ DNS query
▼
dnsmasq ──forwards──► mihomo resolver (127.0.0.1:7874) ──► real IP answer
│ │
│ populates nftables sets (per section) ◄────────────────┘
▼
nftables: destination IP in a section's set?
├─ yes → TPROXY into that section's mihomo listener → proxy / VPN / zapret
└─ no → direct (or mwan3 / default route)
The default mode is real-IP nftset routing. fake-ip and full-core modes are intentionally not the default. Unmatched traffic is never forced through the proxy.
A routing section isn't tied to a mihomo proxy. Each section sends its matched traffic through whichever backend you pick:
- mihomo proxy group — the usual proxied path (selector / url-test / load-balance / …).
- VPN — route the section straight out a WireGuard/VPN interface configured on the router, with no proxy involved.
- zapret — apply DPI circumvention to the section's traffic without any proxy or VPN at all.
- direct / reject — bypass everything, or drop it.
So you can mix freely: e.g. streaming via a proxy group, a work subnet out a WireGuard tunnel, censored sites through zapret, and everything else direct. (VPN and Zapret configurations are even preserved across a wizard reset, since they're yours, not imported.)
After installing the purewrt package and the luci-app-purewrt LuCI app:
- Open LuCI → Services → PureWRT (the General page).
- Click Run Setup Wizard.
⚠️ Applying the wizard resets all PureWRT configuration to defaults (subscriptions, providers, routing sections, device assignments, DNS, settings). Only your VPN and Zapret configs, the mihomo binary, and the controller credentials are preserved. - Step 1 — choose a source:
- Use a subscription URL — paste a Clash/Mihomo subscription URL, a proxy list, or a rule list (recommended if your vendor gave you a URL).
- Default lists — pull curated rule lists straight from the published catalog; optionally add a proxy-nodes URL.
- Manual setup — skip import and configure nodes/providers from the regular tabs later.
- Preview what will be imported, then map rule sets to routing sections and set each section's protocol (proxy group / VPN / zapret / direct) on the drag-flow board.
- Set global options (IPv6, updates, DNS), then finish — the wizard imports, generates, and applies in one go.
Three ways, pick whichever fits:
- Wizard — Step 1 → Use a subscription URL (see above). Best for first-time setup.
- LuCI → Proxy Subscriptions tab — add a subscription, paste the URL (and any panel type / headers), Save & Apply. Use this to add a subscription without resetting anything.
- CLI:
purewrt analyze 'https://panel.example/sub/secret?format=clash' # preview only (optional) purewrt import 'https://panel.example/sub/secret?format=clash' # persist it purewrt update # fetch nodes + rules purewrt apply # generate + apply
- Setup wizard — flush-and-start or import; choose a subscription URL or the curated default lists, map rule sets to routing sections, and configure per-section proxy/VPN/zapret in one drag-flow board.
- Proxy subscriptions — Mihomo/Clash YAML, proxy-URI lists, base64 subscriptions, with panel-aware downloads (Remnawave & compatible) and stable router-derived HWID.
- Rule providers — text, MRS (binary, allocation-free streaming decode), and GeoSite/GeoIP from local v2ray dat files. Import curated lists straight from the published catalog with multi-select.
- Sections / routing — group rules into sections, each routed via a proxy group, a VPN, zapret, or direct/reject; per-section proxy strategy, filters, and group type.
- Per-device routing — assign LAN devices to sections by MAC (survives DHCP churn, covers IPv6).
- Mihomo management — browse proxy groups, switch nodes with latency test + connection draining, track upstream alpha releases.
- Zapret integration — bundled, optional, detected at runtime and degraded gracefully when absent.
- mwan3 coexistence — detect-and-coexist multi-WAN; masked fwmark preserves mwan3 bits.
- Diagnostics — domain checker, "What's Blocked Now", and a Client Traffic page that captures live flows + DNS + rejection signals and flags blocked/DPI-stalled/frozen connections with ASN/country enrichment.
- Operations — config export/import (secrets redacted), push notifications (ntfy/webhook) for update failures and subscription expiry, and Prometheus metrics with example Grafana dashboards/alerts.
| Binary / app | Role |
|---|---|
purewrt |
The CLI manager — import, update, generate, apply, diagnose. |
purewrt-check |
Domain classifier (which section/route a hostname resolves to). |
purewrt-api |
Optional local API daemon. |
| LuCI app | luci-app-purewrt — the web UI (tabs above are LuCI views). |
Configuration lives in UCI at /etc/config/purewrt; generated artifacts in /etc/purewrt/generated/*.
On a router running OpenWrt 24.10 (opkg) or 25.12+ (apk), one line:
wget -O - https://mglants.github.io/purewrt/install.sh | shIt auto-detects your release + architecture, adds the signed PureWRT feed and its trust key, swaps in
dnsmasq-full, and installs purewrt + luci-app-purewrt (add WITH_ZAPRET=1 for the optional
zapret DPI-bypass). Then open LuCI → Services → PureWRT → Run Setup Wizard.
Prefer to add the feed and install manually? wget -O - https://mglants.github.io/purewrt/feed.sh | sh
then opkg install purewrt luci-app-purewrt (or apk add …). Feed layout + public keys live at
https://mglants.github.io/purewrt/. Builds are produced by GitHub Actions for a curated set of
arches (x86_64, aarch64 cortex-a53/a72/a76, mipsel_24kc); for other targets, build from source below.
PureWRT is built with the OpenWrt SDK. The bundled Taskfile.yml orchestrates SDK downloads and builds
in a NixOS dev shell for OpenWrt 24.10 and 25.12 and the two reference devices.
# NixOS dev shell
nix-shell
task doctor # verify the toolchain
task build:25.12 # full SDK build → .apk artifacts
task build:cudy-wr3000h
task build:bananapi-bpi-r3-miniOr build the package in an existing SDK checkout:
./scripts/feeds update -a && ./scripts/feeds install -a
make package/mihomo-alpha/compile V=s
make package/purewrt/compile V=sRuntime dependencies: dnsmasq-full, nftables, kmod-nft-tproxy, ip-full, ca-bundle,
luci-base, and a mihomo binary (the bundled mihomo-alpha package provides /usr/bin/mihomo).
OpenWrt 25.12 uses APK packages, so artifacts are
.apk(not.ipk). See the build notes near the end of this file for SDK target overrides and NixOS troubleshooting.
Everything is UCI-driven. A subscription, for example:
config subscription
option url 'https://panel.example/sub/secret?format=clash'
option panel_type 'remnawave'
option user_agent 'PureWRT/0.2'
list header 'X-Custom-Header: value'
PureWRT derives a stable HWID from router identity and appends hwid/device_name when missing; it
sends x-hwid, x-ver-os, x-device-model (plus compatibility aliases) on every download. Manual HWID
overrides are intentionally ignored to keep identity router-derived.
PureWRT ships a resource_profile setting (standard (default) / low / high). Set it in
Settings, or uci set purewrt.settings.resource_profile='low'. low automatically:
- moves the rule cache to tmpfs (
/tmp/purewrt/cache) instead of flash; - disables the artifact cache (providers re-parse on each update — saves flash + RAM);
- turns off rule dedup (less RAM/CPU; duplicate set entries are harmless);
- disables IPv6 routing when
ipv6_mode=auto(override withipv6_mode=onif you really need it); - forces mihomo geodata off, and tunes the mihomo core (no process matching, shorter keep-alives, proxy health-checks off);
- shrinks the apply config-backup cap to 128 KB.
Knobs low does not set, worth disabling on tight hardware:
config main 'settings'
option resource_profile 'low'
option ipv6_mode 'off'
option dashboard_enabled '0' # the metacubexd dashboard costs ~5 MB
option mihomo_geodata_enabled '0'
option background_updates '0'
option update_concurrency '1'
Also: prefer the native lists from the catalog (parse_mode=native_import, the wizard's
"Default lists — lightest" path) and small text lists over large MRS or GeoSite/GeoIP providers —
MRS decoding and per-section dnsmasq buffers (capped at 32 MB) are the main RAM spikes. Subscriptions
already import only proxy nodes (not their large rule lists) under low unless you opt in per
subscription.
What fits:
| RAM | What to run |
|---|---|
| ~32 MB | The mihomo proxy core (a Go binary needing tens of MB) generally won't fit alongside OpenWrt. Use zapret-only routing — route sections through zapret for DPI bypass with no proxy (see Routing targets); PureWRT's routing/DNS layer itself is light. Or run the proxy on bigger hardware. |
| ~64 MB | A minimal mihomo proxy is possible with resource_profile=low: a few nodes, small native lists, dashboard off, no geodata, IPv6 off. |
| ≥128 MB | Comfortable — standard profile, MRS providers, IPv6, and the dashboard are all fine. |
purewrt analyze <url> # inspect a subscription without importing
purewrt import <url> # import a subscription / provider
purewrt add-native-list <url> <section> [--priority=N]
purewrt update # fetch subscriptions + rule providers
purewrt generate # render artifacts to /etc/purewrt/generated
purewrt apply # generate + install + reload services (self-healing)
purewrt reload # alias of apply
purewrt status # current state
purewrt validate # validate config
purewrt client-traffic <IP> # live flow / DNS / rejection capture for a client
purewrt doctor # check the dev/runtime toolchain
purewrt disable # remove PureWRT routing + DNS changes
purewrt-check chatgpt.com # classify a domain- The mihomo external controller listens on
127.0.0.1by default. - PureWRT owns only
/etc/purewrt/generated/*, its dnsmasq fragments, andtable inet purewrt. - Policy routing uses a masked fwmark
0x1/0xff; nftables sets the mark withmeta mark set meta mark | 0x1, preserving mwan3's bits. applyis self-healing: it probes live state (nft table, ip rules, dnsmasq fragments) before honoring the fingerprint cache, and one broken subscription doesn't abort the whole update.disableremoves only PureWRT-generated routing and DNS changes.
task test:go # go test ./... && go vet ./... — the canonical check
go test ./internal/manager/... -run TestApplyThe Go toolchain is pinned via mise.toml; go.mod declares the language floor. External dependencies
are kept minimal: gopkg.in/yaml.v3 and github.com/klauspost/compress (zstd, for binary MRS decoding).
See CLAUDE.md and AGENTS.md for the architecture map and the non-obvious gotchas (LuCI cache-busting,
dnsmasq restart-not-reload, boot apply --force, self-heal probes) that have dedicated regression tests.
OpenWrt SDK targets and NixOS troubleshooting
Defaults target x86/64. Override target variables when needed:
OPENWRT_VERSION=24.10.0 OPENWRT_TARGET=x86 OPENWRT_SUBTARGET=64 OPENWRT_ARCH=x86_64 task buildThe SDK is downloaded into .build/openwrt/; packages are synced into the SDK under
package/network/services/; resulting .apk/.ipk files are printed from bin/packages / bin/targets.
OPENWRT_VERSION=25.12.0 OPENWRT_GCC_VERSION=14.3.0 OPENWRT_TARGET=mediatek \
OPENWRT_SUBTARGET=filogic OPENWRT_ARCH=x86_64 task buildThe Taskfile intentionally does not run ./scripts/feeds install -a — on mediatek/filogic that
pulls unrelated bootloader variants. It installs only what purewrt and mihomo-alpha need. If an SDK
was polluted with install -a, run task sdk:reset with the matching target vars and rebuild.
shell.nix exports NIX_LD/NIX_LD_LIBRARY_PATH for stub-ld compatibility. If a build failed in
golang-bootstrap, clean host Go artifacts with task sdk:clean-go-host (with target vars) and rebuild.
Runtime overwrite of /usr/bin/mihomo is disabled by design — PureWRT ships package-managed
mihomo-alpha. Update it by rebuilding the package (openwrt/mihomo-alpha/Makefile) or installing a
newer package from your repository.