From 8108012812d0affd77ed0aef48b22f7e9f516b36 Mon Sep 17 00:00:00 2001 From: Norio Nomura Date: Sat, 6 Sep 2025 10:24:02 +0900 Subject: [PATCH 1/2] portfwd: support HostSocket Signed-off-by: Norio Nomura portfwd: remove "unixgram" forwarding code because that does not work Signed-off-by: Norio Nomura portfwd: do not use `listenConfig` param on Unix domain sockets Signed-off-by: Norio Nomura --- pkg/limayaml/defaults.go | 15 +++++++-------- pkg/limayaml/defaults_test.go | 1 + pkg/limayaml/validate.go | 12 +++++++----- pkg/portfwd/listener.go | 12 ++++++++++++ pkg/portfwd/listener_darwin.go | 14 ++++++++++++++ pkg/portfwd/listener_others.go | 16 ++++++++++++++++ 6 files changed, 57 insertions(+), 13 deletions(-) diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go index 1d189c4e6e6..9e546d72e02 100644 --- a/pkg/limayaml/defaults.go +++ b/pkg/limayaml/defaults.go @@ -902,14 +902,6 @@ func FillPortForwardDefaults(rule *limatype.PortForward, instDir string, user li rule.GuestPortRange[1] = rule.GuestPort } } - if rule.HostPortRange[0] == 0 && rule.HostPortRange[1] == 0 { - if rule.HostPort == 0 { - rule.HostPortRange = rule.GuestPortRange - } else { - rule.HostPortRange[0] = rule.HostPort - rule.HostPortRange[1] = rule.HostPort - } - } if rule.GuestSocket != "" { if out, err := executeGuestTemplate(rule.GuestSocket, instDir, user, param); err == nil { rule.GuestSocket = out.String() @@ -926,6 +918,13 @@ func FillPortForwardDefaults(rule *limatype.PortForward, instDir string, user li if !filepath.IsAbs(rule.HostSocket) { rule.HostSocket = filepath.Join(instDir, filenames.SocketDir, rule.HostSocket) } + } else if rule.HostPortRange[0] == 0 && rule.HostPortRange[1] == 0 { + if rule.HostPort == 0 { + rule.HostPortRange = rule.GuestPortRange + } else { + rule.HostPortRange[0] = rule.HostPort + rule.HostPortRange[1] = rule.HostPort + } } } diff --git a/pkg/limayaml/defaults_test.go b/pkg/limayaml/defaults_test.go index aa9a8253c5c..768650a5466 100644 --- a/pkg/limayaml/defaults_test.go +++ b/pkg/limayaml/defaults_test.go @@ -268,6 +268,7 @@ func TestFillDefault(t *testing.T) { expect.PortForwards[2].HostPort = 8888 expect.PortForwards[2].HostPortRange = [2]int{8888, 8888} + expect.PortForwards[3].HostPortRange = [2]int{0, 0} expect.PortForwards[3].GuestSocket = fmt.Sprintf("%s | %s | %s | %s", user.HomeDir, user.Uid, user.Username, y.Param["ONE"]) expect.PortForwards[3].HostSocket = fmt.Sprintf("%s | %s | %s | %s | %s | %s", hostHome, instDir, instName, currentUser.Uid, currentUser.Username, y.Param["ONE"]) diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go index d38d55e1768..87ffb7e65be 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -314,8 +314,10 @@ func Validate(y *limatype.LimaYAML, warn bool) error { if err := validatePort(fmt.Sprintf("%s.guestPortRange[%d]", field, j), rule.GuestPortRange[j]); err != nil { errs = errors.Join(errs, err) } - if err := validatePort(fmt.Sprintf("%s.hostPortRange[%d]", field, j), rule.HostPortRange[j]); err != nil { - errs = errors.Join(errs, err) + if rule.HostSocket == "" { + if err := validatePort(fmt.Sprintf("%s.hostPortRange[%d]", field, j), rule.HostPortRange[j]); err != nil { + errs = errors.Join(errs, err) + } } } if rule.GuestPortRange[0] > rule.GuestPortRange[1] { @@ -324,9 +326,6 @@ func Validate(y *limatype.LimaYAML, warn bool) error { if rule.HostPortRange[0] > rule.HostPortRange[1] { errs = errors.Join(errs, fmt.Errorf("field `%s.hostPortRange[1]` must be greater than or equal to field `%s.hostPortRange[0]`", field, field)) } - if rule.GuestPortRange[1]-rule.GuestPortRange[0] != rule.HostPortRange[1]-rule.HostPortRange[0] { - errs = errors.Join(errs, fmt.Errorf("field `%s.hostPortRange` must specify the same number of ports as field `%s.guestPortRange`", field, field)) - } if rule.GuestSocket != "" { if !path.IsAbs(rule.GuestSocket) { errs = errors.Join(errs, fmt.Errorf("field `%s.guestSocket` must be an absolute path, but is %q", field, rule.GuestSocket)) @@ -343,7 +342,10 @@ func Validate(y *limatype.LimaYAML, warn bool) error { if rule.GuestSocket == "" && rule.GuestPortRange[1]-rule.GuestPortRange[0] > 0 { errs = errors.Join(errs, fmt.Errorf("field `%s.hostSocket` can only be mapped from a single port or socket. not a range", field)) } + } else if rule.GuestPortRange[1]-rule.GuestPortRange[0] != rule.HostPortRange[1]-rule.HostPortRange[0] { + errs = errors.Join(errs, fmt.Errorf("field `%s.hostPortRange` must specify the same number of ports as field `%s.guestPortRange`", field, field)) } + if len(rule.HostSocket) >= osutil.UnixPathMax { errs = errors.Join(errs, fmt.Errorf("field `%s.hostSocket` must be less than UNIX_PATH_MAX=%d characters, but is %d", field, osutil.UnixPathMax, len(rule.HostSocket))) diff --git a/pkg/portfwd/listener.go b/pkg/portfwd/listener.go index f34c18243f1..f1949ce81d8 100644 --- a/pkg/portfwd/listener.go +++ b/pkg/portfwd/listener.go @@ -8,6 +8,8 @@ import ( "errors" "fmt" "net" + "os" + "path/filepath" "strings" "sync" @@ -146,3 +148,13 @@ func (p *ClosableListeners) forwardUDP(ctx context.Context, client *guestagentcl func key(protocol, hostAddress, guestAddress string) string { return fmt.Sprintf("%s-%s-%s", protocol, hostAddress, guestAddress) } + +func prepareUnixSocket(hostSocket string) error { + if err := os.RemoveAll(hostSocket); err != nil { + return fmt.Errorf("can't clean up %q: %w", hostSocket, err) + } + if err := os.MkdirAll(filepath.Dir(hostSocket), 0o755); err != nil { + return fmt.Errorf("can't create directory for local socket %q: %w", hostSocket, err) + } + return nil +} diff --git a/pkg/portfwd/listener_darwin.go b/pkg/portfwd/listener_darwin.go index f884550f81c..ea826f5ad79 100644 --- a/pkg/portfwd/listener_darwin.go +++ b/pkg/portfwd/listener_darwin.go @@ -7,12 +7,26 @@ import ( "context" "fmt" "net" + "path/filepath" "strconv" "github.com/sirupsen/logrus" ) func Listen(ctx context.Context, listenConfig net.ListenConfig, hostAddress string) (net.Listener, error) { + if filepath.IsAbs(hostAddress) { + // Handle Unix domain sockets + if err := prepareUnixSocket(hostAddress); err != nil { + return nil, err + } + var lc net.ListenConfig + unixLis, err := lc.Listen(ctx, "unix", hostAddress) + if err != nil { + logrus.WithError(err).Errorf("failed to listen unix: %v", hostAddress) + return nil, err + } + return unixLis, nil + } localIPStr, localPortStr, _ := net.SplitHostPort(hostAddress) localIP := net.ParseIP(localIPStr) localPort, _ := strconv.Atoi(localPortStr) diff --git a/pkg/portfwd/listener_others.go b/pkg/portfwd/listener_others.go index bab110844dd..d86619b4c94 100644 --- a/pkg/portfwd/listener_others.go +++ b/pkg/portfwd/listener_others.go @@ -8,9 +8,25 @@ package portfwd import ( "context" "net" + "path/filepath" + + "github.com/sirupsen/logrus" ) func Listen(ctx context.Context, listenConfig net.ListenConfig, hostAddress string) (net.Listener, error) { + if filepath.IsAbs(hostAddress) { + // Handle Unix domain sockets + if err := prepareUnixSocket(hostAddress); err != nil { + return nil, err + } + var lc net.ListenConfig + unixLis, err := lc.Listen(ctx, "unix", hostAddress) + if err != nil { + logrus.WithError(err).Errorf("failed to listen unix: %v", hostAddress) + return nil, err + } + return unixLis, nil + } return listenConfig.Listen(ctx, "tcp", hostAddress) } From ef88218e9274e2dd519a3def3218eb4f057ce2b9 Mon Sep 17 00:00:00 2001 From: Norio Nomura Date: Sun, 21 Sep 2025 10:21:03 +0900 Subject: [PATCH 2/2] hack/test-port-forwarding.pl: Add a test using `hostSocket` Signed-off-by: Norio Nomura hack/test-port-forwarding.pl: use platform-independent path on hostSocket Signed-off-by: Norio Nomura hack/test-port-forwarding.pl: Skip hostSocket test on Windows host Signed-off-by: Norio Nomura --- hack/test-port-forwarding.pl | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/hack/test-port-forwarding.pl b/hack/test-port-forwarding.pl index 68394ae9ee4..9e98db43bfa 100755 --- a/hack/test-port-forwarding.pl +++ b/hack/test-port-forwarding.pl @@ -16,6 +16,7 @@ use warnings; use Config qw(%Config); +use File::Spec::Functions qw(catfile); use IO::Handle qw(); use Socket qw(inet_ntoa); use Sys::Hostname qw(hostname); @@ -33,6 +34,11 @@ chomp $ipv4; } +my $instDir = qx(limactl list "$instance" --yq .dir); +chomp $instDir; +# platform independent way to add trailing path separator +my $sockDir = catfile($instDir, "sock", ""); + # If $instance is a filename, add our portForwards to it to enable testing if (-f $instance) { open(my $fh, "+< $instance") or die "Can't open $instance for read/write: $!"; @@ -94,14 +100,17 @@ s/sshLocalPort/$sshLocalPort/g; s/ipv4/$ipv4/g; s/ipv6/$ipv6/g; + s/sockDir\//$sockDir/g; # forward: 127.0.0.1 899 → 127.0.0.1 799 # ignore: 127.0.0.2 8888 - /^(forward|ignore):\s+([0-9.:]+)\s+(\d+)(?:\s+→)?(?:\s+([0-9.:]+)(?:\s+(\d+))?)?/; + /^(forward|ignore):\s+([0-9.:]+)\s+(\d+)(?:\s+→)?(?:\s+(?:([0-9.:]+)(?:\s+(\d+))|(\S+))?)?/; die "Cannot parse test '$_'" unless $1; - my %test; @test{qw(mode guest_ip guest_port host_ip host_port)} = ($1, $2, $3, $4, $5); + my %test; @test{qw(mode guest_ip guest_port host_ip host_port host_socket)} = ($1, $2, $3, $4, $5, $6); + $test{host_ip} ||= "127.0.0.1"; $test{host_port} ||= $test{guest_port}; - if ($test{mode} eq "forward" && $test{host_port} < 1024 && $Config{osname} ne "darwin") { + $test{host_socket} ||= ""; + if ($test{mode} eq "forward" && $test{host_socket} eq "" && $test{host_port} < 1024 && $Config{osname} ne "darwin") { printf "🚧 Not supported on $Config{osname}: # $_\n"; next; } @@ -113,9 +122,13 @@ printf "🚧 Not supported for $instanceType machines: # $_\n"; next; } + if ($test{host_socket} ne "" && $Config{osname} eq "cygwin") { + printf "🚧 Not supported on $Config{osname}: # $_\n"; + next; + } my $remote = JoinHostPort($test{guest_ip},$test{guest_port}); - my $local = JoinHostPort($test{host_ip},$test{host_port}); + my $local = $test{host_socket} eq "" ? JoinHostPort($test{host_ip},$test{host_port}) : $test{host_socket}; if ($test{mode} eq "ignore") { $test{log_msg} = "Not forwarding TCP $remote"; } @@ -163,7 +176,7 @@ # Try to reach each listener from the host foreach my $test (@test) { next if $test->{host_port} == $sshLocalPort; - my $nc = "nc -w 1 $test->{host_ip} $test->{host_port}"; + my $nc = $test->{host_socket} eq "" ? "nc -w 1 $test->{host_ip} $test->{host_port}" : "nc -w 1 -U $test->{host_socket}"; open(my $netcat, "| $nc") or die "Can't run '$nc': $!"; print $netcat "$test->{log_msg}\n"; # Don't check for errors on close; macOS nc seems to return non-zero exit code even on success @@ -175,7 +188,7 @@ seek($log, $ha_log_size, 0) or die "Can't seek $ha_log to $ha_log_size: $!"; my %seen; while (<$log>) { - $seen{$1}++ if /(Forwarding TCP from .*? to (\d.*?|\[.*?\]):\d+)/; + $seen{$1}++ if /(Forwarding TCP from .*? to ((\d.*?|\[.*?\]):\d+|\/[^"]+))/; $seen{$1}++ if /(Not forwarding TCP .*?:\d+)/; } close $log or die; @@ -342,3 +355,8 @@ sub JoinHostPort { - guestIPMustBeZero: true guestPort: 8888 hostIP: 0.0.0.0 + +- guestPort: 5000 + hostSocket: port5000.sock + + # forward: 127.0.0.1 5000 → sockDir/port5000.sock