From d010add86f89c69d5472a2b5abd312612add074d Mon Sep 17 00:00:00 2001 From: Victor Gama Date: Mon, 15 May 2023 20:04:42 -0300 Subject: [PATCH] feat(ipv6): Add support to IPv6 port-forwarding Signed-off-by: Victor Gama --- 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 8def0472c14..4edb6b18d19 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 bb57f856716..121d407c94a 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 31edb0e7698..0b69f26f162 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 fead0401b44..8f9c708502a 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 c86b348dc51..08cc43daf55 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 7b59912ce87..d47d30c1d39 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 8789d98ac77..b77235adbaa 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 4ef2b2065cf..fc3bccfab10 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 != "" {