@@ -170,10 +170,20 @@ func configSSH() *cobra.Command {
170
170
// that it's possible to capture the diff.
171
171
out = cmd .OutOrStderr ()
172
172
}
173
- binaryFile , err := currentBinPath (out )
173
+ coderBinary , err := currentBinPath (out )
174
174
if err != nil {
175
175
return err
176
176
}
177
+ escapedCoderBinary , err := sshConfigExecEscape (coderBinary )
178
+ if err != nil {
179
+ return xerrors .Errorf ("escape coder binary for ssh failed: %w" , err )
180
+ }
181
+
182
+ root := createConfig (cmd )
183
+ escapedGlobalConfig , err := sshConfigExecEscape (string (root ))
184
+ if err != nil {
185
+ return xerrors .Errorf ("escape global config for ssh failed: %w" , err )
186
+ }
177
187
178
188
homedir , err := os .UserHomeDir ()
179
189
if err != nil {
@@ -238,7 +248,6 @@ func configSSH() *cobra.Command {
238
248
}
239
249
240
250
configModified := configRaw
241
- root := createConfig (cmd )
242
251
243
252
buf := & bytes.Buffer {}
244
253
before , after := sshConfigSplitOnCoderSection (configModified )
@@ -280,11 +289,17 @@ func configSSH() *cobra.Command {
280
289
"\t LogLevel ERROR" ,
281
290
)
282
291
if ! skipProxyCommand {
283
- if ! wireguard {
284
- configOptions = append (configOptions , fmt .Sprintf ("\t ProxyCommand %q --global-config %q ssh --stdio %s" , binaryFile , root , hostname ))
285
- } else {
286
- configOptions = append (configOptions , fmt .Sprintf ("\t ProxyCommand %q --global-config %q ssh --wireguard --stdio %s" , binaryFile , root , hostname ))
292
+ wgArg := ""
293
+ if wireguard {
294
+ wgArg = "--wireguard "
287
295
}
296
+ configOptions = append (
297
+ configOptions ,
298
+ fmt .Sprintf (
299
+ "\t ProxyCommand %s --global-config %s ssh %s--stdio %s" ,
300
+ escapedCoderBinary , escapedGlobalConfig , wgArg , hostname ,
301
+ ),
302
+ )
288
303
}
289
304
290
305
_ , _ = buf .WriteString (strings .Join (configOptions , "\n " ))
@@ -482,6 +497,50 @@ func writeWithTempFileAndMove(path string, r io.Reader) (err error) {
482
497
return nil
483
498
}
484
499
500
+ // sshConfigExecEscape quotes the string if it contains spaces, as per
501
+ // `man 5 ssh_config`. However, OpenSSH uses exec in the users shell to
502
+ // run the command, and as such the formatting/escape requirements
503
+ // cannot simply be covered by `fmt.Sprintf("%q", path)`.
504
+ //
505
+ // Always escaping the path with `fmt.Sprintf("%q", path)` usually works
506
+ // on most platforms, but double quotes sometimes break on Windows 10
507
+ // (see #2853). This function takes a best-effort approach to improving
508
+ // compatibility and covering edge cases.
509
+ //
510
+ // Given the following ProxyCommand:
511
+ //
512
+ // ProxyCommand "/path/with space/coder" ssh --stdio work
513
+ //
514
+ // This is ~what OpenSSH would execute:
515
+ //
516
+ // /bin/bash -c '"/path/with space/to/coder" ssh --stdio workspace'
517
+ //
518
+ // However, since it's actually an arg in C, the contents inside the
519
+ // single quotes are interpreted as is, e.g. if there was a '\t', it
520
+ // would be the literal string '\t', not a tab.
521
+ //
522
+ // See:
523
+ // - https://github.com/coder/coder/issues/2853
524
+ // - https://github.com/openssh/openssh-portable/blob/V_9_0_P1/sshconnect.c#L158-L167
525
+ func sshConfigExecEscape (path string ) (string , error ) {
526
+ // This is unlikely to ever happen, but newlines are allowed on
527
+ // certain filesystems, but cannot be used inside ssh config.
528
+ if strings .ContainsAny (path , "\n " ) {
529
+ return "" , xerrors .Errorf ("invalid path: %s" , path )
530
+ }
531
+ // In the unlikely even that a path contains quotes, they must be
532
+ // escaped so that they are not interpreted as shell quotes.
533
+ if strings .Contains (path , "\" " ) {
534
+ path = strings .ReplaceAll (path , "\" " , "\\ \" " )
535
+ }
536
+ // A space or a tab requires quoting, but tabs must not be escaped
537
+ // (\t) since OpenSSH interprets it as a literal \t, not a tab.
538
+ if strings .ContainsAny (path , " \t " ) {
539
+ path = fmt .Sprintf ("\" %s\" " , path )
540
+ }
541
+ return path , nil
542
+ }
543
+
485
544
// currentBinPath returns the path to the coder binary suitable for use in ssh
486
545
// ProxyCommand.
487
546
func currentBinPath (w io.Writer ) (string , error ) {
0 commit comments