mirror of
https://github.com/wweir/sower.git
synced 2026-10-19 16:14:33 +00:00
v1.7.4 asked the system resolver and was wrong on a deployed client: this client's own DNS answers its serve address for every domain it proxies (that is how its clients are steered into it), so "the name resolves to one of my listeners" is true for every normally proxied domain. In the field that refused legitimate traffic (SOCKS5 google answered code 2 not-allowed). The recursion discriminator has to be the name's own record, i.e. what the relay will act on, so the guard now resolves through the configured upstreams (which already exclude this client's serve addresses) and caches the answer, including an empty one, for a minute. Verified against a fake upstream in tests: a name the upstream points at this client is refused, one it resolves remotely is proxied. docs/plans/2026-10-12-self-proxy-target-guard.md records the regression, the rollback and the deploy pitfall (cat > file leaves 0644, so cp -a installed a non-executable binary and procd could not start the service).
19 KiB
19 KiB
Architecture
System Boundary
sower is the client-side transparent proxy entrypoint. It exposes local DNS, HTTP, HTTPS, and SOCKS5 listeners, applies rule-based routing, and forwards proxied traffic to an upstream transport.
sowerd is the server-side TLS ingress. It exposes 80/tcp and 443/tcp, terminates TLS, detects the upstream transport protocol, and relays traffic to the requested target or configured fake site.
For non-transport fallback traffic, it can route by TLS SNI to per-domain upstream site URLs, then falls back to the configured fake_site; directory-backed fake_site is served by the loopback local fileserver.
Layer Responsibilities
cmd/sower
- Process bootstrap
- Config loading and validation
- Local listener lifecycle and graceful shutdown
- Rule file loading
- Upstream proxy dialing
cmd/sowerd
- Process bootstrap
- Interactive self-install mode dispatch
- Config loading and validation
- TLS and HTTP server lifecycle
- Graceful shutdown
config
- Runtime configuration schema
- Input validation rules
- Embedded default
sowerdinstall config template
internal/install
- Interactive systemd installation flow for
sowerd - Binary copy/update and service file generation
- Default config and fake site directory bootstrap
router
- Domain and CIDR rule matching
- Thread-safe
RuleSet(list/retain/rebuild) backing runtime rule management - Direct/proxy/block routing decisions
- DNS query handling and upstream selection
internal/admin
- Admin HTTP server: session-cookie auth, rule CRUD API, traffic stats API
- Traffic stats recorder: aggregate counters and bounded per-domain accounting
- Serves the embedded admin console frontend
web
- Svelte 5 + Vite (Tailwind v4 + hand-maintained shadcn-svelte-style UI, no bits-ui) admin console frontend
- Build output
web/distis embedded into the binary viago:embed
transport/sower
- Sower transport frame encode/decode
pkg/dhcp
- DHCP-based upstream DNS discovery for the client side; the received OFFER is validated (matching transaction id, BOOTREPLY opcode, message type OFFER) so unrelated or spoofed LAN packets cannot inject DNS servers.
sower Data Flow
- Load config from flags, env, and files.
- Validate remote type, listener addresses, and DNS IP fields.
- Log startup metadata with secrets redacted.
- Build the upstream proxy dialer with a stable DNS target. If
dns.upstreamis empty, the dialer usesdns.fallbackto avoid recursive lookup through the local DNS listener. - Build the upstream dialer for the configured remote transport, using standard TLS by default and optional uTLS fingerprints for
sower. - Build the router with suffix-tree rules and optional country CIDRs.
Remote rule files are fetched through the configured upstream proxy dialer, never by direct outbound HTTP, so rule bootstrap uses the same stable egress path as proxied traffic.
Remote domain rule files are filtered through per-router
file_skip_rulesbefore their prefixed entries are appended. - Start enabled local listeners for
udp/53,tcp/80,tcp/443, andtcp/1080only after rule loading completes. - For DNS requests, return local proxy IPs only for explicitly proxy-routed domains and query upstream DNS for direct or unknown domains.
DNS routing intentionally does not mirror smart TCP routing: DNS must support arbitrary protocols and ports, so unknown names stay conservative and are not mapped to local HTTP/HTTPS proxy listeners by default.
Empty
dns.upstreamkeeps router-side DHCP DNS discovery enabled;dns.fallbackis appended as a backup upstream and is also used while initial discovery is in flight. Proxy-routed domains return local A/AAAA records, suppress HTTPS/SVCB and other non-address metadata locally, and never leak proxy-matched names to direct upstream DNS. Direct upstream DNS failures fall back only for retryable upstream service errors; when no fallback succeeds, the last upstream DNS response code is returned as-is. Service discovery names are matched against both the full query name and the base domain only for service record types. Reverse lookups (PTR) for internal ranges (RFC1918, CGNAT 100.64/10, link-local, loopback, IPv6 ULA) are answered with NXDOMAIN locally unless the upstream that would serve the query is an internal DNS server. The gate judges the currently selected upstream rather than the whole pool, so internal layout never leaks to public DNS — including a degraded mixed pool that fell back to a public resolver — and internal reverse resolution still works while an internal DNS server is selected. Client attribution for DNS statistics prefers the EDNS Client Subnet (ECS) address when a forwarding resolver (e.g. dnsmasq --add-subnet) carries the real client in the query with a full-length source prefix (32/128; a truncated prefix such as --add-subnet=24 only identifies the subnet base and is ignored), falling back to the transport source address; ECS is stripped before any query is forwarded upstream so client subnets never reach public resolvers. ECS is trusted only from the local forwarding resolver — a direct client can spoof it, which affects console attribution only. Whendns.reverseis configured, the traffic console resolves client IPs to hostnames via PTR queries to that resolver (typically the local dnsmasq, which answers LAN leases and tailnet names and forwards everything else); results are cached for an hour, failed lookups for five minutes, and failures degrade to the raw IP. - For DNS-mode transparent HTTP traffic, parse the target host from the request line and always forward through the upstream proxy. For DNS-mode transparent HTTPS traffic, peek the TLS ClientHello to extract SNI and always forward through the upstream proxy.
These transparent
80/443listeners are second-stage proxy-only handlers for domains already mapped to local proxy IPs by DNS; they do not run smart routing again. HTTPS transparent proxying reads only the TLS ClientHello, then replays the untouched bytes to the selected upstream; it must not complete or terminate TLS locally. - For SOCKS5 traffic and explicit HTTP proxy traffic, read the client-supplied target host and port, apply smart routing rules, and either dial directly or wrap traffic in the configured upstream transport.
- Wrap every proxied client connection in the admin stats recorder before protocol parsing, attribute bytes to the discovered domain after parsing, and count DNS queries through a handler decorator. Admin rule mutations take effect immediately and persist as
add/removedeltas relative to the startup baseline; state write failures reject the mutation without changing the runtime rule set. - When
[admin]is enabled, serve the admin console: session-cookie auth for the API, persisted rule deltas, sanitized effective-config display, whitelisted config overrides (immediate forlog_leveland DNS upstreams, restart-mode for the rest), per-rule hit and rule-miss statistics, and an in-place process restart endpoint; secrets never leave the server. The Svelte frontend is served from the embeddedweb/dist. By default the admin server owns a dedicated listener; whenadmin.addrexactly matchesdns.serve:80, the admin console and the HTTP proxy share one listener and each connection is classified by its request head (origin-form with the listener IP as Host goes to admin; CONNECT, absolute-form, and other Hosts go to the proxy). - On shutdown signal, stop listeners and DNS servers through
contextpropagation.
sowerd Data Flow
- Load config from flags, env, and files.
- Validate required fields and address/certificate combinations.
- Initialize logger with configured level and redact secrets from startup logs.
- Start
:80HTTP server. - Handle ACME HTTP-01 challenge on
:80. - Redirect normal HTTP traffic to HTTPS.
- If
fakeSiteis a local directory, serve it only for loopback fallback traffic through127.0.0.1:80. - Start
:443TLS listener with HTTP/1.1 ALPN only. - Complete the TLS handshake explicitly (60s budget; first contact can drive synchronous ACME issuance) before applying the short probe read deadline.
- Probe the connection's first bytes to identify the
sowertransport. - If matched, authenticate (the frame checksum covers command, port, and target via HMAC-SHA256; empty or control-character targets are rejected) and relay traffic to the decoded target — any host the process can dial, since there is no target allowlist (see docs/security.md). The header read is bounded by its own deadline so a connection that sends only the probe byte cannot hold a goroutine and fd forever.
- If authentication fails or no transport matches, read the TLS SNI from the terminated TLS connection.
- If the SNI exactly matches a configured
site_routesdomain, apply that route's optional HTTP Basic visitor gate, then reverse-proxy the decrypted HTTP/1.1 request to that route'shttp://orhttps://upstream URL (URL userinfo credentials are sent to the upstream as Basic auth). A matched route always goes to the reverse proxy:site_routesserves HTTP/1.1 only. - If the SNI has no route and the decrypted probe is an HTTP/1.x request line, reverse-proxy to
fakeSitewhile preserving the original Host and setting edge forwarding headers. Otherwise replay the original bytes tofakeSitewith a TCP relay, including sower auth failures and other non-HTTP traffic.
sowerd Install Flow
- Detect install mode from CLI flags before normal config loading.
- Require root privileges because the installer writes to
/usr/local/bin,/etc/systemd/system, and/etc/sower. - Optionally copy or update the current binary to
/usr/local/bin/sowerd. - Write
/etc/systemd/system/sowerd.servicewithExecStart=/usr/local/bin/sowerd -c /etc/sower/sowerd.toml. - Ensure
/etc/sower/sowerd.tomlexists — a fresh one is written with a generated random client password, printed once — and bootstrap/var/wwwfor directory-backed fake site mode. An existing config is never rewritten. - Reload systemd and optionally enable/start or restart the service.
Design Decisions
- Client and server both fail fast on invalid startup configuration instead of silently degrading.
- The client and server load TOML configuration files only; YAML and HCL are not supported.
- Sensitive configuration values must never be printed verbatim in logs.
- Local listeners use explicit shutdown hooks instead of blocking forever with unmanaged goroutines.
- Network operations use timeouts and
contextto limit hangs during dialing and remote rule downloads. - The admin console persists rule changes as bounded deltas in
admin.state_file, without rewriting TOML or rule sources. It also exposes a sanitized effective-config view and persists whitelisted overrides:log_leveland the DNS upstreams apply immediately, every other whitelisted field (remote, listeners, rule sources) takes effect on the next restart. Clearing an override reverts the field to the file/flag configuration.--ignore-admin-stateis the startup escape hatch for a bad state file or override. - The admin server is disabled by default, binds to loopback by default, and requires a password when enabled; an empty password falls back to a startup-generated random one printed once in the log, and the login page tells the user where to find it. Login is rate-limited per remote IP (five failures lock for fifteen minutes). The frontend never stores the password (HttpOnly session cookie only).
- The admin restart endpoint replaces the process image in place (
execon Unix, same PID) so systemd stays unaware; on platforms without in-place restart the endpoint reports an error instead of acknowledging a restart that would fail. Sessions persist toadmin.session_file(atomic write, 0600) so a restart keeps the browser logged in;admin.disable_session_persistenceandadmin.cookie_securetune that behavior. Session persistence is best-effort: a disk write failure never blocks login or leaves a revoked session valid in memory — the in-memory state stays authoritative and the failure is logged. - Rule hit statistics (per matched rule, per category) and rule-miss statistics (per domain for connections that matched no rule) are tracked in bounded in-memory maps fed by router observers; rule mutations invalidate the hit domain cache so counts stay attributable to the current rule set.
- The admin console can share the DNS HTTP proxy listener on port 80 when
admin.addrequalsdns.serve:80; classification is Host-based (origin-form requests with the listener IP as Host are admin traffic), so proxying a target whose Host equals the listener IP is inherently ambiguous and routes to admin. The HTTPS and DNS listeners speak different protocols and cannot be shared. - Traffic monitoring reports proxied payload bytes and request/connection counters, not packet-level network accounting. Per-domain byte attribution is batched per connection (32 KiB threshold or 500 ms window) so the relay hot path pays two atomics per I/O instead of a global mutex; Close always drains the remainder.
- The process sets a soft Go memory limit (128 MiB by default; an explicit
GOMEMLIMITwins,SOWER_MEMORY_LIMIT_MBoverrides with a MiB value,0disables). The default ad/china/gfw rule lists build ~40 MiB of suffix trees and GOGC's 2x target would otherwise keep resident memory near 250 MiB on an idle gateway. - Upstream TLS behavior is configured only on the client side;
sowerdremains a normal TLS server and does not need uTLS-specific logic. - Site routes are public endpoints (autocert issues for their domains, and issued certificates are published in Certificate Transparency logs), so a route whose upstream cannot authenticate its own visitors can opt into an edge-side HTTP Basic gate (
site_routes.auth). The gate consumes the visitor'sAuthorizationheader — the edge credential is not the upstream's — and throttles accumulated failures per source IP. This is a perimeter gate for the route's HTTP surface, not an account system, a replacement for the upstream's own authentication, or a second factor; the mechanism and its rejected alternatives (session login, gating the fake-site decoy) are recorded indocs/security.md. - Rule loading supports local files and remote HTTP sources; remote downloads must use the configured upstream proxy and fail startup if the proxy path cannot fetch them.
- Domain rule files support per-router skip rules for filtering third-party file entries without removing explicit local rules.
- HTTP/HTTPS access probes are cached with an hour-long write TTL through
github.com/maypok86/otter/v2, keeping repeated smart-routing checks bounded without hiding later reachability changes indefinitely. - Country routing treats
router.country.mmdbas optional; an empty value disables GeoIP lookup and keeps CIDR-based matching active without startup warnings. A non-empty invalid MMDB path is a startup error. - DNS routing, transparent HTTP/HTTPS forwarding, and smart TCP routing are separate policies. DNS uses conservative explicit proxy rules only; transparent HTTP/HTTPS is proxy-only; SOCKS5 and explicit HTTP proxy traffic use smart routing.
- A proxy dial must never re-enter this client's own listeners: both dial entries (
router.DialSmartfor rule/probe-routed traffic androuter.DialProxyOnlyfor the tunnel) refuse a target that resolves to one of the addresses the client serves proxy traffic on — the HTTP and HTTPS proxy listeners on every DNS listen address, plus SOCKS5 (router.ErrSelfProxyTarget; HTTP answers403, SOCKS50x02not-allowed). Both are guarded becauseDialSmartcan reachdirectDialwithout passingDialProxyOnly, and either branch re-enters the proxy. Without the guard one request to a name whose A record is the client's own serve address becomes a self-sustaining chain between the client and its relay (2026-10-12: 601 connections on r6c, and the relay host was OOM-killed), or a local loop that never leaves the host when the target is classified direct. The target is resolved through the client's configured upstream resolvers — never the system resolver, because a deployed client answers its own DNS for every domain it proxies, which would make every proxied dial look like a self target — and the answer is cached for a minute so a proxied dial does not pay a DNS round trip per connection; a resolution failure is not treated as evidence of self-targeting. - Fake site directory mode is loopback-only on port
80to avoid exposing local static assets directly to the public internet. sowerdprefers the user cache directory for ACME state, but falls back to/var/cache/sowerso systemd services can start withoutHOME/XDG_CACHE_HOMEor a config file.sowerdfallback site routing is based only on exact TLS SNI matches; wildcard domains are not supported.sowerdsite routes use HTTP reverse proxying to support fullhttp://andhttps://upstream URLs. The reverse proxy rewrites the outbound Host to the upstream host; a route can setauthority = "client"to forward the client's authority instead, for upstreams that validate the authority themselves (see docs/security.md).sowerdis the TLS edge for fallback HTTP. Reverse-proxied site routes and HTTP fake-site fallback overwriteX-Forwarded-For,X-Forwarded-Host,X-Forwarded-Proto, andX-Real-IPfrom the immediate client; client-supplied forwarding headers are discarded.X-Forwarded-Protoishttpsbecause this path is the 443 listener after TLS termination.X-Forwarded-Hostalways carries the original authority. The HTTP fake-site fallback has always kept that authority in the Host header itself, matching the previous TCP relay; a site route keeps it too when the route setsauthority = "client".sowerdreverse-proxied fallback supports HTTP/1.1 protocol upgrades (including websocket) through the standard library ReverseProxy hijack path.sowerdsite routing applies bounded client header, upstream dial, TLS handshake, and upstream response-header timeouts.sowerdadvertises only HTTP/1.1 over TLS because fallback site routing and fake-site serving are HTTP/1.1 paths.- In autocert mode,
sowerdobtains certificates only for configured domains: everysite_routesdomain plus thecert.domainswhitelist (autocert HostPolicy), so an arbitrary SNI cannot drive ACME issuance and exhaust the account's rate limits. Direct-connection domains (e.g. the sower client's remote addr) must be listed incert.domains. In custom certificate mode, the configured certificate must cover every routed domain through SANs.
Operational Notes
sowerusually needs elevated privileges to bind53/udp,80/tcp, and443/tcp.sowerdmust bind privileged ports80and443.sowerd -ialso requires root because it writes system-level files for self-deployment.- ACME mode requires port
80to be reachable from the public internet. - Remote rule download failures stop startup after bounded retries before local listeners are exposed.
- In ACME mode the handshake is answered for arbitrary SNI with the first whitelist domain's certificate (unknown-SNI fallback); keep that entry concrete. See docs/security.md.
Related Documents
- README.md
- docs/README.md — documentation index
- docs/security.md — sowerd edge threat model and anti-probing rules