mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-22 23:50:32 +00:00
03ebd5b841
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
232 lines
7.4 KiB
Go
232 lines
7.4 KiB
Go
// 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 common
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
|
"github.com/drakkan/sftpgo/v2/internal/logger"
|
|
)
|
|
|
|
// HostEvent is the enumerable for the supported host events
|
|
type HostEvent string
|
|
|
|
// Supported host events
|
|
const (
|
|
HostEventLoginFailed HostEvent = "LoginFailed"
|
|
HostEventUserNotFound HostEvent = "UserNotFound"
|
|
HostEventNoLoginTried HostEvent = "NoLoginTried"
|
|
HostEventLimitExceeded HostEvent = "LimitExceeded"
|
|
)
|
|
|
|
// Supported defender drivers
|
|
const (
|
|
DefenderDriverMemory = "memory"
|
|
DefenderDriverProvider = "provider"
|
|
)
|
|
|
|
var (
|
|
supportedDefenderDrivers = []string{DefenderDriverMemory, DefenderDriverProvider}
|
|
)
|
|
|
|
// Defender defines the interface that a defender must implements
|
|
type Defender interface {
|
|
GetHosts() ([]dataprovider.DefenderEntry, error)
|
|
GetHost(ip string) (dataprovider.DefenderEntry, error)
|
|
AddEvent(ip, protocol string, event HostEvent)
|
|
IsBanned(ip, protocol string) bool
|
|
IsSafe(ip, protocol string) bool
|
|
GetBanTime(ip string) (*time.Time, error)
|
|
GetScore(ip string) (int, error)
|
|
DeleteHost(ip string) bool
|
|
}
|
|
|
|
// DefenderConfig defines the "defender" configuration
|
|
type DefenderConfig struct {
|
|
// Set to true to enable the defender
|
|
Enabled bool `json:"enabled" mapstructure:"enabled"`
|
|
// Defender implementation to use, we support "memory" and "provider".
|
|
// Using "provider" as driver you can share the defender events among
|
|
// multiple SFTPGo instances. For a single instance "memory" provider will
|
|
// be much faster
|
|
Driver string `json:"driver" mapstructure:"driver"`
|
|
// BanTime is the number of minutes that a host is banned
|
|
BanTime int `json:"ban_time" mapstructure:"ban_time"`
|
|
// Percentage increase of the ban time if a banned host tries to connect again
|
|
BanTimeIncrement int `json:"ban_time_increment" mapstructure:"ban_time_increment"`
|
|
// Threshold value for banning a client
|
|
Threshold int `json:"threshold" mapstructure:"threshold"`
|
|
// Score for invalid login attempts, eg. non-existent user accounts
|
|
ScoreInvalid int `json:"score_invalid" mapstructure:"score_invalid"`
|
|
// Score for valid login attempts, eg. user accounts that exist
|
|
ScoreValid int `json:"score_valid" mapstructure:"score_valid"`
|
|
// Score for limit exceeded events, generated from the rate limiters or for max connections
|
|
// per-host exceeded
|
|
ScoreLimitExceeded int `json:"score_limit_exceeded" mapstructure:"score_limit_exceeded"`
|
|
// ScoreNoAuth defines the score for clients disconnected without authentication
|
|
// attempts
|
|
ScoreNoAuth int `json:"score_no_auth" mapstructure:"score_no_auth"`
|
|
// Defines the time window, in minutes, for tracking client errors.
|
|
// A host is banned if it has exceeded the defined threshold during
|
|
// the last observation time minutes
|
|
ObservationTime int `json:"observation_time" mapstructure:"observation_time"`
|
|
// The number of banned IPs and host scores kept in memory will vary between the
|
|
// soft and hard limit for the "memory" driver. For the "provider" driver the
|
|
// soft limit is ignored and the hard limit is used to limit the number of entries
|
|
// to return when you request for the entire host list from the defender
|
|
EntriesSoftLimit int `json:"entries_soft_limit" mapstructure:"entries_soft_limit"`
|
|
EntriesHardLimit int `json:"entries_hard_limit" mapstructure:"entries_hard_limit"`
|
|
}
|
|
|
|
type baseDefender struct {
|
|
config *DefenderConfig
|
|
ipList *dataprovider.IPList
|
|
}
|
|
|
|
func (d *baseDefender) isBanned(ip, protocol string) bool {
|
|
isListed, mode, err := d.ipList.IsListed(ip, protocol)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if isListed && mode == dataprovider.ListModeDeny {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (d *baseDefender) IsSafe(ip, protocol string) bool {
|
|
isListed, mode, err := d.ipList.IsListed(ip, protocol)
|
|
if err == nil && isListed && mode == dataprovider.ListModeAllow {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (d *baseDefender) getScore(event HostEvent) int {
|
|
var score int
|
|
|
|
switch event {
|
|
case HostEventLoginFailed:
|
|
score = d.config.ScoreValid
|
|
case HostEventLimitExceeded:
|
|
score = d.config.ScoreLimitExceeded
|
|
case HostEventUserNotFound:
|
|
score = d.config.ScoreInvalid
|
|
case HostEventNoLoginTried:
|
|
score = d.config.ScoreNoAuth
|
|
}
|
|
return score
|
|
}
|
|
|
|
// logEvent logs a defender event that changes a host's score
|
|
func (d *baseDefender) logEvent(ip, protocol string, event HostEvent, totalScore int) {
|
|
// ignore events which do not change the host score
|
|
eventScore := d.getScore(event)
|
|
if eventScore == 0 {
|
|
return
|
|
}
|
|
|
|
logger.GetLogger().Debug().
|
|
Timestamp().
|
|
Str("sender", "defender").
|
|
Str("client_ip", ip).
|
|
Str("protocol", protocol).
|
|
Str("event", string(event)).
|
|
Int("increase_score_by", eventScore).
|
|
Int("score", totalScore).
|
|
Send()
|
|
}
|
|
|
|
// logBan logs a host's ban due to a too high host score
|
|
func (d *baseDefender) logBan(ip, protocol string) {
|
|
logger.GetLogger().Info().
|
|
Timestamp().
|
|
Str("sender", "defender").
|
|
Str("client_ip", ip).
|
|
Str("protocol", protocol).
|
|
Str("event", "banned").
|
|
Send()
|
|
}
|
|
|
|
type hostEvent struct {
|
|
dateTime time.Time
|
|
score int
|
|
}
|
|
|
|
type hostScore struct {
|
|
TotalScore int
|
|
Events []hostEvent
|
|
}
|
|
|
|
func (c *DefenderConfig) checkScores() error {
|
|
if c.ScoreInvalid < 0 {
|
|
c.ScoreInvalid = 0
|
|
}
|
|
if c.ScoreValid < 0 {
|
|
c.ScoreValid = 0
|
|
}
|
|
if c.ScoreLimitExceeded < 0 {
|
|
c.ScoreLimitExceeded = 0
|
|
}
|
|
if c.ScoreNoAuth < 0 {
|
|
c.ScoreNoAuth = 0
|
|
}
|
|
if c.ScoreInvalid == 0 && c.ScoreValid == 0 && c.ScoreLimitExceeded == 0 && c.ScoreNoAuth == 0 {
|
|
return fmt.Errorf("invalid defender configuration: all scores are disabled")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validate returns an error if the configuration is invalid
|
|
func (c *DefenderConfig) validate() error {
|
|
if !c.Enabled {
|
|
return nil
|
|
}
|
|
if err := c.checkScores(); err != nil {
|
|
return err
|
|
}
|
|
if c.ScoreInvalid >= c.Threshold {
|
|
return fmt.Errorf("score_invalid %d cannot be greater than threshold %d", c.ScoreInvalid, c.Threshold)
|
|
}
|
|
if c.ScoreValid >= c.Threshold {
|
|
return fmt.Errorf("score_valid %d cannot be greater than threshold %d", c.ScoreValid, c.Threshold)
|
|
}
|
|
if c.ScoreLimitExceeded >= c.Threshold {
|
|
return fmt.Errorf("score_limit_exceeded %d cannot be greater than threshold %d", c.ScoreLimitExceeded, c.Threshold)
|
|
}
|
|
if c.ScoreNoAuth >= c.Threshold {
|
|
return fmt.Errorf("score_no_auth %d cannot be greater than threshold %d", c.ScoreNoAuth, c.Threshold)
|
|
}
|
|
if c.BanTime <= 0 {
|
|
return fmt.Errorf("invalid ban_time %v", c.BanTime)
|
|
}
|
|
if c.BanTimeIncrement <= 0 {
|
|
return fmt.Errorf("invalid ban_time_increment %v", c.BanTimeIncrement)
|
|
}
|
|
if c.ObservationTime <= 0 {
|
|
return fmt.Errorf("invalid observation_time %v", c.ObservationTime)
|
|
}
|
|
if c.EntriesSoftLimit <= 0 {
|
|
return fmt.Errorf("invalid entries_soft_limit %v", c.EntriesSoftLimit)
|
|
}
|
|
if c.EntriesHardLimit <= c.EntriesSoftLimit {
|
|
return fmt.Errorf("invalid entries_hard_limit %v must be > %v", c.EntriesHardLimit, c.EntriesSoftLimit)
|
|
}
|
|
|
|
return nil
|
|
}
|