Files
shater/docs-shater/ROADMAP.md
T
omarandClaude Opus 5 f86501bf77 ci!: drop the opkg lane — apk only, and fix the stale rolling release
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
2026-07-25 18:46:21 +03:00

7.3 KiB
Raw Blame History

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 main to 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)

  • ✅ main is the fork: merged Leadaxe/sing-box-lx v1.14.0-lx.3 (--allow-unrelated-histories), overlay dirs kept, submodules/wireguard-go pinned 1adc4c71 (AWG 2.0 engine).
  • ✅ Embed proven: shater/cmd/shater-proto drives the engine via box.New (not a subprocess); real socks5 request flows through it on the VM. tproxy + a real protocol outbound constructed in Go and validated via box.New.
  • ✅ AmneziaWG 2.0 E2E: our client did a real handshake against live Cloudflare WARP (config lifted from the router's warp2); cdn-cgi/trace flipped warp=off→warp=on through the tunnel. WARP/AWG params + the i1 CPS packet map 1:1 to sing-box's endpoints[] (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 (see DECISIONS.md D9 — drop with_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.1 shater-core).
  • DPI-bypass, native presets (D13): egress gains an optional dpi preset; off|fragment|record|spoof map to the already-compiled route-action fields (tls_fragment/tls_record_fragment/tls_spoof) on a direct outbound. Lets a ruleset go direct + fragmented (DPI-blocked-but-not-IP-blocked domains) with no tunnel, no new binary. ✅ Wired in generate: a direct egress with dpi emits an egress-<name> direct outbound and buildRoute stamps the matching route-action flag on every rule routed to it (fragment/record/spoof; fragment↔record mutual exclusion enforced). spoof injects 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 dpi value byedpi: a supervised local ciadpi SOCKS5 instance + a socks outbound pointed at it. New openwrt/ procd package + musl-static cross-build of ciadpi (~100 KB); model reserves the egress kind, generate wires the socks outbound.
  • QUIC gap closed by routing (drop udp/443 for desync-domains → TCP+TLS fallback), not by adopting a packet plane.
  • The byedpi package (openwrt/byedpi/, SEPARATE & optional) provides the ciadpi process behind a type='byedpi' egress: it cross-compiles ciadpi via the SDK toolchain and ships a procd init that supervises one ciadpi SOCKS5 desync instance per enabled config instance in /etc/config/byedpi (127.0.0.1:<port>). Install it only when you want a byedpi egress; a type='byedpi' egress with no matching ciadpi listener simply has nothing to dial. shater-core does NOT depend on it (opt-in).
  • Gate: a DPI-blocked domain (that plain fragment can't crack) loads via the byedpi egress 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 update verb; 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.