Files
sower/router/selfproxy.go
T
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

176 lines
5.7 KiB
Go

package router
import (
"errors"
"fmt"
"net"
"strconv"
"strings"
"time"
"github.com/miekg/dns"
)
const (
// selfProxyLookupTTL bounds how long a resolved name is reused for the
// self-target check. A proxied dial must not pay a DNS round trip per
// connection, and a name that points at this client keeps pointing at it
// for far longer than this window.
selfProxyLookupTTL = time.Minute
selfProxyCacheMaxNames = 1024
)
// ErrSelfProxyTarget reports a proxy dial to an address this client serves
// proxy traffic on. Forwarding it would re-enter this proxy, which forwards it
// again, so a single request becomes a self-sustaining chain between this
// client and its remote relay: measured 2026-10-12, one request to a name whose
// A record is the client's own serve address grew to 601 connections on r6c and
// OOM-killed the relay host.
var ErrSelfProxyTarget = errors.New("target is this client's own listener")
// selfProxyGuard is the set of TCP addresses this client serves proxy traffic
// on. A proxy dial to one of them can only re-enter the proxy.
type selfProxyGuard struct {
// sockets holds concrete "ip:port" listeners.
sockets map[string]struct{}
// anyIP holds ports bound to an unspecified or non-IP host: those are
// reachable at every local address, so they match any resolved IP.
anyIP map[uint16]struct{}
// ports is every tracked port, so a dial to a port this client does not
// serve proxy traffic on skips resolution entirely.
ports map[uint16]struct{}
}
func newSelfProxyGuard(addrs []string) *selfProxyGuard {
g := &selfProxyGuard{
sockets: make(map[string]struct{}, len(addrs)),
anyIP: make(map[uint16]struct{}, 1),
ports: make(map[uint16]struct{}, len(addrs)),
}
for _, addr := range addrs {
host, portStr, err := net.SplitHostPort(addr)
if err != nil {
// The addresses come from a config whose listeners already bound
// successfully; an unparsable one is not a listener this process
// serves, so it cannot be a self target.
continue
}
port, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
continue
}
g.ports[uint16(port)] = struct{}{}
ip := net.ParseIP(strings.Trim(host, "[]"))
if ip == nil || ip.IsUnspecified() {
g.anyIP[uint16(port)] = struct{}{}
continue
}
g.sockets[net.JoinHostPort(ip.String(), strconv.FormatUint(port, 10))] = struct{}{}
}
return g
}
func (g *selfProxyGuard) port(port uint16) bool {
_, ok := g.ports[port]
return ok
}
func (g *selfProxyGuard) matches(ip net.IP, port uint16) bool {
if _, ok := g.anyIP[port]; ok {
return true
}
_, ok := g.sockets[net.JoinHostPort(ip.String(), strconv.FormatUint(uint64(port), 10))]
return ok
}
// SetSelfProxyAddrs records the TCP addresses this client serves proxy traffic
// on — the HTTP and HTTPS proxy listeners plus SOCKS5. DialProxyOnly refuses a
// target that resolves to one of them; callers pass every address they listen
// on for proxying, not just the one that received a given request.
func (r *Router) SetSelfProxyAddrs(addrs ...string) {
r.selfProxy.Store(newSelfProxyGuard(addrs))
}
// selfTarget reports whether dialing domain:port reaches one of this client's
// own proxy listeners, and the address it resolved to.
func (r *Router) selfTarget(domain string, port uint16) (net.IP, bool) {
guard := r.selfProxy.Load()
if guard == nil || !guard.port(port) {
return nil, false
}
for _, ip := range r.selfProxyIPs(domain) {
if guard.matches(ip, port) {
return ip, true
}
}
return nil, false
}
// selfProxyIPs resolves domain the way the relay will see it: through the
// configured upstream resolvers, never through the system resolver. A deployed
// client answers its own DNS for every domain it proxies — that is how its
// clients are steered into it — so a system-resolver answer is no evidence of
// self-targeting, while the upstream answer is what the relay acts on. Only a
// name whose own record points at this client (e.g. one published for its
// serve address) resolves to a listener here.
func (r *Router) selfProxyIPs(domain string) []net.IP {
if ip := net.ParseIP(strings.Trim(domain, "[]")); ip != nil {
return []net.IP{ip}
}
if ips, ok := r.selfProxyCache.GetIfPresent(domain); ok {
return ips
}
// A resolution failure is not evidence of self-targeting: it yields no
// addresses and the dial proceeds, leaving the relay to report its own
// failure. The empty answer is cached too, so a failing lookup is not paid
// once per connection.
ips, err := r.selfProxyLookup(domain)
if err != nil {
ips = nil
}
r.selfProxyCache.Set(domain, ips)
return ips
}
// lookupUpstreamIPs asks the configured upstream resolvers for domain's
// addresses. The upstream list already excludes this client's own serve
// addresses (see buildUpstreamAddrs), so this cannot be answered by this
// client's own intercepting DNS.
func (r *Router) lookupUpstreamIPs(domain string) ([]net.IP, error) {
var (
ips []net.IP
lastErr error
)
for _, qtype := range []uint16{dns.TypeA, dns.TypeAAAA} {
req := new(dns.Msg)
req.SetQuestion(dns.Fqdn(domain), qtype)
resp, err := r.Exchange(req)
if err != nil {
lastErr = err
continue
}
for _, answer := range resp.Answer {
switch rr := answer.(type) {
case *dns.A:
ips = append(ips, rr.A)
case *dns.AAAA:
ips = append(ips, rr.AAAA)
}
}
}
if len(ips) == 0 && lastErr != nil {
return nil, lastErr
}
return ips, nil
}
// selfProxyError renders a refused proxy dial.
func selfProxyError(domain string, port uint16, ip net.IP) error {
return fmt.Errorf("%w: %s resolves to %s",
ErrSelfProxyTarget,
net.JoinHostPort(domain, strconv.FormatUint(uint64(port), 10)),
net.JoinHostPort(ip.String(), strconv.FormatUint(uint64(port), 10)))
}