123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436 |
- // Copyright (C) 2019-2023 Nicola Murino
- //
- // This program is free software: you can redistribute it and/or modify
- // it under the terms of the GNU Affero General Public License as published
- // by the Free Software Foundation, version 3.
- //
- // This program is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU Affero General Public License for more details.
- //
- // You should have received a copy of the GNU Affero General Public License
- // along with this program. If not, see <https://www.gnu.org/licenses/>.
- package ftpd
- import (
- "crypto/tls"
- "crypto/x509"
- "errors"
- "fmt"
- "net"
- "os"
- "path/filepath"
- "sync"
- ftpserver "github.com/fclairamb/ftpserverlib"
- "github.com/drakkan/sftpgo/v2/internal/common"
- "github.com/drakkan/sftpgo/v2/internal/dataprovider"
- "github.com/drakkan/sftpgo/v2/internal/logger"
- "github.com/drakkan/sftpgo/v2/internal/metric"
- "github.com/drakkan/sftpgo/v2/internal/util"
- "github.com/drakkan/sftpgo/v2/internal/version"
- )
- // Server implements the ftpserverlib MainDriver interface
- type Server struct {
- ID int
- config *Configuration
- initialMsg string
- statusBanner string
- binding Binding
- tlsConfig *tls.Config
- mu sync.RWMutex
- verifiedTLSConns map[uint32]bool
- }
- // NewServer returns a new FTP server driver
- func NewServer(config *Configuration, configDir string, binding Binding, id int) *Server {
- binding.setCiphers()
- server := &Server{
- config: config,
- initialMsg: config.Banner,
- statusBanner: fmt.Sprintf("SFTPGo %v FTP Server", version.Get().Version),
- binding: binding,
- ID: id,
- verifiedTLSConns: make(map[uint32]bool),
- }
- if config.BannerFile != "" {
- bannerFilePath := config.BannerFile
- if !filepath.IsAbs(bannerFilePath) {
- bannerFilePath = filepath.Join(configDir, bannerFilePath)
- }
- bannerContent, err := os.ReadFile(bannerFilePath)
- if err == nil {
- server.initialMsg = string(bannerContent)
- } else {
- logger.WarnToConsole("unable to read FTPD banner file: %v", err)
- logger.Warn(logSender, "", "unable to read banner file: %v", err)
- }
- }
- server.buildTLSConfig()
- return server
- }
- func (s *Server) isTLSConnVerified(id uint32) bool {
- s.mu.RLock()
- defer s.mu.RUnlock()
- return s.verifiedTLSConns[id]
- }
- func (s *Server) setTLSConnVerified(id uint32, value bool) {
- s.mu.Lock()
- defer s.mu.Unlock()
- s.verifiedTLSConns[id] = value
- }
- func (s *Server) cleanTLSConnVerification(id uint32) {
- s.mu.Lock()
- defer s.mu.Unlock()
- delete(s.verifiedTLSConns, id)
- }
- // GetSettings returns FTP server settings
- func (s *Server) GetSettings() (*ftpserver.Settings, error) {
- if err := s.binding.checkPassiveIP(); err != nil {
- return nil, err
- }
- if err := s.binding.checkSecuritySettings(); err != nil {
- return nil, err
- }
- var portRange *ftpserver.PortRange
- if s.config.PassivePortRange.Start > 0 && s.config.PassivePortRange.End > s.config.PassivePortRange.Start {
- portRange = &ftpserver.PortRange{
- Start: s.config.PassivePortRange.Start,
- End: s.config.PassivePortRange.End,
- }
- }
- var ftpListener net.Listener
- if s.binding.HasProxy() {
- listener, err := net.Listen("tcp", s.binding.GetAddress())
- if err != nil {
- logger.Warn(logSender, "", "error starting listener on address %v: %v", s.binding.GetAddress(), err)
- return nil, err
- }
- ftpListener, err = common.Config.GetProxyListener(listener)
- if err != nil {
- logger.Warn(logSender, "", "error enabling proxy listener: %v", err)
- return nil, err
- }
- if s.binding.TLSMode == 2 && s.tlsConfig != nil {
- ftpListener = tls.NewListener(ftpListener, s.tlsConfig)
- }
- }
- if s.binding.TLSMode < 0 || s.binding.TLSMode > 2 {
- return nil, errors.New("unsupported TLS mode")
- }
- if s.binding.TLSMode > 0 && certMgr == nil {
- return nil, errors.New("to enable TLS you need to provide a certificate")
- }
- return &ftpserver.Settings{
- Listener: ftpListener,
- ListenAddr: s.binding.GetAddress(),
- PublicIPResolver: s.binding.passiveIPResolver,
- PassiveTransferPortRange: portRange,
- ActiveTransferPortNon20: s.config.ActiveTransfersPortNon20,
- IdleTimeout: -1,
- ConnectionTimeout: 20,
- Banner: s.statusBanner,
- TLSRequired: ftpserver.TLSRequirement(s.binding.TLSMode),
- DisableSite: !s.config.EnableSite,
- DisableActiveMode: s.config.DisableActiveMode,
- EnableHASH: s.config.HASHSupport > 0,
- EnableCOMB: s.config.CombineSupport > 0,
- DefaultTransferType: ftpserver.TransferTypeBinary,
- ActiveConnectionsCheck: ftpserver.DataConnectionRequirement(s.binding.ActiveConnectionsSecurity),
- PasvConnectionsCheck: ftpserver.DataConnectionRequirement(s.binding.PassiveConnectionsSecurity),
- }, nil
- }
- // ClientConnected is called to send the very first welcome message
- func (s *Server) ClientConnected(cc ftpserver.ClientContext) (string, error) {
- cc.SetDebug(s.binding.Debug)
- ipAddr := util.GetIPFromRemoteAddress(cc.RemoteAddr().String())
- common.Connections.AddClientConnection(ipAddr)
- if common.IsBanned(ipAddr, common.ProtocolFTP) {
- logger.Log(logger.LevelDebug, common.ProtocolFTP, "", "connection refused, ip %q is banned", ipAddr)
- return "Access denied: banned client IP", common.ErrConnectionDenied
- }
- if err := common.Connections.IsNewConnectionAllowed(ipAddr, common.ProtocolFTP); err != nil {
- logger.Log(logger.LevelDebug, common.ProtocolFTP, "", "connection not allowed from ip %q: %v", ipAddr, err)
- return "Access denied", err
- }
- _, err := common.LimitRate(common.ProtocolFTP, ipAddr)
- if err != nil {
- return fmt.Sprintf("Access denied: %v", err.Error()), err
- }
- if err := common.Config.ExecutePostConnectHook(ipAddr, common.ProtocolFTP); err != nil {
- return "Access denied by post connect hook", err
- }
- connID := fmt.Sprintf("%v_%v", s.ID, cc.ID())
- user := dataprovider.User{}
- connection := &Connection{
- BaseConnection: common.NewBaseConnection(connID, common.ProtocolFTP, cc.LocalAddr().String(),
- cc.RemoteAddr().String(), user),
- clientContext: cc,
- }
- err = common.Connections.Add(connection)
- return s.initialMsg, err
- }
- // ClientDisconnected is called when the user disconnects, even if he never authenticated
- func (s *Server) ClientDisconnected(cc ftpserver.ClientContext) {
- s.cleanTLSConnVerification(cc.ID())
- connID := fmt.Sprintf("%v_%v_%v", common.ProtocolFTP, s.ID, cc.ID())
- common.Connections.Remove(connID)
- common.Connections.RemoveClientConnection(util.GetIPFromRemoteAddress(cc.RemoteAddr().String()))
- }
- // AuthUser authenticates the user and selects an handling driver
- func (s *Server) AuthUser(cc ftpserver.ClientContext, username, password string) (ftpserver.ClientDriver, error) {
- loginMethod := dataprovider.LoginMethodPassword
- if s.isTLSConnVerified(cc.ID()) {
- loginMethod = dataprovider.LoginMethodTLSCertificateAndPwd
- }
- ipAddr := util.GetIPFromRemoteAddress(cc.RemoteAddr().String())
- user, err := dataprovider.CheckUserAndPass(username, password, ipAddr, common.ProtocolFTP)
- if err != nil {
- user.Username = username
- updateLoginMetrics(&user, ipAddr, loginMethod, err)
- return nil, dataprovider.ErrInvalidCredentials
- }
- connection, err := s.validateUser(user, cc, loginMethod)
- defer updateLoginMetrics(&user, ipAddr, loginMethod, err)
- if err != nil {
- return nil, err
- }
- setStartDirectory(user.Filters.StartDirectory, cc)
- connection.Log(logger.LevelInfo, "User %q logged in with %q from ip %q", user.Username, loginMethod, ipAddr)
- dataprovider.UpdateLastLogin(&user)
- return connection, nil
- }
- // PreAuthUser implements the MainDriverExtensionUserVerifier interface
- func (s *Server) PreAuthUser(cc ftpserver.ClientContext, username string) error {
- if s.binding.TLSMode == 0 && s.tlsConfig != nil {
- user, err := dataprovider.GetFTPPreAuthUser(username, util.GetIPFromRemoteAddress(cc.RemoteAddr().String()))
- if err == nil {
- if user.Filters.FTPSecurity == 1 {
- return cc.SetTLSRequirement(ftpserver.MandatoryEncryption)
- }
- return nil
- }
- if !errors.Is(err, util.ErrNotFound) {
- logger.Error(logSender, fmt.Sprintf("%v_%v_%v", common.ProtocolFTP, s.ID, cc.ID()),
- "unable to get user on pre auth: %v", err)
- return common.ErrInternalFailure
- }
- }
- return nil
- }
- // WrapPassiveListener implements the MainDriverExtensionPassiveWrapper interface
- func (s *Server) WrapPassiveListener(listener net.Listener) (net.Listener, error) {
- if s.binding.HasProxy() {
- return common.Config.GetProxyListener(listener)
- }
- return listener, nil
- }
- // VerifyConnection checks whether a user should be authenticated using a client certificate without prompting for a password
- func (s *Server) VerifyConnection(cc ftpserver.ClientContext, user string, tlsConn *tls.Conn) (ftpserver.ClientDriver, error) {
- if !s.binding.isMutualTLSEnabled() {
- return nil, nil
- }
- s.setTLSConnVerified(cc.ID(), false)
- if tlsConn != nil {
- state := tlsConn.ConnectionState()
- if len(state.PeerCertificates) > 0 {
- ipAddr := util.GetIPFromRemoteAddress(cc.RemoteAddr().String())
- dbUser, err := dataprovider.CheckUserBeforeTLSAuth(user, ipAddr, common.ProtocolFTP, state.PeerCertificates[0])
- if err != nil {
- dbUser.Username = user
- updateLoginMetrics(&dbUser, ipAddr, dataprovider.LoginMethodTLSCertificate, err)
- return nil, dataprovider.ErrInvalidCredentials
- }
- if dbUser.IsTLSUsernameVerificationEnabled() {
- dbUser, err = dataprovider.CheckUserAndTLSCert(user, ipAddr, common.ProtocolFTP, state.PeerCertificates[0])
- if err != nil {
- return nil, err
- }
- s.setTLSConnVerified(cc.ID(), true)
- if dbUser.IsLoginMethodAllowed(dataprovider.LoginMethodTLSCertificate, common.ProtocolFTP, nil) {
- connection, err := s.validateUser(dbUser, cc, dataprovider.LoginMethodTLSCertificate)
- defer updateLoginMetrics(&dbUser, ipAddr, dataprovider.LoginMethodTLSCertificate, err)
- if err != nil {
- return nil, err
- }
- setStartDirectory(dbUser.Filters.StartDirectory, cc)
- connection.Log(logger.LevelInfo, "User id: %d, logged in with FTP using a TLS certificate, username: %q, home_dir: %q remote addr: %q",
- dbUser.ID, dbUser.Username, dbUser.HomeDir, ipAddr)
- dataprovider.UpdateLastLogin(&dbUser)
- return connection, nil
- }
- }
- }
- }
- return nil, nil
- }
- func (s *Server) buildTLSConfig() {
- if certMgr != nil {
- certID := common.DefaultTLSKeyPaidID
- if getConfigPath(s.binding.CertificateFile, "") != "" && getConfigPath(s.binding.CertificateKeyFile, "") != "" {
- certID = s.binding.GetAddress()
- }
- s.tlsConfig = &tls.Config{
- GetCertificate: certMgr.GetCertificateFunc(certID),
- MinVersion: util.GetTLSVersion(s.binding.MinTLSVersion),
- CipherSuites: s.binding.ciphers,
- PreferServerCipherSuites: true,
- }
- logger.Debug(logSender, "", "configured TLS cipher suites for binding %q: %v, certID: %v",
- s.binding.GetAddress(), s.binding.ciphers, certID)
- if s.binding.isMutualTLSEnabled() {
- s.tlsConfig.ClientCAs = certMgr.GetRootCAs()
- s.tlsConfig.VerifyConnection = s.verifyTLSConnection
- switch s.binding.ClientAuthType {
- case 1:
- s.tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
- case 2:
- s.tlsConfig.ClientAuth = tls.VerifyClientCertIfGiven
- }
- }
- }
- }
- // GetTLSConfig returns the TLS configuration for this server
- func (s *Server) GetTLSConfig() (*tls.Config, error) {
- if s.tlsConfig != nil {
- return s.tlsConfig, nil
- }
- return nil, errors.New("no TLS certificate configured")
- }
- func (s *Server) verifyTLSConnection(state tls.ConnectionState) error {
- if certMgr != nil {
- var clientCrt *x509.Certificate
- var clientCrtName string
- if len(state.PeerCertificates) > 0 {
- clientCrt = state.PeerCertificates[0]
- clientCrtName = clientCrt.Subject.String()
- }
- if len(state.VerifiedChains) == 0 {
- if s.binding.ClientAuthType == 2 {
- return nil
- }
- logger.Warn(logSender, "", "TLS connection cannot be verified: unable to get verification chain")
- return errors.New("TLS connection cannot be verified: unable to get verification chain")
- }
- for _, verifiedChain := range state.VerifiedChains {
- var caCrt *x509.Certificate
- if len(verifiedChain) > 0 {
- caCrt = verifiedChain[len(verifiedChain)-1]
- }
- if certMgr.IsRevoked(clientCrt, caCrt) {
- logger.Debug(logSender, "", "tls handshake error, client certificate %q has beed revoked", clientCrtName)
- return common.ErrCrtRevoked
- }
- }
- }
- return nil
- }
- func (s *Server) validateUser(user dataprovider.User, cc ftpserver.ClientContext, loginMethod string) (*Connection, error) {
- connectionID := fmt.Sprintf("%v_%v_%v", common.ProtocolFTP, s.ID, cc.ID())
- if !filepath.IsAbs(user.HomeDir) {
- logger.Warn(logSender, connectionID, "user %q has an invalid home dir: %q. Home dir must be an absolute path, login not allowed",
- user.Username, user.HomeDir)
- return nil, fmt.Errorf("cannot login user with invalid home dir: %q", user.HomeDir)
- }
- if util.Contains(user.Filters.DeniedProtocols, common.ProtocolFTP) {
- logger.Info(logSender, connectionID, "cannot login user %q, protocol FTP is not allowed", user.Username)
- return nil, fmt.Errorf("protocol FTP is not allowed for user %q", user.Username)
- }
- if !user.IsLoginMethodAllowed(loginMethod, common.ProtocolFTP, nil) {
- logger.Info(logSender, connectionID, "cannot login user %q, %v login method is not allowed",
- user.Username, loginMethod)
- return nil, fmt.Errorf("login method %v is not allowed for user %q", loginMethod, user.Username)
- }
- if user.MustSetSecondFactorForProtocol(common.ProtocolFTP) {
- logger.Info(logSender, connectionID, "cannot login user %q, second factor authentication is not set",
- user.Username)
- return nil, fmt.Errorf("second factor authentication is not set for user %q", user.Username)
- }
- if user.MaxSessions > 0 {
- activeSessions := common.Connections.GetActiveSessions(user.Username)
- if activeSessions >= user.MaxSessions {
- logger.Info(logSender, connectionID, "authentication refused for user: %q, too many open sessions: %v/%v",
- user.Username, activeSessions, user.MaxSessions)
- return nil, fmt.Errorf("too many open sessions: %v", activeSessions)
- }
- }
- remoteAddr := cc.RemoteAddr().String()
- if !user.IsLoginFromAddrAllowed(remoteAddr) {
- logger.Info(logSender, connectionID, "cannot login user %q, remote address is not allowed: %v",
- user.Username, remoteAddr)
- return nil, fmt.Errorf("login for user %q is not allowed from this address: %v", user.Username, remoteAddr)
- }
- err := user.CheckFsRoot(connectionID)
- if err != nil {
- errClose := user.CloseFs()
- logger.Warn(logSender, connectionID, "unable to check fs root: %v close fs error: %v", err, errClose)
- return nil, common.ErrInternalFailure
- }
- connection := &Connection{
- BaseConnection: common.NewBaseConnection(fmt.Sprintf("%v_%v", s.ID, cc.ID()), common.ProtocolFTP,
- cc.LocalAddr().String(), remoteAddr, user),
- clientContext: cc,
- }
- err = common.Connections.Swap(connection)
- if err != nil {
- errClose := user.CloseFs()
- logger.Warn(logSender, connectionID, "unable to swap connection: %v, close fs error: %v", err, errClose)
- return nil, err
- }
- return connection, nil
- }
- func setStartDirectory(startDirectory string, cc ftpserver.ClientContext) {
- if startDirectory == "" {
- return
- }
- cc.SetPath(startDirectory)
- }
- func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err error) {
- metric.AddLoginAttempt(loginMethod)
- if err != nil && err != common.ErrInternalFailure {
- logger.ConnectionFailedLog(user.Username, ip, loginMethod,
- common.ProtocolFTP, err.Error())
- event := common.HostEventLoginFailed
- if errors.Is(err, util.ErrNotFound) {
- event = common.HostEventUserNotFound
- }
- common.AddDefenderEvent(ip, common.ProtocolFTP, event)
- }
- metric.AddLoginResult(loginMethod, err)
- dataprovider.ExecutePostLoginHook(user, loginMethod, ip, common.ProtocolFTP, err)
- }
|