mirror of
https://github.com/drakkan/sftpgo.git
synced 2025-12-07 23:00:55 +03:00
add support for a basic built-in defender
It can help to prevent DoS and brute force password guessing
This commit is contained in:
@@ -127,8 +127,11 @@ func TestMain(m *testing.M) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
common.Initialize(commonConf)
|
||||
|
||||
err = common.Initialize(commonConf)
|
||||
if err != nil {
|
||||
logger.WarnToConsole("error initializing common: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = dataprovider.Initialize(providerConf, configDir)
|
||||
if err != nil {
|
||||
logger.ErrorToConsole("error initializing data provider: %v", err)
|
||||
@@ -233,12 +236,7 @@ func TestMain(m *testing.M) {
|
||||
}()
|
||||
|
||||
waitTCPListening(ftpdConf.Bindings[0].GetAddress())
|
||||
|
||||
// ensure all the initial connections to check if the service is alive are disconnected
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
for len(common.Connections.GetStats()) > 0 {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
waitNoConnections()
|
||||
|
||||
exitCode := m.Run()
|
||||
os.Remove(logFilePath)
|
||||
@@ -402,6 +400,12 @@ func TestLoginInvalidPwd(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLoginNonExistentUser(t *testing.T) {
|
||||
user := getTestUser()
|
||||
_, err := getFTPClient(user, false)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestLoginExternalAuth(t *testing.T) {
|
||||
if runtime.GOOS == osWindows {
|
||||
t.Skip("this test is not available on Windows")
|
||||
@@ -599,6 +603,47 @@ func TestMaxConnections(t *testing.T) {
|
||||
common.Config.MaxTotalConnections = oldValue
|
||||
}
|
||||
|
||||
func TestDefender(t *testing.T) {
|
||||
oldConfig := config.GetCommonConfig()
|
||||
|
||||
cfg := config.GetCommonConfig()
|
||||
cfg.DefenderConfig.Enabled = true
|
||||
cfg.DefenderConfig.Threshold = 3
|
||||
|
||||
err := common.Initialize(cfg)
|
||||
assert.NoError(t, err)
|
||||
|
||||
user, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
client, err := getFTPClient(user, false)
|
||||
if assert.NoError(t, err) {
|
||||
err = checkBasicFTP(client)
|
||||
assert.NoError(t, err)
|
||||
err = client.Quit()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
user.Password = "wrong_pwd"
|
||||
_, err = getFTPClient(user, false)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
user.Password = defaultPassword
|
||||
_, err = getFTPClient(user, false)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "Access denied, banned client IP")
|
||||
}
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = common.Initialize(oldConfig)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMaxSessions(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.MaxSessions = 1
|
||||
@@ -2035,6 +2080,13 @@ func waitTCPListening(address string) {
|
||||
}
|
||||
}
|
||||
|
||||
func waitNoConnections() {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
for len(common.Connections.GetStats()) > 0 {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func getTestUser() dataprovider.User {
|
||||
user := dataprovider.User{
|
||||
Username: defaultUsername,
|
||||
|
||||
@@ -103,11 +103,16 @@ func (s *Server) GetSettings() (*ftpserver.Settings, error) {
|
||||
|
||||
// ClientConnected is called to send the very first welcome message
|
||||
func (s *Server) ClientConnected(cc ftpserver.ClientContext) (string, error) {
|
||||
ipAddr := utils.GetIPFromRemoteAddress(cc.RemoteAddr().String())
|
||||
if common.IsBanned(ipAddr) {
|
||||
logger.Log(logger.LevelDebug, common.ProtocolFTP, "", "connection refused, ip %#v is banned", ipAddr)
|
||||
return "Access denied, banned client IP", common.ErrConnectionDenied
|
||||
}
|
||||
if !common.Connections.IsNewConnectionAllowed() {
|
||||
logger.Log(logger.LevelDebug, common.ProtocolFTP, "", "connection refused, configured limit reached")
|
||||
return "", common.ErrConnectionDenied
|
||||
}
|
||||
if err := common.Config.ExecutePostConnectHook(cc.RemoteAddr().String(), common.ProtocolFTP); err != nil {
|
||||
if err := common.Config.ExecutePostConnectHook(ipAddr, common.ProtocolFTP); err != nil {
|
||||
return "", err
|
||||
}
|
||||
connID := fmt.Sprintf("%v_%v", s.ID, cc.ID())
|
||||
@@ -128,23 +133,23 @@ func (s *Server) ClientDisconnected(cc ftpserver.ClientContext) {
|
||||
|
||||
// AuthUser authenticates the user and selects an handling driver
|
||||
func (s *Server) AuthUser(cc ftpserver.ClientContext, username, password string) (ftpserver.ClientDriver, error) {
|
||||
remoteAddr := cc.RemoteAddr().String()
|
||||
user, err := dataprovider.CheckUserAndPass(username, password, utils.GetIPFromRemoteAddress(remoteAddr), common.ProtocolFTP)
|
||||
ipAddr := utils.GetIPFromRemoteAddress(cc.RemoteAddr().String())
|
||||
user, err := dataprovider.CheckUserAndPass(username, password, ipAddr, common.ProtocolFTP)
|
||||
if err != nil {
|
||||
updateLoginMetrics(username, remoteAddr, err)
|
||||
updateLoginMetrics(username, ipAddr, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
connection, err := s.validateUser(user, cc)
|
||||
|
||||
defer updateLoginMetrics(username, remoteAddr, err)
|
||||
defer updateLoginMetrics(username, ipAddr, err)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
connection.Fs.CheckRootPath(connection.GetUsername(), user.GetUID(), user.GetGID())
|
||||
connection.Log(logger.LevelInfo, "User id: %d, logged in with FTP, username: %#v, home_dir: %#v remote addr: %#v",
|
||||
user.ID, user.Username, user.HomeDir, remoteAddr)
|
||||
user.ID, user.Username, user.HomeDir, cc.RemoteAddr())
|
||||
dataprovider.UpdateLastLogin(user) //nolint:errcheck
|
||||
return connection, nil
|
||||
}
|
||||
@@ -213,12 +218,16 @@ func (s *Server) validateUser(user dataprovider.User, cc ftpserver.ClientContext
|
||||
return connection, nil
|
||||
}
|
||||
|
||||
func updateLoginMetrics(username, remoteAddress string, err error) {
|
||||
func updateLoginMetrics(username, ip string, err error) {
|
||||
metrics.AddLoginAttempt(dataprovider.LoginMethodPassword)
|
||||
ip := utils.GetIPFromRemoteAddress(remoteAddress)
|
||||
if err != nil {
|
||||
logger.ConnectionFailedLog(username, ip, dataprovider.LoginMethodPassword,
|
||||
common.ProtocolFTP, err.Error())
|
||||
event := common.HostEventLoginFailed
|
||||
if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
|
||||
event = common.HostEventUserNotFound
|
||||
}
|
||||
common.AddDefenderEvent(ip, event)
|
||||
}
|
||||
metrics.AddLoginResult(dataprovider.LoginMethodPassword, err)
|
||||
dataprovider.ExecutePostLoginHook(username, dataprovider.LoginMethodPassword, ip, common.ProtocolFTP, err)
|
||||
|
||||
Reference in New Issue
Block a user