Skip to content

Commit 1b77779

Browse files
committed
feat: support IPv6: fallback logic, dialability filtering (part two)
* cmn/network: - rewrite Host2IP(): remove IPv4-only To4() filter - amend ParseHost2IP(): add dialability check and debug assert - add IsDialableHostIP() to reject link-local, multicast, et al. * cmn/config: - document UseIPv6 as user preference with fallback semantics - note future feature flag for strict IPv6-only mode --------- * when selecting local unicast IPs: - getLocalIPs: pass useIPv6 as parameter - add two-pass resolution - when UseIPv6 is configured but no usable v6 addresses found, fall back to v4 * extract _listen() helper for multihome pub listeners * simplify/refactor _selectHost() -------- * part two, prev. commit: 3a4e3ae Signed-off-by: Alex Aizman <alex.aizman@gmail.com>
1 parent 0b32326 commit 1b77779

4 files changed

Lines changed: 112 additions & 28 deletions

File tree

‎ais/htrun.go‎

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -401,10 +401,31 @@ func (h *htrun) initSnode(config *cmn.Config) {
401401
dataAddr meta.NetInfo
402402
port = strconv.Itoa(config.HostNet.Port)
403403
proto = config.Net.HTTP.Proto
404+
useIPv6 = config.Net.UseIPv6
404405
)
405-
addrList, err := getLocalIPs(config)
406+
addrList, err := getLocalIPs(config, useIPv6)
406407
if err != nil {
407-
cos.ExitLogf("failed to get local IP addr list: %v", err)
408+
if !useIPv6 {
409+
cos.ExitLogf("failed to get local IPv4 addr list: %v", err)
410+
}
411+
// v6 was explicitly requested (and failed) - try fallback to IPv4
412+
var nested error
413+
414+
useIPv6 = false
415+
addrList, nested = getLocalIPs(config, false)
416+
if nested != nil {
417+
cos.ExitLogf("failed to get local IP addr list: %v (nested: %v)", err, nested)
418+
}
419+
nlog.Warningf("no usable IPv6 addresses found (%v) - falling back to IPv4 ...", err)
420+
421+
// update net servers that were initialized with useIPv6=true
422+
g.netServ.pub.useIPv6 = false
423+
if config.HostNet.UseIntraControl {
424+
g.netServ.control.useIPv6 = false
425+
}
426+
if config.HostNet.UseIntraData {
427+
g.netServ.data.useIPv6 = false
428+
}
408429
}
409430

