This repository was archived by the owner on Jul 3, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
feat: implement DNS over QUIC #18
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
bcd6521
feat: implement DNS over QUIC
roopeshsn 9c006f5
fix: alter as per RFC 9250
roopeshsn 2b49672
refactor: reused queryStream method for doq
roopeshsn bd278ee
Merge branch 'main' into feat-DoQ
bassosimone 2ae6018
refactor: based on review comments
roopeshsn a61aa46
doc: add usage example for QUIC
roopeshsn 56272af
Merge branch 'main' into feat-DoQ
bassosimone 25fa493
chore: upgrade dependencies
bassosimone c92360c
Merge branch 'main' into feat-DoQ
bassosimone 70591e4
refactor: closing connections and added test cases for doq
roopeshsn 0c3dc16
refactor: queryQUIC method using the closepool package from rbmk-proj…
roopeshsn 3d53a44
doc: added comments
roopeshsn b2c014e
fix: run `go mod tidy`
bassosimone 59d4b3d
chore: editorial changes ahead of merging
bassosimone 0e51918
chore: add some extra comments
bassosimone 8133b5d
chore: more minor documentation edits
bassosimone File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| } | ||
| }) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.