17 Commits
Author SHA1 Message Date
wweir 4c19815319 fix(sower,admin): keep CONNECT tunnel head, bound SOCKS5 handshake, harden admin config and login
- CONNECT through the HTTP proxy no longer drops the tunnel's first
  bytes: bytes the bufio reader already pulled past the request head
  (pipelined tunnel data) are replayed to the target before relaying.
  Both the dedicated HTTP proxy and the SOCKS5-port HTTP fallback paths.
- The SOCKS5 entry keeps its read deadline across the whole handshake
  (auth + request) instead of clearing it after the first byte, bounding
  slowloris clients that stall mid-negotiation.
- Config override validation now covers dns_reverse, remote_addr,
  remote_tls_server_name, and every rule list, so a malformed override
  can no longer be persisted and crash-loop the next startup.
- admin.state_file is read-only in the console: the state store loads at
  startup, so the runtime override could never take effect.
- Login is rate-limited per remote IP (5 failures lock 15 minutes) and
  the login endpoint carries the Origin guard; the session map refuses
  new logins instead of growing past its bound.
- The admin HTTP server sets IdleTimeout and MaxHeaderBytes.
2026-08-12 17:15:54 +08:00
wweir dda120c8ca feat(admin): show client hostnames via reverse DNS in traffic console 2026-08-11 22:02:57 +08:00
wweir 033a070db6 feat(admin): temporary password fallback with login hint and password masking 2026-08-10 16:17:57 +08:00
wweir 0f67081022 feat(admin): rule hit stats, config console, restart, and session persistence
Rule listings gain hit counts, last-seen timestamps, and sort by rule
text, hits, or recency; per-rule and rule-miss stats are tracked in
bounded maps fed by router observers. The config console exposes
whitelisted editable fields (immediate for log level and DNS upstreams,
restart-mode for the rest), persisted as pointer overrides. The restart
endpoint closes listeners and execs the process in place (same PID).
Session persistence is best-effort with configurable options, and admin
state/session config gained disable and cookie-secure switches.
2026-08-09 19:20:26 +08:00
wweir 94c7105187 feat(admin): persist rule changes and add config/traffic console
- StateStore persists rule deltas and whitelisted config overrides atomically
- Config display/edit API with optimistic concurrency (revision)
- Traffic charts with time-axis linking across all views
- Split server.go into auth/rules/traffic/config by responsibility
2026-08-07 12:02:15 +08:00
wweir 6023537319 feat(admin): test a domain's rule matches and route decision
The rules page can now answer 'what happens to this domain?': RuleSet
gains MatchRule, a linear scan that returns the first matching rule under
the same suffix semantics as the suffix tree, and GET /api/rules/test
(auth) reports per-category hits plus the route a connection would take
(block > direct > proxy priority; 'auto' with a note when no rule
matches, without performing live detection).

- Rules page gets a domain test input with route badge, per-category
  match rows, and the fallback note
- tests: MatchRule agrees with Match, fake + endpoint coverage
2026-08-06 22:42:31 +08:00
wweir 9b74f80cfe feat(admin): cumulative traffic totals for domains and clients
The domain map previously doubled as the live window and dropped entries
after staleDomainAge, so from-start totals were lost. It now accumulates
(bounded at maxTotalDomains with least-recent eviction) while Snapshot
only filters staleness, and a new independent client table (bounded at
maxTotalClients) keeps per-client totals exact even when domains are
evicted. record() takes explicit counter deltas so the cumulative layer
sees the same increments as the window.

- GET /api/totals (auth) returns top domains and clients by bytes since
  start; the SSE stream pushes a totals event initially and every 30s
- Traffic page gains a live/totals toggle; totals view keeps byte columns
  regardless of the source breadcrumb, and clicking a totals client chip
  jumps to the live view filtered by it
- per-domain client breakdown capped at maxClientsPerDomain
- tests: totals survive staleness and domain eviction, both caps enforced
2026-08-06 22:34:55 +08:00
wweir 54f6f61e78 fix(admin): review fixes for the SSE migration
- Session cookie MaxAge was 24h, matching sessionTTL, which defeated
  the sliding expiry: the server session could refresh past 24h but the
  browser cookie lapsed, logging the user out anyway. The cookie is only
  a bearer-token carrier, so its MaxAge is now 30 days and the
  server-side session map (sliding 24h) is the real expiry gate.
- Drop the always-empty statusError state from App.svelte; the SSE
  connection state is surfaced by the page-level reconnect banners.
2026-08-06 21:33:48 +08:00
wweir 64a1b55e99 feat(admin): replace polling with Server-Sent Events
The console previously polled /api/traffic, /api/status, and /api/history
every 5-10s. It now subscribes to one SSE stream:

- GET /api/stream (session-cookie auth) pushes status and the traffic
  snapshot every 5s and history every 10s, with an immediate initial
  payload. The connection carries the page's sort/source/client filters
  and revalidates the session on each tick, closing with an auth event
  when it lapses. X-Accel-Buffering is disabled for proxied setups.
- Frontend: lib/live.svelte.ts is a module-level reactive store fed by
  EventSource (auto-reconnects; safe-parse tolerates malformed frames;
  auth event routes to the logout flow). App owns the connection, pages
  consume the store, Traffic re-points the stream on filter changes.
  The pause button and the polling helper are gone.
- tsconfig gains allowImportingTsExtensions for the .svelte.ts store.

Verified: initial + 5s-tick events stream, 401 without session, filter
params apply to the pushed snapshots.
2026-08-06 20:11:10 +08:00
wweir fbc0d54893 feat(admin): track and filter traffic by client IP
The console now records the client source IP for every connection and
DNS query, shows the top clients, and can filter the domain list by one:

- WrapConn captures conn.RemoteAddr into the counting conn; RecordDNS
  takes the client IP from the DNS ResponseWriter. Domain stats keep a
  per-client breakdown alongside the per-source one.
- /api/traffic?client=<ip> restricts domains to that client and reports
  the client's conns/bytes/lastSeen; it composes with ?source=.
- The snapshot now carries clients: top 50 per-IP aggregates (conns,
  bytes up/down, last seen) for display.
- Traffic page shows a client chip row (全部客户端 + IPs with counts);
  clicking one filters, tooltips show byte totals.

Verified live: ?client=192.168.1.1 lists that device's domains with its
counts; ?source=dns&client=... composes correctly.
2026-08-06 19:54:43 +08:00
wweir 6d52cc14c3 feat(admin): monitor DNS queries and filter traffic by source
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.
2026-08-06 19:35:44 +08:00
wweir 76ab03df73 fix(admin): keep sessions alive across restarts with sliding expiry
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.
2026-08-06 19:13:35 +08:00
wweir e524592f61 feat(admin): paginate and search the rules list
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.
2026-08-06 19:00:09 +08:00
wweir cd5b857328 feat(admin): switchable domain sorting on the traffic page
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.
2026-08-06 18:51:28 +08:00
wweir b4cb204e3f feat(admin): OTEL metrics, real-time rates and in-process history
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.
2026-08-06 17:17:24 +08:00
wweir d4825deed2 fix(admin): harden console review findings 2026-08-06 10:30:47 +08:00
wweir 1e87298d15 feat(admin): embedded admin console for rule management and traffic stats
- 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
2026-08-06 08:56:33 +08:00