From d010add86f89c69d5472a2b5abd312612add074d Mon Sep 17 00:00:00 2001
From: Victor Gama <hey@vito.io>
Date: Mon, 15 May 2023 20:04:42 -0300
Subject: [PATCH 1/2] feat(ipv6): Add support to IPv6 port-forwarding

Signed-off-by: Victor Gama <hey@vito.io>
---
 hack/test-port-forwarding.pl  |  6 ++--
 pkg/hostagent/hostagent.go    | 18 +++++++---
 pkg/hostagent/port.go         | 68 +++++++++++++++++++++++++++++++++--
 pkg/hostagent/port_darwin.go  | 67 ++++++++++++++++++++++++----------
 pkg/limayaml/defaults.go      |  4 +++
 pkg/limayaml/defaults_test.go | 14 ++++----
 pkg/limayaml/limayaml.go      |  6 ++++
 pkg/limayaml/validate.go      |  6 ++--
 8 files changed, 154 insertions(+), 35 deletions(-)

diff --git a/hack/test-port-forwarding.pl b/hack/test-port-forwarding.pl
index 8def0472c14c..4edb6b18d194 100755
--- a/hack/test-port-forwarding.pl
+++ b/hack/test-port-forwarding.pl
@@ -241,8 +241,8 @@ sub JoinHostPort {
   # forward: 127.0.0.2 3020 → 127.0.0.1 2020
   # forward: 127.0.0.1 3021 → 127.0.0.1 2021
   # forward: 0.0.0.0   3022 → 127.0.0.1 2022
-  # forward: ::        3023 → 127.0.0.1 2023
-  # forward: ::1       3024 → 127.0.0.1 2024
+  # forward: ::        3023 → ::1       2023
+  # forward: ::1       3024 → ::1       2024
 
 - guestPortRange: [3030, 3039]
   hostPortRange: [2030, 2039]
@@ -309,7 +309,7 @@ sub JoinHostPort {
   ignore: true
 
   # forward: 0.0.0.0        4040 → 127.0.0.1 4040
-  # forward: ::             4041 → 127.0.0.1 4041
+  # forward: ::             4041 → ::1       4041
   # ignore:  127.0.0.1      4043 → 127.0.0.1 4043
   # ignore:  192.168.5.15   4044 → 127.0.0.1 4044
 
diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go
index bb57f8567165..121d407c94ac 100644
--- a/pkg/hostagent/hostagent.go
+++ b/pkg/hostagent/hostagent.go
@@ -120,7 +120,7 @@ func New(instName string, stdout io.Writer, sigintCh chan os.Signal, opts ...Opt
 		AdditionalArgs: sshutil.SSHArgsFromOpts(sshOpts),
 	}
 
-	rules := make([]limayaml.PortForward, 0, 3+len(y.PortForwards))
+	rules := make([]limayaml.PortForward, 0, 4+len(y.PortForwards))
 	// Block ports 22 and sshLocalPort on all IPs
 	for _, port := range []int{sshGuestPort, sshLocalPort} {
 		rule := limayaml.PortForward{GuestIP: net.IPv4zero, GuestPort: port, Ignore: true}
@@ -129,9 +129,19 @@ func New(instName string, stdout io.Writer, sigintCh chan os.Signal, opts ...Opt
 	}
 	rules = append(rules, y.PortForwards...)
 	// Default forwards for all non-privileged ports from "127.0.0.1" and "::1"
-	rule := limayaml.PortForward{GuestIP: guestagentapi.IPv4loopback1}
-	limayaml.FillPortForwardDefaults(&rule, inst.Dir)
-	rules = append(rules, rule)
+	{
+		rule := limayaml.PortForward{GuestIP: guestagentapi.IPv4loopback1}
+		limayaml.FillPortForwardDefaults(&rule, inst.Dir)
+		rules = append(rules, rule)
+	}
+	{
+		rule := limayaml.PortForward{
+			HostIP:  net.IPv6loopback,
+			GuestIP: net.IPv6loopback,
+		}
+		limayaml.FillPortForwardDefaults(&rule, inst.Dir)
+		rules = append(rules, rule)
+	}
 
 	limaDriver := driverutil.CreateTargetDriverInstance(&driver.BaseDriver{
 		Instance:     inst,
diff --git a/pkg/hostagent/port.go b/pkg/hostagent/port.go
index 31edb0e76987..0b69f26f1620 100644
--- a/pkg/hostagent/port.go
+++ b/pkg/hostagent/port.go
@@ -40,16 +40,62 @@ func hostAddress(rule limayaml.PortForward, guest api.IPPort) string {
 	return host.String()
 }
 
-func (pf *portForwarder) forwardingAddresses(guest api.IPPort) (string, string) {
+func (pf *portForwarder) forwardingAddresses(guest api.IPPort) (hostAddr string, guestAddr string) {
+	// Some rules will require a small patch to the HostIP in order to bind to the
+	// correct IP family.
+	mustAdjustHostIP := false
+
+	// This holds an optional rule that was rejected, but is now stored here to preserve backward
+	// compatibility, and will be used at the bottom of this function if set. See the case
+	// rule.GuestIPMustBeZero && guest.IP.IsUnspecified() for further info.
+	var unspecifiedRuleFallback *limayaml.PortForward
+
 	for _, rule := range pf.rules {
 		if rule.GuestSocket != "" {
+			// Not TCP
 			continue
 		}
+
+		// Check if `guest.Port` is within `rule.GuestPortRange`
 		if guest.Port < rule.GuestPortRange[0] || guest.Port > rule.GuestPortRange[1] {
 			continue
 		}
+
 		switch {
-		case guest.IP.IsUnspecified():
+		// Early-continue in case rule's IP is not zero while it is required.
+		case rule.GuestIPMustBeZero && !guest.IP.IsUnspecified():
+			continue
+
+		// Rule lacks a preferred GuestIP, so guest may be binding to wherever it wants. The rule matches the port range,
+		// so we can continue processing it. However, make sure to correct the rule to use a correct address family if
+		// not specified by the rule.
+		case rule.GuestIPWasUndefined && !rule.GuestIPMustBeZero:
+			mustAdjustHostIP = rule.HostIPWasUndefined
+
+		// if GuestIP and family matches, move along.
+		case rule.GuestIPMustBeZero && guest.IP.IsUnspecified():
+			// This is a breaking change. Here we will keep a backup of the rule, so we can still reuse it
+			// in case everything fails. The idea here is to move a copy of the current rule to outside this
+			// loop, so we can reuse it in case nothing else matches.
+			if !rule.GuestIPWasUndefined && !guest.IP.Equal(rule.GuestIP) {
+				if unspecifiedRuleFallback == nil {
+					// Move the rule to obtain a copy
+					func(p limayaml.PortForward) { unspecifiedRuleFallback = &p }(rule)
+				}
+				continue
+			}
+
+			mustAdjustHostIP = rule.HostIPWasUndefined
+
+		// Rule lack's HostIP, and guest is binding to '0.0.0.0' or '::'. Bind to the same address family.
+		case rule.HostIPWasUndefined && guest.IP.IsUnspecified():
+			mustAdjustHostIP = true
+
+		// We don't have a preferred HostIP in the rule, and guest wants to bind to a loopback
+		// address. In that case, use the same address family.
+		case rule.HostIPWasUndefined && (guest.IP.Equal(net.IPv6loopback) || guest.IP.Equal(api.IPv4loopback1)):
+			mustAdjustHostIP = true
+
 		case guest.IP.Equal(rule.GuestIP):
 		case guest.IP.Equal(net.IPv6loopback) && rule.GuestIP.Equal(api.IPv4loopback1):
 		case rule.GuestIP.IsUnspecified() && !rule.GuestIPMustBeZero:
@@ -58,14 +104,32 @@ func (pf *portForwarder) forwardingAddresses(guest api.IPPort) (string, string)
 		default:
 			continue
 		}
+
 		if rule.Ignore {
 			if guest.IP.IsUnspecified() && !rule.GuestIP.IsUnspecified() {
 				continue
 			}
+
 			break
 		}
+
+		if mustAdjustHostIP {
+			if guest.IP.To4() != nil {
+				rule.HostIP = api.IPv4loopback1
+			} else {
+				rule.HostIP = net.IPv6loopback
+			}
+		}
+
 		return hostAddress(rule, guest), guest.String()
 	}
+
+	// At this point, no other rule matched. So check if this is being impacted by our
+	// breaking change, and return the fallback rule. Otherwise, just ignore it.
+	if unspecifiedRuleFallback != nil {
+		return hostAddress(*unspecifiedRuleFallback, guest), guest.String()
+	}
+
 	return "", guest.String()
 }
 
diff --git a/pkg/hostagent/port_darwin.go b/pkg/hostagent/port_darwin.go
index fead0401b449..8f9c708502ac 100644
--- a/pkg/hostagent/port_darwin.go
+++ b/pkg/hostagent/port_darwin.go
@@ -30,7 +30,7 @@ func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local,
 		return err
 	}
 
-	if !localIP.Equal(api.IPv4loopback1) || localPort >= 1024 {
+	if (!localIP.Equal(api.IPv4loopback1) && !localIP.Equal(net.IPv6loopback)) || localPort >= 1024 {
 		return forwardSSH(ctx, sshConfig, port, local, remote, verb, false)
 	}
 
@@ -86,9 +86,10 @@ func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local,
 var pseudoLoopbackForwarders = make(map[string]*pseudoLoopbackForwarder)
 
 type pseudoLoopbackForwarder struct {
-	ln       *net.TCPListener
-	unixAddr *net.UnixAddr
-	onClose  func() error
+	lns           []*net.TCPListener
+	unixAddr      *net.UnixAddr
+	onClose       func() error
+	incomingConns chan *net.TCPConn
 }
 
 func newPseudoLoopbackForwarder(localPort int, unixSock string) (*pseudoLoopbackForwarder, error) {
@@ -97,30 +98,56 @@ func newPseudoLoopbackForwarder(localPort int, unixSock string) (*pseudoLoopback
 		return nil, err
 	}
 
-	lnAddr, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("0.0.0.0:%d", localPort))
-	if err != nil {
-		return nil, err
+	toResolve := [][]string{
+		{"tcp4", fmt.Sprintf("0.0.0.0:%d", localPort)},
+		{"tcp6", fmt.Sprintf("[::]:%d", localPort)},
 	}
-	ln, err := net.ListenTCP("tcp4", lnAddr)
-	if err != nil {
-		return nil, err
+
+	var lns []*net.TCPListener
+	for _, addr := range toResolve {
+		network, address := addr[0], addr[1]
+		lnAddr, err := net.ResolveTCPAddr(network, address)
+		if err != nil {
+			return nil, err
+		}
+		ln, err := net.ListenTCP(network, lnAddr)
+		if err != nil {
+			return nil, err
+		}
+		lns = append(lns, ln)
 	}
 
 	plf := &pseudoLoopbackForwarder{
-		ln:       ln,
-		unixAddr: unixAddr,
+		lns:           lns,
+		incomingConns: make(chan *net.TCPConn, 10),
+		unixAddr:      unixAddr,
 	}
 
 	return plf, nil
 }
 
-func (plf *pseudoLoopbackForwarder) Serve() error {
-	defer plf.ln.Close()
+func (plf *pseudoLoopbackForwarder) acceptLn(ln *net.TCPListener) {
+	defer ln.Close()
 	for {
-		ac, err := plf.ln.AcceptTCP()
+		ac, err := ln.AcceptTCP()
 		if err != nil {
-			return err
+			logrus.WithError(err).Errorf("Stopping listening %#v", ln)
+			return
 		}
+		plf.incomingConns <- ac
+	}
+}
+
+func (plf *pseudoLoopbackForwarder) accept() {
+	for _, ln := range plf.lns {
+		go plf.acceptLn(ln)
+	}
+}
+
+func (plf *pseudoLoopbackForwarder) Serve() error {
+	plf.accept()
+
+	for ac := range plf.incomingConns {
 		remoteAddr := ac.RemoteAddr().String() // ip:port
 		remoteAddrIP, _, err := net.SplitHostPort(remoteAddr)
 		if err != nil {
@@ -128,7 +155,7 @@ func (plf *pseudoLoopbackForwarder) Serve() error {
 			ac.Close()
 			continue
 		}
-		if remoteAddrIP != "127.0.0.1" {
+		if remoteAddrIP != "127.0.0.1" && remoteAddrIP != "::" {
 			logrus.WithError(err).Debugf("pseudoloopback forwarder: rejecting non-loopback remoteAddr %q", remoteAddr)
 			ac.Close()
 			continue
@@ -139,6 +166,8 @@ func (plf *pseudoLoopbackForwarder) Serve() error {
 			}
 		}(ac)
 	}
+
+	return nil
 }
 
 func (plf *pseudoLoopbackForwarder) forward(ac *net.TCPConn) error {
@@ -153,6 +182,8 @@ func (plf *pseudoLoopbackForwarder) forward(ac *net.TCPConn) error {
 }
 
 func (plf *pseudoLoopbackForwarder) Close() error {
-	_ = plf.ln.Close()
+	for _, ln := range plf.lns {
+		_ = ln.Close()
+	}
 	return plf.onClose()
 }
diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go
index c86b348dc51c..08cc43daf55b 100644
--- a/pkg/limayaml/defaults.go
+++ b/pkg/limayaml/defaults.go
@@ -646,10 +646,14 @@ func FillPortForwardDefaults(rule *PortForward, instDir string) {
 		} else {
 			rule.GuestIP = api.IPv4loopback1
 		}
+		rule.GuestIPWasUndefined = true
 	}
+
 	if rule.HostIP == nil {
 		rule.HostIP = api.IPv4loopback1
+		rule.HostIPWasUndefined = true
 	}
+
 	if rule.GuestPortRange[0] == 0 && rule.GuestPortRange[1] == 0 {
 		if rule.GuestPort == 0 {
 			rule.GuestPortRange[0] = 1
diff --git a/pkg/limayaml/defaults_test.go b/pkg/limayaml/defaults_test.go
index 7b59912ce875..d47d30c1d39d 100644
--- a/pkg/limayaml/defaults_test.go
+++ b/pkg/limayaml/defaults_test.go
@@ -110,12 +110,14 @@ func TestFillDefault(t *testing.T) {
 	}
 
 	defaultPortForward := PortForward{
-		GuestIP:        api.IPv4loopback1,
-		GuestPortRange: [2]int{1, 65535},
-		HostIP:         api.IPv4loopback1,
-		HostPortRange:  [2]int{1, 65535},
-		Proto:          TCP,
-		Reverse:        false,
+		GuestIP:             api.IPv4loopback1,
+		GuestPortRange:      [2]int{1, 65535},
+		HostIP:              api.IPv4loopback1,
+		HostPortRange:       [2]int{1, 65535},
+		Proto:               TCP,
+		Reverse:             false,
+		HostIPWasUndefined:  true,
+		GuestIPWasUndefined: true,
 	}
 
 	// ------------------------------------------------------------------------------------
diff --git a/pkg/limayaml/limayaml.go b/pkg/limayaml/limayaml.go
index 8789d98ac772..b77235adbaa0 100644
--- a/pkg/limayaml/limayaml.go
+++ b/pkg/limayaml/limayaml.go
@@ -189,6 +189,12 @@ type PortForward struct {
 	Proto             Proto  `yaml:"proto,omitempty" json:"proto,omitempty"`
 	Reverse           bool   `yaml:"reverse,omitempty" json:"reverse,omitempty"`
 	Ignore            bool   `yaml:"ignore,omitempty" json:"ignore,omitempty"`
+
+	// Set in case the HostIP field was automatically filled by FillPortForwardDefaults
+	HostIPWasUndefined bool `yaml:"-" json:"-"`
+
+	// Set in case the GuestIP field was automatically filled by FillPortForwardDefaults
+	GuestIPWasUndefined bool `yaml:"-" json:"-"`
 }
 
 type CopyToHost struct {
diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go
index 4ef2b2065cfd..fc3bccfab108 100644
--- a/pkg/limayaml/validate.go
+++ b/pkg/limayaml/validate.go
@@ -172,8 +172,10 @@ func Validate(y LimaYAML, warn bool) error {
 	}
 	for i, rule := range y.PortForwards {
 		field := fmt.Sprintf("portForwards[%d]", i)
-		if rule.GuestIPMustBeZero && !rule.GuestIP.Equal(net.IPv4zero) {
-			return fmt.Errorf("field `%s.guestIPMustBeZero` can only be true when field `%s.guestIP` is 0.0.0.0", field, field)
+		if rule.GuestIPMustBeZero && !rule.GuestIP.Equal(net.IPv4zero) && !rule.GuestIP.Equal(net.IPv6zero) {
+			// Using IPv6 first so go vet doesn't complain about the error
+			// message ending with a colon.
+			return fmt.Errorf("field `%s.guestIPMustBeZero` can only be true when field `%s.guestIP` is either :: or 0.0.0.0", field, field)
 		}
 		if rule.GuestPort != 0 {
 			if rule.GuestSocket != "" {

From 33d7e73d7dd629c135c917fe322b24d11ddff34a Mon Sep 17 00:00:00 2001
From: "Vito (Victor Gama)" <hey@vito.io>
Date: Tue, 16 May 2023 15:35:44 -0300
Subject: [PATCH 2/2] fixup: Update `guestIPMustBeZero` error message

Co-authored-by: Jan Dubois <jan@jandubois.com>
Signed-off-by: Victor Gama <hey@vito.io>
---
 pkg/limayaml/validate.go | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go
index fc3bccfab108..5207e7c692b0 100644
--- a/pkg/limayaml/validate.go
+++ b/pkg/limayaml/validate.go
@@ -173,9 +173,7 @@ func Validate(y LimaYAML, warn bool) error {
 	for i, rule := range y.PortForwards {
 		field := fmt.Sprintf("portForwards[%d]", i)
 		if rule.GuestIPMustBeZero && !rule.GuestIP.Equal(net.IPv4zero) && !rule.GuestIP.Equal(net.IPv6zero) {
-			// Using IPv6 first so go vet doesn't complain about the error
-			// message ending with a colon.
-			return fmt.Errorf("field `%s.guestIPMustBeZero` can only be true when field `%s.guestIP` is either :: or 0.0.0.0", field, field)
+			return fmt.Errorf("field `%s.guestIPMustBeZero` can only be true when field `%s.guestIP` is either `0.0.0.0` or `::`", field, field)
 		}
 		if rule.GuestPort != 0 {
 			if rule.GuestSocket != "" {