Skip to content

Commit

Permalink
Support port forwarding for privileged ports (1-1023)
Browse files Browse the repository at this point in the history
On macOS, listening on 127.0.0.1:80 requires root while 0.0.0.0:80 does not require root.

For such ports, the hostagent launches "pseudoloopback" forwarder that listens on 0.0.0.0:80
but rejects connections from non-loopback src IP.

Signed-off-by: Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
  • Loading branch information
AkihiroSuda committed Oct 5, 2021
1 parent bab1029 commit 11c08ca
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 14 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,6 @@ $ lima nerdctl run -d --name nginx -p 127.0.0.1:8080:80 nginx:alpine

http://127.0.0.1:8080 is accessible from both macOS and Linux.

> **NOTE**
> Privileged ports (1-1023) cannot be forwarded
For the usage of containerd and nerdctl (contaiNERD ctl), visit https://github.com/containerd/containerd and https://github.com/containerd/nerdctl.

## Getting started
Expand Down Expand Up @@ -316,7 +313,11 @@ Note: **Only** on macOS versions **before** 10.15.7 you might need to add this e

### SSH
#### "Port forwarding does not work"
Privileged ports (1-1023) cannot be forwarded. e.g., you have to use 8080, not 80.
Prior to Lima v0.7.0, Lima did not support forwarding privileged ports (1-1023). e.g., you had to use 8080, not 80.

Lima v0.7.0 and later supports forwarding privileged ports on macOS hosts.

On Linux hosts, you might have to set sysctl value `net.ipv4.ip_unprivileged_port_start=0`.

#### stuck on "Waiting for the essential requirement 1 of X: "ssh"

Expand Down
1 change: 0 additions & 1 deletion pkg/hostagent/hostagent.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import (
"github.com/lima-vm/lima/pkg/store"
"github.com/lima-vm/lima/pkg/store/filenames"
"github.com/lima-vm/sshocker/pkg/ssh"

"github.com/sirupsen/logrus"
)

