Pull request 394: bind-retry

Updates AdGuardTeam/AdGuardDNSClient#11.

Squashed commit of the following:

commit ce7cbf5dbfec6e075a2bc4da1d84aefcb90b2899
Author: Eugene Burkov <[email protected]>
Date:   Tue Mar 18 16:10:43 2025 +0300

    proxy: imp code

commit cb0f42b79a44b581597d98374fbf0155a2684360
Author: Eugene Burkov <[email protected]>
Date:   Mon Mar 17 20:16:08 2025 +0300

    proxy: imp docs

commit 87bb259cbfd166d1d58dc4e471163b5c5ea9261f
Author: Eugene Burkov <[email protected]>
Date:   Mon Mar 17 13:55:56 2025 +0300

    proxy: imp alignment

commit 88e883fedff4b558f28e61cef3ce67004bf59a34
Author: Eugene Burkov <[email protected]>
Date:   Fri Mar 14 19:03:31 2025 +0300

    proxy: upd golibs

commit f4edce5a400c98f4ff6d369f47477f4868bcf8f9
Author: Eugene Burkov <[email protected]>
Date:   Fri Mar 14 18:36:31 2025 +0300

    proxy: imp names, doc

commit aafb4e004c79f79179e93f455cab41c2f17d5f81
Author: Eugene Burkov <[email protected]>
Date:   Fri Mar 14 18:06:32 2025 +0300

    proxy: imp code

commit c3db3d09369e39cf45edba8396692c57a9335187
Author: Eugene Burkov <[email protected]>
Date:   Thu Mar 13 20:57:39 2025 +0300

    proxy: add bind retry
This commit is contained in:
Eugene Burkov
2025-03-18 21:26:58 +03:00
parent 7ca733976b
commit d64332fbad
15 changed files with 438 additions and 176 deletions
+10 -7
View File
@@ -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
+16 -14
View File
@@ -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=
-21
View File
@@ -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() }
+4
View File
@@ -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
+4 -5
View File
@@ -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]
+9 -16
View File
@@ -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
}{{
+28 -5
View File
@@ -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 {
+47
View File
@@ -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
}
+94
View File
@@ -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)
})
}
}
+6 -6
View File
@@ -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
}
+56 -16
View File
@@ -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
+37 -17
View File
@@ -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)
}
}
+47 -27
View File
@@ -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.
+49 -25
View File
@@ -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
+31 -17
View File
@@ -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.