123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201 |
- // 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"
- )
- // HostEvent is the enumerable for the supported host events
- type HostEvent int
- // Supported host events
- const (
- HostEventLoginFailed HostEvent = iota
- HostEventUserNotFound
- HostEventNoLoginTried
- HostEventLimitExceeded
- )
- // 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
- }
- 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
- }
|