Skip to content

Commit 90e6c9d

Browse files
authored
Merge pull request #283 from AkihiroSuda/pseudoloopback
Support port forwarding for privileged ports (1-1023)
2 parents 15cd61e + fa0c7ba commit 90e6c9d

File tree

7 files changed

+188
-12
lines changed

7 files changed

+188
-12
lines changed

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,6 @@ $ lima nerdctl run -d --name nginx -p 127.0.0.1:8080:80 nginx:alpine
7777

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

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

8582
## Getting started
@@ -318,7 +315,11 @@ Note: **Only** on macOS versions **before** 10.15.7 you might need to add this e
318315

319316
### SSH
320317
#### "Port forwarding does not work"
321-
Privileged ports (1-1023) cannot be forwarded. e.g., you have to use 8080, not 80.
318+
Prior to Lima v0.7.0, Lima did not support forwarding privileged ports (1-1023). e.g., you had to use 8080, not 80.
319+
320+
Lima v0.7.0 and later supports forwarding privileged ports on macOS hosts.
321+
322+
On Linux hosts, you might have to set sysctl value `net.ipv4.ip_unprivileged_port_start=0`.
322323

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

pkg/hostagent/hostagent.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import (
2626
"github.com/lima-vm/lima/pkg/store"
2727
"github.com/lima-vm/lima/pkg/store/filenames"
2828
"github.com/lima-vm/sshocker/pkg/ssh"
29-
3029
"github.com/sirupsen/logrus"
3130
)
3231

