cmd/cli: use OS resolver as default upstream for SRV lan hostname

Since application may need SRV record for public domains, which could be
blocked by OS resolver, but not with remote upstreams.

This was reported by a Minecraft user, who seeing thing is broken after
upgrading to v1.4.0 release.
This commit is contained in:
Cuong Manh Le
2025-02-19 22:50:15 +07:00
committed by Cuong Manh Le
parent 5b1faf1ce3
commit c7168739c7
3 changed files with 32 additions and 22 deletions
+27 -18
View File
@@ -435,14 +435,17 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse {
if len(upstreamConfigs) == 0 { if len(upstreamConfigs) == 0 {
upstreamConfigs = []*ctrld.UpstreamConfig{osUpstreamConfig} upstreamConfigs = []*ctrld.UpstreamConfig{osUpstreamConfig}
upstreams = []string{upstreamOS} upstreams = []string{upstreamOS}
} // For OS resolver, local addresses are ignored to prevent possible looping.
// However, on Active Directory Domain Controller, where it has local DNS server
if p.isAdDomainQuery(req.msg) { // running and listening on local addresses, these local addresses must be used
ctrld.Log(ctx, mainLog.Load().Debug(), // as nameservers, so queries for ADDC could be resolved as expected.
"AD domain query detected for %s in domain %s", if p.isAdDomainQuery(req.msg) {
req.msg.Question[0].Name, p.adDomain) ctrld.Log(ctx, mainLog.Load().Debug(),
upstreamConfigs = []*ctrld.UpstreamConfig{localUpstreamConfig} "AD domain query detected for %s in domain %s",
upstreams = []string{upstreamOS} req.msg.Question[0].Name, p.adDomain)
upstreamConfigs = []*ctrld.UpstreamConfig{localUpstreamConfig}
upstreams = []string{upstreamOSLocal}
}
} }
res := &proxyResponse{} res := &proxyResponse{}
@@ -458,7 +461,7 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse {
ctrld.Log(ctx, mainLog.Load().Debug(), "%s, %s, %s -> %v", req.ufr.matchedPolicy, req.ufr.matchedNetwork, req.ufr.matchedRule, upstreams) ctrld.Log(ctx, mainLog.Load().Debug(), "%s, %s, %s -> %v", req.ufr.matchedPolicy, req.ufr.matchedNetwork, req.ufr.matchedRule, upstreams)
} else { } else {
switch { switch {
case isSrvLookup(req.msg): case isSrvLanLookup(req.msg):
upstreams = []string{upstreamOS} upstreams = []string{upstreamOS}
upstreamConfigs = []*ctrld.UpstreamConfig{osUpstreamConfig} upstreamConfigs = []*ctrld.UpstreamConfig{osUpstreamConfig}
ctx = ctrld.LanQueryCtx(ctx) ctx = ctrld.LanQueryCtx(ctx)
@@ -1109,21 +1112,27 @@ func isLanHostnameQuery(m *dns.Msg) bool {
default: default:
return false return false
} }
name := strings.TrimSuffix(q.Name, ".") return isLanHostname(q.Name)
}
// isSrvLanLookup reports whether DNS message is an SRV query of a LAN hostname.
func isSrvLanLookup(m *dns.Msg) bool {
if m == nil || len(m.Question) == 0 {
return false
}
q := m.Question[0]
return q.Qtype == dns.TypeSRV && isLanHostname(q.Name)
}
// isLanHostname reports whether name is a LAN hostname.
func isLanHostname(name string) bool {
name = strings.TrimSuffix(name, ".")
return !strings.Contains(name, ".") || return !strings.Contains(name, ".") ||
strings.HasSuffix(name, ".domain") || strings.HasSuffix(name, ".domain") ||
strings.HasSuffix(name, ".lan") || strings.HasSuffix(name, ".lan") ||
strings.HasSuffix(name, ".local") strings.HasSuffix(name, ".local")
} }
// isSrvLookup reports whether DNS message is a SRV query.
func isSrvLookup(m *dns.Msg) bool {
if m == nil || len(m.Question) == 0 {
return false
}
return m.Question[0].Qtype == dns.TypeSRV
}
// isWanClient reports whether the input is a WAN address. // isWanClient reports whether the input is a WAN address.
func isWanClient(na net.Addr) bool { func isWanClient(na net.Addr) bool {
var ip netip.Addr var ip netip.Addr
+4 -3
View File
@@ -418,20 +418,21 @@ func Test_isPrivatePtrLookup(t *testing.T) {
} }
} }
func Test_isSrvLookup(t *testing.T) { func Test_isSrvLanLookup(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
msg *dns.Msg msg *dns.Msg
isSrvLookup bool isSrvLookup bool
}{ }{
{"SRV", newDnsMsgWithHostname("foo", dns.TypeSRV), true}, {"SRV LAN", newDnsMsgWithHostname("foo", dns.TypeSRV), true},
{"Not SRV", newDnsMsgWithHostname("foo", dns.TypeNone), false}, {"Not SRV", newDnsMsgWithHostname("foo", dns.TypeNone), false},
{"Not SRV LAN", newDnsMsgWithHostname("controld.com", dns.TypeSRV), false},
} }
for _, tc := range tests { for _, tc := range tests {
tc := tc tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
if got := isSrvLookup(tc.msg); tc.isSrvLookup != got { if got := isSrvLanLookup(tc.msg); tc.isSrvLookup != got {
t.Errorf("unexpected result, want: %v, got: %v", tc.isSrvLookup, got) t.Errorf("unexpected result, want: %v, got: %v", tc.isSrvLookup, got)
} }
}) })
+1 -1
View File
@@ -43,7 +43,7 @@ const (
ctrldControlUnixSockMobile = "cd.sock" ctrldControlUnixSockMobile = "cd.sock"
upstreamPrefix = "upstream." upstreamPrefix = "upstream."
upstreamOS = upstreamPrefix + "os" upstreamOS = upstreamPrefix + "os"
upstreamPrivate = upstreamPrefix + "private" upstreamOSLocal = upstreamOS + ".local"
dnsWatchdogDefaultInterval = 20 * time.Second dnsWatchdogDefaultInterval = 20 * time.Second
ctrldServiceName = "ctrld" ctrldServiceName = "ctrld"
) )