33 Commits
Author SHA1 Message Date
wweir d15ba70100 feat(sowerd): let a site route present the client's authority to its upstream
A site route always rewrote the outbound Host to the upstream host, so an
upstream that validates the authority it is visited under could not sit behind
one: pi-web compares Origin's host against the Host header, answers 403 to
every browser-originated /api request, and the page loads while its WebSocket
never connects.

Add `authority = "upstream" | "client"` to a site route, defaulting to
"upstream" so existing routes keep the current behavior byte for byte.
"client" forwards the authority the client asked for, which is what such an
upstream needs; its own allowlist must still name the public domain (pi-web's
PI_WEB_ALLOWED_HOSTS), because the forwarded name is client-controlled.

The key is one word on purpose: aconfig v0.19.0 title-cases the keys of an
array-of-tables element, so `preserve_host` is rejected at load time — probed
before the spelling was chosen.

- config: SiteRoute.Authority, the AuthorityUpstream/AuthorityClient values,
  and SiteRoute.PreserveHost(). Validate rejects an unknown value instead of
  silently defaulting, since the two values differ in what the upstream sees.
- cmd/sowerd: newSiteRouter feeds that flag into the siteEntry field the
  fake-site fallback already used.
- docs: ARCHITECTURE.md and security.md record the rule, why the adaptation
  belongs to the edge rather than to an upstream trusting X-Forwarded-Host,
  and what preserving a client-controlled authority does not protect.
- tests: TOML round-trip, unknown-value rejection, router plumbing, and an
  end-to-end handleConn case asserting the upstream sees the client's name.
2026-10-12 14:35:41 +08:00
wweir 2894a15510 docs(security): state what the transport password does and does not gate
The threat model scoped the adversary as "does not know the sower
password" but never said what holding it grants, which is the question an
operator asks before pointing a public domain at an intranet service:

