mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-22 15:40:23 +00:00
1b1745b7f7
this is a backward incompatible change, all previous file based IP/network lists will not work anymore Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
177 lines
5.2 KiB
Go
177 lines
5.2 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 (
|
|
"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))
|
|
}
|