From 4816a09e3abd64668ebed35b1fb33a2ca3706ac4 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 2 Nov 2023 21:53:39 +0700 Subject: [PATCH] all: use private resolver for private IP address These queries could not be resolved by Control D upstreams, so it's useless and less performance to send them to servers. --- cmd/cli/dns_proxy.go | 65 +++++++++++++++++++++++++++++++++++++++ cmd/cli/dns_proxy_test.go | 27 ++++++++++++++++ resolver.go | 4 +++ 3 files changed, 96 insertions(+) diff --git a/cmd/cli/dns_proxy.go b/cmd/cli/dns_proxy.go index 9c486ed..af7628c 100644 --- a/cmd/cli/dns_proxy.go +++ b/cmd/cli/dns_proxy.go @@ -38,6 +38,12 @@ var osUpstreamConfig = &ctrld.UpstreamConfig{ Timeout: 2000, } +var privateUpstreamConfig = &ctrld.UpstreamConfig{ + Name: "Private resolver", + Type: ctrld.ResolverTypePrivate, + Timeout: 2000, +} + var errReload = errors.New("reload") func (p *prog) serveDNS(listenerNum string, reload bool, reloadCh chan struct{}) error { @@ -54,6 +60,11 @@ func (p *prog) serveDNS(listenerNum string, reload bool, reloadCh chan struct{}) handler := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) { p.sema.acquire() defer p.sema.release() + if len(m.Question) == 0 { + answer := new(dns.Msg) + answer.SetRcode(m, dns.RcodeFormatError) + return + } go p.detectLoop(m) q := m.Question[0] domain := canonicalName(q.Name) @@ -261,6 +272,11 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i upstreamConfigs = []*ctrld.UpstreamConfig{osUpstreamConfig} upstreams = []string{upstreamOS} } + if isPrivatePtrLookup(msg) { + ctrld.Log(ctx, mainLog.Load().Info(), "private PTR lookup -> [%s]", upstreamOS) + upstreamConfigs = []*ctrld.UpstreamConfig{privateUpstreamConfig} + upstreams = []string{upstreamOS} + } // Inverse query should not be cached: https://www.rfc-editor.org/rfc/rfc1035#section-7.4 if p.cache != nil && msg.Question[0].Qtype != dns.TypePTR { for _, upstream := range upstreams { @@ -634,3 +650,52 @@ func rfc1918Addresses() []string { }) return res } + +// ipFromARPA parses a FQDN arpa domain and return the IP address if valid. +func ipFromARPA(arpa string) net.IP { + if arpa, ok := strings.CutSuffix(arpa, ".in-addr.arpa."); ok { + if ptrIP := net.ParseIP(arpa); ptrIP != nil { + return net.IP{ptrIP[15], ptrIP[14], ptrIP[13], ptrIP[12]} + } + } + if arpa, ok := strings.CutSuffix(arpa, ".ip6.arpa."); ok { + l := net.IPv6len * 2 + base := 16 + ip := make(net.IP, net.IPv6len) + for i := 0; i < l && arpa != ""; i++ { + idx := strings.LastIndexByte(arpa, '.') + off := idx + 1 + if idx == -1 { + idx = 0 + off = 0 + } else if idx == len(arpa)-1 { + return nil + } + n, err := strconv.ParseUint(arpa[off:], base, 8) + if err != nil { + return nil + } + b := byte(n) + ii := i / 2 + if i&1 == 1 { + b |= ip[ii] << 4 + } + ip[ii] = b + arpa = arpa[:idx] + } + return ip + } + return nil +} + +// isPrivatePtrLookup reports whether DNS message is an PTR query for LAN network. +func isPrivatePtrLookup(m *dns.Msg) bool { + if m == nil || len(m.Question) == 0 { + return false + } + q := m.Question[0] + if ip := ipFromARPA(q.Name); ip != nil { + return ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() + } + return false +} diff --git a/cmd/cli/dns_proxy_test.go b/cmd/cli/dns_proxy_test.go index c3b6c96..8d18fa0 100644 --- a/cmd/cli/dns_proxy_test.go +++ b/cmd/cli/dns_proxy_test.go @@ -238,3 +238,30 @@ func Test_remoteAddrFromMsg(t *testing.T) { }) } } + +func Test_ipFromARPA(t *testing.T) { + tests := []struct { + IP string + ARPA string + }{ + {"1.2.3.4", "4.3.2.1.in-addr.arpa."}, + {"245.110.36.114", "114.36.110.245.in-addr.arpa."}, + {"::ffff:12.34.56.78", "78.56.34.12.in-addr.arpa."}, + {"::1", "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa."}, + {"1::", "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.0.ip6.arpa."}, + {"1234:567::89a:bcde", "e.d.c.b.a.9.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.7.6.5.0.4.3.2.1.ip6.arpa."}, + {"1234:567:fefe:bcbc:adad:9e4a:89a:bcde", "e.d.c.b.a.9.8.0.a.4.e.9.d.a.d.a.c.b.c.b.e.f.e.f.7.6.5.0.4.3.2.1.ip6.arpa."}, + {"", "asd.in-addr.arpa."}, + {"", "asd.ip6.arpa."}, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.IP, func(t *testing.T) { + t.Parallel() + if got := ipFromARPA(tc.ARPA); !got.Equal(net.ParseIP(tc.IP)) { + t.Errorf("unexpected ip, want: %s, got: %s", tc.IP, got) + } + }) + } +} diff --git a/resolver.go b/resolver.go index 969da86..f08263b 100644 --- a/resolver.go +++ b/resolver.go @@ -24,6 +24,8 @@ const ( ResolverTypeOS = "os" // ResolverTypeLegacy specifies legacy resolver. ResolverTypeLegacy = "legacy" + // ResolverTypePrivate is like ResolverTypeOS, but use for local resolver only. + ResolverTypePrivate = "private" ) var bootstrapDNS = "76.76.2.0" @@ -61,6 +63,8 @@ func NewResolver(uc *UpstreamConfig) (Resolver, error) { return or, nil case ResolverTypeLegacy: return &legacyResolver{uc: uc}, nil + case ResolverTypePrivate: + return NewPrivateResolver(), nil } return nil, fmt.Errorf("%w: %s", errUnknownResolver, typ) }