- one shared secret, no per-client identity and no revocation short of
  rotating it for everyone (transport/sower's Sower holds only the password);
- a passing frame reaches any target the host can dial — Sower.Unwrap
  rejects only empty hosts and control characters, so there is no allowlist
  and a leaked password exposes the whole network position;
- no attempt throttling on the transport path (a wrong checksum just falls
  back to the website path); only the site_routes gate is throttled;
- password handling is the operator's: validation rejects only an empty
  value, the HMAC key is the raw password, and the shipped default config
  still carries the change_me placeholder (now flagged in Residual Accepted
  Signals and marked in the example config as requiring replacement).

ARCHITECTURE.md step 11 points at the authority for the no-allowlist fact.
2026-10-12 10:35:33 +08:00
wweir 8a3670d50e feat(sowerd): add an optional HTTP Basic gate for site routes
Site-route domains become public once autocert issues for them
(Certificate Transparency), and the edge answers any SNI. An upstream that
cannot authenticate its own visitors is therefore exposed, and sowerd owns
80/443 on the host, so nothing can be placed in front of it.

Add an opt-in per-route visitor gate:

- config: site_routes[].auth {username, password}; both fields required,
  empty means ungated, ':' rejected in the username (RFC 7617 splits there).
- runtime: the gate runs in the reverse-proxy handler, so it covers path
  overrides and WebSocket upgrades too; the sower transport path is
  dispatched earlier and stays untouched. Credentials are compared in
  constant time, failed attempts accumulate per source IP and lock it out for
  15 minutes (bounded map, mirroring the admin login throttle), and a passing
  request has its Authorization header removed so the edge credential never
  reaches the upstream. The fake-site decoy is deliberately never gated.
- fix: upstream URL userinfo was silently dropped. ReverseProxy sends
  through a Transport, and only net/http.Client turns userinfo into Basic
  credentials, so sowerd now sets that header itself; the upstream password
  is redacted in proxy error logs (url.URL.Redacted).
- docs: security.md records the mechanism and its limits (not a substitute
  for upstream auth, no session/logout, CORS preflight blind spot, and the
  browser WebSocket handshake: script cannot set Authorization, cached-
  credential replay is browser-specific and unverified, and a reconnecting
  client's failed handshakes feed the same per-IP counter, so it can lock its
  own source IP out of the whole route) plus the rejected alternatives
  (session login, gating the fake site).

Gate: gofmt, go vet ./..., go test ./... green; install_e2e_test PASS.
2026-10-10 19:15:32 +08:00
wweir 2988505770 fix(config): keep the checked-in sowerd example in sync with the embedded one
config/sowerd.toml and ExampleSowerdConfigTOML are the same artifact kept in
two places: the constant is what `sowerd -i` writes to /etc/sower/sowerd.toml
(internal/install/service.go), the file is what `-c config/sowerd.toml` loads
during development. They had drifted in both directions -- the constant
carried the [site_routes.routes] path-prefix example and the older cert.domains
wording, while the file carried the newer wording and left `domains`
uncommented -- so a deployment got guidance contradicting the shipped binary
depending on which copy it read.

Take the constant as the source of truth, comment out the placeholder
cert.domains entry (a placeholder domain is not one the operator owns) and pin
the two copies byte for byte with a test that prints the differing lines.
2026-10-10 14:56:49 +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 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 879c83f0fa feat(sowerd): support path-prefix routing within site routes 2026-08-13 08:44:38 +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 203729299e docs(config): document dns.reverse in example config, clarify totals test 2026-08-11 23:35:15 +08:00
wweir dda120c8ca feat(admin): show client hostnames via reverse DNS in traffic console 2026-08-11 22:02:57 +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 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 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 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
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 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 1981c61f0c feat(admin): share port 80 with http proxy via host-based dispatch 2026-08-06 13:55:55 +08:00
wweir 1daeb36d54 feat(config): add [admin] section with validation and examples
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.
2026-08-06 08:56:27 +08:00
wweir 62ca487087 docs: update docs and config examples for trojan removal
- ARCHITECTURE.md: remove trojan transport references, update sowerd flow
- README.md: remove trojan usage instructions, simplify remote type docs
- docs/plans/site-routing.md: update to reflect sower-only probe model
- config/*.toml,*.yaml: remove trojan from example config type options
2026-06-25 09:20:37 +08:00
wweir 474749d865 test(sowerd): add fallback, auth-failure and SNI routing tests
- TestHandleConnRoutesFallbackBySNI: verifies TLS SNI → site route
- TestHandleConnFallsBackToFakeSiteWhenSNIMisses: no-route SNI → fake_site
- TestHandleConnFallsBackAfterSowerAuthFailure: wrong password → fallback
- Add startHTTP1TCPServer and startRawTCPServer test helpers
- Fix config test: trojan → sower for TLS client hello validation
2026-06-25 09:20:34 +08:00
wweir e0874548b7 refactor: remove trojan protocol support
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
2026-06-25 09:20:31 +08:00
wweir 0547502748 feat(sowerd): route fallback sites by SNI 2026-06-24 09:24:25 +08:00
wweir 05f0728aa6 feat(sower): harden client rule loading 2026-04-25 03:09:45 +08:00
wweir 23110fc786 docs: clarify config key mapping comments 2026-03-31 10:14:15 +08:00
wweir 71eede9460 fix: align example config with snake case loader 2026-03-31 07:46:32 +08:00
wweir 6ccdcc131b feat: add sowerd install and startup validation 2026-03-30 23:02:54 +08:00
wweir 2b072584e6 feat: add upstream tls controls for sower 2026-03-30 23:02:38 +08:00
wweir af61d38b9e refactor: extract configuration to dedicated config package 2025-10-27 11:28:42 +08:00
wweir f7a288af4c refactor: extract configuration to dedicated config package
- Move configuration structures from cmd/sower/main.go and cmd/sowerd/main.go to config package
- Add config.SowerConfig and config.SowerdConfig types
- Implement Validate() method for configuration validation
- Update Go module dependencies to latest versions
- Improve code organization and maintainability
2025-09-07 00:10:56 +08:00