Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package dnsproxy for openSUSE:Factory 
checked in at 2024-01-15 22:21:25
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/dnsproxy (Old)
 and      /work/SRC/openSUSE:Factory/.dnsproxy.new.21961 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "dnsproxy"

Mon Jan 15 22:21:25 2024 rev:18 rq:1138973 version:0.62.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/dnsproxy/dnsproxy.changes        2024-01-03 
12:31:03.724790502 +0100
+++ /work/SRC/openSUSE:Factory/.dnsproxy.new.21961/dnsproxy.changes     
2024-01-15 22:21:57.780872666 +0100
@@ -1,0 +2,7 @@
+Mon Jan 15 14:08:31 UTC 2024 - Eyad Issa <eyadlore...@gmail.com>
+
+- Update to version 0.62.0:
+  * Pull request 314: 6321 boot ttl vol.2
+  * Pull request 312: 6321 boot ttl vol.1
+
+-------------------------------------------------------------------

Old:
----
  dnsproxy-0.61.1.obscpio

New:
----
  dnsproxy-0.62.0.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ dnsproxy.spec ++++++
--- /var/tmp/diff_new_pack.MXUuyi/_old  2024-01-15 22:21:59.048919268 +0100
+++ /var/tmp/diff_new_pack.MXUuyi/_new  2024-01-15 22:21:59.052919415 +0100
@@ -17,7 +17,7 @@
 
 
 Name:           dnsproxy
-Version:        0.61.1
+Version:        0.62.0
 Release:        0
 Summary:        A DNS proxy server
 License:        Apache-2.0

++++++ _service ++++++
--- /var/tmp/diff_new_pack.MXUuyi/_old  2024-01-15 22:21:59.076920297 +0100
+++ /var/tmp/diff_new_pack.MXUuyi/_new  2024-01-15 22:21:59.080920444 +0100
@@ -2,7 +2,7 @@
   <service name="obs_scm" mode="manual">
     <param name="scm">git</param>
     <param name="url">https://github.com/AdguardTeam/dnsproxy.git</param>
-    <param name="revision">v0.61.1</param>
+    <param name="revision">v0.62.0</param>
     <param name="match-tag">*</param>
     <param name="versionrewrite-pattern">v(\d+\.\d+\.\d+)</param>
    <param name="versionformat">@PARENT_TAG@</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.MXUuyi/_old  2024-01-15 22:21:59.096921032 +0100
+++ /var/tmp/diff_new_pack.MXUuyi/_new  2024-01-15 22:21:59.100921179 +0100
@@ -1,6 +1,6 @@
 <servicedata>
 <service name="tar_scm">
                 <param 
name="url">https://github.com/AdguardTeam/dnsproxy.git</param>
-              <param 
name="changesrevision">a87a3dfd6b737144a16ef92d130319d12184f129</param></service></servicedata>
+              <param 
name="changesrevision">f1ceef03ad5be7272faeab541b7b71dd18d89f4e</param></service></servicedata>
 (No newline at EOF)
 

++++++ dnsproxy-0.61.1.obscpio -> dnsproxy-0.62.0.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsproxy-0.61.1/internal/bootstrap/bootstrap.go 
new/dnsproxy-0.62.0/internal/bootstrap/bootstrap.go
--- old/dnsproxy-0.61.1/internal/bootstrap/bootstrap.go 2023-12-29 
13:27:08.000000000 +0100
+++ new/dnsproxy-0.62.0/internal/bootstrap/bootstrap.go 2024-01-12 
11:09:22.000000000 +0100
@@ -14,20 +14,42 @@
        "github.com/AdguardTeam/golibs/errors"
        "github.com/AdguardTeam/golibs/log"
        "github.com/AdguardTeam/golibs/netutil"
+       "golang.org/x/exp/slices"
+)
+
+// Network is a network type for use in [Resolver]'s methods.
+type Network = string
+
+const (
+       // NetworkIP is a network type for both address families.
+       NetworkIP Network = "ip"
+
+       // NetworkIP4 is a network type for IPv4 address family.
+       NetworkIP4 Network = "ip4"
+
+       // NetworkIP6 is a network type for IPv6 address family.
+       NetworkIP6 Network = "ip6"
+
+       // NetworkTCP is a network type for TCP connections.
+       NetworkTCP Network = "tcp"
+
+       // NetworkUDP is a network type for UDP connections.
+       NetworkUDP Network = "udp"
 )
 
 // DialHandler is a dial function for creating unencrypted network connections
 // to the upstream server.  It establishes the connection to the server
