nebula/sshd/server.go

204 lines
4.7 KiB
Go

package sshd
import (
"errors"
"fmt"
"net"
"sync"
"github.com/armon/go-radix"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
)
type SSHServer struct {
config *ssh.ServerConfig
l *logrus.Entry
// Map of user -> authorized keys
trustedKeys map[string]map[string]bool
// List of available commands
helpCommand *Command
commands *radix.Tree
listener net.Listener
// Locks the conns/counter to avoid concurrent map access
connsLock sync.Mutex
conns map[int]*session
counter int
}
// NewSSHServer creates a new ssh server rigged with default commands and prepares to listen
func NewSSHServer(l *logrus.Entry) (*SSHServer, error) {
s := &SSHServer{
trustedKeys: make(map[string]map[string]bool),
l: l,
commands: radix.New(),
conns: make(map[int]*session),
}
s.config = &ssh.ServerConfig{
PublicKeyCallback: s.matchPubKey,
//TODO: AuthLogCallback: s.authAttempt,
//TODO: version string
ServerVersion: fmt.Sprintf("SSH-2.0-Nebula???"),
}
s.RegisterCommand(&Command{
Name: "help",
ShortDescription: "prints available commands or help <command> for specific usage info",
Callback: func(a interface{}, args []string, w StringWriter) error {
return helpCallback(s.commands, args, w)
},
})
return s, nil
}
func (s *SSHServer) SetHostKey(hostPrivateKey []byte) error {
private, err := ssh.ParsePrivateKey(hostPrivateKey)
if err != nil {
return fmt.Errorf("failed to parse private key: %s", err)
}
s.config.AddHostKey(private)
return nil
}
func (s *SSHServer) ClearAuthorizedKeys() {
s.trustedKeys = make(map[string]map[string]bool)
}
// AddAuthorizedKey adds an ssh public key for a user
func (s *SSHServer) AddAuthorizedKey(user, pubKey string) error {
pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubKey))
if err != nil {
return err
}
tk, ok := s.trustedKeys[user]
if !ok {
tk = make(map[string]bool)
s.trustedKeys[user] = tk
}
tk[string(pk.Marshal())] = true
s.l.WithField("sshKey", pubKey).WithField("sshUser", user).Info("Authorized ssh key")
return nil
}
// RegisterCommand adds a command that can be run by a user, by default only `help` is available
func (s *SSHServer) RegisterCommand(c *Command) {
s.commands.Insert(c.Name, c)
}
// Run begins listening and accepting connections
func (s *SSHServer) Run(addr string) error {
var err error
s.listener, err = net.Listen("tcp", addr)
if err != nil {
return err
}
s.l.WithField("sshListener", addr).Info("SSH server is listening")
// Run loops until there is an error
s.run()
s.closeSessions()
s.l.Info("SSH server stopped listening")
// We don't return an error because run logs for us
return nil
}
func (s *SSHServer) run() {
for {
c, err := s.listener.Accept()
if err != nil {
if !errors.Is(err, net.ErrClosed) {
s.l.WithError(err).Warn("Error in listener, shutting down")
}
return
}
conn, chans, reqs, err := ssh.NewServerConn(c, s.config)
fp := ""
if conn != nil {
fp = conn.Permissions.Extensions["fp"]
}
if err != nil {
l := s.l.WithError(err).WithField("remoteAddress", c.RemoteAddr())
if conn != nil {
l = l.WithField("sshUser", conn.User())
conn.Close()
}
if fp != "" {
l = l.WithField("sshFingerprint", fp)
}
l.Warn("failed to handshake")
continue
}
l := s.l.WithField("sshUser", conn.User())
l.WithField("remoteAddress", c.RemoteAddr()).WithField("sshFingerprint", fp).Info("ssh user logged in")
session := NewSession(s.commands, conn, chans, l.WithField("subsystem", "sshd.session"))
s.connsLock.Lock()
s.counter++
counter := s.counter
s.conns[counter] = session
s.connsLock.Unlock()
go ssh.DiscardRequests(reqs)
go func() {
<-session.exitChan
s.l.WithField("id", counter).Debug("closing conn")
s.connsLock.Lock()
delete(s.conns, counter)
s.connsLock.Unlock()
}()
}
}
func (s *SSHServer) Stop() {
// Close the listener, this will cause all session to terminate as well, see SSHServer.Run
if s.listener != nil {
if err := s.listener.Close(); err != nil {
s.l.WithError(err).Warn("Failed to close the sshd listener")
}
}
}
func (s *SSHServer) closeSessions() {
s.connsLock.Lock()
for _, c := range s.conns {
c.Close()
}
s.connsLock.Unlock()
}
func (s *SSHServer) matchPubKey(c ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
pk := string(pubKey.Marshal())
fp := ssh.FingerprintSHA256(pubKey)
tk, ok := s.trustedKeys[c.User()]
if !ok {
return nil, fmt.Errorf("unknown user %s", c.User())
}
_, ok = tk[pk]
if !ok {
return nil, fmt.Errorf("unknown public key for %s (%s)", c.User(), fp)
}
return &ssh.Permissions{
// Record the public key used for authentication.
Extensions: map[string]string{
"fp": fp,
"user": c.User(),
},
}, nil
}