63 Commits
Author SHA1 Message Date
wweir 116d16b98b refactor(sower): guard the dial exits instead of the routing entry
Two findings from reviewing the self-target guard:

- the guard sat at the top of DialSmart, so a blocked target that also points
  at this client reported ErrSelfProxyTarget instead of ErrBlocked, and every
  name on a served port paid an upstream lookup before the rules were even
  consulted. It now sits in directDial, the only other dial exit, so blocked
  targets are decided without resolution and policy keeps precedence.
- selfProxyLookupTimeout was left behind by the switch to upstream
  resolution; drop it.
2026-10-13 06:58:40 +08:00
wweir 8db5288365 fix(sower): resolve the self-target check through the upstream resolvers
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).
2026-10-12 20:11:34 +08:00
wweir 2de08e62e4 fix(sower): guard the direct dial path against self-targeting too
DialSmart can reach directDial without passing DialProxyOnly (a direct rule
match, or an access probe that marks the name reachable), and a direct dial
back into this client's own proxy listener is a self-sustaining loop that
never leaves the host. Guard both entries; the second check is a cache hit.
2026-10-12 19:35:38 +08:00
wweir b688b93fbc fix(sower): refuse a proxy dial that resolves into this client's own listener
One request to a name whose A record is the client's own serve address
became a self-sustaining chain: the proxy forwarded it into the tunnel, the
relay resolved the name back to the client and dialed the client's own
listener, which forwarded it again. Measured on r6c: 601 connections and a
relay host that ran out of memory.

A proxy dial must never re-enter a listener this client forwards proxy
traffic on. Router.DialProxyOnly now refuses such a target: the listener set
is the HTTP/HTTPS proxy addresses on every DNS listen address plus SOCKS5,
resolution is skipped for ports this client does not serve, and the answer is
cached for a minute so a proxied dial does not pay a DNS round trip per
connection. Refusals surface as ErrSelfProxyTarget - HTTP 403, SOCKS5 0x02 -
and a resolution failure is not treated as evidence of self-targeting.

Plan, rejected alternatives and the evidence trail:
docs/plans/2026-10-12-self-proxy-target-guard.md
2026-10-12 19:30:56 +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 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 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 0aed36f698 feat(router): report matched rule text for hit tracking
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.
2026-08-09 19:02:18 +08:00
wweir 759794e897 fix(router): drop empty rules parsed from empty TOML arrays
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.
2026-08-09 18:56:45 +08:00
wweir defea58ac2 perf: batch byte accounting and bound gateway memory
- 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
2026-08-07 12:02:26 +08:00
wweir 60ac21f230 feat(router): guard DNS upstream refresh with generation
- Generation counter invalidates in-flight refreshes after SetDNS
- Probe/promote/degrade paths carry the generation to avoid stale mutations
2026-08-07 12:02:21 +08:00
wweir 6023537319 feat(admin): test a domain's rule matches and route decision
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
2026-08-06 22:42:31 +08:00
wweir e524592f61 feat(admin): paginate and search the rules list
The rules page froze because block rules (164k+) were all transferred
and rendered at once. The list API is now paginated and searchable:

- GET /api/rules?category=X&q=...&offset=N&limit=M returns a page plus
  the total match count; q is a case-insensitive substring match
  (alloc-free scan in RuleSet.ListFiltered), limit clamps to 1000,
  default page 200.
- POST/DELETE /api/rules now return 204; the frontend refetches the
  currently loaded range so totals stay accurate.
- Rules view: server-side search box with 300ms debounce, load-more
  button appends pages, count badge shows loaded/total.

Measured on the live box: first page 0.5ms, 164k-rule scan ~22ms.
2026-08-06 19:00:09 +08:00
wweir b4cb204e3f feat(admin): OTEL metrics, real-time rates and in-process history
Monitoring overhaul for the admin console:

- Add internal/metrics: OpenTelemetry meter with Prometheus exporter
  (counters for bytes/dns/connections/rule hits, updown counter for
  active connections, histogram for connection duration, process
  gauges). Served unauthenticated at GET /metrics; an OTLP exporter
  can be added later without touching call sites.
- Fix uptime: TrafficSnapshot.Uptime was a time.Duration serialized
  as nanoseconds while the frontend renders seconds; it is now int64
  seconds. Stats is created before rule loading so uptime tracks the
  process start.
- Extend admin.Stats: per-protocol active connections (idempotent
  decrement on Close), 60s sliding-window rates, rule-hit counters,
  process resources, and a bounded in-process history ring (1h at 5s
  sampling, gap-clamped) served at GET /api/history.
- Router route observer: block/direct decisions in DialSmart, proxy
  decisions in DialProxyOnly (covers HTTP/HTTPS paths), exactly once
  per connection.
- Fix socks5 handler closing the raw conn instead of the wrapped one,
  which would have leaked active-connection counts.
- Frontend: Overview gains rate cards, active connections, rule hits,
  system stats, and four hand-rolled SVG area charts (traffic, DNS/
  conns, active per protocol, rule hits) with hover readouts.
2026-08-06 17:17:24 +08:00
wweir ce5cf284b7 feat(router): add thread-safe RuleSet for runtime rule management
RuleSet retains the raw rule list alongside the suffix tree so rules can be
listed, added and removed at runtime. Removal rebuilds the tree from the
retained list; startup batch loading keeps the old single-compaction flow.
Router.BlockRule/DirectRule/ProxyRule migrate to *RuleSet.
2026-08-06 08:56:22 +08:00
wweir 3dec49dec4 test: add regression tests for routing layer separation and fail-fast
- DNS unknown domain forwards upstream; TCP smart dial falls back proxy.
- Transparent HTTP/HTTPS uses proxy-only even when block rule matches.
- Invalid country CIDR and invalid MMDB path fail at construction.
- Access probe cache is instance-scoped; test injects fake prober.
- net.Pipe tests add timeout protection via waitForHandler helper.
- newTestRouter helper wraps NewRouter error handling.
2026-06-24 13:30:48 +08:00
wweir 51438c93a0 feat(router): split DialSmart/DialProxyOnly, fail-fast on bad MMDB/CIDR, add Close
- Add DialSmart (rule+detect+fallback) and DialProxyOnly (forced proxy)
  as explicit routing entry points; keep Dial as compatibility alias.
- NewRouter now returns (*Router, error); non-empty invalid MMDB path
  is a startup error instead of a silent warning.
- AddCountryCIDRs now returns error; invalid CIDR entries fail fast.
- Add Router.Close() to release geoip2.Reader on shutdown.
- Add country RWMutex so Close() does not race with concurrent reads.
- localSite checks all resolved IPs instead of only the first.
- Move accessCache from package-level global to Router instance field;
  inject accessProbeFunc for testability.
- Disable redirect following in pingClient to avoid misleading probe
  results on redirect responses.
2026-06-24 13:30:18 +08:00
wweir 875f80e999 refactor: replace mem cache with otter 2026-05-14 21:53:16 +08:00
wweir ad338598ae fix: harden dns proxy routing 2026-04-25 04:12:42 +08:00
wweir 5fb9dfcfee fix(router): skip empty country mmdb 2026-04-25 03:17:45 +08:00
wweir 8bbf5ca03d refactor: simplify dns proxy routing checks 2026-03-30 23:12:04 +08:00
wweir 53c55aef24 refactor: simplify dns proxy replies 2026-03-30 23:03:49 +08:00
wweir b2228f8643 refactor: harden routing and transport handling 2026-03-30 23:02:13 +08:00
wweir 91b7f8e8c1 refactor: rename module from github.com/wweir/sower to github.com/sower-proxy/sower
- Update go.mod module declaration
- Update all import paths in Go files
- Update GitHub links in README.md
- All tests pass and build succeeds
2025-09-06 08:27:11 +08:00
wweir c347e15fe2 fix(stop): avoid unexpected stop 2025-05-14 13:17:18 +08:00
wweir 5fe202314d feat: change log package to log/slog 2025-04-29 14:24:35 +08:00
wweir fc7183d70e feat: support force setting upstream dns server 2025-03-18 18:06:33 +08:00
wweir 4a9197a4ec fix: upstream all failed case 2024-11-03 00:40:23 +08:00
wweir 77c4154c7c fix: http_proxy 2024-10-12 07:20:39 +08:00
wweir 3d6ee5e376 feat: support http(s)_proxy 2024-09-10 07:40:08 +08:00
wweir 45c9eea6e6 feat: support listen on multi ip 2024-05-14 13:23:25 +08:00
wweir 15c02b55d8 feat: optimize for network changed case 2023-04-24 10:44:37 +08:00
wweir 19dcf8e0d7 fix: dns proxy hang case 2022-05-29 12:43:48 +08:00
wweir 795bf73fb2 fix: DNS upstream connection close 2022-05-19 15:29:35 +08:00
wweir 6fc97a3c9f feat: optimize code with 1.18 generic 2022-05-01 22:53:51 +08:00
wweir 5ebd33b0d7 feat: migrate to golang 1.18 2022-04-30 14:33:54 +08:00
wweir b1f152cc0c fix: dns upstream lookback case 2022-04-05 23:42:33 +08:00
wweir bad7ae702a chore: separate packages 2021-12-06 14:44:24 +08:00
wweir 0f029f8c15 refactor: reduce memory usage 2021-11-15 14:21:23 +08:00
wweir 48e9680c03 chore: update dependency 2021-11-10 18:58:24 +08:00
wweir a28a211d2d change ping logix 2021-08-15 13:45:28 +08:00
wweir 38c7391ceb 1. add comments, 2. optimize text message 2021-08-11 06:48:18 +08:00
wweir 65234b1666 migrate deferlog to the separate package 2021-08-09 23:49:51 +08:00
wweir 59dfa934a2 optimize code, comment / log / flag 2021-07-29 19:06:34 +08:00
wweir 8bfb26ba3f support use socks5 as upstream proxy 2021-07-29 16:19:55 +08:00
wweir 2d269468ea add dns cache for dns proxy 2021-07-29 07:14:49 +08:00
wweir d7c0ba112e fix DNS forword issue, inspired by skydns 2021-07-28 14:59:48 +08:00