Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

feat: Display error to user when SSHing into offline workspace #411

Merged
merged 3 commits into from
Aug 9, 2021
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
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ require (
github.com/rjeczalik/notify v0.9.2
github.com/spf13/cobra v1.2.1
github.com/stretchr/testify v1.7.0
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
golang.org/x/net v0.0.0-20210614182718-04defd469f4e
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -430,8 +430,9 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
Expand Down Expand Up @@ -590,8 +591,9 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
Expand Down
146 changes: 119 additions & 27 deletions internal/cmd/tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ import (

"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"github.com/fatih/color"
"github.com/pion/webrtc/v3"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh"
"golang.org/x/xerrors"

"cdr.dev/coder-cli/coder-sdk"
"cdr.dev/coder-cli/internal/x/xcobra"
"cdr.dev/coder-cli/pkg/clog"
"cdr.dev/coder-cli/wsnet"
)

Expand Down Expand Up @@ -59,20 +62,34 @@ coder tunnel my-dev 3000 3000
}
baseURL := sdk.BaseURL()

workspaces, err := getWorkspaces(ctx, sdk, coder.Me)
workspace, err := findWorkspace(ctx, sdk, args[0], coder.Me)
if err != nil {
return xerrors.Errorf("get workspaces: %w", err)
}

var workspaceID string
for _, workspace := range workspaces {
if workspace.Name == args[0] {
workspaceID = workspace.ID
break
if workspace.LatestStat.ContainerStatus != coder.WorkspaceOn {
color.NoColor = false
notAvailableError := clog.Error("workspace not available",
fmt.Sprintf("current status: %q", workspace.LatestStat.ContainerStatus),
clog.BlankLine,
clog.Tipf("use \"coder workspaces rebuild %s\" to rebuild this workspace", workspace.Name),
)
// If we're attempting to forward our remote SSH port,
// we want to communicate with the OpenSSH protocol so
// SSH clients can properly display output to our users.
if remotePort == 12213 {
rawKey, err := sdk.SSHKey(ctx)
if err != nil {
return xerrors.Errorf("get ssh key: %w", err)
}
err = discardSSHConnection(&stdioConn{}, rawKey.PrivateKey, notAvailableError.String())
if err != nil {
return err
}
return nil
}
}
if workspaceID == "" {
return xerrors.Errorf("No workspace found by name '%s'", args[0])

return notAvailableError
}

iceServers, err := sdk.ICEServers(ctx)
Expand All @@ -82,14 +99,14 @@ coder tunnel my-dev 3000 3000
log.Debug(ctx, "got ICE servers", slog.F("ice", iceServers))

c := &tunnneler{
log: log,
brokerAddr: &baseURL,
token: sdk.Token(),
workspaceID: workspaceID,
iceServers: iceServers,
stdio: args[2] == "stdio",
localPort: uint16(localPort),
remotePort: uint16(remotePort),
log: log,
brokerAddr: &baseURL,
token: sdk.Token(),
workspace: workspace,
iceServers: iceServers,
stdio: args[2] == "stdio",
localPort: uint16(localPort),
remotePort: uint16(remotePort),
}

err = c.start(ctx)
Expand All @@ -105,14 +122,14 @@ coder tunnel my-dev 3000 3000
}

type tunnneler struct {
log slog.Logger
brokerAddr *url.URL
token string
workspaceID string
iceServers []webrtc.ICEServer
remotePort uint16
localPort uint16
stdio bool
log slog.Logger
brokerAddr *url.URL
token string
workspace *coder.Workspace
iceServers []webrtc.ICEServer
remotePort uint16
localPort uint16
stdio bool
}

func (c *tunnneler) start(ctx context.Context) error {
Expand All @@ -121,7 +138,7 @@ func (c *tunnneler) start(ctx context.Context) error {
dialLog := c.log.Named("wsnet")
wd, err := wsnet.DialWebsocket(
ctx,
wsnet.ConnectEndpoint(c.brokerAddr, c.workspaceID, c.token),
wsnet.ConnectEndpoint(c.brokerAddr, c.workspace.ID, c.token),
&wsnet.DialOptions{
Log: &dialLog,
TURNProxyAuthToken: c.token,
Expand Down Expand Up @@ -156,7 +173,7 @@ func (c *tunnneler) start(ctx context.Context) error {
return
case <-ticker.C:
// silently ignore failures so we don't spam the console
_ = sdk.UpdateLastConnectionAt(ctx, c.workspaceID)
_ = sdk.UpdateLastConnectionAt(ctx, c.workspace.ID)
}
}
}()
Expand Down Expand Up @@ -203,3 +220,78 @@ func (c *tunnneler) start(ctx context.Context) error {
}()
}
}

// Used to treat stdio like a connection for proxying SSH.
type stdioConn struct{}

func (s *stdioConn) Read(b []byte) (n int, err error) {
return os.Stdin.Read(b)
}

func (s *stdioConn) Write(b []byte) (n int, err error) {
return os.Stdout.Write(b)
}

func (s *stdioConn) Close() error {
return nil
}

func (s *stdioConn) LocalAddr() net.Addr {
return nil
}

func (s *stdioConn) RemoteAddr() net.Addr {
return nil
}

func (s *stdioConn) SetDeadline(t time.Time) error {
return nil
}

func (s *stdioConn) SetReadDeadline(t time.Time) error {
return nil
}

func (s *stdioConn) SetWriteDeadline(t time.Time) error {
return nil
}

// discardSSHConnection accepts a connection then outputs the message provided
// to any channel opened, immediately closing the connection afterwards.
//
// Used to provide status to connecting clients while still aligning with the
// native SSH protocol.
func discardSSHConnection(nc net.Conn, privateKey string, msg string) error {
config := &ssh.ServerConfig{
NoClientAuth: true,
}
key, err := ssh.ParseRawPrivateKey([]byte(privateKey))
if err != nil {
return fmt.Errorf("parse private key: %w", err)
}
signer, err := ssh.NewSignerFromKey(key)
if err != nil {
return fmt.Errorf("signer from private key: %w", err)
}
config.AddHostKey(signer)
conn, chans, reqs, err := ssh.NewServerConn(nc, config)
if err != nil {
return fmt.Errorf("create server conn: %w", err)
}
go ssh.DiscardRequests(reqs)
ch, req, err := (<-chans).Accept()
if err != nil {
return fmt.Errorf("accept channel: %w", err)
}
go ssh.DiscardRequests(req)

_, err = ch.Write([]byte(msg))
if err != nil {
return fmt.Errorf("write channel: %w", err)
}
err = ch.Close()
if err != nil {
return fmt.Errorf("close channel: %w", err)
}
return conn.Close()
}