diff --git a/cmd/ctrld/netlink_linux.go b/cmd/ctrld/netlink_linux.go new file mode 100644 index 0000000..86eb45b --- /dev/null +++ b/cmd/ctrld/netlink_linux.go @@ -0,0 +1,27 @@ +package main + +import ( + "github.com/vishvananda/netlink" + "golang.org/x/sys/unix" +) + +func (p *prog) watchLinkState() { + ch := make(chan netlink.LinkUpdate) + done := make(chan struct{}) + defer close(done) + if err := netlink.LinkSubscribe(ch, done); err != nil { + mainLog.Warn().Err(err).Msg("could not subscribe link") + return + } + for lu := range ch { + if lu.Change == 0xFFFFFFFF { + continue + } + if lu.Change&unix.IFF_UP != 0 { + mainLog.Debug().Msgf("link state changed, re-bootstrapping") + for _, uc := range p.cfg.Upstream { + uc.ReBootstrap() + } + } + } +} diff --git a/cmd/ctrld/netlink_others.go b/cmd/ctrld/netlink_others.go new file mode 100644 index 0000000..d069661 --- /dev/null +++ b/cmd/ctrld/netlink_others.go @@ -0,0 +1,5 @@ +//go:build !linux + +package main + +func (p *prog) watchLinkState() {} diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index 9aba028..a170b36 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -82,6 +82,8 @@ func (p *prog) run() { uc.SetupTransport() } + go p.watchLinkState() + for listenerNum := range p.cfg.Listener { p.cfg.Listener[listenerNum].Init() go func(listenerNum string) { diff --git a/config.go b/config.go index 9257351..4eb4e56 100644 --- a/config.go +++ b/config.go @@ -9,6 +9,7 @@ import ( "net/url" "os" "strings" + "sync" "sync/atomic" "time" @@ -127,6 +128,7 @@ type UpstreamConfig struct { u *url.URL `mapstructure:"-" toml:"-"` g singleflight.Group + mu sync.Mutex bootstrapIPs []string nextBootstrapIP atomic.Uint32 } @@ -268,6 +270,8 @@ func (uc *UpstreamConfig) setupDOHTransport() { } func (uc *UpstreamConfig) setupDOHTransportWithoutPingUpstream() { + uc.mu.Lock() + defer uc.mu.Unlock() uc.transport = http.DefaultTransport.(*http.Transport).Clone() uc.transport.IdleConnTimeout = 5 * time.Second uc.transport.TLSClientConfig = &tls.Config{RootCAs: uc.certPool} diff --git a/config_quic.go b/config_quic.go index 4b9f4c9..9c6d668 100644 --- a/config_quic.go +++ b/config_quic.go @@ -19,6 +19,8 @@ func (uc *UpstreamConfig) setupDOH3Transport() { } func (uc *UpstreamConfig) setupDOH3TransportWithoutPingUpstream() { + uc.mu.Lock() + defer uc.mu.Unlock() rt := &http3.RoundTripper{} rt.TLSClientConfig = &tls.Config{RootCAs: uc.certPool} rt.Dial = func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { diff --git a/doh.go b/doh.go index 76f81ea..000cc49 100644 --- a/doh.go +++ b/doh.go @@ -8,8 +8,6 @@ import ( "io" "net/http" "net/url" - "runtime" - "time" "github.com/miekg/dns" ) @@ -23,10 +21,13 @@ const ( ) func newDohResolver(uc *UpstreamConfig) *dohResolver { + uc.mu.Lock() + transport := uc.transport + uc.mu.Unlock() r := &dohResolver{ endpoint: uc.u, isDoH3: uc.Type == ResolverTypeDOH3, - transport: uc.transport, + transport: transport, http3RoundTripper: uc.http3RoundTripper, sendClientInfo: uc.UpstreamSendClientInfo(), uc: uc, @@ -61,12 +62,14 @@ func (r *dohResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, erro } addHeader(ctx, req, r.sendClientInfo) - var resp *http.Response - if runtime.GOOS == "linux" { - resp, err = r.doRequestWithFailover(req) - } else { - resp, err = r.doRequest(req) + c := http.Client{Transport: r.transport} + if r.isDoH3 { + if r.http3RoundTripper == nil { + return nil, errors.New("DoH3 is not supported") + } + c.Transport = r.http3RoundTripper } + resp, err := c.Do(req) if err != nil { if r.isDoH3 { if closer, ok := r.http3RoundTripper.(io.Closer); ok { @@ -93,41 +96,6 @@ func (r *dohResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, erro return answer, nil } -func (r *dohResolver) doRequest(req *http.Request) (*http.Response, error) { - c := http.Client{Transport: r.transport} - if r.isDoH3 { - if r.http3RoundTripper == nil { - return nil, errors.New("DoH3 is not supported") - } - c.Transport = r.http3RoundTripper - } - return c.Do(req) -} - -const failoverTimeout = 500 * time.Millisecond - -// doRequestWithFailover is like doRequest, but wrap the request with initial timeout. -// If the first request failed, it's likely that the transport was broken, then trigger -// re-bootstrapping and retry the request. -func (r *dohResolver) doRequestWithFailover(req *http.Request) (*http.Response, error) { - c := http.Client{Transport: r.transport} - if r.isDoH3 { - if r.http3RoundTripper == nil { - return nil, errors.New("DoH3 is not supported") - } - c.Transport = r.http3RoundTripper - } - ctx, cancel := context.WithTimeout(context.Background(), failoverTimeout) - defer cancel() - resp, err := c.Do(req.WithContext(ctx)) - if err == nil { - return resp, err - } - r.uc.ReBootstrap() - c.Transport = r.uc.transport - return c.Do(req) -} - func addHeader(ctx context.Context, req *http.Request, sendClientInfo bool) { req.Header.Set("Content-Type", headerApplicationDNS) req.Header.Set("Accept", headerApplicationDNS) diff --git a/go.mod b/go.mod index 1624260..1d1788c 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/spf13/cobra v1.4.0 github.com/spf13/viper v1.14.0 github.com/stretchr/testify v1.8.1 + github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54 golang.org/x/net v0.7.0 golang.org/x/sync v0.1.0 golang.org/x/sys v0.5.0 @@ -65,6 +66,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.1 // indirect github.com/u-root/uio v0.0.0-20221213070652-c3537552635f // indirect + github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect golang.org/x/crypto v0.6.0 // indirect golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect diff --git a/go.sum b/go.sum index c3cb4be..6aedff1 100644 --- a/go.sum +++ b/go.sum @@ -282,6 +282,11 @@ github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/u-root/uio v0.0.0-20221213070652-c3537552635f h1:dpx1PHxYqAnXzbryJrWP1NQLzEjwcVgFLhkknuFQ7ww= github.com/u-root/uio v0.0.0-20221213070652-c3537552635f/go.mod h1:IogEAUBXDEwX7oR/BMmCctShYs80ql4hF0ySdzGxf7E= +github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54 h1:8mhqcHPqTMhSPoslhGYihEgSfc77+7La1P6kiB6+9So= +github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= +github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -429,6 +434,7 @@ golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -437,6 +443,7 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=