Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 092a22f

Browse files
authored
feat: Support for comma-separation and ranges in port-forward (#4166)
Fixes #3766
1 parent 4919975 commit 092a22f

File tree

3 files changed

+204
-33
lines changed

3 files changed

+204
-33
lines changed

cli/portforward.go

Lines changed: 92 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818

1919
"cdr.dev/slog"
2020
"github.com/coder/coder/agent"
21+
"github.com/coder/coder/cli/cliflag"
2122
"github.com/coder/coder/cli/cliui"
2223
"github.com/coder/coder/codersdk"
2324
)
@@ -45,6 +46,10 @@ func portForward() *cobra.Command {
4546
Description: "Port forward multiple TCP ports and a UDP port",
4647
Command: "coder port-forward <workspace> --tcp 8080:8080 --tcp 9000:3000 --udp 5353:53",
4748
},
49+
example{
50+
Description: "Port forward multiple ports (TCP or UDP) in condensed syntax",
51+
Command: "coder port-forward <workspace> --tcp 8080,9000:3000,9090-9092,10000-10002:10010-10012",
52+
},
4853
),
4954
RunE: func(cmd *cobra.Command, args []string) error {
5055
ctx, cancel := context.WithCancel(cmd.Context())
@@ -164,8 +169,8 @@ func portForward() *cobra.Command {
164169
},
165170
}
166171

167-
cmd.Flags().StringArrayVarP(&tcpForwards, "tcp", "p", []string{}, "Forward a TCP port from the workspace to the local machine")
168-
cmd.Flags().StringArrayVar(&udpForwards, "udp", []string{}, "Forward a UDP port from the workspace to the local machine. The UDP connection has TCP-like semantics to support stateful UDP protocols")
172+
cliflag.StringArrayVarP(cmd.Flags(), &tcpForwards, "tcp", "p", "CODER_PORT_FORWARD_TCP", nil, "Forward TCP port(s) from the workspace to the local machine")
173+
cliflag.StringArrayVarP(cmd.Flags(), &udpForwards, "udp", "", "CODER_PORT_FORWARD_UDP", nil, "Forward UDP port(s) from the workspace to the local machine. The UDP connection has TCP-like semantics to support stateful UDP protocols")
169174
return cmd
170175
}
171176

