Skip to content

feat(cli): allow specifying the listen address in coder port-forward #7635

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 48 additions & 15 deletions cli/portforward.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net"
"net/netip"
"os"
"os/signal"
"strconv"
Expand Down Expand Up @@ -47,6 +48,10 @@ func (r *RootCmd) portForward() *clibase.Cmd {
Description: "Port forward multiple ports (TCP or UDP) in condensed syntax",
Command: "coder port-forward <workspace> --tcp 8080,9000:3000,9090-9092,10000-10002:10010-10012",
},
example{
Description: "Port forward specifying the local address to bind to",
Command: "coder port-forward <workspace> --tcp 1.2.3.4:8080:8080",
},
),
Middleware: clibase.Chain(
clibase.RequireNArgs(1),
Expand Down Expand Up @@ -255,9 +260,9 @@ func parsePortForwards(tcpSpecs, udpSpecs []string) ([]portForwardSpec, error) {
for _, port := range ports {
specs = append(specs, portForwardSpec{
listenNetwork: "tcp",
listenAddress: fmt.Sprintf("127.0.0.1:%v", port.local),
listenAddress: port.local.String(),
dialNetwork: "tcp",
dialAddress: fmt.Sprintf("127.0.0.1:%v", port.remote),
dialAddress: port.remote.String(),
})
}
}
Expand All @@ -273,9 +278,9 @@ func parsePortForwards(tcpSpecs, udpSpecs []string) ([]portForwardSpec, error) {
for _, port := range ports {
specs = append(specs, portForwardSpec{
listenNetwork: "udp",
listenAddress: fmt.Sprintf("127.0.0.1:%v", port.local),
listenAddress: port.local.String(),
dialNetwork: "udp",
dialAddress: fmt.Sprintf("127.0.0.1:%v", port.remote),
dialAddress: port.remote.String(),
})
}
}
Expand Down Expand Up @@ -307,29 +312,57 @@ func parsePort(in string) (uint16, error) {
}

type parsedSrcDestPort struct {
local, remote uint16
local, remote netip.AddrPort
}

func parseSrcDestPorts(in string) ([]parsedSrcDestPort, error) {
parts := strings.Split(in, ":")
if len(parts) > 2 {
return nil, xerrors.Errorf("invalid port specification %q", in)
}
if len(parts) == 1 {
var (
err error
parts = strings.Split(in, ":")
localAddr = netip.AddrFrom4([4]byte{127, 0, 0, 1})
remoteAddr = netip.AddrFrom4([4]byte{127, 0, 0, 1})
)

switch len(parts) {
case 1:
// Duplicate the single part
parts = append(parts, parts[0])
case 2:
// Check to see if the first part is an IP address.
_localAddr, err := netip.ParseAddr(parts[0])
if err != nil {
break
}
// The first part is the local address, so duplicate the port.
localAddr = _localAddr
parts = []string{parts[1], parts[1]}

case 3:
_localAddr, err := netip.ParseAddr(parts[0])
if err != nil {
return nil, xerrors.Errorf("invalid port specification %q; invalid ip %q: %w", in, parts[0], err)
}
localAddr = _localAddr
parts = parts[1:]

default:
return nil, xerrors.Errorf("invalid port specification %q", in)
}

if !strings.Contains(parts[0], "-") {
local, err := parsePort(parts[0])
localPort, err := parsePort(parts[0])
if err != nil {
return nil, xerrors.Errorf("parse local port from %q: %w", in, err)
}
remote, err := parsePort(parts[1])
remotePort, err := parsePort(parts[1])
if err != nil {
return nil, xerrors.Errorf("parse remote port from %q: %w", in, err)
}

return []parsedSrcDestPort{{local: local, remote: remote}}, nil
return []parsedSrcDestPort{{
local: netip.AddrPortFrom(localAddr, localPort),
remote: netip.AddrPortFrom(remoteAddr, remotePort),
}}, nil
}

local, err := parsePortRange(parts[0])
Expand All @@ -346,8 +379,8 @@ func parseSrcDestPorts(in string) ([]parsedSrcDestPort, error) {
var out []parsedSrcDestPort
for i := range local {
out = append(out, parsedSrcDestPort{
local: local[i],
remote: remote[i],
local: netip.AddrPortFrom(localAddr, local[i]),
remote: netip.AddrPortFrom(remoteAddr, remote[i]),
})
}
return out, nil
Expand Down
19 changes: 19 additions & 0 deletions cli/portforward_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,25 @@ func TestPortForward(t *testing.T) {
return l.Addr().String(), port
},
},
{
name: "TCPWithAddress",
network: "tcp",
flag: "--tcp=%v:%v",
setupRemote: func(t *testing.T) net.Listener {
l, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err, "create TCP listener")
return l
},
setupLocal: func(t *testing.T) (string, string) {
l, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err, "create TCP listener to generate random port")
defer l.Close()

_, port, err := net.SplitHostPort(l.Addr().String())
require.NoErrorf(t, err, "split TCP address %q", l.Addr().String())
return l.Addr().String(), fmt.Sprint("0.0.0.0:", port)
},
},
}

// Setup agent once to be shared between test-cases (avoid expensive
Expand Down
4 changes: 4 additions & 0 deletions cli/testdata/coder_port-forward_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ Aliases: tunnel

 $ coder port-forward <workspace> --tcp 8080,9000:3000,9090-9092,10000-10002:10010-10012 

- Port forward specifying the local address to bind to:

 $ coder port-forward <workspace> --tcp 1.2.3.4:8080:8080 

Options
-p, --tcp string-array, $CODER_PORT_FORWARD_TCP
Forward TCP port(s) from the workspace to the local machine.
Expand Down
4 changes: 4 additions & 0 deletions docs/cli/port-forward.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ coder port-forward [flags] <workspace>
- Port forward multiple ports (TCP or UDP) in condensed syntax:

$ coder port-forward <workspace> --tcp 8080,9000:3000,9090-9092,10000-10002:10010-10012

- Port forward specifying the local address to bind to:

$ coder port-forward <workspace> --tcp 1.2.3.4:8080:8080
```

## Options
Expand Down