PKG_VERSION/PKG_RELEASE were hand-written literals nobody bumped, so
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). Both opkg
and apk offer 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 is now the single source of truth. It derives the version
from `git describe`:
tag `vX.Y.Z` -> PKG_VERSION=X.Y.Z PKG_RELEASE=1
off-tag build -> nearest tag + PKG_RELEASE=<commits since it> + 1
no tag/no git -> 0.0.0-r1 (below everything ever published)
Ordering verified with the real tools, not from memory — apk-tools 3.0.3
(`apk version -t`) and opkg 38eccbb1 (`opkg compare-versions`) agree that
0.2.0-r3 < 0.2.6-r2 < 0.2.6-r10 < 0.2.6-r12 < 0.2.7-r1 < 0.3.0-r1, so a
release always outranks the rolling builds that preceded it and rolling
builds grow monotonically between releases.
The value travels as SHATER_PKG_VERSION/SHATER_PKG_RELEASE in the SDK
build environment of BOTH lanes; the Makefiles keep a literal fallback so
a manual/offline build still works with no CI and no git. Because the
hand-off crosses docker, `su` and make's env import, ci/sdk-build.sh and
ci/sdk-build-apk.sh now ASSERT that the produced .ipk/.apk really carries
that version — the B4 failure mode was a stale version shipping silently,
and that can no longer happen quietly.
The binary agrees with the package: scripts/build-shaterd.sh takes
constant.Version from the same ci/version.sh (vX.Y.Z-rR[-g<sha>]) instead
of its own `git describe`, and the workflow computes it once per job.
Both build jobs now check out with fetch-depth: 0 — `git describe` needs
tags and ancestry, which the default shallow checkout has neither of.
byedpi is deliberately left alone: PKG_VERSION:=0.17.3 is upstream
ByeDPI's own version, what PKG_HASH pins and what tells an operator 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), verified.
Docs: INSTALL.md gains §2.1 (the scheme + the ordering evidence), and the
update sections of §5/§6 now explicitly warn against a bare `opkg upgrade`
/ `apk upgrade` and give the targeted form instead, quoting apk-tools 3:
"If list of packages is provided, only those packages are upgraded along
with needed dependencies". README.md and the release bodies match.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
14 KiB
Shater v0.2 — Build & Install
How to build the ship artifact (the SPA-embedded shaterd binary) and install
the OpenWrt feed 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:
- Builds the admin SPA —
cd panel && npm ci && npm run build(Vite →panel/dist). - Copies
panel/dist/*intoshater/panel/webroot/, so//go:embed all:webrootbakes the real SPA into the binary (not the "SPA not embedded" placeholder). - Cross-builds, for each of
{amd64, arm64}, with the D9 musl-static router tag set (CGO_ENABLED=0 GOOS=linux), stripped + trimmed, intodist/shaterd-<arch>(uncompressed, kept for debugging). - UPX
--lzma --best(D10) →dist/shaterd-<arch>.upx(~42 MB → ~8–11 MB). - Stages
dist/shaterd-<arch>.upxintoopenwrt/shaterd/files/for the package. - Prints a size table + a per-arch static check (must be
ET_EXEC, noPT_INTERP).
Arg / env:
VERSION— stamped intoconstant.Version. Resolution: positional arg →$SHATER_VERSION→ci/version.sh --binary→v0.2.0-dev.ci/version.shis the same computation the package version comes from (§2.1), so the string the panel shows always matches whatapk info shaterd/opkg statusreport.--fast— skipnpm ciwhenpanel/node_modulesalready exists.UPX=/path/to/upx— override the UPX binary (defaultupxonPATH). UPX is cross-arch, so one host packs both the amd64 and aarch64 ELFs. (Note: UPX also reads$UPXas its own options — the scriptunsets 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 — keep in sync with docs-shater/DECISIONS.md):
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_gvisor: the shater data plane is tproxy/redirect and generate
never emits a tun inbound, so the userspace gvisor netstack is unreachable code.
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.
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).
Both package managers offer 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, and both managers agree on it (checked with
apk version -t on apk-tools 3.0.3 and opkg compare-versions on opkg
38eccbb1): 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. Both lanes then assert the produced .ipk/.apk really carries it,
so a lost variable fails the build instead of shipping a stale version.
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
Install order follows the deps (shaterd → shater-core → luci-app-shater):
# <ver> = the release version, e.g. 0.2.7-r1 (§2.1 — it comes from the git tag)
opkg install shaterd_<ver>_<arch>.ipk # or: apk add shaterd (25.12+)
opkg install shater-core_<ver>_all.ipk
opkg install luci-app-shater_<ver>_all.ipk
opkg install byedpi_0.17.3-r1_<arch>.ipk # optional: ByeDPI egress
Installing from a signed feed instead:
# add the feed (customfeeds.conf / apk repositories), then:
opkg update && opkg install shater-core luci-app-shater # shaterd pulled in as a dep
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.
5. Add the signed feed (recommended — then opkg upgrade just works)
CI (.gitea/workflows/release.yml) publishes every build as a rolling latest
Gitea release that is itself a signed opkg src/gz feed: the release holds the
.ipk for all arches, a Packages/Packages.gz index, a usign Packages.sig,
and the public key shater-feed.pub. opkg filters by Architecture, so the same
two lines work on every device (x86 testbed picks x86_64 + all; the BPI routers
pick aarch64_cortex-a53 + all).
Format: OpenWrt 24.10 (our SDK) uses opkg (
.ipk,Packages.gz, usign), so the feed issrc/gzand the trust anchor is the usign keydist/shater-feed.pub(fingerprint5ac4b177689cb8e0). apk only replaces opkg at OpenWrt 25.12 — see §6.
One-time setup on the router:
# 1) trust the feed key — the FILENAME must equal the usign key fingerprint.
wget -O /etc/opkg/keys/5ac4b177689cb8e0 \
https://git.qomar.pw/omar/shater/releases/download/latest/shater-feed.pub
# 2) add the feed (one URL serves every arch).
echo "src/gz shater https://git.qomar.pw/omar/shater/releases/download/latest" \
>> /etc/opkg/customfeeds.conf
# 3) refresh + install (shaterd is pulled in as a dependency).
opkg update
opkg install luci-app-shater # -> shater-core -> shaterd
opkg install byedpi # optional: ByeDPI desync egress
With the key installed, opkg's default check_signature 1 verifies the feed on
every opkg update; no --nocheck-signature needed. A tagged release
(vX.Y.Z) publishes the identical layout at
.../releases/download/vX.Y.Z if you prefer to pin a version instead of tracking
latest.
Updating
Name the packages. Never run a bare opkg upgrade — with no arguments it
tries to upgrade every installed package from every configured feed, which on
OpenWrt means base/system packages on the overlay and is a well-known way to
brick a router.
opkg update
opkg upgrade shaterd shater-core luci-app-shater byedpi # only our own packages
Drop byedpi from the list if you never installed it. An upgrade is offered only
when the feed's Version differs from the installed one — that is exactly what
bug B4 broke (v0.2.2…v0.2.6 all published as 0.2.0-r3). Since then CI derives
the version from the git tag on every build (§2.1), so there is nothing to bump
by hand any more; check with:
opkg list-installed | grep -E 'shaterd|shater-core|luci-app-shater|byedpi'
6. apk feed (OpenWrt/ImmortalWrt 25.12+ — incl. BananaWRT 25.12-mtk-vendor)
OpenWrt/ImmortalWrt 25.12 replaces opkg 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). The
package Makefiles are unchanged — the SDK release decides the format.
CI builds this lane in parallel with the opkg feed (same manual triggers:
v* tag push or workflow_dispatch): the build-apk jobs in
.gitea/workflows/release.yml compile the same 4 packages through the official
ImmortalWrt 25.12 SDK (tarballs from
downloads.immortalwrt.org/releases/25.12.1/targets/{x86/64,mediatek/filogic}/)
and publish one release per arch — rolling apk-latest-x86_64 /
apk-latest-aarch64_cortex-a53, or apk-vX.Y.Z-<arch> for a tagged version.
Per-arch (unlike the combined opkg release) because apk filenames carry no
architecture and packages are fetched relative to the packages.adb URL.
Key: apk cannot use the usign key. The apk trust anchor is the separate EC public key
dist/shater-apk.pem(generated once byci/gen-apk-key.sh; private half lives ONLY in the Gitea secretKEY_APK, the apk analog ofKEY_BUILD). Never regenerate either key — that invalidates every deployed router's trust. The usign identityshater-feed.pubkeeps signing the opkg/24.10 feed, untouched.
One-time setup on a 25.12 router (BananaWRT 25.12-mtk-vendor on the BPI-R3
mini, BPI-R4 on 25.12, or the future 25.12 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.
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
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). Pin a version instead of tracking
rolling by pointing the repo line at
.../download/apk-vX.Y.Z-$(cat /etc/apk/arch)/packages.adb.
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.