v0.2.17
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4078334d85 |
fix(stats,alert,panel): put a ceiling on everything that only grew
Four maps had no bound on a box with 512 MB that runs for months. The health board only ever inserted — the delete exists but no path in this fork calls it — and it lives on the engine context, so it outlives every generation. Its keys are node tags, and providers rename nodes on each subscription refresh: about 440k keys a year, some 88 MB. Alert dedup keyed on MAC with no delete at all. The stats aggregator's server and outbound counters were the only ones with no cap, no prune and no top-N, and one of them was handed to the panel whole on every poll. They are bounded now, evicting least-recently-seen, with numbers argued from this box rather than round: the board holds 4096 against a live generation of about 1200 tags, so a rename day cannot evict a tag still in use. Nothing is dropped silently — the same rule the log sink already follows — and a new Dropped section in the snapshot reports all six bounded aggregates, including the three that had been evicting without saying so. Snapshot did O(devices × domains) under the aggregator lock, sorting five thousand entries to show fifteen, and could read the DHCP lease file from inside it. Meanwhile the event subscribers have 64-slot buffers that drop without a counter, so an open Overview page cost the query log real rows. Selection is top-K now — proven byte-identical to the old sort over 200 random trials — and both the lease read and the row ordering happen outside the lock. The panel server had one timeout, on headers. An unauthenticated client could hold a goroutine, a socket and a descriptor forever by sending its body one byte at a time; a stopped reader on the log stream held the handler, the pipe and a child process that outlived the request. Every phase is bounded now, with the unauthenticated route on a tighter budget than the rest, and the log stream renewing its deadline per chunk so a slow-but-reading client is never truncated. And the last of the detour transports: each call built a fresh one, and the alert delivery path dropped it, pinning keep-alive sessions through the engine's own outbounds for 90 seconds — eighteen times the budget a retiring generation gets. The race skip is gone from the gate. The test it existed for raced in its own clock, not in the product; that is fixed, so nothing is excluded under -race any more. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
56a276bcc1 |
ci: make the tests a gate instead of a decoration
The fork had a full suite and no CI that ran it. Upstream's test workflows trigger on stable/testing/unstable; this repo only has main. And Gitea does not read .github/workflows at all once .gitea/workflows exists, so those files were decoration here. 115 of the 116 test files under shater/** had never executed in CI even once, which is how TestDNSFilterRemoteBlocklistHTTPClient stayed red across two published releases without anyone noticing. The gate is a job inside release.yml that build-apk needs, because a separate workflow cannot block another one. It runs the suite under the shipped tag set, on Linux — 6 of 7 test files in transport/wireguard and 12 in shater/generate compile only there or only under those tags, and those are exactly the files covering AmneziaWG. Three guards stop it from passing by running nothing, which is the failure this whole change is about. The tag set may only ADD test files, never remove one. Every package go list says has tests must appear as "ok <pkg>" in the output, so a suite that collapses to "no test files" fails instead of passing. And the panel run counts its test files first, because node --test exits 0 with "pass 0" when the glob matches nothing. The publish step used to exit 0 having published nothing: its assertions all live inside a loop over artifacts, so an empty directory ran the body zero times and reported success. It now counts what it published and fails on zero. Verified by extracting the shipped step text and running it against stubs: empty artifacts gives exit 0 before and exit 10 after; the rolling-release readback still fires its own exit 14. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1746d4d0ef |
fix: stop the panel and the shipped binary from lying about what works
Four defects, all found by the owner on the live router, all of the same family: something declared itself working while it was not. WIREGUARD WAS DEAD IN THE SHIPPED BINARY (B17). Setting up WireGuard gave "create WireGuard device: gVisor is not included in this build". The router tag set carried with_wireguard and with_awg but not with_gvisor, so sing-tun compiled its stub instead of the netstack every WireGuard device needs. FEATURES.md marks WireGuard [MVP] and AmneziaWG "a driving requirement", so this was a broken promise, not a trim. The tag itself was the small half. The tag set was the ONE build configuration nothing in the repo tested: TestAmneziaWGEndpoint passes because tests build with the full upstream tags. So the set now lives in one file (scripts/router-tags.sh) and two guards hold it to the feature list -- a static check that needs no tags, no Linux and no network (so the next such gap fails on the developer's machine), and a behavioural one that constructs every declared protocol through box.New UNDER THE SHIPPED TAGS, where skipping is forbidden. Removing the tag now fails with the feature name, the missing tag, and why: "Either add the tag back, or stop declaring the feature -- those are the only two honest options." Cost: +2.8 MB raw, +0.6-0.7 MB packed per arch. D23; D9 corrected. THE PANEL CALLED A DIRECT-ONLY ROUTER "PROTECTED" (B16). The headline came from plane === 'full', which reports whether the data plane is installed -- nft table, policy routing, live engine -- and says nothing about where the traffic goes. On a config with one `default -> direct` rule and no groups the plane is fully installed and every packet leaves in the clear, so the worst possible state rendered as the reassuring one. The verdict is now computed on the daemon FROM THE GENERATED OPTIONS at the moment they reach the engine, not from the model: buildRoute changes the answer (a scheduled rule outside its window is never emitted, only the last condition-less rule reaches Final, an unresolved target is rewritten by ruleKillFallback), and re-deriving it anywhere else is a second implementation that will drift -- model/reachability.go exists because two already did. Four verdicts, not three: `blocked` is separate because under a closed kill-switch with no catch-all nothing leaks, and calling that "going out directly" is a lie in the alarm direction. Rider: Overview's defaultTarget printed the highest-Order enabled rule as the default; a rule becomes Final by having no conditions, whatever its Order. "PREVENT THIS PAGE FROM CREATING ADDITIONAL DIALOGS" KILLED EVERY DELETE (B15). Once the browser suppresses dialogs, window.confirm returns false immediately, so all 15 confirmations across 7 pages read as "cancelled" and silently did nothing, with no way to recover from inside the panel. Replaced with an in-app dialog the browser cannot mute: focus trapped and parked on Cancel, Esc and veil cancel, focus returned to the opener, crit styling for destructive commits. useConfirm() throws if the provider is missing rather than falling back to a quiet false -- the failure mode being fixed. HYSTERIA2 AND TUIC NODES WERE DROPPED (B6). No share-link parser existed, so a feed's nodes of those types vanished. The real landmine was one layer up: ParseSubscriptionBody splits a feed by scheme prefix before parsing, so without schemePrefixes the links were gone before any parser ran and the fix would have looked complete. Undeliverable parameters are refused when the node cannot work or would be less secure than the link asked (obfs, pinSHA256, tuic v4/non-UUID) and flagged via Proxy.Warnings when it survives -- shaterd nodes shows both. uTLS is dropped for QUIC: it cannot produce a QUIC TLS config, and that fails at dial time, not at box.New. Also: nodes added by hand can be named and renamed. The name is the outbound tag, so a rename rewrites every reference in one PUT -- rule targets, group members, chain hops, detours -- in the spelling each already uses, and is refused outright when a group answers to the same bare name. Subscription nodes state why they cannot be renamed instead of hiding the control. go build, go vet, go test ./shater/... (13 packages), panel npm run build and npm test (13/13) all green. NOT yet verified on hardware. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H4PcWfrBRyg4eWN58axaGN |
||
|
|
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>
|
||
|
|
53cbdc75f1 |
build(shaterd): drop with_dhcp from the router tag set
shater resolver types are udp/tcp/doh/dot/local/fakeip; a dhcp:// DNS transport is never generated, and the slim shater/registry never registers the transport, so the tag gated nothing in this binary. D9 in DECISIONS.md and the INSTALL.md tag block updated to match. |
||
|
|
0e5b1afb80 |
build(shaterd): drop with_clash_api from the router tag set
The admin panel is shater's own web server and generate never emits a clash_api service (shater/engine/engine.go pre-registers its own dnstrack.Manager precisely because no api/clash_api observer exists on the router). With include.Context gone the Clash server was already out of the link; dropping the tag records the decision. Desktop/CLI LX_TAGS keeps with_clash_api for external dashboards. D9 in DECISIONS.md and the INSTALL.md tag block updated to match. |
||
|
|
d291c90cab |
build(shaterd): drop with_gvisor from the router tag set
The shater data plane is tproxy/redirect (netplane); generate never emits a tun inbound, so the userspace gvisor netstack is unreachable code. With the slim registry it was already dead-code eliminated by the linker — dropping the tag makes the intent explicit and stops compiling ~3.6 MB of gvisor sources into the build at all. A future tun inbound would fall back to the system stack; re-add the tag if that ever lands. D9 in DECISIONS.md and the INSTALL.md tag block updated to match. |
||
|
|
a5c74209e4 |
feat(openwrt/shaterd): release build + prebuilt shaterd package (Phase 8 ship)
Makes the whole product installable — shater-core DEPENDS +shaterd, and
this is what resolves it.
- scripts/build-shaterd.sh: the release build. Builds the panel SPA
(npm ci && npm run build), copies panel/dist -> shater/panel/webroot
(the go:embed dir), cross-builds shaterd for amd64 + arm64 with the D9
router tag set (CGO_ENABLED=0, -checklinkname=0 -s -w, static ET_EXEC no
PT_INTERP), then UPX --lzma --best (D10) and stages the .upx into
openwrt/shaterd/files. Version from arg/SHATER_VERSION/git-describe.
Measured: amd64 40.3MB->10.4MB, arm64 37.6MB->8.5MB.
- openwrt/shaterd: prebuilt-binary package (npm+embed+UPX don't reproduce
cleanly in the SDK, so CI stages the artifact). Maps OpenWrt ARCH
(x86_64->amd64, aarch64->arm64 = both BPI routers) to files/shaterd-<a>.upx,
installs /usr/bin/shaterd. RSTRIP/STRIP disabled (the SDK strip would
corrupt the UPX binary); DEPENDS empty (static); errors clearly when no
artifact is staged. GPL-3.0-or-later.
- docs-shater/INSTALL.md: build + install order (shaterd -> shater-core ->
luci-app-shater, optional byedpi) + enable/apply.
- gitignore: dist/shaterd-*, openwrt/shaterd/files/*.upx, panel webroot.
Verified on the OpenWrt musl VM: dist/shaterd-amd64.upx (10.4MB) decompresses
into RAM + runs (shaterd status OK), serves the REAL embedded Faceplate SPA
at :8088 ('SPA embedded=true', real Vite index.html + assets — not the
placeholder).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLthkP2S8WAfxu7fcYbPfE
|