matrix.service lists space-separated file names ("sower.service
sowerd.service"), but the .github/ prefix only applied to the first,
so every Linux job failed with 'cannot stat sowerd.service' in
Prepare packaging files. Broken since 1e44871; first exercised by
the v1.6.0 tag. Loop over the list and prefix each file individually.
Review of the anti-probing hardening found no code defects; the fixes
are documentation:
- docs/security.md: authoritative threat model, the three camouflage
layers, unknown-SNI certificate fallback rules (whitelist source,
error surfacing, capability propagation, concrete first entry), the
client_hello fingerprint knob, rejected directions (QUIC, h2) with
evidence, and residual accepted signals.
- Promote the plan doc to a non-authoritative historical record per
AGENTS.md docs rules; index security.md from docs/README.md and
ARCHITECTURE.md.
- config/sowerd.toml: document cert.domains; the first entry answers
unknown-SNI handshakes.
A handshake failure on unknown SNI is an active-probing signal: real
sites present their default vhost certificate. Whitelisted names keep
the plain autocert path with errors surfaced as-is; unknown or empty
SNI falls back to the primary domain's certificate, carrying the
client hello over so autocert derives the same ECDSA/RSA cache key.
The h2-on-fallback companion idea was investigated and rejected:
httputil.ReverseProxy has no RFC 8441 support and x/net gates
extended CONNECT behind GODEBUG only, which would break WingGate WSS.
The default ReverseProxy error handler logs a bare
"http: proxy error: <err>": with site routes and path overrides the
upstream address alone does not identify which route failed, so a
refused upstream could not be attributed to a route without guessing.
Report upstream, request method/host/path and the peer address, and
answer 502 with a body instead of an empty response. The handler is also
reachable before any byte is written (round-trip and upgrade failures),
so the response semantics are unchanged.
Unmatched HTTP/1.x browsing is reverse-proxied to fake_site instead of
TCP-spliced, preserving Host. Site routes and this path overwrite
X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Proto, and X-Real-IP from
the TLS peer and drop client-supplied forwarding headers. Non-HTTP
fallback, including sower auth failure, still relays the original bytes.
Rewrite dropdown-menu, label, and utils as native Svelte 5 components
with a shared context store; remove now-unused separator and tabs;
drop the bits-ui dependency.
- Config override validation now accepts bracketed IPv6 remote addresses
([2001:db8::1]:8443): the host part is checked as an IP literal before
falling back to hostname rules, so a valid IPv6 remote no longer fails
validation.
- Directory fake sites serve local relays even when serve_ip is a
concrete non-loopback address: the local check now recognizes any
address bound to a local interface, not just loopback, so fallback
traffic is not wrongly 307-redirected to https.
- The config view's restart handler ignores failures after the component
is destroyed, so a late 401 from the in-flight restart request can no
longer trigger a global logout from a dead page.
- The login throttle bounds its failure map (purges expired entries when
full) so rotating source IPs cannot grow it without bound.
- The post-restart polling loop in the config view is cancelled by an
AbortController on component destroy, so navigating away no longer
leaves background probes running or lets a stale 401 from a dead page
trigger a global logout.
- The traffic and overview views show the disconnect alert even before
the first SSE payload arrives, instead of a permanent spinner with no
hint when the stream never connects.
- ConfigChanges declares dns_reverse so the frontend contract matches the
backend whitelist.
- The rule-miss list guards against out-of-order responses; the rules
view clears its timers on destroy; the chart a11y warnings are silenced
with an explanation.
- The sower frame checksum now covers the command and port in addition to
the target, using HMAC-SHA256 (length-extension resistant) so a
man-in-the-middle on a plaintext link cannot retarget a captured frame
to an arbitrary port.
- Unwrap rejects empty or control-character target hosts.
- The SOCKS5 server writes the RFC 1928 METHOD=0xFF failure response
instead of leaving the peer hanging on an unusable negotiation.
- DHCP discovery validates the reply (matching xid, BOOTREPLY, message
type OFFER) and skips unrelated packets instead of trusting the first
datagram, closing a LAN-side DNS-hijack vector; the socket also sets
SO_BROADCAST explicitly (Go 1.26 removed UDPConn.SetBroadcast).
- The uTLS handshake gets a bounded default deadline when the dialer has
no timeout, so it cannot hang open forever.
- The suffix tree trims every trailing dot so dirty rule files cannot
build unreachable empty-label subtrees.
- The installer writes /etc/sower/sowerd.toml with 0600 instead of 0644:
the file holds the plaintext client-auth password.
- Binary updates are atomic (temp file + rename) so an interrupted update
cannot leave a truncated sowerd at /usr/local/bin.
- ExecStart quotes both paths for systemd parsing.
- remote.password for a socks5 upstream is rejected at validation instead
of being silently ignored (the transport only negotiates no-auth).
- splitRemoteAddr accepts bare IPv6 literals (default port) and rejects
malformed multi-colon addresses like '::::' instead of passing them
through as hosts.
- autocert gets a HostPolicy: certificates are only issued for site_routes
domains plus the new cert.domains whitelist, so an arbitrary SNI can no
longer drive ACME issuance and exhaust the account's rate limits. Direct
connection domains (the sower client's remote addr) must be listed in
cert.domains.
- The TLS handshake now completes explicitly (60s budget) before the 1s
protocol probe, so the first connection to a new domain no longer loses
the race against synchronous ACME issuance inside GetCertificate.
- The transport header read after a probe match keeps a 5s deadline
instead of none: a connection that sends only the 0x80 probe byte can no
longer hold a goroutine and fd forever.
- Directory fake sites relay to the configured serve_ip:80 when it is
concrete, instead of always 127.0.0.1:80, which broke HTTPS fallback
when serve_ip did not cover loopback.
- 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.
- degradeUpstream now takes the caller's snapshot length: a concurrent
refresh that grows the live upstream list no longer lets the retry
index past the stale snapshot and panic the DNS handler.
- Internal reverse lookups take a dedicated forward path that never runs
the degrade state machine: the query goes only to the selected internal
upstream and surfaces SERVFAIL on failure instead of retrying onto a
public fallback, closing the TOCTOU leak in the PTR gate.
- Local DNS replies set the RecursionAvailable bit per RFC 1035.
- A failed TCP retry for a truncated UDP response keeps the truncated
response (with TC set) instead of dropping it for SERVFAIL, so
networks that block TCP/53 still get usable answers.
Review findings on the DNS attribution and hostname series:
- ClientIPOf only trusts EDNS Client Subnet addresses with a full-length
source prefix (32/128); a truncated prefix like dnsmasq --add-subnet=24
carries only the subnet base and is ignored, so attribution stays on the
real client instead of the /24 network address.
- The internal PTR gate now judges the currently selected upstream rather
than the whole pool, so a degraded mixed pool cannot forward internal
reverse lookups to a public fallback.
- Failed hostname lookups are cached for five minutes instead of the full
hour, so a resolver blip stops suppressing names quickly while keeping
retries rare.
- Duplicate IPs in one decoration batch resolve once instead of once per
entry.
- Traffic-view hostname decoration runs on a private copy of the cached
snapshot outside trafficMu, so a stalled resolver neither blocks other
readers nor races on the shared backing array.
- Local DNS replies (failures and empty answers) echo an OPT record when
the query carried EDNS0, per RFC 6891.
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.