-// specified at initialization and ignores the addr.
-type DialHandler func(ctx context.Context, network, addr string) (conn 
net.Conn, err error)
+// specified at initialization and ignores the addr.  network must be one of
+// [NetworkTCP] or [NetworkUDP].
+type DialHandler func(ctx context.Context, network Network, addr string) (conn 
net.Conn, err error)
 
 // ResolveDialContext returns a DialHandler that uses addresses resolved from u
 // using resolver.  u must not be nil.
 func ResolveDialContext(
        u *url.URL,
        timeout time.Duration,
-       resolver Resolver,
-       preferIPv6 bool,
+       r Resolver,
+       preferV6 bool,
 ) (h DialHandler, err error) {
        defer func() { err = errors.Annotate(err, "dialing %q: %w", u.Host) }()
 
@@ -38,7 +60,7 @@
                return nil, err
        }
 
-       if resolver == nil {
+       if r == nil {
                return nil, fmt.Errorf("resolver is nil: %w", ErrNoResolvers)
        }
 
@@ -49,21 +71,20 @@
                defer cancel()
        }
 
-       ips, err := resolver.LookupNetIP(ctx, "ip", host)
+       ips, err := r.LookupNetIP(ctx, NetworkIP, host)
        if err != nil {
                return nil, fmt.Errorf("resolving hostname: %w", err)
        }
 
-       proxynetutil.SortNetIPAddrs(ips, preferIPv6)
+       if preferV6 {
+               slices.SortStableFunc(ips, proxynetutil.PreferIPv6)
+       } else {
+               slices.SortStableFunc(ips, proxynetutil.PreferIPv4)
+       }
 
        addrs := make([]string, 0, len(ips))
        for _, ip := range ips {
-               if !ip.IsValid() {
-                       // All invalid addresses should be in the tail after 
sorting.
-                       break
-               }
-
-               addrs = append(addrs, netip.AddrPortFrom(ip, 
uint16(port)).String())
+               addrs = append(addrs, netip.AddrPortFrom(ip, port).String())
        }
 
        return NewDialContext(timeout, addrs...), nil
@@ -71,14 +92,7 @@
 
 // NewDialContext returns a DialHandler that dials addrs and returns the first
 // successful connection.  At least a single addr should be specified.
-//
-// TODO(e.burkov):  Consider using [Resolver] instead of
-// [upstream.Options.Bootstrap] and [upstream.Options.ServerIPAddrs].
 func NewDialContext(timeout time.Duration, addrs ...string) (h DialHandler) {
-       dialer := &net.Dialer{
-               Timeout: timeout,
-       }
-
        l := len(addrs)
        if l == 0 {
                log.Debug("bootstrap: no addresses to dial")
@@ -88,9 +102,11 @@
                }
        }
 
-       // TODO(e.burkov):  Check IPv6 preference here.
+       dialer := &net.Dialer{
+               Timeout: timeout,
+       }
 
-       return func(ctx context.Context, network, _ string) (conn net.Conn, err 
error) {
+       return func(ctx context.Context, network Network, _ string) (conn 
net.Conn, err error) {
                var errs []error
 
                // Return first succeeded connection.  Note that we're using 
addrs
@@ -101,17 +117,18 @@
                        start := time.Now()
                        conn, err = dialer.DialContext(ctx, network, addr)
                        elapsed := time.Since(start)
-                       if err == nil {
-                               log.Debug("bootstrap: connection to %s 
succeeded in %s", addr, elapsed)
+                       if err != nil {
+                               log.Debug("bootstrap: connection to %s failed 
in %s: %s", addr, elapsed, err)
+                               errs = append(errs, err)
 
-                               return conn, nil
+                               continue
                        }
 
-                       log.Debug("bootstrap: connection to %s failed in %s: 
%s", addr, elapsed, err)
-                       errs = append(errs, err)
+                       log.Debug("bootstrap: connection to %s succeeded in 
%s", addr, elapsed)
+
+                       return conn, nil
                }
 
-               // TODO(e.burkov):  Use errors.Join in Go 1.20.
-               return nil, errors.List("all dialers failed", errs...)
+               return nil, errors.Join(errs...)
        }
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsproxy-0.61.1/internal/bootstrap/bootstrap_test.go 
new/dnsproxy-0.62.0/internal/bootstrap/bootstrap_test.go
--- old/dnsproxy-0.61.1/internal/bootstrap/bootstrap_test.go    2023-12-29 
13:27:08.000000000 +0100
+++ new/dnsproxy-0.62.0/internal/bootstrap/bootstrap_test.go    2024-01-12 
11:09:22.000000000 +0100
@@ -87,7 +87,7 @@
                                network string,
                                host string,
                        ) (addrs []netip.Addr, err error) {
-                               require.Equal(pt, "ip", network)
+                               require.Equal(pt, bootstrap.NetworkIP, network)
                                require.Equal(pt, hostname, host)
 
                                return tc.addresses, nil
@@ -103,7 +103,7 @@
                        )
                        require.NoError(t, err)
 
-                       conn, err := dialContext(context.Background(), "tcp", 
"")
+                       conn, err := dialContext(context.Background(), 
bootstrap.NetworkTCP, "")
                        require.NoError(t, err)
 
                        expected, ok := testutil.RequireReceive(t, sig, 
testTimeout)
@@ -120,7 +120,7 @@
                                network string,
                                host string,
                        ) (addrs []netip.Addr, err error) {
-                               require.Equal(pt, "ip", network)
+                               require.Equal(pt, bootstrap.NetworkIP, network)
                                require.Equal(pt, hostname, host)
 
                                return nil, nil
@@ -135,7 +135,7 @@
                )
                require.NoError(t, err)
 
-               _, err = dialContext(context.Background(), "tcp", "")
+               _, err = dialContext(context.Background(), 
bootstrap.NetworkTCP, "")
                testutil.AssertErrorMsg(t, "no addresses", err)
        })
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsproxy-0.61.1/internal/bootstrap/error.go 
new/dnsproxy-0.62.0/internal/bootstrap/error.go
--- old/dnsproxy-0.61.1/internal/bootstrap/error.go     1970-01-01 
01:00:00.000000000 +0100
+++ new/dnsproxy-0.62.0/internal/bootstrap/error.go     2024-01-12 
11:09:22.000000000 +0100
@@ -0,0 +1,6 @@
+package bootstrap
+
+import "github.com/AdguardTeam/golibs/errors"
+
+// ErrNoResolvers is returned when zero resolvers specified.
+const ErrNoResolvers errors.Error = "no resolvers specified"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsproxy-0.61.1/internal/bootstrap/resolver.go 
new/dnsproxy-0.62.0/internal/bootstrap/resolver.go
--- old/dnsproxy-0.61.1/internal/bootstrap/resolver.go  2023-12-29 
13:27:08.000000000 +0100
+++ new/dnsproxy-0.62.0/internal/bootstrap/resolver.go  2024-01-12 
11:09:22.000000000 +0100
@@ -8,22 +8,21 @@
 
        "github.com/AdguardTeam/golibs/errors"
        "github.com/AdguardTeam/golibs/log"
+       "golang.org/x/exp/slices"
 )
 
