Files
shater/docs-shater/INSTALL.md
T
omarandClaude Opus 5 a8f2b0f068 ci: derive package versions from the git tag (B4)
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>
2026-07-25 12:32:38 +03:00

14 KiB
Raw Blame History

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:

  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 info shaterd / opkg status report.
  • --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 — 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.

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 is src/gz and the trust anchor is the usign key dist/shater-feed.pub (fingerprint 5ac4b177689cb8e0). 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 by ci/gen-apk-key.sh; private half lives ONLY in the Gitea secret KEY_APK, the apk analog of KEY_BUILD). Never regenerate either key — that invalidates every deployed router's trust. The usign identity shater-feed.pub keeps 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.