Files
shater/docs-shater/INSTALL.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

16 KiB
Raw Blame History

Shater v0.2 — Build & Install

How to build the ship artifact (the SPA-embedded shaterd binary) and install the signed apk repo onto a router.

1. Build the shaterd binary

scripts/build-shaterd.sh produces the release binary for every router arch:

scripts/build-shaterd.sh [VERSION] [--fast]

What it does:

  1. Builds the admin SPA — cd panel && npm ci && npm run build (Vite → panel/dist).
  2. Copies panel/dist/* into shater/panel/webroot/, so //go:embed all:webroot bakes the real SPA into the binary (not the "SPA not embedded" placeholder).
  3. Cross-builds, for each of {amd64, arm64}, with the D9 musl-static router tag set (CGO_ENABLED=0 GOOS=linux), stripped + trimmed, into dist/shaterd-<arch> (uncompressed, kept for debugging).
  4. UPX --lzma --best (D10) → dist/shaterd-<arch>.upx (~42 MB → ~8–11 MB).
  5. Stages dist/shaterd-<arch>.upx into openwrt/shaterd/files/ for the package.
  6. Prints a size table + a per-arch static check (must be ET_EXEC, no PT_INTERP).

Arg / env:

  • VERSION — stamped into constant.Version. Resolution: positional arg → $SHATER_VERSION → ci/version.sh --binary → v0.2.0-dev. ci/version.sh is the same computation the package version comes from (§2.1), so the string the panel shows always matches what apk list -I shaterd reports.
  • --fast — skip npm ci when panel/node_modules already exists.
  • UPX=/path/to/upx — override the UPX binary (default upx on PATH). UPX is cross-arch, so one host packs both the amd64 and aarch64 ELFs. (Note: UPX also reads $UPX as its own options — the script unsets it after capturing the path.)

Example (Windows host, Git Bash):

UPX="…/scratchpad/upx-4.2.4-win64/upx.exe" scripts/build-shaterd.sh v0.2.0 --fast

The dist/* and openwrt/shaterd/files/shaterd-*.upx outputs are gitignored — they are release artifacts, not source.

Tag set (D9/D23) — defined in one place, scripts/router-tags.sh, which documents every tag and is sourced by the build:

with_gvisor,with_quic,with_wireguard,with_utls,
badlinkname,tfogo_checklinkname0,with_xhttp,with_awg,with_lx_command

We drop with_purego,with_naive_outbound: they pull cronet-go, which forces a glibc PT_INTERP even under CGO_ENABLED=0, making the binary unusable on musl. We drop with_clash_api: the admin panel is shater's own web server and the generator never emits a clash_api service, so the Clash server is dead code. We drop with_dhcp: shater resolver types are udp/tcp/doh/dot/local/fakeip; a dhcp:// DNS transport is never generated or registered.

with_gvisor was dropped in 2026-07 as "unreachable — we emit no tun inbound" and put back on 2026-07-25: gVisor is also the netstack of the WireGuard endpoint, so without it every wg:///awg:// node died at apply time with "gVisor is not included in this build" while the panel still offered the feature. It costs ~2.8 MB raw / ~0.65 MB UPX per arch. Full story: DECISIONS.md D23.

Changing the tag set

Run the guard — it is what stands between a size trim and a silently dead feature, and CI runs it before the artifact is built:

scripts/check-router-tags.sh     # from Windows/macOS it re-execs itself in golang:1.26

It (1) fails if a feature declared in FEATURES.md lost a build tag it needs to run (shater/buildtags, no tags/OS/network required) and (2) constructs one node of every declared protocol through box.New compiled with the shipped tag set — nothing may be skipped in that run. Adding a protocol to shater/parse+shater/generate means adding a row to buildtags.Features and a probe case in shater/generate/shipped_tags_linux_test.go.

2. Packages

Four OpenWrt packages live under openwrt/:

Package Arch What it ships
shaterd per-arch Prebuilt static shaterd binary → /usr/bin/shaterd (this is the ship artifact from step 1).
shater-core all procd init (supervises shaterd run), cron, hotplug, sysctl, inert default UCI. DEPENDS:=+shaterd +kmod-nft-tproxy +kmod-nft-socket +ip-full.
luci-app-shater all Thin LuCI launcher: mini dashboard + token-handoff "Open panel" button. DEPENDS:=+shater-core +rpcd.
byedpi per-arch Optional ByeDPI (ciadpi) local desync SOCKS proxy for a type='byedpi' egress.

Why shaterd is a prebuilt-binary package

The binary is the product of a toolchain the OpenWrt SDK can't easily reproduce: an npm/Vite SPA build, embedded via //go:embed at go build time, the D9 musl-static tag set, and a UPX pass. Instead of running node + embed + UPX inside the SDK, we build out-of-tree with scripts/build-shaterd.sh and package the arch-matched artifact. The Makefile maps OpenWrt $(ARCH) → the artifact (x86_64→amd64, aarch64→arm64), verifies it was staged, and $(INSTALL_BIN)s it. Because the binary is UPX-packed, the package disables the SDK's default strip (RSTRIP:=:) — stripping a packed executable would corrupt it.

CI order: (1) run scripts/build-shaterd.sh (builds + stages both arches into openwrt/shaterd/files/); (2) copy openwrt/* into the SDK tree with the luci feed installed and run make package/shaterd/compile (and the others) per target. See openwrt-package-build-ci for SDK/feed mechanics.

2.1 Package versions come from the git tag

PKG_VERSION/PKG_RELEASE are not maintained by hand. They used to be, and nobody bumped them: v0.2.2 … v0.2.6 all shipped as shaterd 0.2.0-r3 with different binaries inside (v0.2.6's ELF is 5 491 616 B against r2's 5 488 336 B). apk offers an upgrade only when the feed's version string differs from the installed one, so apk update saw nothing new and the routers could not be updated through the normal path at all.

ci/version.sh now derives them from git describe, once per CI job:

Build PKG_VERSION PKG_RELEASE constant.Version
tag push v0.2.7 0.2.7 1 v0.2.7-r1
dispatch, 3 commits past v0.2.7 0.2.7 4 v0.2.7-r4-g<sha>
no reachable tag / no git 0.0.0 1 v0.0.0-r1

Ordering is what makes this safe (checked with apk version -t on apk-tools 3.0.3): the dotted part decides first, -rN only breaks ties — so 0.2.7-r1 > 0.2.6-r12 > 0.2.6-r1 > 0.2.0-r3. A release therefore always outranks every rolling build before it, rolling builds between two releases grow monotonically, and an untagged build (0.0.0) can never masquerade as an upgrade.

The value travels as SHATER_PKG_VERSION/SHATER_PKG_RELEASE in the SDK build environment; the Makefiles read it with a literal fallback for manual/offline builds. ci/sdk-build-apk.sh then asserts the produced .apk really carries it, so a lost variable fails the build instead of shipping a stale version. The release job asserts the same version again on the published rolling repo (§5.1).

byedpi is deliberately excluded — PKG_VERSION:=0.17.3 is upstream ByeDPI's version, which is what PKG_HASH pins and what tells you which ByeDPI is installed. Stamping our tag on it would also be a downgrade: every comparator reads 0.2.7 < 0.17.3 (component-wise, 2 < 17). Bump its PKG_RELEASE by hand when our packaging of it changes.

3. Install on a router

The normal path is the signed apk repo — §5. This section is the manual fallback (a router with no route to the Gitea host, or a hand-carried build).

Install order follows the deps (shaterd → shater-core → luci-app-shater). apk filenames carry no architecture, so make sure you copied the .apk built for this router's arch (cat /etc/apk/arch):

# <ver> = the release version, e.g. 0.2.7-r1 (§2.1 — it comes from the git tag)
# --allow-untrusted: our member .apk are unsigned by design — trust lives in the
# signed packages.adb index (§5), which a loose file install does not consult.
apk add --allow-untrusted ./shaterd-<ver>.apk
apk add --allow-untrusted ./shater-core-<ver>.apk
apk add --allow-untrusted ./luci-app-shater-<ver>.apk
apk add --allow-untrusted ./byedpi-0.17.3-r1.apk       # optional: ByeDPI egress

From the repo instead (§5 sets it up once), deps pull the rest in:

apk update && apk add luci-app-shater      # -> shater-core -> shaterd

4. Enable

Shater ships inert (globals disabled) so it never breaks connectivity on install. Configure nodes/rules (via the LuCI panel or uci), then enable and apply:

uci set shater.globals.enabled=1
uci commit shater
shaterd apply        # apply + arm commit-confirm on the running daemon
shaterd confirm      # confirm (cancels the auto-rollback)

/etc/init.d/shater enable && /etc/init.d/shater start brings up the procd-supervised daemon (shaterd run), which owns the engine, the inet shater data plane, policy routing, in-process DNS, and the admin panel (default :8088). The LuCI app's "Open panel" button mints a single-use token and hands the browser off to the panel.

What enabling does to DNS

From the first apply, every LAN plaintext :53 goes into the engine — including the queries a client sends to the router's own address, which is what DHCP hands out. That is globals.dns_intercept, and it is on by default (D24); without it those queries reach dnsmasq and the ISP unfiltered, i.e. the client with default settings leaks while the one that hard-coded 8.8.8.8 does not. What follows from it:

  • .lan and private reverse (PTR) lookups still go to dnsmasq — the engine gets a rule for those suffixes. If you renamed dnsmasq's domain away from lan, add a config dns_rule for the new suffix.
  • Configure at least one config resolver. With none, the engine has no resolver plane: intercepted queries fall through to the system resolver (dnsmasq → your ISP, in the clear), blocklists and per-device DNS rules are inert, and the apply says so in its warnings.
  • While the engine is DOWN, DNS is not blacked out: the fail-closed holding plane hooks forward only, so dnsmasq keeps answering router-addressed :53 (unfiltered, plaintext) while client traffic and DNS to external resolvers stay blocked. "The tunnel is down" is not "DNS is private".

To opt out, on the router:

uci set shater.globals.dns_intercept=0
uci commit shater
shaterd apply

Your 0 is kept: /etc/config/shater is a conffile (upgrades never replace it) and the daemon always writes the option back explicitly, so it is never re-enabled by a default.

5. The signed apk repo (the normal install path)

OpenWrt/ImmortalWrt 25.12 packages with Alpine's apk: .apk files, a binary packages.adb index, EC (prime256v1) keys in /etc/apk/keys/, and effectively mandatory signatures (unsigned needs --allow-untrusted). This is the only format shater publishes — the .ipk/opkg lane was removed in 2026-07 (DECISIONS.md D22); every device we serve is on 25.12 with apk-tools 3.

CI (v* tag push or workflow_dispatch) compiles the 4 packages through the official ImmortalWrt 25.12 SDK (tarballs from downloads.immortalwrt.org/releases/25.12.1/targets/{x86/64,mediatek/filogic}/) and publishes one release per arch: the rolling apk-latest-x86_64 / apk-latest-aarch64_cortex-a53, plus apk-vX.Y.Z-<arch> on a tag. Per-arch because apk filenames carry no architecture and packages are fetched relative to the packages.adb URL, so one flat multi-arch release would collide.

Key: the trust anchor is the EC public key dist/shater-apk.pem (generated once by ci/gen-apk-key.sh; the private half lives ONLY in the Gitea secret KEY_APK). Never regenerate it — that invalidates every deployed router's trust.

5.1 Rolling or pinned — pick the repo URL deliberately

The repo line names an index file, and which one you name is the whole update policy:

Repo line points at Behaviour Cost
apk-latest-<arch>/packages.adb (rolling) Every release run REPLACES this release's assets, so apk update && apk upgrade <our packages> always sees the newest build. Install once, never touch the file again. You get whatever CI published last; there is no per-router pin.
apk-vX.Y.Z-<arch>/packages.adb (pinned) The router stays on exactly that build. apk update will never offer a newer shater. /etc/apk/repositories.d/shater.list must be edited by hand on every upgrade, on every router.

mini_router is deliberately on a pinned URL — a considered choice, and the hand-edit per release is its price. Use rolling unless you specifically want to freeze a device.

The rolling release used to go stale silently: publishing was an either/or, so tag runs wrote only apk-vX.Y.Z-<arch> and apk-latest-<arch> was last refreshed on 2026-07-24 at 0.2.0 while v0.2.9/v0.2.10 shipped. A router on the rolling URL kept getting a successful apk update with nothing new. Fixed 2026-07-25: release-apk writes the rolling pointer on every run and then reads the release back over the Gitea API, asserting it holds our three tag-versioned packages at exactly the version just built and no leftover asset at another version (two versions of one package in one index would let apk choose instead of us).

5.2 One-time setup on the router

BananaWRT 25.12-mtk-vendor on the BPI-R3 mini, OpenWrt 25.12 on the BPI-R4, or the testbed VM — /etc/apk/arch picks the right per-arch release automatically:

# 1) trust the apk feed key (any *.pem filename under /etc/apk/keys works).
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"

# 2) add the repo — the line points at the packages.adb INDEX FILE itself.
#    (rolling; for a pinned router put apk-vX.Y.Z-$(cat /etc/apk/arch) here — §5.1)
echo "https://git.qomar.pw/omar/shater/releases/download/apk-latest-$(cat /etc/apk/arch)/packages.adb" \
  > /etc/apk/repositories.d/shater.list

# 3) refresh + install (shaterd pulled in as a dependency).
apk update
apk add luci-app-shater        # -> shater-core -> shaterd
apk add byedpi                 # optional: ByeDPI desync egress

5.3 Updating

Never run a bare apk upgrade. With no arguments apk reconciles every installed package against every configured repository at once; on a router whose distfeeds point at a moving snapshot that can pull in — or roll back — unrelated system packages. Always name ours:

apk update
apk upgrade shaterd shater-core luci-app-shater byedpi

apk-tools 3 documents exactly this behaviour for apk upgrade: "When no packages are specified, all packages are upgraded if possible. If list of packages is provided, only those packages are upgraded along with needed dependencies." The equivalent form, which additionally re-pins the packages in world, is:

apk add -u shaterd shater-core luci-app-shater byedpi     # -u = --upgrade

Drop byedpi from either list if you never installed it. Check what you are on with apk list -I shaterd shater-core luci-app-shater byedpi — the version reads 0.2.7-r1 (§2.1: PKG_VERSION-rPKG_RELEASE, derived from the git tag by CI, so every build really is a new version; before that fix v0.2.2…v0.2.6 all published as 0.2.0-r3 and apk update offered nothing). Rolling vs pinned repo URL — §5.1.

BananaWRT 25.12-mtk-vendor compatibility

The mtk-vendor channel (base: SuperKali/immortalwrt-mt798x-rebase, branch 25.12-linkup) builds target mediatek/filogic, pkg arch aarch64_cortex-a53, and its images even point their distfeeds at vanilla downloads.immortalwrt.org/releases/25.12-SNAPSHOT — so packages built with the vanilla ImmortalWrt 25.12 filogic SDK install cleanly; no SuperKali-special SDK is needed. We ship no kmods (shaterd is a static Go binary, byedpi plain C), so the vendor 6.6 kernel is irrelevant to our packages; the kmod dependencies of shater-core (kmod-nft-tproxy, kmod-nft-socket, plus ip-full) are already baked into the BananaWRT mtk-vendor image (verified in its config.buildinfo). On a self-built 25.12 image, make sure those kmods come from the image's own kernel build.