mirror of https://github.com/coder/coder.git
feat(cli): implement ssh remote forward (#8515)
This commit is contained in:
parent
c68e80970d
commit
9689bca5d2
|
@ -32,7 +32,7 @@ func (r *RootCmd) portForward() *clibase.Cmd {
|
||||||
client := new(codersdk.Client)
|
client := new(codersdk.Client)
|
||||||
cmd := &clibase.Cmd{
|
cmd := &clibase.Cmd{
|
||||||
Use: "port-forward <workspace>",
|
Use: "port-forward <workspace>",
|
||||||
Short: "Forward ports from machine to a workspace",
|
Short: `Forward ports from a workspace to the local machine. Forward ports from a workspace to the local machine. For reverse port forwarding, use "coder ssh -R".`,
|
||||||
Aliases: []string{"tunnel"},
|
Aliases: []string{"tunnel"},
|
||||||
Long: formatExamples(
|
Long: formatExamples(
|
||||||
example{
|
example{
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
gossh "golang.org/x/crypto/ssh"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
|
"github.com/coder/coder/agent/agentssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
// cookieAddr is a special net.Addr accepted by sshRemoteForward() which includes a
|
||||||
|
// cookie which is written to the connection before forwarding.
|
||||||
|
type cookieAddr struct {
|
||||||
|
net.Addr
|
||||||
|
cookie []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format:
|
||||||
|
// remote_port:local_address:local_port
|
||||||
|
var remoteForwardRegex = regexp.MustCompile(`^(\d+):(.+):(\d+)$`)
|
||||||
|
|
||||||
|
func validateRemoteForward(flag string) bool {
|
||||||
|
return remoteForwardRegex.MatchString(flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRemoteForward(flag string) (net.Addr, net.Addr, error) {
|
||||||
|
matches := remoteForwardRegex.FindStringSubmatch(flag)
|
||||||
|
|
||||||
|
remotePort, err := strconv.Atoi(matches[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, xerrors.Errorf("remote port is invalid: %w", err)
|
||||||
|
}
|
||||||
|
localAddress, err := net.ResolveIPAddr("ip", matches[2])
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, xerrors.Errorf("local address is invalid: %w", err)
|
||||||
|
}
|
||||||
|
localPort, err := strconv.Atoi(matches[3])
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, xerrors.Errorf("local port is invalid: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
localAddr := &net.TCPAddr{
|
||||||
|
IP: localAddress.IP,
|
||||||
|
Port: localPort,
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteAddr := &net.TCPAddr{
|
||||||
|
IP: net.ParseIP("127.0.0.1"),
|
||||||
|
Port: remotePort,
|
||||||
|
}
|
||||||
|
return localAddr, remoteAddr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sshRemoteForward starts forwarding connections from a remote listener to a
|
||||||
|
// local address via SSH in a goroutine.
|
||||||
|
//
|
||||||
|
// Accepts a `cookieAddr` as the local address.
|
||||||
|
func sshRemoteForward(ctx context.Context, stderr io.Writer, sshClient *gossh.Client, localAddr, remoteAddr net.Addr) (io.Closer, error) {
|
||||||
|
listener, err := sshClient.Listen(remoteAddr.Network(), remoteAddr.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerrors.Errorf("listen on remote SSH address %s: %w", remoteAddr.String(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
remoteConn, err := listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() == nil {
|
||||||
|
_, _ = fmt.Fprintf(stderr, "Accept SSH listener connection: %+v\n", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer remoteConn.Close()
|
||||||
|
|
||||||
|
localConn, err := net.Dial(localAddr.Network(), localAddr.String())
|
||||||
|
if err != nil {
|
||||||
|
_, _ = fmt.Fprintf(stderr, "Dial local address %s: %+v\n", localAddr.String(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer localConn.Close()
|
||||||
|
|
||||||
|
if c, ok := localAddr.(cookieAddr); ok {
|
||||||
|
_, err = localConn.Write(c.cookie)
|
||||||
|
if err != nil {
|
||||||
|
_, _ = fmt.Fprintf(stderr, "Write cookie to local connection: %+v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
agentssh.Bicopy(ctx, localConn, remoteConn)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return listener, nil
|
||||||
|
}
|
98
cli/ssh.go
98
cli/ssh.go
|
@ -6,7 +6,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
@ -27,7 +26,6 @@ import (
|
||||||
"cdr.dev/slog"
|
"cdr.dev/slog"
|
||||||
"cdr.dev/slog/sloggers/sloghuman"
|
"cdr.dev/slog/sloggers/sloghuman"
|
||||||
|
|
||||||
"github.com/coder/coder/agent/agentssh"
|
|
||||||
"github.com/coder/coder/cli/clibase"
|
"github.com/coder/coder/cli/clibase"
|
||||||
"github.com/coder/coder/cli/cliui"
|
"github.com/coder/coder/cli/cliui"
|
||||||
"github.com/coder/coder/coderd/autobuild/notify"
|
"github.com/coder/coder/coderd/autobuild/notify"
|
||||||
|
@ -53,6 +51,7 @@ func (r *RootCmd) ssh() *clibase.Cmd {
|
||||||
waitEnum string
|
waitEnum string
|
||||||
noWait bool
|
noWait bool
|
||||||
logDirPath string
|
logDirPath string
|
||||||
|
remoteForward string
|
||||||
)
|
)
|
||||||
client := new(codersdk.Client)
|
client := new(codersdk.Client)
|
||||||
cmd := &clibase.Cmd{
|
cmd := &clibase.Cmd{
|
||||||
|
@ -122,6 +121,16 @@ func (r *RootCmd) ssh() *clibase.Cmd {
|
||||||
client.SetLogger(logger)
|
client.SetLogger(logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if remoteForward != "" {
|
||||||
|
isValid := validateRemoteForward(remoteForward)
|
||||||
|
if !isValid {
|
||||||
|
return xerrors.Errorf(`invalid format of remote-forward, expected: remote_port:local_address:local_port`)
|
||||||
|
}
|
||||||
|
if isValid && stdio {
|
||||||
|
return xerrors.Errorf(`remote-forward can't be enabled in the stdio mode`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, codersdk.Me, inv.Args[0])
|
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, codersdk.Me, inv.Args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -198,6 +207,7 @@ func (r *RootCmd) ssh() *clibase.Cmd {
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
conn.AwaitReachable(ctx)
|
conn.AwaitReachable(ctx)
|
||||||
|
|
||||||
stopPolling := tryPollWorkspaceAutostop(ctx, client, workspace)
|
stopPolling := tryPollWorkspaceAutostop(ctx, client, workspace)
|
||||||
defer stopPolling()
|
defer stopPolling()
|
||||||
|
|
||||||
|
@ -300,6 +310,19 @@ func (r *RootCmd) ssh() *clibase.Cmd {
|
||||||
defer closer.Close()
|
defer closer.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if remoteForward != "" {
|
||||||
|
localAddr, remoteAddr, err := parseRemoteForward(remoteForward)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
closer, err := sshRemoteForward(ctx, inv.Stderr, sshClient, localAddr, remoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("ssh remote forward: %w", err)
|
||||||
|
}
|
||||||
|
defer closer.Close()
|
||||||
|
}
|
||||||
|
|
||||||
stdoutFile, validOut := inv.Stdout.(*os.File)
|
stdoutFile, validOut := inv.Stdout.(*os.File)
|
||||||
stdinFile, validIn := inv.Stdin.(*os.File)
|
stdinFile, validIn := inv.Stdin.(*os.File)
|
||||||
if validOut && validIn && isatty.IsTerminal(stdoutFile.Fd()) {
|
if validOut && validIn && isatty.IsTerminal(stdoutFile.Fd()) {
|
||||||
|
@ -424,6 +447,13 @@ func (r *RootCmd) ssh() *clibase.Cmd {
|
||||||
FlagShorthand: "l",
|
FlagShorthand: "l",
|
||||||
Value: clibase.StringOf(&logDirPath),
|
Value: clibase.StringOf(&logDirPath),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Flag: "remote-forward",
|
||||||
|
Description: "Enable remote port forwarding (remote_port:local_address:local_port).",
|
||||||
|
Env: "CODER_SSH_REMOTE_FORWARD",
|
||||||
|
FlagShorthand: "R",
|
||||||
|
Value: clibase.StringOf(&remoteForward),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
@ -568,8 +598,15 @@ func getWorkspaceAndAgent(ctx context.Context, inv *clibase.Invocation, client *
|
||||||
// of the CLI running simultaneously.
|
// of the CLI running simultaneously.
|
||||||
func tryPollWorkspaceAutostop(ctx context.Context, client *codersdk.Client, workspace codersdk.Workspace) (stop func()) {
|
func tryPollWorkspaceAutostop(ctx context.Context, client *codersdk.Client, workspace codersdk.Workspace) (stop func()) {
|
||||||
lock := flock.New(filepath.Join(os.TempDir(), "coder-autostop-notify-"+workspace.ID.String()))
|
lock := flock.New(filepath.Join(os.TempDir(), "coder-autostop-notify-"+workspace.ID.String()))
|
||||||
condition := notifyCondition(ctx, client, workspace.ID, lock)
|
conditionCtx, cancelCondition := context.WithCancel(ctx)
|
||||||
return notify.Notify(condition, workspacePollInterval, autostopNotifyCountdown...)
|
condition := notifyCondition(conditionCtx, client, workspace.ID, lock)
|
||||||
|
stopFunc := notify.Notify(condition, workspacePollInterval, autostopNotifyCountdown...)
|
||||||
|
return func() {
|
||||||
|
// With many "ssh" processes running, `lock.TryLockContext` can be hanging until the context canceled.
|
||||||
|
// Without this cancellation, a CLI process with failed remote-forward could be hanging indefinitely.
|
||||||
|
cancelCondition()
|
||||||
|
stopFunc()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify the user if the workspace is due to shutdown.
|
// Notify the user if the workspace is due to shutdown.
|
||||||
|
@ -752,56 +789,3 @@ func remoteGPGAgentSocket(sshClient *gossh.Client) (string, error) {
|
||||||
|
|
||||||
return string(bytes.TrimSpace(remoteSocket)), nil
|
return string(bytes.TrimSpace(remoteSocket)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// cookieAddr is a special net.Addr accepted by sshForward() which includes a
|
|
||||||
// cookie which is written to the connection before forwarding.
|
|
||||||
type cookieAddr struct {
|
|
||||||
net.Addr
|
|
||||||
cookie []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// sshForwardRemote starts forwarding connections from a remote listener to a
|
|
||||||
// local address via SSH in a goroutine.
|
|
||||||
//
|
|
||||||
// Accepts a `cookieAddr` as the local address.
|
|
||||||
func sshForwardRemote(ctx context.Context, stderr io.Writer, sshClient *gossh.Client, localAddr, remoteAddr net.Addr) (io.Closer, error) {
|
|
||||||
listener, err := sshClient.Listen(remoteAddr.Network(), remoteAddr.String())
|
|
||||||
if err != nil {
|
|
||||||
return nil, xerrors.Errorf("listen on remote SSH address %s: %w", remoteAddr.String(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
remoteConn, err := listener.Accept()
|
|
||||||
if err != nil {
|
|
||||||
if ctx.Err() == nil {
|
|
||||||
_, _ = fmt.Fprintf(stderr, "Accept SSH listener connection: %+v\n", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer remoteConn.Close()
|
|
||||||
|
|
||||||
localConn, err := net.Dial(localAddr.Network(), localAddr.String())
|
|
||||||
if err != nil {
|
|
||||||
_, _ = fmt.Fprintf(stderr, "Dial local address %s: %+v\n", localAddr.String(), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer localConn.Close()
|
|
||||||
|
|
||||||
if c, ok := localAddr.(cookieAddr); ok {
|
|
||||||
_, err = localConn.Write(c.cookie)
|
|
||||||
if err != nil {
|
|
||||||
_, _ = fmt.Fprintf(stderr, "Write cookie to local connection: %+v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
agentssh.Bicopy(ctx, localConn, remoteConn)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return listener, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -44,5 +44,5 @@ func forwardGPGAgent(ctx context.Context, stderr io.Writer, sshClient *gossh.Cli
|
||||||
Net: "unix",
|
Net: "unix",
|
||||||
}
|
}
|
||||||
|
|
||||||
return sshForwardRemote(ctx, stderr, sshClient, localAddr, remoteAddr)
|
return sshRemoteForward(ctx, stderr, sshClient, localAddr, remoteAddr)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -408,6 +410,58 @@ func TestSSH(t *testing.T) {
|
||||||
<-cmdDone
|
<-cmdDone
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("RemoteForward", func(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("Test not supported on windows")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte("hello world"))
|
||||||
|
}))
|
||||||
|
defer httpServer.Close()
|
||||||
|
|
||||||
|
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
||||||
|
|
||||||
|
agentClient := agentsdk.New(client.URL)
|
||||||
|
agentClient.SetSessionToken(agentToken)
|
||||||
|
agentCloser := agent.New(agent.Options{
|
||||||
|
Client: agentClient,
|
||||||
|
Logger: slogtest.Make(t, nil).Named("agent"),
|
||||||
|
})
|
||||||
|
defer agentCloser.Close()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
inv, root := clitest.New(t,
|
||||||
|
"ssh",
|
||||||
|
workspace.Name,
|
||||||
|
"--remote-forward",
|
||||||
|
"8222:"+httpServer.Listener.Addr().String(),
|
||||||
|
)
|
||||||
|
clitest.SetupConfig(t, client, root)
|
||||||
|
pty := ptytest.New(t).Attach(inv)
|
||||||
|
inv.Stderr = pty.Output()
|
||||||
|
cmdDone := tGo(t, func() {
|
||||||
|
err := inv.WithContext(ctx).Run()
|
||||||
|
assert.NoError(t, err, "ssh command failed")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait for the prompt or any output really to indicate the command has
|
||||||
|
// started and accepting input on stdin.
|
||||||
|
_ = pty.Peek(ctx, 1)
|
||||||
|
|
||||||
|
// Download the test page
|
||||||
|
pty.WriteLine("curl localhost:8222")
|
||||||
|
pty.ExpectMatch("hello world")
|
||||||
|
|
||||||
|
// And we're done.
|
||||||
|
pty.WriteLine("exit")
|
||||||
|
<-cmdDone
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("FileLogging", func(t *testing.T) {
|
t.Run("FileLogging", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|
|
@ -101,5 +101,5 @@ func forwardGPGAgent(ctx context.Context, stderr io.Writer, sshClient *gossh.Cli
|
||||||
Net: "unix",
|
Net: "unix",
|
||||||
}
|
}
|
||||||
|
|
||||||
return sshForwardRemote(ctx, stderr, sshClient, localAddr, remoteAddr)
|
return sshRemoteForward(ctx, stderr, sshClient, localAddr, remoteAddr)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,9 @@ Coder v0.0.0-devel — A tool for provisioning self-hosted development environme
|
||||||
logout Unauthenticate your local session
|
logout Unauthenticate your local session
|
||||||
netcheck Print network debug information for DERP and STUN
|
netcheck Print network debug information for DERP and STUN
|
||||||
ping Ping a workspace
|
ping Ping a workspace
|
||||||
port-forward Forward ports from machine to a workspace
|
port-forward Forward ports from a workspace to the local machine.
|
||||||
|
Forward ports from a workspace to the local machine. For
|
||||||
|
reverse port forwarding, use "coder ssh -R".
|
||||||
publickey Output your Coder public key used for Git operations
|
publickey Output your Coder public key used for Git operations
|
||||||
rename Rename a workspace
|
rename Rename a workspace
|
||||||
reset-password Directly connect to the database to reset a user's
|
reset-password Directly connect to the database to reset a user's
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
Usage: coder port-forward [flags] <workspace>
|
Usage: coder port-forward [flags] <workspace>
|
||||||
|
|
||||||
Forward ports from machine to a workspace
|
Forward ports from a workspace to the local machine. Forward ports from a
|
||||||
|
workspace to the local machine. For reverse port forwarding, use "coder ssh -R".
|
||||||
|
|
||||||
Aliases: tunnel
|
Aliases: tunnel
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,9 @@ Start a shell into a workspace
|
||||||
behavior as non-blocking.
|
behavior as non-blocking.
|
||||||
DEPRECATED: Use --wait instead.
|
DEPRECATED: Use --wait instead.
|
||||||
|
|
||||||
|
-R, --remote-forward string, $CODER_SSH_REMOTE_FORWARD
|
||||||
|
Enable remote port forwarding (remote_port:local_address:local_port).
|
||||||
|
|
||||||
--stdio bool, $CODER_SSH_STDIO
|
--stdio bool, $CODER_SSH_STDIO
|
||||||
Specifies whether to emit SSH output over stdin/stdout.
|
Specifies whether to emit SSH output over stdin/stdout.
|
||||||
|
|
||||||
|
|
68
docs/cli.md
68
docs/cli.md
|
@ -23,40 +23,40 @@ Coder — A tool for provisioning self-hosted development environments with Terr
|
||||||
|
|
||||||
## Subcommands
|
## Subcommands
|
||||||
|
|
||||||
| Name | Purpose |
|
| Name | Purpose |
|
||||||
| ------------------------------------------------------ | ---------------------------------------------------------------------- |
|
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| [<code>config-ssh</code>](./cli/config-ssh.md) | Add an SSH Host entry for your workspaces "ssh coder.workspace" |
|
| [<code>config-ssh</code>](./cli/config-ssh.md) | Add an SSH Host entry for your workspaces "ssh coder.workspace" |
|
||||||
| [<code>create</code>](./cli/create.md) | Create a workspace |
|
| [<code>create</code>](./cli/create.md) | Create a workspace |
|
||||||
| [<code>delete</code>](./cli/delete.md) | Delete a workspace |
|
| [<code>delete</code>](./cli/delete.md) | Delete a workspace |
|
||||||
| [<code>dotfiles</code>](./cli/dotfiles.md) | Personalize your workspace by applying a canonical dotfiles repository |
|
| [<code>dotfiles</code>](./cli/dotfiles.md) | Personalize your workspace by applying a canonical dotfiles repository |
|
||||||
| [<code>features</code>](./cli/features.md) | List Enterprise features |
|
| [<code>features</code>](./cli/features.md) | List Enterprise features |
|
||||||
| [<code>groups</code>](./cli/groups.md) | Manage groups |
|
| [<code>groups</code>](./cli/groups.md) | Manage groups |
|
||||||
| [<code>licenses</code>](./cli/licenses.md) | Add, delete, and list licenses |
|
| [<code>licenses</code>](./cli/licenses.md) | Add, delete, and list licenses |
|
||||||
| [<code>list</code>](./cli/list.md) | List workspaces |
|
| [<code>list</code>](./cli/list.md) | List workspaces |
|
||||||
| [<code>login</code>](./cli/login.md) | Authenticate with Coder deployment |
|
| [<code>login</code>](./cli/login.md) | Authenticate with Coder deployment |
|
||||||
| [<code>logout</code>](./cli/logout.md) | Unauthenticate your local session |
|
| [<code>logout</code>](./cli/logout.md) | Unauthenticate your local session |
|
||||||
| [<code>netcheck</code>](./cli/netcheck.md) | Print network debug information for DERP and STUN |
|
| [<code>netcheck</code>](./cli/netcheck.md) | Print network debug information for DERP and STUN |
|
||||||
| [<code>ping</code>](./cli/ping.md) | Ping a workspace |
|
| [<code>ping</code>](./cli/ping.md) | Ping a workspace |
|
||||||
| [<code>port-forward</code>](./cli/port-forward.md) | Forward ports from machine to a workspace |
|
| [<code>port-forward</code>](./cli/port-forward.md) | Forward ports from a workspace to the local machine. Forward ports from a workspace to the local machine. For reverse port forwarding, use "coder ssh -R". |
|
||||||
| [<code>provisionerd</code>](./cli/provisionerd.md) | Manage provisioner daemons |
|
| [<code>provisionerd</code>](./cli/provisionerd.md) | Manage provisioner daemons |
|
||||||
| [<code>publickey</code>](./cli/publickey.md) | Output your Coder public key used for Git operations |
|
| [<code>publickey</code>](./cli/publickey.md) | Output your Coder public key used for Git operations |
|
||||||
| [<code>rename</code>](./cli/rename.md) | Rename a workspace |
|
| [<code>rename</code>](./cli/rename.md) | Rename a workspace |
|
||||||
| [<code>reset-password</code>](./cli/reset-password.md) | Directly connect to the database to reset a user's password |
|
| [<code>reset-password</code>](./cli/reset-password.md) | Directly connect to the database to reset a user's password |
|
||||||
| [<code>restart</code>](./cli/restart.md) | Restart a workspace |
|
| [<code>restart</code>](./cli/restart.md) | Restart a workspace |
|
||||||
| [<code>schedule</code>](./cli/schedule.md) | Schedule automated start and stop times for workspaces |
|
| [<code>schedule</code>](./cli/schedule.md) | Schedule automated start and stop times for workspaces |
|
||||||
| [<code>server</code>](./cli/server.md) | Start a Coder server |
|
| [<code>server</code>](./cli/server.md) | Start a Coder server |
|
||||||
| [<code>show</code>](./cli/show.md) | Display details of a workspace's resources and agents |
|
| [<code>show</code>](./cli/show.md) | Display details of a workspace's resources and agents |
|
||||||
| [<code>speedtest</code>](./cli/speedtest.md) | Run upload and download tests from your machine to a workspace |
|
| [<code>speedtest</code>](./cli/speedtest.md) | Run upload and download tests from your machine to a workspace |
|
||||||
| [<code>ssh</code>](./cli/ssh.md) | Start a shell into a workspace |
|
| [<code>ssh</code>](./cli/ssh.md) | Start a shell into a workspace |
|
||||||
| [<code>start</code>](./cli/start.md) | Start a workspace |
|
| [<code>start</code>](./cli/start.md) | Start a workspace |
|
||||||
| [<code>stat</code>](./cli/stat.md) | Show resource usage for the current workspace. |
|
| [<code>stat</code>](./cli/stat.md) | Show resource usage for the current workspace. |
|
||||||
| [<code>state</code>](./cli/state.md) | Manually manage Terraform state to fix broken workspaces |
|
| [<code>state</code>](./cli/state.md) | Manually manage Terraform state to fix broken workspaces |
|
||||||
| [<code>stop</code>](./cli/stop.md) | Stop a workspace |
|
| [<code>stop</code>](./cli/stop.md) | Stop a workspace |
|
||||||
| [<code>templates</code>](./cli/templates.md) | Manage templates |
|
| [<code>templates</code>](./cli/templates.md) | Manage templates |
|
||||||
| [<code>tokens</code>](./cli/tokens.md) | Manage personal access tokens |
|
| [<code>tokens</code>](./cli/tokens.md) | Manage personal access tokens |
|
||||||
| [<code>update</code>](./cli/update.md) | Will update and start a given workspace if it is out of date |
|
| [<code>update</code>](./cli/update.md) | Will update and start a given workspace if it is out of date |
|
||||||
| [<code>users</code>](./cli/users.md) | Manage users |
|
| [<code>users</code>](./cli/users.md) | Manage users |
|
||||||
| [<code>version</code>](./cli/version.md) | Show coder version |
|
| [<code>version</code>](./cli/version.md) | Show coder version |
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
# port-forward
|
# port-forward
|
||||||
|
|
||||||
Forward ports from machine to a workspace
|
Forward ports from a workspace to the local machine. Forward ports from a workspace to the local machine. For reverse port forwarding, use "coder ssh -R".
|
||||||
|
|
||||||
Aliases:
|
Aliases:
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,15 @@ Specify the directory containing SSH diagnostic log files.
|
||||||
|
|
||||||
Enter workspace immediately after the agent has connected. This is the default if the template has configured the agent startup script behavior as non-blocking.
|
Enter workspace immediately after the agent has connected. This is the default if the template has configured the agent startup script behavior as non-blocking.
|
||||||
|
|
||||||
|
### -R, --remote-forward
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| ----------- | -------------------------------------- |
|
||||||
|
| Type | <code>string</code> |
|
||||||
|
| Environment | <code>$CODER_SSH_REMOTE_FORWARD</code> |
|
||||||
|
|
||||||
|
Enable remote port forwarding (remote_port:local_address:local_port).
|
||||||
|
|
||||||
### --stdio
|
### --stdio
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|
|
|
@ -610,7 +610,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "port-forward",
|
"title": "port-forward",
|
||||||
"description": "Forward ports from machine to a workspace",
|
"description": "Forward ports from a workspace to the local machine. Forward ports from a workspace to the local machine. For reverse port forwarding, use \"coder ssh -R\".",
|
||||||
"path": "cli/port-forward.md"
|
"path": "cli/port-forward.md"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue