123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177 |
- // 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 (
- "sync/atomic"
- "time"
- "github.com/drakkan/sftpgo/v2/internal/dataprovider"
- "github.com/drakkan/sftpgo/v2/internal/logger"
- "github.com/drakkan/sftpgo/v2/internal/util"
- )
- type dbDefender struct {
- baseDefender
- lastCleanup atomic.Int64
- }
- func newDBDefender(config *DefenderConfig) (Defender, error) {
- err := config.validate()
- if err != nil {
- return nil, err
- }
- ipList, err := dataprovider.NewIPList(dataprovider.IPListTypeDefender)
- if err != nil {
- return nil, err
- }
- defender := &dbDefender{
- baseDefender: baseDefender{
- config: config,
- ipList: ipList,
- },
- }
- defender.lastCleanup.Store(0)
- return defender, nil
- }
- // GetHosts returns hosts that are banned or for which some violations have been detected
- func (d *dbDefender) GetHosts() ([]dataprovider.DefenderEntry, error) {
- return dataprovider.GetDefenderHosts(d.getStartObservationTime(), d.config.EntriesHardLimit)
- }
- // GetHost returns a defender host by ip, if any
- func (d *dbDefender) GetHost(ip string) (dataprovider.DefenderEntry, error) {
- return dataprovider.GetDefenderHostByIP(ip, d.getStartObservationTime())
- }
- // IsBanned returns true if the specified IP is banned
- // and increase ban time if the IP is found.
- // This method must be called as soon as the client connects
- func (d *dbDefender) IsBanned(ip, protocol string) bool {
- if d.baseDefender.isBanned(ip, protocol) {
- return true
- }
- _, err := dataprovider.IsDefenderHostBanned(ip)
- if err != nil {
- // not found or another error, we allow this host
- return false
- }
- increment := d.config.BanTime * d.config.BanTimeIncrement / 100
- if increment == 0 {
- increment++
- }
- dataprovider.UpdateDefenderBanTime(ip, increment) //nolint:errcheck
- return true
- }
- // DeleteHost removes the specified IP from the defender lists
- func (d *dbDefender) DeleteHost(ip string) bool {
- if _, err := d.GetHost(ip); err != nil {
- return false
- }
- return dataprovider.DeleteDefenderHost(ip) == nil
- }
- // AddEvent adds an event for the given IP.
- // This method must be called for clients not yet banned
- func (d *dbDefender) AddEvent(ip, protocol string, event HostEvent) {
- if d.IsSafe(ip, protocol) {
- return
- }
- score := d.baseDefender.getScore(event)
- host, err := dataprovider.AddDefenderEvent(ip, score, d.getStartObservationTime())
- if err != nil {
- return
- }
- if host.Score > d.config.Threshold {
- banTime := time.Now().Add(time.Duration(d.config.BanTime) * time.Minute)
- err = dataprovider.SetDefenderBanTime(ip, util.GetTimeAsMsSinceEpoch(banTime))
- if err == nil {
- eventManager.handleIPBlockedEvent(EventParams{
- Event: ipBlockedEventName,
- IP: ip,
- Timestamp: time.Now().UnixNano(),
- Status: 1,
- })
- }
- }
- if err == nil {
- d.cleanup()
- }
- }
- // GetBanTime returns the ban time for the given IP or nil if the IP is not banned
- func (d *dbDefender) GetBanTime(ip string) (*time.Time, error) {
- host, err := d.GetHost(ip)
- if err != nil {
- return nil, err
- }
- if host.BanTime.IsZero() {
- return nil, nil
- }
- return &host.BanTime, nil
- }
- // GetScore returns the score for the given IP
- func (d *dbDefender) GetScore(ip string) (int, error) {
- host, err := d.GetHost(ip)
- if err != nil {
- return 0, err
- }
- return host.Score, nil
- }
- func (d *dbDefender) cleanup() {
- lastCleanup := d.getLastCleanup()
- if lastCleanup.IsZero() || lastCleanup.Add(time.Duration(d.config.ObservationTime)*time.Minute*3).Before(time.Now()) {
- // FIXME: this could be racy in rare cases but it is better than acquire the lock for the cleanup duration
- // or to always acquire a read/write lock.
- // Concurrent cleanups could happen anyway from multiple SFTPGo instances and should not cause any issues
- d.setLastCleanup(time.Now())
- expireTime := time.Now().Add(-time.Duration(d.config.ObservationTime+1) * time.Minute)
- logger.Debug(logSender, "", "cleanup defender hosts before %v, last cleanup %v", expireTime, lastCleanup)
- if err := dataprovider.CleanupDefender(util.GetTimeAsMsSinceEpoch(expireTime)); err != nil {
- logger.Error(logSender, "", "defender cleanup error, reset last cleanup to %v", lastCleanup)
- d.setLastCleanup(lastCleanup)
- }
- }
- }
- func (d *dbDefender) getStartObservationTime() int64 {
- t := time.Now().Add(-time.Duration(d.config.ObservationTime) * time.Minute)
- return util.GetTimeAsMsSinceEpoch(t)
- }
- func (d *dbDefender) getLastCleanup() time.Time {
- val := d.lastCleanup.Load()
- if val == 0 {
- return time.Time{}
- }
- return util.GetTimeFromMsecSinceEpoch(val)
- }
- func (d *dbDefender) setLastCleanup(when time.Time) {
- if when.IsZero() {
- d.lastCleanup.Store(0)
- return
- }
- d.lastCleanup.Store(util.GetTimeAsMsSinceEpoch(when))
- }
|