| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 | 
							- // Copyright (C) 2019 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) bool
 
- 	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
 
- 	DelayLogin(err error)
 
- }
 
- // 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"`
 
- 	// Configuration to impose a delay between login attempts
 
- 	LoginDelay LoginDelay `json:"login_delay" mapstructure:"login_delay"`
 
- }
 
- // LoginDelay defines the delays to impose between login attempts.
 
- type LoginDelay struct {
 
- 	// The number of milliseconds to pause prior to allowing a successful login
 
- 	Success int `json:"success" mapstructure:"success"`
 
- 	// The number of milliseconds to pause prior to reporting a failed login
 
- 	PasswordFailed int `json:"password_failed" mapstructure:"password_failed"`
 
- }
 
- 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()
 
- }
 
- // DelayLogin applies the configured login delay.
 
- func (d *baseDefender) DelayLogin(err error) {
 
- 	if err == nil {
 
- 		if d.config.LoginDelay.Success > 0 {
 
- 			time.Sleep(time.Duration(d.config.LoginDelay.Success) * time.Millisecond)
 
- 		}
 
- 		return
 
- 	}
 
- 	if d.config.LoginDelay.PasswordFailed > 0 {
 
- 		time.Sleep(time.Duration(d.config.LoginDelay.PasswordFailed) * time.Millisecond)
 
- 	}
 
- }
 
- 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
 
- }
 
 
  |