Skip to content

Commit 7ac20c7

Browse files
committed
Allow forwarding of specified port ranges to all interfaces
This allows to expose e.g. an ingress port outside of the local host while still forwarding other ports only to localhost. Forwarding to all interfaces is curiously not limited to unprivileged ports. Signed-off-by: Jan Dubois <[email protected]>
1 parent 682b377 commit 7ac20c7

File tree

6 files changed

+148
-15
lines changed

6 files changed

+148
-15
lines changed

pkg/hostagent/hostagent.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func New(instName string, stdout, stderr io.Writer, sigintCh chan os.Signal) (*H
9191
y: y,
9292
instDir: inst.Dir,
9393
sshConfig: sshConfig,
94-
portForwarder: newPortForwarder(l, sshConfig, y.SSH.LocalPort),
94+
portForwarder: newPortForwarder(l, sshConfig, y.SSH.LocalPort, y.Ports),
9595
qExe: qExe,
9696
qArgs: qArgs,
9797
sigintCh: sigintCh,

pkg/hostagent/port.go

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package hostagent
22

33
import (
44
"context"
5+
"fmt"
56
"strconv"
67

78
"github.com/AkihiroSuda/lima/pkg/guestagent/api"
9+
"github.com/AkihiroSuda/lima/pkg/limayaml"
810
"github.com/AkihiroSuda/sshocker/pkg/ssh"
911
"github.com/sirupsen/logrus"
1012
)
@@ -14,19 +16,34 @@ type portForwarder struct {
1416
sshConfig *ssh.SSHConfig
1517
sshHostPort int
1618
tcp map[int]struct{} // key: int (NOTE: this might be inconsistent with the actual status of SSH master)
19+
ports []limayaml.Port
1720
}
1821

1922
const sshGuestPort = 22
2023

21-
func newPortForwarder(l *logrus.Logger, sshConfig *ssh.SSHConfig, sshHostPort int) *portForwarder {
24+
func newPortForwarder(l *logrus.Logger, sshConfig *ssh.SSHConfig, sshHostPort int, ports []limayaml.Port) *portForwarder {
2225
return &portForwarder{
2326
l: l,
2427
sshConfig: sshConfig,
2528
sshHostPort: sshHostPort,
2629
tcp: make(map[int]struct{}),
30+
ports: ports,
2731
}
2832
}
2933

34+
func (pf *portForwarder) forwardingAddresses(guestPort int) (string, string) {
35+
for _, port := range pf.ports {
36+
if port.GuestPortRange[0] <= guestPort && guestPort <= port.GuestPortRange[1] {
37+
guestAddr := fmt.Sprintf("%s:%d", port.GuestIP, guestPort)
38+
offset := port.HostPortRange[0] - port.GuestPortRange[0]
39+
hostAddr := fmt.Sprintf("%s:%d", port.HostIP, guestPort + offset)
40+
return guestAddr, hostAddr
41+
}
42+
}
43+
addr := "127.0.0.1:" + strconv.Itoa(guestPort)
44+
return addr, addr
45+
}
46+
3047
func (pf *portForwarder) OnEvent(ctx context.Context, ev api.Event) {
3148
ignore := func(x api.IPPort) bool {
3249
switch x.Port {
@@ -42,9 +59,10 @@ func (pf *portForwarder) OnEvent(ctx context.Context, ev api.Event) {
4259
}
4360
// pf.tcp might be inconsistent with the actual state of the SSH master,
4461
// so we always attempt to cancel forwarding, even when f.Port is not tracked in pf.tcp.
45-
pf.l.Infof("Stopping forwarding TCP port %d", f.Port)
62+
guestAddr, hostAddr := pf.forwardingAddresses(f.Port)
63+
pf.l.Infof("Stopping forwarding TCP from %s to %s", guestAddr, hostAddr)
4664
verbCancel := true
47-
if err := forwardSSH(ctx, pf.sshConfig, pf.sshHostPort, "127.0.0.1:"+strconv.Itoa(f.Port), "127.0.0.1:"+strconv.Itoa(f.Port), verbCancel); err != nil {
65+
if err := forwardSSH(ctx, pf.sshConfig, pf.sshHostPort, hostAddr, guestAddr, verbCancel); err != nil {
4866
if _, ok := pf.tcp[f.Port]; ok {
4967
pf.l.WithError(err).Warnf("failed to stop forwarding TCP port %d", f.Port)
5068
} else {
@@ -57,8 +75,9 @@ func (pf *portForwarder) OnEvent(ctx context.Context, ev api.Event) {
5775
if ignore(f) {
5876
continue
5977
}
60-
pf.l.Infof("Forwarding TCP port %d", f.Port)
61-
if err := forwardSSH(ctx, pf.sshConfig, pf.sshHostPort, "127.0.0.1:"+strconv.Itoa(f.Port), "127.0.0.1:"+strconv.Itoa(f.Port), false); err != nil {
78+
guestAddr, hostAddr := pf.forwardingAddresses(f.Port)
79+
pf.l.Infof("Forwarding TCP from %s to %s", guestAddr, hostAddr)
80+
if err := forwardSSH(ctx, pf.sshConfig, pf.sshHostPort, hostAddr, guestAddr, false); err != nil {
6281
pf.l.WithError(err).Warnf("failed to setting up forward TCP port %d (negligible if already forwarded)", f.Port)
6382
} else {
6483
pf.tcp[f.Port] = struct{}{}

pkg/limayaml/default.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,24 @@ containerd:
7373
# Default: true
7474
user: true
7575

76+
# port forwarding rules.
77+
# By default guest ports are forwarded to the same port on the 127.0.0.1 interface on the host.
78+
# ports:
79+
80+
# - guestPort: 443
81+
# hostIP: "0.0.0.0" # overrides the default value "127.0.0.1"
82+
# # default: hostPort: 443
83+
# # default: guestIP: "127.0.0.1" (only valid value right now)
84+
# # default: proto: "tcp" (only valid value right now)
85+
86+
# - guestPortRange: [4000, 4999]
87+
# hostIP: "0.0.0.0" # overrides the default value "127.0.0.1"
88+
# # default: hostPortRange: [4000, 4999] (must specify same number of ports as guestPortRange)
89+
90+
# - guestPort: 80
91+
# hostPort: 8080 # overrides the default value 80
92+
93+
7694
# Provisioning scripts need to be idempotent because they might be called
7795
# multiple times, e.g. when the host VM is being restarted.
7896
# provision:

pkg/limayaml/defaults.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,29 @@ func FillDefault(y *LimaYAML) {
4949
probe.Description = fmt.Sprintf("user probe %d/%d", i+1, len(y.Probes))
5050
}
5151
}
52+
for i := range y.Ports {
53+
port := &y.Ports[i]
54+
if port.GuestIP == "" {
55+
port.GuestIP = "127.0.0.1"
56+
}
57+
if port.GuestPortRange[0] == 0 && port.GuestPortRange[1] == 0 {
58+
port.GuestPortRange[0] = port.GuestPort
59+
port.GuestPortRange[1] = port.GuestPort
60+
}
61+
if port.HostIP == "" {
62+
port.HostIP = "127.0.0.1"
63+
}
64+
if port.HostPortRange[0] == 0 && port.HostPortRange[1] == 0 {
65+
if port.HostPort == 0 {
66+
port.HostPort = port.GuestPortRange[0]
67+
}
68+
port.HostPortRange[0] = port.HostPort
69+
port.HostPortRange[1] = port.HostPort
70+
}
71+
if port.Proto == "" {
72+
port.Proto = TCP
73+
}
74+
}
5275
}
5376

5477
func resolveArch(s string) Arch {

pkg/limayaml/limayaml.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type LimaYAML struct {
1515
Provision []Provision `yaml:"provision,omitempty"`
1616
Containerd Containerd `yaml:"containerd,omitempty"`
1717
Probes []Probe `yaml:"probes,omitempty"`
18+
Ports []Port `yaml:"ports,omitempty"`
1819
}
1920

2021
type Arch = string
@@ -83,3 +84,19 @@ type Probe struct {
8384
Script string
8485
Hint string
8586
}
87+
88+
type Proto = string
89+
90+
const (
91+
TCP Proto = "tcp"
92+
)
93+
94+
type Port struct {
95+
GuestIP string `yaml:"guestIP,omitempty"`
96+
GuestPort int `yaml:"guestPort,omitempty"`
97+
GuestPortRange [2]int `yaml:"guestPortRange,omitempty"`
98+
HostIP string `yaml:"hostIP,omitempty"`
99+
HostPort int `yaml:"hostPort,omitempty"`
100+
HostPortRange [2]int `yaml:"hostPortRange,omitempty"`
101+
Proto Proto `yaml:"proto,omitempty"`
102+
}

pkg/limayaml/validate.go

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,8 @@ func ValidateRaw(y LimaYAML) error {
9898
}
9999
}
100100

101-
switch {
102-
case y.SSH.LocalPort < 0:
103-
return errors.New("field `ssh.localPort` must be > 0")
104-
case y.SSH.LocalPort == 0:
105-
return errors.New("field `ssh.localPort` must be set, e.g, 60022 (FIXME: support automatic port assignment)")
106-
case y.SSH.LocalPort == 22:
107-
return errors.New("field `ssh.localPort` must not be 22")
108-
case y.SSH.LocalPort > 65535:
109-
return errors.New("field `ssh.localPort` must be < 65535")
101+
if err := validatePort("ssh.localPort", y.SSH.LocalPort); err != nil {
102+
return err
110103
}
111104

112105
// y.Firmware.LegacyBIOS is ignored for aarch64, but not a fatal error.
@@ -127,5 +120,68 @@ func ValidateRaw(y LimaYAML) error {
127120
i, ProbeModeReadiness)
128121
}
129122
}
123+
for i, port := range y.Ports {
124+
field := fmt.Sprintf("ports[%d]", i)
125+
if port.GuestIP != "127.0.0.1" {
126+
return errors.Errorf("field `%s.guestIP` must be \"127.0.0.1\"", field)
127+
}
128+
if port.HostIP != "127.0.0.1" && port.HostIP != "0.0.0.0" {
129+
return errors.Errorf("field `%s.hostIP` must be either \"127.0.0.1\" or \"0.0.0.0\"", field)
130+
}
131+
if port.GuestPort != 0 {
132+
if port.GuestPort != port.GuestPortRange[0] {
133+
return errors.Errorf("field `%s.guestPort` must match field `%s.guestPortRange[0]`", field, field)
134+
}
135+
// redundant validation to make sure the error contains the correct field name
136+
if err := validatePort(field+".guestPort", port.GuestPort); err != nil {
137+
return err
138+
}
139+
}
140+
if port.HostPort != 0 {
141+
if port.HostPort != port.HostPortRange[0] {
142+
return errors.Errorf("field `%s.hostPort` must match field `%s.hostPortRange[0]`", field, field)
143+
}
144+
// redundant validation to make sure the error contains the correct field name
145+
if err := validatePort(field+".hostPort", port.HostPort); err != nil {
146+
return err
147+
}
148+
}
149+
for j := 0; j < 2; j++ {
150+
if err := validatePort(fmt.Sprintf("%s.guestPortRange[%d]", field, j), port.GuestPortRange[j]); err != nil {
151+
return err
152+
}
153+
if err := validatePort(fmt.Sprintf("%s.hostPortRange[%d]", field, j), port.HostPortRange[j]); err != nil {
154+
return err
155+
}
156+
}
157+
if port.GuestPortRange[0] > port.GuestPortRange[1] {
158+
return errors.Errorf("field `%s.guestPortRange[1]` must be greater than or equal to field `%s.guestPortRange[0]`", field, field)
159+
}
160+
if port.HostPortRange[0] > port.HostPortRange[1] {
161+
return errors.Errorf("field `%s.hostPortRange[1]` must be greater than or equal to field `%s.hostPortRange[0]`", field, field)
162+
}
163+
if port.GuestPortRange[1] - port.GuestPortRange[0] != port.HostPortRange[1] - port.HostPortRange[0] {
164+
return errors.Errorf("field `%s.hostPortRange` must specify the same number of ports as field `%s.guestPortRange`", field, field)
165+
}
166+
if port.Proto != TCP {
167+
return errors.Errorf("field `%s.proto` must be %q", field, TCP)
168+
}
169+
// Not validating that the various GuestPortRanges and HostPortRanges are not overlapping. Rules will be
170+
// processed sequentially and the first matching rule for a guest port determines forwarding behavior.
171+
}
172+
return nil
173+
}
174+
175+
func validatePort(field string, port int) error {
176+
switch {
177+
case port < 0:
178+
return errors.Errorf("field `%s` must be > 0", field)
179+
case port == 0:
180+
return errors.Errorf("field `%s` must be set", field)
181+
case port == 22:
182+
return errors.Errorf("field `%s` must not be 22", field)
183+
case port > 65535:
184+
return errors.Errorf("field `%s` must be < 65536", field)
185+
}
130186
return nil
131187
}

0 commit comments

Comments
 (0)