diff --git a/src/net/addrselect.go b/src/net/addrselect.go index 0ff8ec37c8de35..68631119ee34a3 100644 --- a/src/net/addrselect.go +++ b/src/net/addrselect.go @@ -7,32 +7,51 @@ package net import ( + "math/rand/v2" "net/netip" "slices" ) -func sortByRFC6724(addrs []IPAddr) { +// sortByRFC6724 sort ip by RFC6724 +// dnsLoadBalance true for dial usage with shuffle before sort +// dnsLoadBalance flase for simple DNS with stable sort +func sortByRFC6724(addrs []IPAddr, dnsLoadbalance bool) { if len(addrs) < 2 { return } - sortByRFC6724withSrcs(addrs, srcAddrs(addrs)) + sortByRFC6724withSrcs(addrs, srcAddrs(addrs), dnsLoadbalance) } -func sortByRFC6724withSrcs(addrs []IPAddr, srcs []netip.Addr) { +// sortByRFC6724withSrcs sort by RFC6724 with srcs +// shuffle addrInfos before sorting if dnsLoadbalance is true +// section 6. Rule9 and Rule10 superseded for DNS load-balance indirectly +func sortByRFC6724withSrcs(addrs []IPAddr, srcs []netip.Addr, dnsLoadbalance bool) { if len(addrs) != len(srcs) { panic("internal error") } addrInfos := make([]byRFC6724Info, len(addrs)) for i, v := range addrs { addrAttrIP, _ := netip.AddrFromSlice(v.IP) + addrAttr := ipAttrOf(addrAttrIP) + srcAttr := ipAttrOf(srcs[i]) addrInfos[i] = byRFC6724Info{ addr: addrs[i], - addrAttr: ipAttrOf(addrAttrIP), + addrAttr: addrAttr, src: srcs[i], - srcAttr: ipAttrOf(srcs[i]), + srcAttr: srcAttr, + score: scoreByRFC6724(addrs[i].IP, srcs[i], addrAttr, srcAttr, dnsLoadbalance), } } + + // shuffle addrInfos before sorting if dnsLoadbalance is true + // section 6. Rule9 and Rule10 superseded for DNS load-balance indirectly + if dnsLoadbalance { + rand.Shuffle(len(addrInfos), func(i, j int) { + addrInfos[i], addrInfos[j] = addrInfos[j], addrInfos[i] + }) + } slices.SortStableFunc(addrInfos, compareByRFC6724) + for i := range addrInfos { addrs[i] = addrInfos[i].addr } @@ -81,6 +100,8 @@ type byRFC6724Info struct { addrAttr ipAttr src netip.Addr srcAttr ipAttr + // score use to sort to implement RFC 6724 section 6. + score int } // compareByRFC6724 compares two byRFC6724Info records and returns an integer @@ -88,123 +109,102 @@ type byRFC6724Info struct { // RFC 6724 section 6. Returns -1 if a is preferred, 1 if b is preferred, // and 0 if they are equal. func compareByRFC6724(a, b byRFC6724Info) int { - DA := a.addr.IP - DB := b.addr.IP - SourceDA := a.src - SourceDB := b.src - attrDA := &a.addrAttr - attrDB := &b.addrAttr - attrSourceDA := &a.srcAttr - attrSourceDB := &b.srcAttr - - const preferDA = -1 - const preferDB = 1 + return b.score - a.score +} - // Rule 1: Avoid unusable destinations. - // If DB is known to be unreachable or if Source(DB) is undefined, then - // prefer DA. Similarly, if DA is known to be unreachable or if - // Source(DA) is undefined, then prefer DB. - if !SourceDA.IsValid() && !SourceDB.IsValid() { - return 0 // "equal" - } - if !SourceDB.IsValid() { - return preferDA - } - if !SourceDA.IsValid() { - return preferDB +// scoreByRFC6724 Calculate score with bit in reverse for sort +// Rules 9 and 10 ignored if ignoreRule9and10 +func scoreByRFC6724(ip IP, source netip.Addr, attr, attrSource ipAttr, ignoreRule9and10 bool) int { + var score int + bitIndex := 1 + + // RFC6724 section 6. + // 6. Destination Address Selection + // Rule 9: Use longest matching prefix. + // When DA and DB belong to the same address family (both are IPv6 or + // both are IPv4): If CommonPrefixLen(Source(DA), DA) > + // CommonPrefixLen(Source(DB), DB), then prefer DA. Similarly, if + // CommonPrefixLen(Source(DA), DA) < CommonPrefixLen(Source(DB), DB), + // then prefer DB. + // Rule 10: Otherwise, leave the order unchanged. + // If DA preceded DB in the original list, prefer DA. Otherwise, prefer + // DB. + // Rules 9 and 10 MAY be superseded if the implementation has other + // means of sorting destination addresses. For example, if the + // implementation somehow knows which destination addresses will result + // in the "best" communications performance. + if !ignoreRule9and10 { + // However, applying this rule to IPv4 addresses causes + // problems (see issues 13283 and 18518), so limit to IPv6. + // TODO compare between ipv4 and ipv6 ignoreed insignificant ? + if ip.To4() == nil { + score += commonPrefixLen(source, ip) + } } + bitIndex += 7 - // Rule 2: Prefer matching scope. - // If Scope(DA) = Scope(Source(DA)) and Scope(DB) <> Scope(Source(DB)), - // then prefer DA. Similarly, if Scope(DA) <> Scope(Source(DA)) and - // Scope(DB) = Scope(Source(DB)), then prefer DB. - if attrDA.Scope == attrSourceDA.Scope && attrDB.Scope != attrSourceDB.Scope { - return preferDA - } - if attrDA.Scope != attrSourceDA.Scope && attrDB.Scope == attrSourceDB.Scope { - return preferDB - } + // Rule 8: Prefer smaller scope. + // If Scope(DA) < Scope(DB), then prefer DA. Similarly, if Scope(DA) > + // Scope(DB), then prefer DB. + score += ((1 << 8) - int(attr.Scope)) << bitIndex + bitIndex += 8 - // Rule 3: Avoid deprecated addresses. - // If Source(DA) is deprecated and Source(DB) is not, then prefer DB. - // Similarly, if Source(DA) is not deprecated and Source(DB) is - // deprecated, then prefer DA. + // Rule 7: Prefer native transport. + // If DA is reached via an encapsulating transition mechanism (e.g., + // IPv6 in IPv4) and DB is not, then prefer DB. Similarly, if DB is + // reached via encapsulation and DA is not, then prefer DA. // TODO(bradfitz): implement? low priority for now. - // Rule 4: Prefer home addresses. - // If Source(DA) is simultaneously a home address and care-of address - // and Source(DB) is not, then prefer DA. Similarly, if Source(DB) is - // simultaneously a home address and care-of address and Source(DA) is - // not, then prefer DB. - - // TODO(bradfitz): implement? low priority for now. + // Rule 6: Prefer higher precedence. + // If Precedence(DA) > Precedence(DB), then prefer DA. Similarly, if + // Precedence(DA) < Precedence(DB), then prefer DB. + score += int(attr.Precedence) << bitIndex + bitIndex += 8 // Rule 5: Prefer matching label. // If Label(Source(DA)) = Label(DA) and Label(Source(DB)) <> Label(DB), // then prefer DA. Similarly, if Label(Source(DA)) <> Label(DA) and // Label(Source(DB)) = Label(DB), then prefer DB. - if attrSourceDA.Label == attrDA.Label && - attrSourceDB.Label != attrDB.Label { - return preferDA - } - if attrSourceDA.Label != attrDA.Label && - attrSourceDB.Label == attrDB.Label { - return preferDB + if attrSource.Label == attr.Label { + score += 1 << bitIndex } + bitIndex++ - // Rule 6: Prefer higher precedence. - // If Precedence(DA) > Precedence(DB), then prefer DA. Similarly, if - // Precedence(DA) < Precedence(DB), then prefer DB. - if attrDA.Precedence > attrDB.Precedence { - return preferDA - } - if attrDA.Precedence < attrDB.Precedence { - return preferDB - } + // Rule 4: Prefer home addresses. + // If Source(DA) is simultaneously a home address and care-of address + // and Source(DB) is not, then prefer DA. Similarly, if Source(DB) is + // simultaneously a home address and care-of address and Source(DA) is + // not, then prefer DB. - // Rule 7: Prefer native transport. - // If DA is reached via an encapsulating transition mechanism (e.g., - // IPv6 in IPv4) and DB is not, then prefer DB. Similarly, if DB is - // reached via encapsulation and DA is not, then prefer DA. + // TODO(bradfitz): implement? low priority for now. + + // Rule 3: Avoid deprecated addresses. + // If Source(DA) is deprecated and Source(DB) is not, then prefer DB. + // Similarly, if Source(DA) is not deprecated and Source(DB) is + // deprecated, then prefer DA. // TODO(bradfitz): implement? low priority for now. - // Rule 8: Prefer smaller scope. - // If Scope(DA) < Scope(DB), then prefer DA. Similarly, if Scope(DA) > - // Scope(DB), then prefer DB. - if attrDA.Scope < attrDB.Scope { - return preferDA - } - if attrDA.Scope > attrDB.Scope { - return preferDB + // Rule 2: Prefer matching scope. + // If Scope(DA) = Scope(Source(DA)) and Scope(DB) <> Scope(Source(DB)), + // then prefer DA. Similarly, if Scope(DA) <> Scope(Source(DA)) and + // Scope(DB) = Scope(Source(DB)), then prefer DB. + if attr.Scope == attrSource.Scope { + score += 1 << bitIndex } + bitIndex++ - // Rule 9: Use the longest matching prefix. - // When DA and DB belong to the same address family (both are IPv6 or - // both are IPv4 [but see below]): If CommonPrefixLen(Source(DA), DA) > - // CommonPrefixLen(Source(DB), DB), then prefer DA. Similarly, if - // CommonPrefixLen(Source(DA), DA) < CommonPrefixLen(Source(DB), DB), - // then prefer DB. - // - // However, applying this rule to IPv4 addresses causes - // problems (see issues 13283 and 18518), so limit to IPv6. - if DA.To4() == nil && DB.To4() == nil { - commonA := commonPrefixLen(SourceDA, DA) - commonB := commonPrefixLen(SourceDB, DB) - - if commonA > commonB { - return preferDA - } - if commonA < commonB { - return preferDB - } + // Rule 1: Avoid unusable destinations. + // If DB is known to be unreachable or if Source(DB) is undefined, then + // prefer DA. Similarly, if DA is known to be unreachable or if + // Source(DA) is undefined, then prefer DB. + if source.IsValid() { + score += 1 << bitIndex } + bitIndex++ - // Rule 10: Otherwise, leave the order unchanged. - // If DA preceded DB in the original list, prefer DA. - // Otherwise, prefer DB. - return 0 // "equal" + return score } type policyTableEntry struct { diff --git a/src/net/addrselect_test.go b/src/net/addrselect_test.go index 7e8134d754447b..297187dce7199e 100644 --- a/src/net/addrselect_test.go +++ b/src/net/addrselect_test.go @@ -124,7 +124,7 @@ func TestSortByRFC6724(t *testing.T) { copy(inCopy, tt.in) srcCopy := make([]netip.Addr, len(tt.in)) copy(srcCopy, tt.srcs) - sortByRFC6724withSrcs(inCopy, srcCopy) + sortByRFC6724withSrcs(inCopy, srcCopy, false) if !reflect.DeepEqual(inCopy, tt.want) { t.Errorf("test %d:\nin = %s\ngot: %s\nwant: %s\n", i, tt.in, inCopy, tt.want) } @@ -136,14 +136,13 @@ func TestSortByRFC6724(t *testing.T) { inCopy[j], inCopy[k] = inCopy[k], inCopy[j] srcCopy[j], srcCopy[k] = srcCopy[k], srcCopy[j] } - sortByRFC6724withSrcs(inCopy, srcCopy) + sortByRFC6724withSrcs(inCopy, srcCopy, false) if !reflect.DeepEqual(inCopy, tt.want) { t.Errorf("test %d, starting backwards:\nin = %s\ngot: %s\nwant: %s\n", i, tt.in, inCopy, tt.want) } } } - } func TestRFC6724PolicyTableOrder(t *testing.T) { @@ -308,5 +307,4 @@ func TestRFC6724CommonPrefixLength(t *testing.T) { t.Errorf("%d. commonPrefixLen(%s, %s) = %d; want %d", i, tt.a, tt.b, got, tt.want) } } - } diff --git a/src/net/dial.go b/src/net/dial.go index a87c57603a813c..ee21babfa24bbd 100644 --- a/src/net/dial.go +++ b/src/net/dial.go @@ -332,7 +332,7 @@ func parseNetwork(ctx context.Context, network string, needsProto bool) (afnet s // resolveAddrList resolves addr using hint and returns a list of // addresses. The result contains at least one address when error is // nil. -func (r *Resolver) resolveAddrList(ctx context.Context, op, network, addr string, hint Addr) (addrList, error) { +func (r *Resolver) resolveAddrList(ctx context.Context, op, network, addr string, hint Addr, dnsLoadBalance bool) (addrList, error) { afnet, _, err := parseNetwork(ctx, network, true) if err != nil { return nil, err @@ -351,7 +351,7 @@ func (r *Resolver) resolveAddrList(ctx context.Context, op, network, addr string } return addrList{addr}, nil } - addrs, err := r.internetAddrList(ctx, afnet, addr) + addrs, err := r.internetAddrList(ctx, afnet, addr, dnsLoadBalance) if err != nil || op != "dial" || hint == nil { return addrs, err } @@ -536,7 +536,13 @@ func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn resolveCtx = context.WithValue(resolveCtx, nettrace.TraceKey{}, &shadow) } - addrs, err := d.resolver().resolveAddrList(resolveCtx, "dial", network, address, d.LocalAddr) + // Multiple hosts implements load-balancing for DNS by randomly sorts the list of A or AAAA records. + // See https://github.com/golang/go/issues/34511 + // See https://github.com/grafana/loki/issues/3301 + // See https://github.com/grafana/loki/issues/9239 + // See https://github.com/restic/restic/issues/4444 + // See https://github.com/golang/go/issues/31698 + addrs, err := d.resolver().resolveAddrList(resolveCtx, "dial", network, address, d.LocalAddr, true) if err != nil { return nil, &OpError{Op: "dial", Net: network, Source: nil, Addr: nil, Err: err} } @@ -872,7 +878,7 @@ func (lc *ListenConfig) SetMultipathTCP(use bool) { // The ctx argument is used while resolving the address on which to listen; // it does not affect the returned Listener. func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (Listener, error) { - addrs, err := DefaultResolver.resolveAddrList(ctx, "listen", network, address, nil) + addrs, err := DefaultResolver.resolveAddrList(ctx, "listen", network, address, nil, false) if err != nil { return nil, &OpError{Op: "listen", Net: network, Source: nil, Addr: nil, Err: err} } @@ -909,7 +915,7 @@ func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (Li // The ctx argument is used while resolving the address on which to listen; // it does not affect the returned PacketConn. func (lc *ListenConfig) ListenPacket(ctx context.Context, network, address string) (PacketConn, error) { - addrs, err := DefaultResolver.resolveAddrList(ctx, "listen", network, address, nil) + addrs, err := DefaultResolver.resolveAddrList(ctx, "listen", network, address, nil, false) if err != nil { return nil, &OpError{Op: "listen", Net: network, Source: nil, Addr: nil, Err: err} } diff --git a/src/net/dnsclient_unix.go b/src/net/dnsclient_unix.go index 40f76062944f46..248c63b9b3898b 100644 --- a/src/net/dnsclient_unix.go +++ b/src/net/dnsclient_unix.go @@ -595,7 +595,7 @@ func goLookupIPFiles(name string) (addrs []IPAddr, canonical string) { addrs = append(addrs, addr) } } - sortByRFC6724(addrs) + sortByRFC6724(addrs, false) return addrs, canonical } @@ -796,7 +796,7 @@ func (r *Resolver) goLookupIPCNAMEOrder(ctx context.Context, network, name strin // just one is misleading. See also golang.org/issue/6324. lastErr.Name = name } - sortByRFC6724(addrs) + sortByRFC6724(addrs, false) if len(addrs) == 0 && !(network == "CNAME" && cname.Length > 0) { if order == hostLookupDNSFiles { var canonical string diff --git a/src/net/iprawsock.go b/src/net/iprawsock.go index 26134d7e76a2ec..42fc22993295b7 100644 --- a/src/net/iprawsock.go +++ b/src/net/iprawsock.go @@ -98,7 +98,7 @@ func ResolveIPAddr(network, address string) (*IPAddr, error) { default: return nil, UnknownNetworkError(network) } - addrs, err := DefaultResolver.internetAddrList(context.Background(), afnet, address) + addrs, err := DefaultResolver.internetAddrList(context.Background(), afnet, address, false) if err != nil { return nil, err } diff --git a/src/net/ipsock.go b/src/net/ipsock.go index 496faf346ec1e0..d1fc0157748794 100644 --- a/src/net/ipsock.go +++ b/src/net/ipsock.go @@ -246,7 +246,7 @@ func JoinHostPort(host, port string) string { // address or a DNS name, and returns a list of internet protocol // family addresses. The result contains at least one address when // error is nil. -func (r *Resolver) internetAddrList(ctx context.Context, net, addr string) (addrList, error) { +func (r *Resolver) internetAddrList(ctx context.Context, net, addr string, dnsLoadBalance bool) (addrList, error) { var ( err error host, port string @@ -290,6 +290,10 @@ func (r *Resolver) internetAddrList(ctx context.Context, net, addr string) (addr if err != nil { return nil, err } + if dnsLoadBalance { + sortByRFC6724(ips, true) + } + // Issue 18806: if the machine has halfway configured // IPv6 such that it can bind on "::" (IPv6unspecified) // but not connect back to that same address, fall diff --git a/src/net/lookup.go b/src/net/lookup.go index d4be8eaa0e10c3..66a9f3ca1e9caf 100644 --- a/src/net/lookup.go +++ b/src/net/lookup.go @@ -239,7 +239,7 @@ func (r *Resolver) LookupIP(ctx context.Context, network, host string) ([]IP, er if host == "" { return nil, newDNSError(errNoSuchHost, host, "") } - addrs, err := r.internetAddrList(ctx, afnet, host) + addrs, err := r.internetAddrList(ctx, afnet, host, false) if err != nil { return nil, err } diff --git a/src/net/tcpsock.go b/src/net/tcpsock.go index 35eda25ead07c5..43aff179db829a 100644 --- a/src/net/tcpsock.go +++ b/src/net/tcpsock.go @@ -89,7 +89,7 @@ func ResolveTCPAddr(network, address string) (*TCPAddr, error) { default: return nil, UnknownNetworkError(network) } - addrs, err := DefaultResolver.internetAddrList(context.Background(), network, address) + addrs, err := DefaultResolver.internetAddrList(context.Background(), network, address, false) if err != nil { return nil, err } diff --git a/src/net/udpsock.go b/src/net/udpsock.go index fcd6a065688b9e..9c768cab647586 100644 --- a/src/net/udpsock.go +++ b/src/net/udpsock.go @@ -89,7 +89,7 @@ func ResolveUDPAddr(network, address string) (*UDPAddr, error) { default: return nil, UnknownNetworkError(network) } - addrs, err := DefaultResolver.internetAddrList(context.Background(), network, address) + addrs, err := DefaultResolver.internetAddrList(context.Background(), network, address, false) if err != nil { return nil, err }