410431
if l := len(addrList); l > 1 {
@@ -428,7 +449,7 @@ func (h *htrun) initSnode(config *cmn.Config) {
428449
// public hostname could be a load balancer's external IP or a service DNS
429450
nlog.Infoln("K8s deployment: skipping hostname validation for", config.HostNet.Hostname)
430451
pubAddr.Init(proto, pub, port)
431-
} else if err = initNetInfo(&pubAddr, addrList, proto, config.HostNet.Hostname, port, config.Net.UseIPv6); err != nil {
452+
} else if err = initNetInfo(&pubAddr, addrList, proto, config.HostNet.Hostname, port, useIPv6); err != nil {
432453
cos.ExitLogf("failed to select %s IP/hostname: %v", cmn.NetPublic, err)
433454
}
434455

@@ -452,7 +473,7 @@ func (h *htrun) initSnode(config *cmn.Config) {
452473
ctrlAddr = pubAddr
453474
if config.HostNet.UseIntraControl {
454475
icport := strconv.Itoa(config.HostNet.PortIntraControl)
455-
err = initNetInfo(&ctrlAddr, addrList, proto, config.HostNet.HostnameIntraControl, icport, config.Net.UseIPv6)
476+
err = initNetInfo(&ctrlAddr, addrList, proto, config.HostNet.HostnameIntraControl, icport, useIPv6)
456477
if err != nil {
457478
cos.ExitLogf("failed to select %s IP/hostname: %v", cmn.NetIntraControl, err)
458479
}
@@ -465,7 +486,7 @@ func (h *htrun) initSnode(config *cmn.Config) {
465486
dataAddr = pubAddr
466487
if config.HostNet.UseIntraData {
467488
idport := strconv.Itoa(config.HostNet.PortIntraData)
468-
err = initNetInfo(&dataAddr, addrList, proto, config.HostNet.HostnameIntraData, idport, config.Net.UseIPv6)
489+
err = initNetInfo(&dataAddr, addrList, proto, config.HostNet.HostnameIntraData, idport, useIPv6)
469490
if err != nil {
470491
cos.ExitLogf("failed to select %s IP/hostname: %v", cmn.NetIntraData, err)
471492
}
@@ -626,19 +647,29 @@ func (h *htrun) run(config *cmn.Config) error {
626647
if h.pubAddrAny(config) {
627648
ep = ":" + h.si.PubNet.Port
628649
} else if len(h.si.PubExtra) > 0 {
650+
// multihome: listen on additional configured pub addr-s
629651
for _, pubExtra := range h.si.PubExtra {
630-
debug.Assert(pubExtra.Port == h.si.PubNet.Port, "expecting the same TCP port for all multi-home interfaces")
631-
server := &netServer{muxers: g.netServ.pub.muxers, sndRcvBufSize: g.netServ.pub.sndRcvBufSize, useIPv6: config.Net.UseIPv6}
632-
go func() {
633-
_ = server.listen(pubExtra.TCPEndpoint(), logger, tlsConf, config)
634-
}()
635-
g.netServ.pubExtra = append(g.netServ.pubExtra, server)
652+
h._listen(pubExtra, logger, tlsConf, config, g.netServ.pub.useIPv6)
636653
}
637654
}
638655

639656
return g.netServ.pub.listen(ep, logger, tlsConf, config) // stay here
640657
}
641658

659+
func (h *htrun) _listen(pubExtra meta.NetInfo, logger *log.Logger, tlsConf *tls.Config, config *cmn.Config, useIPv6 bool) {
660+
debug.Assert(pubExtra.Port == h.si.PubNet.Port, "expecting the same TCP port for all multi-home interfaces")
661+
server := &netServer{
662+
muxers: g.netServ.pub.muxers,
663+
sndRcvBufSize: g.netServ.pub.sndRcvBufSize,
664+
useIPv6: useIPv6, // in fact, expecting the same TCP port _and_ the same IP family as PubNet
665+
}
666+
ep := pubExtra.TCPEndpoint()
667+
go func() {
668+
_ = server.listen(ep, logger, tlsConf, config)
669+
}()
670+
g.netServ.pubExtra = append(g.netServ.pubExtra, server)
671+
}
672+
642673
// return true to start listening on `INADDR_ANY:PubNet.Port`
643674
func (h *htrun) pubAddrAny(config *cmn.Config) (inaddrAny bool) {
644675
switch {

‎ais/utils.go‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ func (addr *localIPInfo) warn() {
7171
//
7272

7373
// returns a list of local unicast IPs (and their MTU)
74-
func getLocalIPs(config *cmn.Config) ([]*localIPInfo, error) {
75-
if config.Net.UseIPv6 {
74+
func getLocalIPs(config *cmn.Config, useIPv6 bool) ([]*localIPInfo, error) {
75+
if useIPv6 {
7676
return _getLocalIPv6s(config)
7777
}
7878
return _getLocalIPv4s(config)
@@ -276,10 +276,7 @@ func _selectHost(locIPs []*localIPInfo, hostnames []string, useIPv6 bool) (strin
276276

277277
var ipstr string
278278
if pip := net.ParseIP(hostNoBr); pip != nil { // parses as IP literal
279-
if useIPv6 && !_isIPv6(pip) {
280-
continue
281-
}
282-
if !useIPv6 && !_isIPv4(pip) {
279+
if useIPv6 != _isIPv6(pip) {
283280
continue
284281
}
285282
ipstr = pip.String()

‎cmn/config.go‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -575,8 +575,14 @@ type (
575575

576576
HTTP HTTPConf `json:"http"`
577577

578-
// cluster-wide default (read-only)
579-
// in the future, use LocalNetConfig to override (e.g., pub: IPv6, intra: IPv4)
578+
// cluster-wide user preference (read-only)
579+
// when true: prefer IPv6, fall back to IPv4 if no usable v6 addresses
580+
// when false: use IPv4 (default)
581+
//
582+
// NOTE:
583+
// in the future:
584+
// - use LocalNetConfig to override (e.g., pub: IPv6, intra: IPv4)
585+
// - add a feature flag to enforce IPv6-only (and disallow IPv4 fallback)
580586
UseIPv6 bool `json:"use_ipv6"`
581587
}
582588
NetConfToSet struct {

‎cmn/network.go‎

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"time"
1515

1616
"github.com/NVIDIA/aistore/cmn/debug"
17+
"github.com/NVIDIA/aistore/cmn/nlog"
1718
)
1819

1920
const (
@@ -44,10 +45,10 @@ func ValidatePort(port int) (int, error) {
4445
return port, nil
4546
}
4647

47-
// TODO -- FIXME: git grep -n '\.To4()', and similar
48-
func Host2IP(host string, local bool) (net.IP, error) {
48+
// resolve `host` and returns the first usable (TCP dialable) IP address (v4 or v6)
49+
func Host2IP(host string, localTimeout bool) (net.IP, error) {
4950
timeout := max(time.Second, Rom.MaxKeepalive())
50-
if local {
51+
if localTimeout {
5152
timeout = max(time.Second, Rom.CplaneOperation())
5253
}
5354
ctx, cancel := context.WithTimeout(context.Background(), timeout)
@@ -57,23 +58,72 @@ func Host2IP(host string, local bool) (net.IP, error) {
5758
if err != nil {
5859
return nil, err
5960
}
61+
62+
// pass 1: prefer non-loopback if possible
63+
for _, addr := range addrs {
64+
ip := addr.IP
65+
if IsDialableHostIP(ip) && !ip.IsLoopback() {
66+
return ip, nil
67+
}
68+
}
69+
// pass 2 (fallback)
6070
for _, addr := range addrs {
61-
if ip := addr.IP.To4(); ip != nil {
71+
ip := addr.IP
72+
if IsDialableHostIP(ip) {
6273
return ip, nil
6374
}
6475
}
6576

66-
// return err
67-
ips := make([]net.IP, len(addrs))
68-
for i, addr := range addrs {
69-
ips[i] = addr.IP
77+
// return detailed error
78+
ips := make([]net.IP, 0, len(addrs))
79+
for _, addr := range addrs {
80+
ips = append(ips, addr.IP)
7081
}
71-
return nil, fmt.Errorf("failed to locally resolve %q (have IPs %v)", host, ips)
82+
return nil, fmt.Errorf("failed to resolve dialable IP for %q (have IPs %v)", host, ips)
83+
}
84+
85+
// check whether `ip` is usable for TCP dialing
86+
// in particular:
87+
// - reject IPv6 link-local
88+
// - reject multicast and unspecified
89+
// - allow loopback
90+
func IsDialableHostIP(ip net.IP) bool {
91+
if ip == nil {
92+
return false
93+
}
94+
95+
ip4 := ip.To4()
96+
if ip4 != nil {
97+
return !ip4.IsUnspecified() && !ip4.IsMulticast()
98+
}
99+
100+
ip16 := ip.To16()
101+
if ip16 == nil {
102+
return false // invalid
103+
}
104+
105+
// filter IPv6
106+
if ip16.IsUnspecified() || ip16.IsMulticast() {
107+
return false
108+
}
109+
if ip16.IsLinkLocalUnicast() {
110+
return false // link-local requires a zone
111+
}
112+
if ip16[0] == 0x20 && ip16[1] == 0x01 && ip16[2] == 0x0d && ip16[3] == 0xb8 {
113+
return false // reject RFC 3849 documentation prefix: 2001:db8::/32
114+
}
115+
116+
return true
72117
}
73118

74119
func ParseHost2IP(host string, local bool) (net.IP, error) {
75120
ip := net.ParseIP(host)
76121
if ip != nil {
122+
if !IsDialableHostIP(ip) {
123+
warn := fmt.Errorf("ParseHost2IP: %s (%s) is not dialable", host, ip.String())
124+
debug.Assert(false, warn)
125+
nlog.Warningln(warn)
126+
}
77127
return ip, nil // is a parse-able IP addr
78128
}
79129
return Host2IP(host, local)

0 commit comments

Comments
 (0)