defender.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. // Copyright (C) 2019 Nicola Murino
  2. //
  3. // This program is free software: you can redistribute it and/or modify
  4. // it under the terms of the GNU Affero General Public License as published
  5. // by the Free Software Foundation, version 3.
  6. //
  7. // This program is distributed in the hope that it will be useful,
  8. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. // GNU Affero General Public License for more details.
  11. //
  12. // You should have received a copy of the GNU Affero General Public License
  13. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  14. package common
  15. import (
  16. "fmt"
  17. "time"
  18. "github.com/drakkan/sftpgo/v2/internal/dataprovider"
  19. "github.com/drakkan/sftpgo/v2/internal/logger"
  20. )
  21. // HostEvent is the enumerable for the supported host events
  22. type HostEvent string
  23. // Supported host events
  24. const (
  25. HostEventLoginFailed HostEvent = "LoginFailed"
  26. HostEventUserNotFound HostEvent = "UserNotFound"
  27. HostEventNoLoginTried HostEvent = "NoLoginTried"
  28. HostEventLimitExceeded HostEvent = "LimitExceeded"
  29. )
  30. // Supported defender drivers
  31. const (
  32. DefenderDriverMemory = "memory"
  33. DefenderDriverProvider = "provider"
  34. )
  35. var (
  36. supportedDefenderDrivers = []string{DefenderDriverMemory, DefenderDriverProvider}
  37. )
  38. // Defender defines the interface that a defender must implements
  39. type Defender interface {
  40. GetHosts() ([]dataprovider.DefenderEntry, error)
  41. GetHost(ip string) (dataprovider.DefenderEntry, error)
  42. AddEvent(ip, protocol string, event HostEvent) bool
  43. IsBanned(ip, protocol string) bool
  44. IsSafe(ip, protocol string) bool
  45. GetBanTime(ip string) (*time.Time, error)
  46. GetScore(ip string) (int, error)
  47. DeleteHost(ip string) bool
  48. DelayLogin(err error)
  49. }
  50. // DefenderConfig defines the "defender" configuration
  51. type DefenderConfig struct {
  52. // Set to true to enable the defender
  53. Enabled bool `json:"enabled" mapstructure:"enabled"`
  54. // Defender implementation to use, we support "memory" and "provider".
  55. // Using "provider" as driver you can share the defender events among
  56. // multiple SFTPGo instances. For a single instance "memory" provider will
  57. // be much faster
  58. Driver string `json:"driver" mapstructure:"driver"`
  59. // BanTime is the number of minutes that a host is banned
  60. BanTime int `json:"ban_time" mapstructure:"ban_time"`
  61. // Percentage increase of the ban time if a banned host tries to connect again
  62. BanTimeIncrement int `json:"ban_time_increment" mapstructure:"ban_time_increment"`
  63. // Threshold value for banning a client
  64. Threshold int `json:"threshold" mapstructure:"threshold"`
  65. // Score for invalid login attempts, eg. non-existent user accounts
  66. ScoreInvalid int `json:"score_invalid" mapstructure:"score_invalid"`
  67. // Score for valid login attempts, eg. user accounts that exist
  68. ScoreValid int `json:"score_valid" mapstructure:"score_valid"`
  69. // Score for limit exceeded events, generated from the rate limiters or for max connections
  70. // per-host exceeded
  71. ScoreLimitExceeded int `json:"score_limit_exceeded" mapstructure:"score_limit_exceeded"`
  72. // ScoreNoAuth defines the score for clients disconnected without authentication
  73. // attempts
  74. ScoreNoAuth int `json:"score_no_auth" mapstructure:"score_no_auth"`
  75. // Defines the time window, in minutes, for tracking client errors.
  76. // A host is banned if it has exceeded the defined threshold during
  77. // the last observation time minutes
  78. ObservationTime int `json:"observation_time" mapstructure:"observation_time"`
  79. // The number of banned IPs and host scores kept in memory will vary between the
  80. // soft and hard limit for the "memory" driver. For the "provider" driver the
  81. // soft limit is ignored and the hard limit is used to limit the number of entries
  82. // to return when you request for the entire host list from the defender
  83. EntriesSoftLimit int `json:"entries_soft_limit" mapstructure:"entries_soft_limit"`
  84. EntriesHardLimit int `json:"entries_hard_limit" mapstructure:"entries_hard_limit"`
  85. // Configuration to impose a delay between login attempts
  86. LoginDelay LoginDelay `json:"login_delay" mapstructure:"login_delay"`
  87. }
  88. // LoginDelay defines the delays to impose between login attempts.
  89. type LoginDelay struct {
  90. // The number of milliseconds to pause prior to allowing a successful login
  91. Success int `json:"success" mapstructure:"success"`
  92. // The number of milliseconds to pause prior to reporting a failed login
  93. PasswordFailed int `json:"password_failed" mapstructure:"password_failed"`
  94. }
  95. type baseDefender struct {
  96. config *DefenderConfig
  97. ipList *dataprovider.IPList
  98. }
  99. func (d *baseDefender) isBanned(ip, protocol string) bool {
  100. isListed, mode, err := d.ipList.IsListed(ip, protocol)
  101. if err != nil {
  102. return false
  103. }
  104. if isListed && mode == dataprovider.ListModeDeny {
  105. return true
  106. }
  107. return false
  108. }
  109. func (d *baseDefender) IsSafe(ip, protocol string) bool {
  110. isListed, mode, err := d.ipList.IsListed(ip, protocol)
  111. if err == nil && isListed && mode == dataprovider.ListModeAllow {
  112. return true
  113. }
  114. return false
  115. }
  116. func (d *baseDefender) getScore(event HostEvent) int {
  117. var score int
  118. switch event {
  119. case HostEventLoginFailed:
  120. score = d.config.ScoreValid
  121. case HostEventLimitExceeded:
  122. score = d.config.ScoreLimitExceeded
  123. case HostEventUserNotFound:
  124. score = d.config.ScoreInvalid
  125. case HostEventNoLoginTried:
  126. score = d.config.ScoreNoAuth
  127. }
  128. return score
  129. }
  130. // logEvent logs a defender event that changes a host's score
  131. func (d *baseDefender) logEvent(ip, protocol string, event HostEvent, totalScore int) {
  132. // ignore events which do not change the host score
  133. eventScore := d.getScore(event)
  134. if eventScore == 0 {
  135. return
  136. }
  137. logger.GetLogger().Debug().
  138. Timestamp().
  139. Str("sender", "defender").
  140. Str("client_ip", ip).
  141. Str("protocol", protocol).
  142. Str("event", string(event)).
  143. Int("increase_score_by", eventScore).
  144. Int("score", totalScore).
  145. Send()
  146. }
  147. // logBan logs a host's ban due to a too high host score
  148. func (d *baseDefender) logBan(ip, protocol string) {
  149. logger.GetLogger().Info().
  150. Timestamp().
  151. Str("sender", "defender").
  152. Str("client_ip", ip).
  153. Str("protocol", protocol).
  154. Str("event", "banned").
  155. Send()
  156. }
  157. // DelayLogin applies the configured login delay.
  158. func (d *baseDefender) DelayLogin(err error) {
  159. if err == nil {
  160. if d.config.LoginDelay.Success > 0 {
  161. time.Sleep(time.Duration(d.config.LoginDelay.Success) * time.Millisecond)
  162. }
  163. return
  164. }
  165. if d.config.LoginDelay.PasswordFailed > 0 {
  166. time.Sleep(time.Duration(d.config.LoginDelay.PasswordFailed) * time.Millisecond)
  167. }
  168. }
  169. type hostEvent struct {
  170. dateTime time.Time
  171. score int
  172. }
  173. type hostScore struct {
  174. TotalScore int
  175. Events []hostEvent
  176. }
  177. func (c *DefenderConfig) checkScores() error {
  178. if c.ScoreInvalid < 0 {
  179. c.ScoreInvalid = 0
  180. }
  181. if c.ScoreValid < 0 {
  182. c.ScoreValid = 0
  183. }
  184. if c.ScoreLimitExceeded < 0 {
  185. c.ScoreLimitExceeded = 0
  186. }
  187. if c.ScoreNoAuth < 0 {
  188. c.ScoreNoAuth = 0
  189. }
  190. if c.ScoreInvalid == 0 && c.ScoreValid == 0 && c.ScoreLimitExceeded == 0 && c.ScoreNoAuth == 0 {
  191. return fmt.Errorf("invalid defender configuration: all scores are disabled")
  192. }
  193. return nil
  194. }
  195. // validate returns an error if the configuration is invalid
  196. func (c *DefenderConfig) validate() error {
  197. if !c.Enabled {
  198. return nil
  199. }
  200. if err := c.checkScores(); err != nil {
  201. return err
  202. }
  203. if c.ScoreInvalid >= c.Threshold {
  204. return fmt.Errorf("score_invalid %d cannot be greater than threshold %d", c.ScoreInvalid, c.Threshold)
  205. }
  206. if c.ScoreValid >= c.Threshold {
  207. return fmt.Errorf("score_valid %d cannot be greater than threshold %d", c.ScoreValid, c.Threshold)
  208. }
  209. if c.ScoreLimitExceeded >= c.Threshold {
  210. return fmt.Errorf("score_limit_exceeded %d cannot be greater than threshold %d", c.ScoreLimitExceeded, c.Threshold)
  211. }
  212. if c.ScoreNoAuth >= c.Threshold {
  213. return fmt.Errorf("score_no_auth %d cannot be greater than threshold %d", c.ScoreNoAuth, c.Threshold)
  214. }
  215. if c.BanTime <= 0 {
  216. return fmt.Errorf("invalid ban_time %v", c.BanTime)
  217. }
  218. if c.BanTimeIncrement <= 0 {
  219. return fmt.Errorf("invalid ban_time_increment %v", c.BanTimeIncrement)
  220. }
  221. if c.ObservationTime <= 0 {
  222. return fmt.Errorf("invalid observation_time %v", c.ObservationTime)
  223. }
  224. if c.EntriesSoftLimit <= 0 {
  225. return fmt.Errorf("invalid entries_soft_limit %v", c.EntriesSoftLimit)
  226. }
  227. if c.EntriesHardLimit <= c.EntriesSoftLimit {
  228. return fmt.Errorf("invalid entries_hard_limit %v must be > %v", c.EntriesHardLimit, c.EntriesSoftLimit)
  229. }
  230. return nil
  231. }