mock.ts was a static import and the mock switch was read from the query string at runtime, so the bundle that ships inside the daemon carried a complete fictional router and a link ending in ?dev rendered it: protected, 119 of 122 nodes alive, without a single request to the daemon. The only tell was a line in the footer. That is worse than any wrong number — there is no data at all and nothing says so. It is out of the production bundle now, which is 21 kB smaller for it. Unknown state stopped reading as good news in two more places. The kill-switch tile treated an absent plane as armed, because the check was "not none" and undefined satisfies it — the contract in the API types says the opposite. And the apply page announced "daemon auto-rolled back" from its own timer, while the daemon, seeing the state generation move, disarms and says it is NOT rolling back in the log only. Alerts moved to Settings. They are about the kill switch, apply failures, new devices and subscription expiry, and they lived at the bottom of the DNS page, while Settings mentioned them in prose with nothing to click. Findings truncation is visible now: the notice that says how many were suppressed arrives as info, and the attention list keeps only critical and warning, so past fifty findings the operator saw forty-nine and no hint of the rest. 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, MASQUE/CONNECT-IP.
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 and commit-confirm auto-rollback. - 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 byedpi
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 commit shater,
then shaterd apply and shaterd confirm.
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.
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, byedpi |
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.