Skip to content

Commit

Permalink
feat(ipv6): Add support to IPv6 port-forwarding
Browse files Browse the repository at this point in the history
Signed-off-by: Victor Gama <[email protected]>
  • Loading branch information
heyvito committed May 18, 2023
1 parent e57ac78 commit d010add
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 35 deletions.
6 changes: 3 additions & 3 deletions hack/test-port-forwarding.pl
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down
18 changes: 14 additions & 4 deletions pkg/hostagent/hostagent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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,
Expand Down
68 changes: 66 additions & 2 deletions pkg/hostagent/port.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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()
}

Expand Down
67 changes: 49 additions & 18 deletions pkg/hostagent/port_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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) {
Expand All @@ -97,38 +98,64 @@ 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 {
logrus.WithError(err).Debugf("pseudoloopback forwarder: rejecting non-loopback remoteAddr %q (unparsable)", remoteAddr)
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
Expand All @@ -139,6 +166,8 @@ func (plf *pseudoLoopbackForwarder) Serve() error {
}
}(ac)
}

return nil
}

func (plf *pseudoLoopbackForwarder) forward(ac *net.TCPConn) error {
Expand All @@ -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()
}
4 changes: 4 additions & 0 deletions pkg/limayaml/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 8 additions & 6 deletions pkg/limayaml/defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

// ------------------------------------------------------------------------------------
Expand Down
6 changes: 6 additions & 0 deletions pkg/limayaml/limayaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 4 additions & 2 deletions pkg/limayaml/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down

0 comments on commit d010add

Please sign in to comment.