@@ -62,12 +62,24 @@ func (c *criService) portForward(ctx context.Context, id string, port int32, str
6262 log .G (ctx ).Infof ("Executing port forwarding in network namespace %q" , netNSPath )
6363 err = netNSDo (func (_ ns.NetNS ) error {
6464 defer stream .Close ()
65- // TODO: hardcoded to tcp4 because localhost resolves to ::1 by default if the system has IPv6 enabled.
66- // Theoretically happy eyeballs will try IPv6 first and fallback to IPv4
67- // but resolving localhost doesn't seem to return and IPv4 address, thus failing the connection.
65+ // localhost can resolve to both IPv4 and IPv6 addresses in dual-stack systems
66+ // but the application can be listening in one of the IP families only.
67+ // golang has enabled RFC 6555 Fast Fallback (aka HappyEyeballs) by default in 1.12
68+ // It means that if a host resolves to both IPv6 and IPv4, it will try to connect to any
69+ // of those addresses and use the working connection.
70+ // However, the implementation uses go routines to start both connections in parallel,
71+ // and this cases that the connection is done outside the namespace, so we try to connect
72+ // serially.
73+ // We try IPv4 first to keep current behavior and we fallback to IPv6 if the connection fails.
74+ // xref https://github.com/golang/go/issues/44922
75+ var conn net.Conn
6876 conn , err := net .Dial ("tcp4" , fmt .Sprintf ("localhost:%d" , port ))
6977 if err != nil {
70- return errors .Wrapf (err , "failed to dial %d" , port )
78+ var errV6 error
79+ conn , errV6 = net .Dial ("tcp6" , fmt .Sprintf ("localhost:%d" , port ))
80+ if errV6 != nil {
81+ return fmt .Errorf ("failed to connect to localhost:%d inside namespace %q, IPv4: %v IPv6 %v " , port , id , err , errV6 )
82+ }
7183 }
7284 defer conn .Close ()
7385
0 commit comments