omarandClaude Opus 5 8c0ea55054
test / go + panel tests (push) Successful in 1m42s
release / test gate (push) Successful in 1m40s
release / apk aarch64_cortex-a53 (push) Successful in 2m55s
release / apk x86_64 (push) Successful in 2m56s
release / release apk (push) Successful in 9s
feat(dns,fetch): resolvers and list fetches follow the uplink; the node cache survives the reboot it exists for
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>
2026-07-28 14:40:58 +03:00
2026-06-25 16:36:07 +08:00
2026-06-20 22:24:42 +08:00
2026-06-20 22:24:55 +08:00
2026-02-27 14:58:06 +08:00
2026-06-25 17:38:01 +08:00
2025-04-29 20:45:19 +08:00
2023-12-29 18:00:40 +08:00
2023-12-29 18:00:40 +08:00
2026-02-26 14:13:32 +08:00
2026-06-25 19:47:32 +08:00
2026-07-08 00:34:26 +08:00

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.

License: GPL-3.0 targets: x86_64 · aarch64_cortex-a53

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 — :53 interception 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 shater nft table; atomic apply with nft -c validation. 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 .srs matcher; 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.

S
Description
OpenWrt XRAY management plugin � passwall-class, but cleaner. Design + code.
Readme
56 MiB
2026-07-28 07:50:40 -04:00
Languages
Go 77.7%
TypeScript 12.4%
PureBasic 4.2%
Shell 2.5%
CSS 2.1%
Other 1%