Skip to content

Commit 53af60f

Browse files
committed
Pull request #412: AGDNS-3434-impl-0RTT-tests
Merge in GO/dnsproxy from AGDNS-3434-impl-0RTT-tests to master Squashed commit of the following: commit 3413457 Author: f.setrakov <[email protected]> Date: Fri Nov 21 15:46:18 2025 +0300 upstream: use defer for mutext unlocking commit b148fd5 Author: f.setrakov <[email protected]> Date: Fri Nov 21 12:03:27 2025 +0300 upstream: imp 0RTT-tests commit c3c0025 Author: f.setrakov <[email protected]> Date: Thu Nov 20 12:34:02 2025 +0300 upstream: impl 0RTT tests
1 parent 314ac48 commit 53af60f

File tree

6 files changed

+257
-14
lines changed

6 files changed

+257
-14
lines changed

upstream/doh.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,19 @@ func newDoH(addr *url.URL, opts *Options) (u Upstream, err error) {
9898
httpVersions = DefaultHTTPVersions
9999
}
100100

101+
quicConf := &quic.Config{
102+
KeepAlivePeriod: QUICKeepAlivePeriod,
103+
TokenStore: newQUICTokenStore(),
104+
}
105+
106+
if opts.QUICTracer != nil {
107+
quicConf.Tracer = opts.QUICTracer.TraceForConnection
108+
}
109+
101110
ups := &dnsOverHTTPS{
102-
getDialer: newDialerInitializer(addr, opts),
103-
addr: addr,
104-
quicConf: &quic.Config{
105-
KeepAlivePeriod: QUICKeepAlivePeriod,
106-
TokenStore: newQUICTokenStore(),
107-
},
111+
getDialer: newDialerInitializer(addr, opts),
112+
addr: addr,
113+
quicConf: quicConf,
108114
quicConfMu: &sync.Mutex{},
109115
tlsConf: &tls.Config{
110116
ServerName: addr.Hostname(),

upstream/doh_internal_test.go

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222
"github.com/stretchr/testify/require"
2323
)
2424

25-
// TODO(f.setrakov): Implement 0RTT tests.
2625
func TestUpstreamDoH(t *testing.T) {
2726
t.Parallel()
2827

@@ -269,6 +268,60 @@ func TestUpstreamDoH_serverRestart(t *testing.T) {
269268
}
270269
}
271270

271+
func TestUpstreamDoH_0RTT(t *testing.T) {
272+
t.Parallel()
273+
274+
// Run the first server instance.
275+
srv := startDoHServer(t, testDoHServerOptions{
276+
http3Enabled: true,
277+
})
278+
279+
// Create a DNS-over-HTTPS upstream.
280+
tracer := &testTracer{}
281+
address := fmt.Sprintf("h3://%s/dns-query", srv.addr)
282+
u, err := AddressToUpstream(address, &Options{
283+
Logger: testLogger,
284+
InsecureSkipVerify: true,
285+
QUICTracer: tracer,
286+
})
287+
require.NoError(t, err)
288+
testutil.CleanupAndRequireSuccess(t, u.Close)
289+
290+
uh := testutil.RequireTypeAssert[*dnsOverHTTPS](t, u)
291+
req := createTestMessage()
292+
293+
// Trigger connection to a DoH3 server.
294+
resp, err := uh.Exchange(req)
295+
require.NoError(t, err)
296+
requireResponse(t, req, resp)
297+
298+
// Close the active connection to make sure we'll reconnect.
299+
func() {
300+
uh.clientMu.Lock()
301+
defer uh.clientMu.Unlock()
302+
303+
err = uh.closeClient(uh.client)
304+
require.NoError(t, err)
305+
306+
uh.client = nil
307+
}()
308+
309+
// Trigger second connection.
310+
resp, err = uh.Exchange(req)
311+
require.NoError(t, err)
312+
requireResponse(t, req, resp)
313+
314+
// Check traced connections info.
315+
conns := tracer.connectionsInfo()
316+
require.Len(t, conns, 2)
317+
318+
// Examine the first connection (no 0-RTT there).
319+
require.False(t, conns[0].is0RTT())
320+
321+
// Examine the second connection (the one that used 0-RTT).
322+
require.True(t, conns[1].is0RTT())
323+
}
324+
272325
// testDoHServerOptions allows customizing testDoHServer behavior.
273326
type testDoHServerOptions struct {
274327
// handler is an HTTP handler that should be used by the server. The

upstream/doq.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,19 @@ type dnsOverQUIC struct {
9999
func newDoQ(addr *url.URL, opts *Options) (u Upstream, err error) {
100100
addPort(addr, defaultPortDoQ)
101101

102+
quicConf := &quic.Config{
103+
KeepAlivePeriod: QUICKeepAlivePeriod,
104+
TokenStore: newQUICTokenStore(),
105+
}
106+
107+
if opts.QUICTracer != nil {
108+
quicConf.Tracer = opts.QUICTracer.TraceForConnection
109+
}
110+
102111
u = &dnsOverQUIC{
103-
getDialer: newDialerInitializer(addr, opts),
104-
addr: addr,
105-
quicConfig: &quic.Config{
106-
KeepAlivePeriod: QUICKeepAlivePeriod,
107-
TokenStore: newQUICTokenStore(),
108-
},
112+
getDialer: newDialerInitializer(addr, opts),
113+
addr: addr,
114+
quicConfig: quicConf,
109115
tlsConf: &tls.Config{
110116
ServerName: addr.Hostname(),
111117
RootCAs: opts.RootCAs,

upstream/doq_internal_test.go

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import (
2424
"github.com/stretchr/testify/require"
2525
)
2626

27-
// TODO(f.setrakov): Implement 0RTT tests.
2827
func TestUpstreamDoQ(t *testing.T) {
2928
tlsConf, rootCAs := createServerTLSConfig(t, "127.0.0.1")
3029

@@ -183,6 +182,56 @@ func TestUpstreamDoQ_serverRestart(t *testing.T) {
183182
})
184183
}
185184

185+
func TestUpstreamDoQ_0RTT(t *testing.T) {
186+
tlsConf, rootCAs := createServerTLSConfig(t, "127.0.0.1")
187+
188+
srv := startDoQServer(t, tlsConf, 0)
189+
190+
tracer := &testTracer{}
191+
address := fmt.Sprintf("quic://%s", srv.addr)
192+
u, err := AddressToUpstream(address, &Options{
193+
Logger: testLogger,
194+
QUICTracer: tracer,
195+
RootCAs: rootCAs,
196+
})
197+
require.NoError(t, err)
198+
testutil.CleanupAndRequireSuccess(t, u.Close)
199+
200+
uq := testutil.RequireTypeAssert[*dnsOverQUIC](t, u)
201+
req := createTestMessage()
202+
203+
// Trigger connection to a QUIC server.
204+
resp, err := uq.Exchange(req)
205+
require.NoError(t, err)
206+
requireResponse(t, req, resp)
207+
208+
// Close the active connection to make sure we'll reconnect.
209+
func() {
210+
uq.connMu.Lock()
211+
defer uq.connMu.Unlock()
212+
213+
err = uq.conn.CloseWithError(QUICCodeNoError, "")
214+
require.NoError(t, err)
215+
216+
uq.conn = nil
217+
}()
218+
219+
// Trigger second connection.
220+
resp, err = uq.Exchange(req)
221+
require.NoError(t, err)
222+
requireResponse(t, req, resp)
223+
224+
// Check traced connections info.
225+
conns := tracer.connectionsInfo()
226+
require.Len(t, conns, 2)
227+
228+
// Examine the first connection (no 0-RTT there).
229+
require.False(t, conns[0].is0RTT())
230+
231+
// Examine the second connection (the one that used 0-RTT).
232+
require.True(t, conns[1].is0RTT())
233+
}
234+
186235
// testDoHServer is an instance of a test DNS-over-QUIC server.
187236
type testDoQServer struct {
188237
// listener is the QUIC connections listener.

upstream/upstream.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import (
2424
"github.com/ameshkov/dnscrypt/v2"
2525
"github.com/ameshkov/dnsstamps"
2626
"github.com/miekg/dns"
27+
"github.com/quic-go/quic-go"
28+
"github.com/quic-go/quic-go/qlogwriter"
2729
)
2830

2931
// Upstream is an interface for a DNS resolver. All the methods must be safe
@@ -43,6 +45,17 @@ type Upstream interface {
4345
io.Closer
4446
}
4547

48+
// QUICTracer creates [qlogwriter.Trace] instances for QUIC connection tracing.
49+
type QUICTracer interface {
50+
// TraceForConnection creates a [qlogwriter.Trace] specific for a given
51+
// role and connection ID.
52+
TraceForConnection(
53+
ctx context.Context,
54+
isClient bool,
55+
connID quic.ConnectionID,
56+
) (trace qlogwriter.Trace)
57+
}
58+
4659
// Options for AddressToUpstream func. With these options we can configure the
4760
// upstream properties.
4861
type Options struct {
@@ -63,6 +76,10 @@ type Options struct {
6376
// Upstream.Exchange method returns any error caused by it.
6477
VerifyDNSCryptCertificate func(cert *dnscrypt.Cert) error
6578

79+
// QUICTracer allows tracing every QUIC connection and logging every packet
80+
// that goes through.
81+
QUICTracer QUICTracer
82+
6683
// RootCAs is the CertPool that must be used by all upstreams. Redefining
6784
// RootCAs makes sense on iOS to overcome the 15MB memory limit of the
6885
// NEPacketTunnelProvider.
@@ -102,6 +119,7 @@ func (o *Options) Clone() (clone *Options) {
102119
VerifyDNSCryptCertificate: o.VerifyDNSCryptCertificate,
103120
InsecureSkipVerify: o.InsecureSkipVerify,
104121
PreferIPv6: o.PreferIPv6,
122+
QUICTracer: o.QUICTracer,
105123
RootCAs: o.RootCAs,
106124
CipherSuites: o.CipherSuites,
107125
Logger: o.Logger,

upstream/upstream_internal_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package upstream
22

33
import (
4+
"context"
45
"crypto/ecdsa"
56
"crypto/rand"
67
"crypto/rsa"
@@ -24,6 +25,9 @@ import (
2425
"github.com/AdguardTeam/golibs/testutil"
2526
"github.com/ameshkov/dnsstamps"
2627
"github.com/miekg/dns"
28+
"github.com/quic-go/quic-go"
29+
"github.com/quic-go/quic-go/qlog"
30+
"github.com/quic-go/quic-go/qlogwriter"
2731
"github.com/stretchr/testify/assert"
2832
"github.com/stretchr/testify/require"
2933
)
@@ -772,3 +776,110 @@ func publicKey(priv any) (pub any) {
772776
return nil
773777
}
774778
}
779+
780+
// testTracer collects QUIC connection traces for testing.
781+
type testTracer struct {
782+
tracers []*quicTracer
783+
}
784+
785+
// TraceForConnection creates a tracer for a QUIC connection.
786+
func (t *testTracer) TraceForConnection(
787+
_ context.Context,
788+
_ bool,
789+
_ quic.ConnectionID,
790+
) (tracer qlogwriter.Trace) {
791+
newTracer := &quicTracer{recorder: &headerRecorder{}}
792+
t.tracers = append(t.tracers, newTracer)
793+
794+
return newTracer
795+
}
796+
797+
// connectionsInfo returns info for all traced connections.
798+
func (t *testTracer) connectionsInfo() (res []*connInfo) {
799+
res = make([]*connInfo, 0, len(t.tracers))
800+
for _, tracer := range t.tracers {
801+
hdrs := tracer.recorder.headersWithLock()
802+
803+
res = append(res, &connInfo{
804+
headers: hdrs,
805+
})
806+
}
807+
808+
return res
809+
}
810+
811+
// connInfo contains all trace event headers recorded for single connection.
812+
type connInfo struct {
813+
headers []qlog.PacketHeader
814+
}
815+
816+
// is0RTT returns true if the connection used 0-RTT packets.
817+
func (c *connInfo) is0RTT() (ok bool) {
818+
for _, hdr := range c.headers {
819+
if hdr.PacketType == qlog.PacketType0RTT {
820+
return true
821+
}
822+
}
823+
824+
return false
825+
}
826+
827+
// quicTracer is an implementation of [qlogwriter.Trace] for testing.
828+
type quicTracer struct {
829+
// recorder is used for recording trace events. It must not be nil.
830+
recorder *headerRecorder
831+
}
832+
833+
// type check
834+
var _ qlogwriter.Trace = (*quicTracer)(nil)
835+
836+
// AddProducer implements the [qlogwriter.Trace] interface for *quicTracer.
837+
func (q *quicTracer) AddProducer() (recorder qlogwriter.Recorder) {
838+
return q.recorder
839+
}
840+
841+
// SupportsSchemas implements the [qlogwriter.Trace] interface for *quicTracer.
842+
func (q *quicTracer) SupportsSchemas(string) (ok bool) {
843+
return false
844+
}
845+
846+
// Recorder is an implementation of [qlogwriter.Recorder] that records
847+
// [qlog.PacketSent] events headers.
848+
type headerRecorder struct {
849+
headers []qlog.PacketHeader
850+
mx sync.Mutex
851+
}
852+
853+
// type check
854+
var _ qlogwriter.Recorder = (*headerRecorder)(nil)
855+
856+
// RecordEvent implements the [qlogwriter.Recorder] interface for
857+
// *headerRecorder.
858+
func (r *headerRecorder) RecordEvent(ev qlogwriter.Event) {
859+
event, ok := ev.(qlog.PacketSent)
860+
if !ok {
861+
return
862+
}
863+
864+
r.mx.Lock()
865+
defer r.mx.Unlock()
866+
867+
r.headers = append(r.headers, event.Header)
868+
}
869+
870+
// headersWithLock returns copy of recorded headers. It is safe for concurrent
871+
// use.
872+
func (r *headerRecorder) headersWithLock() (res []qlog.PacketHeader) {
873+
r.mx.Lock()
874+
defer r.mx.Unlock()
875+
876+
res = r.headers
877+
878+
return res
879+
}
880+
881+
// Close implements the [qlogwriter.Recorder] interface for
882+
// *headerRecorder.
883+
func (*headerRecorder) Close() (err error) {
884+
return nil
885+
}

0 commit comments

Comments
 (0)