package stronghold
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"hash/fnv"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
"git.maze.io/maze/stronghold/authority"
|
|
)
|
|
|
|
const (
|
|
DefaultAddr = ":22"
|
|
|
|
tag = "server"
|
|
maxAuthRetries = 16
|
|
version = "SSH-2.0-StrongHold"
|
|
)
|
|
|
|
// Server receives connections from SSH clients.
|
|
type Server struct {
|
|
// KeyboardInteractiveHandler is a callback for doing keyboard-interactive authentication.
|
|
KeyboardInteractiveHandler func(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error)
|
|
|
|
// PasswordHandler is a callback for doing password authentication.
|
|
PasswordHandler func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error)
|
|
|
|
// PublicKeyHandler is a callback for doing public key authentication.
|
|
PublicKeyHandler func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error)
|
|
|
|
// Issuer is a callback for proxied SSH connections.
|
|
CertificateAuthority *authority.CertificateAuthority
|
|
|
|
// SessionHandler is a callback for SSH shell sessions.
|
|
SessionHandler func(conn ssh.ConnMetadata, channel ssh.Channel, pty *PTY, windowChanges <-chan *WindowChange) error
|
|
|
|
recordDir string
|
|
config *ssh.ServerConfig
|
|
closed chan struct{}
|
|
}
|
|
|
|
// New server
|
|
func New(recordDir string, hostKeyFiles ...string) (*Server, error) {
|
|
if len(hostKeyFiles) == 0 {
|
|
return nil, errors.New("server: at least one key file must be specified")
|
|
}
|
|
|
|
if !filepath.IsAbs(recordDir) {
|
|
var err error
|
|
if recordDir, err = filepath.Abs(recordDir); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := os.MkdirAll(recordDir, 0700); err != nil && !os.IsExist(err) {
|
|
return nil, err
|
|
}
|
|
|
|
server := &Server{
|
|
config: &ssh.ServerConfig{
|
|
NoClientAuth: true,
|
|
MaxAuthTries: maxAuthRetries,
|
|
ServerVersion: version,
|
|
},
|
|
closed: make(chan struct{}),
|
|
}
|
|
|
|
for _, keyFile := range hostKeyFiles {
|
|
key, err := loadPrivateKeyFile(keyFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("server: error loading private key from %s: %w", keyFile, err)
|
|
}
|
|
server.config.AddHostKey(key)
|
|
}
|
|
|
|
return server, nil
|
|
}
|
|
|
|
// ListenAndServe starts the SSH server on the given address.
|
|
func (server *Server) ListenAndServe(addr string) error {
|
|
l, err := net.Listen("tcp", addr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if server.KeyboardInteractiveHandler != nil {
|
|
server.config.NoClientAuth = false
|
|
server.config.KeyboardInteractiveCallback = server.KeyboardInteractiveHandler
|
|
}
|
|
if server.PasswordHandler != nil {
|
|
server.config.NoClientAuth = false
|
|
server.config.PasswordCallback = server.PasswordHandler
|
|
}
|
|
if server.PublicKeyHandler != nil {
|
|
server.config.NoClientAuth = false
|
|
server.config.PublicKeyCallback = server.PublicKeyHandler
|
|
}
|
|
|
|
for {
|
|
var conn net.Conn
|
|
if conn, err = l.Accept(); err != nil {
|
|
logrus.WithError(err).Error("error accepting client")
|
|
continue
|
|
}
|
|
go server.handleConn(conn)
|
|
}
|
|
}
|
|
|
|
func (server *Server) handleConn(netConn net.Conn) {
|
|
defer netConn.Close()
|
|
|
|
log := logrus.WithFields(logrus.Fields{
|
|
"tag": tag,
|
|
"src": netConn.RemoteAddr().String(),
|
|
})
|
|
log.Info("new client connection")
|
|
defer log.Info("client connection closed")
|
|
|
|
sshConn, newChannels, requests, err := ssh.NewServerConn(netConn, server.config)
|
|
if err != nil {
|
|
log.WithError(err).Error("handshakeAsServer failed")
|
|
return
|
|
}
|
|
|
|
client := newClient(netConn, sshConn, server.recordDir)
|
|
client.CertificateAuthority = server.CertificateAuthority
|
|
client.SessionHandler = server.SessionHandler
|
|
go client.handleRequests(requests)
|
|
if err = client.handleNewChannels(newChannels); err != nil {
|
|
if err != io.EOF {
|
|
log.WithError(err).Error("fatal error")
|
|
}
|
|
}
|
|
<-client.closed
|
|
}
|
|
|
|
func fingerprint(key ssh.PublicKey) string {
|
|
h := sha256.New()
|
|
_, _ = h.Write(key.Marshal())
|
|
return "SHA256:" + base64.RawStdEncoding.EncodeToString(h.Sum(nil))
|
|
}
|
|
|
|
func sessionID(metadata ssh.ConnMetadata) string {
|
|
h := fnv.New64()
|
|
_, _ = h.Write(metadata.SessionID())
|
|
return hex.EncodeToString(h.Sum(nil))
|
|
}
|
|
|
|
// PermitAllPasswords is a password handler that accepts all credentials.
|
|
func PermitAllPasswords(metadata ssh.ConnMetadata, _ []byte) (*ssh.Permissions, error) {
|
|
log := logrus.WithFields(logrus.Fields{
|
|
"session": sessionID(metadata),
|
|
"src": metadata.RemoteAddr().String(),
|
|
"user": metadata.User(),
|
|
})
|
|
log.Info("accepting password")
|
|
return new(ssh.Permissions), nil
|
|
}
|
|
|
|
// PermitAllPublicKeys is a public key handler that accepts all public keys.
|
|
func PermitAllPublicKeys(metadata ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
|
log := logrus.WithFields(logrus.Fields{
|
|
"session": sessionID(metadata),
|
|
"src": metadata.RemoteAddr().String(),
|
|
"user": metadata.User(),
|
|
})
|
|
log.WithFields(logrus.Fields{
|
|
"fingerprint": fingerprint(key),
|
|
"key_type": key.Type(),
|
|
}).Info("accepting public key")
|
|
return new(ssh.Permissions), nil
|
|
}
|
|
|
|
// PermitAllHostKeys logs and accepts all server keys.
|
|
func PermitAllHostKeys(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
|
log := logrus.WithFields(logrus.Fields{
|
|
"dst": remote.String(),
|
|
"hostname": hostname,
|
|
"fingerprint": fingerprint(key),
|
|
"key_type": key.Type(),
|
|
})
|
|
log.Info("accepting host key")
|
|
return nil
|
|
}
|