A label like "ff" slipped through ParseUint and silently truncated
to a single nibble. Each ip6.arpa label must be exactly one hex
digit. Also cover IPv6 upstreams in the internal-upstream tests and
add direct isInternalHost cases.
Parse in-addr.arpa/ip6.arpa query names back to IPs and answer
NXDOMAIN locally when the target is an internal range (RFC1918,
CGNAT 100.64/10, link-local, loopback, IPv6 ULA) and no internal
DNS server is among the upstreams. Reverse lookups still forward
when an internal DNS server can answer them, and public reverse
names keep the existing forwarding path.
Success feedback no longer waits for the list reload: on huge rule sets a
slow or failing reload delayed the toast or swallowed it entirely, leaving
the user with a misleading refresh error while the mutation had succeeded.
- add/undo/reset: flash the toast right after the API call succeeds
- remove: open the undo window and release the busy lock immediately, so
the undo button is not disabled during the reload
- success toasts render the rule part in monospace, matching the table,
delta panel, and delete toast styling
Replace the generic "已保存" flash with operation-specific messages:
- add: 已添加规则 <rule>
- undo: 已恢复规则 <rule>
- reset: 已重置<分类>规则
- delete toast unified to 已删除规则 <rule>,已保存
Messages are truncated to one line and announced via role=status;
the saved timer is cleared on category switch so stale flashes cannot
overwrite newer ones.
- replace hand-rolled make build loop with a strategy.matrix across 11
targets: linux (amd64, arm64, armv7, riscv64, loong64, mips, mipsle),
darwin (amd64, arm64), windows (amd64, arm64)
- add dedicated web job so the admin console is built once and reused
- aggregate all tarballs in a single release job to avoid concurrent
release edits from matrix jobs
- drop stale sower.yaml references (config is TOML-only) and the
bun-less make build that would fail on runners
Review fixes for the error visibility feature: the destructive alert now
only shows while a failure occurred within the last 10 minutes, so a past
outage does not keep a red banner up forever (cumulative counts remain as
context). Error details are truncated on a UTF-8 rune boundary and have
line breaks collapsed, so an IDN host cannot produce invalid UTF-8 in the
SSE payload and a multiline error cannot break the alert layout. Adds
tests for both.
Proxy dial/DNS/accept failures were logged but invisible to the console:
an unreachable upstream looked identical to no traffic. Stats now count
failures by kind, keep a bounded error event ring, and expose both in the
traffic snapshot (and Prometheus via sower.proxy_errors). The overview
shows a destructive alert with recent events when failures occur, and a
distinct stale-data alert when the SSE stream stays open but traffic
ticks stop parsing (three consecutive failures). Pins the persist-failure
-> 500 contract with a handler test.
The stream handler wrote without a deadline: a client that vanished
without closing the TCP connection (asleep laptop, dropped WiFi) could
block the handler forever once the kernel send buffer filled, leaking
the goroutine and connection on a 24x7 router. Each write now carries a
30s deadline via http.ResponseController and the stream is torn down on
the first failed write; the new failingFlusher test pins the exit path.
Also drop the traffic page's sort/source/client filters from the shared
SSE stream when the page unmounts, so the overview is never served a
hidden page's filtered snapshot, and stop toggling the disconnected
alert during the planned session-renewal reconnect.
deferlog v2.1.5 removes the base64-or-plain heuristic from Password input:
values are verbatim unless explicitly prefixed with "b64:", and marshaling
emits the same tagged form for exact round-trips. The admin password keeps
deferlog.Password and now takes "b64:"-prefixed encoded values; plaintext
passwords (even ones that are valid canonical base64) are never decoded.
Update the tagged-base64 test and document the prefix in README.
Review follow-up: the load-failure path logged the whole partially-populated
SowerConfig, which re-exposed the plain Remote.Password string after the
verbatim-password fix. Extract loadConfig/logConfigLoadError so the error
log carries only the error and remote_addr, matching sowerd. Add tests
pinning the redaction, JSON-handler redaction for the admin password, and
verbatim loading of sowerd's password. Document the no-base64 semantics of
remote/sowerd passwords and the admin password base64 caveat in config
comments and README.
Review follow-up on the live-value chart work: touch scrolling from the
chart area used to pin a stray crosshair (pointerdown committed the pin; a
scroll gesture ends in pointercancel), so the pin now commits on pointerup
with an 8px tap threshold and pointercancel rolls back the transient hover.
Escape cancels any pending frame before clearing. The sr-only status region
now lives permanently in the DOM and announces only when this chart is the
interaction source (chartLink.owner), so linked crosshairs no longer produce
an announcement burst from every chart. A stale pin whose timestamp slides
out of the history window is released automatically, and the legend omits
values below two samples instead of fabricating a zero.
deferlog.Password decodes canonical base64 input on unmarshal, so a password
that happens to be valid base64 (e.g. "fnhkwfnh") was silently corrupted
before reaching sower.Wrap. Auth then failed and the upstream fell back to its
camouflage HTTP site, breaking rule-file loading at startup. Restore plain
string for Remote.Password and sowerd Password, keep deferlog.Password for the
admin console only, and mask the remote password in the startup log again via
deferlog.Secret.
AreaChart now always renders a value per series: the hovered sample while a
crosshair is active, otherwise the latest sample, so current levels are
readable without hovering (and on touch devices at all). Touch taps pin the
crosshair until a second tap or Escape. Add a visually-hidden role=status
region announcing the inspected point to screen readers, and bump 11px muted
axis/badge text to text-xs for contrast. Backfill the first rate-series sample
with the second rate so charts don't show a fake drop to zero at the left
edge.
The previous commit stopped tracking web/dist, so the parallel
go vet run no longer finds the embedded dist directory in CI.
Make the test target depend on the web target to guarantee the
frontend is built first.
Add /web/dist to .gitignore, untrack the committed vite build output,
and update the embed.go comment to reflect that dist is generated by
the Makefile web target instead of being committed.
Split host:port from the request line before dialing so the target
address stays valid in the sower protocol, reply 200 to CONNECT before
tunneling, and drop the buffered request head so only raw tunnel data
reaches the target. Previously CONNECT targets were dialed as
host:port:80, which the remote sowerd could not connect to.
ARCHITECTURE.md and README.md now describe editable config fields,
in-place restart, rule hit/miss stats, and session persistence; the
admin console plan is marked implemented.
Rules page gains hit/last-seen columns, sorting, and a rule-miss view;
Config page adds field filter, list editing, restart button and polling;
URL routing with deep links, favorite pages with sub-menu state, session
probe, and reduced-motion/accessibility refinements.
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.
MatchRule resolves the matching rule in tree depth (most specific wins)
instead of a linear scan, and the router gains hit/miss observers so
per-rule counters can be fed without coupling the router to admin.
aconfig parses an empty TOML array (rules = []) as a slice holding one
empty string, which the router then treated as a real rule. Empty rules
can never match a real domain; Add and Replace now drop them.
Add a third 拦截 view to the Traffic console listing domains blocked
since start, ordered by count, with a search filter and live SSE updates.
The view hides the sort and client-chip controls that only apply to
traffic domains.
Adapt RecordRoute call sites to the two-argument signature and assert
the blocked ranking in snapshots. Add a cap test proving batch eviction
of the blocked map.
Feed the domain through the router observer into Stats.RecordRoute so
block decisions are attributed per domain. The bounded blocked map
(LRU-evicted at 10000 entries) feeds TrafficSnapshot.Blocked, ordered
by count, and rides the existing SSE traffic stream unchanged.
- countingConn batches per-domain attribution (32KiB/500ms) so the relay
hot path pays two atomics per I/O instead of a global mutex; Close drains
- Precompute OTel byte attributes to drop per-I/O allocations
- suffixtree: indexMap only for nodes with >=8 children; Match stays
read-only for concurrent queries
- Soft Go memory limit (128MiB default, GOMEMLIMIT/SOWER_MEMORY_LIMIT_MB
override) cuts resident memory from ~240MB to ~130MB on the default
ad/china/gfw rule sets
- Split stats.go into snapshot/eviction/counting by responsibility
- 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
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
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
Adds lastClientIP to every domain row on the traffic page so operators
can see at a glance who last touched a domain. Under a client filter the
column shows the filter itself; otherwise it reports the client with the
most recent activity on the domain (approximation for source-filtered
views, since the per-client map has no source dimension).
The clients aggregate was built only from domains surviving the client
filter, so selecting a client chip collapsed every other chip's count to
the few shared domains (or made it vanish), and the chips row hid itself
entirely when the filter matched no domains - stranding the user with no
way to clear the filter.
- Snapshot now aggregates chips over the source-filtered view before the
client filter is applied; chips stay globally counted and stable.
- Traffic page keeps the toolbar and chips visible when the filtered
domain list is empty; the empty state only replaces the table.
- Regression test pins chips stability, including clients that share no
domain with the selected one.
- 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.
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.
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.