pkg/hostagent/port.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func (pf *portForwarder) OnEvent(ctx context.Context, ev api.Event) {
6868
}
6969
pf.l.Infof("Stopping forwarding TCP from %s to %s", remote, local)
7070
verbCancel := true
71-
if err := forwardSSH(ctx, pf.sshConfig, pf.sshHostPort, local, remote, verbCancel); err != nil {
71+
if err := forwardTCP(ctx, pf.l, pf.sshConfig, pf.sshHostPort, local, remote, verbCancel); err != nil {
7272
if _, ok := pf.tcp[f.Port]; ok {
7373
pf.l.WithError(err).Warnf("failed to stop forwarding TCP port %d", f.Port)
7474
} else {
@@ -84,7 +84,7 @@ func (pf *portForwarder) OnEvent(ctx context.Context, ev api.Event) {
8484
continue
8585
}
8686
pf.l.Infof("Forwarding TCP from %s to %s", remote, local)
87-
if err := forwardSSH(ctx, pf.sshConfig, pf.sshHostPort, local, remote, false); err != nil {
87+
if err := forwardTCP(ctx, pf.l, pf.sshConfig, pf.sshHostPort, local, remote, false); err != nil {
8888
pf.l.WithError(err).Warnf("failed to set up forwarding TCP port %d (negligible if already forwarded)", f.Port)
8989
} else {
9090
pf.tcp[f.Port] = struct{}{}

pkg/hostagent/port_darwin.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package hostagent
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net"
7+
"os"
8+
"path/filepath"
9+
"strconv"
10+
11+
"github.com/lima-vm/sshocker/pkg/ssh"
12+
"github.com/norouter/norouter/pkg/agent/bicopy"
13+
"github.com/sirupsen/logrus"
14+
)
15+
16+
// forwardTCP is not thread-safe
17+
func forwardTCP(ctx context.Context, l *logrus.Logger, sshConfig *ssh.SSHConfig, port int, local, remote string, cancel bool) error {
18+
localIPStr, localPortStr, err := net.SplitHostPort(local)
19+
if err != nil {
20+
return err
21+
}
22+
localIP := net.ParseIP(localIPStr)
23+
localPort, err := strconv.Atoi(localPortStr)
24+
if err != nil {
25+
return err
26+
}
27+
28+
if !net.ParseIP("127.0.0.1").Equal(localIP) || localPort >= 1024 {
29+
return forwardSSH(ctx, sshConfig, port, local, remote, cancel)
30+
}
31+
32+
// on macOS, listening on 127.0.0.1:80 requires root while 0.0.0.0:80 does not require root.
33+
// https://twitter.com/_AkihiroSuda_/status/1403403845842075648
34+
//
35+
// We use "pseudoloopback" forwarder that listens on 0.0.0.0:80 but rejects connections from non-loopback src IP.
36+
l.Debugf("using pseudoloopback port forwarder for %q", local)
37+
38+
if cancel {
39+
plf, ok := pseudoLoopbackForwarders[local]
40+
if ok {
41+
localUnix := plf.unixAddr.Name
42+
_ = plf.Close()
43+
delete(pseudoLoopbackForwarders, local)
44+
if err := forwardSSH(ctx, sshConfig, port, localUnix, remote, cancel); err != nil {
45+
return err
46+
}
47+
} else {
48+
l.Warnf("forwarding for %q seems already cancelled?", local)
49+
}
50+
return nil
51+
}
52+
53+
localUnixDir, err := os.MkdirTemp("/tmp", fmt.Sprintf("lima-psl-%s-%d-", localIP, localPort))
54+
if err != nil {
55+
return err
56+
}
57+
localUnix := filepath.Join(localUnixDir, "sock")
58+
l.Debugf("forwarding %q to %q", localUnix, remote)
59+
if err := forwardSSH(ctx, sshConfig, port, localUnix, remote, cancel); err != nil {
60+
if removeErr := os.RemoveAll(localUnixDir); removeErr != nil {
61+
l.WithError(removeErr).Warnf("failed to remove %q", removeErr)
62+
}
63+
return err
64+
}
65+
plf, err := newPseudoLoopbackForwarder(l, localPort, localUnix)
66+
if err != nil {
67+
if removeErr := os.RemoveAll(localUnixDir); removeErr != nil {
68+
l.WithError(removeErr).Warnf("failed to remove %q", removeErr)
69+
}
70+
if cancelErr := forwardSSH(ctx, sshConfig, port, localUnix, remote, true); cancelErr != nil {
71+
l.WithError(cancelErr).Warnf("failed to cancel forwarding %q to %q", localUnix, remote)
72+
}
73+
return err
74+
}
75+
plf.onClose = func() error {
76+
return os.RemoveAll(localUnixDir)
77+
}
78+
pseudoLoopbackForwarders[local] = plf
79+
go func() {
80+
if plfErr := plf.Serve(); plfErr != nil {
81+
l.WithError(plfErr).Warning("pseudoloopback forwarder crashed")
82+
}
83+
}()
84+
return nil
85+
}
86+
87+
var pseudoLoopbackForwarders = make(map[string]*pseudoLoopbackForwarder)
88+
89+
type pseudoLoopbackForwarder struct {
90+
l *logrus.Logger
91+
ln *net.TCPListener
92+
unixAddr *net.UnixAddr
93+
onClose func() error
94+
}
95+
96+
func newPseudoLoopbackForwarder(l *logrus.Logger, localPort int, unixSock string) (*pseudoLoopbackForwarder, error) {
97+
unixAddr, err := net.ResolveUnixAddr("unix", unixSock)
98+
if err != nil {
99+
return nil, err
100+
}
101+
102+
lnAddr, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("0.0.0.0:%d", localPort))
103+
if err != nil {
104+
return nil, err
105+
}
106+
ln, err := net.ListenTCP("tcp4", lnAddr)
107+
if err != nil {
108+
return nil, err
109+
}
110+
111+
plf := &pseudoLoopbackForwarder{
112+
l: l,
113+
ln: ln,
114+
unixAddr: unixAddr,
115+
}
116+
117+
return plf, nil
118+
}
119+
120+
func (plf *pseudoLoopbackForwarder) Serve() error {
121+
defer plf.ln.Close()
122+
for {
123+
ac, err := plf.ln.AcceptTCP()
124+
if err != nil {
125+
return err
126+
}
127+
remoteAddr := ac.RemoteAddr().String() // ip:port
128+
remoteAddrIP, _, err := net.SplitHostPort(remoteAddr)
129+
if err != nil {
130+
plf.l.WithError(err).Debugf("pseudoloopback forwarder: rejecting non-loopback remoteAddr %q (unparsable)", remoteAddr)
131+
ac.Close()
132+
continue
133+
}
134+
if remoteAddrIP != "127.0.0.1" {
135+
plf.l.WithError(err).Debugf("pseudoloopback forwarder: rejecting non-loopback remoteAddr %q", remoteAddr)
136+
ac.Close()
137+
continue
138+
}
139+
go func(ac *net.TCPConn) {
140+
if fErr := plf.forward(ac); fErr != nil {
141+
plf.l.Error(fErr)
142+
}
143+
}(ac)
144+
}
145+
}
146+
147+
func (plf *pseudoLoopbackForwarder) forward(ac *net.TCPConn) error {
148+
defer ac.Close()
149+
unixConn, err := net.DialUnix("unix", nil, plf.unixAddr)
150+
if err != nil {
151+
return err
152+
}
153+
defer unixConn.Close()
154+
bicopy.Bicopy(ac, unixConn, nil)
155+
return nil
156+
}
157+
158+
func (plf *pseudoLoopbackForwarder) Close() error {
159+
_ = plf.ln.Close()
160+
return plf.onClose()
161+
}

pkg/hostagent/port_others.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//go:build !darwin
2+
// +build !darwin
3+
4+
package hostagent
5+
6+
import (
7+
"context"
8+
9+
"github.com/lima-vm/sshocker/pkg/ssh"
10+
"github.com/sirupsen/logrus"
11+
)
12+
13+
func forwardTCP(ctx context.Context, l *logrus.Logger, sshConfig *ssh.SSHConfig, port int, local, remote string, cancel bool) error {
14+
return forwardSSH(ctx, sshConfig, port, local, remote, cancel)
15+
}

pkg/limayaml/default.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,15 +171,15 @@ networks:
171171
# hostPort: 8080 # overrides the default value 80
172172
# - guestIP: "127.0.0.2" # overrides the default value "127.0.0.1"
173173
# hostIP: "127.0.0.2" # overrides the default value "127.0.0.1"
174-
# # default: guestPortRange: [1024, 65535]
175-
# # default: hostPortRange: [1024, 65535]
174+
# # default: guestPortRange: [1, 65535]
175+
# # default: hostPortRange: [1, 65535]
176176
# - guestPort: 8888
177177
# ignore: true (don't forward this port)
178178
# # Lima internally appends this fallback rule at the end:
179179
# - guestIP: "127.0.0.1"
180-
# guestPortRange: [1024, 65535]
180+
# guestPortRange: [1, 65535]
181181
# hostIP: "127.0.0.1"
182-
# hostPortRange: [1024, 65535]
182+
# hostPortRange: [1, 65535]
183183
# # Any port still not matched by a rule will not be forwarded (ignored)
184184

185185
# Extra environment variables that will be loaded into the VM at start up.

pkg/limayaml/defaults.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ func FillPortForwardDefaults(rule *PortForward) {
140140
}
141141
if rule.GuestPortRange[0] == 0 && rule.GuestPortRange[1] == 0 {
142142
if rule.GuestPort == 0 {
143-
rule.GuestPortRange[0] = 1024
143+
rule.GuestPortRange[0] = 1
144144
rule.GuestPortRange[1] = 65535
145145
} else {
146146
rule.GuestPortRange[0] = rule.GuestPort

0 commit comments

Comments
 (0)