session.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. package sshd
  2. import (
  3. "errors"
  4. "log/slog"
  5. "sort"
  6. "strings"
  7. "github.com/slackhq/nebula/commands"
  8. "golang.org/x/crypto/ssh"
  9. "golang.org/x/term"
  10. )
  11. type session struct {
  12. l *slog.Logger
  13. c *ssh.ServerConn
  14. term *term.Terminal
  15. commands *commands.Registry
  16. cancel func()
  17. }
  18. func NewSession(reg *commands.Registry, conn *ssh.ServerConn, chans <-chan ssh.NewChannel, cancel func(), l *slog.Logger) *session {
  19. s := &session{
  20. commands: reg.Clone(),
  21. l: l,
  22. c: conn,
  23. cancel: cancel,
  24. }
  25. s.commands.DefaultFormat = DefaultFormat
  26. s.commands.Register(LogoutCommand(s.Close))
  27. go s.handleChannels(chans)
  28. return s
  29. }
  30. // LogoutCommand is the logout command a session registers on its clone of the registry,
  31. // with end as what it does. Exported so a test can serve what a session serves.
  32. func LogoutCommand(end func()) *commands.Command {
  33. return &commands.Command{
  34. Name: commands.Logout,
  35. ShortDescription: "Ends the current session",
  36. Run: func(a any, args []string) (commands.Result, error) {
  37. end()
  38. return commands.Nothing(), nil
  39. },
  40. }
  41. }
  42. // DefaultFormat is what an ssh session renders when the operator gave neither -json nor
  43. // -pretty. It encodes what every command printed before rendering was shared: text
  44. // where a command has a human form, and compact JSON for the ones that never did
  45. // (print-tunnel, print-relays, query-lighthouse). It is ssh's policy, exported so a test
  46. // can dispatch through the same choice, and ssh's to change.
  47. func DefaultFormat(c *commands.Command, r commands.Result) commands.Format {
  48. if !c.JSONOnly {
  49. return commands.FormatText
  50. }
  51. // Backwards compatibility. The JSON-only commands always printed their refusals as
  52. // a plain sentence ("No vpn address was provided") even though their data is JSON,
  53. // so an outcome keeps rendering as text here. Drop this branch to make ssh's default
  54. // apply uniformly; the output golden will show exactly which lines change.
  55. if r.Value == nil {
  56. return commands.FormatText
  57. }
  58. return commands.FormatJSON
  59. }
  60. func (s *session) handleChannels(chans <-chan ssh.NewChannel) {
  61. defer s.Close()
  62. for newChannel := range chans {
  63. if newChannel.ChannelType() != "session" {
  64. s.l.Error("unknown channel type", "sshChannelType", newChannel.ChannelType())
  65. newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
  66. continue
  67. }
  68. channel, requests, err := newChannel.Accept()
  69. if err != nil {
  70. s.l.Warn("could not accept channel", "error", err)
  71. continue
  72. }
  73. go s.handleRequests(requests, channel)
  74. }
  75. }
  76. func (s *session) handleRequests(in <-chan *ssh.Request, channel ssh.Channel) {
  77. for req := range in {
  78. var err error
  79. switch req.Type {
  80. case "shell":
  81. if s.term == nil {
  82. s.term = s.createTerm(channel)
  83. err = req.Reply(true, nil)
  84. } else {
  85. err = req.Reply(false, nil)
  86. }
  87. case "pty-req":
  88. err = req.Reply(true, nil)
  89. case "window-change":
  90. err = req.Reply(true, nil)
  91. case "exec":
  92. var payload = struct{ Value string }{}
  93. cErr := ssh.Unmarshal(req.Payload, &payload)
  94. if cErr != nil {
  95. req.Reply(false, nil)
  96. return
  97. }
  98. req.Reply(true, nil)
  99. err := s.dispatchCommand(payload.Value, commands.MergedOutput(channel))
  100. // A command that failed exits 1, so `ssh host cmd` callers can tell.
  101. status := struct{ Status uint32 }{uint32(0)}
  102. if err != nil {
  103. status.Status = 1
  104. }
  105. channel.SendRequest("exit-status", false, ssh.Marshal(status))
  106. channel.Close()
  107. return
  108. default:
  109. s.l.Debug("Rejected unknown request", "sshRequest", req.Type)
  110. err = req.Reply(false, nil)
  111. }
  112. if err != nil {
  113. s.l.Info("Error handling ssh session requests", "error", err)
  114. return
  115. }
  116. }
  117. }
  118. func (s *session) createTerm(channel ssh.Channel) *term.Terminal {
  119. term := term.NewTerminal(channel, s.c.User()+"@nebula > ")
  120. term.AutoCompleteCallback = func(line string, pos int, key rune) (newLine string, newPos int, ok bool) {
  121. // key 9 is tab
  122. if key == 9 {
  123. cmds := s.commands.Match(line)
  124. if len(cmds) == 1 {
  125. return cmds[0] + " ", len(cmds[0]) + 1, true
  126. }
  127. sort.Strings(cmds)
  128. term.Write([]byte(strings.Join(cmds, "\n") + "\n\n"))
  129. }
  130. return "", 0, false
  131. }
  132. go s.handleInput()
  133. return term
  134. }
  135. func (s *session) handleInput() {
  136. w := commands.MergedOutput(s.term)
  137. for {
  138. line, err := s.term.ReadLine()
  139. if err != nil {
  140. break
  141. }
  142. s.dispatchCommand(line, w)
  143. }
  144. }
  145. // dispatchCommand runs line and returns the command's error, which it has logged unless
  146. // the command already told the user everything: the log line is for the operator reading
  147. // the daemon's log, who learns nothing from a failure the user caused.
  148. func (s *session) dispatchCommand(line string, w commands.Output) error {
  149. err := s.commands.Dispatch(line, w)
  150. if err == nil {
  151. return nil
  152. }
  153. var f *commands.Failure
  154. if !errors.As(err, &f) {
  155. s.l.Debug("Command returned an error", "line", line, "error", err)
  156. }
  157. return err
  158. }
  159. func (s *session) Close() {
  160. s.c.Close()
  161. s.cancel()
  162. }