Skip to content

Commit 28154be

Browse files
committed
net/dial: load-balancing for DNS
Multiple hosts implements load-balancing for DNS by randomly sorts the list of A or AAAA records. Fixes #34511 Fixes #31698
1 parent 57362e9 commit 28154be

File tree

9 files changed

+124
-116
lines changed

9 files changed

+124
-116
lines changed

src/net/addrselect.go

Lines changed: 100 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,51 @@
77
package net
88

99
import (
10+
"math/rand/v2"
1011
"net/netip"
1112
"slices"
1213
)
1314

14-
func sortByRFC6724(addrs []IPAddr) {
15+
// sortByRFC6724 sort ip by RFC6724
16+
// dnsLoadBalance true for dial usage with shuffle before sort
17+
// dnsLoadBalance flase for simple DNS with stable sort
18+
func sortByRFC6724(addrs []IPAddr, dnsLoadbalance bool) {
1519
if len(addrs) < 2 {
1620
return
1721
}
18-
sortByRFC6724withSrcs(addrs, srcAddrs(addrs))
22+
sortByRFC6724withSrcs(addrs, srcAddrs(addrs), dnsLoadbalance)
1923
}
2024

21-
func sortByRFC6724withSrcs(addrs []IPAddr, srcs []netip.Addr) {
25+
// sortByRFC6724withSrcs sort by RFC6724 with srcs
26+
// shuffle addrInfos before sorting if dnsLoadbalance is true
27+
// section 6. Rule9 and Rule10 superseded for DNS load-balance indirectly
28+
func sortByRFC6724withSrcs(addrs []IPAddr, srcs []netip.Addr, dnsLoadbalance bool) {
2229
if len(addrs) != len(srcs) {
2330
panic("internal error")
2431
}
2532
addrInfos := make([]byRFC6724Info, len(addrs))
2633
for i, v := range addrs {
2734
addrAttrIP, _ := netip.AddrFromSlice(v.IP)
35+
addrAttr := ipAttrOf(addrAttrIP)
36+
srcAttr := ipAttrOf(srcs[i])
2837
addrInfos[i] = byRFC6724Info{
2938
addr: addrs[i],
30-
addrAttr: ipAttrOf(addrAttrIP),
39+
addrAttr: addrAttr,
3140
src: srcs[i],
32-
srcAttr: ipAttrOf(srcs[i]),
41+
srcAttr: srcAttr,
42+
score: scoreByRFC6724(addrs[i].IP, srcs[i], addrAttr, srcAttr, dnsLoadbalance),
3343
}
3444
}
45+
46+
// shuffle addrInfos before sorting if dnsLoadbalance is true
47+
// section 6. Rule9 and Rule10 superseded for DNS load-balance indirectly
48+
if dnsLoadbalance {
49+
rand.Shuffle(len(addrInfos), func(i, j int) {
50+
addrInfos[i], addrInfos[j] = addrInfos[j], addrInfos[i]
51+
})
52+
}
3553
slices.SortStableFunc(addrInfos, compareByRFC6724)
54+
3655
for i := range addrInfos {
3756
addrs[i] = addrInfos[i].addr
3857
}
@@ -81,130 +100,111 @@ type byRFC6724Info struct {
81100
addrAttr ipAttr
82101
src netip.Addr
83102
srcAttr ipAttr
103+
// score use to sort to implement RFC 6724 section 6.
104+
score int
84105
}
85106

86107
// compareByRFC6724 compares two byRFC6724Info records and returns an integer
87108
// indicating the order. It follows the algorithm and variable names from
88109
// RFC 6724 section 6. Returns -1 if a is preferred, 1 if b is preferred,
89110
// and 0 if they are equal.
90111
func compareByRFC6724(a, b byRFC6724Info) int {
91-
DA := a.addr.IP
92-
DB := b.addr.IP
93-
SourceDA := a.src
94-
SourceDB := b.src
95-
attrDA := &a.addrAttr
96-
attrDB := &b.addrAttr
97-
attrSourceDA := &a.srcAttr
98-
attrSourceDB := &b.srcAttr
99-
100-
const preferDA = -1
101-
const preferDB = 1
112+
return b.score - a.score
113+
}
102114

103-
// Rule 1: Avoid unusable destinations.
104-
// If DB is known to be unreachable or if Source(DB) is undefined, then
105-
// prefer DA. Similarly, if DA is known to be unreachable or if
106-
// Source(DA) is undefined, then prefer DB.
107-
if !SourceDA.IsValid() && !SourceDB.IsValid() {
108-
return 0 // "equal"
109-
}
110-
if !SourceDB.IsValid() {
111-
return preferDA
112-
}
113-
if !SourceDA.IsValid() {
114-
return preferDB
115+
// scoreByRFC6724 Calculate score with bit in reverse for sort
116+
// Rules 9 and 10 ignored if ignoreRule9and10
117+
func scoreByRFC6724(ip IP, source netip.Addr, attr, attrSource ipAttr, ignoreRule9and10 bool) int {
118+
var score int
119+
bitIndex := 1
120+
121+
// RFC6724 section 6.
122+
// 6. Destination Address Selection
123+
// Rule 9: Use longest matching prefix.
124+
// When DA and DB belong to the same address family (both are IPv6 or
125+
// both are IPv4): If CommonPrefixLen(Source(DA), DA) >
126+
// CommonPrefixLen(Source(DB), DB), then prefer DA. Similarly, if
127+
// CommonPrefixLen(Source(DA), DA) < CommonPrefixLen(Source(DB), DB),
128+
// then prefer DB.
129+
// Rule 10: Otherwise, leave the order unchanged.
130+
// If DA preceded DB in the original list, prefer DA. Otherwise, prefer
131+
// DB.
132+
// Rules 9 and 10 MAY be superseded if the implementation has other
133+
// means of sorting destination addresses. For example, if the
134+
// implementation somehow knows which destination addresses will result
135+
// in the "best" communications performance.
136+
if !ignoreRule9and10 {
137+
// However, applying this rule to IPv4 addresses causes
138+
// problems (see issues 13283 and 18518), so limit to IPv6.
139+
140+
if ip.To4() == nil {
141+
score += commonPrefixLen(source, ip)
142+
}
115143
}
144+
bitIndex += 7
116145

117-
// Rule 2: Prefer matching scope.
118-
// If Scope(DA) = Scope(Source(DA)) and Scope(DB) <> Scope(Source(DB)),
119-
// then prefer DA. Similarly, if Scope(DA) <> Scope(Source(DA)) and
120-
// Scope(DB) = Scope(Source(DB)), then prefer DB.
121-
if attrDA.Scope == attrSourceDA.Scope && attrDB.Scope != attrSourceDB.Scope {
122-
return preferDA
123-
}
124-
if attrDA.Scope != attrSourceDA.Scope && attrDB.Scope == attrSourceDB.Scope {
125-
return preferDB
126-
}
146+
// Rule 8: Prefer smaller scope.
147+
// If Scope(DA) < Scope(DB), then prefer DA. Similarly, if Scope(DA) >
148+
// Scope(DB), then prefer DB.
149+
score += ((1 << 8) - int(attr.Scope)) << bitIndex
150+
bitIndex += 8
127151

128-
// Rule 3: Avoid deprecated addresses.
129-
// If Source(DA) is deprecated and Source(DB) is not, then prefer DB.
130-
// Similarly, if Source(DA) is not deprecated and Source(DB) is
131-
// deprecated, then prefer DA.
152+
// Rule 7: Prefer native transport.
153+
// If DA is reached via an encapsulating transition mechanism (e.g.,
154+
// IPv6 in IPv4) and DB is not, then prefer DB. Similarly, if DB is
155+
// reached via encapsulation and DA is not, then prefer DA.
132156

133157
// TODO(bradfitz): implement? low priority for now.
134158

135-
// Rule 4: Prefer home addresses.
136-
// If Source(DA) is simultaneously a home address and care-of address
137-
// and Source(DB) is not, then prefer DA. Similarly, if Source(DB) is
138-
// simultaneously a home address and care-of address and Source(DA) is
139-
// not, then prefer DB.
140-
141-
// TODO(bradfitz): implement? low priority for now.
159+
// Rule 6: Prefer higher precedence.
160+
// If Precedence(DA) > Precedence(DB), then prefer DA. Similarly, if
161+
// Precedence(DA) < Precedence(DB), then prefer DB.
162+
score += int(attr.Precedence) << bitIndex
163+
bitIndex += 8
142164

143165
// Rule 5: Prefer matching label.
144166
// If Label(Source(DA)) = Label(DA) and Label(Source(DB)) <> Label(DB),
145167
// then prefer DA. Similarly, if Label(Source(DA)) <> Label(DA) and
146168
// Label(Source(DB)) = Label(DB), then prefer DB.
147-
if attrSourceDA.Label == attrDA.Label &&
148-
attrSourceDB.Label != attrDB.Label {
149-
return preferDA
150-
}
151-
if attrSourceDA.Label != attrDA.Label &&
152-
attrSourceDB.Label == attrDB.Label {
153-
return preferDB
169+
if attrSource.Label == attr.Label {
170+
score += 1 << bitIndex
154171
}
172+
bitIndex++
155173

156-
// Rule 6: Prefer higher precedence.
157-
// If Precedence(DA) > Precedence(DB), then prefer DA. Similarly, if
158-
// Precedence(DA) < Precedence(DB), then prefer DB.
159-
if attrDA.Precedence > attrDB.Precedence {
160-
return preferDA
161-
}
162-
if attrDA.Precedence < attrDB.Precedence {
163-
return preferDB
164-
}
174+
// Rule 4: Prefer home addresses.
175+
// If Source(DA) is simultaneously a home address and care-of address
176+
// and Source(DB) is not, then prefer DA. Similarly, if Source(DB) is
177+
// simultaneously a home address and care-of address and Source(DA) is
178+
// not, then prefer DB.
165179

166-
// Rule 7: Prefer native transport.
167-
// If DA is reached via an encapsulating transition mechanism (e.g.,
168-
// IPv6 in IPv4) and DB is not, then prefer DB. Similarly, if DB is
169-
// reached via encapsulation and DA is not, then prefer DA.
180+
// TODO(bradfitz): implement? low priority for now.
181+
182+
// Rule 3: Avoid deprecated addresses.
183+
// If Source(DA) is deprecated and Source(DB) is not, then prefer DB.
184+
// Similarly, if Source(DA) is not deprecated and Source(DB) is
185+
// deprecated, then prefer DA.
170186

171187
// TODO(bradfitz): implement? low priority for now.
172188

173-
// Rule 8: Prefer smaller scope.
174-
// If Scope(DA) < Scope(DB), then prefer DA. Similarly, if Scope(DA) >
175-
// Scope(DB), then prefer DB.
176-
if attrDA.Scope < attrDB.Scope {
177-
return preferDA
178-
}
179-
if attrDA.Scope > attrDB.Scope {
180-
return preferDB
189+
// Rule 2: Prefer matching scope.
190+
// If Scope(DA) = Scope(Source(DA)) and Scope(DB) <> Scope(Source(DB)),
191+
// then prefer DA. Similarly, if Scope(DA) <> Scope(Source(DA)) and
192+
// Scope(DB) = Scope(Source(DB)), then prefer DB.
193+
if attr.Scope == attrSource.Scope {
194+
score += 1 << bitIndex
181195
}
196+
bitIndex++
182197

183-
// Rule 9: Use the longest matching prefix.
184-
// When DA and DB belong to the same address family (both are IPv6 or
185-
// both are IPv4 [but see below]): If CommonPrefixLen(Source(DA), DA) >
186-
// CommonPrefixLen(Source(DB), DB), then prefer DA. Similarly, if
187-
// CommonPrefixLen(Source(DA), DA) < CommonPrefixLen(Source(DB), DB),
188-
// then prefer DB.
189-
//
190-
// However, applying this rule to IPv4 addresses causes
191-
// problems (see issues 13283 and 18518), so limit to IPv6.
192-
if DA.To4() == nil && DB.To4() == nil {
193-
commonA := commonPrefixLen(SourceDA, DA)
194-
commonB := commonPrefixLen(SourceDB, DB)
195-
196-
if commonA > commonB {
197-
return preferDA
198-
}
199-
if commonA < commonB {
200-
return preferDB
201-
}
198+
// Rule 1: Avoid unusable destinations.
199+
// If DB is known to be unreachable or if Source(DB) is undefined, then
200+
// prefer DA. Similarly, if DA is known to be unreachable or if
201+
// Source(DA) is undefined, then prefer DB.
202+
if source.IsValid() {
203+
score += 1 << bitIndex
202204
}
205+
bitIndex++
203206

204-
// Rule 10: Otherwise, leave the order unchanged.
205-
// If DA preceded DB in the original list, prefer DA.
206-
// Otherwise, prefer DB.
207-
return 0 // "equal"
207+
return score
208208
}
209209

210210
type policyTableEntry struct {

src/net/addrselect_test.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ func TestSortByRFC6724(t *testing.T) {
124124
copy(inCopy, tt.in)
125125
srcCopy := make([]netip.Addr, len(tt.in))
126126
copy(srcCopy, tt.srcs)
127-
sortByRFC6724withSrcs(inCopy, srcCopy)
127+
sortByRFC6724withSrcs(inCopy, srcCopy, false)
128128
if !reflect.DeepEqual(inCopy, tt.want) {
129129
t.Errorf("test %d:\nin = %s\ngot: %s\nwant: %s\n", i, tt.in, inCopy, tt.want)
130130
}
@@ -136,14 +136,13 @@ func TestSortByRFC6724(t *testing.T) {
136136
inCopy[j], inCopy[k] = inCopy[k], inCopy[j]
137137
srcCopy[j], srcCopy[k] = srcCopy[k], srcCopy[j]
138138
}
139-
sortByRFC6724withSrcs(inCopy, srcCopy)
139+
sortByRFC6724withSrcs(inCopy, srcCopy, false)
140140
if !reflect.DeepEqual(inCopy, tt.want) {
141141
t.Errorf("test %d, starting backwards:\nin = %s\ngot: %s\nwant: %s\n", i, tt.in, inCopy, tt.want)
142142
}
143143
}
144144

145145
}
146-
147146
}
148147

149148
func TestRFC6724PolicyTableOrder(t *testing.T) {
@@ -308,5 +307,4 @@ func TestRFC6724CommonPrefixLength(t *testing.T) {
308307
t.Errorf("%d. commonPrefixLen(%s, %s) = %d; want %d", i, tt.a, tt.b, got, tt.want)
309308
}
310309
}
311-
312310
}

src/net/dial.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ func parseNetwork(ctx context.Context, network string, needsProto bool) (afnet s
332332
// resolveAddrList resolves addr using hint and returns a list of
333333
// addresses. The result contains at least one address when error is
334334
// nil.
335-
func (r *Resolver) resolveAddrList(ctx context.Context, op, network, addr string, hint Addr) (addrList, error) {
335+
func (r *Resolver) resolveAddrList(ctx context.Context, op, network, addr string, hint Addr, dnsLoadBalance bool) (addrList, error) {
336336
afnet, _, err := parseNetwork(ctx, network, true)
337337
if err != nil {
338338
return nil, err
@@ -351,7 +351,7 @@ func (r *Resolver) resolveAddrList(ctx context.Context, op, network, addr string
351351
}
352352
return addrList{addr}, nil
353353
}
354-
addrs, err := r.internetAddrList(ctx, afnet, addr)
354+
addrs, err := r.internetAddrList(ctx, afnet, addr, dnsLoadBalance)
355355
if err != nil || op != "dial" || hint == nil {
356356
return addrs, err
357357
}
@@ -536,7 +536,13 @@ func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn
536536
resolveCtx = context.WithValue(resolveCtx, nettrace.TraceKey{}, &shadow)
537537
}
538538

539-
addrs, err := d.resolver().resolveAddrList(resolveCtx, "dial", network, address, d.LocalAddr)
539+
// Multiple hosts implements load-balancing for DNS by randomly sorts the list of A or AAAA records.
540+
// See https://github.com/golang/go/issues/34511
541+
// See https://github.com/grafana/loki/issues/3301
542+
// See https://github.com/grafana/loki/issues/9239
543+
// See https://github.com/restic/restic/issues/4444
544+
// See https://github.com/golang/go/issues/31698
545+
addrs, err := d.resolver().resolveAddrList(resolveCtx, "dial", network, address, d.LocalAddr, true)
540546
if err != nil {
541547
return nil, &OpError{Op: "dial", Net: network, Source: nil, Addr: nil, Err: err}
542548
}
@@ -872,7 +878,7 @@ func (lc *ListenConfig) SetMultipathTCP(use bool) {
872878
// The ctx argument is used while resolving the address on which to listen;
873879
// it does not affect the returned Listener.
874880
func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (Listener, error) {
875-
addrs, err := DefaultResolver.resolveAddrList(ctx, "listen", network, address, nil)
881+
addrs, err := DefaultResolver.resolveAddrList(ctx, "listen", network, address, nil, false)
876882
if err != nil {
877883
return nil, &OpError{Op: "listen", Net: network, Source: nil, Addr: nil, Err: err}
878884
}
@@ -909,7 +915,7 @@ func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (Li
909915
// The ctx argument is used while resolving the address on which to listen;
910916
// it does not affect the returned PacketConn.
911917
func (lc *ListenConfig) ListenPacket(ctx context.Context, network, address string) (PacketConn, error) {
912-
addrs, err := DefaultResolver.resolveAddrList(ctx, "listen", network, address, nil)
918+
addrs, err := DefaultResolver.resolveAddrList(ctx, "listen", network, address, nil, false)
913919
if err != nil {
914920
return nil, &OpError{Op: "listen", Net: network, Source: nil, Addr: nil, Err: err}
915921
}

src/net/dnsclient_unix.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,7 @@ func goLookupIPFiles(name string) (addrs []IPAddr, canonical string) {
595595
addrs = append(addrs, addr)
596596
}
597597
}
598-
sortByRFC6724(addrs)
598+
sortByRFC6724(addrs, false)
599599
return addrs, canonical
600600
}
601601

@@ -796,7 +796,7 @@ func (r *Resolver) goLookupIPCNAMEOrder(ctx context.Context, network, name strin
796796
// just one is misleading. See also golang.org/issue/6324.
797797
lastErr.Name = name
798798
}
799-
sortByRFC6724(addrs)
799+
sortByRFC6724(addrs, false)
800800
if len(addrs) == 0 && !(network == "CNAME" && cname.Length > 0) {
801801
if order == hostLookupDNSFiles {
802802
var canonical string

src/net/iprawsock.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ func ResolveIPAddr(network, address string) (*IPAddr, error) {
9898
default:
9999
return nil, UnknownNetworkError(network)
100100
}
101-
addrs, err := DefaultResolver.internetAddrList(context.Background(), afnet, address)
101+
addrs, err := DefaultResolver.internetAddrList(context.Background(), afnet, address, false)
102102
if err != nil {
103103
return nil, err
104104
}

src/net/ipsock.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ func JoinHostPort(host, port string) string {
246246
// address or a DNS name, and returns a list of internet protocol
247247
// family addresses. The result contains at least one address when
248248
// error is nil.
249-
func (r *Resolver) internetAddrList(ctx context.Context, net, addr string) (addrList, error) {
249+
func (r *Resolver) internetAddrList(ctx context.Context, net, addr string, dnsLoadBalance bool) (addrList, error) {
250250
var (
251251
err error
252252
host, port string
@@ -290,6 +290,10 @@ func (r *Resolver) internetAddrList(ctx context.Context, net, addr string) (addr
290290
if err != nil {
291291
return nil, err
292292
}
293+
if dnsLoadBalance {
294+
sortByRFC6724(ips, true)
295+
}
296+
293297
// Issue 18806: if the machine has halfway configured
294298
// IPv6 such that it can bind on "::" (IPv6unspecified)
295299
// but not connect back to that same address, fall

0 commit comments

Comments
 (0)