Skip to content

Commit

Permalink
Allow forwarding of specified port ranges to all interfaces
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
jandubois committed Jul 9, 2021
1 parent 1de95ce commit 7fad0c8
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 15 deletions.
2 changes: 1 addition & 1 deletion pkg/hostagent/hostagent.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func New(instName string, stdout, stderr io.Writer, sigintCh chan os.Signal) (*H
y: y,
instDir: inst.Dir,
sshConfig: sshConfig,
portForwarder: newPortForwarder(l, sshConfig, y.SSH.LocalPort),
portForwarder: newPortForwarder(l, sshConfig, y.SSH.LocalPort, y.Ports),
qExe: qExe,
qArgs: qArgs,
sigintCh: sigintCh,
Expand Down
29 changes: 24 additions & 5 deletions pkg/hostagent/port.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package hostagent

import (
"context"
"fmt"
"strconv"

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

const sshGuestPort = 22

func newPortForwarder(l *logrus.Logger, sshConfig *ssh.SSHConfig, sshHostPort int) *portForwarder {
func newPortForwarder(l *logrus.Logger, sshConfig *ssh.SSHConfig, sshHostPort int, ports []limayaml.Port) *portForwarder {
return &portForwarder{
l: l,
sshConfig: sshConfig,
sshHostPort: sshHostPort,
tcp: make(map[int]struct{}),
ports: ports,
}
}

func (pf *portForwarder) forwardingAddresses(guestPort int) (string, string) {
for _, port := range pf.ports {
if port.GuestPortRange[0] <= guestPort && guestPort <= port.GuestPortRange[1] {
guestAddr := fmt.Sprintf("%s:%d", port.GuestIP, guestPort)
offset := port.HostPortRange[0] - port.GuestPortRange[0]
hostAddr := fmt.Sprintf("%s:%d", port.HostIP, guestPort + offset)
return guestAddr, hostAddr
}
}
addr := "127.0.0.1:" + strconv.Itoa(guestPort)
return addr, addr
}

func (pf *portForwarder) OnEvent(ctx context.Context, ev api.Event) {
ignore := func(x api.IPPort) bool {
switch x.Port {
Expand All @@ -42,9 +59,10 @@ func (pf *portForwarder) OnEvent(ctx context.Context, ev api.Event) {
}
// pf.tcp might be inconsistent with the actual state of the SSH master,
// so we always attempt to cancel forwarding, even when f.Port is not tracked in pf.tcp.
pf.l.Infof("Stopping forwarding TCP port %d", f.Port)
guestAddr, hostAddr := pf.forwardingAddresses(f.Port)
pf.l.Infof("Stopping forwarding TCP from %s to %s", guestAddr, hostAddr)
verbCancel := true
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 {
if err := forwardSSH(ctx, pf.sshConfig, pf.sshHostPort, hostAddr, guestAddr, 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 @@ -57,8 +75,9 @@ func (pf *portForwarder) OnEvent(ctx context.Context, ev api.Event) {
if ignore(f) {
continue
}
pf.l.Infof("Forwarding TCP port %d", f.Port)
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 {
guestAddr, hostAddr := pf.forwardingAddresses(f.Port)
pf.l.Infof("Forwarding TCP from %s to %s", guestAddr, hostAddr)
if err := forwardSSH(ctx, pf.sshConfig, pf.sshHostPort, hostAddr, guestAddr, false); err != nil {
pf.l.WithError(err).Warnf("failed to setting up forward TCP port %d (negligible if already forwarded)", f.Port)
} else {
pf.tcp[f.Port] = struct{}{}
Expand Down
18 changes: 18 additions & 0 deletions pkg/limayaml/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,24 @@ containerd:
# Default: true
user: true

# port forwarding rules.
# By default guest ports are forwarded to the same port on the 127.0.0.1 interface on the host.
# ports:

# - guestPort: 443
# hostIP: "0.0.0.0" # overrides the default value "127.0.0.1"
# # default: hostPort: 443
# # default: guestIP: "127.0.0.1" (only valid value right now)
# # default: proto: "tcp" (only valid value right now)

# - guestPortRange: [4000, 4999]
# hostIP: "0.0.0.0" # overrides the default value "127.0.0.1"
# # default: hostPortRange: [4000, 4999] (must specify same number of ports as guestPortRange)

# - guestPort: 80
# hostPort: 8080 # overrides the default value 80


# Provisioning scripts need to be idempotent because they might be called
# multiple times, e.g. when the host VM is being restarted.
# provision:
Expand Down
23 changes: 23 additions & 0 deletions pkg/limayaml/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,29 @@ func FillDefault(y *LimaYAML) {
probe.Description = fmt.Sprintf("user probe %d/%d", i+1, len(y.Probes))
}
}
for i := range y.Ports {
port := &y.Ports[i]
if port.GuestIP == "" {
port.GuestIP = "127.0.0.1"
}
if port.GuestPortRange[0] == 0 && port.GuestPortRange[1] == 0 {
port.GuestPortRange[0] = port.GuestPort
port.GuestPortRange[1] = port.GuestPort
}
if port.HostIP == "" {
port.HostIP = "127.0.0.1"
}
if port.HostPortRange[0] == 0 && port.HostPortRange[1] == 0 {
if port.HostPort == 0 {
port.HostPort = port.GuestPortRange[0]
}
port.HostPortRange[0] = port.HostPort
port.HostPortRange[1] = port.HostPort
}
if port.Proto == "" {
port.Proto = TCP
}
}
}

func resolveArch(s string) Arch {
Expand Down
17 changes: 17 additions & 0 deletions pkg/limayaml/limayaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type LimaYAML struct {
Provision []Provision `yaml:"provision,omitempty"`
Containerd Containerd `yaml:"containerd,omitempty"`
Probes []Probe `yaml:"probes,omitempty"`
Ports []Port `yaml:"ports,omitempty"`
}

type Arch = string
Expand Down Expand Up @@ -83,3 +84,19 @@ type Probe struct {
Script string
Hint string
}

type Proto = string

const (
TCP Proto = "tcp"
)

type Port struct {
GuestIP string `yaml:"guestIP,omitempty"`
GuestPort int `yaml:"guestPort,omitempty"`
GuestPortRange [2]int `yaml:"guestPortRange,omitempty"`
HostIP string `yaml:"hostIP,omitempty"`
HostPort int `yaml:"hostPort,omitempty"`
HostPortRange [2]int `yaml:"hostPortRange,omitempty"`
Proto Proto `yaml:"proto,omitempty"`
}
74 changes: 65 additions & 9 deletions pkg/limayaml/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,8 @@ func ValidateRaw(y LimaYAML) error {
}
}

switch {
case y.SSH.LocalPort < 0:
return errors.New("field `ssh.localPort` must be > 0")
case y.SSH.LocalPort == 0:
return errors.New("field `ssh.localPort` must be set, e.g, 60022 (FIXME: support automatic port assignment)")
case y.SSH.LocalPort == 22:
return errors.New("field `ssh.localPort` must not be 22")
case y.SSH.LocalPort > 65535:
return errors.New("field `ssh.localPort` must be < 65535")
if err := validatePort("ssh.localPort", y.SSH.LocalPort); err != nil {
return err
}

// y.Firmware.LegacyBIOS is ignored for aarch64, but not a fatal error.
Expand All @@ -127,5 +120,68 @@ func ValidateRaw(y LimaYAML) error {
i, ProbeModeReadiness)
}
}
for i, port := range y.Ports {
field := fmt.Sprintf("ports[%d]", i)
if port.GuestIP != "127.0.0.1" {
return errors.Errorf(`field '%s.guestIP' must be "127.0.0.1"`, field)
}
if port.HostIP != "127.0.0.1" && port.HostIP != "0.0.0.0" {
return errors.Errorf(`field '%s.hostIP' must be either "127.0.0.1" or "0.0.0.0"`, field)
}
if port.GuestPort != 0 {
if port.GuestPort != port.GuestPortRange[0] {
return errors.Errorf("field '%s.guestPort' must match field '%s.guestPortRange[0]", field, field)
}
// redundant validation to make sure the error contains the correct field name
if err := validatePort(field+".guestPort", port.GuestPort); err != nil {
return err
}
}
if port.HostPort != 0 {
if port.HostPort != port.HostPortRange[0] {
return errors.Errorf("field '%s.hostPort' must match field '%s.hostPortRange[0]", field, field)
}
// redundant validation to make sure the error contains the correct field name
if err := validatePort(field+".hostPort", port.HostPort); err != nil {
return err
}
}
for j := 0; j < 2; j++ {
if err := validatePort(fmt.Sprintf("%s.guestPortRange[%d]", field, j), port.GuestPortRange[j]); err != nil {
return err
}
if err := validatePort(fmt.Sprintf("%s.hostPortRange[%d]", field, j), port.HostPortRange[j]); err != nil {
return err
}
}
if port.GuestPortRange[0] > port.GuestPortRange[1] {
return errors.Errorf("field `%s.guestPortRange[1]` must be greater than or equal to field `%s.guestPortRange[0]`", field, field)
}
if port.HostPortRange[0] > port.HostPortRange[1] {
return errors.Errorf("field `%s.hostPortRange[1]` must be greater than or equal to field `%s.hostPortRange[0]`", field, field)
}
if port.GuestPortRange[1] - port.GuestPortRange[0] != port.HostPortRange[1] - port.HostPortRange[0] {
return errors.Errorf("field `%s.hostPortRange` must specify the same number of ports as field `%s.guestPortRange`", field, field)
}
if port.Proto != TCP {
return errors.Errorf("field `%s.proto` must be %q", TCP)
}
// Not validating that the various GuestPortRanges and HostPortRanges are not overlapping. Rules will be
// processed sequentially and the first matching rule for a guest port determines forwarding behavior.
}
return nil
}

func validatePort(field string, port int) error {
switch {
case port < 0:
return errors.Errorf("field `%s` must be > 0", field)
case port == 0:
return errors.Errorf("field `%s` must be set", field)
case port == 22:
return errors.Errorf("field `%s` must not be 22", field)
case port > 65535:
return errors.Errorf("field `%s` must be < 65535", field)
}
return nil
}

0 comments on commit 7fad0c8

Please sign in to comment.