diff --git a/go.mod b/go.mod index dd0580e7..4760b5c0 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/AdguardTeam/dnsproxy go 1.24.1 require ( - github.com/AdguardTeam/golibs v0.31.0 + github.com/AdguardTeam/golibs v0.32.6 github.com/ameshkov/dnscrypt/v2 v2.3.0 github.com/ameshkov/dnsstamps v1.0.3 github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 @@ -12,15 +12,17 @@ require ( github.com/patrickmn/go-cache v2.1.0+incompatible github.com/quic-go/quic-go v0.48.2 github.com/stretchr/testify v1.10.0 - golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 golang.org/x/net v0.37.0 golang.org/x/sys v0.31.0 + // TODO(e.burkov): Either find a better maintained dependency or implement + // the weighted random algorithm yourself. gonum.org/v1/gonum v0.15.1 gopkg.in/yaml.v3 v3.0.1 ) require ( - cloud.google.com/go v0.118.3 // indirect + cloud.google.com/go v0.119.0 // indirect cloud.google.com/go/ai v0.10.0 // indirect cloud.google.com/go/auth v0.15.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect @@ -52,6 +54,7 @@ require ( github.com/onsi/ginkgo/v2 v2.22.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.5.1 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/securego/gosec/v2 v2.22.2 // indirect github.com/uudashr/gocognit v1.2.0 // indirect @@ -68,15 +71,15 @@ require ( golang.org/x/mod v0.24.0 // indirect golang.org/x/oauth2 v0.28.0 // indirect golang.org/x/sync v0.12.0 // indirect - golang.org/x/telemetry v0.0.0-20250305155315-2a181eac97a3 // indirect + golang.org/x/telemetry v0.0.0-20250310203348-fdfaad844314 // indirect golang.org/x/term v0.30.0 // indirect golang.org/x/text v0.23.0 // indirect golang.org/x/time v0.11.0 // indirect golang.org/x/tools v0.31.0 // indirect golang.org/x/vuln v1.1.4 // indirect - google.golang.org/api v0.224.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/api v0.225.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250311190419-81fb87f6b8bf // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf // indirect google.golang.org/grpc v1.71.0 // indirect google.golang.org/protobuf v1.36.5 // indirect honnef.co/go/tools v0.6.1 // indirect diff --git a/go.sum b/go.sum index 2a9f624b..5dcc87ca 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cloud.google.com/go v0.118.3 h1:jsypSnrE/w4mJysioGdMBg4MiW/hHx/sArFpaBWHdME= -cloud.google.com/go v0.118.3/go.mod h1:Lhs3YLnBlwJ4KA6nuObNMZ/fCbOQBPuWKPoE0Wa/9Vc= +cloud.google.com/go v0.119.0 h1:tw7OjErMzJKbbjaEHkrt60KQrK5Wus/boCZ7tm5/RNE= +cloud.google.com/go v0.119.0/go.mod h1:fwB8QLzTcNevxqi8dcpR+hoMIs3jBherGS9VUBDAW08= cloud.google.com/go/ai v0.10.0 h1:hwj6CI6sMKubXodoJJGTy/c2T1RbbLGM6TL3QoAvzU8= cloud.google.com/go/ai v0.10.0/go.mod h1:kvnt2KeHqX8+41PVeMRBETDyQAp/RFvBWGdx/aGjNMo= cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= @@ -10,8 +10,8 @@ cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4 cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= cloud.google.com/go/longrunning v0.6.5 h1:sD+t8DO8j4HKW4QfouCklg7ZC1qC4uzVZt8iz3uTW+Q= cloud.google.com/go/longrunning v0.6.5/go.mod h1:Et04XK+0TTLKa5IPYryKf5DkpwImy6TluQ1QTLwlKmY= -github.com/AdguardTeam/golibs v0.31.0 h1:Z0oPfLTLw6iZmpE58dePy2Bel0MaX+lnDwtFEE5EmIo= -github.com/AdguardTeam/golibs v0.31.0/go.mod h1:wIkZ9o2UnppeW6/YD7yJB71dYbMhiuC1Fh/I2ElW7GQ= +github.com/AdguardTeam/golibs v0.32.6 h1:J/s6BiDHIJa5VsumGAzgI6S1SVLOaVB3YIvFlBK79HU= +github.com/AdguardTeam/golibs v0.32.6/go.mod h1:bE8KV1zqTzgZjmjFyBJ9f9O5DEKO717r7e57j1HclJA= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= @@ -94,6 +94,8 @@ github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE= github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/securego/gosec/v2 v2.22.2 h1:IXbuI7cJninj0nRpZSLCUlotsj8jGusohfONMrHoF6g= @@ -129,8 +131,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo= -golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/exp/typeparams v0.0.0-20250305212735-054e65f0b394 h1:VI4qDpTkfFaCXEPrbojidLgVQhj2x4nzTccG0hjaLlU= golang.org/x/exp/typeparams v0.0.0-20250305212735-054e65f0b394/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -154,8 +156,8 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/telemetry v0.0.0-20250305155315-2a181eac97a3 h1:k+pofz4/0MRETtVtItAwfDgPUvNlWrUrFw+8dtUVUa8= -golang.org/x/telemetry v0.0.0-20250305155315-2a181eac97a3/go.mod h1:16eI1RtbPZAEm3u7hpIh7JM/w5AbmlDtnrdKYaREic8= +golang.org/x/telemetry v0.0.0-20250310203348-fdfaad844314 h1:UY+gQAskx5vohcvUlJDKkJPt9lALCgtZs3rs8msRatU= +golang.org/x/telemetry v0.0.0-20250310203348-fdfaad844314/go.mod h1:16eI1RtbPZAEm3u7hpIh7JM/w5AbmlDtnrdKYaREic8= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= @@ -177,12 +179,12 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= -google.golang.org/api v0.224.0 h1:Ir4UPtDsNiwIOHdExr3fAj4xZ42QjK7uQte3lORLJwU= -google.golang.org/api v0.224.0/go.mod h1:3V39my2xAGkodXy0vEqcEtkqgw2GtrFL5WuBZlCTCOQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/api v0.225.0 h1:+4/IVqBQm0MV5S+JW3kdEGC1WtOmM2mXN1LKH1LdNlw= +google.golang.org/api v0.225.0/go.mod h1:WP/0Xm4LVvMOCldfvOISnWquSRWbG2kArDZcg+W2DbY= +google.golang.org/genproto/googleapis/api v0.0.0-20250311190419-81fb87f6b8bf h1:BdIVRm+fyDUn8lrZLPSlBCfM/YKDwUBYgDoLv9+DYo0= +google.golang.org/genproto/googleapis/api v0.0.0-20250311190419-81fb87f6b8bf/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf h1:dHDlF3CWxQkefK9IJx+O8ldY0gLygvrlYRBNbPqDWuY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= diff --git a/proxy/clock.go b/proxy/clock.go deleted file mode 100644 index 3727dbd6..00000000 --- a/proxy/clock.go +++ /dev/null @@ -1,21 +0,0 @@ -package proxy - -import "time" - -// clock is the interface for provider of current time. It's used to simplify -// testing. -// -// TODO(e.burkov): Move to golibs. -type clock interface { - // Now returns the current local time. - Now() (now time.Time) -} - -// type check -var _ clock = realClock{} - -// realClock is the [clock] which actually uses the [time] package. -type realClock struct{} - -// Now implements the [clock] interface for RealClock. -func (realClock) Now() (now time.Time) { return time.Now() } diff --git a/proxy/config.go b/proxy/config.go index cd61a91d..69e4487c 100644 --- a/proxy/config.go +++ b/proxy/config.go @@ -97,6 +97,10 @@ type Config struct { // DNSCrypt server. DNSCryptResolverCert *dnscrypt.Cert + // BindRetryConfig configures the listeners binding retrying. If nil, + // retries are disabled. + BindRetryConfig *BindRetryConfig + // DNSCryptProviderName is the DNSCrypt provider name. Required for // DNSCrypt server. DNSCryptProviderName string diff --git a/proxy/exchange.go b/proxy/exchange.go index c04a6657..a34eaf63 100644 --- a/proxy/exchange.go +++ b/proxy/exchange.go @@ -34,7 +34,7 @@ func (p *Proxy) exchangeUpstreams( if len(ups) == 1 { u = ups[0] - resp, _, err = p.exchange(u, req, p.time) + resp, _, err = p.exchange(u, req) if err != nil { return nil, nil, err } @@ -50,7 +50,7 @@ func (p *Proxy) exchangeUpstreams( u = ups[i] var elapsed time.Duration - resp, elapsed, err = p.exchange(u, req, p.time) + resp, elapsed, err = p.exchange(u, req) if err == nil { p.updateRTT(u.Address(), elapsed) @@ -75,13 +75,12 @@ func (p *Proxy) exchangeUpstreams( func (p *Proxy) exchange( u upstream.Upstream, req *dns.Msg, - c clock, ) (resp *dns.Msg, dur time.Duration, err error) { - startTime := c.Now() + startTime := p.time.Now() resp, err = u.Exchange(req) // Don't use [time.Since] because it uses [time.Now]. - dur = c.Now().Sub(startTime) + dur = p.time.Now().Sub(startTime) addr := u.Address() q := &req.Question[0] diff --git a/proxy/exchange_internal_test.go b/proxy/exchange_internal_test.go index 8cb2c5ef..0c494b06 100644 --- a/proxy/exchange_internal_test.go +++ b/proxy/exchange_internal_test.go @@ -10,22 +10,15 @@ import ( "github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/golibs/logutil/slogutil" "github.com/AdguardTeam/golibs/netutil" + "github.com/AdguardTeam/golibs/testutil/faketime" + "github.com/AdguardTeam/golibs/timeutil" "github.com/miekg/dns" "github.com/stretchr/testify/assert" + + //lint:ignore SA1019 See TODO for the gonum.org/v1/gonum import in go.mod. "golang.org/x/exp/rand" ) -// fakeClock is the function-based implementation of the [clock] interface. -type fakeClock struct { - onNow func() (now time.Time) -} - -// type check -var _ clock = (*fakeClock)(nil) - -// Now implements the [clock] interface for *fakeClock. -func (c *fakeClock) Now() (now time.Time) { return c.onNow() } - // newUpstreamWithErrorRate returns an [upstream.Upstream] that responds with an // error every [rate] requests. The returned upstream isn't safe for concurrent // use. @@ -80,15 +73,15 @@ func TestProxy_Exchange_loadBalance(t *testing.T) { // value until currentNow is modified elsewhere. zeroTime := time.Unix(0, 0) currentNow := zeroTime - zeroingClock := &fakeClock{ - onNow: func() (now time.Time) { + zeroingClock := &faketime.Clock{ + OnNow: func() (now time.Time) { now, currentNow = currentNow, zeroTime return now }, } - constClock := &fakeClock{ - onNow: func() (now time.Time) { + constClock := &faketime.Clock{ + OnNow: func() (now time.Time) { now, currentNow = currentNow, currentNow.Add(testRTT/50) return now @@ -153,7 +146,7 @@ func TestProxy_Exchange_loadBalance(t *testing.T) { testCases := []struct { wantStat map[string]int64 - clock clock + clock timeutil.Clock name string servers []upstream.Upstream }{{ diff --git a/proxy/proxy.go b/proxy/proxy.go index 8367b54a..6c22e0e4 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -25,11 +25,14 @@ import ( "github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/service" "github.com/AdguardTeam/golibs/syncutil" + "github.com/AdguardTeam/golibs/timeutil" "github.com/ameshkov/dnscrypt/v2" "github.com/miekg/dns" gocache "github.com/patrickmn/go-cache" "github.com/quic-go/quic-go" "github.com/quic-go/quic-go/http3" + + //lint:ignore SA1019 See TODO for the gonum.org/v1/gonum import in go.mod. "golang.org/x/exp/rand" ) @@ -77,7 +80,7 @@ type Proxy struct { // time provides the current time. // // TODO(e.burkov): Consider configuring it. - time clock + time timeutil.Clock // randSrc provides the source of randomness. // @@ -178,6 +181,14 @@ type Proxy struct { // udpOOBSize is the size of the out-of-band data for UDP connections. udpOOBSize int + // bindRetryNum is the number of retries for binding to an address for + // listening. Zero means one attempt and no retries. + bindRetryNum uint + + // bindRetryIvl is the interval between attempts to bind to an address for + // listening. + bindRetryIvl time.Duration + // counter counts message contexts created with [Proxy.newDNSContext]. counter atomic.Uint64 @@ -226,7 +237,7 @@ func New(c *Config) (p *Proxy, err error) { }, }, udpOOBSize: proxynetutil.UDPGetOOBSize(), - time: realClock{}, + time: timeutil.SystemClock{}, messages: cmp.Or[MessageConstructor]( c.MessageConstructor, dnsmsg.DefaultMessageConstructor{}, @@ -263,15 +274,19 @@ func New(c *Config) (p *Proxy, err error) { p.requestsSema = syncutil.EmptySemaphore{} } - if p.UpstreamMode == "" { - p.UpstreamMode = UpstreamModeLoadBalance - } else if p.UpstreamMode == UpstreamModeFastestAddr { + p.UpstreamMode = cmp.Or(p.UpstreamMode, UpstreamModeLoadBalance) + if p.UpstreamMode == UpstreamModeFastestAddr { p.fastestAddr = fastip.New(&fastip.Config{ Logger: p.Logger, PingWaitTimeout: p.FastestPingTimeout, }) } + if bindRetries := c.BindRetryConfig; bindRetries != nil && bindRetries.Enabled { + p.bindRetryNum = bindRetries.Count + p.bindRetryIvl = bindRetries.Interval + } + err = p.setupDNS64() if err != nil { return nil, fmt.Errorf("setting up DNS64: %w", err) @@ -337,6 +352,14 @@ func (p *Proxy) Start(ctx context.Context) (err error) { return nil } +// logClose closes the closer and logs the error at the specified level if it +// occurs. +func (p *Proxy) logClose(ctx context.Context, l slog.Level, c io.Closer, msg string, args ...any) { + if err := c.Close(); err != nil { + p.logger.Log(ctx, l, msg, append(args, slogutil.KeyError, err)...) + } +} + // closeAll closes all closers and appends the occurred errors to errs. func closeAll[C io.Closer](errs []error, closers ...C) (appended []error) { for _, c := range closers { diff --git a/proxy/retry.go b/proxy/retry.go new file mode 100644 index 00000000..64a81e8d --- /dev/null +++ b/proxy/retry.go @@ -0,0 +1,47 @@ +package proxy + +import ( + "context" + "time" + + "github.com/AdguardTeam/golibs/logutil/slogutil" +) + +// BindRetryConfig contains configuration for the listeners binding retry +// mechanism. +type BindRetryConfig struct { + // Interval is the minimum time to wait after the latest failure. It must + // not be negative if Enabled is true. + Interval time.Duration + + // Count is the maximum number of retries after the first attempt. + Count uint + + // Enabled indicates whether the binding should be retried. + Enabled bool +} + +// bindWithRetry calls f until it returns no error or the retries limit is +// reached, sleeping for configured interval between attempts. bindFunc must +// not be nil and should carry the result of the binding operation itself. +func (p *Proxy) bindWithRetry(ctx context.Context, bindFunc func() (err error)) (err error) { + err = bindFunc() + if err == nil { + return nil + } + + p.logger.WarnContext(ctx, "binding", "attempt", 1, slogutil.KeyError, err) + + for attempt := uint(1); attempt <= p.bindRetryNum; attempt++ { + time.Sleep(p.bindRetryIvl) + + retryErr := bindFunc() + if retryErr == nil { + return nil + } + + p.logger.WarnContext(ctx, "binding", "attempt", attempt+1, slogutil.KeyError, retryErr) + } + + return err +} diff --git a/proxy/retry_internal_test.go b/proxy/retry_internal_test.go new file mode 100644 index 00000000..23ec9c23 --- /dev/null +++ b/proxy/retry_internal_test.go @@ -0,0 +1,94 @@ +package proxy + +import ( + "testing" + + "github.com/AdguardTeam/golibs/errors" + "github.com/AdguardTeam/golibs/logutil/slogutil" + "github.com/AdguardTeam/golibs/testutil" + "github.com/stretchr/testify/assert" +) + +func TestWithRetry(t *testing.T) { + t.Parallel() + + const ( + errA errors.Error = "error about a" + errB errors.Error = "error about b" + ) + + var ( + good = func() (err error) { + return nil + } + + badOne = func() (err error) { + return errA + } + + // Don't protect against concurrent access since the closure is expected + // to be used in a single case. + returnedA = false + badBoth = func() (err error) { + if !returnedA { + returnedA = true + + return errA + } + + return errB + } + + // Don't protect against concurrent access since the closure is expected + // to be used in a single case. + returnedErr = false + badThenOk = func() (err error) { + if !returnedErr { + returnedErr = true + + return assert.AnError + } + + return nil + } + ) + + testCases := []struct { + f func() (err error) + wantErr error + name string + }{{ + f: good, + wantErr: nil, + name: "no_error", + }, { + f: badOne, + wantErr: errA, + name: "one_error", + }, { + f: badBoth, + wantErr: errA, + name: "two_errors", + }, { + f: badThenOk, + wantErr: nil, + name: "error_then_ok", + }} + + p := &Proxy{ + logger: slogutil.NewDiscardLogger(), + bindRetryNum: 1, + bindRetryIvl: 0, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx := testutil.ContextWithTimeout(t, testTimeout) + + err := p.bindWithRetry(ctx, tc.f) + assert.ErrorIs(t, err, tc.wantErr) + }) + } +} diff --git a/proxy/server.go b/proxy/server.go index 19f439eb..646c5238 100644 --- a/proxy/server.go +++ b/proxy/server.go @@ -17,32 +17,32 @@ import ( // configureListeners configures listeners. func (p *Proxy) configureListeners(ctx context.Context) (err error) { - err = p.createUDPListeners(ctx) + err = p.initUDPListeners(ctx) if err != nil { return err } - err = p.createTCPListeners(ctx) + err = p.initTCPListeners(ctx) if err != nil { return err } - err = p.createTLSListeners() + err = p.initTLSListeners(ctx) if err != nil { return err } - err = p.createHTTPSListeners() + err = p.initHTTPSListeners(ctx) if err != nil { return err } - err = p.createQUICListeners() + err = p.initQUICListeners(ctx) if err != nil { return err } - err = p.createDNSCryptListeners() + err = p.initDNSCryptListeners(ctx) if err != nil { return err } diff --git a/proxy/server_dnscrypt.go b/proxy/server_dnscrypt.go index ece958da..0f758655 100644 --- a/proxy/server_dnscrypt.go +++ b/proxy/server_dnscrypt.go @@ -13,7 +13,7 @@ import ( "github.com/miekg/dns" ) -func (p *Proxy) createDNSCryptListeners() (err error) { +func (p *Proxy) initDNSCryptListeners(ctx context.Context) (err error) { if len(p.DNSCryptUDPListenAddr) == 0 && len(p.DNSCryptTCPListenAddr) == 0 { // Do nothing if DNSCrypt listen addresses are not specified. return nil @@ -23,42 +23,82 @@ func (p *Proxy) createDNSCryptListeners() (err error) { return errors.Error("invalid dnscrypt configuration: no certificate or provider name") } - p.logger.Info("initializing dnscrypt", "provider", p.DNSCryptProviderName) + p.logger.InfoContext(ctx, "initializing dnscrypt", "provider", p.DNSCryptProviderName) p.dnsCryptServer = &dnscrypt.Server{ ProviderName: p.DNSCryptProviderName, ResolverCert: p.DNSCryptResolverCert, Handler: &dnsCryptHandler{ - proxy: p, - + proxy: p, reqSema: p.requestsSema, }, } - for _, a := range p.DNSCryptUDPListenAddr { - p.logger.Info("creating dnscrypt udp listener") - udpListen, lErr := net.ListenUDP(bootstrap.NetworkUDP, a) + for _, addr := range p.DNSCryptUDPListenAddr { + udp, lErr := p.listenDNSCryptUDP(ctx, addr) if lErr != nil { - return fmt.Errorf("listening to dnscrypt udp socket: %w", lErr) + return fmt.Errorf("listening to dnscrypt udp on addr %s: %w", addr, lErr) } - p.dnsCryptUDPListen = append(p.dnsCryptUDPListen, udpListen) - p.logger.Info("listening for dnscrypt messages on udp", "addr", udpListen.LocalAddr()) + p.dnsCryptUDPListen = append(p.dnsCryptUDPListen, udp) } - for _, a := range p.DNSCryptTCPListenAddr { - p.logger.Info("creating a dnscrypt tcp listener") - tcpListen, lErr := net.ListenTCP(bootstrap.NetworkTCP, a) + for _, addr := range p.DNSCryptTCPListenAddr { + tcp, lErr := p.listenDNSCryptTCP(ctx, addr) if lErr != nil { - return fmt.Errorf("listening to dnscrypt tcp socket: %w", lErr) + return fmt.Errorf("listening to dnscrypt tcp on addr %s: %w", addr, lErr) } - p.dnsCryptTCPListen = append(p.dnsCryptTCPListen, tcpListen) - p.logger.Info("listening for dnscrypt messages on tcp", "addr", tcpListen.Addr()) + p.dnsCryptTCPListen = append(p.dnsCryptTCPListen, tcp) } return nil } +// listenDNSCryptUDP returns a new UDP connection for DNSCrypt listening on +// addr. +func (p *Proxy) listenDNSCryptUDP( + ctx context.Context, + addr *net.UDPAddr, +) (conn *net.UDPConn, err error) { + addrStr := addr.String() + p.logger.InfoContext(ctx, "creating dnscrypt udp server socket", "addr", addrStr) + + err = p.bindWithRetry(ctx, func() (listenErr error) { + conn, listenErr = net.ListenUDP(bootstrap.NetworkUDP, addr) + + return listenErr + }) + if err != nil { + return nil, fmt.Errorf("listening to udp socket: %w", err) + } + + p.logger.InfoContext(ctx, "listening for dnscrypt messages on udp", "addr", conn.LocalAddr()) + + return conn, nil +} + +// listenDNSCryptTCP returns a new TCP listener for DNSCrypt listening on addr. +func (p *Proxy) listenDNSCryptTCP( + ctx context.Context, + addr *net.TCPAddr, +) (conn *net.TCPListener, err error) { + addrStr := addr.String() + p.logger.InfoContext(ctx, "creating dnscrypt tcp server socket", "addr", addrStr) + + err = p.bindWithRetry(ctx, func() (listenErr error) { + conn, listenErr = net.ListenTCP(bootstrap.NetworkTCP, addr) + + return listenErr + }) + if err != nil { + return nil, fmt.Errorf("listening to tcp socket: %w", err) + } + + p.logger.InfoContext(ctx, "listening for dnscrypt messages on tcp", "addr", conn.Addr()) + + return conn, nil +} + // dnsCryptHandler - dnscrypt.Handler implementation type dnsCryptHandler struct { proxy *Proxy diff --git a/proxy/server_https.go b/proxy/server_https.go index 90c84f3d..36677cd0 100644 --- a/proxy/server_https.go +++ b/proxy/server_https.go @@ -25,42 +25,56 @@ import ( // listenHTTP creates instances of TLS listeners that will be used to run an // H1/H2 server. Returns the address the listener actually listens to (useful // in the case if port 0 is specified). -func (p *Proxy) listenHTTP(addr *net.TCPAddr) (laddr *net.TCPAddr, err error) { - tcpListen, err := net.ListenTCP(bootstrap.NetworkTCP, addr) +func (p *Proxy) listenHTTP( + ctx context.Context, + addr *net.TCPAddr, +) (ln net.Listener, tcpAddr *net.TCPAddr, err error) { + var tcpListen *net.TCPListener + err = p.bindWithRetry(ctx, func() (listenErr error) { + tcpListen, listenErr = net.ListenTCP(bootstrap.NetworkTCP, addr) + + return listenErr + }) if err != nil { - return nil, fmt.Errorf("tcp listener: %w", err) + return nil, nil, fmt.Errorf("tcp listener: %w", err) } - p.logger.Info("listening to https", "addr", tcpListen.Addr()) + laddr := tcpListen.Addr() + tcpAddr, ok := laddr.(*net.TCPAddr) + if !ok { + return nil, nil, fmt.Errorf("bad listener address type: %T", laddr) + } + + p.logger.InfoContext(ctx, "listening to https", "addr", tcpAddr) tlsConfig := p.TLSConfig.Clone() tlsConfig.NextProtos = []string{http2.NextProtoTLS, "http/1.1"} tlsListen := tls.NewListener(tcpListen, tlsConfig) - p.httpsListen = append(p.httpsListen, tlsListen) - return tcpListen.Addr().(*net.TCPAddr), nil + return tlsListen, tcpAddr, nil } // listenH3 creates instances of QUIC listeners that will be used for running // an HTTP/3 server. -func (p *Proxy) listenH3(addr *net.UDPAddr) (err error) { +func (p *Proxy) listenH3( + ctx context.Context, + addr *net.UDPAddr, +) (ln *quic.EarlyListener, err error) { tlsConfig := p.TLSConfig.Clone() tlsConfig.NextProtos = []string{"h3"} quicListen, err := quic.ListenAddrEarly(addr.String(), tlsConfig, newServerQUICConfig()) if err != nil { - return fmt.Errorf("quic listener: %w", err) + return nil, fmt.Errorf("quic listener: %w", err) } - p.logger.Info("listening to h3", "addr", quicListen.Addr()) + p.logger.InfoContext(ctx, "listening to h3", "addr", quicListen.Addr()) - p.h3Listen = append(p.h3Listen, quicListen) - - return nil + return quicListen, nil } -// createHTTPSListeners creates TCP/UDP listeners and HTTP/H3 servers. -func (p *Proxy) createHTTPSListeners() (err error) { +// initHTTPSListeners creates TCP/UDP listeners and HTTP/H3 servers. +func (p *Proxy) initHTTPSListeners(ctx context.Context) (err error) { p.httpsServer = &http.Server{ Handler: p, ReadHeaderTimeout: defaultTimeout, @@ -74,21 +88,27 @@ func (p *Proxy) createHTTPSListeners() (err error) { } for _, addr := range p.HTTPSListenAddr { - p.logger.Info("creating an https server") + p.logger.InfoContext(ctx, "creating an https server") - tcpAddr, lErr := p.listenHTTP(addr) + ln, tcpAddr, lErr := p.listenHTTP(ctx, addr) if lErr != nil { return fmt.Errorf("failed to start HTTPS server on %s: %w", addr, lErr) } + p.httpsListen = append(p.httpsListen, ln) + if p.HTTP3 { // HTTP/3 server listens to the same pair IP:port as the one HTTP/2 // server listens to. udpAddr := &net.UDPAddr{IP: tcpAddr.IP, Port: tcpAddr.Port} - err = p.listenH3(udpAddr) + + var quicListen *quic.EarlyListener + quicListen, err = p.listenH3(ctx, udpAddr) if err != nil { return fmt.Errorf("failed to start HTTP/3 server on %s: %w", udpAddr, err) } + + p.h3Listen = append(p.h3Listen, quicListen) } } diff --git a/proxy/server_quic.go b/proxy/server_quic.go index e76369a1..faa1388e 100644 --- a/proxy/server_quic.go +++ b/proxy/server_quic.go @@ -62,43 +62,63 @@ const ( DoQCodeProtocolError quic.ApplicationErrorCode = 2 ) -// createQUICListeners creates QUIC listeners for the DoQ server. -func (p *Proxy) createQUICListeners() error { +// initQUICListeners creates QUIC listeners for the DoQ server. +func (p *Proxy) initQUICListeners(ctx context.Context) (err error) { for _, a := range p.QUICListenAddr { - p.logger.Info("creating quic listener", "addr", a) - - conn, err := net.ListenUDP(bootstrap.NetworkUDP, a) + var conn *net.UDPConn + var ln *quic.EarlyListener + var tr *quic.Transport + conn, ln, tr, err = p.listenQUIC(ctx, a) if err != nil { - return fmt.Errorf("listening to %s: %w", a, err) + return fmt.Errorf("listening on quic addr %s: %w", a, err) } p.quicConns = append(p.quicConns, conn) - - v := newQUICAddrValidator(quicAddrValidatorCacheSize, quicAddrValidatorCacheTTL) - transport := &quic.Transport{ - Conn: conn, - VerifySourceAddress: v.requiresValidation, - } - - tlsConfig := p.TLSConfig.Clone() - tlsConfig.NextProtos = compatProtoDQ - quicListen, err := transport.ListenEarly( - tlsConfig, - newServerQUICConfig(), - ) - if err != nil { - return fmt.Errorf("quic listener: %w", err) - } - - p.quicTransports = append(p.quicTransports, transport) - p.quicListen = append(p.quicListen, quicListen) - - p.logger.Info("listening quic", "addr", quicListen.Addr()) + p.quicTransports = append(p.quicTransports, tr) + p.quicListen = append(p.quicListen, ln) } return nil } +// listenQUIC returns a new UDP connection listening on addr, the QUIC listener +// utilizing it, and the associated QUIC transport. +func (p *Proxy) listenQUIC( + ctx context.Context, + addr *net.UDPAddr, +) (conn *net.UDPConn, l *quic.EarlyListener, tr *quic.Transport, err error) { + p.logger.InfoContext(ctx, "creating quic listener", "addr", addr) + + err = p.bindWithRetry(ctx, func() (listenErr error) { + conn, listenErr = net.ListenUDP(bootstrap.NetworkUDP, addr) + + return listenErr + }) + if err != nil { + return nil, nil, nil, fmt.Errorf("listening to udp socket: %w", err) + } + + v := newQUICAddrValidator(quicAddrValidatorCacheSize, quicAddrValidatorCacheTTL) + tr = &quic.Transport{ + Conn: conn, + VerifySourceAddress: v.requiresValidation, + } + + tlsConfig := p.TLSConfig.Clone() + tlsConfig.NextProtos = compatProtoDQ + l, err = tr.ListenEarly( + tlsConfig, + newServerQUICConfig(), + ) + if err != nil { + return nil, nil, nil, fmt.Errorf("listening early: %w", err) + } + + p.logger.InfoContext(ctx, "listening quic", "addr", l.Addr()) + + return conn, l, tr, nil +} + // quicPacketLoop listens for incoming QUIC packets. // // See also the comment on Proxy.requestsSema. diff --git a/proxy/server_tcp.go b/proxy/server_tcp.go index bccb3928..8cb9fe65 100644 --- a/proxy/server_tcp.go +++ b/proxy/server_tcp.go @@ -18,46 +18,70 @@ import ( "github.com/miekg/dns" ) -func (p *Proxy) createTCPListeners(ctx context.Context) (err error) { - for _, a := range p.TCPListenAddr { - p.logger.Info("creating tcp server socket", "addr", a) - - lsnr, lErr := proxynetutil.ListenConfig(p.logger).Listen( - ctx, - bootstrap.NetworkTCP, - a.String(), - ) - if lErr != nil { - return fmt.Errorf("listening to tcp socket: %w", lErr) +// initTCPListeners initializes TCP listeners with configured addresses. +func (p *Proxy) initTCPListeners(ctx context.Context) (err error) { + for _, addr := range p.TCPListenAddr { + var ln *net.TCPListener + ln, err = p.listenTCP(ctx, addr) + if err != nil { + return fmt.Errorf("listening on tcp addr %s: %w", addr, err) } - tcpListener, ok := lsnr.(*net.TCPListener) - if !ok { - return fmt.Errorf("wrong listener type on tcp addr %s: %T", a, lsnr) - } - - p.tcpListen = append(p.tcpListen, tcpListener) - - p.logger.Info("listening to tcp", "addr", tcpListener.Addr()) + p.tcpListen = append(p.tcpListen, ln) } return nil } -func (p *Proxy) createTLSListeners() (err error) { - for _, a := range p.TLSListenAddr { - p.logger.Info("creating tls server socket", "addr", a) +// listenTCP returns a new TCP listener listening on addr. +func (p *Proxy) listenTCP(ctx context.Context, addr *net.TCPAddr) (ln *net.TCPListener, err error) { + addrStr := addr.String() + p.logger.InfoContext(ctx, "creating tcp server socket", "addr", addrStr) + + conf := proxynetutil.ListenConfig(p.logger) + + var listener net.Listener + err = p.bindWithRetry(ctx, func() (listenErr error) { + listener, listenErr = conf.Listen(ctx, bootstrap.NetworkTCP, addrStr) + + return listenErr + }) + if err != nil { + return nil, fmt.Errorf("listening to tcp socket: %w", err) + } + + var ok bool + ln, ok = listener.(*net.TCPListener) + if !ok { + // TODO(e.burkov): Close the listener. + + return nil, fmt.Errorf("bad listener type: %T", listener) + } + + p.logger.InfoContext(ctx, "listening to tcp", "addr", ln.Addr()) + + return ln, nil +} + +// initTLSListeners initializes TLS listeners with configured addresses. +func (p *Proxy) initTLSListeners(ctx context.Context) (err error) { + for _, addr := range p.TLSListenAddr { + p.logger.InfoContext(ctx, "creating tls server socket", "addr", addr) var tcpListen *net.TCPListener - tcpListen, err = net.ListenTCP("tcp", a) + err = p.bindWithRetry(ctx, func() (listenErr error) { + tcpListen, listenErr = net.ListenTCP("tcp", addr) + + return listenErr + }) if err != nil { - return fmt.Errorf("listening on tls addr %s: %w", a, err) + return fmt.Errorf("listening on tls addr %s: %w", addr, err) } l := tls.NewListener(tcpListen, p.TLSConfig) p.tlsListen = append(p.tlsListen, l) - p.logger.Info("listening to tls", "addr", l.Addr()) + p.logger.InfoContext(ctx, "listening to tls", "addr", l.Addr()) } return nil diff --git a/proxy/server_udp.go b/proxy/server_udp.go index 41989995..f9e1c3d1 100644 --- a/proxy/server_udp.go +++ b/proxy/server_udp.go @@ -16,10 +16,11 @@ import ( "github.com/miekg/dns" ) -func (p *Proxy) createUDPListeners(ctx context.Context) (err error) { +// initUDPListeners initializes UDP listeners with configured addresses. +func (p *Proxy) initUDPListeners(ctx context.Context) (err error) { for _, a := range p.UDPListenAddr { var pc *net.UDPConn - pc, sErr := p.udpCreate(ctx, a) + pc, sErr := p.listenUDP(ctx, a) if sErr != nil { return fmt.Errorf("listening on udp addr %s: %w", a, sErr) } @@ -30,39 +31,52 @@ func (p *Proxy) createUDPListeners(ctx context.Context) (err error) { return nil } -// udpCreate - create a UDP listening socket -func (p *Proxy) udpCreate(ctx context.Context, udpAddr *net.UDPAddr) (*net.UDPConn, error) { - p.logger.InfoContext(ctx, "creating udp server socket", "addr", udpAddr) +// listenUDP returns a new UDP connection listening on addr. +func (p *Proxy) listenUDP(ctx context.Context, addr *net.UDPAddr) (conn *net.UDPConn, err error) { + addrStr := addr.String() + p.logger.InfoContext(ctx, "creating udp server socket", "addr", addrStr) - packetConn, err := proxynetutil.ListenConfig(p.logger).ListenPacket( - ctx, - bootstrap.NetworkUDP, - udpAddr.String(), - ) + conf := proxynetutil.ListenConfig(p.logger) + + var packetConn net.PacketConn + err = p.bindWithRetry(ctx, func() (listenErr error) { + packetConn, listenErr = conf.ListenPacket(ctx, bootstrap.NetworkUDP, addrStr) + + return listenErr + }) if err != nil { return nil, fmt.Errorf("listening to udp socket: %w", err) } - udpListen := packetConn.(*net.UDPConn) + // TODO(e.burkov): Use [errors.WithDeferred] for closing errors. + + var ok bool + conn, ok = packetConn.(*net.UDPConn) + if !ok { + // TODO(e.burkov): Close the connection. + + return nil, fmt.Errorf("bad conn type: %T(%[1]v)", packetConn) + } + if p.Config.UDPBufferSize > 0 { - err = udpListen.SetReadBuffer(p.Config.UDPBufferSize) + err = conn.SetReadBuffer(p.Config.UDPBufferSize) if err != nil { - _ = udpListen.Close() + p.logClose(ctx, slog.LevelDebug, conn, "closing after failed read buffer size setting") return nil, fmt.Errorf("setting udp buf size: %w", err) } } - err = proxynetutil.UDPSetOptions(udpListen) + err = proxynetutil.UDPSetOptions(conn) if err != nil { - _ = udpListen.Close() + p.logClose(ctx, slog.LevelDebug, conn, "closing after failed options setting") return nil, fmt.Errorf("setting udp opts: %w", err) } - p.logger.InfoContext(ctx, "listening to udp", "addr", udpListen.LocalAddr()) + p.logger.InfoContext(ctx, "listening to udp", "addr", conn.LocalAddr()) - return udpListen, nil + return conn, nil } // udpPacketLoop listens for incoming UDP packets.