From e095779078428b38e2dfeda83c8f15151bf3f855 Mon Sep 17 00:00:00 2001 From: folbrich Date: Mon, 7 Jul 2025 10:16:49 +0200 Subject: [PATCH 1/2] Update quic-go to v0.53.0 --- dohclient.go | 71 +++++++++++++++++++++------------------------------- go.mod | 12 +++------ go.sum | 34 ++++++------------------- 3 files changed, 40 insertions(+), 77 deletions(-) diff --git a/dohclient.go b/dohclient.go index 3eedca29..5e5ac5d3 100644 --- a/dohclient.go +++ b/dohclient.go @@ -304,11 +304,11 @@ func dohQuicTransport(endpoint string, opt DoHClientOptions) (http.RoundTripper, lAddr = opt.LocalAddr } - dialer := func(ctx context.Context, addr string, tlsConfig *tls.Config, config *quic.Config) (quic.EarlyConnection, error) { + dialer := func(ctx context.Context, addr string, tlsConfig *tls.Config, config *quic.Config) (*quic.Conn, error) { return newQuicConnection(u.Hostname(), addr, lAddr, tlsConfig, config, opt.Use0RTT) } if opt.BootstrapAddr != "" { - dialer = func(ctx context.Context, addr string, tlsConfig *tls.Config, config *quic.Config) (quic.EarlyConnection, error) { + dialer = func(ctx context.Context, addr string, tlsConfig *tls.Config, config *quic.Config) (*quic.Conn, error) { _, port, err := net.SplitHostPort(addr) if err != nil { return nil, err @@ -333,7 +333,7 @@ func dohQuicTransport(endpoint string, opt DoHClientOptions) (http.RoundTripper, // connections aren't restarted. This one uses EarlyConnection so we can use 0-RTT if the // server supports it (lower latency) type quicConnection struct { - quic.EarlyConnection + *quic.Conn hostname string rAddr string @@ -345,7 +345,7 @@ type quicConnection struct { Use0RTT bool } -func newQuicConnection(hostname, rAddr string, lAddr net.IP, tlsConfig *tls.Config, config *quic.Config, use0RTT bool) (quic.EarlyConnection, error) { +func newQuicConnection(hostname, rAddr string, lAddr net.IP, tlsConfig *tls.Config, config *quic.Config, use0RTT bool) (*quicConnection, error) { connection, udpConn, err := quicDial(context.TODO(), rAddr, lAddr, tlsConfig, config, use0RTT) if err != nil { return nil, err @@ -359,46 +359,46 @@ func newQuicConnection(hostname, rAddr string, lAddr net.IP, tlsConfig *tls.Conf ) return &quicConnection{ - hostname: hostname, - rAddr: rAddr, - lAddr: lAddr, - tlsConfig: tlsConfig, - config: config, - udpConn: udpConn, - EarlyConnection: connection, - Use0RTT: use0RTT, + hostname: hostname, + rAddr: rAddr, + lAddr: lAddr, + tlsConfig: tlsConfig, + config: config, + udpConn: udpConn, + Conn: connection, + Use0RTT: use0RTT, }, nil } -func (s *quicConnection) OpenStreamSync(ctx context.Context) (quic.Stream, error) { +func (s *quicConnection) OpenStreamSync(ctx context.Context) (*quic.Stream, error) { s.mu.Lock() defer s.mu.Unlock() - stream, err := s.EarlyConnection.OpenStreamSync(ctx) + stream, err := s.Conn.OpenStreamSync(ctx) if netErr, ok := err.(net.Error); ok && (netErr.Timeout() || netErr.Temporary()) { Log.Debug("temporary fail when trying to open stream, attempting new connection", "error", err) if err = quicRestart(s); err != nil { return nil, err } - stream, err = s.EarlyConnection.OpenStreamSync(ctx) + stream, err = s.Conn.OpenStreamSync(ctx) } return stream, err } -func (s *quicConnection) OpenStream() (quic.Stream, error) { +func (s *quicConnection) OpenStream() (*quic.Stream, error) { s.mu.Lock() defer s.mu.Unlock() - stream, err := s.EarlyConnection.OpenStream() + stream, err := s.Conn.OpenStream() if netErr, ok := err.(net.Error); ok && (netErr.Timeout() || netErr.Temporary()) { Log.Debug("temporary fail when trying to open stream, attempting new connection", "error", err) if err = quicRestart(s); err != nil { return nil, err } - stream, err = s.EarlyConnection.OpenStream() + stream, err = s.Conn.OpenStream() } return stream, err } -func (s *quicConnection) NextConnection(context.Context) (quic.Connection, error) { +func (s *quicConnection) NextConnection(context.Context) (*quic.Conn, error) { return nil, errors.New("not implemented") } @@ -406,7 +406,7 @@ func quicRestart(s *quicConnection) error { // Try to open a new connection, but clean up our mess before we do so // This function should be called with the quicConnection locked, but lock checking isn't provided // in golang; the issue was closed with "Won't fix" - _ = s.EarlyConnection.CloseWithError(DOQNoError, "") + _ = s.Conn.CloseWithError(DOQNoError, "") // We need to close the UDP socket ourselves as we own the socket not the quic-go module // c.f. https://github.com/quic-go/quic-go/issues/1457 @@ -420,20 +420,20 @@ func quicRestart(s *quicConnection) error { slog.String("remote", s.rAddr), ) var err error - var earlyConn quic.EarlyConnection - earlyConn, s.udpConn, err = quicDial(context.TODO(), s.rAddr, s.lAddr, s.tlsConfig, s.config, s.Use0RTT) + var conn *quic.Conn + conn, s.udpConn, err = quicDial(context.TODO(), s.rAddr, s.lAddr, s.tlsConfig, s.config, s.Use0RTT) if err != nil || s.udpConn == nil { Log.Warn("couldn't restart quic connection", slog.Group("details", slog.String("protocol", "quic"), slog.String("address", s.hostname), slog.String("local", s.lAddr.String())), "error", err) return err } Log.Debug("restarted quic connection", slog.Group("details", slog.String("protocol", "quic"), slog.String("address", s.hostname), slog.String("local", s.lAddr.String()), slog.String("rAddr", s.rAddr))) - s.EarlyConnection = earlyConn + s.Conn = conn return nil } -func quicDial(ctx context.Context, rAddr string, lAddr net.IP, tlsConfig *tls.Config, config *quic.Config, use0RTT bool) (quic.EarlyConnection, *net.UDPConn, error) { - var earlyConn quic.EarlyConnection +func quicDial(ctx context.Context, rAddr string, lAddr net.IP, tlsConfig *tls.Config, config *quic.Config, use0RTT bool) (*quic.Conn, *net.UDPConn, error) { + var conn *quic.Conn udpAddr, err := net.ResolveUDPAddr("udp", rAddr) if err != nil { Log.Error("couldn't resolve remote addr for UDP quic client", "error", err, "rAddr", rAddr) @@ -446,34 +446,19 @@ func quicDial(ctx context.Context, rAddr string, lAddr net.IP, tlsConfig *tls.Co } if use0RTT { - earlyConn, err = quic.DialEarly(ctx, udpConn, udpAddr, tlsConfig, config) + conn, err = quic.DialEarly(ctx, udpConn, udpAddr, tlsConfig, config) if err != nil { _ = udpConn.Close() Log.Warn("couldn't dial quic early connection", "error", err) return nil, nil, err } } else { - conn, err := quic.Dial(ctx, udpConn, udpAddr, tlsConfig, config) + conn, err = quic.Dial(ctx, udpConn, udpAddr, tlsConfig, config) if err != nil { _ = udpConn.Close() Log.Warn("couldn't dial quic connection", "error", err) return nil, nil, err } - earlyConn = &earlyConnWrapper{Connection: conn} } - return earlyConn, udpConn, nil -} - -type earlyConnWrapper struct { - quic.Connection -} - -func (e *earlyConnWrapper) HandshakeComplete() <-chan struct{} { - ch := make(chan struct{}) - close(ch) - return ch -} - -func (e *earlyConnWrapper) NextConnection(ctx context.Context) (quic.Connection, error) { - return nil, fmt.Errorf("NextConnection not supported for non-0RTT connections") + return conn, udpConn, nil } diff --git a/go.mod b/go.mod index 49d929cd..564e7bca 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/oschwald/maxminddb-golang v1.12.0 github.com/pion/dtls/v2 v2.2.11 github.com/pkg/errors v0.9.1 - github.com/quic-go/quic-go v0.48.2 + github.com/quic-go/quic-go v0.53.0 github.com/redis/go-redis/v9 v9.5.5 github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.9.0 @@ -30,11 +30,8 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/go-task/slim-sprig/v3 v3.0.0 // indirect - github.com/google/pprof v0.0.0-20240507183855-6f11f98ebb1c // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/onsi/ginkgo/v2 v2.17.3 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pion/logging v0.2.2 // indirect github.com/pion/transport/v2 v2.2.5 // indirect @@ -42,13 +39,12 @@ require ( github.com/quic-go/qpack v0.5.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/txthinking/runnergroup v0.0.0-20230325130830-408dc5853f86 // indirect - go.uber.org/mock v0.4.0 // indirect + go.uber.org/mock v0.5.0 // indirect golang.org/x/crypto v0.36.0 // indirect - golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect - golang.org/x/mod v0.17.0 // indirect + golang.org/x/mod v0.18.0 // indirect golang.org/x/sync v0.12.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + golang.org/x/tools v0.22.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index eca110c8..e33f79bd 100644 --- a/go.sum +++ b/go.sum @@ -25,16 +25,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20240507183855-6f11f98ebb1c h1:GCixZ7sgey01Kjw8pxBzCD0uVrubxl8SRzRgI0jwP+A= -github.com/google/pprof v0.0.0-20240507183855-6f11f98ebb1c/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -47,10 +39,6 @@ github.com/jtacoma/uritemplates v1.0.0/go.mod h1:IhIICdE9OcvgUnGwTtJxgBQ+VrTrti5 github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c= github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= -github.com/onsi/ginkgo/v2 v2.17.3 h1:oJcvKpIb7/8uLpDDtnQuf18xVnwKp8DTD7DQ6gTd/MU= -github.com/onsi/ginkgo/v2 v2.17.3/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc= -github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE= -github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY= github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs= github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= @@ -68,8 +56,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/quic-go/quic-go v0.53.0 h1:QHX46sISpG2S03dPeZBgVIZp8dGagIaiu2FiVYvpCZI= +github.com/quic-go/quic-go v0.53.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= github.com/redis/go-redis/v9 v9.5.5 h1:51VEyMF8eOO+NUHFm8fpg+IOc1xFuFOhxs3R+kPu1FM= github.com/redis/go-redis/v9 v9.5.5/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -92,8 +80,8 @@ github.com/txthinking/runnergroup v0.0.0-20230325130830-408dc5853f86/go.mod h1:c github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 h1:d/Wr/Vl/wiJHc3AHYbYs5I3PucJvRuw3SvbmlIRf+oM= github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301/go.mod h1:ntmMHL/xPq1WLeKiw8p/eRATaae6PiVRNipHFJxI8PM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= -go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -101,13 +89,11 @@ golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98y golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= 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-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -155,18 +141,14 @@ golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 415fbe704ec420efa39e8d687643eeed697ecf48 Mon Sep 17 00:00:00 2001 From: folbrich Date: Sun, 13 Jul 2025 12:17:35 +0200 Subject: [PATCH 2/2] Refactor quic transport --- .../example-config/doh-quic-client.toml | 4 +- .../example-config/doq-client-simple.toml | 1 + dohclient.go | 205 +++--------------- doqclient.go | 105 +++++++-- doqlistener.go | 4 +- 5 files changed, 125 insertions(+), 194 deletions(-) diff --git a/cmd/routedns/example-config/doh-quic-client.toml b/cmd/routedns/example-config/doh-quic-client.toml index 42519601..fffbf120 100644 --- a/cmd/routedns/example-config/doh-quic-client.toml +++ b/cmd/routedns/example-config/doh-quic-client.toml @@ -1,9 +1,9 @@ # DNS-over-HTTPS using the QUIC protocol. # New connections get initiated with 0-RTT if possible. -# enable-0rtt will overwrite the method to GET. [resolvers.cloudflare-doh-quic] -address = "https://cloudflare-dns.com/dns-query" +address = "https://cloudflare-dns.com/dns-query{?dns}" +doh = { method = "GET" } protocol = "doh" transport = "quic" enable-0rtt = true diff --git a/cmd/routedns/example-config/doq-client-simple.toml b/cmd/routedns/example-config/doq-client-simple.toml index 90b5814c..a545454f 100644 --- a/cmd/routedns/example-config/doq-client-simple.toml +++ b/cmd/routedns/example-config/doq-client-simple.toml @@ -3,6 +3,7 @@ [resolvers.adguard-doq] address = "dns-unfiltered.adguard.com:8853" protocol = "doq" +enable-0rtt = true [listeners.local-udp] address = "127.0.0.1:53" diff --git a/dohclient.go b/dohclient.go index 5e5ac5d3..4a98a06e 100644 --- a/dohclient.go +++ b/dohclient.go @@ -11,7 +11,6 @@ import ( "net" "net/http" "net/url" - "sync" "time" "github.com/quic-go/quic-go" @@ -49,28 +48,6 @@ type DoHClientOptions struct { Use0RTT bool } -// Returns an HTTP client based on the DoH options -func (opt DoHClientOptions) client(endpoint string) (*http.Client, error) { - var ( - tr http.RoundTripper - err error - ) - switch opt.Transport { - case "tcp", "": - tr, err = dohTcpTransport(opt) - case "quic": - tr, err = dohQuicTransport(endpoint, opt) - default: - err = fmt.Errorf("unknown protocol: '%s'", opt.Transport) - } - if err != nil { - return nil, err - } - return &http.Client{ - Transport: tr, - }, nil -} - // DoHClient is a DNS-over-HTTP resolver with support fot HTTP/2. type DoHClient struct { id string @@ -90,17 +67,28 @@ func NewDoHClient(id, endpoint string, opt DoHClientOptions) (*DoHClient, error) return nil, err } - client, err := opt.client(endpoint) - if err != nil { - return nil, err + // Configure the HTTP Client and Transport based on connection options + var client *http.Client + switch opt.Transport { + case "tcp", "": + tr, err := dohTcpTransport(opt) + if err != nil { + return nil, err + } + client = &http.Client{Transport: tr} + case "quic": + tr, err := dohQuicTransport(endpoint, opt) + if err != nil { + return nil, err + } + client = &http.Client{Transport: tr} + default: + return nil, fmt.Errorf("unknown protocol: '%s'", opt.Transport) } if opt.Method == "" { opt.Method = "POST" } - if opt.Use0RTT && opt.Transport == "quic" { - opt.Method = "GET" - } if opt.Method != "POST" && opt.Method != "GET" { return nil, fmt.Errorf("unsupported method '%s'", opt.Method) } @@ -304,18 +292,32 @@ func dohQuicTransport(endpoint string, opt DoHClientOptions) (http.RoundTripper, lAddr = opt.LocalAddr } - dialer := func(ctx context.Context, addr string, tlsConfig *tls.Config, config *quic.Config) (*quic.Conn, error) { - return newQuicConnection(u.Hostname(), addr, lAddr, tlsConfig, config, opt.Use0RTT) + // Initialize the local UDP connection, it'll be re-used for all connections + udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: lAddr, Port: 0}) + if err != nil { + Log.Error("couldn't listen on UDP socket on local address", "error", err, "local", lAddr.String()) + return nil, err + } + quicTransport := &quic.Transport{Conn: udpConn} + + dialFunc := quicTransport.Dial + if opt.Use0RTT { + dialFunc = quicTransport.DialEarly } - if opt.BootstrapAddr != "" { - dialer = func(ctx context.Context, addr string, tlsConfig *tls.Config, config *quic.Config) (*quic.Conn, error) { + + dialer := func(ctx context.Context, addr string, tlsConfig *tls.Config, config *quic.Config) (*quic.Conn, error) { + if opt.BootstrapAddr != "" { _, port, err := net.SplitHostPort(addr) if err != nil { return nil, err } addr = net.JoinHostPort(opt.BootstrapAddr, port) - return newQuicConnection(u.Hostname(), addr, lAddr, tlsConfig, config, opt.Use0RTT) } + rAddr, err := net.ResolveUDPAddr("udp", addr) + if err != nil { + return nil, err + } + return dialFunc(ctx, rAddr, tlsConfig, config) } tr := &http3.Transport{ @@ -327,138 +329,3 @@ func dohQuicTransport(endpoint string, opt DoHClientOptions) (http.RoundTripper, } return tr, nil } - -// QUIC connection that automatically restarts when it's used after having timed out. Needed -// since the quic-go RoundTripper doesn't have any connection management and timed out -// connections aren't restarted. This one uses EarlyConnection so we can use 0-RTT if the -// server supports it (lower latency) -type quicConnection struct { - *quic.Conn - - hostname string - rAddr string - lAddr net.IP - tlsConfig *tls.Config - config *quic.Config - mu sync.Mutex - udpConn *net.UDPConn - Use0RTT bool -} - -func newQuicConnection(hostname, rAddr string, lAddr net.IP, tlsConfig *tls.Config, config *quic.Config, use0RTT bool) (*quicConnection, error) { - connection, udpConn, err := quicDial(context.TODO(), rAddr, lAddr, tlsConfig, config, use0RTT) - if err != nil { - return nil, err - } - - Log.Debug("new quic connection", - slog.String("protocol", "quic"), - slog.String("hostname", hostname), - slog.String("remote", rAddr), - slog.String("local", lAddr.String()), - ) - - return &quicConnection{ - hostname: hostname, - rAddr: rAddr, - lAddr: lAddr, - tlsConfig: tlsConfig, - config: config, - udpConn: udpConn, - Conn: connection, - Use0RTT: use0RTT, - }, nil -} - -func (s *quicConnection) OpenStreamSync(ctx context.Context) (*quic.Stream, error) { - s.mu.Lock() - defer s.mu.Unlock() - stream, err := s.Conn.OpenStreamSync(ctx) - if netErr, ok := err.(net.Error); ok && (netErr.Timeout() || netErr.Temporary()) { - Log.Debug("temporary fail when trying to open stream, attempting new connection", "error", err) - if err = quicRestart(s); err != nil { - return nil, err - } - stream, err = s.Conn.OpenStreamSync(ctx) - } - return stream, err -} - -func (s *quicConnection) OpenStream() (*quic.Stream, error) { - s.mu.Lock() - defer s.mu.Unlock() - stream, err := s.Conn.OpenStream() - if netErr, ok := err.(net.Error); ok && (netErr.Timeout() || netErr.Temporary()) { - Log.Debug("temporary fail when trying to open stream, attempting new connection", "error", err) - if err = quicRestart(s); err != nil { - return nil, err - } - stream, err = s.Conn.OpenStream() - } - return stream, err -} - -func (s *quicConnection) NextConnection(context.Context) (*quic.Conn, error) { - return nil, errors.New("not implemented") -} - -func quicRestart(s *quicConnection) error { - // Try to open a new connection, but clean up our mess before we do so - // This function should be called with the quicConnection locked, but lock checking isn't provided - // in golang; the issue was closed with "Won't fix" - _ = s.Conn.CloseWithError(DOQNoError, "") - - // We need to close the UDP socket ourselves as we own the socket not the quic-go module - // c.f. https://github.com/quic-go/quic-go/issues/1457 - if s.udpConn != nil { - _ = s.udpConn.Close() - s.udpConn = nil - } - Log.Debug("attempt reconnect", slog.String("protocol", "quic"), - slog.String("hostname", s.hostname), - slog.String("local", s.lAddr.String()), - slog.String("remote", s.rAddr), - ) - var err error - var conn *quic.Conn - conn, s.udpConn, err = quicDial(context.TODO(), s.rAddr, s.lAddr, s.tlsConfig, s.config, s.Use0RTT) - if err != nil || s.udpConn == nil { - Log.Warn("couldn't restart quic connection", slog.Group("details", slog.String("protocol", "quic"), slog.String("address", s.hostname), slog.String("local", s.lAddr.String())), "error", err) - return err - } - Log.Debug("restarted quic connection", slog.Group("details", slog.String("protocol", "quic"), slog.String("address", s.hostname), slog.String("local", s.lAddr.String()), slog.String("rAddr", s.rAddr))) - - s.Conn = conn - return nil -} - -func quicDial(ctx context.Context, rAddr string, lAddr net.IP, tlsConfig *tls.Config, config *quic.Config, use0RTT bool) (*quic.Conn, *net.UDPConn, error) { - var conn *quic.Conn - udpAddr, err := net.ResolveUDPAddr("udp", rAddr) - if err != nil { - Log.Error("couldn't resolve remote addr for UDP quic client", "error", err, "rAddr", rAddr) - return nil, nil, err - } - udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: lAddr, Port: 0}) - if err != nil { - Log.Error("couldn't listen on UDP socket on local address", "error", err, "local", lAddr.String()) - return nil, nil, err - } - - if use0RTT { - conn, err = quic.DialEarly(ctx, udpConn, udpAddr, tlsConfig, config) - if err != nil { - _ = udpConn.Close() - Log.Warn("couldn't dial quic early connection", "error", err) - return nil, nil, err - } - } else { - conn, err = quic.Dial(ctx, udpConn, udpAddr, tlsConfig, config) - if err != nil { - _ = udpConn.Close() - Log.Warn("couldn't dial quic connection", "error", err) - return nil, nil, err - } - } - return conn, udpConn, nil -} diff --git a/doqclient.go b/doqclient.go index 3feaba2b..9c12673b 100644 --- a/doqclient.go +++ b/doqclient.go @@ -6,6 +6,7 @@ import ( "encoding/binary" "io" "net" + "sync" "time" "log/slog" @@ -28,7 +29,7 @@ type DoQClient struct { log *slog.Logger metrics *ListenerMetrics - connection quicConnection + connection *quicConnection } // DoQClientOptions contains options used by the DNS-over-QUIC resolver. @@ -89,23 +90,22 @@ func NewDoQClient(id, endpoint string, opt DoQClientOptions) (*DoQClient, error) "protocol", "doq", "endpoint", endpoint, ) + config := &quic.Config{ + TokenStore: quic.NewLRUTokenStore(10, 10), + HandshakeIdleTimeout: opt.QueryTimeout, + } + qConn, err := newQuicConnection(lAddr, tlsConfig, config, opt.Use0RTT) + if err != nil { + return nil, err + } return &DoQClient{ id: id, endpoint: endpoint, DoQClientOptions: opt, requests: make(chan *request), log: log, - connection: quicConnection{ - hostname: host, - lAddr: lAddr, - tlsConfig: tlsConfig, - config: &quic.Config{ - TokenStore: quic.NewLRUTokenStore(10, 10), - HandshakeIdleTimeout: opt.QueryTimeout, - }, - Use0RTT: opt.Use0RTT, - }, - metrics: NewListenerMetrics("client", id), + connection: qConn, + metrics: NewListenerMetrics("client", id), }, nil } @@ -208,35 +208,78 @@ func (d *DoQClient) String() string { return d.id } -func (s *quicConnection) getStream(endpoint string, log *slog.Logger) (quic.Stream, error) { +// QUIC connection that automatically restarts when it's used after having +// timed out. Needed since the quic.Transport doesn't have any connection +// management and timed out connections aren't restarted. This one uses +// EarlyConnection so we can use 0-RTT if the server supports it (lower +// latency) +type quicConnection struct { + *quic.Conn + + lAddr net.IP + tlsConfig *tls.Config + config *quic.Config + mu sync.Mutex + udpConn *net.UDPConn + dialFunc func(ctx context.Context, addr net.Addr, tlsConf *tls.Config, conf *quic.Config) (*quic.Conn, error) +} + +func newQuicConnection(lAddr net.IP, tlsConfig *tls.Config, config *quic.Config, use0RTT bool) (*quicConnection, error) { + // Initialize the local UDP connection, it'll be re-used for all connections + udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: lAddr, Port: 0}) + if err != nil { + Log.Error("couldn't listen on UDP socket on local address", "error", err, "local", lAddr.String()) + return nil, err + } + quicTransport := &quic.Transport{Conn: udpConn} + + dialFunc := quicTransport.Dial + if use0RTT { + dialFunc = quicTransport.DialEarly + } + + return &quicConnection{ + lAddr: lAddr, + tlsConfig: tlsConfig, + config: config, + udpConn: udpConn, + dialFunc: dialFunc, + }, nil +} + +func (s *quicConnection) getStream(endpoint string, log *slog.Logger) (*quic.Stream, error) { + rAddr, err := net.ResolveUDPAddr("udp", endpoint) + if err != nil { + return nil, err + } + s.mu.Lock() defer s.mu.Unlock() // If we don't have a connection yet, make one - if s.EarlyConnection == nil { + if s.Conn == nil { var err error - s.EarlyConnection, s.udpConn, err = quicDial(context.TODO(), endpoint, s.lAddr, s.tlsConfig, s.config, s.Use0RTT) + s.Conn, err = s.dialFunc(context.Background(), rAddr, s.tlsConfig, s.config) if err != nil { log.Warn("failed to open connection", - "hostname", s.hostname, + "hostname", endpoint, "error", err, ) return nil, err } - s.rAddr = endpoint } // If we can't get a stream then restart the connection and try again once - stream, err := s.EarlyConnection.OpenStream() + stream, err := s.Conn.OpenStream() if err != nil { log.Debug("temporary fail when trying to open stream, attempting new connection", "error", err, ) - if err = quicRestart(s); err != nil { - log.Warn("failed to open connection", "hostname", s.hostname, "error", err) + if err = s.restart(rAddr); err != nil { + log.Warn("failed to open connection", "hostname", endpoint, "error", err) return nil, err } - stream, err = s.EarlyConnection.OpenStream() + stream, err = s.Conn.OpenStream() if err != nil { log.Warn("failed to open stream", "error", err, @@ -245,3 +288,23 @@ func (s *quicConnection) getStream(endpoint string, log *slog.Logger) (quic.Stre } return stream, err } + +// Try to open a new connection. This function should be called with the mutex +// locked. +func (s *quicConnection) restart(rAddr *net.UDPAddr) error { + _ = s.Conn.CloseWithError(DOQNoError, "") + + Log.Debug("attempt reconnect", slog.String("protocol", "quic"), + slog.String("local", s.lAddr.String()), + slog.String("remote", rAddr.String()), + ) + conn, err := s.dialFunc(context.TODO(), rAddr, s.tlsConfig, s.config) + if err != nil { + Log.Warn("couldn't restart quic connection", slog.Group("details", slog.String("protocol", "quic"), slog.String("remote", rAddr.String()), slog.String("local", s.lAddr.String())), "error", err) + return err + } + Log.Debug("restarted quic connection", slog.Group("details", slog.String("protocol", "quic"), slog.String("remote", rAddr.String()), slog.String("local", s.lAddr.String()))) + + s.Conn = conn + return nil +} diff --git a/doqlistener.go b/doqlistener.go index f86aa292..ab8b99e3 100644 --- a/doqlistener.go +++ b/doqlistener.go @@ -103,7 +103,7 @@ func (s DoQListener) Stop() error { return s.ln.Close() } -func (s DoQListener) handleConnection(connection quic.Connection) { +func (s DoQListener) handleConnection(connection *quic.Conn) { tlsServerName := connection.ConnectionState().TLS.ServerName ci := ClientInfo{ @@ -139,7 +139,7 @@ func (s DoQListener) handleConnection(connection quic.Connection) { } } -func (s DoQListener) handleStream(stream quic.Stream, log *slog.Logger, ci ClientInfo) { +func (s DoQListener) handleStream(stream *quic.Stream, log *slog.Logger, ci ClientInfo) { // DNS over QUIC uses one stream per query/response. defer stream.Close() s.metrics.stream.Add(1)