@@ -242,32 +247,40 @@ type portForwardSpec struct {
242247
func parsePortForwards(tcpSpecs, udpSpecs []string) ([]portForwardSpec, error) {
243248
specs := []portForwardSpec{}
244249

245-
for _, spec := range tcpSpecs {
246-
local, remote, err := parsePortPort(spec)
247-
if err != nil {
248-
return nil, xerrors.Errorf("failed to parse TCP port-forward specification %q: %w", spec, err)
249-
}
250+
for _, specEntry := range tcpSpecs {
251+
for _, spec := range strings.Split(specEntry, ",") {
252+
ports, err := parseSrcDestPorts(spec)
253+
if err != nil {
254+
return nil, xerrors.Errorf("failed to parse TCP port-forward specification %q: %w", spec, err)
255+
}
250256

251-
specs = append(specs, portForwardSpec{
252-
listenNetwork: "tcp",
253-
listenAddress: fmt.Sprintf("127.0.0.1:%v", local),
254-
dialNetwork: "tcp",
255-
dialAddress: fmt.Sprintf("127.0.0.1:%v", remote),
256-
})
257+
for _, port := range ports {
258+
specs = append(specs, portForwardSpec{
259+
listenNetwork: "tcp",
260+
listenAddress: fmt.Sprintf("127.0.0.1:%v", port.local),
261+
dialNetwork: "tcp",
262+
dialAddress: fmt.Sprintf("127.0.0.1:%v", port.remote),
263+
})
264+
}
265+
}
257266
}
258267

259-
for _, spec := range udpSpecs {
260-
local, remote, err := parsePortPort(spec)
261-
if err != nil {
262-
return nil, xerrors.Errorf("failed to parse UDP port-forward specification %q: %w", spec, err)
263-
}
268+
for _, specEntry := range udpSpecs {
269+
for _, spec := range strings.Split(specEntry, ",") {
270+
ports, err := parseSrcDestPorts(spec)
271+
if err != nil {
272+
return nil, xerrors.Errorf("failed to parse UDP port-forward specification %q: %w", spec, err)
273+
}
264274

265-
specs = append(specs, portForwardSpec{
266-
listenNetwork: "udp",
267-
listenAddress: fmt.Sprintf("127.0.0.1:%v", local),
268-
dialNetwork: "udp",
269-
dialAddress: fmt.Sprintf("127.0.0.1:%v", remote),
270-
})
275+
for _, port := range ports {
276+
specs = append(specs, portForwardSpec{
277+
listenNetwork: "udp",
278+
listenAddress: fmt.Sprintf("127.0.0.1:%v", port.local),
279+
dialNetwork: "udp",
280+
dialAddress: fmt.Sprintf("127.0.0.1:%v", port.remote),
281+
})
282+
}
283+
}
271284
}
272285

273286
// Check for duplicate entries.
@@ -295,24 +308,72 @@ func parsePort(in string) (uint16, error) {
295308
return uint16(port), nil
296309
}
297310

298-
func parsePortPort(in string) (local uint16, remote uint16, err error) {
311+
type parsedSrcDestPort struct {
312+
local, remote uint16
313+
}
314+
315+
func parseSrcDestPorts(in string) ([]parsedSrcDestPort, error) {
299316
parts := strings.Split(in, ":")
300317
if len(parts) > 2 {
301-
return 0, 0, xerrors.Errorf("invalid port specification %q", in)
318+
return nil, xerrors.Errorf("invalid port specification %q", in)
302319
}
303320
if len(parts) == 1 {
304321
// Duplicate the single part
305322
parts = append(parts, parts[0])
306323
}
324+
if !strings.Contains(parts[0], "-") {
325+
local, err := parsePort(parts[0])
326+
if err != nil {
327+
return nil, xerrors.Errorf("parse local port from %q: %w", in, err)
328+
}
329+
remote, err := parsePort(parts[1])
330+
if err != nil {
331+
return nil, xerrors.Errorf("parse remote port from %q: %w", in, err)
332+
}
307333

308-
local, err = parsePort(parts[0])
334+
return []parsedSrcDestPort{{local: local, remote: remote}}, nil
335+
}
336+
337+
local, err := parsePortRange(parts[0])
309338
if err != nil {
310-
return 0, 0, xerrors.Errorf("parse local port from %q: %w", in, err)
339+
return nil, xerrors.Errorf("parse local port range from %q: %w", in, err)
311340
}
312-
remote, err = parsePort(parts[1])
341+
remote, err := parsePortRange(parts[1])
313342
if err != nil {
314-
return 0, 0, xerrors.Errorf("parse remote port from %q: %w", in, err)
343+
return nil, xerrors.Errorf("parse remote port range from %q: %w", in, err)
344+
}
345+
if len(local) != len(remote) {
346+
return nil, xerrors.Errorf("port ranges must be the same length, got %d ports forwarded to %d ports", len(local), len(remote))
347+
}
348+
var out []parsedSrcDestPort
349+
for i := range local {
350+
out = append(out, parsedSrcDestPort{
351+
local: local[i],
352+
remote: remote[i],
353+
})
315354
}
355+
return out, nil
356+
}
316357

317-
return local, remote, nil
358+
func parsePortRange(in string) ([]uint16, error) {
359+
parts := strings.Split(in, "-")
360+
if len(parts) != 2 {
361+
return nil, xerrors.Errorf("invalid port range specification %q", in)
362+
}
363+
start, err := parsePort(parts[0])
364+
if err != nil {
365+
return nil, xerrors.Errorf("parse range start port from %q: %w", in, err)
366+
}
367+
end, err := parsePort(parts[1])
368+
if err != nil {
369+
return nil, xerrors.Errorf("parse range end port from %q: %w", in, err)
370+
}
371+
if end < start {
372+
return nil, xerrors.Errorf("range end port %v is less than start port %v", end, start)
373+
}
374+
var ports []uint16
375+
for i := start; i <= end; i++ {
376+
ports = append(ports, i)
377+
}
378+
return ports, nil
318379
}

cli/portforward_internal_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func Test_parsePortForwards(t *testing.T) {
12+
t.Parallel()
13+
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+
}
21+
type args struct {
22+
tcpSpecs []string
23+
udpSpecs []string
24+
}
25+
tests := []struct {
26+
name string
27+
args args
28+
want []string
29+
wantErr bool
30+
}{
31+
{
32+
name: "TCP mixed ports and ranges",
33+
args: args{
34+
tcpSpecs: []string{
35+
"8000,8080:8081,9000-9002,9003-9004:9005-9006",
36+
"10000",
37+
},
38+
},
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",
48+
},
49+
},
50+
{
51+
name: "UDP with port range",
52+
args: args{
53+
udpSpecs: []string{"8000,8080-8081"},
54+
},
55+
want: []string{
56+
"8000:8000",
57+
"8080:8080",
58+
"8081:8081",
59+
},
60+
},
61+
{
62+
name: "Bad port range",
63+
args: args{
64+
tcpSpecs: []string{"8000-7000"},
65+
},
66+
wantErr: true,
67+
},
68+
{
69+
name: "Bad dest port range",
70+
args: args{
71+
tcpSpecs: []string{"8080-8081:9080-9082"},
72+
},
73+
wantErr: true,
74+
},
75+
}
76+
for _, tt := range tests {
77+
tt := tt
78+
t.Run(tt.name, func(t *testing.T) {
79+
t.Parallel()
80+
81+
got, err := parsePortForwards(tt.args.tcpSpecs, tt.args.udpSpecs)
82+
if (err != nil) != tt.wantErr {
83+
t.Fatalf("parsePortForwards() error = %v, wantErr %v", err, tt.wantErr)
84+
return
85+
}
86+
gotStrings := portForwardSpecToString(got)
87+
require.Equal(t, tt.want, gotStrings)
88+
})
89+
}
90+
}

docs/networking/port-forwarding.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,34 @@ There are three ways to forward ports in Coder:
1212

1313
The `coder port-forward` command is generally more performant.
1414

15-
## coder port-forward
15+
## The `coder port-forward` command
1616

17-
Forward the remote TCP port `8080` to local port `8000` like so:
17+
This command can be used to forward TCP or UDP ports from the remote
18+
workspace so they can be accessed locally. Both the TCP and UDP command
19+
line flags (`--tcp` and `--udp`) can be given once or multiple times.
20+
21+
The supported syntax variations for the `--tcp` and `--udp` flag are:
22+
23+
- Single port with optional remote port: `local_port[:remote_port]`
24+
- Comma separation `local_port1,local_port2`
25+
- Port ranges `start_port-end_port`
26+
- Any combination of the above
27+
28+
### Examples
29+
30+
Forward the remote TCP port `8080` to local port `8000`:
1831

1932
```console
2033
coder port-forward myworkspace --tcp 8000:8080
2134
```
2235

36+
Forward the remote TCP port `3000` and all ports from `9990` to `9999`
37+
to their respective local ports.
38+
39+
```console
40+
coder port-forward myworkspace --tcp 3000,9990-9999
41+
```
42+
2343
For more examples, see `coder port-forward --help`.
2444

2545
## Dashboard

0 commit comments

Comments
 (0)