omarandClaude Opus 5 8fd5c52488
release / aarch64_cortex-a53 (push) Successful in 3m29s
release / x86_64 (push) Successful in 3m21s
release / apk aarch64_cortex-a53 (push) Successful in 2m38s
release / apk x86_64 (push) Successful in 2m35s
release / release (push) Successful in 8s
release / release apk (push) Successful in 6s
fix(tproxy): connect the UDP write-back socket + release NAT sessions on close
B3, real root cause. On the live BPi-R3 Mini `netstat -lnup` showed shaterd
holding 33 sockets on the router's own LAN address 10.67.0.1:53, next to
dnsmasq's single socket, several with a growing Recv-Q. Reproduced read-only on
the box: 5 host queries to 10.67.0.1 -> 0 answers and total Recv-Q on those
sockets 0 -> 19200 (5 x 3840, one datagram parked in each, never read); 3
control queries to 127.0.0.1 -> all answered.

Where they come from: protocol/redirect/tproxy.go, tproxyPacketWriter.
WritePacket. The TPROXY UDP write-back socket must carry the ORIGINAL
DESTINATION as its source address, so upstream binds it there — but leaves it
UNCONNECTED (net.ListenPacket + WriteToUDPAddrPort) and sets SO_REUSEADDR AND
SO_REUSEPORT (sing's control.ReuseAddr sets both). An unconnected bound socket
is a RECEIVER as far as the kernel is concerned, so each one silently joins the
UDP demultiplex/reuseport set for that address:port. Nothing ever reads them —
this writer only sends.

With dns_intercept the original destination IS the router's LAN address, so
every intercepted DNS session parks another silent receiver on <lan-ip>:53. The
host's own queries to that address take the loopback path, are never diverted by
the nft plane (iifname is scoped to LAN devices), and are therefore spread across
that set by the reuseport 4-tuple hash: they land in a silent socket at random
and time out. Hence "2 restarts of 3 fine, the third dead", and hence a failure
that no ruleset rebuild or reconcile can touch. The stale [UNREPLIED] conntrack
entry seen alongside is a CONSEQUENCE of the unanswered query, not the cause.

Fix (upstream file, lx:tproxy_writeback_connect):
  * CONNECT the write-back socket to the one peer it ever talks to. The kernel's
    compute_score() rejects a connected socket for any other peer, and a
    connected UDP socket (sk_state == TCP_ESTABLISHED) is excluded from
    reuseport selection outright — so it can no longer be handed a datagram it
    will not read. Nothing about the reply changes: same spoofed source, same
    single peer, Write instead of WriteTo. The unconnected path is kept verbatim
    for a destination that cannot be bound (domain socksaddr).
  * A failed cached write now CLOSES the socket instead of only dropping the
    reference (upstream left the fd to the GC finalizer).
  * TProxy.Close() purges the UDP NAT cache. Closing the listener stops ingress
    but the cache evicts lazily, so after the inbound is gone nothing wakes the
    live sessions and each strands its write-back socket. Invisible upstream
    (one close at shutdown); on this fork the engine is rebuilt on every apply,
    so it was one stranded generation per apply.

Measured on the live box: the socket count is steady-state (22-40, fds 55-66),
i.e. bounded by the udpnat session lifetime rather than an unbounded leak — the
count itself is inherent to per-session write-back sockets and is harmless once
they are connected. The Close() purge removes the per-apply generations on top
of it.

The netplane UDP:53 conntrack flush from 32e8f8ff0 is KEPT, with its comment
corrected: it is hygiene on plane transitions, not the cure for B3.

Regression tests fail on the pre-fix code (verified by reverting each half):
TestWriteBackUsesConnectedSocket / TestWriteBackReusesOneSocket /
TestWriteBackClosesSocketOnWriteFailure ("use of WriteTo with pre-connected
connection") and TestTProxyCloseReleasesNatSessions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 13:41:02 +03:00
2026-06-25 16:36:07 +08:00
2026-06-25 17:38:53 +08:00
2026-06-25 17:39:02 +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, 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 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

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.

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%