Skip to content

Commit dbc1703

Browse files
jfbusbradfitz
authored andcommitted
net: support single-request resolv.conf option in pure Go resolver
There is a DNS resolution issue in Kubernetes (UDP response packets get dropped due to a race in conntrack between the parallel A and AAAA queries, causing timeouts in DNS queries). A workaround is to enable single-request / single-request-reopen in resolv.conf in order to use sequential A and AAAA queries instead of parallel queries. With this PR, the pure Go resolver searches for "single-request" and "single-request-reopen" in resolv.conf and send A and AAAA queries sequentially when found. Fixes #29644 Change-Id: I906b3484008c1b9adf2e3e9241ea23767e29df59 GitHub-Last-Rev: d481acf GitHub-Pull-Request: #29661 Reviewed-on: https://go-review.googlesource.com/c/go/+/157377 Run-TryBot: Brad Fitzpatrick <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]>
1 parent 33e5da4 commit dbc1703

6 files changed

+155
-31
lines changed

src/net/dnsclient_unix.go

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -569,34 +569,52 @@ func (r *Resolver) goLookupIPCNAMEOrder(ctx context.Context, name string, order
569569
resolvConf.mu.RLock()
570570
conf := resolvConf.dnsConfig
571571
resolvConf.mu.RUnlock()
572-
type racer struct {
572+
type result struct {
573573
p dnsmessage.Parser
574574
server string
575575
error
576576
}
577-
lane := make(chan racer, 1)
577+
lane := make(chan result, 1)
578578
qtypes := [...]dnsmessage.Type{dnsmessage.TypeA, dnsmessage.TypeAAAA}
579-
var lastErr error
580-
for _, fqdn := range conf.nameList(name) {
581-
for _, qtype := range qtypes {
579+
var queryFn func(fqdn string, qtype dnsmessage.Type)
580+
var responseFn func(fqdn string, qtype dnsmessage.Type) result
581+
if conf.singleRequest {
582+
queryFn = func(fqdn string, qtype dnsmessage.Type) {}
583+
responseFn = func(fqdn string, qtype dnsmessage.Type) result {
584+
dnsWaitGroup.Add(1)
585+
defer dnsWaitGroup.Done()
586+
p, server, err := r.tryOneName(ctx, conf, fqdn, qtype)
587+
return result{p, server, err}
588+
}
589+
} else {
590+
queryFn = func(fqdn string, qtype dnsmessage.Type) {
582591
dnsWaitGroup.Add(1)
583592
go func(qtype dnsmessage.Type) {
584593
p, server, err := r.tryOneName(ctx, conf, fqdn, qtype)
585-
lane <- racer{p, server, err}
594+
lane <- result{p, server, err}
586595
dnsWaitGroup.Done()
587596
}(qtype)
588597
}
598+
responseFn = func(fqdn string, qtype dnsmessage.Type) result {
599+
return <-lane
600+
}
601+
}
602+
var lastErr error
603+
for _, fqdn := range conf.nameList(name) {
604+
for _, qtype := range qtypes {
605+
queryFn(fqdn, qtype)
606+
}
589607
hitStrictError := false
590-
for range qtypes {
591-
racer := <-lane
592-
if racer.error != nil {
593-
if nerr, ok := racer.error.(Error); ok && nerr.Temporary() && r.strictErrors() {
608+
for _, qtype := range qtypes {
609+
result := responseFn(fqdn, qtype)
610+
if result.error != nil {
611+
if nerr, ok := result.error.(Error); ok && nerr.Temporary() && r.strictErrors() {
594612
// This error will abort the nameList loop.
595613
hitStrictError = true
596-
lastErr = racer.error
614+
lastErr = result.error
597615
} else if lastErr == nil || fqdn == name+"." {
598616
// Prefer error for original name.
599-
lastErr = racer.error
617+
lastErr = result.error
600618
}
601619
continue
602620
}
@@ -618,48 +636,48 @@ func (r *Resolver) goLookupIPCNAMEOrder(ctx context.Context, name string, order
618636

619637
loop:
620638
for {
621-
h, err := racer.p.AnswerHeader()
639+
h, err := result.p.AnswerHeader()
622640
if err != nil && err != dnsmessage.ErrSectionDone {
623641
lastErr = &DNSError{
624642
Err: "cannot marshal DNS message",
625643
Name: name,
626-
Server: racer.server,
644+
Server: result.server,
627645
}
628646
}
629647
if err != nil {
630648
break
631649
}
632650
switch h.Type {
633651
case dnsmessage.TypeA:
634-
a, err := racer.p.AResource()
652+
a, err := result.p.AResource()
635653
if err != nil {
636654
lastErr = &DNSError{
637655
Err: "cannot marshal DNS message",
638656
Name: name,
639-
Server: racer.server,
657+
Server: result.server,
640658
}
641659
break loop
642660
}
643661
addrs = append(addrs, IPAddr{IP: IP(a.A[:])})
644662

645663
case dnsmessage.TypeAAAA:
646-
aaaa, err := racer.p.AAAAResource()
664+
aaaa, err := result.p.AAAAResource()
647665
if err != nil {
648666
lastErr = &DNSError{
649667
Err: "cannot marshal DNS message",
650668
Name: name,
651-
Server: racer.server,
669+
Server: result.server,
652670
}
653671
break loop
654672
}
655673
addrs = append(addrs, IPAddr{IP: IP(aaaa.AAAA[:])})
656674

657675
default:
658-
if err := racer.p.SkipAnswer(); err != nil {
676+
if err := result.p.SkipAnswer(); err != nil {
659677
lastErr = &DNSError{
660678
Err: "cannot marshal DNS message",
661679
Name: name,
662-
Server: racer.server,
680+
Server: result.server,
663681
}
664682
break loop
665683
}

src/net/dnsclient_unix_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"reflect"
1818
"strings"
1919
"sync"
20+
"sync/atomic"
2021
"testing"
2122
"time"
2223

@@ -1621,3 +1622,76 @@ func TestTXTRecordTwoStrings(t *testing.T) {
16211622
t.Errorf("txt[1], got %q, want %q", txt[1], want)
16221623
}
16231624
}
1625+
1626+
// Issue 29644: support single-request resolv.conf option in pure Go resolver.
1627+
// The A and AAAA queries will be sent sequentially, not in parallel.
1628+
func TestSingleRequestLookup(t *testing.T) {
1629+
defer dnsWaitGroup.Wait()
1630+
var (
1631+
firstcalled int32
1632+
ipv4 int32 = 1
1633+
ipv6 int32 = 2
1634+
)
1635+
fake := fakeDNSServer{rh: func(n, s string, q dnsmessage.Message, _ time.Time) (dnsmessage.Message, error) {
1636+
r := dnsmessage.Message{
1637+
Header: dnsmessage.Header{
1638+
ID: q.ID,
1639+
Response: true,
1640+
},
1641+
Questions: q.Questions,
1642+
}
1643+
for _, question := range q.Questions {
1644+
switch question.Type {
1645+
case dnsmessage.TypeA:
1646+
if question.Name.String() == "slowipv4.example.net." {
1647+
time.Sleep(10 * time.Millisecond)
1648+
}
1649+
if !atomic.CompareAndSwapInt32(&firstcalled, 0, ipv4) {
1650+
t.Errorf("the A query was received after the AAAA query !")
1651+
}
1652+
r.Answers = append(r.Answers, dnsmessage.Resource{
1653+
Header: dnsmessage.ResourceHeader{
1654+
Name: q.Questions[0].Name,
1655+
Type: dnsmessage.TypeA,
1656+
Class: dnsmessage.ClassINET,
1657+
Length: 4,
1658+
},
1659+
Body: &dnsmessage.AResource{
1660+
A: TestAddr,
1661+
},
1662+
})
1663+
case dnsmessage.TypeAAAA:
1664+
atomic.CompareAndSwapInt32(&firstcalled, 0, ipv6)
1665+
r.Answers = append(r.Answers, dnsmessage.Resource{
1666+
Header: dnsmessage.ResourceHeader{
1667+
Name: q.Questions[0].Name,
1668+
Type: dnsmessage.TypeAAAA,
1669+
Class: dnsmessage.ClassINET,
1670+
Length: 16,
1671+
},
1672+
Body: &dnsmessage.AAAAResource{
1673+
AAAA: TestAddr6,
1674+
},
1675+
})
1676+
}
1677+
}
1678+
return r, nil
1679+
}}
1680+
r := Resolver{PreferGo: true, Dial: fake.DialContext}
1681+
1682+
conf, err := newResolvConfTest()
1683+
if err != nil {
1684+
t.Fatal(err)
1685+
}
1686+
defer conf.teardown()
1687+
if err := conf.writeAndUpdate([]string{"options single-request"}); err != nil {
1688+
t.Fatal(err)
1689+
}
1690+
for _, name := range []string{"hostname.example.net", "slowipv4.example.net"} {
1691+
firstcalled = 0
1692+
_, err := r.LookupIPAddr(context.Background(), name)
1693+
if err != nil {
1694+
t.Error(err)
1695+
}
1696+
}
1697+
}

src/net/dnsconfig_unix.go

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,18 @@ var (
2121
)
2222

2323
type dnsConfig struct {
24-
servers []string // server addresses (in host:port form) to use
25-
search []string // rooted suffixes to append to local name
26-
ndots int // number of dots in name to trigger absolute lookup
27-
timeout time.Duration // wait before giving up on a query, including retries
28-
attempts int // lost packets before giving up on server
29-
rotate bool // round robin among servers
30-
unknownOpt bool // anything unknown was encountered
31-
lookup []string // OpenBSD top-level database "lookup" order
32-
err error // any error that occurs during open of resolv.conf
33-
mtime time.Time // time of resolv.conf modification
34-
soffset uint32 // used by serverOffset
24+
servers []string // server addresses (in host:port form) to use
25+
search []string // rooted suffixes to append to local name
26+
ndots int // number of dots in name to trigger absolute lookup
27+
timeout time.Duration // wait before giving up on a query, including retries
28+
attempts int // lost packets before giving up on server
29+
rotate bool // round robin among servers
30+
unknownOpt bool // anything unknown was encountered
31+
lookup []string // OpenBSD top-level database "lookup" order
32+
err error // any error that occurs during open of resolv.conf
33+
mtime time.Time // time of resolv.conf modification
34+
soffset uint32 // used by serverOffset
35+
singleRequest bool // use sequential A and AAAA queries instead of parallel queries
3536
}
3637

3738
// See resolv.conf(5) on a Linux machine.
@@ -115,6 +116,13 @@ func dnsReadConfig(filename string) *dnsConfig {
115116
conf.attempts = n
116117
case s == "rotate":
117118
conf.rotate = true
119+
case s == "single-request" || s == "single-request-reopen":
120+
// Linux option:
121+
// http://man7.org/linux/man-pages/man5/resolv.conf.5.html
122+
// "By default, glibc performs IPv4 and IPv6 lookups in parallel [...]
123+
// This option disables the behavior and makes glibc
124+
// perform the IPv6 and IPv4 requests sequentially."
125+
conf.singleRequest = true
118126
default:
119127
conf.unknownOpt = true
120128
}

src/net/dnsconfig_unix_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,28 @@ var dnsReadConfigTests = []struct {
102102
search: []string{"c.symbolic-datum-552.internal."},
103103
},
104104
},
105+
{
106+
name: "testdata/single-request-resolv.conf",
107+
want: &dnsConfig{
108+
servers: defaultNS,
109+
ndots: 1,
110+
singleRequest: true,
111+
timeout: 5 * time.Second,
112+
attempts: 2,
113+
search: []string{"domain.local."},
114+
},
115+
},
116+
{
117+
name: "testdata/single-request-reopen-resolv.conf",
118+
want: &dnsConfig{
119+
servers: defaultNS,
120+
ndots: 1,
121+
singleRequest: true,
122+
timeout: 5 * time.Second,
123+
attempts: 2,
124+
search: []string{"domain.local."},
125+
},
126+
},
105127
}
106128

107129
func TestDNSReadConfig(t *testing.T) {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
options single-request-reopen
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
options single-request

0 commit comments

Comments
 (0)