Skip to content

Commit 68457db

Browse files
committed
Pull request #420: 484-fix-quic-buffers
Merge in GO/dnsproxy from 484-fix-quic-buffers to master * commit '70dfcdc18d5992a507781932567ad48a7e91dad7': upstream: imp test upstream: imp code all: imp code, docs all: imp test code Fix DoQ server parsing to avoid stale pooled bytes Fix DoQ response parsing and add partial read regression test
2 parents d2736e5 + 70dfcdc commit 68457db

5 files changed

Lines changed: 244 additions & 19 deletions

File tree

proxy/proxy_internal_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ var defaultTrustedProxies netutil.SubnetSet = netutil.SliceSubnetSet{
6363
// mustNew wraps [New] function failing the test on error.
6464
//
6565
// TODO(e.burkov): Move into the proxytest package.
66+
//
67+
// TODO(e.burkov): Put default values into configuration.
6668
func mustNew(tb testing.TB, conf *Config) (p *Proxy) {
6769
tb.Helper()
6870

proxy/serverquic.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,9 +248,9 @@ func (p *Proxy) handleQUICStream(ctx context.Context, stream *quic.Stream, conn
248248
// draft DNS messages were not prefixed with the message length.
249249
packetLen := binary.BigEndian.Uint16(buf[:2])
250250
if packetLen == uint16(n-2) {
251-
err = req.Unpack(buf[2:])
251+
err = req.Unpack(buf[2:n])
252252
} else {
253-
err = req.Unpack(buf)
253+
err = req.Unpack(buf[:n])
254254
doqVersion = DoQv1Draft
255255
}
256256

proxy/serverquic_internal_test.go

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@ import (
1111

1212
"github.com/AdguardTeam/dnsproxy/proxyutil"
1313
"github.com/AdguardTeam/golibs/logutil/slogutil"
14+
"github.com/AdguardTeam/golibs/syncutil"
1415
"github.com/AdguardTeam/golibs/testutil"
1516
"github.com/AdguardTeam/golibs/testutil/servicetest"
1617
"github.com/miekg/dns"
1718
"github.com/quic-go/quic-go"
19+
"github.com/stretchr/testify/assert"
1820
"github.com/stretchr/testify/require"
1921
)
2022

21-
func TestQuicProxy(t *testing.T) {
23+
func TestProxy_quic(t *testing.T) {
2224
serverConfig, caPem := newTLSConfig(t)
2325

2426
roots := x509.NewCertPool()
@@ -83,7 +85,7 @@ func TestQuicProxy(t *testing.T) {
8385
})
8486
}
8587

86-
func TestQuicProxy_largePackets(t *testing.T) {
88+
func TestProxy_quicLargePackets(t *testing.T) {
8789
reqHandler := &TestHandler{
8890
OnHandle: func(_ *Proxy, d *DNSContext) (err error) {
8991
d.Res = newTestResponse(d)
@@ -141,6 +143,88 @@ func TestQuicProxy_largePackets(t *testing.T) {
141143
requireResponse(t, msg, resp)
142144
}
143145

146+
func TestProxy_quicTruncatedRequest(t *testing.T) {
147+
serverConfig, caPem := newTLSConfig(t)
148+
149+
conf := &Config{
150+
Logger: slogutil.NewDiscardLogger(),
151+
QUICListenAddr: []*net.UDPAddr{net.UDPAddrFromAddrPort(localhostAnyPort)},
152+
TLSConfig: serverConfig,
153+
UpstreamConfig: newTestUpstreamConfig(t, defaultTimeout, testDefaultUpstreamAddr),
154+
TrustedProxies: defaultTrustedProxies,
155+
RatelimitSubnetLenIPv4: 24,
156+
RatelimitSubnetLenIPv6: 64,
157+
RequestHandler: &TestHandler{
158+
OnHandle: func(p *Proxy, d *DNSContext) (_ error) {
159+
panic(testutil.UnexpectedCall(p, d))
160+
},
161+
},
162+
}
163+
164+
dnsProxy := mustNew(t, conf)
165+
166+
req := (&dns.Msg{
167+
MsgHdr: dns.MsgHdr{
168+
Id: 0,
169+
RecursionDesired: true,
170+
},
171+
Question: []dns.Question{{
172+
Name: "example.org.",
173+
Qtype: dns.TypeA,
174+
Qclass: dns.ClassINET,
175+
}},
176+
}).SetEdns0(4096, false)
177+
178+
packed, err := req.Pack()
179+
require.NoError(t, err)
180+
181+
fullBuf := proxyutil.AddPrefix(packed)
182+
183+
dnsProxy.bytesPool = syncutil.NewPool(func() (v *[]byte) {
184+
b := make([]byte, 2+dns.MaxMsgSize)
185+
copy(b, fullBuf)
186+
187+
return &b
188+
})
189+
190+
servicetest.RequireRun(t, dnsProxy, testTimeout)
191+
192+
addr := dnsProxy.Addr(ProtoQUIC)
193+
194+
roots := x509.NewCertPool()
195+
require.True(t, roots.AppendCertsFromPEM(caPem))
196+
197+
tlsConfig := &tls.Config{
198+
ServerName: tlsServerName,
199+
RootCAs: roots,
200+
NextProtos: append([]string{NextProtoDQ}, compatProtoDQ...),
201+
}
202+
203+
ctx := testutil.ContextWithTimeout(t, testTimeout)
204+
205+
conn, err := quic.DialAddrEarly(ctx, addr.String(), tlsConfig, nil)
206+
require.NoError(t, err)
207+
testutil.CleanupAndRequireSuccess(t, func() (err error) {
208+
return conn.CloseWithError(DoQCodeNoError, "")
209+
})
210+
211+
truncLen := len(packed) - len(packed)/2
212+
require.Greater(t, truncLen, 0)
213+
214+
truncated := packed[:truncLen]
215+
reqBuf := proxyutil.AddPrefix(truncated)
216+
require.Greater(t, len(reqBuf), minDNSPacketSize)
217+
218+
stream, err := conn.OpenStreamSync(ctx)
219+
require.NoError(t, err)
220+
testutil.CleanupAndRequireSuccess(t, stream.Close)
221+
222+
n, err := stream.Write(reqBuf)
223+
require.NoError(t, err)
224+
225+
assert.Greater(t, n, minDNSPacketSize)
226+
}
227+
144228
// newTestResponse creates a test response for the specified request.
145229
func newTestResponse(d *DNSContext) (resp *dns.Msg) {
146230
resp = &dns.Msg{}

upstream/doq.go

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package upstream
33
import (
44
"context"
55
"crypto/tls"
6+
"encoding/binary"
67
"fmt"
8+
"io"
79
"log/slog"
810
"net"
911
"net/url"
@@ -15,6 +17,7 @@ import (
1517
"github.com/AdguardTeam/dnsproxy/proxyutil"
1618
"github.com/AdguardTeam/golibs/errors"
1719
"github.com/AdguardTeam/golibs/logutil/slogutil"
20+
"github.com/AdguardTeam/golibs/validate"
1821
"github.com/miekg/dns"
1922
"github.com/quic-go/quic-go"
2023
"github.com/quic-go/quic-go/http3"
@@ -95,6 +98,23 @@ type dnsOverQUIC struct {
9598
timeout time.Duration
9699
}
97100

101+
// quicStream is the interface of QUIC stream used by readMsg to simplify
102+
// testing.
103+
//
104+
// Note, that this interface is implemented by [*quic.Stream].
105+
type quicStream interface {
106+
// Read reads data from the stream. If the stream was canceled, the error
107+
// is a [quic.StreamError].
108+
Read(p []byte) (n int, err error)
109+
110+
// CancelRead aborts receiving on this stream. See
111+
// [quic.ReceiveStream.CancelRead] for more details.
112+
CancelRead(code quic.StreamErrorCode)
113+
}
114+
115+
// type check
116+
var _ quicStream = (*quic.Stream)(nil)
117+
98118
// newDoQ returns the DNS-over-QUIC Upstream.
99119
func newDoQ(addr *url.URL, opts *Options) (u Upstream, err error) {
100120
addPort(addr, defaultPortDoQ)
@@ -397,29 +417,42 @@ func (p *dnsOverQUIC) closeConnWithError(conn *quic.Conn, err error) {
397417
}
398418

399419
// readMsg reads the incoming DNS message from the QUIC stream.
400-
func (p *dnsOverQUIC) readMsg(stream *quic.Stream) (m *dns.Msg, err error) {
420+
func (p *dnsOverQUIC) readMsg(stream quicStream) (m *dns.Msg, err error) {
421+
defer func() { err = errors.Annotate(err, "from %s: %w", p.addr) }()
422+
423+
var lenBuf [2]byte
424+
_, err = io.ReadFull(stream, lenBuf[:])
425+
if err != nil {
426+
return nil, fmt.Errorf("reading response length: %w", err)
427+
}
428+
429+
msgLen := binary.BigEndian.Uint16(lenBuf[:])
430+
err = validate.Positive("response length", msgLen)
431+
if err != nil {
432+
// Don't wrap the error, since it's informative enough as is.
433+
return nil, err
434+
}
435+
401436
pool := p.getBytesPool()
402437
bufPtr := pool.Get().(*[]byte)
403-
404438
defer pool.Put(bufPtr)
405439

406-
respBuf := *bufPtr
407-
n, err := stream.Read(respBuf)
408-
if err != nil && n == 0 {
409-
return nil, fmt.Errorf("reading response from %s: %w", p.addr, err)
440+
respBuf := (*bufPtr)[:msgLen]
441+
442+
_, err = io.ReadFull(stream, respBuf)
443+
if err != nil {
444+
return nil, err
410445
}
411446

412447
stream.CancelRead(0)
413448

414449
// All DNS messages (queries and responses) sent over DoQ connections MUST
415450
// be encoded as a 2-octet length field followed by the message content as
416451
// specified in [RFC1035].
417-
// IMPORTANT: Note, that we ignore this prefix here as this implementation
418-
// does not support receiving multiple messages over a single connection.
419-
m = new(dns.Msg)
420-
err = m.Unpack(respBuf[2:])
452+
m = &dns.Msg{}
453+
err = m.Unpack(respBuf)
421454
if err != nil {
422-
return nil, fmt.Errorf("unpacking response from %s: %w", p.addr, err)
455+
return nil, fmt.Errorf("from %s: unpacking response: %w", p.addr, err)
423456
}
424457

425458
return m, nil

upstream/doq_internal_test.go

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

27-
func TestUpstreamDoQ(t *testing.T) {
27+
func TestDNSOverQUIC(t *testing.T) {
2828
tlsConf, rootCAs := createServerTLSConfig(t, "127.0.0.1")
2929

3030
srv := startDoQServer(t, tlsConf, 0)
@@ -77,7 +77,7 @@ func TestUpstreamDoQ(t *testing.T) {
7777
checkRaceCondition(u)
7878
}
7979

80-
func TestUpstream_Exchange_quicServerCloseConn(t *testing.T) {
80+
func TestDNSOverQUIC_Exchange_quicCloseConn(t *testing.T) {
8181
// Use the same tlsConf for all servers to preserve the data necessary for
8282
// 0-RTT connections.
8383
tlsConf, rootCAs := createServerTLSConfig(t, "127.0.0.1")
@@ -129,7 +129,7 @@ func TestUpstream_Exchange_quicServerCloseConn(t *testing.T) {
129129
wg.Wait()
130130
}
131131

132-
func TestUpstreamDoQ_serverRestart(t *testing.T) {
132+
func TestDNSOverQUIC_serverRestart(t *testing.T) {
133133
t.Parallel()
134134

135135
// Use the same tlsConf for all servers to preserve the data necessary for
@@ -182,7 +182,7 @@ func TestUpstreamDoQ_serverRestart(t *testing.T) {
182182
})
183183
}
184184

185-
func TestUpstreamDoQ_0RTT(t *testing.T) {
185+
func TestDNSOverQUIC_0RTT(t *testing.T) {
186186
tlsConf, rootCAs := createServerTLSConfig(t, "127.0.0.1")
187187

188188
srv := startDoQServer(t, tlsConf, 0)
@@ -232,6 +232,112 @@ func TestUpstreamDoQ_0RTT(t *testing.T) {
232232
require.True(t, conns[1].is0RTT())
233233
}
234234

235+
func TestDNSOverQUIC_ReadMsg_partialRead(t *testing.T) {
236+
oldReq := createHostTestMessage("old.example")
237+
oldResp := (&dns.Msg{}).SetReply(oldReq)
238+
oldResp.Answer = []dns.RR{&dns.A{
239+
Hdr: dns.RR_Header{
240+
Name: dns.Fqdn("old.example"),
241+
Rrtype: dns.TypeA,
242+
Class: dns.ClassINET,
243+
Ttl: 60,
244+
},
245+
A: net.IP{192, 0, 2, 1},
246+
}}
247+
248+
oldPacked, err := oldResp.Pack()
249+
require.NoError(t, err)
250+
251+
oldBuf := make([]byte, 2+len(oldPacked))
252+
binary.BigEndian.PutUint16(oldBuf, uint16(len(oldPacked)))
253+
copy(oldBuf[2:], oldPacked)
254+
255+
pool := &sync.Pool{
256+
New: func() (buf any) {
257+
b := make([]byte, dns.MaxMsgSize)
258+
copy(b, oldBuf)
259+
260+
return &b
261+
},
262+
}
263+
264+
ups, err := newDoQ(&url.URL{Host: "example:853"}, &Options{})
265+
require.NoError(t, err)
266+
testutil.CleanupAndRequireSuccess(t, ups.Close)
267+
268+
doq := testutil.RequireTypeAssert[*dnsOverQUIC](t, ups)
269+
doq.bytesPool = pool
270+
271+
newAnsIP := net.IP{192, 0, 2, 2}
272+
273+
newReq := createHostTestMessage("new.example")
274+
newResp := (&dns.Msg{}).SetReply(newReq)
275+
newResp.Answer = []dns.RR{&dns.A{
276+
Hdr: dns.RR_Header{
277+
Name: dns.Fqdn("new.example"),
278+
Rrtype: dns.TypeA,
279+
Class: dns.ClassINET,
280+
Ttl: 60,
281+
},
282+
A: newAnsIP,
283+
}}
284+
285+
newPacked, err := newResp.Pack()
286+
require.NoError(t, err)
287+
288+
newBuf := make([]byte, 2+len(newPacked))
289+
binary.BigEndian.PutUint16(newBuf, uint16(len(newPacked)))
290+
copy(newBuf[2:], newPacked)
291+
292+
stream := &testQUICStream{
293+
onRead: func(p []byte) (n int, err error) {
294+
if len(newBuf) == 0 {
295+
return 0, io.EOF
296+
}
297+
298+
n = min(1, len(newBuf), len(p))
299+
300+
copy(p, newBuf[:n])
301+
newBuf = newBuf[n:]
302+
303+
return n, nil
304+
},
305+
onCancelRead: func(code quic.StreamErrorCode) {
306+
assert.Equal(t, quic.StreamErrorCode(0), code)
307+
},
308+
}
309+
310+
got, err := doq.readMsg(stream)
311+
require.NoError(t, err)
312+
require.Len(t, got.Answer, 1)
313+
314+
assert.Equal(t, dns.Fqdn("new.example"), got.Question[0].Name)
315+
316+
a := testutil.RequireTypeAssert[*dns.A](t, got.Answer[0])
317+
318+
assert.Equal(t, newAnsIP, a.A)
319+
}
320+
321+
// testQUICStream is a mock implementation of the [quicStream] interface for
322+
// tests.
323+
type testQUICStream struct {
324+
onRead func(p []byte) (n int, err error)
325+
onCancelRead func(code quic.StreamErrorCode)
326+
}
327+
328+
// type check
329+
var _ quicStream = (*testQUICStream)(nil)
330+
331+
// Read implements the [quicStream] interface for *testQUICStream.
332+
func (s *testQUICStream) Read(p []byte) (n int, err error) {
333+
return s.onRead(p)
334+
}
335+
336+
// CancelRead implements the [quicStream] interface for *testQUICStream.
337+
func (s *testQUICStream) CancelRead(code quic.StreamErrorCode) {
338+
s.onCancelRead(code)
339+
}
340+
235341
// testDoHServer is an instance of a test DNS-over-QUIC server.
236342
type testDoQServer struct {
237343
// listener is the QUIC connections listener.

0 commit comments

Comments
 (0)