diff --git a/doquic.go b/doquic.go new file mode 100644 index 0000000..0096136 --- /dev/null +++ b/doquic.go @@ -0,0 +1,147 @@ +// +// SPDX-License-Identifier: GPL-3.0-or-later +// +// DNS-over-QUIC implementation +// +// See // https://datatracker.ietf.org/doc/rfc9250/ +// + +package dnscore + +import ( + "context" + "crypto/tls" + "net" + "time" + + "github.com/miekg/dns" + "github.com/quic-go/quic-go" + "github.com/rbmk-project/common/closepool" +) + +func (t *Transport) queryQUIC(ctx context.Context, addr *ServerAddr, query *dns.Msg) (*dns.Msg, error) { + // 0. immediately fail if the context is already done, which + // is useful to write unit tests + if ctx.Err() != nil { + return nil, ctx.Err() + } + + // 1. Fill the TLS configuration + hostname, _, err := net.SplitHostPort(addr.Address) + if err != nil { + return nil, err + } + tlsConfig := &tls.Config{ + NextProtos: []string{"doq"}, + ServerName: hostname, + RootCAs: t.RootCAs, + } + + // 2. Create a connection pool to close all opened connections + // and ensure we don't leak resources by using defer. + connPool := &closepool.Pool{} + defer connPool.Close() + + // TODO(bassosimone,roopeshsn): for TCP connections, we abstract + // this process of combining the DNS lookup and dialing a connection, + // which, in turn, allows for better unit testing and also allows + // rbmk-project/rbmk to use rbmk-project/x/netcore for dialing. + // + // We should probably see to create a similar dialing interface in + // rbmk-project/x/netcore for QUIC connections. We started discussing + // this in https://github.com/rbmk-project/dnscore/pull/18. + + // 3. Open the UDP connection for supporting QUIC + listenConfig := &net.ListenConfig{} + udpConn, err := listenConfig.ListenPacket(ctx, "udp", ":0") + if err != nil { + return nil, err + } + connPool.Add(udpConn) + + // 4. Map the UDP address, which may possibly contain a domain + // name, to an actual UDP address structure to dial with + udpAddr, err := net.ResolveUDPAddr("udp", addr.Address) + if err != nil { + return nil, err + } + + // 5. Establish a QUIC connection. Note that the default + // configuration implies a 5s timeout for handshaking and + // a 30s idle connection timeout. + tr := &quic.Transport{ + Conn: udpConn, + } + connPool.Add(tr) + quicConfig := &quic.Config{} + quicConn, err := tr.Dial(ctx, udpAddr, tlsConfig, quicConfig) + if err != nil { + return nil, err + } + connPool.Add(closepool.CloserFunc(func() error { + // Closing w/o specific error -- RFC 9250 Sect. 4.3 + const doq_no_error = 0x00 + return quicConn.CloseWithError(doq_no_error, "") + })) + + // 6. Open a stream for sending the DoQ query and wrap it into + // an adapter that makes it usable by DNS-over-stream code + quicStream, err := quicConn.OpenStream() + if err != nil { + return nil, err + } + stream := &quicStreamAdapter{ + Stream: quicStream, + localAddr: quicConn.LocalAddr(), + remoteAddr: quicConn.RemoteAddr(), + } + connPool.Add(stream) + + // 7. Ensure that we tear down everything which we have set up + // in the case in which the context is canceled + ctx, cancel := context.WithCancel(ctx) + defer cancel() + go func() { + defer connPool.Close() + <-ctx.Done() + }() + + // 8. defer to queryStream. Note that this method TAKES OWNERSHIP of + // the stream and closes it after we've sent the query, honouring the + // expectations for DoQ queries -- see RFC 9250 Sect. 4.2. + return t.queryStream(ctx, addr, query, stream) +} + +// quicStreamAdapter ensures a QUIC stream implements [dnsStream]. +type quicStreamAdapter struct { + Stream quic.Stream + localAddr net.Addr + remoteAddr net.Addr +} + +// Make sure we actually implement [dnsStream]. +var _ dnsStream = &quicStreamAdapter{} + +func (qsw *quicStreamAdapter) Read(p []byte) (int, error) { + return qsw.Stream.Read(p) +} + +func (qsw *quicStreamAdapter) Write(p []byte) (int, error) { + return qsw.Stream.Write(p) +} + +func (qsw *quicStreamAdapter) Close() error { + return qsw.Stream.Close() +} + +func (qsw *quicStreamAdapter) SetDeadline(t time.Time) error { + return qsw.Stream.SetDeadline(t) +} + +func (qsw *quicStreamAdapter) LocalAddr() net.Addr { + return qsw.localAddr +} + +func (qsw *quicStreamAdapter) RemoteAddr() net.Addr { + return qsw.remoteAddr +} diff --git a/doquic_test.go b/doquic_test.go new file mode 100644 index 0000000..2c195fd --- /dev/null +++ b/doquic_test.go @@ -0,0 +1,51 @@ +package dnscore + +import ( + "context" + "testing" + + "github.com/miekg/dns" + "github.com/stretchr/testify/assert" +) + +func TestTransport_queryQUIC(t *testing.T) { + // TODO(bassosimone,roopeshsn): currently this is an integration test + // using the network w/ real servers but we should instead have: + // + // 1. an integration test using the network but using a QUIC server running + // locally (a test which should live inside integration_test.go) + // + // 2. unit tests using mocking like we do for, e.g.m dohttps_test.go + + tests := []struct { + name string + setupTransport func() *Transport + expectedError error + }{ + { + name: "Successful query", + setupTransport: func() *Transport { + return &Transport{} + }, + expectedError: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + transport := tt.setupTransport() + addr := NewServerAddr(ProtocolDoQ, "dns.adguard.com:853") + query := new(dns.Msg) + query.SetQuestion("example.com.", dns.TypeA) + + _, err := transport.queryQUIC(context.Background(), addr, query) + + if tt.expectedError != nil { + assert.Error(t, err) + assert.Equal(t, tt.expectedError.Error(), err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/dotcp.go b/dotcp.go index ee7eb04..73aef4c 100644 --- a/dotcp.go +++ b/dotcp.go @@ -3,7 +3,8 @@ // // Adapted from: https://github.com/ooni/probe-engine/blob/v0.23.0/netx/resolver/dnsovertcp.go // -// DNS-over-TCP implementation +// DNS-over-TCP implementation. Includes generic code to +// send queries over streams used by DoT and DoQ. // package dnscore @@ -16,10 +17,19 @@ import ( "io" "math" "net" + "time" "github.com/miekg/dns" ) +// dnsStream is the interface expected by [*Transport.queryStream], +type dnsStream interface { + io.ReadWriteCloser + SetDeadline(t time.Time) error + LocalAddr() net.Addr + RemoteAddr() net.Addr +} + // queryTCP implements [*Transport.Query] for DNS over TCP. func (t *Transport) queryTCP(ctx context.Context, addr *ServerAddr, query *dns.Msg) (*dns.Msg, error) { @@ -55,7 +65,7 @@ type queryMsg interface { // This method TAKES OWNERSHIP of the provided connection and is // responsible for closing it when done. func (t *Transport) queryStream(ctx context.Context, - addr *ServerAddr, query queryMsg, conn net.Conn) (*dns.Msg, error) { + addr *ServerAddr, query queryMsg, conn dnsStream) (*dns.Msg, error) { // 1. Use a single connection for request, which is what the standard library // does as well for TCP and is more robust in terms of residual censorship. @@ -96,6 +106,22 @@ func (t *Transport) queryStream(ctx context.Context, return nil, err } + // 5b. Ensure we close the stream when using DoQ to signal the + // upstream server that it is okay to send a response. + // + // RFC 9250 is very clear in this respect: + // + // 4.2. Stream Mapping and Usage + // client MUST send the DNS query over the selected stream and MUST + // indicate through the STREAM FIN mechanism that no further data will + // be sent on that stream. + // + // Empirical testing during https://github.com/rbmk-project/dnscore/pull/18 + // showed that, in fact, some servers misbehave if we don't do this. + if _, ok := conn.(*quicStreamAdapter); ok { + _ = conn.Close() + } + // 6. Wrap the conn to avoid issuing too many reads // then read the response header and query br := bufio.NewReader(conn) diff --git a/example_quic_test.go b/example_quic_test.go new file mode 100644 index 0000000..a449c89 --- /dev/null +++ b/example_quic_test.go @@ -0,0 +1,66 @@ +package dnscore_test + +import ( + "context" + "fmt" + "log" + "slices" + "strings" + "time" + + "github.com/miekg/dns" + "github.com/rbmk-project/common/runtimex" + "github.com/rbmk-project/dnscore" +) + +func ExampleTransport_dnsOverQUIC() { + // create transport, server addr, and query + txp := &dnscore.Transport{} + serverAddr := &dnscore.ServerAddr{ + Protocol: dnscore.ProtocolDoQ, + Address: "dns0.eu:853", + } + options := []dnscore.QueryOption{ + dnscore.QueryOptionEDNS0( + dnscore.EDNS0SuggestedMaxResponseSizeOtherwise, + dnscore.EDNS0FlagDO|dnscore.EDNS0FlagBlockLengthPadding, + ), + } + query, err := dnscore.NewQueryWithServerAddr(serverAddr, "dns.google", dns.TypeA, options...) + if err != nil { + log.Fatal(err) + } + + // issue the query and get the response + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + resp, err := txp.Query(ctx, serverAddr, query) + if err != nil { + log.Fatal(err) + } + + // validate the response + if err := dnscore.ValidateResponse(query, resp); err != nil { + log.Fatal(err) + } + runtimex.Assert(len(query.Question) > 0, "expected at least one question") + rrs, err := dnscore.ValidAnswers(query.Question[0], resp) + if err != nil { + log.Fatal(err) + } + + // print the results + var addrs []string + for _, rr := range rrs { + switch rr := rr.(type) { + case *dns.A: + addrs = append(addrs, rr.A.String()) + } + } + slices.Sort(addrs) + fmt.Printf("%s\n", strings.Join(addrs, "\n")) + + // Output: + // 8.8.4.4 + // 8.8.8.8 +} diff --git a/go.mod b/go.mod index b420b65..51263c2 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,10 @@ module github.com/rbmk-project/dnscore -go 1.23 +go 1.23.0 require ( github.com/miekg/dns v1.1.63 + github.com/quic-go/quic-go v0.50.0 github.com/rbmk-project/common v0.17.0 github.com/stretchr/testify v1.10.0 golang.org/x/net v0.35.0 @@ -11,7 +12,13 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/google/pprof v0.0.0-20250208200701-d0013a598941 // indirect + github.com/onsi/ginkgo/v2 v2.22.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/crypto v0.35.0 // indirect + golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect golang.org/x/mod v0.23.0 // indirect golang.org/x/sync v0.11.0 // indirect golang.org/x/sys v0.30.0 // indirect diff --git a/go.sum b/go.sum index a80bacb..492d41b 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,33 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/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/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-20250208200701-d0013a598941 h1:43XjGa6toxLpeksjcxs1jIoIyr+vUfOqY2c6HB4bpoc= +github.com/google/pprof v0.0.0-20250208200701-d0013a598941/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= +github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= +github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/quic-go v0.50.0 h1:3H/ld1pa3CYhkcc20TPIyG1bNsdhn9qZBGN3b9/UyUo= +github.com/quic-go/quic-go v0.50.0/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= github.com/rbmk-project/common v0.17.0 h1:Af00606njA1M9BNYMlFpx/ikalChE41R5jTqwbgB5/c= github.com/rbmk-project/common v0.17.0/go.mod h1:jhbcSyhbC6F4PDfSXqmCuVnDHa4eq+fUdYWxjsRStP8= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= @@ -20,8 +38,12 @@ golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +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.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/integration_test.go b/integration_test.go index 59aa5e6..00a5546 100644 --- a/integration_test.go +++ b/integration_test.go @@ -166,3 +166,5 @@ func TestTransport_RoundTrip_HTTPS(t *testing.T) { // verify the results checkResult(t, resp, err) } + +// TODO(bassosimone,roopeshsn): add integration tests for DoQ diff --git a/internal/cmd/transport/main.go b/internal/cmd/transport/main.go index 3da0418..f90759e 100644 --- a/internal/cmd/transport/main.go +++ b/internal/cmd/transport/main.go @@ -20,7 +20,7 @@ var ( serverAddr = flag.String("server", "8.8.8.8:53", "DNS server address") domain = flag.String("domain", "www.example.com", "Domain to query") qtype = flag.String("type", "A", "Query type (A, AAAA, CNAME, etc.)") - protocol = flag.String("protocol", "udp", "DNS protocol (udp, tcp, dot, doh)") + protocol = flag.String("protocol", "udp", "DNS protocol (udp, tcp, dot, doh, doq)") ) func main() { @@ -47,11 +47,14 @@ func main() { panic(fmt.Errorf("transport: unsupported query type: %s", *qtype)) } - // Create the server address + // Create the server address. Ensure that we set the proper flags + // depending on the protocol that we're going to use. server := dnscore.NewServerAddr(dnscore.Protocol(*protocol), *serverAddr) flags := 0 maxlength := uint16(dnscore.EDNS0SuggestedMaxResponseSizeUDP) - if *protocol == string(dnscore.ProtocolDoT) || *protocol == string(dnscore.ProtocolDoH) { + if *protocol == string(dnscore.ProtocolDoT) || + *protocol == string(dnscore.ProtocolDoH) || + *protocol == string(dnscore.ProtocolDoQ) { flags |= dnscore.EDNS0FlagDO | dnscore.EDNS0FlagBlockLengthPadding } if *protocol != string(dnscore.ProtocolUDP) { diff --git a/internal/cmd/transport/main_test.go b/internal/cmd/transport/main_test.go index eacf774..dd3b642 100644 --- a/internal/cmd/transport/main_test.go +++ b/internal/cmd/transport/main_test.go @@ -38,6 +38,12 @@ func Test_main(t *testing.T) { main() }) + t.Run("DNS-over-QUIC", func(t *testing.T) { + *serverAddr = "dns0.eu:853" + *protocol = "doq" + main() + }) + t.Run("AAAA query", func(t *testing.T) { *serverAddr = "8.8.8.8:53" *protocol = "udp" diff --git a/query.go b/query.go index 54f2c4f..195117a 100644 --- a/query.go +++ b/query.go @@ -123,9 +123,8 @@ func NewQueryWithServerAddr(serverAddr *ServerAddr, name string, qtype uint16, // Only set the queryID for protocols that actually // require a nonzero queryID to be set. - // TODO(bassosimone,roopeshsn): update for DoQ switch serverAddr.Protocol { - case ProtocolDoH: + case ProtocolDoH, ProtocolDoQ: // for DoH/DoQ, by default we leave the query ID to // zero, which is what the RFCs suggest/require. default: diff --git a/query_test.go b/query_test.go index e222513..774afd2 100644 --- a/query_test.go +++ b/query_test.go @@ -16,9 +16,6 @@ func TestNewQueryWithServerAddr(t *testing.T) { dns.Id = func() uint16 { return expectedNonZeroQueryID } defer func() { dns.Id = savedId }() - // TODO(bassosimone,roopeshsn): ensure we also test for DoQ - // once we merge the corresponding PR. - tests := []struct { name string serverAddr *ServerAddr @@ -76,6 +73,14 @@ func TestNewQueryWithServerAddr(t *testing.T) { options: []QueryOption{mockedFailingOption}, wantErr: true, }, + { + name: "DoQ query should have zero ID", + serverAddr: NewServerAddr(ProtocolQUIC, "dns.adguard-dns.com:853"), + qname: "example.com", + qtype: dns.TypeAAAA, + wantName: "example.com.", + wantId: 0, + }, } for _, tt := range tests { diff --git a/serveraddr.go b/serveraddr.go index af20209..fa352ed 100644 --- a/serveraddr.go +++ b/serveraddr.go @@ -18,6 +18,9 @@ const ( // ProtocolDoH is DNS over HTTPS. ProtocolDoH = Protocol("doh") + + // ProtocolDoQ is DNS over QUIC. + ProtocolDoQ = Protocol("doq") ) // Name aliases for DNS protocols. @@ -27,6 +30,9 @@ const ( // ProtocolHTTPS is an alias for ProtocolDoH. ProtocolHTTPS = ProtocolDoH + + // ProtocolQUIC is an alias for ProtocolDoQ. + ProtocolQUIC = ProtocolDoQ ) // ServerAddr is a DNS server address. @@ -45,6 +51,7 @@ type ServerAddr struct { // - [ProtocolTCP] // - [ProtocolDoT] // - [ProtocolDoH] + // - [ProtocolDoQ] Protocol Protocol // Address is the network address of the server. diff --git a/slog.go b/slog.go index 688e386..6e85143 100644 --- a/slog.go +++ b/slog.go @@ -5,7 +5,6 @@ package dnscore import ( "context" "log/slog" - "net" "net/netip" "time" @@ -21,6 +20,7 @@ var protocolMap = map[Protocol]string{ ProtocolTCP: "tcp", ProtocolDoT: "tcp", ProtocolUDP: "udp", + ProtocolDoQ: "udp", } // maybeLogQuery is a helper function that logs the query if the logger is set @@ -74,7 +74,7 @@ func (t *Transport) maybeLogResponseAddrPort(ctx context.Context, // maybeLogResponseConn is a helper function that logs the response if the logger is set. func (t *Transport) maybeLogResponseConn(ctx context.Context, addr *ServerAddr, t0 time.Time, rawQuery, rawResp []byte, - conn net.Conn) { + conn dnsStream) { if t.Logger != nil { t.maybeLogResponseAddrPort( ctx, diff --git a/transport.go b/transport.go index 7196268..0efa3e3 100644 --- a/transport.go +++ b/transport.go @@ -109,6 +109,9 @@ func (t *Transport) Query(ctx context.Context, case ProtocolDoH: return t.queryHTTPS(ctx, addr, query) + case ProtocolDoQ: + return t.queryQUIC(ctx, addr, query) + default: return nil, fmt.Errorf("%w: %s", ErrNoSuchTransportProtocol, addr.Protocol) }