-// Resolver resolves the hostnames to IP addresses.
+// Resolver resolves the hostnames to IP addresses.  Note, that [net.Resolver]
+// from standard library also implements this interface.
 type Resolver interface {
-       // LookupNetIP looks up the IP addresses for the given host.  network 
must
-       // be one of "ip", "ip4" or "ip6".  The response may be empty even if 
err is
-       // nil.
-       LookupNetIP(ctx context.Context, network, host string) (addrs 
[]netip.Addr, err error)
+       // LookupNetIP looks up the IP addresses for the given host.  network 
should
+       // be one of [NetworkIP], [NetworkIP4] or [NetworkIP6].  The response 
may be
+       // empty even if err is nil.  All the addrs must be valid.
+       LookupNetIP(ctx context.Context, network Network, host string) (addrs 
[]netip.Addr, err error)
 }
 
 // type check
 var _ Resolver = &net.Resolver{}
 
-// ErrNoResolvers is returned when zero resolvers specified.
-const ErrNoResolvers errors.Error = "no resolvers specified"
-
 // ParallelResolver is a slice of resolvers that are queried concurrently.  The
 // first successful response is returned.
 type ParallelResolver []Resolver
@@ -34,7 +33,7 @@
 // LookupNetIP implements the [Resolver] interface for ParallelResolver.
 func (r ParallelResolver) LookupNetIP(
        ctx context.Context,
-       network string,
+       network Network,
        host string,
 ) (addrs []netip.Addr, err error) {
        resolversNum := len(r)
@@ -48,7 +47,7 @@
        }
 
        // Size of channel must accommodate results of lookups from all 
resolvers,
-       // sending into channel will be block otherwise.
+       // sending into channel will block otherwise.
        ch := make(chan any, resolversNum)
        for _, rslv := range r {
                go lookupAsync(ctx, rslv, network, host, ch)
@@ -97,3 +96,50 @@
 
        return addrs, err
 }
+
+// ConsequentResolver is a slice of resolvers that are queried in order until
+// the first successful non-empty response, as opposed to just successful
+// response requirement in [ParallelResolver].
+type ConsequentResolver []Resolver
+
+// type check
+var _ Resolver = ConsequentResolver(nil)
+
+// LookupNetIP implements the [Resolver] interface for ConsequentResolver.
+func (resolvers ConsequentResolver) LookupNetIP(
+       ctx context.Context,
+       network Network,
+       host string,
+) (addrs []netip.Addr, err error) {
+       if len(resolvers) == 0 {
+               return nil, ErrNoResolvers
+       }
+
+       var errs []error
+       for _, r := range resolvers {
+               addrs, err = r.LookupNetIP(ctx, network, host)
+               if err == nil && len(addrs) > 0 {
+                       return addrs, nil
+               }
+
+               errs = append(errs, err)
+       }
+
+       return nil, errors.Join(errs...)
+}
+
+// StaticResolver is a resolver which always responds with an underlying slice
+// of IP addresses regardless of host and network.
+type StaticResolver []netip.Addr
+
+// type check
+var _ Resolver = StaticResolver(nil)
+
+// LookupNetIP implements the [Resolver] interface for StaticResolver.
+func (r StaticResolver) LookupNetIP(
+       _ context.Context,
+       _ Network,
+       _ string,
+) (addrs []netip.Addr, err error) {
+       return slices.Clone(r), nil
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsproxy-0.61.1/internal/netutil/netutil.go 
new/dnsproxy-0.62.0/internal/netutil/netutil.go
--- old/dnsproxy-0.61.1/internal/netutil/netutil.go     2023-12-29 
13:27:08.000000000 +0100
+++ new/dnsproxy-0.62.0/internal/netutil/netutil.go     2024-01-12 
11:09:22.000000000 +0100
@@ -12,6 +12,42 @@
        "golang.org/x/exp/slices"
 )
 
