diff --git a/hack/test-port-forwarding.pl b/hack/test-port-forwarding.pl index ed65b34eafaf..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 @@ -318,4 +318,4 @@ sub JoinHostPort { # The actual test code is in test-example.sh in the "port-forwarding" block. - guestIPMustBeZero: true guestPort: 8888 - hostIP: 0.0.0.0 \ No newline at end of file + hostIP: 0.0.0.0 diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index 72448b4498af..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} diff --git a/pkg/hostagent/port.go b/pkg/hostagent/port.go index ce6202c3179b..26a8e3bf5c08 100644 --- a/pkg/hostagent/port.go +++ b/pkg/hostagent/port.go @@ -40,38 +40,62 @@ func hostAddress(rule limayaml.PortForward, guest api.IPPort) string { return host.String() } -func tcpRuleIsWithinBounds(guest api.IPPort, rule limayaml.PortForward) bool { - if rule.GuestSocket != "" { - // Not TCP - return false - } +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 - // Check if `guest.Port` is within `rule.GuestPortRange` - return guest.Port >= rule.GuestPortRange[0] && guest.Port <= rule.GuestPortRange[1] -} + // 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 -func (pf *portForwarder) forwardingAddresses(guest api.IPPort) (hostAddr string, guestAddr string) { - // Check if it matches new IPv6 rule, otherwise fallback to the pre-IPv6 checks to maintain - // compatibility for _, rule := range pf.rules { - if !tcpRuleIsWithinBounds(guest, rule) { + if rule.GuestSocket != "" { + // Not TCP continue } - // guest.IP and HostIP must be either :: or 0.0.0.0, and be forwarded to the same family on HostIP - if guest.IP.IsUnspecified() && rule.HostIP.IsUnspecified() && ((guest.IP.To4() == nil) == (rule.HostIP.To4() == nil)) { - return hostAddress(rule, guest), guest.String() - } - } - - // Before giving up, try to match against the old rule set - for _, rule := range pf.rules { - if !tcpRuleIsWithinBounds(guest, rule) { + // 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 = true + + // 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: @@ -80,15 +104,32 @@ func (pf *portForwarder) forwardingAddresses(guest api.IPPort) (hostAddr 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/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/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 {