Run 60 settled what runs 58/59 left open. The second pass wrote an explicit
`# CONFIG_PACKAGE_kmod-x is not set` for all 1126 selected kmods and re-ran
defconfig; the count came back 1078, unchanged. The same explicit form DID hold
for CONFIG_ALL/ALL_KMODS/ALL_NONSHARED in the same run.
The difference is prompts. kconfig honours a user value only for symbols that
have one — sym_calc_value ignores S_DEF_USER for a promptless symbol and falls
back to its `default`. ALL* carry prompts in the SDK's Config.in; the blocks
convert-config.pl generates are bare:
config PACKAGE_kmod-mlx5-core
tristate
default m
No value written into .config can turn those off, so remove the `default m`
itself: drop every generated `config PACKAGE_*` block from Config-build.in
before the first defconfig. Nothing is lost — those blocks only replay which
packages the buildbot built. The packages stay declared, with prompts, by the
package tree (tmp/.config-package.in), which is what makes our four selectable
and what `select` acts on; KERNEL_*/LIBC/TOOLCHAIN blocks are untouched, so the
SDK still reproduces its own toolchain settings.
The .config second pass is kept as a cheap backstop (it no-ops once the count
is 0), as are both tripwires.
Verified: bash -n on the file and on the extracted INNER body; the paragraph
delete tested on a synthetic Config-build.in (3 PACKAGE blocks -> 0, KERNEL_*,
LIBC and TOOLCHAINOPTS preserved); the missing-file path exercised under set -eu.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Turning ALL/ALL_KMODS/ALL_NONSHARED off (22d7161c0) provably worked — run 59
logs all three as `is not set` after defconfig — and changed the kmod count by
exactly zero, 1078 both times. The kmods never came from ALL_KMODS.
They come from the SDK itself. target/sdk/Makefile generates the SDK's
Config-build.in by running convert-config.pl over the BUILDBOT's .config, in
which ALL_KMODS=y had already expanded into one `CONFIG_PACKAGE_kmod-*=m` line
per module. convert-config.pl turns every `CONFIG_X=<val>` line into a symbol
with an unconditional `default <val>`; its `next if /^(# )?CONFIG_PACKAGE/`
filter sits in the `else` branch, which a line containing `=` never reaches.
The SDK therefore ships ~1078 verbatim blocks of `config PACKAGE_kmod-x /
tristate / default m`, none of which consult ALL_KMODS.
Fix: a second pass. The names only exist after kconfig has expanded the tree,
so after the first defconfig rewrite every selected kmod to `is not set` and
re-run defconfig. Two documented kconfig rules make this exact:
- an explicit value in .config beats a `default` (same rule that kept our
`# CONFIG_ALL* is not set` lines alive in run 59) -> the ~1078 stay off;
- `select` is OR-ed in after the user value, so shater-core's
`DEPENDS:=+kmod-nft-tproxy +kmod-nft-socket` brings those (and their
transitive kmods) back on their own.
Also correct the tripwire message, which still blamed CONFIG_ALL_KMODS: it now
prints the ALL* state AND the first few surviving kmods, so the two failure
modes are distinguishable at a glance.
Verified: bash -n on the file and on the extracted INNER heredoc body; the
rewrite simulated against a run-59-shaped .config (1078 -> 0 selected, our 4
packages, LOCALMIRROR and the ALL* lines untouched).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Run 58 proved the previous commit aimed at the wrong thing, and the
diagnostics it added are what showed it: "0 lines carried over" plus a
`grep: .config: No such file or directory`, then 1078 kmods selected
anyway (1109 on x86_64). So an SDK tarball ships no top-level .config at
all — there was never a buildbot config for us to be appending to.
The real source is the SDK's OWN top-level Config.in, target/sdk/files/
Config.in, which it carries instead of the main tree's:
config ALL_NONSHARED ... default ALL
config ALL_KMODS ... default ALL
config ALL ... default y
In the main tree all three default to n; the SDK flips ALL to y so that
`make world` in a bare SDK builds something. `make defconfig` therefore
selects the whole kernel from ANY .config, empty or not. This is stock
OpenWrt rather than an ImmortalWrt quirk — openwrt/openwrt's copy is
identical, which also means the awg-openwrt reference builds every kmod
too; it just never meets a disk quota on GitHub's runners.
Fix: write all three out as `# CONFIG_X is not set` before defconfig.
They have prompts in the SDK's Config.in, so they are user-settable and
an explicit value beats the default; `CONFIG_X=n` is not reliably
honoured for bools, hence the `is not set` form. Setting all three, not
just the root ALL, keeps this working whichever symbol roots the chain
in a future SDK.
Drops the hand-rolled CONFIG_TARGET_*/CONFIG_KERNEL_* carry-over as
redundant: target/sdk/convert-config.pl bakes the buildbot's non-package
settings into the SDK's generated Config-build.in as kconfig defaults,
so defconfig reproduces them by itself. A soft branch keeps target
identity and CONFIG_USE_APK if some future SDK does ship a .config.
Diagnostics gain a post-defconfig readout of the three mass-select
symbols and, while the list is short, the actual kmods selected — a
count of 0 is not fatal (the router's base feed carries them) but is
worth seeing. Guards and the 200 threshold are unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both apk jobs of v0.2.2 died with `Disk quota exceeded`. The SDK was
running `apk mkpkg` on 3593 kmod-* packages (mlx5, amdgpu, ata, isdn —
none of which we ship) before it ever got near our four.
Root cause: ci/sdk-build-apk.sh APPENDED our package selections to the
.config that ships inside the ImmortalWrt SDK tarball. That file is the
buildbot's fully-expanded config and carries CONFIG_ALL_KMODS=y plus
CONFIG_ALL_NONSHARED=y (see config.buildinfo next to the SDK), so
`make defconfig` re-selected every kernel module of the target as =m and
package/kernel/linux/compile — pulled in via shater-core's nft kmod
deps — packed the lot.
Fix, modelled on Slava-Shchipunov/awg-openwrt's "Setup SDK and feeds":
start the .config EMPTY so kconfig can only pull in what our packages
actually select. Carried over from the SDK's .config, nothing more:
the target choice and its BOARD/SUBTARGET/ARCH_PACKAGES identities (a
wrong guess here means silently cross-compiling for another arch),
CONFIG_USE_APK (decides .apk vs .ipk — the point of this lane), and
CONFIG_KERNEL_* verbatim (they generate the kernel .config; dropping one
makes the buildsystem reconfigure and rebuild the SDK's prebuilt kernel).
Also adds the diagnostics this lane never had, since a failed run leaves
a 27 MB log: the carried-over identity lines, the post-defconfig kmod
count and target readout, a hard check that all four of our packages
survived defconfig, an abort if the kmod count is back in the hundreds,
and du/df after compile.
opkg lane (ci/sdk-build.sh, ci/make-index.sh) untouched. LOCALMIRROR,
CONFIG_DOWNLOAD_FOLDER and every cache path are unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- ci/make-index.sh: set -e → set -euo pipefail so a failing sha256sum|cut in
the signed Packages index can't mask an empty SHA256. Script survives -u
(all vars use :? or :- defaults).
- .github/deb2ipk.sh: quote $2/$DEB_NAME/output, derive the deb name from the
copied file via basename instead of parsing `ls *.deb` (glob-fragile), add a
trap-based tmpdir cleanup, and set -euo pipefail.
bash -n clean on both.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Audit of the run-51 logs showed actions/cache@v3.3.2 works on the act_runner
(cold: "Cache saved" x4; next job: "Cache restored" in ~2s, npm --fast skip,
usign/dl reused) and the sdk-cache mirror seeds correctly — but the single
biggest recurring cost was NOT cached: `scripts/feeds update -a` re-cloned
base+packages+luci+routing+telephony every run (~7.8 min warm x 4 SDK jobs on
the serial runner ≈ ~28 min/run wasted; github ~1 MB/s from this host).
Cache .cache/feeds/{opkg,apk} (workspace dir, actions/cache-persisted, visible
in the SDK container via --volumes-from) symlinked over the SDK's empty feeds/:
`feeds update` now git-fetches deltas (seconds) instead of full clones, always
checking out feeds.conf's pins. Fail-safe: any error on the cached checkouts
wipes the cache and clones fresh. Key by SDK release (feeds-opkg-24.10.4 /
feeds-apk-25.12.1) — stable across runs, invalidates on an SDK bump; both arch
jobs of a lane share one entry (identical pins, serial runner).
Steady-state warm run: ~60+ min -> ~20-22 min. Also documented in the workflow
header: never key a cache on github.sha — each cache SAVE stalls the act_runner
~3 min, so per-run-changing keys would add +3 min/entry every run.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds were dominated by re-fetching the ImmortalWrt 25.12 SDK tarball
(~300 MB) every run, and a stalled downloads.immortalwrt.org transfer wedged
the apk job for 40+ min (plain `wget -q`, no timeout — same class as the
elfutils hang).
- New ci/fetch-sdk.sh (runner-side): cache -> our durable `sdk-cache` release
mirror -> upstream with a stall-kill (curl --speed-limit 64K --speed-time 60
--max-time 1800) + 3 retries + zstd-magic/size validation; seeds the mirror
best-effort (github.token, non-fatal) so cold runs never touch upstream again.
A 40-min hang is now impossible; the in-container fallback wget also gets
--timeout=60 --tries=3.
- actions/cache@v3.3.2 (last release on the OLD cache API that Gitea act_runner
implements; v4/v3.4.x use the new GitHub cache service) for: SDK tarball, SDK
dl/ sources (hash of package Makefiles; PKG_HASH re-verified so a stale cache
can't leak a wrong source), Go mod+build (go.sum), npm node_modules
(package-lock.json) with build-shaterd.sh --fast, apt archives, built usign.
Degrades safely if the cache server is off — the SDK mirror is independent.
- concurrency group release-${github.ref} cancel-in-progress so a re-dispatch
cancels the stale run instead of piling up (tags stay isolated).
Signing (usign/apk), both keys, per-arch publish, manual triggers, LOCALMIRROR
and the scoped 4-package collection are unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The apk lane runs the SDK on a bare debian:bookworm host, and the ImmortalWrt
25.12 SDK prerequisite check requires python3-distutils ("Checking
'python3-distutils'... failed. Prerequisite check failed." ->
.prereq-build Error 1), aborting before any package built. The opkg lane was
unaffected because the openwrt/sdk image ships the prereqs. Add
python3-distutils (and python3-setuptools defensively) to the host deps. apk
lane only; opkg untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two build-harness bugs surfaced once the SDK builds actually ran:
1. Permission denied writing the feed. ci/build-feed.sh creates $OUT as root on
the runner, but the openwrt/sdk container runs as the unprivileged `buildbot`
(uid 1000) — so `cp` of the .ipk into $OUT failed ("Permission denied"),
yielding 0 packages and then "usign signing failed" (nothing to sign). Set
`chmod 0777 "$OUT"` on the runner before docker run (a chmod from inside the
container, as buildbot, cannot fix a root-owned dir). The apk lane already
chmods $OUT from its root debian container, so it was unaffected.
2. Collecting the whole SDK. ci/sdk-build.sh did `find bin -name '*.ipk'`, which
swept up the hundreds of prebuilt kmod/base .ipk shipped in the SDK image —
bloating the feed and signing foreign kmods under our key. Collect strictly
our four by name (`<pkg>_*.ipk`) and require >=4. Applied the same narrowing
to ci/sdk-build-apk.sh (apk names carry no arch: `<pkg>-*.apk`), keeping the
"wrong SDK produced only .ipk" guard.
No change to the feed format/signing (usign/KEY_BUILD/shater-feed.pub, apk EC
key), the package set, or triggers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The apk (and opkg) SDK builds intermittently hung fetching build-time
sources like elfutils-0.192.tar.bz2 from sourceware.org: curl's
--connect-timeout covers only the TCP handshake, not a stalled mid-transfer,
so a slow upstream hangs the whole job (no --max-time in OpenWrt download.mk).
Set CONFIG_LOCALMIRROR=https://sources.cdn.openwrt.org in .config before
`make defconfig` in both ci/sdk-build-apk.sh and ci/sdk-build.sh so the SDK
tries the fast OpenWrt source CDN before each package's own PKG_SOURCE_URL —
fixes elfutils and any other flaky upstream. Mirror verified to hold the file.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Additive next to the opkg/24.10 lane — nothing existing changed. The same 4
packages (shaterd, shater-core, luci-app-shater, byedpi) are built through the
official ImmortalWrt 25.12 apk-SDK and published as per-arch rolling releases
apk-latest-<arch> / apk-<tag>-<arch> (x86_64, aarch64_cortex-a53).
- ci/sdk-build-apk.sh: drives the 25.12 SDK inside debian:bookworm, compiles
.apk, then `apk mkndx --root T --keys-dir T/keys --allow-untrusted
--sign KEY --output packages.adb *.apk` — the exact form the OpenWrt 25.12
buildsystem uses (unsigned members, signed index).
- ci/build-feed-apk.sh: per-arch runner entrypoint (same --volumes-from and
artifact-order contract as ci/build-feed.sh).
- ci/gen-apk-key.sh: one-shot EC (prime256v1) keypair generator; private half
-> Gitea secret KEY_APK, public dist/shater-apk.pem committed.
- release.yml: additive build-apk / release-apk jobs; `on:` triggers untouched
(v* tags + workflow_dispatch); apk release tags deliberately non-`v*`.
- docs-shater/INSTALL.md section 6, .gitignore (out-apk/), dist/shater-apk.pem.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports the v0.1 Gitea release flow to the v0.2 single-binary + 4-package
layout, so a tag publishes a signed opkg feed the routers install from.
- .gitea/workflows/release.yml: on tag v* (+ dispatch), matrix over
{x86_64, aarch64_cortex-a53}. Per arch: setup Go 1.24/Node 20/UPX ->
scripts/build-shaterd.sh (SPA-embedded shaterd, stages the .upx) ->
ci/build-feed.sh (OpenWrt SDK container builds all 4 packages ->
usign-signed Packages index). A release job merges both arches into one
signed feed + publishes the rolling 'latest'/tag release via the Gitea API.
- ci/sdk-build.sh: in-SDK build — add openwrt/ as the 'shater' feed, feeds
update/install, make package/{shaterd,shater-core,byedpi,luci-app-shater}/
compile (shaterd validates+installs the staged prebuilt; byedpi cross-
compiles from source). ci/make-index.sh: opkg Packages(.gz) + usign sign
with KEY_BUILD (keyfile umask 077, no secret hardcoded), verifiable by
dist/shater-feed.pub. ci/install-usign.sh + ci/gitea-release.sh ported.
- INSTALL.md: add the signed feed src/gz line + import dist/shater-feed.pub
to /etc/opkg/keys; apk (25.12) path noted.
Key kept: usign feed key 5ac4b177689cb8e0 (public dist/shater-feed.pub,
secret Gitea repo secret KEY_BUILD). Decision: opkg (24.10 uses opkg; apk
is 25.12) — matches the existing usign trust anchor.
Verified structurally (no live runner here): release.yml is valid YAML, all
ci/*.sh are bash -n clean, no hardcoded secrets, and every package name/
path/arch/artifact/secret reference cross-checks against openwrt/, scripts/
build-shaterd.sh, and dist/shater-feed.pub. Live-runner unknowns (full SDK
compile of the 4 packages, router-side signature verify) flagged in-agent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLthkP2S8WAfxu7fcYbPfE
Foundation pivot. The complete, working, VM-verified xray-based project is
preserved on the `v0.1` branch; `main` is reset to a docs-first scaffold for
v0.2, which will be built as a FORK of sing-box-lx with our control-plane,
DNS filter, stats and admin panel embedded in the one binary.
- Preserve everything on branch v0.1 (pushed).
- Remove the v0.1 implementation + old design docs from main (recoverable from
v0.1); keep LICENSE, .gitignore, .gitattributes, dist/shater-feed.pub (feed
signing key 5ac4b177689cb8e0 carries over).
- License -> GPL-3.0 (sing-box is GPL-3.0).
- Add full project context so it survives compaction:
docs/CONTEXT.md (start here), DECISIONS.md, ARCHITECTURE.md, ROADMAP.md,
FEATURES.md, and a new README.
Engine/UI decisions (see docs/DECISIONS.md): fork sing-box-lx (AmneziaWG 2.0 +
broad protocols, GPL-3.0, library-first) and embed the whole product for tight
integration; keep the fork maintainable via an additive overlay (shater/, panel/,
openwrt/) rebased on upstream tags. UI = thin LuCI launcher + a separate admin
panel served by the daemon, entered via a short-lived token minted in the
authenticated LuCI session. Do NOT write a proxy engine from scratch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLthkP2S8WAfxu7fcYbPfE
The published feed is now usign-signed, so routers keep opkg's signature
verification ON instead of needing --no-check-signature.
- ci/install-usign.sh builds the standalone usign on the runner (the index steps
run on the bare runner, not in the SDK container).
- ci/make-index.sh signs Packages -> Packages.sig with the secret key from the
Gitea repo secret KEY_BUILD; it now FAILS the build if KEY_BUILD is set but
usign is missing/broken, rather than silently shipping an unsigned feed.
- build.yml installs usign in both the per-arch build and the combined-index
release step, passes KEY_BUILD to the release step, and publishes the public
key (dist/shater-feed.pub) as the release asset shater-feed.pub.
- Setup is now: install the public key once into /etc/opkg/keys/<fingerprint>,
then plain opkg update/install/upgrade with check_signature left on.
Verified locally on the VM: usign -S/-V round-trips, and with check_signature=1
and only the signed feed, `opkg update` + `opkg install luci-app-shater` succeed
with no --no-check-signature. FEED.md/README updated (key rotation documented).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLthkP2S8WAfxu7fcYbPfE
The release job failed on re-runs (rolling `latest` already present): a greedy
`.*"id":` match extracted the LAST id on the one-line JSON (the author id, 10)
instead of the release id, so the delete hit a nonexistent release, create then
409'd, and an unguarded `curl -f` aborted the job.
- extract the FIRST "<key>": <n> (release id precedes nested author/asset ids);
- drive the API through a code-returning helper (no `-f` aborts) and branch on
HTTP status; reuse the existing release on 409;
- delete a same-named asset before re-upload so re-runs replace cleanly;
- per-object tag scan in the list fallback.
Verified end-to-end (two idempotent runs) against the live Gitea API.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLthkP2S8WAfxu7fcYbPfE
pack-core.sh / pack-luci.sh hardcoded 0.1.0-r7, so shater-core and
luci-app-shater never changed version and `opkg upgrade` ignored new builds.
Derive PKG_VERSION-rPKG_RELEASE from each package Makefile (matching what the
SDK already does for xrayctl); add explicit PKG_VERSION/PKG_RELEASE to the
luci Makefile. Result: shater-core r19, luci-app-shater r8, xrayctl r18.
Document the Gitea auto-release flow and router install in BUILD.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLthkP2S8WAfxu7fcYbPfE
- document/label the arch matrix: aarch64_cortex-a53 covers BOTH production
routers (mini_router = BPI-R3 / MT7986, main_router = BPI-R4 / MT7988 — both
mediatek/filogic); x86_64 is the testbed VM. Only xrayctl is arch-specific.
- add a `release` job (needs: build) that publishes automatically via the Gitea
API (curl, ci/gitea-release.sh — no external action needed on the self-hosted
runner): push to main refreshes a rolling `latest` pre-release; a `vX.Y.Z`
tag publishes a versioned release. Assets: per-arch feed tarball
(shater-feed-<arch>.tar.gz, ready to serve to opkg) + loose .ipk files.
Auth: repo secret RELEASE_TOKEN if set, else the auto GITHUB_TOKEN.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLthkP2S8WAfxu7fcYbPfE
Records live-VM proof for EVERY MVP/T1 catalog item (nodes/subs/HAPP/hwid, all six
balancer strategies, chains, full rule matcher set incl. MAC/iface/geosite/geoip,
egress iface/tunnel/proxy, lists, DNS DoH/FakeIP/nftset/hijack/DoT-block/IPv6,
reliability commit-confirm/rollback/watchdog/kill-switch/hotplug, per-client &
per-rule stats, LuCI CRUD via Playwright, CI both arches, feed install+upgrade).
Two bugs found & fixed during the sweep (balancer observatory; group probe_url
override). PKG_RELEASE -> 7.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The group parser defaulted probe_url/probe_interval to gstatic/60s via optOr, so
EVERY group carried a non-empty ProbeURL that overrode the new global observatory
setting in newBuilder — the Settings "Health-check URL" never took effect. Groups
now start empty and only override when explicitly set; the built-in default lives in
newBuilder. Verified: setting globals.probe_url -> the generated observatory block
uses it (cloudflare/30s), xray -test OK.
Also bumps PKG_RELEASE 5->6 across the packages for the probe/observatory feature.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PKG_RELEASE 4->5 for the Nodes Test fix, Overview dashboard redesign, and egress
exit-interface data-plane. PROOFS.md updated with the live-VM evidence; verified via
opkg upgrade r4->r5 and Playwright over all 11 pages.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two release-blocking defects found via live 512M OpenWrt VM testing.
1) Fork-storm / OOM (critical). Reconcile() -> reloadXray() ->
"/etc/init.d/shater reload" -> reload_service -> shater_reconcile ->
`xrayctl reconcile` -> Reconcile() was an infinite mutual recursion; each
level blocked on CombinedOutput and spawned a process (854x reconcile +
853x reload observed), exhausting RAM and pinning both vCPUs -> OOM-killer.
- Reconcile() no longer calls reloadXray(); procd's file-watch on run.json
restarts xray. Reconcile runs inside the init lifecycle, so calling the
init back is the recursion.
- reloadXray() now runs `/etc/init.d/shater start` (not `reload`); it is only
invoked from Apply()/Rollback() (LuCI/ubus/CLI), never the init lifecycle.
- init reload_service() simplified to start/stop (dropped the double reconcile).
Verified: `xrayctl apply` 0.39s (was >185s hang); 93 procs / load 0.00 stable
with an active 254-node config on 512M; xray binds :10853 API + :12345 tproxy.
2) UCI config namespace clash. shater shipped /etc/config/xray, which collides
with the xray-core dependency's own /etc/config/xray (opkg dropped ours to
/etc/config/xray-opkg, so the schema never took effect). Renamed the UCI
namespace xray -> shater everywhere (Go uci export/commit, all 11 LuCI views +
acl, init scripts, hotplug, Makefile conffiles, examples). Runtime paths
(/etc/xray/run.json, /usr/share/xray), the xray-core package, and the ubus
object name are intentionally unchanged.
Also: ci/pack-xrayctl.sh (local ipk packer) with a postinst chmod so the binary
is executable regardless of host-tar mode handling; PKG_RELEASE bumped to r3.
Verified end-to-end on a fresh 512M OpenWrt 24.10.3 VM: feed install
(opkg install luci-app-shater pulls xrayctl+shater-core), real LAN client
(netns in br-lan) proxied through a live subscription node -> exit IP changed
(104.156.233.234 vs 45.131.214.140 direct); per-client nft counters + xray
stats API populated. See PROOFS.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- build-sdk.sh: build ONLY xrayctl (its deps are light) — no kernel/kmod rebuild
(shater-core's kmod-nft-tproxy dep was dragging in a full kernel build)
- pack-core.sh: shater-core data .ipk (Depends/conffiles/uci-defaults per Makefile)
- pack-luci.sh: fix $OUT to an ABSOLUTE path (relative path after 'cd $W' made tar
write to the wrong dir -> 'Cannot open ... No such file')
- workflow: add Pack shater-core step
- all pack scripts dry-run green locally (valid .ipk structure + index)
bare -v $PWD failed (act_runner DinD: $PWD is a host path that doesn't exist)
-> 'No such file /repo/ci/build-sdk.sh'. Use --volumes-from $(hostname) so the
job's workspace volume is mounted in the SDK container; build-sdk.sh now reads
$REPO=$GITHUB_WORKSPACE and guards against an unmounted repo.
- ci/build-sdk.sh: run openwrt/sdk image with a CLEAN single-package feed; compile
xrayctl (Go) + shater-core (in-tree, files only, no dep rebuild)
- ci/pack-luci.sh: luci-app-shater as data .ipk via tar (no luci-base compile)
- ci/make-index.sh: opkg Packages index + SHA256 (+ usign sign if KEY_BUILD set)
- workflow: docker-run the SDK per arch (x86_64, aarch64_cortex-a53), upload feed
This mirrors the recipe verified locally that produced all 3 installable .ipk.