Expand Down
4 changes: 2 additions & 2 deletions pkg/hostagent/port.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func (pf *portForwarder) OnEvent(ctx context.Context, ev api.Event) {
}
pf.l.Infof("Stopping forwarding TCP from %s to %s", remote, local)
verbCancel := true
if err := forwardSSH(ctx, pf.sshConfig, pf.sshHostPort, local, remote, verbCancel); err != nil {
if err := forwardTCP(ctx, pf.sshConfig, pf.sshHostPort, local, remote, verbCancel); err != nil {
if _, ok := pf.tcp[f.Port]; ok {
pf.l.WithError(err).Warnf("failed to stop forwarding TCP port %d", f.Port)
} else {
Expand All @@ -84,7 +84,7 @@ func (pf *portForwarder) OnEvent(ctx context.Context, ev api.Event) {
continue
}
pf.l.Infof("Forwarding TCP from %s to %s", remote, local)
if err := forwardSSH(ctx, pf.sshConfig, pf.sshHostPort, local, remote, false); err != nil {
if err := forwardTCP(ctx, pf.sshConfig, pf.sshHostPort, local, remote, false); err != nil {
pf.l.WithError(err).Warnf("failed to set up forwarding TCP port %d (negligible if already forwarded)", f.Port)
} else {
pf.tcp[f.Port] = struct{}{}
Expand Down
158 changes: 158 additions & 0 deletions pkg/hostagent/port_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package hostagent

import (
"context"
"fmt"
"net"
"os"
"path/filepath"
"strconv"
"sync"

"github.com/lima-vm/sshocker/pkg/ssh"
"github.com/norouter/norouter/pkg/agent/bicopy"
"github.com/sirupsen/logrus"
)

func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, remote string, cancel bool) error {
localIPStr, localPortStr, err := net.SplitHostPort(local)
if err != nil {
return err
}
localIP := net.ParseIP(localIPStr)
localPort, err := strconv.Atoi(localPortStr)
if err != nil {
return err
}

if !net.ParseIP("127.0.0.1").Equal(localIP) || localPort >= 1024 {
return forwardSSH(ctx, sshConfig, port, local, remote, cancel)
}

// on macOS, listening on 127.0.0.1:80 requires root while 0.0.0.0:80 does not require root.
// https://twitter.com/_AkihiroSuda_/status/1403403845842075648
//
// We use "pseudoloopback" forwarder that listens on 0.0.0.0:80 but rejects connections from non-loopback src IP.
logrus.Debugf("using pseudoloopback port forwarder for %q", local)

if cancel {
pseudoLoopbackForwardersMu.Lock()
plf, ok := pseudoLoopbackForwarders[local]
pseudoLoopbackForwardersMu.Unlock()
if ok {
localUnix := plf.unixAddr.Name
_ = plf.Close()
pseudoLoopbackForwardersMu.Lock()
delete(pseudoLoopbackForwarders, local)
pseudoLoopbackForwardersMu.Unlock()
if err := forwardSSH(ctx, sshConfig, port, localUnix, remote, cancel); err != nil {
return err
}
}
return nil
}

localUnixDir, err := os.MkdirTemp("/tmp", fmt.Sprintf("lima-psl-%s-%d-", localIP, localPort))
if err != nil {
return err
}
localUnix := filepath.Join(localUnixDir, "sock")
if err := forwardSSH(ctx, sshConfig, port, localUnix, remote, cancel); err != nil {
_ = os.RemoveAll(localUnixDir)
return err
}
plf, err := newPseudoLoopbackForwarder(localPort, localUnix)
if err != nil {
_ = os.RemoveAll(localUnixDir)
return err
}
plf.onClose = func() error {
return os.RemoveAll(localUnixDir)
}
pseudoLoopbackForwardersMu.Lock()
pseudoLoopbackForwarders[local] = plf
pseudoLoopbackForwardersMu.Unlock()
go func() {
if plfErr := plf.Serve(); plfErr != nil {
logrus.WithError(plfErr).Warning("pseudoloopback forwarder crashed")
}
}()
return nil
}

var (
pseudoLoopbackForwarders = make(map[string]*pseudoLoopbackForwarder)
pseudoLoopbackForwardersMu sync.Mutex
)

type pseudoLoopbackForwarder struct {
ln *net.TCPListener
unixAddr *net.UnixAddr
onClose func() error
}

func newPseudoLoopbackForwarder(localPort int, unixSock string) (*pseudoLoopbackForwarder, error) {
unixAddr, err := net.ResolveUnixAddr("unix", unixSock)
if err != nil {
return nil, err
}

lnAddr, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("0.0.0.0:%d", localPort))
if err != nil {
return nil, err
}
ln, err := net.ListenTCP("tcp4", lnAddr)
if err != nil {
return nil, err
}

plf := &pseudoLoopbackForwarder{
ln: ln,
unixAddr: unixAddr,
}

return plf, nil
}

func (plf *pseudoLoopbackForwarder) Serve() error {
defer plf.ln.Close()
for {
ac, err := plf.ln.AcceptTCP()
if err != nil {
return err
}
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" {
logrus.WithError(err).Debugf("pseudoloopback forwarder: rejecting non-loopback remoteAddr %q", remoteAddr)
ac.Close()
continue
}
go func(ac *net.TCPConn) {
if fErr := plf.forward(ac); fErr != nil {
logrus.Error(fErr)
}
}(ac)
}
}

func (plf *pseudoLoopbackForwarder) forward(ac *net.TCPConn) error {
defer ac.Close()
unixConn, err := net.DialUnix("unix", nil, plf.unixAddr)
if err != nil {
return err
}
defer unixConn.Close()
bicopy.Bicopy(ac, unixConn, nil)
return nil
}

func (plf *pseudoLoopbackForwarder) Close() error {
_ = plf.ln.Close()
return plf.onClose()
}
14 changes: 14 additions & 0 deletions pkg/hostagent/port_others.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//go:build !darwin
// +build !darwin

package hostagent

import (
"context"

"github.com/lima-vm/sshocker/pkg/ssh"
)

func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, remote string, cancel bool) error {
return forwardSSH(ctx, sshConfig, port, local, remote, cancel)
}
8 changes: 4 additions & 4 deletions pkg/limayaml/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -165,15 +165,15 @@ networks:
# hostPort: 8080 # overrides the default value 80
# - guestIP: "127.0.0.2" # overrides the default value "127.0.0.1"
# hostIP: "127.0.0.2" # overrides the default value "127.0.0.1"
# # default: guestPortRange: [1024, 65535]
# # default: hostPortRange: [1024, 65535]
# # default: guestPortRange: [1, 65535]
# # default: hostPortRange: [1, 65535]
# - guestPort: 8888
# ignore: true (don't forward this port)
# # Lima internally appends this fallback rule at the end:
# - guestIP: "127.0.0.1"
# guestPortRange: [1024, 65535]
# guestPortRange: [1, 65535]
# hostIP: "127.0.0.1"
# hostPortRange: [1024, 65535]
# hostPortRange: [1, 65535]
# # Any port still not matched by a rule will not be forwarded (ignored)

# Extra environment variables that will be loaded into the VM at start up.
Expand Down
6 changes: 3 additions & 3 deletions pkg/limayaml/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ func FillDefault(y *LimaYAML, filePath string) {
if len(y.Network.VDEDeprecated) > 0 && len(y.Networks) == 0 {
for _, vde := range y.Network.VDEDeprecated {
network := Network{
Interface: vde.Name,
Interface: vde.Name,
MACAddress: vde.MACAddress,
SwitchPort: vde.SwitchPort,
VNL: vde.VNL,
VNL: vde.VNL,
}
y.Networks = append(y.Networks, network)
}
Expand Down Expand Up @@ -112,7 +112,7 @@ func FillPortForwardDefaults(rule *PortForward) {
}
if rule.GuestPortRange[0] == 0 && rule.GuestPortRange[1] == 0 {
if rule.GuestPort == 0 {
rule.GuestPortRange[0] = 1024
rule.GuestPortRange[0] = 1
rule.GuestPortRange[1] = 65535
} else {
rule.GuestPortRange[0] = rule.GuestPort
Expand Down

0 comments on commit 11c08ca

Please sign in to comment.