The carrier behind this router's SIM refuses TCP/443 to 9.9.9.9 and 1.1.1.1 while
carrying everything else — measured with a positive control (ya.ru:443 and
77.88.8.8:53 connect, every sim-bypass node connects, those two are refused). The
configured resolvers go out DIRECT, not through the tunnel, so on that uplink DNS
resolved nothing: the vless server names did not resolve, the hop in front of
awgout never came up, and the whole chain died with it. One pair of global scalars
cannot be right for two uplinks; the object that knows which uplink is live is the
profile.
* config profile gains resolver_default, resolver_fallback and fetch_detour
beside endpoint_resolver. Empty = inherit, PER FIELD.
* globals.fetch_detour replaces `const filterFetchDetour = tagDirect`. Behind a
carrier whitelist `direct` is not the safe path, it is the path where the
source is refused forever and the list never loads.
* A subscription's fetch_via becomes an OVERRIDE, which gives it a third state.
ReadUCI used to parse an absent option as the literal "direct", so "chose
clear-text" and "never touched this row" were the same value. migrate2to3
performs the reinterpretation ONCE, in the open. Schema 2 -> 3.
* An unusable override falls back (resolvers to globals, fetch_detour to direct)
and says so at critical, naming profile, field, value and what is in force.
The panel was displaying globals while the engine used the profile's value; the
owner caught it. The field now keeps the STORED value with a separate line naming
what is in force, and the rule that answers "what is in force" moved to the daemon
(GET /api/config/effective) so it stops existing in two languages.
Cold start, by owner's requirement: rule-sets are read from the cache when the
source is unreachable instead of being dropped, and the subscription cache reader
is fixed. Its first fix was wrong and only Linux said so — mtime ties to the digit
because the kernel caches the stamp per tick, and this board has no RTC, so the
ordering can invert across a reboot. Replaced by a generation counter in the file.
Woke and closed a LAN-dark defect: wgdedup read only the deprecated, always-empty
DownloadDetour, never HTTPClient.Detour, so fetch_detour=node:<awg> made a node
used, the dedup pass did not know, merged it away, and left the rule-set pointing
at a tag box.Start could not resolve. Reproduced through a real box.New.
Also: ValidateProfiles had no caller; "applied from the cache" graded critical
though the list is in force; the auth matrix never walked /api/log or
/api/rules/reachability; the CLI and daemon disagreed about where a subscription
is fetched.
NOT fixed, stated rather than implied: the R5 preflight still probes direct, so a
list never yet fetched cannot bootstrap over the detour alone; the router's own
DNS on the SIM stays dead (dnscrypt-proxy bootstraps via blocked addresses).
Gate: bash scripts/run-tests.sh green, 7/7, privileged tests really ran.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shater
A self-hosted internet-control appliance for OpenWrt routers. One box turns a home or office network into a transparent VPN gateway, a network-wide ad/tracker/malware blocker, per-device parental control, and a live traffic dashboard — all local, all configured from a rich built-in web panel.
The primary README is Russian — README.md. This is a condensed English mirror.
What it is
shater is a network proxy stack for OpenWrt / ImmortalWrt / BananaWRT routers (Banana Pi BPI-R3, BPI-R4 and compatible). It transparently routes all LAN traffic through a proxy (split by domain/geo/client), filters DNS, gathers statistics, and is managed from a built-in web panel.
The engine is a fork of sing-box via
sing-box-lx, compiled into a single Go
binary shaterd together with the control plane, DNS filter, stats aggregator and
the web panel itself. Broad protocol set: VLESS/VMess/Trojan/Shadowsocks,
Reality/XTLS, WireGuard, AmneziaWG 2.0, Hysteria2, TUIC, XHTTP — exactly what
shater/parse can read and shater/registry registers in the engine.
A thin LuCI launcher (mini-dashboard + "Open panel" button) hands the browser a
single-use token into the standalone SPA the daemon serves on its own port
(default :8088).
Highlights
- Transparent TPROXY data plane (TCP + UDP), SNI/Host/QUIC sniffing, no DNS leaks
—
:53interception is on by default and covers the queries a client sends to the router itself, not just the ones aimed around it (globals.dns_intercept, D24). - First-match routing by source / destination / list / geo / client → outbound / selector / chain / direct / block; node groups with balancer/observatory; multi-hop chains; per-rule egress.
- Fail-closed kill-switch (dead group → block, never a silent direct leak); own
inet shaternft table; atomic apply withnft -cvalidation. Commit-confirm auto-rollback exists but ships OFF (confirm_timeout=0) — arm it yourself. - DNS filtering & blocklists with flexible sources (inline / file / url /
geosite), compiled
.srsmatcher; Block-DoH/DoT to stop filter bypass. - Subscriptions (Clash / sing-box / Xray-JSON) and manual nodes; node health board.
- Per-device control (proxy/blocklist toggles, exit country, per-device block/allow, schedules) and per-domain/client/device statistics from in-process DNS events.
Full list with MVP/T1/T2 tags — docs-shater/FEATURES.md.
Install
One signed apk feed (OpenWrt / ImmortalWrt / BananaWRT 25.12+), one
release per arch. Verbatim commands, the manual .apk install and the
rolling-vs-pinned choice are in
docs-shater/INSTALL.md.
wget -O /etc/apk/keys/shater-apk.pem "https://git.qomar.pw/omar/shater/releases/download/apk-latest-$(cat /etc/apk/arch)/shater-apk.pem"
echo "https://git.qomar.pw/omar/shater/releases/download/apk-latest-$(cat /etc/apk/arch)/packages.adb" > /etc/apk/repositories.d/shater.list
apk update && apk add luci-app-shater # -> shater-core -> shaterd
apk-latest-<arch> is a moving pointer refreshed by every release run — install
once and apk update && apk upgrade shaterd shater-core luci-app-shater
keeps the router current. Point the repo line at apk-vX.Y.Z-<arch> instead to
pin a build; that file then has to be edited by hand for every upgrade.
shater ships inert (globals off) so install never breaks connectivity. After configuring nodes/rules:
uci set shater.globals.enabled=1
uci set shater.globals.confirm_timeout=120 # commit-confirm ships OFF — arm it
uci commit shater
shaterd apply && shaterd confirm
Without that middle line shaterd apply arms no auto-rollback (and says so), so an
apply that costs you SSH/LuCI access has to be undone by hand.
Once an enabled, fail-closed config has been applied, /etc/init.d/shater-armor
loads a saved fail-closed plane at boot, before the daemon exists: LAN→WAN
forwarding is blocked until shaterd applies, while SSH/LuCI/the panel stay
reachable on purpose (the chain hooks forward only). What arms it, what refuses
to arm, and how to switch it off — INSTALL.md §4.
Build from source
scripts/build-shaterd.sh [VERSION] [--fast] builds the SPA (Vite), embeds it via
//go:embed, cross-builds musl-static {amd64, arm64} and UPX-packs the artifact
into openwrt/shaterd/files/. Details in
docs-shater/INSTALL.md.
bash scripts/run-tests.sh is the test gate: the whole suite under the shipped
build tags (scripts/router-tags.sh), on linux (it re-execs in Docker from a
non-linux host), with -race, plus three machine checks against a silent skip —
the tag set may only add test files, every package with tests must report ok by
name, and every TestIntegration* must produce a verdict by name.
scripts/check-router-tags.sh separately proves no feature declared in
FEATURES.md lost a build tag it needs. A green gate is necessary but not
sufficient: it does not see the kernel, procd or nftables seams.
Repository layout
| Path | What |
|---|---|
shater/ |
Go control plane, DNS filter, stats aggregator, engine host |
panel/ |
Admin SPA (Vite + React + TS) and its Go server |
openwrt/ |
Packages: shaterd, shater-core, luci-app-shater |
docs-shater/ |
Product documentation |
scripts/, ci/, .gitea/workflows/ |
Build script, apk feed/release scripts, CI |
SPECS/, docs-lx/ |
Engine-fork constitution/specs and feature-config reference |
docs/, mkdocs.yml |
Upstream sing-box docs (mkdocs) — kept as-is |
adapter/ cmd/ dns/ route/ option/ protocol/ transport/ … |
sing-box-lx engine tree |
CI, upstream & license
CI (.gitea/workflows/release.yml) builds all 4 packages and publishes a signed
per-arch apk repo (EC key shater-apk.pem). A vX.Y.Z tag → the pinnable
apk-vX.Y.Z-<arch>; every run also refreshes the rolling apk-latest-<arch> and
asserts over the API that it really serves the version just built.
The engine is the sing-box-lx fork — a thin downstream of upstream sing-box that
lives by rebase, never merge; its constitution is
SPECS/CONSTITUTION.md. Licensed under
GPL-3.0, like upstream sing-box. Unofficial fork, not affiliated with
SagerNet.