1
1
package agent
2
2
3
3
import (
4
+ "bufio"
4
5
"context"
5
6
"crypto/rand"
6
7
"crypto/rsa"
@@ -32,6 +33,7 @@ import (
32
33
"golang.org/x/xerrors"
33
34
"tailscale.com/net/speedtest"
34
35
"tailscale.com/tailcfg"
36
+ "tailscale.com/types/netlogtype"
35
37
36
38
"cdr.dev/slog"
37
39
"github.com/coder/coder/agent/usershell"
@@ -98,7 +100,6 @@ func New(options Options) io.Closer {
98
100
exchangeToken : options .ExchangeToken ,
99
101
filesystem : options .Filesystem ,
100
102
tempDir : options .TempDir ,
101
- stats : & Stats {},
102
103
}
103
104
server .init (ctx )
104
105
return server
@@ -126,7 +127,6 @@ type agent struct {
126
127
sshServer * ssh.Server
127
128
128
129
network * tailnet.Conn
129
- stats * Stats
130
130
}
131
131
132
132
// runLoop attempts to start the agent in a retry loop.
@@ -238,22 +238,16 @@ func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) (*t
238
238
return nil , xerrors .New ("closed" )
239
239
}
240
240
network , err := tailnet .NewConn (& tailnet.Options {
241
- Addresses : []netip.Prefix {netip .PrefixFrom (codersdk .TailnetIP , 128 )},
242
- DERPMap : derpMap ,
243
- Logger : a .logger .Named ("tailnet" ),
241
+ Addresses : []netip.Prefix {netip .PrefixFrom (codersdk .TailnetIP , 128 )},
242
+ DERPMap : derpMap ,
243
+ Logger : a .logger .Named ("tailnet" ),
244
+ EnableTrafficStats : true ,
244
245
})
245
246
if err != nil {
246
247
a .closeMutex .Unlock ()
247
248
return nil , xerrors .Errorf ("create tailnet: %w" , err )
248
249
}
249
250
a .network = network
250
- network .SetForwardTCPCallback (func (conn net.Conn , listenerExists bool ) net.Conn {
251
- if listenerExists {
252
- // If a listener already exists, we would double-wrap the conn.
253
- return conn
254
- }
255
- return a .stats .wrapConn (conn )
256
- })
257
251
a .connCloseWait .Add (4 )
258
252
a .closeMutex .Unlock ()
259
253
@@ -268,7 +262,7 @@ func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) (*t
268
262
if err != nil {
269
263
return
270
264
}
271
- go a .sshServer .HandleConn (a . stats . wrapConn ( conn ) )
265
+ go a .sshServer .HandleConn (conn )
272
266
}
273
267
}()
274
268
@@ -284,7 +278,6 @@ func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) (*t
284
278
a .logger .Debug (ctx , "accept pty failed" , slog .Error (err ))
285
279
return
286
280
}
287
- conn = a .stats .wrapConn (conn )
288
281
// This cannot use a JSON decoder, since that can
289
282
// buffer additional data that is required for the PTY.
290
283
rawLen := make ([]byte , 2 )
@@ -487,12 +480,11 @@ func (a *agent) init(ctx context.Context) {
487
480
var opts []sftp.ServerOption
488
481
// Change current working directory to the users home
489
482
// directory so that SFTP connections land there.
490
- // https://github.com/coder/coder/issues/3620
491
- u , err := user .Current ()
483
+ homedir , err := userHomeDir ()
492
484
if err != nil {
493
- sshLogger .Warn (ctx , "get sftp working directory failed, unable to get current user " , slog .Error (err ))
485
+ sshLogger .Warn (ctx , "get sftp working directory failed, unable to get home dir " , slog .Error (err ))
494
486
} else {
495
- opts = append (opts , sftp .WithServerWorkingDirectory (u . HomeDir ))
487
+ opts = append (opts , sftp .WithServerWorkingDirectory (homedir ))
496
488
}
497
489
498
490
server , err := sftp .NewServer (session , opts ... )
@@ -523,7 +515,13 @@ func (a *agent) init(ctx context.Context) {
523
515
524
516
go a .runLoop (ctx )
525
517
cl , err := a .client .AgentReportStats (ctx , a .logger , func () * codersdk.AgentStats {
526
- return a .stats .Copy ()
518
+ stats := map [netlogtype.Connection ]netlogtype.Counts {}
519
+ a .closeMutex .Lock ()
520
+ if a .network != nil {
521
+ stats = a .network .ExtractTrafficStats ()
522
+ }
523
+ a .closeMutex .Unlock ()
524
+ return convertAgentStats (stats )
527
525
})
528
526
if err != nil {
529
527
a .logger .Error (ctx , "report stats" , slog .Error (err ))
@@ -537,6 +535,23 @@ func (a *agent) init(ctx context.Context) {
537
535
}()
538
536
}
539
537
538
+ func convertAgentStats (counts map [netlogtype.Connection ]netlogtype.Counts ) * codersdk.AgentStats {
539
+ stats := & codersdk.AgentStats {
540
+ ConnsByProto : map [string ]int64 {},
541
+ NumConns : int64 (len (counts )),
542
+ }
543
+
544
+ for conn , count := range counts {
545
+ stats .ConnsByProto [conn .Proto .String ()]++
546
+ stats .RxPackets += int64 (count .RxPackets )
547
+ stats .RxBytes += int64 (count .RxBytes )
548
+ stats .TxPackets += int64 (count .TxPackets )
549
+ stats .TxBytes += int64 (count .TxBytes )
550
+ }
551
+
552
+ return stats
553
+ }
554
+
540
555
// createCommand processes raw command input with OpenSSH-like behavior.
541
556
// If the rawCommand provided is empty, it will default to the users shell.
542
557
// This injects environment variables specified by the user at launch too.
@@ -583,8 +598,12 @@ func (a *agent) createCommand(ctx context.Context, rawCommand string, env []stri
583
598
cmd := exec .CommandContext (ctx , shell , args ... )
584
599
cmd .Dir = metadata .Directory
585
600
if cmd .Dir == "" {
586
- // Default to $HOME if a directory is not set!
587
- cmd .Dir = os .Getenv ("HOME" )
601
+ // Default to user home if a directory is not set.
602
+ homedir , err := userHomeDir ()
603
+ if err != nil {
604
+ return nil , xerrors .Errorf ("get home dir: %w" , err )
605
+ }
606
+ cmd .Dir = homedir
588
607
}
589
608
cmd .Env = append (os .Environ (), env ... )
590
609
executablePath , err := os .Executable ()
@@ -660,6 +679,18 @@ func (a *agent) handleSSHSession(session ssh.Session) (retErr error) {
660
679
// See https://github.com/coder/coder/issues/3371.
661
680
session .DisablePTYEmulation ()
662
681
682
+ if ! isQuietLogin (session .RawCommand ()) {
683
+ metadata , ok := a .metadata .Load ().(codersdk.WorkspaceAgentMetadata )
684
+ if ok {
685
+ err = showMOTD (session , metadata .MOTDFile )
686
+ if err != nil {
687
+ a .logger .Error (ctx , "show MOTD" , slog .Error (err ))
688
+ }
689
+ } else {
690
+ a .logger .Warn (ctx , "metadata lookup failed, unable to show MOTD" )
691
+ }
692
+ }
693
+
663
694
cmd .Env = append (cmd .Env , fmt .Sprintf ("TERM=%s" , sshPty .Term ))
664
695
665
696
// The pty package sets `SSH_TTY` on supported platforms.
@@ -985,19 +1016,74 @@ func Bicopy(ctx context.Context, c1, c2 io.ReadWriteCloser) {
985
1016
}
986
1017
}
987
1018
988
- // ExpandRelativeHomePath expands the tilde at the beginning of a path to the
989
- // current user's home directory and returns a full absolute path.
990
- func ExpandRelativeHomePath (in string ) (string , error ) {
991
- usr , err := user .Current ()
1019
+ // isQuietLogin checks if the SSH server should perform a quiet login or not.
1020
+ //
1021
+ // https://github.com/openssh/openssh-portable/blob/25bd659cc72268f2858c5415740c442ee950049f/session.c#L816
1022
+ func isQuietLogin (rawCommand string ) bool {
1023
+ // We are always quiet unless this is a login shell.
1024
+ if len (rawCommand ) != 0 {
1025
+ return true
1026
+ }
1027
+
1028
+ // Best effort, if we can't get the home directory,
1029
+ // we can't lookup .hushlogin.
1030
+ homedir , err := userHomeDir ()
992
1031
if err != nil {
993
- return "" , xerrors .Errorf ("get current user details: %w" , err )
1032
+ return false
1033
+ }
1034
+
1035
+ _ , err = os .Stat (filepath .Join (homedir , ".hushlogin" ))
1036
+ return err == nil
1037
+ }
1038
+
1039
+ // showMOTD will output the message of the day from
1040
+ // the given filename to dest, if the file exists.
1041
+ //
1042
+ // https://github.com/openssh/openssh-portable/blob/25bd659cc72268f2858c5415740c442ee950049f/session.c#L784
1043
+ func showMOTD (dest io.Writer , filename string ) error {
1044
+ if filename == "" {
1045
+ return nil
1046
+ }
1047
+
1048
+ f , err := os .Open (filename )
1049
+ if err != nil {
1050
+ if xerrors .Is (err , os .ErrNotExist ) {
1051
+ // This is not an error, there simply isn't a MOTD to show.
1052
+ return nil
1053
+ }
1054
+ return xerrors .Errorf ("open MOTD: %w" , err )
994
1055
}
1056
+ defer f .Close ()
995
1057
996
- if in == "~" {
997
- in = usr .HomeDir
998
- } else if strings .HasPrefix (in , "~/" ) {
999
- in = filepath .Join (usr .HomeDir , in [2 :])
1058
+ s := bufio .NewScanner (f )
1059
+ for s .Scan () {
1060
+ // Carriage return ensures each line starts
1061
+ // at the beginning of the terminal.
1062
+ _ , err = fmt .Fprint (dest , s .Text ()+ "\r \n " )
1063
+ if err != nil {
1064
+ return xerrors .Errorf ("write MOTD: %w" , err )
1065
+ }
1066
+ }
1067
+ if err := s .Err (); err != nil {
1068
+ return xerrors .Errorf ("read MOTD: %w" , err )
1069
+ }
1070
+
1071
+ return nil
1072
+ }
1073
+
1074
+ // userHomeDir returns the home directory of the current user, giving
1075
+ // priority to the $HOME environment variable.
1076
+ func userHomeDir () (string , error ) {
1077
+ // First we check the environment.
1078
+ homedir , err := os .UserHomeDir ()
1079
+ if err == nil {
1080
+ return homedir , nil
1000
1081
}
1001
1082
1002
- return filepath .Abs (in )
1083
+ // As a fallback, we try the user information.
1084
+ u , err := user .Current ()
1085
+ if err != nil {
1086
+ return "" , xerrors .Errorf ("current user: %w" , err )
1087
+ }
1088
+ return u .HomeDir , nil
1003
1089
}
0 commit comments