`/etc/init.d/shater restart` left DNS to the router's own LAN address dead
and never recovering, while `stop` + pause + `start` was fine — with the
status still reporting plane=full / engine_running=true and `shaterd
reconcile` fixing nothing.
Cause: `restart` is not synchronised end to end.
* procd's `stop` is ASYNCHRONOUS. rc.common's `restart` is literally
`stop; start`, and the `service delete` ubus call returns as soon as
SIGTERM has been SENT. `start_service` therefore re-adds the instance
(and runs `shaterd migrate`) while the outgoing `shaterd run` is still
executing its honest teardown.
* The successor's only defence was `daemonAlive()` -> exit(1), leaning on
procd's `respawn 3600 5 0` to try again five seconds later. That is a
blind retry, not synchronisation: it neither knows nor waits for the
teardown, and it turns every restart into a logged crash plus a
five-second hole with no data plane.
* `term_timeout 10` SIGKILLs a predecessor whose teardown outlives it —
engine.Close of a several-hundred-outbound box flushes cache.db to
flash before the netplane teardown even starts — aborting the teardown
at an arbitrary point and leaving the plane HALF removed.
* Nothing in the tree ever touched conntrack, so flows that crossed one
of those windows kept entries formed against a plane that no longer
exists. For UDP there is no handshake to resynchronise on and every
retry merely refreshes the entry, so the flow stays wedged for as long
as the client keeps asking — a flow-scoped, permanent failure that no
ruleset rebuild can reach.
* RoutingPresent() reported "plane intact" from the ip RULE alone, while
ApplyRouting installs a rule AND a `local default dev lo` route removed
by two independent commands. A teardown interrupted between them was
therefore invisible, applyLocked's fast-path skipped ApplyRouting
forever, and no reconcile could repair it.
Fix (fail-closed posture unchanged — no new window in which LAN traffic can
reach the WAN; teardown still removes the table LAST and the forward-chain
drop is untouched):
* init: `start_service` waits for a live predecessor pidfile to clear
before opening the instance, so restart == stop + pause + start. Zero
cost at boot. term_timeout 10 -> 30 so an honest teardown is never
killed halfway.
* daemon: the single-owner guard WAITS for the predecessor (bounded,
60s) instead of exiting 1; it still refuses if the budget expires.
* netplane: new FlushDNSConntrack() (ctnetlink, UDP orig-dport 53 only —
a blanket flush would drop the admin's own SSH/LuCI sessions) called
on every plane transition: after a ruleset loads, after the table is
removed, and once more in applyLocked when the whole plane (table +
policy routing + sysctls) is assembled.
* netplane: RoutingPresent() now verifies both halves it installs.
Regression tests fail on the pre-fix code (verified by reverting each fix):
TestApplyNftFlushesDNSConntrack, TestTeardownNftFlushesDNSConntrack,
TestRoutingPresentRequiresLocalDefaultRoute, TestWaitForPredecessor*.
Co-Authored-By: Claude Opus 5 <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.
- 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
Two signed feeds. Pick by the router's OpenWrt version. Verbatim commands and the
manual .ipk/.apk install are in docs-shater/INSTALL.md.
opkg (OpenWrt 24.10):
wget -O /etc/opkg/keys/5ac4b177689cb8e0 \
https://git.qomar.pw/omar/shater/releases/download/latest/shater-feed.pub
echo "src/gz shater https://git.qomar.pw/omar/shater/releases/download/latest" \
>> /etc/opkg/customfeeds.conf
opkg update && opkg install luci-app-shater # -> shater-core -> shaterd
apk (OpenWrt / ImmortalWrt / BananaWRT 25.12+):
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
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, 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 signed
feeds: opkg (usign, key 5ac4b177689cb8e0) and apk (EC key shater-apk.pem). A
vX.Y.Z tag → versioned release; workflow_dispatch → rolling latest.
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.