Repository: lima-vm/lima
Pull request #836 (2022-05-01 11:43:20)

Enable reverse forwarding of portForwards
By default, the portForwards will be forwarded
from local (host) to remote (guest). Flag: `-L`

With reverse, the portForwards will be forwarded
from remote (guest) to local (host). Flag: `-R`

Only implemented for unix sockets at the moment.

I kept the confusing wording, of the logging entries.

Issue #834

Tested by forwarding a memcached socket from the host.

`memcached -s mc.sock`

```yaml
portForwards:
 - guestSocket: "/tmp/mc.sock"
   hostSocket: "{{.Home}}/mc.sock"
   reverse: true
```

`lima sudo apt install -y libmemcached-tools`
`lima MEMCACHED_SERVERS=/tmp/mc.sock memcping`

----

ssh says:

`-L local_socket:remote_socket`
Specifies that connections to the given TCP port or Unix socket on the local (client) host are to be forwarded to the given host and port, or Unix socket, on the remote side.
i.e. from host, to guest
`-R remote_socket:local_socket`
Specifies that connections to the given TCP port or Unix socket on the remote (server) host are to be forwarded to the local side.
i.e. from guest, to host

lima says:

reverse: false
`logrus.Infof("Forwarding %q (guest) to %q (host)", remote, local)`
reverse: true
`logrus.Infof("Forwarding %q (host) to %q (guest)", local, remote)`



jandubois: > Only implemented for unix sockets at the moment.

I think the "documentation" in `default.yaml` should mention that this option is only supported for unix sockets, and the code in `pkg/limayaml/validate.go` should throw an error when it is set for regular TCP ports.

Patch:
diff --git a/examples/default.yaml b/examples/default.yaml
index 8e760fee..799f5d5f 100644
--- a/examples/default.yaml
+++ b/examples/default.yaml
@@ -266,8 +266,10 @@ networks:
 #
 # - guestSocket: "/run/user/{{.UID}}/my.sock"
 #   hostSocket: mysocket
+# # default: reverse: false
 # # "guestSocket" can include these template variables: {{.Home}}, {{.UID}}, and {{.User}}.
 # # "hostSocket" can include {{.Home}}, {{.Dir}}, {{.Name}}, {{.UID}}, and {{.User}}.
+# # "reverse" can only be used for unix sockets right now, not for tcp sockets.
 # # Put sockets into "{{.Dir}}/sock" to avoid collision with Lima internal sockets!
 # # Sockets can also be forwarded to ports and vice versa, but not to/from a range of ports.
 # # Forwarding requires the lima user to have rw access to the "guestsocket",
diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go
index bbf2e794..3755edc8 100644
--- a/pkg/hostagent/hostagent.go
+++ b/pkg/hostagent/hostagent.go
@@ -423,7 +423,7 @@ func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) {
 	for _, rule := range a.y.PortForwards {
 		if rule.GuestSocket != "" {
 			local := hostAddress(rule, guestagentapi.IPPort{})
-			_ = forwardSSH(ctx, a.sshConfig, a.sshLocalPort, local, rule.GuestSocket, verbForward)
+			_ = forwardSSH(ctx, a.sshConfig, a.sshLocalPort, local, rule.GuestSocket, verbForward, rule.Reverse)
 		}
 	}
 
@@ -437,12 +437,12 @@ func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) {
 			if rule.GuestSocket != "" {
 				local := hostAddress(rule, guestagentapi.IPPort{})
 				// using ctx.Background() because ctx has already been cancelled
-				if err := forwardSSH(context.Background(), a.sshConfig, a.sshLocalPort, local, rule.GuestSocket, verbCancel); err != nil {
+				if err := forwardSSH(context.Background(), a.sshConfig, a.sshLocalPort, local, rule.GuestSocket, verbCancel, rule.Reverse); err != nil {
 					mErr = multierror.Append(mErr, err)
 				}
 			}
 		}
-		if err := forwardSSH(context.Background(), a.sshConfig, a.sshLocalPort, localUnix, remoteUnix, verbCancel); err != nil {
+		if err := forwardSSH(context.Background(), a.sshConfig, a.sshLocalPort, localUnix, remoteUnix, verbCancel, false); err != nil {
 			mErr = multierror.Append(mErr, err)
 		}
 		return mErr
@@ -450,7 +450,7 @@ func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) {
 
 	for {
 		if !isGuestAgentSocketAccessible(ctx, localUnix) {
-			_ = forwardSSH(ctx, a.sshConfig, a.sshLocalPort, localUnix, remoteUnix, verbForward)
+			_ = forwardSSH(ctx, a.sshConfig, a.sshLocalPort, localUnix, remoteUnix, verbForward, false)
 		}
 		if err := a.processGuestAgentEvents(ctx, localUnix); err != nil {
 			if !errors.Is(err, context.Canceled) {
@@ -506,12 +506,22 @@ const (
 	verbCancel  = "cancel"
 )
 
-func forwardSSH(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, remote string, verb string) error {
+func forwardSSH(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, remote string, verb string, reverse bool) error {
 	args := sshConfig.Args()
 	args = append(args,
 		"-T",
 		"-O", verb,
-		"-L", local+":"+remote,
+	)
+	if reverse {
+		args = append(args,
+			"-R", remote+":"+local,
+		)
+	} else {
+		args = append(args,
+			"-L", local+":"+remote,
+		)
+	}
+	args = append(args,
 		"-N",
 		"-f",
 		"-p", strconv.Itoa(port),
@@ -521,7 +531,11 @@ func forwardSSH(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local,
 	if strings.HasPrefix(local, "/") {
 		switch verb {
 		case verbForward:
-			logrus.Infof("Forwarding %q (guest) to %q (host)", remote, local)
+			if reverse {
+				logrus.Infof("Forwarding %q (host) to %q (guest)", local, remote)
+			} else {
+				logrus.Infof("Forwarding %q (guest) to %q (host)", remote, local)
+			}
 			if err := os.RemoveAll(local); err != nil {
 				logrus.WithError(err).Warnf("Failed to clean up %q (host) before setting up forwarding", local)
 			}
@@ -529,7 +543,11 @@ func forwardSSH(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local,
 				return fmt.Errorf("can't create directory for local socket %q: %w", local, err)
 			}
 		case verbCancel:
-			logrus.Infof("Stopping forwarding %q (guest) to %q (host)", remote, local)
+			if reverse {
+				logrus.Infof("Stopping forwarding %q (host) to %q (guest)", local, remote)
+			} else {
+				logrus.Infof("Stopping forwarding %q (guest) to %q (host)", remote, local)
+			}
 			defer func() {
 				if err := os.RemoveAll(local); err != nil {
 					logrus.WithError(err).Warnf("Failed to clean up %q (host) after stopping forwarding", local)
diff --git a/pkg/hostagent/port_darwin.go b/pkg/hostagent/port_darwin.go
index 86617b25..fead0401 100644
--- a/pkg/hostagent/port_darwin.go
+++ b/pkg/hostagent/port_darwin.go
@@ -18,7 +18,7 @@ import (
 // forwardTCP is not thread-safe
 func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, remote string, verb string) error {
 	if strings.HasPrefix(local, "/") {
-		return forwardSSH(ctx, sshConfig, port, local, remote, verb)
+		return forwardSSH(ctx, sshConfig, port, local, remote, verb, false)
 	}
 	localIPStr, localPortStr, err := net.SplitHostPort(local)
 	if err != nil {
@@ -31,7 +31,7 @@ func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local,
 	}
 
 	if !localIP.Equal(api.IPv4loopback1) || localPort >= 1024 {
-		return forwardSSH(ctx, sshConfig, port, local, remote, verb)
+		return forwardSSH(ctx, sshConfig, port, local, remote, verb, false)
 	}
 
 	// on macOS, listening on 127.0.0.1:80 requires root while 0.0.0.0:80 does not require root.
@@ -46,7 +46,7 @@ func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local,
 			localUnix := plf.unixAddr.Name
 			_ = plf.Close()
 			delete(pseudoLoopbackForwarders, local)
-			if err := forwardSSH(ctx, sshConfig, port, localUnix, remote, verb); err != nil {
+			if err := forwardSSH(ctx, sshConfig, port, localUnix, remote, verb, false); err != nil {
 				return err
 			}
 		} else {
@@ -61,12 +61,12 @@ func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local,
 	}
 	localUnix := filepath.Join(localUnixDir, "sock")
 	logrus.Debugf("forwarding %q to %q", localUnix, remote)
-	if err := forwardSSH(ctx, sshConfig, port, localUnix, remote, verb); err != nil {
+	if err := forwardSSH(ctx, sshConfig, port, localUnix, remote, verb, false); err != nil {
 		return err
 	}
 	plf, err := newPseudoLoopbackForwarder(localPort, localUnix)
 	if err != nil {
-		if cancelErr := forwardSSH(ctx, sshConfig, port, localUnix, remote, verbCancel); cancelErr != nil {
+		if cancelErr := forwardSSH(ctx, sshConfig, port, localUnix, remote, verbCancel, false); cancelErr != nil {
 			logrus.WithError(cancelErr).Warnf("failed to cancel forwarding %q to %q", localUnix, remote)
 		}
 		return err
diff --git a/pkg/hostagent/port_others.go b/pkg/hostagent/port_others.go
index 6854c43f..35d276d6 100644
--- a/pkg/hostagent/port_others.go
+++ b/pkg/hostagent/port_others.go
@@ -10,5 +10,5 @@ import (
 )
 
 func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, remote string, verb string) error {
-	return forwardSSH(ctx, sshConfig, port, local, remote, verb)
+	return forwardSSH(ctx, sshConfig, port, local, remote, verb, false)
 }
diff --git a/pkg/limayaml/limayaml.go b/pkg/limayaml/limayaml.go
index c7ba62f0..e165241a 100644
--- a/pkg/limayaml/limayaml.go
+++ b/pkg/limayaml/limayaml.go
@@ -144,6 +144,7 @@ type PortForward struct {
 	HostPortRange     [2]int `yaml:"hostPortRange,omitempty" json:"hostPortRange,omitempty"`
 	HostSocket        string `yaml:"hostSocket,omitempty" json:"hostSocket,omitempty"`
 	Proto             Proto  `yaml:"proto,omitempty" json:"proto,omitempty"`
+	Reverse           bool   `yaml:"reverse,omitempty" json:"reverse,omitempty"`
 	Ignore            bool   `yaml:"ignore,omitempty" json:"ignore,omitempty"`
 }
 
diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go
index 6c8ada3e..ed7edc22 100644
--- a/pkg/limayaml/validate.go
+++ b/pkg/limayaml/validate.go
@@ -212,6 +212,12 @@ func Validate(y LimaYAML, warn bool) error {
 		if rule.Proto != TCP {
 			return fmt.Errorf("field `%s.proto` must be %q", field, TCP)
 		}
+		if rule.Reverse && rule.GuestSocket == "" {
+			return fmt.Errorf("field `%s.reverse` must be %t", field, false)
+		}
+		if rule.Reverse && rule.HostSocket == "" {
+			return fmt.Errorf("field `%s.reverse` must be %t", field, false)
+		}
 		// Not validating that the various GuestPortRanges and HostPortRanges are not overlapping. Rules will be
 		// processed sequentially and the first matching rule for a guest port determines forwarding behavior.
 	}


Test patch:
diff --git a/pkg/limayaml/defaults_test.go b/pkg/limayaml/defaults_test.go
index a7327a4c..72ca04b3 100644
--- a/pkg/limayaml/defaults_test.go
+++ b/pkg/limayaml/defaults_test.go
@@ -88,6 +88,7 @@ func TestFillDefault(t *testing.T) {
 		HostIP:         api.IPv4loopback1,
 		HostPortRange:  [2]int{1, 65535},
 		Proto:          TCP,
+		Reverse:        false,
 	}
 
 	// ------------------------------------------------------------------------------------