Skip to content

Commit 648cdd0

Browse files
authored
fix: fix parsing of IPv6 addresses in coder port-forward (#15627)
fixes: #15561 Fixes parsing of IPv6 local addresses on `coder port-forward`
1 parent a8becfb commit 648cdd0

File tree

3 files changed

+107
-79
lines changed

3 files changed

+107
-79
lines changed

cli/portforward.go

Lines changed: 44 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/netip"
88
"os"
99
"os/signal"
10+
"regexp"
1011
"strconv"
1112
"strings"
1213
"sync"
@@ -263,7 +264,7 @@ func parsePortForwards(tcpSpecs, udpSpecs []string) ([]portForwardSpec, error) {
263264

264265
for _, specEntry := range tcpSpecs {
265266
for _, spec := range strings.Split(specEntry, ",") {
266-
ports, err := parseSrcDestPorts(spec)
267+
ports, err := parseSrcDestPorts(strings.TrimSpace(spec))
267268
if err != nil {
268269
return nil, xerrors.Errorf("failed to parse TCP port-forward specification %q: %w", spec, err)
269270
}
@@ -281,7 +282,7 @@ func parsePortForwards(tcpSpecs, udpSpecs []string) ([]portForwardSpec, error) {
281282

282283
for _, specEntry := range udpSpecs {
283284
for _, spec := range strings.Split(specEntry, ",") {
284-
ports, err := parseSrcDestPorts(spec)
285+
ports, err := parseSrcDestPorts(strings.TrimSpace(spec))
285286
if err != nil {
286287
return nil, xerrors.Errorf("failed to parse UDP port-forward specification %q: %w", spec, err)
287288
}
@@ -326,63 +327,53 @@ type parsedSrcDestPort struct {
326327
local, remote netip.AddrPort
327328
}
328329

330+
// specRegexp matches port specs. It handles all the following formats:
331+
//
332+
// 8000
333+
// 8888:9999
334+
// 1-5:6-10
335+
// 8000-8005
336+
// 127.0.0.1:4000:4000
337+
// [::1]:8080:8081
338+
// 127.0.0.1:4000-4005
339+
// [::1]:4000-4001:5000-5001
340+
//
341+
// Important capturing groups:
342+
//
343+
// 2: local IP address (including [] for IPv6)
344+
// 3: local port, or start of local port range
345+
// 5: end of local port range
346+
// 7: remote port, or start of remote port range
347+
// 9: end or remote port range
348+
var specRegexp = regexp.MustCompile(`^((\[[0-9a-fA-F:]+]|\d+\.\d+\.\d+\.\d+):)?(\d+)(-(\d+))?(:(\d+)(-(\d+))?)?$`)
349+
329350
func parseSrcDestPorts(in string) ([]parsedSrcDestPort, error) {
330351
var (
331352
err error
332-
parts = strings.Split(in, ":")
333353
localAddr = netip.AddrFrom4([4]byte{127, 0, 0, 1})
334354
remoteAddr = netip.AddrFrom4([4]byte{127, 0, 0, 1})
335355
)
336-
337-
switch len(parts) {
338-
case 1:
339-
// Duplicate the single part
340-
parts = append(parts, parts[0])
341-
case 2:
342-
// Check to see if the first part is an IP address.
343-
_localAddr, err := netip.ParseAddr(parts[0])
344-
if err != nil {
345-
break
346-
}
347-
// The first part is the local address, so duplicate the port.
348-
localAddr = _localAddr
349-
parts = []string{parts[1], parts[1]}
350-
351-
case 3:
352-
_localAddr, err := netip.ParseAddr(parts[0])
353-
if err != nil {
354-
return nil, xerrors.Errorf("invalid port specification %q; invalid ip %q: %w", in, parts[0], err)
355-
}
356-
localAddr = _localAddr
357-
parts = parts[1:]
358-
359-
default:
356+
groups := specRegexp.FindStringSubmatch(in)
357+
if len(groups) == 0 {
360358
return nil, xerrors.Errorf("invalid port specification %q", in)
361359
}
362-
363-
if !strings.Contains(parts[0], "-") {
364-
localPort, err := parsePort(parts[0])
360+
if groups[2] != "" {
361+
localAddr, err = netip.ParseAddr(strings.Trim(groups[2], "[]"))
365362
if err != nil {
366-
return nil, xerrors.Errorf("parse local port from %q: %w", in, err)
363+
return nil, xerrors.Errorf("invalid IP address %q", groups[2])
367364
}
368-
remotePort, err := parsePort(parts[1])
369-
if err != nil {
370-
return nil, xerrors.Errorf("parse remote port from %q: %w", in, err)
371-
}
372-
373-
return []parsedSrcDestPort{{
374-
local: netip.AddrPortFrom(localAddr, localPort),
375-
remote: netip.AddrPortFrom(remoteAddr, remotePort),
376-
}}, nil
377365
}
378366

379-
local, err := parsePortRange(parts[0])
367+
local, err := parsePortRange(groups[3], groups[5])
380368
if err != nil {
381369
return nil, xerrors.Errorf("parse local port range from %q: %w", in, err)
382370
}
383-
remote, err := parsePortRange(parts[1])
384-
if err != nil {
385-
return nil, xerrors.Errorf("parse remote port range from %q: %w", in, err)
371+
remote := local
372+
if groups[7] != "" {
373+
remote, err = parsePortRange(groups[7], groups[9])
374+
if err != nil {
375+
return nil, xerrors.Errorf("parse remote port range from %q: %w", in, err)
376+
}
386377
}
387378
if len(local) != len(remote) {
388379
return nil, xerrors.Errorf("port ranges must be the same length, got %d ports forwarded to %d ports", len(local), len(remote))
@@ -397,18 +388,17 @@ func parseSrcDestPorts(in string) ([]parsedSrcDestPort, error) {
397388
return out, nil
398389
}
399390

400-
func parsePortRange(in string) ([]uint16, error) {
401-
parts := strings.Split(in, "-")
402-
if len(parts) != 2 {
403-
return nil, xerrors.Errorf("invalid port range specification %q", in)
404-
}
405-
start, err := parsePort(parts[0])
391+
func parsePortRange(s, e string) ([]uint16, error) {
392+
start, err := parsePort(s)
406393
if err != nil {
407-
return nil, xerrors.Errorf("parse range start port from %q: %w", in, err)
394+
return nil, xerrors.Errorf("parse range start port from %q: %w", s, err)
408395
}
409-
end, err := parsePort(parts[1])
410-
if err != nil {
411-
return nil, xerrors.Errorf("parse range end port from %q: %w", in, err)
396+
end := start
397+
if len(e) != 0 {
398+
end, err = parsePort(e)
399+
if err != nil {
400+
return nil, xerrors.Errorf("parse range end port from %q: %w", e, err)
401+
}
412402
}
413403
if end < start {
414404
return nil, xerrors.Errorf("range end port %v is less than start port %v", end, start)

cli/portforward_internal_test.go

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package cli
22

33
import (
4-
"fmt"
5-
"strings"
64
"testing"
75

86
"github.com/stretchr/testify/require"
@@ -11,21 +9,14 @@ import (
119
func Test_parsePortForwards(t *testing.T) {
1210
t.Parallel()
1311

14-
portForwardSpecToString := func(v []portForwardSpec) (out []string) {
15-
for _, p := range v {
16-
require.Equal(t, p.listenNetwork, p.dialNetwork)
17-
out = append(out, fmt.Sprintf("%s:%s", strings.Replace(p.listenAddress, "127.0.0.1:", "", 1), strings.Replace(p.dialAddress, "127.0.0.1:", "", 1)))
18-
}
19-
return out
20-
}
2112
type args struct {
2213
tcpSpecs []string
2314
udpSpecs []string
2415
}
2516
tests := []struct {
2617
name string
2718
args args
28-
want []string
19+
want []portForwardSpec
2920
wantErr bool
3021
}{
3122
{
@@ -34,28 +25,66 @@ func Test_parsePortForwards(t *testing.T) {
3425
tcpSpecs: []string{
3526
"8000,8080:8081,9000-9002,9003-9004:9005-9006",
3627
"10000",
28+
"4444-4444",
3729
},
3830
},
39-
want: []string{
40-
"8000:8000",
41-
"8080:8081",
42-
"9000:9000",
43-
"9001:9001",
44-
"9002:9002",
45-
"9003:9005",
46-
"9004:9006",
47-
"10000:10000",
31+
want: []portForwardSpec{
32+
{"tcp", "127.0.0.1:8000", "tcp", "127.0.0.1:8000"},
33+
{"tcp", "127.0.0.1:8080", "tcp", "127.0.0.1:8081"},
34+
{"tcp", "127.0.0.1:9000", "tcp", "127.0.0.1:9000"},
35+
{"tcp", "127.0.0.1:9001", "tcp", "127.0.0.1:9001"},
36+
{"tcp", "127.0.0.1:9002", "tcp", "127.0.0.1:9002"},
37+
{"tcp", "127.0.0.1:9003", "tcp", "127.0.0.1:9005"},
38+
{"tcp", "127.0.0.1:9004", "tcp", "127.0.0.1:9006"},
39+
{"tcp", "127.0.0.1:10000", "tcp", "127.0.0.1:10000"},
40+
{"tcp", "127.0.0.1:4444", "tcp", "127.0.0.1:4444"},
41+
},
42+
},
43+
{
44+
name: "TCP IPv4 local",
45+
args: args{
46+
tcpSpecs: []string{"127.0.0.1:8080:8081"},
47+
},
48+
want: []portForwardSpec{
49+
{"tcp", "127.0.0.1:8080", "tcp", "127.0.0.1:8081"},
50+
},
51+
},
52+
{
53+
name: "TCP IPv6 local",
54+
args: args{
55+
tcpSpecs: []string{"[::1]:8080:8081"},
56+
},
57+
want: []portForwardSpec{
58+
{"tcp", "[::1]:8080", "tcp", "127.0.0.1:8081"},
4859
},
4960
},
5061
{
5162
name: "UDP with port range",
5263
args: args{
5364
udpSpecs: []string{"8000,8080-8081"},
5465
},
55-
want: []string{
56-
"8000:8000",
57-
"8080:8080",
58-
"8081:8081",
66+
want: []portForwardSpec{
67+
{"udp", "127.0.0.1:8000", "udp", "127.0.0.1:8000"},
68+
{"udp", "127.0.0.1:8080", "udp", "127.0.0.1:8080"},
69+
{"udp", "127.0.0.1:8081", "udp", "127.0.0.1:8081"},
70+
},
71+
},
72+
{
73+
name: "UDP IPv4 local",
74+
args: args{
75+
udpSpecs: []string{"127.0.0.1:8080:8081"},
76+
},
77+
want: []portForwardSpec{
78+
{"udp", "127.0.0.1:8080", "udp", "127.0.0.1:8081"},
79+
},
80+
},
81+
{
82+
name: "UDP IPv6 local",
83+
args: args{
84+
udpSpecs: []string{"[::1]:8080:8081"},
85+
},
86+
want: []portForwardSpec{
87+
{"udp", "[::1]:8080", "udp", "127.0.0.1:8081"},
5988
},
6089
},
6190
{
@@ -83,8 +112,7 @@ func Test_parsePortForwards(t *testing.T) {
83112
t.Fatalf("parsePortForwards() error = %v, wantErr %v", err, tt.wantErr)
84113
return
85114
}
86-
gotStrings := portForwardSpecToString(got)
87-
require.Equal(t, tt.want, gotStrings)
115+
require.Equal(t, tt.want, got)
88116
})
89117
}
90118
}

cli/portforward_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,16 @@ func TestPortForward(t *testing.T) {
9292
},
9393
localAddress: []string{"10.10.10.99:9999", "10.10.10.10:1010"},
9494
},
95+
{
96+
name: "TCP-IPv6",
97+
network: "tcp", flag: []string{"--tcp=[fe80::99]:9999:%v", "--tcp=[fe80::10]:1010:%v"},
98+
setupRemote: func(t *testing.T) net.Listener {
99+
l, err := net.Listen("tcp", "127.0.0.1:0")
100+
require.NoError(t, err, "create TCP listener")
101+
return l
102+
},
103+
localAddress: []string{"[fe80::99]:9999", "[fe80::10]:1010"},
104+
},
95105
}
96106

97107
// Setup agent once to be shared between test-cases (avoid expensive

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy