wings/sftp/server.go

253 lines
7.8 KiB
Go
Raw Normal View History

package sftp
import (
2021-02-02 04:59:17 +00:00
"context"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"io"
"net"
"os"
"path"
2021-02-02 04:59:17 +00:00
"regexp"
2021-01-10 23:06:06 +00:00
"strconv"
"strings"
2021-01-10 01:22:39 +00:00
"emperror.dev/errors"
2021-01-10 01:22:39 +00:00
"github.com/apex/log"
"github.com/pkg/sftp"
"golang.org/x/crypto/ed25519"
"golang.org/x/crypto/ssh"
"github.com/pterodactyl/wings/config"
2021-02-02 04:59:17 +00:00
"github.com/pterodactyl/wings/remote"
"github.com/pterodactyl/wings/server"
)
2021-02-02 04:59:17 +00:00
// Usernames all follow the same format, so don't even bother hitting the API if the username is not
// at least in the expected format. This is very basic protection against random bots finding the SFTP
// server and sending a flood of usernames.
var validUsernameRegexp = regexp.MustCompile(`^(?i)(.+)\.([a-z0-9]{8})$`)
//goland:noinspection GoNameStartsWithPackageName
type SFTPServer struct {
2021-01-26 04:28:24 +00:00
manager *server.Manager
2021-01-10 23:06:06 +00:00
BasePath string
ReadOnly bool
Listen string
}
2021-01-26 04:28:24 +00:00
func New(m *server.Manager) *SFTPServer {
cfg := config.Get().System
return &SFTPServer{
2021-01-26 04:28:24 +00:00
manager: m,
2021-01-10 23:06:06 +00:00
BasePath: cfg.Data,
ReadOnly: cfg.Sftp.ReadOnly,
Listen: cfg.Sftp.Address + ":" + strconv.Itoa(cfg.Sftp.Port),
}
}
// Run starts the SFTP server and add a persistent listener to handle inbound
// SFTP connections. This will automatically generate an ED25519 key if one does
// not already exist on the system for host key verification purposes.
func (c *SFTPServer) Run() error {
if _, err := os.Stat(c.PrivateKeyPath()); os.IsNotExist(err) {
if err := c.generateED25519PrivateKey(); err != nil {
return err
}
} else if err != nil {
return errors.Wrap(err, "sftp: could not stat private key file")
2019-12-07 23:53:07 +00:00
}
pb, err := os.ReadFile(c.PrivateKeyPath())
if err != nil {
return errors.Wrap(err, "sftp: could not read private key file")
}
2021-01-10 23:06:06 +00:00
private, err := ssh.ParsePrivateKey(pb)
if err != nil {
return err
}
2019-12-07 23:53:07 +00:00
2021-01-10 23:06:06 +00:00
conf := &ssh.ServerConfig{
2023-01-17 18:50:06 +00:00
Config: ssh.Config{
KeyExchanges: []string{
"curve25519-sha256", "curve25519-sha256@libssh.org",
"ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521",
"diffie-hellman-group14-sha256",
},
Ciphers: []string{
"aes128-gcm@openssh.com",
"chacha20-poly1305@openssh.com",
"aes128-ctr", "aes192-ctr", "aes256-ctr",
},
MACs: []string{
"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256",
},
},
2022-05-15 20:01:52 +00:00
NoClientAuth: false,
MaxAuthTries: 6,
PasswordCallback: func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
return c.makeCredentialsRequest(conn, remote.SftpAuthPassword, string(password))
},
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
return c.makeCredentialsRequest(conn, remote.SftpAuthPublicKey, string(ssh.MarshalAuthorizedKey(key)))
2022-05-15 20:01:52 +00:00
},
2021-01-10 23:06:06 +00:00
}
conf.AddHostKey(private)
2019-12-08 01:35:45 +00:00
2021-01-10 23:06:06 +00:00
listener, err := net.Listen("tcp", c.Listen)
if err != nil {
return err
2019-12-08 01:35:45 +00:00
}
public := string(ssh.MarshalAuthorizedKey(private.PublicKey()))
log.WithField("listen", c.Listen).WithField("public_key", strings.Trim(public, "\n")).Info("sftp server listening for connections")
for {
2021-01-10 23:06:06 +00:00
if conn, _ := listener.Accept(); conn != nil {
go func(conn net.Conn) {
defer conn.Close()
if err := c.AcceptInbound(conn, conf); err != nil {
log.WithField("error", err).WithField("ip", conn.RemoteAddr().String()).Error("sftp: failed to accept inbound connection")
}
2021-01-10 23:06:06 +00:00
}(conn)
}
}
}
// AcceptInbound handles an inbound connection to the instance and determines if we should
// serve the request or not.
func (c *SFTPServer) AcceptInbound(conn net.Conn, config *ssh.ServerConfig) error {
// Before beginning a handshake must be performed on the incoming net.Conn
sconn, chans, reqs, err := ssh.NewServerConn(conn, config)
if err != nil {
return errors.WithStack(err)
2019-12-08 01:35:45 +00:00
}
defer sconn.Close()
go ssh.DiscardRequests(reqs)
2021-01-10 23:06:06 +00:00
for ch := range chans {
// If its not a session channel we just move on because its not something we
// know how to handle at this point.
2021-01-10 23:06:06 +00:00
if ch.ChannelType() != "session" {
ch.Reject(ssh.UnknownChannelType, "unknown channel type")
continue
}
2021-01-10 23:06:06 +00:00
channel, requests, err := ch.Accept()
if err != nil {
continue
}
go func(in <-chan *ssh.Request) {
for req := range in {
2021-01-10 23:06:06 +00:00
// Channels have a type that is dependent on the protocol. For SFTP
// this is "subsystem" with a payload that (should) be "sftp". Discard
// anything else we receive ("pty", "shell", etc)
req.Reply(req.Type == "subsystem" && string(req.Payload[4:]) == "sftp", nil)
}
}(requests)
2021-01-10 23:06:06 +00:00
// If no UUID has been set on this inbound request then we can assume we
// have screwed up something in the authentication code. This is a sanity
// check, but should never be encountered (ideally...).
//
// This will also attempt to match a specific server out of the global server
// store and return nil if there is no match.
uuid := sconn.Permissions.Extensions["uuid"]
2021-01-26 04:28:24 +00:00
srv := c.manager.Find(func(s *server.Server) bool {
2021-01-10 23:06:06 +00:00
if uuid == "" {
return false
}
return s.ID() == uuid
2021-01-10 23:06:06 +00:00
})
if srv == nil {
continue
}
2021-01-10 23:06:06 +00:00
// Spin up a SFTP server instance for the authenticated user's server allowing
// them access to the underlying filesystem.
handler, err := NewHandler(sconn, srv)
if err != nil {
return errors.WithStackIf(err)
}
rs := sftp.NewRequestServer(channel, handler.Handlers())
if err := rs.Serve(); err == io.EOF {
_ = rs.Close()
}
}
return nil
}
// Generates a new ED25519 private key that is used for host authentication when
// a user connects to the SFTP server.
func (c *SFTPServer) generateED25519PrivateKey() error {
_, priv, err := ed25519.GenerateKey(rand.Reader)
2019-12-08 01:35:45 +00:00
if err != nil {
return errors.Wrap(err, "sftp: failed to generate ED25519 private key")
}
if err := os.MkdirAll(path.Dir(c.PrivateKeyPath()), 0o755); err != nil {
return errors.Wrap(err, "sftp: could not create internal sftp data directory")
2019-12-08 01:35:45 +00:00
}
o, err := os.OpenFile(c.PrivateKeyPath(), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
2021-01-10 23:06:06 +00:00
return errors.WithStack(err)
}
defer o.Close()
2019-12-08 01:35:45 +00:00
b, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return errors.Wrap(err, "sftp: failed to marshal private key into bytes")
}
if err := pem.Encode(o, &pem.Block{Type: "PRIVATE KEY", Bytes: b}); err != nil {
return errors.Wrap(err, "sftp: failed to write ED25519 private key to disk")
}
return nil
}
2022-05-15 20:01:52 +00:00
func (c *SFTPServer) makeCredentialsRequest(conn ssh.ConnMetadata, t remote.SftpAuthRequestType, p string) (*ssh.Permissions, error) {
2021-02-02 04:59:17 +00:00
request := remote.SftpAuthRequest{
2022-05-15 20:01:52 +00:00
Type: t,
User: conn.User(),
2022-05-15 20:01:52 +00:00
Pass: p,
IP: conn.RemoteAddr().String(),
SessionID: conn.SessionID(),
ClientVersion: conn.ClientVersion(),
}
2022-05-15 20:01:52 +00:00
logger := log.WithFields(log.Fields{"subsystem": "sftp", "method": request.Type, "username": request.User, "ip": request.IP})
logger.Debug("validating credentials for SFTP connection")
2021-02-02 04:59:17 +00:00
if !validUsernameRegexp.MatchString(request.User) {
logger.Warn("failed to validate user credentials (invalid format)")
return nil, &remote.SftpInvalidCredentialsError{}
}
resp, err := c.manager.Client().ValidateSftpCredentials(context.Background(), request)
if err != nil {
2021-02-02 05:32:34 +00:00
if _, ok := err.(*remote.SftpInvalidCredentialsError); ok {
logger.Warn("failed to validate user credentials (invalid username or password)")
} else {
2021-01-10 23:12:13 +00:00
logger.WithField("error", err).Error("encountered an error while trying to validate user credentials")
}
return nil, err
}
logger.WithField("server", resp.Server).Debug("credentials validated and matched to server instance")
2022-05-15 20:01:52 +00:00
permissions := ssh.Permissions{
Extensions: map[string]string{
"ip": conn.RemoteAddr().String(),
"uuid": resp.Server,
"user": resp.User,
"permissions": strings.Join(resp.Permissions, ","),
},
}
2022-05-15 20:01:52 +00:00
return &permissions, nil
}
// PrivateKeyPath returns the path the host private key for this server instance.
func (c *SFTPServer) PrivateKeyPath() string {
return path.Join(c.BasePath, ".sftp/id_ed25519")
}