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

Commit 3536869

Browse files
kylecarbsjawnsy
andauthored
feat: Display error to user when SSHing into offline workspace (#411)
* feat: Display error to user when SSH'ing into offline workspace * Remove new \r * Update internal/cmd/tunnel.go Co-authored-by: Jonathan Yu <jonathan@coder.com> Co-authored-by: Jonathan Yu <jonathan@coder.com>
1 parent 3af8385 commit 3536869

File tree

3 files changed

+125
-30
lines changed

3 files changed

+125
-30
lines changed

go.mod

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@ require (
2525
github.com/rjeczalik/notify v0.9.2
2626
github.com/spf13/cobra v1.2.1
2727
github.com/stretchr/testify v1.7.0
28+
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
2829
golang.org/x/net v0.0.0-20210614182718-04defd469f4e
2930
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
30-
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015
31+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1
3132
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1
3233
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
3334
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1

go.sum

+4-2
Original file line numberDiff line numberDiff line change
@@ -430,8 +430,9 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U
430430
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
431431
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
432432
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
433-
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
434433
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
434+
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
435+
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
435436
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
436437
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
437438
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -590,8 +591,9 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
590591
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
591592
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
592593
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
593-
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E=
594594
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
595+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
596+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
595597
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
596598
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
597599
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

internal/cmd/tunnel.go

+119-27
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ import (
1212

1313
"cdr.dev/slog"
1414
"cdr.dev/slog/sloggers/sloghuman"
15+
"github.com/fatih/color"
1516
"github.com/pion/webrtc/v3"
1617
"github.com/spf13/cobra"
18+
"golang.org/x/crypto/ssh"
1719
"golang.org/x/xerrors"
1820

1921
"cdr.dev/coder-cli/coder-sdk"
2022
"cdr.dev/coder-cli/internal/x/xcobra"
23+
"cdr.dev/coder-cli/pkg/clog"
2124
"cdr.dev/coder-cli/wsnet"
2225
)
2326

@@ -59,20 +62,34 @@ coder tunnel my-dev 3000 3000
5962
}
6063
baseURL := sdk.BaseURL()
6164

62-
workspaces, err := getWorkspaces(ctx, sdk, coder.Me)
65+
workspace, err := findWorkspace(ctx, sdk, args[0], coder.Me)
6366
if err != nil {
6467
return xerrors.Errorf("get workspaces: %w", err)
6568
}
6669

67-
var workspaceID string
68-
for _, workspace := range workspaces {
69-
if workspace.Name == args[0] {
70-
workspaceID = workspace.ID
71-
break
70+
if workspace.LatestStat.ContainerStatus != coder.WorkspaceOn {
71+
color.NoColor = false
72+
notAvailableError := clog.Error("workspace not available",
73+
fmt.Sprintf("current status: %q", workspace.LatestStat.ContainerStatus),
74+
clog.BlankLine,
75+
clog.Tipf("use \"coder workspaces rebuild %s\" to rebuild this workspace", workspace.Name),
76+
)
77+
// If we're attempting to forward our remote SSH port,
78+
// we want to communicate with the OpenSSH protocol so
79+
// SSH clients can properly display output to our users.
80+
if remotePort == 12213 {
81+
rawKey, err := sdk.SSHKey(ctx)
82+
if err != nil {
83+
return xerrors.Errorf("get ssh key: %w", err)
84+
}
85+
err = discardSSHConnection(&stdioConn{}, rawKey.PrivateKey, notAvailableError.String())
86+
if err != nil {
87+
return err
88+
}
89+
return nil
7290
}
73-
}
74-
if workspaceID == "" {
75-
return xerrors.Errorf("No workspace found by name '%s'", args[0])
91+
92+
return notAvailableError
7693
}
7794

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

84101
c := &tunnneler{
85-
log: log,
86-
brokerAddr: &baseURL,
87-
token: sdk.Token(),
88-
workspaceID: workspaceID,
89-
iceServers: iceServers,
90-
stdio: args[2] == "stdio",
91-
localPort: uint16(localPort),
92-
remotePort: uint16(remotePort),
102+
log: log,
103+
brokerAddr: &baseURL,
104+
token: sdk.Token(),
105+
workspace: workspace,
106+
iceServers: iceServers,
107+
stdio: args[2] == "stdio",
108+
localPort: uint16(localPort),
109+
remotePort: uint16(remotePort),
93110
}
94111

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

107124
type tunnneler struct {
108-
log slog.Logger
109-
brokerAddr *url.URL
110-
token string
111-
workspaceID string
112-
iceServers []webrtc.ICEServer
113-
remotePort uint16
114-
localPort uint16
115-
stdio bool
125+
log slog.Logger
126+
brokerAddr *url.URL
127+
token string
128+
workspace *coder.Workspace
129+
iceServers []webrtc.ICEServer
130+
remotePort uint16
131+
localPort uint16
132+
stdio bool
116133
}
117134

118135
func (c *tunnneler) start(ctx context.Context) error {
@@ -121,7 +138,7 @@ func (c *tunnneler) start(ctx context.Context) error {
121138
dialLog := c.log.Named("wsnet")
122139
wd, err := wsnet.DialWebsocket(
123140
ctx,
124-
wsnet.ConnectEndpoint(c.brokerAddr, c.workspaceID, c.token),
141+
wsnet.ConnectEndpoint(c.brokerAddr, c.workspace.ID, c.token),
125142
&wsnet.DialOptions{
126143
Log: &dialLog,
127144
TURNProxyAuthToken: c.token,
@@ -156,7 +173,7 @@ func (c *tunnneler) start(ctx context.Context) error {
156173
return
157174
case <-ticker.C:
158175
// silently ignore failures so we don't spam the console
159-
_ = sdk.UpdateLastConnectionAt(ctx, c.workspaceID)
176+
_ = sdk.UpdateLastConnectionAt(ctx, c.workspace.ID)
160177
}
161178
}
162179
}()
@@ -203,3 +220,78 @@ func (c *tunnneler) start(ctx context.Context) error {
203220
}()
204221
}
205222
}
223+
224+
// Used to treat stdio like a connection for proxying SSH.
225+
type stdioConn struct{}
226+
227+
func (s *stdioConn) Read(b []byte) (n int, err error) {
228+
return os.Stdin.Read(b)
229+
}
230+
231+
func (s *stdioConn) Write(b []byte) (n int, err error) {
232+
return os.Stdout.Write(b)
233+
}
234+
235+
func (s *stdioConn) Close() error {
236+
return nil
237+
}
238+
239+
func (s *stdioConn) LocalAddr() net.Addr {
240+
return nil
241+
}
242+
243+
func (s *stdioConn) RemoteAddr() net.Addr {
244+
return nil
245+
}
246+
247+
func (s *stdioConn) SetDeadline(t time.Time) error {
248+
return nil
249+
}
250+
251+
func (s *stdioConn) SetReadDeadline(t time.Time) error {
252+
return nil
253+
}
254+
255+
func (s *stdioConn) SetWriteDeadline(t time.Time) error {
256+
return nil
257+
}
258+
259+
// discardSSHConnection accepts a connection then outputs the message provided
260+
// to any channel opened, immediately closing the connection afterwards.
261+
//
262+
// Used to provide status to connecting clients while still aligning with the
263+
// native SSH protocol.
264+
func discardSSHConnection(nc net.Conn, privateKey string, msg string) error {
265+
config := &ssh.ServerConfig{
266+
NoClientAuth: true,
267+
}
268+
key, err := ssh.ParseRawPrivateKey([]byte(privateKey))
269+
if err != nil {
270+
return fmt.Errorf("parse private key: %w", err)
271+
}
272+
signer, err := ssh.NewSignerFromKey(key)
273+
if err != nil {
274+
return fmt.Errorf("signer from private key: %w", err)
275+
}
276+
config.AddHostKey(signer)
277+
conn, chans, reqs, err := ssh.NewServerConn(nc, config)
278+
if err != nil {
279+
return fmt.Errorf("create server conn: %w", err)
280+
}
281+
go ssh.DiscardRequests(reqs)
282+
ch, req, err := (<-chans).Accept()
283+
if err != nil {
284+
return fmt.Errorf("accept channel: %w", err)
285+
}
286+
go ssh.DiscardRequests(req)
287+
288+
_, err = ch.Write([]byte(msg))
289+
if err != nil {
290+
return fmt.Errorf("write channel: %w", err)
291+
}
292+
err = ch.Close()
293+
if err != nil {
294+
return fmt.Errorf("close channel: %w", err)
295+
}
296+
return conn.Close()
297+
}

0 commit comments

Comments
 (0)