+// PreferIPv4 compares two addresses, preferring IPv4 addresses over IPv6 ones.
+// Invalid addresses are sorted near the end.
+func PreferIPv4(a, b netip.Addr) (res int) {
+       if !a.IsValid() {
+               return 1
+       } else if !b.IsValid() {
+               return -1
+       }
+
+       if aIs4 := a.Is4(); aIs4 == b.Is4() {
+               return a.Compare(b)
+       } else if aIs4 {
+               return -1
+       }
+
+       return 1
+}
+
+// PreferIPv6 compares two addresses, preferring IPv6 addresses over IPv4 ones.
+// Invalid addresses are sorted near the end.
+func PreferIPv6(a, b netip.Addr) (res int) {
+       if !a.IsValid() {
+               return 1
+       } else if !b.IsValid() {
+               return -1
+       }
+
+       if aIs6 := a.Is6(); aIs6 == b.Is6() {
+               return a.Compare(b)
+       } else if aIs6 {
+               return -1
+       }
+
+       return 1
+}
+
 // SortNetIPAddrs sorts addrs in accordance with the protocol preferences.
 // Invalid addresses are sorted near the end.  Zones are ignored.
 func SortNetIPAddrs(addrs []netip.Addr, preferIPv6 bool) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsproxy-0.61.1/main.go new/dnsproxy-0.62.0/main.go
--- old/dnsproxy-0.61.1/main.go 2023-12-29 13:27:08.000000000 +0100
+++ new/dnsproxy-0.62.0/main.go 2024-01-12 11:09:22.000000000 +0100
@@ -465,13 +465,13 @@
        var resolvers []upstream.Resolver
 
        for i, b := range bootstraps {
-               var resolver upstream.Resolver
-               resolver, err = upstream.NewUpstreamResolver(b, opts)
+               var ur *upstream.UpstreamResolver
+               ur, err = upstream.NewUpstreamResolver(b, opts)
                if err != nil {
                        return nil, fmt.Errorf("creating bootstrap resolver at 
index %d: %w", i, err)
                }
 
-               resolvers = append(resolvers, resolver)
+               resolvers = append(resolvers, upstream.NewCachingResolver(ur))
        }
 
        switch len(resolvers) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsproxy-0.61.1/proxy/proxy_test.go 
new/dnsproxy-0.62.0/proxy/proxy_test.go
--- old/dnsproxy-0.61.1/proxy/proxy_test.go     2023-12-29 13:27:08.000000000 
+0100
+++ new/dnsproxy-0.62.0/proxy/proxy_test.go     2024-01-12 11:09:22.000000000 
+0100
@@ -329,7 +329,7 @@
                upstreams,
                &upstream.Options{
                        InsecureSkipVerify: false,
-                       Bootstrap:          googleRslv,
+                       Bootstrap:          
upstream.NewCachingResolver(googleRslv),
                        Timeout:            1 * time.Second,
                },
        )
@@ -412,7 +412,7 @@
                u, err = upstream.AddressToUpstream(
                        line,
                        &upstream.Options{
-                               Bootstrap: googleRslv,
+                               Bootstrap: 
upstream.NewCachingResolver(googleRslv),
                                Timeout:   timeOut,
                        },
                )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsproxy-0.61.1/upstream/resolver.go 
new/dnsproxy-0.62.0/upstream/resolver.go
--- old/dnsproxy-0.61.1/upstream/resolver.go    2023-12-29 13:27:08.000000000 
+0100
+++ new/dnsproxy-0.62.0/upstream/resolver.go    2024-01-12 11:09:22.000000000 
+0100
@@ -5,20 +5,35 @@
        "fmt"
        "net/netip"
        "net/url"
+       "strings"
+       "sync"
+       "time"
 
        "github.com/AdguardTeam/dnsproxy/internal/bootstrap"
        "github.com/AdguardTeam/dnsproxy/proxyutil"
        "github.com/AdguardTeam/golibs/errors"
        "github.com/AdguardTeam/golibs/log"
        "github.com/miekg/dns"
-       "golang.org/x/exp/slices"
 )
 
-// Resolver is an alias for the internal [bootstrap.Resolver] to allow custom
-// implementations.  Note, that the [net.Resolver] from standard library also
-// implements this interface.
+// Resolver resolves the hostnames to IP addresses.  Note, that [net.Resolver]
+// from standard library also implements this interface.
 type Resolver = bootstrap.Resolver
 
+// StaticResolver is a resolver which always responds with an underlying slice
+// of IP addresses.
+type StaticResolver = bootstrap.StaticResolver
+
+// ParallelResolver is a slice of resolvers that are queried concurrently until
+// the first successful response is returned, as opposed to all resolvers being
+// queried in order in [ConsequentResolver].
+type ParallelResolver = bootstrap.ParallelResolver
+
+// ConsequentResolver is a slice of resolvers that are queried in order until
+// the first successful non-empty response, as opposed to just successful
+// response requirement in [ParallelResolver].
+type ConsequentResolver = bootstrap.ConsequentResolver
+
 // UpstreamResolver is a wrapper around Upstream that implements the
 // [bootstrap.Resolver] interface.
 type UpstreamResolver struct {
@@ -105,57 +120,107 @@
 // type check
 var _ Resolver = &UpstreamResolver{}
 
-// LookupNetIP implements the [Resolver] interface for upstreamResolver.
+// LookupNetIP implements the [Resolver] interface for *UpstreamResolver.  It
+// doesn't consider the TTL of the DNS records.
 //
-// TODO(e.burkov):  Use context.
+// TODO(e.burkov):  Investigate why the empty slice is returned instead of nil.
 func (r *UpstreamResolver) LookupNetIP(
-       _ context.Context,
-       network string,
+       ctx context.Context,
+       network bootstrap.Network,
        host string,
 ) (ips []netip.Addr, err error) {
        if host == "" {
                return nil, nil
        }
 
-       switch network {
-       case "ip4", "ip6":
-               host = dns.Fqdn(host)
-               ips, err = r.resolve(host, network)
-       case "ip":
-               host = dns.Fqdn(host)
-               resCh := make(chan any, 2)
-               go r.resolveAsync(resCh, host, "ip4")
-               go r.resolveAsync(resCh, host, "ip6")
-
-               var errs []error
-               for i := 0; i < 2; i++ {
-                       switch res := <-resCh; res := res.(type) {
-                       case error:
-                               errs = append(errs, res)
-                       case []netip.Addr:
-                               ips = append(ips, res...)
-                       }
+       host = dns.Fqdn(strings.ToLower(host))
+
+       rr, err := r.resolveIP(ctx, network, host)
+       if err != nil {
+               return []netip.Addr{}, err
+       }
+
+       for _, ip := range rr {
+               ips = append(ips, ip.addr)
+       }
+
+       return ips, err
+}
+
+// ipResult reflects a single A/AAAA record from the DNS response.  It's used
+// to cache the results of lookups.
+type ipResult struct {
+       addr   netip.Addr
+       expire time.Time
+}
+
+// filterExpired returns the addresses from res that are not expired yet.  It
+// returns nil if all the addresses are expired.
+func filterExpired(res []ipResult, now time.Time) (filtered []netip.Addr) {
+       for _, r := range res {
+               if r.expire.After(now) {
+                       filtered = append(filtered, r.addr)
                }
+       }
 
-               err = errors.Join(errs...)
+       return filtered
+}
+
+// resolveIP performs a DNS lookup of host and returns the result.  network 
must
+// be either [bootstrap.NetworkIP4], [bootstrap.NetworkIP6] or
+// [bootstrap.NetworkIP].  host must be in a lower-case FQDN form.
+//
+// TODO(e.burkov):  Use context.
+func (r *UpstreamResolver) resolveIP(
+       _ context.Context,
+       network bootstrap.Network,
+       host string,
+) (rr []ipResult, err error) {
+       switch network {
+       case bootstrap.NetworkIP4, bootstrap.NetworkIP6:
+               return r.resolve(host, network)
+       case bootstrap.NetworkIP:
+               // Go on.
        default:
-               return []netip.Addr{}, fmt.Errorf("unsupported network %s", 
network)
+               return nil, fmt.Errorf("unsupported network %s", network)
        }
 
-       if len(ips) == 0 {
-               ips = []netip.Addr{}
+       resCh := make(chan any, 2)
+       go r.resolveAsync(resCh, host, bootstrap.NetworkIP4)
+       go r.resolveAsync(resCh, host, bootstrap.NetworkIP6)
+
+       var errs []error
+
+       for i := 0; i < 2; i++ {
+               switch res := <-resCh; res := res.(type) {
+               case error:
+                       errs = append(errs, res)
+               case []ipResult:
+                       rr = append(rr, res...)
+               }
        }
 
-       return ips, err
+       return rr, errors.Join(errs...)
 }
 
 // resolve performs a single DNS lookup of host and returns all the valid
 // addresses from the answer section of the response.  network must be either
-// "ip4" or "ip6".
-func (r *UpstreamResolver) resolve(host, network string) (addrs []netip.Addr, 
err error) {
-       qtype := dns.TypeA
-       if network == "ip6" {
+// "ip4" or "ip6".  host must be in a lower-case FQDN form.
+//
+// TODO(e.burkov):  Consider NS and Extra sections when setting TTL.  Check out
+// what RFCs say about it.
+func (r *UpstreamResolver) resolve(
+       host string,
+       n bootstrap.Network,
+) (res []ipResult, err error) {
+       var qtype uint16
+       switch n {
+       case bootstrap.NetworkIP4:
+               qtype = dns.TypeA
+       case bootstrap.NetworkIP6:
                qtype = dns.TypeAAAA
+       default:
+               panic(fmt.Sprintf("unsupported network %q", n))
        }
 
        req := &dns.Msg{
@@ -170,78 +235,107 @@
                }},
        }
 
-       resp, err := r.Upstream.Exchange(req)
-       if err != nil || resp == nil {
+       // As per [upstream.Exchange] documentation, the response is always 
returned
+       // if no error occurred.
+       resp, err := r.Exchange(req)
+       if err != nil {
                return nil, err
        }
 
+       now := time.Now()
        for _, rr := range resp.Answer {
-               if addr := proxyutil.IPFromRR(rr); addr.IsValid() {
-                       addrs = append(addrs, addr)
+               ip := proxyutil.IPFromRR(rr)
+               if !ip.IsValid() {
+                       continue
                }
+
+               res = append(res, ipResult{
+                       addr:   ip,
+                       expire: now.Add(time.Duration(rr.Header().Ttl) * 
time.Second),
+               })
        }
 
-       return addrs, nil
+       return res, nil
 }
 
 // resolveAsync performs a single DNS lookup and sends the result to ch.  It's
 // intended to be used as a goroutine.
 func (r *UpstreamResolver) resolveAsync(resCh chan<- any, host, network 
string) {
-       resp, err := r.resolve(host, network)
+       res, err := r.resolve(host, network)
        if err != nil {
                resCh <- err
        } else {
-               resCh <- resp
+               resCh <- res
        }
 }
 
-// StaticResolver is a resolver which always responds with an underlying slice
-// of IP addresses.
-type StaticResolver []netip.Addr
+// CachingResolver is a [Resolver] that caches the results of lookups.  It's
+// required to be created with [NewCachingResolver].
+type CachingResolver struct {
+       // resolver is the underlying resolver to use for lookups.
+       resolver *UpstreamResolver
 
-// type check
-var _ Resolver = StaticResolver(nil)
+       // mu protects cached and it's elements.
+       mu *sync.RWMutex
 
-// LookupNetIP implements the [Resolver] interface for StaticResolver.
-func (r StaticResolver) LookupNetIP(
-       ctx context.Context,
-       network string,
-       host string,
-) (addrs []netip.Addr, err error) {
-       return slices.Clone(r), nil
+       // cached is the set of cached results sorted by [resolveResult.name].
+       cached map[string][]ipResult
 }
 
-// ConsequentResolver is a slice of resolvers that are queried in order until
-// the first successful non-empty response, as opposed to just successful
-// response requirement in [ParallelResolver].
-type ConsequentResolver []Resolver
+// NewCachingResolver creates a new caching resolver that uses r for lookups.
+func NewCachingResolver(r *UpstreamResolver) (cr *CachingResolver) {
+       return &CachingResolver{
+               resolver: r,
+               mu:       &sync.RWMutex{},
+               cached:   map[string][]ipResult{},
+       }
+}
 
 // type check
-var _ Resolver = ConsequentResolver(nil)
+var _ Resolver = (*CachingResolver)(nil)
 
-// LookupNetIP implements the [Resolver] interface for ConsequentResolver.
-func (resolvers ConsequentResolver) LookupNetIP(
+// LookupNetIP implements the [Resolver] interface for *CachingResolver.
+func (r *CachingResolver) LookupNetIP(
        ctx context.Context,
-       network string,
+       network bootstrap.Network,
        host string,
 ) (addrs []netip.Addr, err error) {
-       if len(resolvers) == 0 {
-               return nil, bootstrap.ErrNoResolvers
+       now := time.Now()
+       host = dns.Fqdn(strings.ToLower(host))
+
+       addrs = r.findCached(host, now)
+       if addrs != nil {
+               return addrs, nil
        }
 
-       var errs []error
-       for _, r := range resolvers {
-               addrs, err = r.LookupNetIP(ctx, network, host)
-               if err == nil && len(addrs) > 0 {
-                       return addrs, nil
-               }
+       newRes, err := r.resolver.resolveIP(ctx, network, host)
+       if err != nil {
+               return []netip.Addr{}, err
+       }
 
-               errs = append(errs, err)
+       addrs = filterExpired(newRes, now)
+       if len(addrs) == 0 {
+               return []netip.Addr{}, nil
        }
 
-       return nil, errors.Join(errs...)
+       r.mu.Lock()
+       defer r.mu.Unlock()
+
+       r.cached[host] = newRes
+
+       return addrs, nil
 }
 
-// ParallelResolver is an alias for the internal [bootstrap.ParallelResolver] 
to
-// allow it's usage outside of the module.
-type ParallelResolver = bootstrap.ParallelResolver
+// findCached returns the cached addresses for host if it's not expired yet, 
and
+// the corresponding cached result, if any.
+func (r *CachingResolver) findCached(host string, now time.Time) (addrs 
[]netip.Addr) {
+       r.mu.RLock()
+       defer r.mu.RUnlock()
+
+       res, ok := r.cached[host]
+       if !ok {
+               return nil
+       }
+
+       return filterExpired(res, now)
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsproxy-0.61.1/upstream/upstream.go 
new/dnsproxy-0.62.0/upstream/upstream.go
--- old/dnsproxy-0.61.1/upstream/upstream.go    2023-12-29 13:27:08.000000000 
+0100
+++ new/dnsproxy-0.62.0/upstream/upstream.go    2024-01-12 11:09:22.000000000 
+0100
@@ -14,7 +14,6 @@
        "os"
        "strconv"
        "strings"
-       "sync/atomic"
        "time"
 
        "github.com/AdguardTeam/dnsproxy/internal/bootstrap"
@@ -323,9 +322,7 @@
        }
 }
 
-// DialerInitializer returns the handler that it creates.  All the subsequent
-// calls to it, except the first one, will return the same handler so that
-// resolving will be performed only once.
+// DialerInitializer returns the handler that it creates.
 type DialerInitializer func() (handler bootstrap.DialHandler, err error)
 
 // newDialerInitializer creates an initializer of the dialer that will dial the
@@ -335,7 +332,9 @@
                // Don't resolve the address of the server since it's already 
an IP.
                handler := bootstrap.NewDialContext(opts.Timeout, u.Host)
 
-               return func() (bootstrap.DialHandler, error) { return handler, 
nil }
+               return func() (h bootstrap.DialHandler, dialerErr error) {
+                       return handler, nil
+               }
        }
 
        boot := opts.Bootstrap
@@ -344,27 +343,7 @@
                boot = net.DefaultResolver
        }
 
-       var dialHandler atomic.Pointer[bootstrap.DialHandler]
-
        return func() (h bootstrap.DialHandler, err error) {
-               // Check if the dial handler has already been created.
-               if hPtr := dialHandler.Load(); hPtr != nil {
-                       return *hPtr, nil
-               }
-
-               // TODO(e.burkov):  It may appear that several exchanges will 
try to
-               // resolve the upstream hostname at the same time.  Currently, 
the last
-               // successful value will be stored in dialHandler, but ideally 
we should
-               // resolve only once.
-               h, err = bootstrap.ResolveDialContext(u, opts.Timeout, boot, 
opts.PreferIPv6)
-               if err != nil {
-                       return nil, fmt.Errorf("creating dial handler: %w", err)
-               }
-
-               if !dialHandler.CompareAndSwap(nil, &h) {
-                       return *dialHandler.Load(), nil
-               }
-
-               return h, nil
+               return bootstrap.ResolveDialContext(u, opts.Timeout, boot, 
opts.PreferIPv6)
        }
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsproxy-0.61.1/upstream/upstream_test.go 
new/dnsproxy-0.62.0/upstream/upstream_test.go
--- old/dnsproxy-0.61.1/upstream/upstream_test.go       2023-12-29 
13:27:08.000000000 +0100
+++ new/dnsproxy-0.62.0/upstream/upstream_test.go       2024-01-12 
11:09:22.000000000 +0100
@@ -55,7 +55,7 @@
 
        // Create an upstream that uses this faulty bootstrap.
        u, err := AddressToUpstream("tls://random-domain-name", &Options{
-               Bootstrap: rslv,
+               Bootstrap: NewCachingResolver(rslv),
                Timeout:   timeout,
        })
        require.NoError(t, err)
@@ -114,17 +114,20 @@
        })
        require.NoError(t, err)
 
+       googleBoot := NewCachingResolver(googleRslv)
+       cloudflareBoot := NewCachingResolver(cloudflareRslv)
+
        upstreams := []struct {
                bootstrap Resolver
                address   string
        }{{
-               bootstrap: googleRslv,
+               bootstrap: googleBoot,
                address:   "8.8.8.8:53",
        }, {
                bootstrap: nil,
                address:   "1.1.1.1",
        }, {
-               bootstrap: cloudflareRslv,
+               bootstrap: cloudflareBoot,
                address:   "1.1.1.1",
        }, {
                bootstrap: nil,
@@ -139,19 +142,19 @@
                bootstrap: nil,
                address:   "tls://9.9.9.9:853",
        }, {
-               bootstrap: googleRslv,
+               bootstrap: googleBoot,
                address:   "tls://dns.adguard.com",
        }, {
-               bootstrap: googleRslv,
+               bootstrap: googleBoot,
                address:   "tls://dns.adguard.com:853",
        }, {
-               bootstrap: googleRslv,
+               bootstrap: googleBoot,
                address:   "tls://dns.adguard.com:853",
        }, {
                bootstrap: nil,
                address:   "tls://one.one.one.one",
        }, {
-               bootstrap: googleRslv,
+               bootstrap: googleBoot,
                address:   "https://1dot1dot1dot1.cloudflare-dns.com/dns-query";,
        }, {
                bootstrap: nil,
@@ -165,11 +168,11 @@
                address:   
"sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
        }, {
                // AdGuard Family (DNSCrypt)
-               bootstrap: googleRslv,
+               bootstrap: googleBoot,
                address:   
"sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMjo1NDQzILgxXdexS27jIKRw3C7Wsao5jMnlhvhdRUXWuMm1AFq6ITIuZG5zY3J5cHQuZmFtaWx5Lm5zMS5hZGd1YXJkLmNvbQ",
        }, {
                // Cloudflare DNS (DNS-over-HTTPS)
-               bootstrap: googleRslv,
+               bootstrap: googleBoot,
                address:   
"sdns://AgcAAAAAAAAABzEuMC4wLjGgENk8mGSlIfMGXMOlIlCcKvq7AVgcrZxtjon911-ep0cg63Ul-I8NlFj4GplQGb_TTLiczclX57DvMV8Q-JdjgRgSZG5zLmNsb3VkZmxhcmUuY29tCi9kbnMtcXVlcnk",
        }, {
                // Google (Plain)
@@ -177,11 +180,11 @@
                address:   "sdns://AAcAAAAAAAAABzguOC44Ljg",
        }, {
                // AdGuard DNS (DNS-over-TLS)
-               bootstrap: googleRslv,
+               bootstrap: googleBoot,
                address:   "sdns://AwAAAAAAAAAAAAAPZG5zLmFkZ3VhcmQuY29t",
        }, {
                // AdGuard DNS (DNS-over-QUIC)
-               bootstrap: googleRslv,
+               bootstrap: googleBoot,
                address:   
"sdns://BAcAAAAAAAAAAAAXZG5zLmFkZ3VhcmQtZG5zLmNvbTo3ODQ",
        }, {
                // Cloudflare DNS (DNS-over-HTTPS)
@@ -189,7 +192,7 @@
                address:   "https://1.1.1.1/dns-query";,
        }, {
                // AdGuard DNS (DNS-over-QUIC)
-               bootstrap: googleRslv,
+               bootstrap: googleBoot,
                address:   "quic://dns.adguard-dns.com",
        }, {
                // Google DNS (HTTP3)
@@ -215,7 +218,7 @@
        cloudflareRslv, err := NewUpstreamResolver("1.1.1.1", nil)
        require.NoError(t, err)
 
-       opt := &Options{Bootstrap: cloudflareRslv}
+       opt := &Options{Bootstrap: NewCachingResolver(cloudflareRslv)}
 
        testCases := []struct {
                addr string
@@ -314,7 +317,7 @@
                        require.NoError(t, err)
 
                        u, err := AddressToUpstream(tc.address, &Options{
-                               Bootstrap: rslv,
+                               Bootstrap: NewCachingResolver(rslv),
                                Timeout:   timeout,
                        })
                        require.NoErrorf(t, err, "failed to generate upstream 
from address %s", tc.address)
@@ -361,7 +364,7 @@
                                })
                                require.NoError(t, err)
 
-                               rslv = append(rslv, r)
+                               rslv = append(rslv, NewCachingResolver(r))
                        }
 
                        u, err := AddressToUpstream(tc.address, &Options{

++++++ dnsproxy.obsinfo ++++++
--- /var/tmp/diff_new_pack.MXUuyi/_old  2024-01-15 22:21:59.220925590 +0100
+++ /var/tmp/diff_new_pack.MXUuyi/_new  2024-01-15 22:21:59.224925736 +0100
@@ -1,5 +1,5 @@
 name: dnsproxy
-version: 0.61.1
-mtime: 1703852828
-commit: a87a3dfd6b737144a16ef92d130319d12184f129
+version: 0.62.0
+mtime: 1705054162
+commit: f1ceef03ad5be7272faeab541b7b71dd18d89f4e
 

++++++ vendor.tar.zstd ++++++
Binary files /var/tmp/diff_new_pack.MXUuyi/_old and 
/var/tmp/diff_new_pack.MXUuyi/_new differ

Reply via email to