Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ipv6): Add support to IPv6 port-forwarding #1541

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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():
heyvito marked this conversation as resolved.
Show resolved Hide resolved
// 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):
heyvito marked this conversation as resolved.
Show resolved Hide resolved
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
4 changes: 2 additions & 2 deletions pkg/limayaml/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,8 @@ 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) {
return fmt.Errorf("field `%s.guestIPMustBeZero` can only be true when field `%s.guestIP` is either `0.0.0.0` or `::`", field, field)
}
if rule.GuestPort != 0 {
if rule.GuestSocket != "" {
Expand Down