Skip to content

Commit 40df7e1

Browse files
committed
based on Release v2026.04.04.040456-af79daf
1 parent 8779647 commit 40df7e1

11 files changed

Lines changed: 530 additions & 40 deletions

File tree

internal/client/tunnel_runtime.go

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
package client
1313

1414
import (
15+
"encoding/binary"
1516
"errors"
1617
"net"
1718
"time"
@@ -24,6 +25,7 @@ const (
2425
// RuntimeUDPReadBufferSize defines the maximum size of the UDP read buffer.
2526
RuntimeUDPReadBufferSize = 65535
2627
runtimeUDPMaxMismatchedResponses = 64
28+
runtimeUDPDrainGrace = time.Millisecond
2729

2830
// pooledConnMaxAge is the maximum time a UDP connection can remain idle in
2931
// the resolver pool before it is discarded and re-dialed. Stale connections can have their
@@ -37,45 +39,76 @@ type pooledUDPConn struct {
3739
pooledAt time.Time
3840
}
3941

42+
func (c *Client) drainStaleUDPResponses(conn *net.UDPConn, buffer []byte) error {
43+
if conn == nil || len(buffer) == 0 {
44+
return nil
45+
}
46+
47+
for drained := 0; drained < runtimeUDPMaxMismatchedResponses*2; drained++ {
48+
if err := conn.SetReadDeadline(time.Now().Add(runtimeUDPDrainGrace)); err != nil {
49+
return err
50+
}
51+
52+
_, err := conn.Read(buffer)
53+
if err != nil {
54+
if ne, ok := err.(net.Error); ok && ne.Timeout() {
55+
return nil
56+
}
57+
return err
58+
}
59+
}
60+
61+
return nil
62+
}
63+
4064
// exchangeUDPQueryWithConn sends one UDP packet through the provided connection
4165
// and waits for a response with a matching DNS transaction ID.
4266
func (c *Client) exchangeUDPQueryWithConn(conn *net.UDPConn, packet []byte, timeout time.Duration) ([]byte, error) {
4367
if len(packet) < 2 {
4468
return nil, errors.New("malformed dns query")
4569
}
46-
expectedID0 := packet[0]
47-
expectedID1 := packet[1]
70+
expectedID := binary.BigEndian.Uint16(packet[:2])
71+
72+
buffer := c.getRuntimeUDPBuffer()
73+
defer c.putRuntimeUDPBuffer(buffer)
74+
defer func() {
75+
_ = conn.SetDeadline(time.Time{})
76+
}()
77+
78+
if err := c.drainStaleUDPResponses(conn, buffer); err != nil {
79+
return nil, err
80+
}
4881

49-
deadline := time.Now().Add(timeout)
50-
if err := conn.SetDeadline(deadline); err != nil {
82+
writeDeadline := time.Now().Add(timeout)
83+
if err := conn.SetWriteDeadline(writeDeadline); err != nil {
5184
return nil, err
5285
}
5386

5487
if _, err := conn.Write(packet); err != nil {
5588
return nil, err
5689
}
5790

58-
buffer := c.getRuntimeUDPBuffer()
91+
if err := conn.SetReadDeadline(writeDeadline); err != nil {
92+
return nil, err
93+
}
94+
5995
mismatchedResponses := 0
6096

6197
for {
6298
n, err := conn.Read(buffer)
6399
if err != nil {
64-
c.putRuntimeUDPBuffer(buffer)
65100
return nil, err
66101
}
67102

68-
if n >= 2 && buffer[0] == expectedID0 && buffer[1] == expectedID1 {
103+
if n >= 2 && binary.BigEndian.Uint16(buffer[:2]) == expectedID {
69104
// Copy matched response out so the pooled buffer can be recycled.
70105
result := make([]byte, n)
71106
copy(result, buffer[:n])
72-
c.putRuntimeUDPBuffer(buffer)
73107
return result, nil
74108
}
75109

76110
mismatchedResponses++
77111
if mismatchedResponses >= runtimeUDPMaxMismatchedResponses {
78-
c.putRuntimeUDPBuffer(buffer)
79112
return nil, errors.New("too many mismatched dns responses on shared udp socket")
80113
}
81114
}

internal/client/tunnel_runtime_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package client
99

1010
import (
11+
"encoding/binary"
1112
"net"
1213
"strings"
1314
"testing"
@@ -93,3 +94,64 @@ func TestExchangeUDPQueryWithConnFailsAfterTooManyMismatches(t *testing.T) {
9394

9495
<-done
9596
}
97+
98+
func TestExchangeUDPQueryWithConnDrainsStaleResponsesBeforeSending(t *testing.T) {
99+
serverConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
100+
if err != nil {
101+
t.Fatalf("ListenUDP server failed: %v", err)
102+
}
103+
defer serverConn.Close()
104+
105+
clientConn, err := net.DialUDP("udp", nil, serverConn.LocalAddr().(*net.UDPAddr))
106+
if err != nil {
107+
t.Fatalf("DialUDP client failed: %v", err)
108+
}
109+
defer clientConn.Close()
110+
111+
serverDone := make(chan struct{})
112+
go func() {
113+
defer close(serverDone)
114+
buf := make([]byte, 512)
115+
116+
n, addr, err := serverConn.ReadFromUDP(buf)
117+
if err != nil || n < 2 {
118+
return
119+
}
120+
121+
time.Sleep(40 * time.Millisecond)
122+
for i := 0; i < runtimeUDPMaxMismatchedResponses+4; i++ {
123+
_, _ = serverConn.WriteToUDP([]byte{buf[0], buf[1], 0x81, byte(i)}, addr)
124+
}
125+
126+
n, addr, err = serverConn.ReadFromUDP(buf)
127+
if err != nil || n < 2 {
128+
return
129+
}
130+
131+
resp := []byte{buf[0], buf[1], 0x81, 0x00}
132+
_, _ = serverConn.WriteToUDP(resp, addr)
133+
}()
134+
135+
c := &Client{}
136+
137+
firstQuery := make([]byte, 4)
138+
binary.BigEndian.PutUint16(firstQuery[0:2], 0x1234)
139+
_, err = c.exchangeUDPQueryWithConn(clientConn, firstQuery, 10*time.Millisecond)
140+
if err == nil {
141+
t.Fatal("expected first query to time out before delayed stale responses arrive")
142+
}
143+
144+
time.Sleep(60 * time.Millisecond)
145+
146+
secondQuery := make([]byte, 4)
147+
binary.BigEndian.PutUint16(secondQuery[0:2], 0x5678)
148+
resp, err := c.exchangeUDPQueryWithConn(clientConn, secondQuery, 500*time.Millisecond)
149+
if err != nil {
150+
t.Fatalf("second query failed after stale drain: %v", err)
151+
}
152+
if len(resp) < 2 || binary.BigEndian.Uint16(resp[0:2]) != 0x5678 {
153+
t.Fatalf("unexpected second response: %#v", resp)
154+
}
155+
156+
<-serverDone
157+
}

internal/dnsparser/parser.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,10 +321,9 @@ func parseName(data []byte, offset int) (string, int, error) {
321321
return name.String(), origNext, nil
322322
}
323323

324-
325324
func writeLowerASCIILabel(dst *strings.Builder, label []byte) {
326325
upperIndex := -1
327-
for i := 0; i < len(label); i++ {
326+
for i := range label {
328327
if label[i] >= 'A' && label[i] <= 'Z' {
329328
upperIndex = i
330329
break

internal/dnsparser/response.go

Lines changed: 126 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@ func BuildEmptyNoErrorResponseFromLite(request []byte, parsed LitePacket) ([]byt
2828
return buildResponseWithRCodeLite(request, parsed, Enums.DNSR_CODE_NO_ERROR)
2929
}
3030

31+
func BuildNoDataResponse(request []byte) ([]byte, error) {
32+
parsed, err := ParseDNSRequestLite(request)
33+
if err != nil {
34+
return nil, err
35+
}
36+
return BuildNoDataResponseFromLite(request, parsed)
37+
}
38+
39+
func BuildNoDataResponseFromLite(request []byte, parsed LitePacket) ([]byte, error) {
40+
return buildNoDataResponseLite(request, parsed)
41+
}
42+
3143
func BuildFormatErrorResponse(request []byte) ([]byte, error) {
3244
return buildResponseWithRCode(request, Enums.DNSR_CODE_FORMAT_ERROR)
3345
}
@@ -52,6 +64,27 @@ func BuildNotImplementedResponseFromLite(request []byte, parsed LitePacket) ([]b
5264
return buildResponseWithRCodeLite(request, parsed, Enums.DNSR_CODE_NOT_IMPLEMENTED)
5365
}
5466

67+
const (
68+
syntheticNoDataTTL = 60
69+
syntheticNoDataRefresh = 60
70+
syntheticNoDataRetry = 60
71+
syntheticNoDataExpire = 300
72+
syntheticNoDataMinimum = 60
73+
)
74+
75+
var (
76+
syntheticSOAMName = []byte{
77+
0x02, 'n', 's',
78+
0x07, 'i', 'n', 'v', 'a', 'l', 'i', 'd',
79+
0x00,
80+
}
81+
syntheticSOARName = []byte{
82+
0x0a, 'h', 'o', 's', 't', 'm', 'a', 's', 't', 'e', 'r',
83+
0x07, 'i', 'n', 'v', 'a', 'l', 'i', 'd',
84+
0x00,
85+
}
86+
)
87+
5588
func buildResponseWithRCode(request []byte, rcode uint8) ([]byte, error) {
5689
if len(request) < dnsHeaderSize {
5790
return nil, ErrPacketTooShort
@@ -70,7 +103,6 @@ func buildResponseWithRCode(request []byte, rcode uint8) ([]byte, error) {
70103
questionCount = header.QDCount
71104
}
72105

73-
74106
optStart, optLen := findOPTRecordRange(request, header, questionEndOffset)
75107

76108
response := make([]byte, dnsHeaderSize+questionLen+optLen)
@@ -87,7 +119,6 @@ func buildResponseWithRCode(request []byte, rcode uint8) ([]byte, error) {
87119
copy(response[dnsHeaderSize+questionLen:], request[optStart:optStart+optLen])
88120
}
89121

90-
91122
return response, nil
92123
}
93124

@@ -122,14 +153,83 @@ func buildResponseWithRCodeLite(request []byte, parsed LitePacket, rcode uint8)
122153
return response, nil
123154
}
124155

156+
func buildNoDataResponseLite(request []byte, parsed LitePacket) ([]byte, error) {
157+
if len(request) < dnsHeaderSize {
158+
return nil, ErrPacketTooShort
159+
}
160+
if !isLikelyDNSRequestHeader(parsed.Header) {
161+
return nil, ErrNotDNSRequest
162+
}
163+
164+
optStart, optLen := findOPTRecordRange(request, parsed.Header, parsed.QuestionEndOffset)
165+
166+
questionLen := 0
167+
if parsed.QuestionEndOffset >= dnsHeaderSize && parsed.QuestionEndOffset <= len(request) {
168+
questionLen = parsed.QuestionEndOffset - dnsHeaderSize
169+
}
170+
171+
authority := buildSyntheticSOAAuthority(parsed)
172+
response := make([]byte, dnsHeaderSize+questionLen+len(authority)+optLen)
173+
binary.BigEndian.PutUint16(response[0:2], parsed.Header.ID)
174+
binary.BigEndian.PutUint16(response[2:4], buildResponseFlags(parsed.Header.Flags, Enums.DNSR_CODE_NO_ERROR))
175+
binary.BigEndian.PutUint16(response[4:6], parsed.Header.QDCount)
176+
if len(authority) > 0 {
177+
binary.BigEndian.PutUint16(response[8:10], 1)
178+
}
179+
binary.BigEndian.PutUint16(response[10:12], uint16(getARCount(optLen)))
180+
181+
offset := dnsHeaderSize
182+
if questionLen > 0 {
183+
copy(response[offset:], request[dnsHeaderSize:parsed.QuestionEndOffset])
184+
offset += questionLen
185+
}
186+
if len(authority) > 0 {
187+
copy(response[offset:], authority)
188+
offset += len(authority)
189+
}
190+
if optLen > 0 {
191+
copy(response[offset:], request[optStart:optStart+optLen])
192+
}
193+
194+
return response, nil
195+
}
196+
197+
func buildSyntheticSOAAuthority(parsed LitePacket) []byte {
198+
owner := []byte{0}
199+
if parsed.HasQuestion {
200+
// Reuse the first question name via compression pointer to keep the
201+
// synthetic authority compact and avoid re-encoding the qname.
202+
owner = []byte{0xC0, 0x0C}
203+
}
204+
205+
rdataLen := len(syntheticSOAMName) + len(syntheticSOARName) + 20
206+
record := make([]byte, len(owner)+10+rdataLen)
207+
208+
offset := copy(record, owner)
209+
binary.BigEndian.PutUint16(record[offset:offset+2], Enums.DNS_RECORD_TYPE_SOA)
210+
binary.BigEndian.PutUint16(record[offset+2:offset+4], Enums.DNSQ_CLASS_IN)
211+
binary.BigEndian.PutUint32(record[offset+4:offset+8], syntheticNoDataTTL)
212+
binary.BigEndian.PutUint16(record[offset+8:offset+10], uint16(rdataLen))
213+
offset += 10
214+
215+
offset += copy(record[offset:], syntheticSOAMName)
216+
offset += copy(record[offset:], syntheticSOARName)
217+
binary.BigEndian.PutUint32(record[offset:offset+4], 1)
218+
binary.BigEndian.PutUint32(record[offset+4:offset+8], syntheticNoDataRefresh)
219+
binary.BigEndian.PutUint32(record[offset+8:offset+12], syntheticNoDataRetry)
220+
binary.BigEndian.PutUint32(record[offset+12:offset+16], syntheticNoDataExpire)
221+
binary.BigEndian.PutUint32(record[offset+16:offset+20], syntheticNoDataMinimum)
222+
223+
return record
224+
}
225+
125226
func getARCount(optLen int) int {
126227
if optLen > 0 {
127228
return 1
128229
}
129230
return 0
130231
}
131232

132-
133233
func isLikelyDNSRequestHeader(header Header) bool {
134234
if header.QR != 0 {
135235
return false
@@ -153,7 +253,29 @@ func isLikelyDNSRequestHeader(header Header) bool {
153253
}
154254

155255
func buildResponseFlags(requestFlags uint16, rcode uint8) uint16 {
156-
return (1 << 15) | (requestFlags & 0x7810) | uint16(rcode&0x0F)
256+
const (
257+
flagQR uint16 = 1 << 15
258+
flagAA uint16 = 1 << 10
259+
flagTC uint16 = 1 << 9
260+
flagRD uint16 = 1 << 8
261+
flagRA uint16 = 1 << 7
262+
flagCD uint16 = 1 << 4
263+
opcodeMask uint16 = 0x7800
264+
)
265+
266+
flags := flagQR | flagRA | (requestFlags & opcodeMask) | uint16(rcode&0x0F)
267+
if requestFlags&flagRD != 0 {
268+
flags |= flagRD
269+
}
270+
if requestFlags&flagCD != 0 {
271+
flags |= flagCD
272+
}
273+
274+
// Resolver-generated local answers should look recursive, not authoritative.
275+
// AA/TC are intentionally cleared unless we are relaying an upstream answer
276+
// verbatim, in which case those bits come from the upstream packet itself.
277+
flags &^= flagAA | flagTC
278+
return flags
157279
}
158280

159281
func extractQuestionSection(request []byte, header Header) ([]byte, uint16, int) {
@@ -244,7 +366,6 @@ func findFirstOPTRecordInAdditional(data []byte, offset int, count int) (int, in
244366
return 0, 0
245367
}
246368

247-
248369
func skipQuestions(data []byte, offset int, count int) (int, error) {
249370
for range count {
250371
nextOffset, err := skipName(data, offset)
@@ -345,4 +466,3 @@ func skipName(data []byte, offset int) (int, error) {
345466
offset += length + 1
346467
}
347468
}
348-

0 commit comments

Comments
 (0)