88 "fmt"
99 "io"
1010 "log"
11+ "net"
1112 "net/http"
1213 "net/url"
1314 "os"
@@ -66,6 +67,7 @@ func (r *RootCmd) ssh() *serpent.Command {
6667 stdio bool
6768 hostPrefix string
6869 hostnameSuffix string
70+ forceNewTunnel bool
6971 forwardAgent bool
7072 forwardGPG bool
7173 identityAgent string
@@ -85,6 +87,7 @@ func (r *RootCmd) ssh() *serpent.Command {
8587 containerUser string
8688 )
8789 client := new (codersdk.Client )
90+ wsClient := workspacesdk .New (client )
8891 cmd := & serpent.Command {
8992 Annotations : workspaceCommand ,
9093 Use : "ssh <workspace>" ,
@@ -203,14 +206,14 @@ func (r *RootCmd) ssh() *serpent.Command {
203206 parsedEnv = append (parsedEnv , [2 ]string {k , v })
204207 }
205208
206- deploymentSSHConfig := codersdk.SSHConfigResponse {
209+ cliConfig := codersdk.SSHConfigResponse {
207210 HostnamePrefix : hostPrefix ,
208211 HostnameSuffix : hostnameSuffix ,
209212 }
210213
211214 workspace , workspaceAgent , err := findWorkspaceAndAgentByHostname (
212215 ctx , inv , client ,
213- inv .Args [0 ], deploymentSSHConfig , disableAutostart )
216+ inv .Args [0 ], cliConfig , disableAutostart )
214217 if err != nil {
215218 return err
216219 }
@@ -275,10 +278,44 @@ func (r *RootCmd) ssh() *serpent.Command {
275278 return err
276279 }
277280
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+
278315 if r .disableDirect {
279316 _ , _ = fmt .Fprintln (inv .Stderr , "Direct connections disabled." )
280317 }
281- conn , err := workspacesdk . New ( client ) .
318+ conn , err := wsClient .
282319 DialAgent (ctx , workspaceAgent .ID , & workspacesdk.DialAgentOptions {
283320 Logger : logger ,
284321 BlockEndpoints : r .disableDirect ,
@@ -660,6 +697,12 @@ func (r *RootCmd) ssh() *serpent.Command {
660697 Value : serpent .StringOf (& containerUser ),
661698 Hidden : true , // Hidden until this features is at least in beta.
662699 },
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+ },
663706 sshDisableAutostartOption (serpent .BoolOf (& disableAutostart )),
664707 }
665708 return cmd
@@ -1372,12 +1415,13 @@ func setStatsCallback(
13721415}
13731416
13741417type 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"`
13811425}
13821426
13831427func 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,
14481492 }, nil
14491493}
14501494
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+
14511565// Converts workspace name input to owner/workspace.agent format
14521566// Possible valid input formats:
14531567// workspace
0 commit comments