8
8
"fmt"
9
9
"io"
10
10
"log"
11
+ "net"
11
12
"net/http"
12
13
"net/url"
13
14
"os"
@@ -66,6 +67,7 @@ func (r *RootCmd) ssh() *serpent.Command {
66
67
stdio bool
67
68
hostPrefix string
68
69
hostnameSuffix string
70
+ forceNewTunnel bool
69
71
forwardAgent bool
70
72
forwardGPG bool
71
73
identityAgent string
@@ -85,6 +87,7 @@ func (r *RootCmd) ssh() *serpent.Command {
85
87
containerUser string
86
88
)
87
89
client := new (codersdk.Client )
90
+ wsClient := workspacesdk .New (client )
88
91
cmd := & serpent.Command {
89
92
Annotations : workspaceCommand ,
90
93
Use : "ssh <workspace>" ,
@@ -203,14 +206,14 @@ func (r *RootCmd) ssh() *serpent.Command {
203
206
parsedEnv = append (parsedEnv , [2 ]string {k , v })
204
207
}
205
208
206
- deploymentSSHConfig := codersdk.SSHConfigResponse {
209
+ cliConfig := codersdk.SSHConfigResponse {
207
210
HostnamePrefix : hostPrefix ,
208
211
HostnameSuffix : hostnameSuffix ,
209
212
}
210
213
211
214
workspace , workspaceAgent , err := findWorkspaceAndAgentByHostname (
212
215
ctx , inv , client ,
213
- inv .Args [0 ], deploymentSSHConfig , disableAutostart )
216
+ inv .Args [0 ], cliConfig , disableAutostart )
214
217
if err != nil {
215
218
return err
216
219
}
@@ -275,10 +278,44 @@ func (r *RootCmd) ssh() *serpent.Command {
275
278
return err
276
279
}
277
280
281
+ // If we're in stdio mode, check to see if we can use Coder Connect.
282
+ // We don't support Coder Connect over non-stdio coder ssh yet.
283
+ if stdio && ! forceNewTunnel {
284
+ connInfo , err := wsClient .AgentConnectionInfoGeneric (ctx )
285
+ if err != nil {
286
+ return xerrors .Errorf ("get agent connection info: %w" , err )
287
+ }
288
+ coderConnectHost := fmt .Sprintf ("%s.%s.%s.%s" ,
289
+ workspaceAgent .Name , workspace .Name , workspace .OwnerName , connInfo .HostnameSuffix )
290
+ exists , _ := workspacesdk .ExistsViaCoderConnect (ctx , coderConnectHost )
291
+ if exists {
292
+ defer cancel ()
293
+
294
+ if networkInfoDir != "" {
295
+ if err := writeCoderConnectNetInfo (ctx , networkInfoDir ); err != nil {
296
+ logger .Error (ctx , "failed to write coder connect net info file" , slog .Error (err ))
297
+ }
298
+ }
299
+
300
+ stopPolling := tryPollWorkspaceAutostop (ctx , client , workspace )
301
+ defer stopPolling ()
302
+
303
+ usageAppName := getUsageAppName (usageApp )
304
+ if usageAppName != "" {
305
+ closeUsage := client .UpdateWorkspaceUsageWithBodyContext (ctx , workspace .ID , codersdk.PostWorkspaceUsageRequest {
306
+ AgentID : workspaceAgent .ID ,
307
+ AppName : usageAppName ,
308
+ })
309
+ defer closeUsage ()
310
+ }
311
+ return runCoderConnectStdio (ctx , fmt .Sprintf ("%s:22" , coderConnectHost ), stdioReader , stdioWriter , stack )
312
+ }
313
+ }
314
+
278
315
if r .disableDirect {
279
316
_ , _ = fmt .Fprintln (inv .Stderr , "Direct connections disabled." )
280
317
}
281
- conn , err := workspacesdk . New ( client ) .
318
+ conn , err := wsClient .
282
319
DialAgent (ctx , workspaceAgent .ID , & workspacesdk.DialAgentOptions {
283
320
Logger : logger ,
284
321
BlockEndpoints : r .disableDirect ,
@@ -660,6 +697,12 @@ func (r *RootCmd) ssh() *serpent.Command {
660
697
Value : serpent .StringOf (& containerUser ),
661
698
Hidden : true , // Hidden until this features is at least in beta.
662
699
},
700
+ {
701
+ Flag : "force-new-tunnel" ,
702
+ Description : "Force the creation of a new tunnel to the workspace, even if the Coder Connect tunnel is available." ,
703
+ Value : serpent .BoolOf (& forceNewTunnel ),
704
+ Hidden : true ,
705
+ },
663
706
sshDisableAutostartOption (serpent .BoolOf (& disableAutostart )),
664
707
}
665
708
return cmd
@@ -1372,12 +1415,13 @@ func setStatsCallback(
1372
1415
}
1373
1416
1374
1417
type sshNetworkStats struct {
1375
- P2P bool `json:"p2p"`
1376
- Latency float64 `json:"latency"`
1377
- PreferredDERP string `json:"preferred_derp"`
1378
- DERPLatency map [string ]float64 `json:"derp_latency"`
1379
- UploadBytesSec int64 `json:"upload_bytes_sec"`
1380
- DownloadBytesSec int64 `json:"download_bytes_sec"`
1418
+ P2P bool `json:"p2p"`
1419
+ Latency float64 `json:"latency"`
1420
+ PreferredDERP string `json:"preferred_derp"`
1421
+ DERPLatency map [string ]float64 `json:"derp_latency"`
1422
+ UploadBytesSec int64 `json:"upload_bytes_sec"`
1423
+ DownloadBytesSec int64 `json:"download_bytes_sec"`
1424
+ UsingCoderConnect bool `json:"using_coder_connect"`
1381
1425
}
1382
1426
1383
1427
func collectNetworkStats (ctx context.Context , agentConn * workspacesdk.AgentConn , start , end time.Time , counts map [netlogtype.Connection ]netlogtype.Counts ) (* sshNetworkStats , error ) {
@@ -1448,6 +1492,76 @@ func collectNetworkStats(ctx context.Context, agentConn *workspacesdk.AgentConn,
1448
1492
}, nil
1449
1493
}
1450
1494
1495
+ type coderConnectDialerContextKey struct {}
1496
+
1497
+ type coderConnectDialer interface {
1498
+ DialContext (ctx context.Context , network , addr string ) (net.Conn , error )
1499
+ }
1500
+
1501
+ func WithTestOnlyCoderConnectDialer (ctx context.Context , dialer coderConnectDialer ) context.Context {
1502
+ return context .WithValue (ctx , coderConnectDialerContextKey {}, dialer )
1503
+ }
1504
+
1505
+ func testOrDefaultDialer (ctx context.Context ) coderConnectDialer {
1506
+ dialer , ok := ctx .Value (coderConnectDialerContextKey {}).(coderConnectDialer )
1507
+ if ! ok || dialer == nil {
1508
+ return & net.Dialer {}
1509
+ }
1510
+ return dialer
1511
+ }
1512
+
1513
+ func runCoderConnectStdio (ctx context.Context , addr string , stdin io.Reader , stdout io.Writer , stack * closerStack ) error {
1514
+ dialer := testOrDefaultDialer (ctx )
1515
+ conn , err := dialer .DialContext (ctx , "tcp" , addr )
1516
+ if err != nil {
1517
+ return xerrors .Errorf ("dial coder connect host: %w" , err )
1518
+ }
1519
+ if err := stack .push ("tcp conn" , conn ); err != nil {
1520
+ return err
1521
+ }
1522
+
1523
+ agentssh .Bicopy (ctx , conn , & StdioRwc {
1524
+ Reader : stdin ,
1525
+ Writer : stdout ,
1526
+ })
1527
+
1528
+ return nil
1529
+ }
1530
+
1531
+ type StdioRwc struct {
1532
+ io.Reader
1533
+ io.Writer
1534
+ }
1535
+
1536
+ func (* StdioRwc ) Close () error {
1537
+ return nil
1538
+ }
1539
+
1540
+ func writeCoderConnectNetInfo (ctx context.Context , networkInfoDir string ) error {
1541
+ fs , ok := ctx .Value ("fs" ).(afero.Fs )
1542
+ if ! ok {
1543
+ fs = afero .NewOsFs ()
1544
+ }
1545
+ // The VS Code extension obtains the PID of the SSH process to
1546
+ // find the log file associated with a SSH session.
1547
+ //
1548
+ // We get the parent PID because it's assumed `ssh` is calling this
1549
+ // command via the ProxyCommand SSH option.
1550
+ networkInfoFilePath := filepath .Join (networkInfoDir , fmt .Sprintf ("%d.json" , os .Getppid ()))
1551
+ stats := & sshNetworkStats {
1552
+ UsingCoderConnect : true ,
1553
+ }
1554
+ rawStats , err := json .Marshal (stats )
1555
+ if err != nil {
1556
+ return xerrors .Errorf ("marshal network stats: %w" , err )
1557
+ }
1558
+ err = afero .WriteFile (fs , networkInfoFilePath , rawStats , 0o600 )
1559
+ if err != nil {
1560
+ return xerrors .Errorf ("write network stats: %w" , err )
1561
+ }
1562
+ return nil
1563
+ }
1564
+
1451
1565
// Converts workspace name input to owner/workspace.agent format
1452
1566
// Possible valid input formats:
1453
1567
// workspace
0 commit comments