Files
shater/README.en.md
T
omarandClaude Opus 5 a0de597d69
test / go + panel tests (push) Successful in 4m56s
feat(dns): intercept by default, and bootstrap node addresses off the tunnel
The posture was inverted. A client using the DHCP-supplied resolver — the router
itself — was NOT intercepted: dnsmasq answered and forwarded to the ISP in the
clear, so the filter, the blocklists, the per-device rules and BlockDoH were all
inert for exactly the clients that did nothing wrong. A client that hardcoded
8.8.8.8 to route around us WAS intercepted, by the catch-all. Meanwhile the
docs promised no DNS leaks. The default now matches the promise.

Turning it on crosses a threshold that was already dangerous for anyone with two
resolvers. Above one transport, a node's domain server address stops being
resolved by the transport directly and goes through the client DNS plane
instead — so a blocklist entry, a block_doh NXDOMAIN or any dns_rule can answer
your own node's hostname, and one sloppy line in an ad list stops being an ad
that got through and becomes a tunnel that never comes up.

So the fix is gated on having two or more transports, not on the intercept
toggle: resolver_default plus resolver_fallback always reached that threshold,
long before this change. When no endpoint_resolver is configured the plane now
carries a bootstrap server — the default resolver cloned with its detour
dropped, keeping its type, so a DoH default stays DoH and only the tunnel hop
goes. An explicit endpoint_resolver still wins.

This is not a restore of the previous behaviour and the comment says so: at one
transport the dialer used the default resolver WITH its detour, so a lone
DoH-through-the-tunnel resolver was already a bootstrap loop. It is strictly
better than what came before.

Existing installs keep whatever they set — the config file is a conffile and is
never replaced — and an explicit dns_intercept '0' survives the render-parse
round trip, which a default-true bool otherwise makes easy to lose.

The no-resolver warning stays, and no default resolver is shipped to silence it:
a placeholder would remove the sentence without moving a single query, and the
panel would then say a resolver was configured while nothing was filtered. Its
wording is corrected instead — .lan keeps working through the built-in local
transport, which the old text denied.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 04:54:15 +03:00

5.3 KiB

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, 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 — :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 and commit-confirm auto-rollback.
  • 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 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.