modernc.org/sqlite is the only pure-Go SQLite and costs ~3.5 MB in the
static shaterd link; the stats store never used anything SQL-specific —
it is a ring of two append-only streams with a monotonic seq cursor.
bbolt is already linked via experimental/cachefile, so the swap is free.
sqlitering.go -> boltring.go: buckets queries/conns keyed by 8-byte
big-endian seq (bbolt key order == cursor order), rows as JSON of the
existing LogEntry/ConnLogEntry structs, meta bucket carries the durable
per-stream HWM (same max-only monotonic semantics). The async writer
contract is untouched (writeCh 4096, drop counters, 256-row/500ms
batches, 30s retention tick). Disk cap: chunked oldest-first deletes
with the same hysteresis, then at most one bbolt Compact per pass
(sagernet/bbolt exports Compact) behind the same 110%+1MiB free-space
guard that gated VACUUM. A legacy SQLite-format stats.db (or any
unreadable file) is replaced in place with one warning; open failure
still falls back to the in-memory ring.
Zero user-visible change: the "sqlite" backend selector value and the
Snapshot.Backend string are kept verbatim. Tests ported assert-for-
assert plus new coverage: legacy-file replacement, overflow drops,
memRing parity round-trip, disk-cap convergence.
Router shaterd (linux/amd64): 28,004,478 -> 24,428,670 bytes (-3.58 MB);
modernc.org/* gone from go.mod/go.sum and the dep graph.
The stats log rings move behind a logRing seam: memRing (extracted RAM ring,
byte-identical to before) + sqliteRing (new, modernc.org/sqlite v1.38.2 pure-Go,
CGO-free musl-static). backend=sqlite persists the query/conn LOG rows to
/etc/shater/stats.db (WAL, tmpfs fallback /tmp/shater-stats.db, own lock) via an
async batched writer off the DNS/conn hot path; seq = the PK (monotonic, resumes
from the persisted max after restart). Cursor reads = WHERE seq</> ? ORDER BY
seq DESC LIMIT. Retention: keep <= StatsRingSize rows/table + a StatsDiskLimitMB
disk cap (0=unlimited) with prune + wal_checkpoint/VACUUM. Aggregates
(top-domains/timeline/hosts/devices/node-health) stay in RAM (bounded, rebuild
fast) — only the unbounded LOGS persist. Open failure → warn + memRing fallback.
Globals.StatsDiskLimitMB (0=unlimited, intOptAlways round-trip); Settings shows
it when backend=sqlite. Size: +1.2MB UPX (10.5->11.7MB), static/musl OK.
Verified: build (router tags)/vet 0, go test + -race ok (sqliteRing cursor
parity, retention, disk-cap, PERSIST-across-reopen, async no-loss, memory
regression); panel tsc/build clean. VM: backend=sqlite → /etc/shater/stats.db
created, 75 conns logged, **survive daemon restart** (75 rows intact, seq
resumes 76->79), disk cap set 16MB. box.New applies.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLthkP2S8WAfxu7fcYbPfE
Merges 14 upstream commits including L3-forwarding support (which bumped
wireguard-go v0.0.3->v0.0.5, already re-grafted in the prior commit),
snell protocol, bridge outbound, flow-tracking/sniff improvements, and
DNS/dialer fixes.
lx conflict resolutions:
- protocol/wireguard/endpoint.go: took upstream's new flow API
(PreMatchFlow/PortAddresses/PortMTU/AttachReturn/DetachReturn/JudgeFlow),
dropped our old PrepareConnection/NewDirectRouteConnection. SPEC 020
idle-suspend wake guard (resumeOnDial) moved to WritePackets — the single
point every L3-forwarded packet transits, incl. established flows that
bypass DialContext.
- adapter/outbound.go: kept lx IdleSuspendable/ReachabilityInvalidator,
restored 'time' import dropped by auto-merge.
- go.mod/go.sum + test/: took upstream dependency bumps (tailscale, sing,
sing-tun); wireguard-go stays v0.0.5 with local submodule replace.
Green: full sing-box CLI with LX_TAGS (Go 1.24.7), libbox, wireguard/
adapter/dns/daemon packages, transport+protocol/wireguard tests, AWG
config validation.