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.
This commit is contained in:
wweir
2026-10-12 19:35:38 +08:00
parent b688b93fbc
commit 2de08e62e4
4 changed files with 44 additions and 6 deletions
+8
View File
@@ -223,6 +223,14 @@ func (r *Router) DialSmart(network, domain string, port uint16) (net.Conn, error
ctx := context.Background()
addr := net.JoinHostPort(domain, strconv.FormatUint(uint64(port), 10))
// Guarded here as well as in DialProxyOnly: this path reaches directDial
// without passing it (rule-based and probe-based direct decisions), and
// dialing a listener this client itself forwards on re-enters the proxy
// either way — locally, in that case, since no tunnel is involved.
if ip, self := r.selfTarget(domain, port); self {
return nil, selfProxyError(domain, port, ip)
}
// 1. rule_based( block > direct > proxy )
// 2. detect_based( CN IP || access site )
// 3. fallback( proxy )
+26
View File
@@ -108,3 +108,29 @@ func TestDialProxyOnlyAllowsTargetThatFailsToResolve(t *testing.T) {
t.Fatalf("dial after a resolution failure: %v", err)
}
}
// DialSmart reaches directDial without passing DialProxyOnly, so the guard has
// to sit on that path too: a name a direct rule matches, or one the access
// probe marks reachable, would otherwise dial this client's own listener in a
// loop that never leaves the host.
func TestDialSmartRefusesDirectSelfTarget(t *testing.T) {
t.Parallel()
r := newSelfTargetRouter(t, []string{"100.64.1.1:80"}, func(context.Context, string) ([]net.IP, error) {
return []net.IP{net.ParseIP("100.64.1.1")}, nil
})
r.DirectRule.Add("self.example.com")
if _, err := r.DialSmart("tcp", "self.example.com", 80); !errors.Is(err, ErrSelfProxyTarget) {
t.Fatalf("direct-routed self target err = %v, want ErrSelfProxyTarget", err)
}
}
func TestDialSmartRefusesSelfTargetLiteral(t *testing.T) {
t.Parallel()
r := newSelfTargetRouter(t, []string{"100.64.1.1:443"}, nil)
if _, err := r.DialSmart("tcp", "100.64.1.1", 443); !errors.Is(err, ErrSelfProxyTarget) {
t.Fatalf("literal self target on the smart path err = %v, want ErrSelfProxyTarget", err)
}
}