Skip to content

Commit d010add

Browse files
committed
feat(ipv6): Add support to IPv6 port-forwarding
Signed-off-by: Victor Gama <hey@vito.io>
1 parent e57ac78 commit d010add

File tree

8 files changed

+154
-35
lines changed

8 files changed

+154
-35
lines changed

hack/test-port-forwarding.pl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,8 @@ sub JoinHostPort {
241241
# forward: 127.0.0.2 3020 → 127.0.0.1 2020
242242
# forward: 127.0.0.1 3021 → 127.0.0.1 2021
243243
# forward: 0.0.0.0 3022 → 127.0.0.1 2022
244-
# forward: :: 3023 → 127.0.0.1 2023
245-
# forward: ::1 3024 → 127.0.0.1 2024
244+
# forward: :: 3023 → ::1 2023
245+
# forward: ::1 3024 → ::1 2024
246246
247247
- guestPortRange: [3030, 3039]
248248
hostPortRange: [2030, 2039]
@@ -309,7 +309,7 @@ sub JoinHostPort {
309309
ignore: true
310310
311311
# forward: 0.0.0.0 4040 → 127.0.0.1 4040
312-
# forward: :: 4041 → 127.0.0.1 4041
312+
# forward: :: 4041 → ::1 4041
313313
# ignore: 127.0.0.1 4043 → 127.0.0.1 4043
314314
# ignore: 192.168.5.15 4044 → 127.0.0.1 4044
315315

pkg/hostagent/hostagent.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ func New(instName string, stdout io.Writer, sigintCh chan os.Signal, opts ...Opt
120120
AdditionalArgs: sshutil.SSHArgsFromOpts(sshOpts),
121121
}
122122

123-
rules := make([]limayaml.PortForward, 0, 3+len(y.PortForwards))
123+
rules := make([]limayaml.PortForward, 0, 4+len(y.PortForwards))
124124
// Block ports 22 and sshLocalPort on all IPs
125125
for _, port := range []int{sshGuestPort, sshLocalPort} {
126126
rule := limayaml.PortForward{GuestIP: net.IPv4zero, GuestPort: port, Ignore: true}
@@ -129,9 +129,19 @@ func New(instName string, stdout io.Writer, sigintCh chan os.Signal, opts ...Opt
129129
}
130130
rules = append(rules, y.PortForwards...)
131131
// Default forwards for all non-privileged ports from "127.0.0.1" and "::1"
132-
rule := limayaml.PortForward{GuestIP: guestagentapi.IPv4loopback1}
133-
limayaml.FillPortForwardDefaults(&rule, inst.Dir)
134-
rules = append(rules, rule)
132+
{
133+
rule := limayaml.PortForward{GuestIP: guestagentapi.IPv4loopback1}
134+
limayaml.FillPortForwardDefaults(&rule, inst.Dir)
135+
rules = append(rules, rule)
136+
}
137+
{
138+
rule := limayaml.PortForward{
139+
HostIP: net.IPv6loopback,
140+
GuestIP: net.IPv6loopback,
141+
}
142+
limayaml.FillPortForwardDefaults(&rule, inst.Dir)
143+
rules = append(rules, rule)
144+
}
135145

136146
limaDriver := driverutil.CreateTargetDriverInstance(&driver.BaseDriver{
137147
Instance: inst,

pkg/hostagent/port.go

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,62 @@ func hostAddress(rule limayaml.PortForward, guest api.IPPort) string {
4040
return host.String()
4141
}
4242

43-
func (pf *portForwarder) forwardingAddresses(guest api.IPPort) (string, string) {
43+
func (pf *portForwarder) forwardingAddresses(guest api.IPPort) (hostAddr string, guestAddr string) {
44+
// Some rules will require a small patch to the HostIP in order to bind to the
45+
// correct IP family.
46+
mustAdjustHostIP := false
47+
48+
// This holds an optional rule that was rejected, but is now stored here to preserve backward
49+
// compatibility, and will be used at the bottom of this function if set. See the case
50+
// rule.GuestIPMustBeZero && guest.IP.IsUnspecified() for further info.
51+
var unspecifiedRuleFallback *limayaml.PortForward
52+
4453
for _, rule := range pf.rules {
4554
if rule.GuestSocket != "" {
55+
// Not TCP
4656
continue
4757
}
58+
59+
// Check if `guest.Port` is within `rule.GuestPortRange`
4860
if guest.Port < rule.GuestPortRange[0] || guest.Port > rule.GuestPortRange[1] {
4961
continue
5062
}
63+
5164
switch {
52-
case guest.IP.IsUnspecified():
65+
// Early-continue in case rule's IP is not zero while it is required.
66+
case rule.GuestIPMustBeZero && !guest.IP.IsUnspecified():
67+
continue
68+
69+
// Rule lacks a preferred GuestIP, so guest may be binding to wherever it wants. The rule matches the port range,
70+
// so we can continue processing it. However, make sure to correct the rule to use a correct address family if
71+
// not specified by the rule.
72+
case rule.GuestIPWasUndefined && !rule.GuestIPMustBeZero:
73+
mustAdjustHostIP = rule.HostIPWasUndefined
74+
75+
// if GuestIP and family matches, move along.
76+
case rule.GuestIPMustBeZero && guest.IP.IsUnspecified():
77+
// This is a breaking change. Here we will keep a backup of the rule, so we can still reuse it
78+
// in case everything fails. The idea here is to move a copy of the current rule to outside this
79+
// loop, so we can reuse it in case nothing else matches.
80+
if !rule.GuestIPWasUndefined && !guest.IP.Equal(rule.GuestIP) {
81+
if unspecifiedRuleFallback == nil {
82+
// Move the rule to obtain a copy
83+
func(p limayaml.PortForward) { unspecifiedRuleFallback = &p }(rule)
84+
}
85+
continue
86+
}
87+
88+
mustAdjustHostIP = rule.HostIPWasUndefined
89+
90+
// Rule lack's HostIP, and guest is binding to '0.0.0.0' or '::'. Bind to the same address family.
91+
case rule.HostIPWasUndefined && guest.IP.IsUnspecified():
92+
mustAdjustHostIP = true
93+
94+
// We don't have a preferred HostIP in the rule, and guest wants to bind to a loopback
95+
// address. In that case, use the same address family.
96+
case rule.HostIPWasUndefined && (guest.IP.Equal(net.IPv6loopback) || guest.IP.Equal(api.IPv4loopback1)):
97+
mustAdjustHostIP = true
98+
5399
case guest.IP.Equal(rule.GuestIP):
54100
case guest.IP.Equal(net.IPv6loopback) && rule.GuestIP.Equal(api.IPv4loopback1):
55101
case rule.GuestIP.IsUnspecified() && !rule.GuestIPMustBeZero:
@@ -58,14 +104,32 @@ func (pf *portForwarder) forwardingAddresses(guest api.IPPort) (string, string)
58104
default:
59105
continue
60106
}
107+
61108
if rule.Ignore {
62109
if guest.IP.IsUnspecified() && !rule.GuestIP.IsUnspecified() {
63110
continue
64111
}
112+
65113
break
66114
}
115+
116+
if mustAdjustHostIP {
117+
if guest.IP.To4() != nil {
118+
rule.HostIP = api.IPv4loopback1
119+
} else {
120+
rule.HostIP = net.IPv6loopback
121+
}
122+
}
123+
67124
return hostAddress(rule, guest), guest.String()
68125
}
126+
127+
// At this point, no other rule matched. So check if this is being impacted by our
128+
// breaking change, and return the fallback rule. Otherwise, just ignore it.
129+
if unspecifiedRuleFallback != nil {
130+
return hostAddress(*unspecifiedRuleFallback, guest), guest.String()
131+
}
132+
69133
return "", guest.String()
70134
}
71135

pkg/hostagent/port_darwin.go

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local,
3030
return err
3131
}
3232

33-
if !localIP.Equal(api.IPv4loopback1) || localPort >= 1024 {
33+
if (!localIP.Equal(api.IPv4loopback1) && !localIP.Equal(net.IPv6loopback)) || localPort >= 1024 {
3434
return forwardSSH(ctx, sshConfig, port, local, remote, verb, false)
3535
}
3636

@@ -86,9 +86,10 @@ func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local,
8686
var pseudoLoopbackForwarders = make(map[string]*pseudoLoopbackForwarder)
8787

8888
type pseudoLoopbackForwarder struct {
89-
ln *net.TCPListener
90-
unixAddr *net.UnixAddr
91-
onClose func() error
89+
lns []*net.TCPListener
90+
unixAddr *net.UnixAddr
91+
onClose func() error
92+
incomingConns chan *net.TCPConn
9293
}
9394

9495
func newPseudoLoopbackForwarder(localPort int, unixSock string) (*pseudoLoopbackForwarder, error) {
@@ -97,38 +98,64 @@ func newPseudoLoopbackForwarder(localPort int, unixSock string) (*pseudoLoopback
9798
return nil, err
9899
}
99100

100-
lnAddr, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("0.0.0.0:%d", localPort))
101-
if err != nil {
102-
return nil, err
101+
toResolve := [][]string{
102+
{"tcp4", fmt.Sprintf("0.0.0.0:%d", localPort)},
103+
{"tcp6", fmt.Sprintf("[::]:%d", localPort)},
103104
}
104-
ln, err := net.ListenTCP("tcp4", lnAddr)
105-
if err != nil {
106-
return nil, err
105+
106+
var lns []*net.TCPListener
107+
for _, addr := range toResolve {
108+
network, address := addr[0], addr[1]
109+
lnAddr, err := net.ResolveTCPAddr(network, address)
110+
if err != nil {
111+
return nil, err
112+
}
113+
ln, err := net.ListenTCP(network, lnAddr)
114+
if err != nil {
115+
return nil, err
116+
}
117+
lns = append(lns, ln)
107118
}
108119

109120
plf := &pseudoLoopbackForwarder{
110-
ln: ln,
111-
unixAddr: unixAddr,
121+
lns: lns,
122+
incomingConns: make(chan *net.TCPConn, 10),
123+
unixAddr: unixAddr,
112124
}
113125

114126
return plf, nil
115127
}
116128

117-
func (plf *pseudoLoopbackForwarder) Serve() error {
118-
defer plf.ln.Close()
129+
func (plf *pseudoLoopbackForwarder) acceptLn(ln *net.TCPListener) {
130+
defer ln.Close()
119131
for {
120-
ac, err := plf.ln.AcceptTCP()
132+
ac, err := ln.AcceptTCP()
121133
if err != nil {
122-
return err
134+
logrus.WithError(err).Errorf("Stopping listening %#v", ln)
135+
return
123136
}
137+
plf.incomingConns <- ac
138+
}
139+
}
140+
141+
func (plf *pseudoLoopbackForwarder) accept() {
142+
for _, ln := range plf.lns {
143+
go plf.acceptLn(ln)
144+
}
145+
}
146+
147+
func (plf *pseudoLoopbackForwarder) Serve() error {
148+
plf.accept()
149+
150+
for ac := range plf.incomingConns {
124151
remoteAddr := ac.RemoteAddr().String() // ip:port
125152
remoteAddrIP, _, err := net.SplitHostPort(remoteAddr)
126153
if err != nil {
127154
logrus.WithError(err).Debugf("pseudoloopback forwarder: rejecting non-loopback remoteAddr %q (unparsable)", remoteAddr)
128155
ac.Close()
129156
continue
130157
}
131-
if remoteAddrIP != "127.0.0.1" {
158+
if remoteAddrIP != "127.0.0.1" && remoteAddrIP != "::" {
132159
logrus.WithError(err).Debugf("pseudoloopback forwarder: rejecting non-loopback remoteAddr %q", remoteAddr)
133160
ac.Close()
134161
continue
@@ -139,6 +166,8 @@ func (plf *pseudoLoopbackForwarder) Serve() error {
139166
}
140167
}(ac)
141168
}
169+
170+
return nil
142171
}
143172

144173
func (plf *pseudoLoopbackForwarder) forward(ac *net.TCPConn) error {
@@ -153,6 +182,8 @@ func (plf *pseudoLoopbackForwarder) forward(ac *net.TCPConn) error {
153182
}
154183

155184
func (plf *pseudoLoopbackForwarder) Close() error {
156-
_ = plf.ln.Close()
185+
for _, ln := range plf.lns {
186+
_ = ln.Close()
187+
}
157188
return plf.onClose()
158189
}

pkg/limayaml/defaults.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,10 +646,14 @@ func FillPortForwardDefaults(rule *PortForward, instDir string) {
646646
} else {
647647
rule.GuestIP = api.IPv4loopback1
648648
}
649+
rule.GuestIPWasUndefined = true
649650
}
651+
650652
if rule.HostIP == nil {
651653
rule.HostIP = api.IPv4loopback1
654+
rule.HostIPWasUndefined = true
652655
}
656+
653657
if rule.GuestPortRange[0] == 0 && rule.GuestPortRange[1] == 0 {
654658
if rule.GuestPort == 0 {
655659
rule.GuestPortRange[0] = 1

pkg/limayaml/defaults_test.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,14 @@ func TestFillDefault(t *testing.T) {
110110
}
111111

112112
defaultPortForward := PortForward{
113-
GuestIP: api.IPv4loopback1,
114-
GuestPortRange: [2]int{1, 65535},
115-
HostIP: api.IPv4loopback1,
116-
HostPortRange: [2]int{1, 65535},
117-
Proto: TCP,
118-
Reverse: false,
113+
GuestIP: api.IPv4loopback1,
114+
GuestPortRange: [2]int{1, 65535},
115+
HostIP: api.IPv4loopback1,
116+
HostPortRange: [2]int{1, 65535},
117+
Proto: TCP,
118+
Reverse: false,
119+
HostIPWasUndefined: true,
120+
GuestIPWasUndefined: true,
119121
}
120122

121123
// ------------------------------------------------------------------------------------

pkg/limayaml/limayaml.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,12 @@ type PortForward struct {
189189
Proto Proto `yaml:"proto,omitempty" json:"proto,omitempty"`
190190
Reverse bool `yaml:"reverse,omitempty" json:"reverse,omitempty"`
191191
Ignore bool `yaml:"ignore,omitempty" json:"ignore,omitempty"`
192+
193+
// Set in case the HostIP field was automatically filled by FillPortForwardDefaults
194+
HostIPWasUndefined bool `yaml:"-" json:"-"`
195+
196+
// Set in case the GuestIP field was automatically filled by FillPortForwardDefaults
197+
GuestIPWasUndefined bool `yaml:"-" json:"-"`
192198
}
193199

194200
type CopyToHost struct {

pkg/limayaml/validate.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,10 @@ func Validate(y LimaYAML, warn bool) error {
172172
}
173173
for i, rule := range y.PortForwards {
174174
field := fmt.Sprintf("portForwards[%d]", i)
175-
if rule.GuestIPMustBeZero && !rule.GuestIP.Equal(net.IPv4zero) {
176-
return fmt.Errorf("field `%s.guestIPMustBeZero` can only be true when field `%s.guestIP` is 0.0.0.0", field, field)
175+
if rule.GuestIPMustBeZero && !rule.GuestIP.Equal(net.IPv4zero) && !rule.GuestIP.Equal(net.IPv6zero) {
176+
// Using IPv6 first so go vet doesn't complain about the error
177+
// message ending with a colon.
178+
return fmt.Errorf("field `%s.guestIPMustBeZero` can only be true when field `%s.guestIP` is either :: or 0.0.0.0", field, field)
177179
}
178180
if rule.GuestPort != 0 {
179181
if rule.GuestSocket != "" {

0 commit comments

Comments
 (0)