server.go 8.2 KB

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