Both routers are past opkg: mini_router runs ImmortalWrt 25.12.1 and main_router OpenWrt 25.12.0, both with apk-tools 3.0.5, and main_router has no `opkg` binary at all. The 24.10 lane was building and signing a feed no device could consume. Removed jobs `build` and `release` with the scripts only they called (ci/build-feed.sh, ci/sdk-build.sh, ci/make-index.sh, ci/install-usign.sh) and the usign trust anchor dist/shater-feed.pub. A committed public key is an instruction: it invites the old install path for a feed that is no longer produced. The key is retired, not revoked -- git history keeps it, KEY_BUILD still holds the secret half, and a usign secret contains its own public half, so the identity is reconstructible if a 24.10 device ever needs serving. D7 is marked SUPERSEDED by the new D22 rather than deleted. Separately: the rolling `apk-latest-<arch>` release was frozen at 0.2.0 from 2026-07-24 while every tag run published its versioned release correctly. The publish loop was an either/or -- `TAG=apk-latest-<arch>` when VER=latest (workflow_dispatch only), ELSE `TAG=apk-<ver>-<arch>` -- so a `v*` tag run never touched the rolling pointer. Asset replacement was never the problem; ci/gitea-release.sh already deletes before recreating. A router pinned to the rolling URL sat on 0.2.0 while `apk update` reported success: silent staleness, the failure mode this repo keeps having to close. The rolling pointer is now published on EVERY run, tag runs included, and a new assert reads the release back over the API afterwards: our three tag-versioned packages at the built version plus the index and the key must be present (exit 13), and no package asset at any other version may survive (exit 14). Same class of check as sdk-build-apk.sh's package-version assert, added for the same reason -- the previous failure mode was silent. KEY_BUILD can now be deleted from the Gitea repo secrets; nothing references it. Docs state plainly that mini_router is deliberately pinned to a versioned URL and that the hand-edit per release is the price of pinning. Known consequence: the x86_64 QEMU testbed is still OpenWrt 24.10.3 and can no longer install our packages. Its 25.12 rebuild is in flight separately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H4PcWfrBRyg4eWN58axaGN
7.3 KiB
7.3 KiB
Roadmap
Phased plan for shater v0.2. Each phase ends with something verifiable on the test
VM (see CONTEXT.md for the testbed). Port proven logic from the v0.1 branch;
build new logic in the shater/, panel/, openwrt/ overlay.
Phase 0 — Repo reset & context ✅
- Preserve the working xray-based project on branch
v0.1. - Clean
mainto a docs-first scaffold; write CONTEXT / DECISIONS / ARCHITECTURE / ROADMAP / FEATURES / README. License → GPL-3.0. Keep the feed key.
Phase 1 — Fork & embedding prototype ✅ DONE (2026-07-14)
- ✅
mainis the fork: mergedLeadaxe/sing-box-lxv1.14.0-lx.3 (--allow-unrelated-histories), overlay dirs kept,submodules/wireguard-gopinned1adc4c71(AWG 2.0 engine). - ✅ Embed proven:
shater/cmd/shater-protodrives the engine viabox.New(not a subprocess); real socks5 request flows through it on the VM. tproxy + a real protocol outbound constructed in Go and validated viabox.New. - ✅ AmneziaWG 2.0 E2E: our client did a real handshake against live Cloudflare
WARP (config lifted from the router's
warp2);cdn-cgi/traceflippedwarp=off→warp=onthrough the tunnel. WARP/AWG params + thei1CPS packet map 1:1 to sing-box'sendpoints[](wireguard type) — no transform needed. - ✅ Size: raw static ~40–43 MB → UPX-lzma ~9–11 MB (well under ≤18 MB).
- ✅ Cross-compiled
x86_64+aarch64(arm64); runs on the musl OpenWrt VM using the router tag set (seeDECISIONS.mdD9 — dropwith_naive_outbound,with_purego). - Gate: PASSED → proceed to Phase 2.
Phase 2 — Control-plane port ✅ DONE (E2E gate PASSED 2026-07-15)
- Port from v0.1 (adapt to sing-box types): UCI model, config generator (→ sing-box
config, not xray JSON), share-link handling (reuse sing-box's parser where
possible), atomic apply/validate/rollback, nft
inet shater+ policy routing, idempotent reconcile under flock, kill-switch, management-bypass, live-flag, hotplug/boot persistence, subscription fetch (+ HAPP), rulesets/lists. - procd init + uci-defaults in
openwrt/(from v0.1shater-core). - DPI-bypass, native presets (D13): egress gains an optional
dpipreset;off|fragment|record|spoofmap to the already-compiled route-action fields (tls_fragment/tls_record_fragment/tls_spoof) on adirectoutbound. Lets a ruleset go direct + fragmented (DPI-blocked-but-not-IP-blocked domains) with no tunnel, no new binary. ✅ Wired ingenerate: adirectegress withdpiemits anegress-<name>direct outbound andbuildRoutestamps the matching route-action flag on every rule routed to it (fragment/record/spoof; fragment↔record mutual exclusion enforced).spoofinjects a decoy ClientHello and validates on the router build. - Gate: a real LAN client on the VM is proxied end-to-end through a sing-box
outbound, DNS anti-leak holds, kill-switch is honest. ✅ PASSED on the
OpenWrt VM (netns client → tproxy → engine → live Cloudflare WARP/AmneziaWG):
client egress 104.28.212.73 warp=on (direct = warp=off); DNS anti-leak holds
with dnsmasq stopped (engine hijack-dns answers, no WAN :53 leak); kill-switch
honest (SIGKILL engine → client blocked, no WAN leak). Three bugs found+fixed by
the gate: fail-closed forward drop (
4f618140), engine apply-swap close-first fallback (9b6b9406), DNS hijack-dns per D14 (86194ce6).
Phase 2b — DPI-bypass egress = ByeDPI (D13)
- The one external desync tool is ByeDPI (ciadpi) — chosen over zapret because it is a SOCKS egress (fits shater's "routing picks the egress" model with zero packet-plane conflict); zapret is explicitly rejected (see D13).
- Add egress
dpivaluebyedpi: a supervised localciadpiSOCKS5 instance + asocksoutbound pointed at it. Newopenwrt/procd package + musl-static cross-build of ciadpi (~100 KB);modelreserves the egress kind,generatewires thesocksoutbound. - QUIC gap closed by routing (drop
udp/443for desync-domains → TCP+TLS fallback), not by adopting a packet plane. - The
byedpipackage (openwrt/byedpi/, SEPARATE & optional) provides theciadpiprocess behind atype='byedpi'egress: it cross-compiles ciadpi via the SDK toolchain and ships a procd init that supervises oneciadpiSOCKS5 desync instance per enabledconfig instancein/etc/config/byedpi(127.0.0.1:<port>). Install it only when you want a byedpi egress; atype='byedpi'egress with no matchingciadpilistener simply has nothing to dial.shater-coredoes NOT depend on it (opt-in). - Gate: a DPI-blocked domain (that plain
fragmentcan't crack) loads via thebyedpiegress on the VM, direct (no tunnel), kill-switch still honest.
Phase 3 — Admin panel MVP + thin LuCI launcher ✅ DONE (2026-07-15)
panel/: embedded web server on its own port + session store; token-mint ubus method in the thin LuCI app; token-handoff auth (see ARCHITECTURE §2).- Thin LuCI app: mini dashboard (status + throughput) + "Open panel" button; reuse the v0.1 "instrument panel" design language.
- Panel SPA MVP: status/overview, node/subscription management, basic routing rules, apply/rollback. Frontend build embedded into the binary.
- Gate: log into the panel from LuCI via token, configure a proxy, apply.
Phase 4 — DNS filter & blocklists ✅ DONE (gate PASSED 2026-07-15)
- ✅ Foundation (D15): model blocklist/allowlist + generate rule-sets + reject/allow DNS rules (box.New-validated); panel DNS/Blocklists page; seeds StevenBlack/OISD/AdGuard; cache_file +
blocklist updateverb; blocks return NXDOMAIN/0.0.0.0. Gate PASSED on VM (blocked→NXDOMAIN, normal resolves, allowlist overrides, geosite megalist RSS ~39MB). - In-process DNS filter hooking sing-box DNS: blocklists with flexible sources
(
inline/file/url/geosite), allowlist overrides, NXDOMAIN/0.0.0.0. - Efficient matcher for megalists (compiled/cached, dedup, bloom prefilter); seed well-known lists (StevenBlack/OISD/AdGuard).
- Gate: ad/tracker domains blocked network-wide; big list loads fast; RAM sane.
Phase 5 — Statistics (per-domain / client / device)
- Stats aggregator consuming the engine's DNS/routing/stats observability + nft counters: top domains, allowed vs blocked, per-device breakdown, timelines, per-node/per-rule traffic, live query log with one-click block.
- Panel dashboards for all of the above.
- Gate: live, accurate per-domain and per-device stats in the panel.
Phase 6 — Per-device control & parental ✅ DONE (gate PASSED)
- Devices page: discover (dhcp.leases + ip neigh), name, live status; per-device toggles (proxy on/off, blocklists, exit country), per-device domain block/allow.
- Gate: block a domain for one device only; route one device via a chosen exit.
Phase 7 — Schedules & alerts ✅ DONE
- Time-based rules (bedtime/school hours) via the ported scheduler; Telegram/ webhook alerts (sub expiry, node down, kill-switch trip, new device).
Phase 8 — Ship it ✅ DONE
- Adapt CI to build/sign the single forked binary for both arches; publish the
signed feed (apk since D22, EC key
dist/shater-apk.pem); install/upgrade docs. - Set an upstream-rebase cadence (merge new sing-box-lx tags, run the smoke suite).
Cross-cutting (every phase)
Tests + live VM verification before commit; keep the fork overlay conflict-free; document as we go.