Domain stats are now tracked per entry point (http/https/socks5/dns)
instead of one mixed counter, and /api/traffic accepts ?source= so each
can be inspected separately:
- Stats.record attributes events to a source; DNS queries land in the
dns slot with no bytes, proxy connections in their protocol slot.
- Snapshot(sort, source) filters domains to one source, reporting that
source's conns/bytes/lastSeen; source=all (default) aggregates.
- The traffic page breadcrumb gains a source switcher (all/http/https/
socks5/dns), the view reloads on switch even while paused, and the
DNS view hides the byte columns and labels the list as DNS queries.
Verified live: source=dns lists queried domains with query counts;
source=https lists proxied domains with bytes; all still aggregates.
Sessions were stored in memory with a fixed 24h TTL, so every process
restart (deploys, crashes) logged everyone out and an active session
could still lapse mid-browsing.
- Admin sessions now persist to admin.session_file (default
/etc/sower/sessions.json, 0600, atomic temp+rename writes). Sessions
are restored on startup, dropping already-expired tokens. IO failures
degrade to the in-memory store.
- Sliding expiry: a session used past the halfway point of its TTL is
refreshed instead of left to lapse, so active users stay logged in.
Persisting only on refresh bounds disk writes to a couple per session
per TTL.
- Login, logout, expiry purge, and refresh all sync the file.
Verified live: after systemctl restart, the pre-restart cookie still
authenticates.
The rules page froze because block rules (164k+) were all transferred
and rendered at once. The list API is now paginated and searchable:
- GET /api/rules?category=X&q=...&offset=N&limit=M returns a page plus
the total match count; q is a case-insensitive substring match
(alloc-free scan in RuleSet.ListFiltered), limit clamps to 1000,
default page 200.
- POST/DELETE /api/rules now return 204; the frontend refetches the
currently loaded range so totals stay accurate.
- Rules view: server-side search box with 300ms debounce, load-more
button appends pages, count badge shows loaded/total.
Measured on the live box: first page 0.5ms, 164k-rule scan ~22ms.
Traffic view now sorts domains three ways via ?sort= on /api/traffic:
- bytes (default): total traffic, high to low (the former fixed order)
- recent: most recently seen first
- conns: most connections first
Invalid or missing sort falls back to bytes; the top-100 cap is kept.
Frontend gains a segmented sort switcher next to the filter; the footer
label follows the active mode. api.ts picks up the sort query param and
is committed with the user's prettier reformat (pure formatting) since
the change is entangled with it.
Review fixes for the monitoring overhaul:
- rangeNote used $derived(() => ...) which evaluates to the function
itself, rendering the function source in the footer; switch to
$derived.by so it evaluates to the time-range string.
- Restore the rule-count and cumulative-connection totals that the
Overview rewrite dropped: 规则命中 card now shows configured rule
counts, 活跃连接 card shows cumulative per-protocol connections.
- formatUptime: show bare seconds under one minute instead of '0m 13s'.
- Reformat format.ts to the project's prettier style (tabs).
Monitoring overhaul for the admin console:
- Add internal/metrics: OpenTelemetry meter with Prometheus exporter
(counters for bytes/dns/connections/rule hits, updown counter for
active connections, histogram for connection duration, process
gauges). Served unauthenticated at GET /metrics; an OTLP exporter
can be added later without touching call sites.
- Fix uptime: TrafficSnapshot.Uptime was a time.Duration serialized
as nanoseconds while the frontend renders seconds; it is now int64
seconds. Stats is created before rule loading so uptime tracks the
process start.
- Extend admin.Stats: per-protocol active connections (idempotent
decrement on Close), 60s sliding-window rates, rule-hit counters,
process resources, and a bounded in-process history ring (1h at 5s
sampling, gap-clamped) served at GET /api/history.
- Router route observer: block/direct decisions in DialSmart, proxy
decisions in DialProxyOnly (covers HTTP/HTTPS paths), exactly once
per connection.
- Fix socks5 handler closing the raw conn instead of the wrapped one,
which would have leaked active-connection counts.
- Frontend: Overview gains rate cards, active connections, rule hits,
system stats, and four hand-rolled SVG area charts (traffic, DNS/
conns, active per protocol, rule hits) with hover readouts.
Port the "glass reading room" light theme from WingGate/web: warm
paper canvas (#eaebe5) with a soft light-falloff texture, warm ink
text, hairline warm borders, and teal (#0f766e) reserved for primary
actions, focus rings, and selection. Corner radius moves to 12px to
match. Token-only change; components consume the same semantic roles.
Replace the dark-only shadcn palette with the standard light neutral
tokens and declare color-scheme: light. All components consume tokens,
so no per-component changes are needed.
Note: main.ts still adds the dark class to <html>; it is visually
harmless now that :root carries light tokens, but should be removed
when its pending formatting changes settle.
- internal/admin: session-cookie auth server, rule CRUD API, bounded
per-domain traffic stats with pre-parse byte accounting
- cmd/sower: wrap proxied connections and DNS queries into stats, serve the
admin console on its own listener
- web/: Svelte 5 + Vite admin console with shadcn-svelte UI and an advanced
breadcrumb header (section/context dropdowns, favorites), embedded via
go:embed and committed to web/dist
- Makefile: sower build depends on the frontend build (bun); sowerd does not
- docs: ARCHITECTURE, README, and plan updated for the admin surface
Admin is disabled by default, binds to loopback, and requires a non-empty
password when enabled. Zero-value configs skip admin validation so existing
tests and example files keep loading.
RuleSet retains the raw rule list alongside the suffix tree so rules can be
listed, added and removed at runtime. Removal rebuilds the tree from the
retained list; startup batch loading keeps the old single-compaction flow.
Router.BlockRule/DirectRule/ProxyRule migrate to *RuleSet.
sumChecksum used binary.Uvarint over the digest, which stops at the
first byte with a clear continuation bit: ~50% of checksums collapsed
to 7 bits of entropy (~15 bits effective), making the auth check
brute-forceable. Take a fixed 64-bit window of a SHA-256 digest
instead. Wire layout is unchanged (checksum stays 8 bytes), and the
randomized field also removes the "high bytes all zero" fingerprint.
Allow HTTP upgrade requests (WebSocket/CONNECT) through the fallback
reverse proxy instead of rejecting them with 400. Once the connection
is hijacked it is owned by the ReverseProxy upgrade goroutine, so
handleConn skips closing it; the single-conn listener is closed to
unblock Serve while the tunnel stays alive.
Previously the probe read deadline (1s absolute time set at handleConn
entry) remained active while handler.Unwrap ran its io.ReadFull on the
protocol header. A slow client could let Unwrap be interrupted by the
remaining sliver of the probe timeout, causing spurious auth/decode
failures and fallback to the fake site. Clear the deadline once at the
top of the probeMatch case, before Unwrap, so the protocol handshake
gets its own time budget consistent with the subsequent relay path.
Trojan transport, config options, and tests are removed entirely.
The sowerd probe abstraction now handles only the sower protocol;
adding new transports requires only a new proxyProtocolHandler impl.
- transport/trojan/: deleted
- cmd/sower/proxy.go: remove trojan import and branch
- config/sower.go,sowerd.go: remove trojan from type options and validate
- transport/transport_test.go: remove trojan test cases
Introduce proxyProtocolHandler interface with Probe/Unwrap methods,
extracting protocol detection from handleConn into a clean pipeline:
probe first bytes → match handler → Unwrap, or fallback by SNI.
- probe.go: probeVerdict enum, proxyProtocolHandler interface,
sowerProtocolHandler, readProtocolProbe
- handleConn now iterates handlers list, falls through to TLS SNI
routing on probeNoMatch, and to fake_site on auth failure
- serve443 signature changed: trojan param replaced by []proxyProtocolHandler
Upgrade the golang.org/x/* family to their current latest:
- crypto v0.49.0 -> v0.53.0 (only direct dep with an upgrade)
- net, sys, text, mod, sync, tools pulled forward as transitive deps.
No code changes: the sole x/crypto usage (autocert in cmd/sowerd) is
stable across these versions.
- Add DialSmart (rule+detect+fallback) and DialProxyOnly (forced proxy)
as explicit routing entry points; keep Dial as compatibility alias.
- NewRouter now returns (*Router, error); non-empty invalid MMDB path
is a startup error instead of a silent warning.
- AddCountryCIDRs now returns error; invalid CIDR entries fail fast.
- Add Router.Close() to release geoip2.Reader on shutdown.
- Add country RWMutex so Close() does not race with concurrent reads.
- localSite checks all resolved IPs instead of only the first.
- Move accessCache from package-level global to Router instance field;
inject accessProbeFunc for testability.
- Disable redirect following in pingClient to avoid misleading probe
results on redirect responses.
Document that DNS routing intentionally does not mirror smart TCP
routing, transparent HTTP/HTTPS listeners are proxy-only second-stage
handlers, and SOCKS5/explicit HTTP proxy entries use smart routing.