423 Commits
Author SHA1 Message Date
wweir f37e5fa758 feat(install): add a one-shot installer for systemd, procd and launchd
Installing sower meant grabbing a tarball and wiring the service up by hand,
and the packaged unit was too fragile for something a resolver depends on
(on-failure restart after 1min, no memory limit).

Add install.sh: detects the platform and init system, installs or upgrades
the binary atomically, keeps an existing config, verifies the release
SHA256SUMS unless explicitly opted out, and registers the service on systemd,
OpenWrt procd or macOS launchd. Every network call carries an explicit
timeout, the OpenWrt init script ships in Linux tarballs, the release
workflow generates SHA256SUMS, the README documents the installer, and
make test syntax-checks the scripts.
v1.6.1
2026-09-18 22:53:55 +08:00
wweir d0031e4106 fix(sower): keep the soft heap limit above the live rule heap
The default 128 MiB soft memory limit sits below the ~105 MB live heap the
full pexcn rule sets build, so the GC goal gets pinned to the live heap and
every allocation triggers a full-heap mark: measured 39% CPU on an otherwise
idle gateway with a GC cycle every 300-500 ms.

Raise the default to 512 MiB, resolve the limit in a testable helper that
covers GOMEMLIMIT/override/disable precedence, and declare the same value in
the packaged service plus the example configs so a deployment can tune it
without a rebuild.
2026-09-18 22:37:03 +08:00
wweir 44b7eefe91 ci(release): copy every matrix service file from .github
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.
v1.6.0
2026-09-18 18:16:00 +08:00
wweir 5dc57bc234 docs: security model for the sowerd edge; document the unknown-SNI cert fallback
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.
2026-09-18 18:05:02 +08:00
wweir b2b344e948 feat(sowerd): answer TLS handshake for non-whitelisted SNI
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.
2026-09-18 17:38:17 +08:00
wweir 5c15453dc6 fix(sowerd): log the routing context of proxy upstream failures
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.
2026-09-16 15:36:03 +08:00
wweir c4129c06ef docs: describe fake-site HTTP reverse proxy and forwarding headers
Correct the sowerd data flow and the stale claim that fallback rejects
HTTP upgrades. README notes X-Forwarded-* on unmatched browsing.
2026-08-28 13:37:14 +08:00
wweir 8dad77b778 feat(sowerd): reverse-proxy HTTP fake-site fallback with edge headers
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.
2026-08-28 13:37:05 +08:00
wweir bfad951fb0 docs: reflect hand-maintained shadcn-style UI without bits-ui 2026-08-20 18:15:23 +08:00
wweir f0cfd6941d refactor(web): hand-maintain shadcn-style UI components, drop bits-ui
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.
2026-08-20 18:15:19 +08:00
wweir 879c83f0fa feat(sowerd): support path-prefix routing within site routes 2026-08-13 08:44:38 +08:00
wweir b16325b413 fix: correct three regressions from the hardening pass
- 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.
2026-08-12 17:43:24 +08:00
wweir f3894ef110 docs: reflect cert whitelist, probe handshake, frame checksum, DHCP validation 2026-08-12 17:34:37 +08:00
wweir f89ca0425a fix(web): cancel restart polling on destroy, surface dead SSE, harden rules races
- 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.
2026-08-12 17:31:35 +08:00
wweir 3ea411e5fd fix(transport,pkg): authenticate full sower frame, validate DHCP offers, harden handshakes
- 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.
2026-08-12 17:26:26 +08:00
wweir 3286851fdf fix(install,config): lock down installed config, atomic binary copy, reject socks5 password, parse bare ipv6
- 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.
2026-08-12 17:23:14 +08:00
wweir 0ad94ba31c fix(sowerd): whitelist autocert issuance, bound probe handshake, fix fake site target
- 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.
2026-08-12 17:19:31 +08:00
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 30e80ab510 fix(router): bound degrade retry to snapshot, gate internal PTR on selected upstream
- 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.
2026-08-12 17:08:32 +08:00
wweir 1e64542adb style: gofmt fixes for legacy build tag and test alignment 2026-08-12 16:19:48 +08:00
wweir a0f627621b fix(router,admin): trust full-length ECS only, gate PTR on selected upstream, harden hostname cache
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.
2026-08-12 14:53:13 +08:00
wweir 203729299e docs(config): document dns.reverse in example config, clarify totals test 2026-08-11 23:35:15 +08:00
wweir 0f7e395ab3 fix(admin): decorate SSE totals hostnames, resolve concurrently, expose dns.reverse override 2026-08-11 22:19:57 +08:00
wweir dda120c8ca feat(admin): show client hostnames via reverse DNS in traffic console 2026-08-11 22:02:57 +08:00
wweir 2e839fe9f3 test(router): cover ECS stripping on the TCP exchange path 2026-08-11 21:46:54 +08:00
wweir 5a57267512 feat(router): attribute DNS stats via EDNS client subnet, strip ECS upstream 2026-08-11 21:34:11 +08:00
wweir 83675fe607 fix(router): reject multi-char ip6.arpa nibbles in reverse-name parse
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.
2026-08-11 21:06:18 +08:00
wweir b4d92703ce feat(router): answer internal PTR lookups locally, keep external DNS unexposed
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.
2026-08-11 21:02:41 +08:00
wweir 683b1da33f fix(admin): decouple rule feedback from list reload, unify rule styling
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
2026-08-11 19:10:36 +08:00
wweir f3b59f7444 fix(admin): show concrete success feedback after rule mutations
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.
2026-08-11 18:57:33 +08:00
wweir 1e4487193c ci(release): build per-arch matrix with aggregated release job
- 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
2026-08-11 14:32:45 +08:00
wweir c86937820a fix(admin): bound error alert to recent window, keep UTF-8-safe details
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.
2026-08-11 10:29:10 +08:00
wweir cfddf76eb3 feat(admin): surface proxy-side errors and stale data in overview
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.
2026-08-11 09:24:53 +08:00
wweir 0bd6e433a3 fix(admin): bound SSE writes and reset page filters on leave
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.
2026-08-11 08:35:14 +08:00
wweir 548a45559f chore(deps): deferlog v2.1.5, admin password b64 prefix
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.
2026-08-11 08:20:44 +08:00
wweir e526a73469 fix(sower): redact remote password from config load-failure log
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.
2026-08-11 08:07:34 +08:00
wweir d983885739 fix(admin): harden chart pin and screen-reader announcements
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.
2026-08-11 08:07:26 +08:00
wweir 249c3ae63f fix(config): send remote password verbatim to upstream proxy
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.
2026-08-11 00:17:47 +08:00
wweir 961c7e2e72 fix(admin): show live current values in chart legends
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.
2026-08-11 00:17:42 +08:00
wweir 1dc5f50818 fix(ci): build web assets before running go vet and test
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.
2026-08-10 16:35:06 +08:00
wweir 53d4271813 chore: stop tracking web dist build artifacts
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.
2026-08-10 16:29:31 +08:00
wweir 112bd84bc2 chore: ignore cross-compiled binaries 2026-08-10 16:18:12 +08:00
wweir 033a070db6 feat(admin): temporary password fallback with login hint and password masking 2026-08-10 16:17:57 +08:00
wweir 52f9205e6a refactor(config): drop YAML support, keep TOML only 2026-08-10 16:17:11 +08:00
wweir 6ec5d44d7c fix(sower): handle CONNECT in http proxy handler
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.
2026-08-10 12:12:32 +08:00
wweir 265f7e14ba docs: detail admin config console whitelist and in-place restart 2026-08-09 23:26:37 +08:00
wweir 4c3d4e399e ci: checkout before setup-go to enable module cache 2026-08-09 21:34:08 +08:00
wweir b77925d22e chore(deps): update go, web and CI dependencies 2026-08-09 21:31:15 +08:00
wweir ffc631adaa fix(ci): install bun before building web assets 2026-08-09 21:19:07 +08:00
wweir 9ef545b189 docs: complete admin configuration and plan status updates
README gains an admin configuration table (state_file, session_file,
disable_session_persistence, cookie_secure); sower.toml documents
cookie_secure; ARCHITECTURE.md records best-effort session persistence;
both admin plans note the evolved whitelist, restart, stats, and SSE
implementation.
2026-08-09 21:18:40 +08:00