server.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. package sshd
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "fmt"
  7. "log/slog"
  8. "net"
  9. "sync"
  10. "github.com/armon/go-radix"
  11. "golang.org/x/crypto/ssh"
  12. )
  13. type SSHServer struct {
  14. config *ssh.ServerConfig
  15. l *slog.Logger
  16. certChecker *ssh.CertChecker
  17. // authLock guards trustedKeys and trustedCAs
  18. authLock sync.RWMutex
  19. // Map of user -> authorized keys
  20. trustedKeys map[string]map[string]bool
  21. trustedCAs []ssh.PublicKey
  22. // List of available commands
  23. helpCommand *Command
  24. commands *radix.Tree
  25. listener net.Listener
  26. // ctx parents per-Run contexts. Cancelling it (e.g. via Control.Stop) tears the server down even
  27. // across reloads, since each Run derives a fresh child rather than reusing this one directly.
  28. ctx context.Context
  29. }
  30. // NewSSHServer creates a new ssh server rigged with default commands and prepares to listen.
  31. // The ssh server's context is parented off the supplied ctx so cancelling it
  32. // (e.g. on Control.Stop) tears down active sessions and closes the listener.
  33. func NewSSHServer(ctx context.Context, l *slog.Logger) (*SSHServer, error) {
  34. s := &SSHServer{
  35. trustedKeys: make(map[string]map[string]bool),
  36. l: l,
  37. commands: radix.New(),
  38. ctx: ctx,
  39. }
  40. cc := ssh.CertChecker{
  41. IsUserAuthority: func(auth ssh.PublicKey) bool {
  42. s.authLock.RLock()
  43. defer s.authLock.RUnlock()
  44. for _, ca := range s.trustedCAs {
  45. if bytes.Equal(ca.Marshal(), auth.Marshal()) {
  46. return true
  47. }
  48. }
  49. return false
  50. },
  51. UserKeyFallback: func(c ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
  52. pk := string(pubKey.Marshal())
  53. fp := ssh.FingerprintSHA256(pubKey)
  54. s.authLock.RLock()
  55. defer s.authLock.RUnlock()
  56. tk, ok := s.trustedKeys[c.User()]
  57. if !ok {
  58. return nil, fmt.Errorf("unknown user %s", c.User())
  59. }
  60. _, ok = tk[pk]
  61. if !ok {
  62. return nil, fmt.Errorf("unknown public key for %s (%s)", c.User(), fp)
  63. }
  64. return &ssh.Permissions{
  65. // Record the public key used for authentication.
  66. Extensions: map[string]string{
  67. "fp": fp,
  68. "user": c.User(),
  69. },
  70. }, nil
  71. },
  72. }
  73. s.config = &ssh.ServerConfig{
  74. PublicKeyCallback: cc.Authenticate,
  75. ServerVersion: fmt.Sprintf("SSH-2.0-Nebula???"),
  76. }
  77. s.RegisterCommand(&Command{
  78. Name: "help",
  79. ShortDescription: "prints available commands or help <command> for specific usage info",
  80. Callback: func(a any, args []string, w StringWriter) error {
  81. return helpCallback(s.commands, args, w)
  82. },
  83. })
  84. return s, nil
  85. }
  86. func (s *SSHServer) SetHostKey(hostPrivateKey []byte) error {
  87. private, err := ssh.ParsePrivateKey(hostPrivateKey)
  88. if err != nil {
  89. return fmt.Errorf("failed to parse private key: %s", err)
  90. }
  91. s.config.AddHostKey(private)
  92. return nil
  93. }
  94. func (s *SSHServer) ClearTrustedCAs() {
  95. s.authLock.Lock()
  96. s.trustedCAs = []ssh.PublicKey{}
  97. s.authLock.Unlock()
  98. }
  99. func (s *SSHServer) ClearAuthorizedKeys() {
  100. s.authLock.Lock()
  101. s.trustedKeys = make(map[string]map[string]bool)
  102. s.authLock.Unlock()
  103. }
  104. // AddTrustedCA adds a trusted CA for user certificates
  105. func (s *SSHServer) AddTrustedCA(pubKey string) error {
  106. pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubKey))
  107. if err != nil {
  108. return err
  109. }
  110. s.authLock.Lock()
  111. s.trustedCAs = append(s.trustedCAs, pk)
  112. s.authLock.Unlock()
  113. s.l.Info("Trusted CA key", "sshKey", pubKey)
  114. return nil
  115. }
  116. // AddAuthorizedKey adds an ssh public key for a user
  117. func (s *SSHServer) AddAuthorizedKey(user, pubKey string) error {
  118. pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubKey))
  119. if err != nil {
  120. return err
  121. }
  122. s.authLock.Lock()
  123. tk, ok := s.trustedKeys[user]
  124. if !ok {
  125. tk = make(map[string]bool)
  126. s.trustedKeys[user] = tk
  127. }
  128. tk[string(pk.Marshal())] = true
  129. s.authLock.Unlock()
  130. s.l.Info("Authorized ssh key",
  131. "sshKey", pubKey,
  132. "sshUser", user,
  133. )
  134. return nil
  135. }
  136. // RegisterCommand adds a command that can be run by a user, by default only `help` is available
  137. func (s *SSHServer) RegisterCommand(c *Command) {
  138. s.commands.Insert(c.Name, c)
  139. }
  140. // Run begins listening and accepting connections. Each invocation derives a fresh per-Run context
  141. // from the constructor-supplied ctx so a Stop+Run sequence (used by config reload) starts clean
  142. // rather than carrying a permanently-cancelled context across runs.
  143. func (s *SSHServer) Run(addr string) error {
  144. if s.ctx.Err() != nil {
  145. return s.ctx.Err()
  146. }
  147. listener, err := net.Listen("tcp", addr)
  148. if err != nil {
  149. return err
  150. }
  151. // s.listener is the public handle Stop uses to interrupt the active run; listener (the local) is what
  152. // this run owns. They start equal but a fast reload may overwrite s.listener with the next run's
  153. // listener before this run's watcher fires, so each run must close its own listener via the local
  154. // reference.
  155. s.listener = listener
  156. runCtx, cancel := context.WithCancel(s.ctx)
  157. defer cancel()
  158. // Close the listener when this run's context is cancelled. That can come from the parent
  159. // (Control.Stop), from Run returning normally (defer cancel above), or transitively when a sibling
  160. // run cancels through Stop closing the listener. net.Listener.Close is idempotent so a duplicate
  161. // close from Stop is benign.
  162. go func() {
  163. <-runCtx.Done()
  164. if err := listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
  165. s.l.Warn("Failed to close the sshd listener", "error", err)
  166. }
  167. }()
  168. s.l.Info("SSH server is listening", "sshListener", addr)
  169. // Run loops until there is an error
  170. s.run(runCtx, listener)
  171. s.l.Info("SSH server stopped listening")
  172. // We don't return an error because run logs for us
  173. return nil
  174. }
  175. func (s *SSHServer) run(ctx context.Context, listener net.Listener) {
  176. for {
  177. c, err := listener.Accept()
  178. if err != nil {
  179. if !errors.Is(err, net.ErrClosed) {
  180. s.l.Warn("Error in listener, shutting down", "error", err)
  181. }
  182. return
  183. }
  184. go func(c net.Conn) {
  185. // NewServerConn may block while waiting for the client to complete the handshake.
  186. // Ensure that a bad client doesn't hurt us by checking for the parent context
  187. // cancellation before calling NewServerConn, and forcing the socket to close when
  188. // the context is cancelled.
  189. sessionContext, sessionCancel := context.WithCancel(ctx)
  190. go func() {
  191. <-sessionContext.Done()
  192. c.Close()
  193. }()
  194. conn, chans, reqs, err := ssh.NewServerConn(c, s.config)
  195. fp := ""
  196. if conn != nil {
  197. fp = conn.Permissions.Extensions["fp"]
  198. }
  199. if err != nil {
  200. l := s.l.With(
  201. "error", err,
  202. "remoteAddress", c.RemoteAddr(),
  203. )
  204. if conn != nil {
  205. l = l.With("sshUser", conn.User())
  206. conn.Close()
  207. }
  208. if fp != "" {
  209. l = l.With("sshFingerprint", fp)
  210. }
  211. l.Warn("failed to handshake")
  212. sessionCancel()
  213. return
  214. }
  215. l := s.l.With("sshUser", conn.User())
  216. l.Info("ssh user logged in",
  217. "remoteAddress", c.RemoteAddr(),
  218. "sshFingerprint", fp,
  219. )
  220. NewSession(s.commands, conn, chans, sessionCancel, l.With("subsystem", "sshd.session"))
  221. go ssh.DiscardRequests(reqs)
  222. }(c)
  223. }
  224. }
  225. func (s *SSHServer) Stop() {
  226. if s.listener != nil {
  227. if err := s.listener.Close(); err != nil {
  228. s.l.Warn("Failed to close the sshd listener", "error", err)
  229. }
  230. }
  231. }