You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

dline.go 6.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. // Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
  2. // released under the MIT license
  3. package irc
  4. import (
  5. "encoding/json"
  6. "fmt"
  7. "strings"
  8. "sync"
  9. "time"
  10. "github.com/ergochat/ergo/irc/flatip"
  11. "github.com/tidwall/buntdb"
  12. )
  13. const (
  14. keyDlineEntry = "bans.dlinev2 %s"
  15. )
  16. // IPBanInfo holds info about an IP/net ban.
  17. type IPBanInfo struct {
  18. // RequireSASL indicates a "soft" ban; connections are allowed but they must SASL
  19. RequireSASL bool
  20. // Reason is the ban reason.
  21. Reason string `json:"reason"`
  22. // OperReason is an oper ban reason.
  23. OperReason string `json:"oper_reason"`
  24. // OperName is the oper who set the ban.
  25. OperName string `json:"oper_name"`
  26. // time of ban creation
  27. TimeCreated time.Time
  28. // duration of the ban; 0 means "permanent"
  29. Duration time.Duration
  30. }
  31. func (info IPBanInfo) timeLeft() time.Duration {
  32. return time.Until(info.TimeCreated.Add(info.Duration))
  33. }
  34. func (info IPBanInfo) TimeLeft() string {
  35. if info.Duration == 0 {
  36. return "indefinite"
  37. } else {
  38. return info.timeLeft().Truncate(time.Second).String()
  39. }
  40. }
  41. // BanMessage returns the ban message.
  42. func (info IPBanInfo) BanMessage(message string) string {
  43. reason := info.Reason
  44. if reason == "" {
  45. reason = "No reason given"
  46. }
  47. message = fmt.Sprintf(message, reason)
  48. if info.Duration != 0 {
  49. message += fmt.Sprintf(" [%s]", info.TimeLeft())
  50. }
  51. return message
  52. }
  53. // DLineManager manages and dlines.
  54. type DLineManager struct {
  55. sync.RWMutex // tier 1
  56. persistenceMutex sync.Mutex // tier 2
  57. // networks that are dlined:
  58. networks map[flatip.IPNet]IPBanInfo
  59. // this keeps track of expiration timers for temporary bans
  60. expirationTimers map[flatip.IPNet]*time.Timer
  61. server *Server
  62. }
  63. // NewDLineManager returns a new DLineManager.
  64. func NewDLineManager(server *Server) *DLineManager {
  65. var dm DLineManager
  66. dm.networks = make(map[flatip.IPNet]IPBanInfo)
  67. dm.expirationTimers = make(map[flatip.IPNet]*time.Timer)
  68. dm.server = server
  69. dm.loadFromDatastore()
  70. return &dm
  71. }
  72. // AllBans returns all bans (for use with APIs, etc).
  73. func (dm *DLineManager) AllBans() map[string]IPBanInfo {
  74. allb := make(map[string]IPBanInfo)
  75. dm.RLock()
  76. defer dm.RUnlock()
  77. for key, info := range dm.networks {
  78. allb[key.HumanReadableString()] = info
  79. }
  80. return allb
  81. }
  82. // AddNetwork adds a network to the blocked list.
  83. func (dm *DLineManager) AddNetwork(network flatip.IPNet, duration time.Duration, requireSASL bool, reason, operReason, operName string) error {
  84. dm.persistenceMutex.Lock()
  85. defer dm.persistenceMutex.Unlock()
  86. // assemble ban info
  87. info := IPBanInfo{
  88. RequireSASL: requireSASL,
  89. Reason: reason,
  90. OperReason: operReason,
  91. OperName: operName,
  92. TimeCreated: time.Now().UTC(),
  93. Duration: duration,
  94. }
  95. id := dm.addNetworkInternal(network, info)
  96. return dm.persistDline(id, info)
  97. }
  98. func (dm *DLineManager) addNetworkInternal(flatnet flatip.IPNet, info IPBanInfo) (id flatip.IPNet) {
  99. id = flatnet
  100. var timeLeft time.Duration
  101. if info.Duration != 0 {
  102. timeLeft = info.timeLeft()
  103. if timeLeft <= 0 {
  104. return
  105. }
  106. }
  107. dm.Lock()
  108. defer dm.Unlock()
  109. dm.networks[flatnet] = info
  110. dm.cancelTimer(flatnet)
  111. if info.Duration == 0 {
  112. return
  113. }
  114. // set up new expiration timer
  115. timeCreated := info.TimeCreated
  116. processExpiration := func() {
  117. dm.Lock()
  118. defer dm.Unlock()
  119. banInfo, ok := dm.networks[flatnet]
  120. if ok && banInfo.TimeCreated.Equal(timeCreated) {
  121. delete(dm.networks, flatnet)
  122. // TODO(slingamn) here's where we'd remove it from the radix tree
  123. delete(dm.expirationTimers, flatnet)
  124. }
  125. }
  126. dm.expirationTimers[flatnet] = time.AfterFunc(timeLeft, processExpiration)
  127. return
  128. }
  129. func (dm *DLineManager) cancelTimer(flatnet flatip.IPNet) {
  130. oldTimer := dm.expirationTimers[flatnet]
  131. if oldTimer != nil {
  132. oldTimer.Stop()
  133. delete(dm.expirationTimers, flatnet)
  134. }
  135. }
  136. func (dm *DLineManager) persistDline(id flatip.IPNet, info IPBanInfo) error {
  137. // save in datastore
  138. dlineKey := fmt.Sprintf(keyDlineEntry, id.String())
  139. // assemble json from ban info
  140. b, err := json.Marshal(info)
  141. if err != nil {
  142. dm.server.logger.Error("internal", "couldn't marshal d-line", err.Error())
  143. return err
  144. }
  145. bstr := string(b)
  146. var setOptions *buntdb.SetOptions
  147. if info.Duration != 0 {
  148. setOptions = &buntdb.SetOptions{Expires: true, TTL: info.Duration}
  149. }
  150. err = dm.server.store.Update(func(tx *buntdb.Tx) error {
  151. _, _, err := tx.Set(dlineKey, bstr, setOptions)
  152. return err
  153. })
  154. if err != nil {
  155. dm.server.logger.Error("internal", "couldn't store d-line", err.Error())
  156. }
  157. return err
  158. }
  159. func (dm *DLineManager) unpersistDline(id flatip.IPNet) error {
  160. dlineKey := fmt.Sprintf(keyDlineEntry, id.String())
  161. return dm.server.store.Update(func(tx *buntdb.Tx) error {
  162. _, err := tx.Delete(dlineKey)
  163. return err
  164. })
  165. }
  166. // RemoveNetwork removes a network from the blocked list.
  167. func (dm *DLineManager) RemoveNetwork(network flatip.IPNet) error {
  168. dm.persistenceMutex.Lock()
  169. defer dm.persistenceMutex.Unlock()
  170. id := network
  171. present := func() bool {
  172. dm.Lock()
  173. defer dm.Unlock()
  174. _, ok := dm.networks[id]
  175. delete(dm.networks, id)
  176. dm.cancelTimer(id)
  177. return ok
  178. }()
  179. if !present {
  180. return errNoExistingBan
  181. }
  182. return dm.unpersistDline(id)
  183. }
  184. // CheckIP returns whether or not an IP address was banned, and how long it is banned for.
  185. func (dm *DLineManager) CheckIP(addr flatip.IP) (isBanned bool, info IPBanInfo) {
  186. dm.RLock()
  187. defer dm.RUnlock()
  188. // check networks
  189. // TODO(slingamn) use a radix tree as the data plane for this
  190. for flatnet, info := range dm.networks {
  191. if flatnet.Contains(addr) {
  192. return true, info
  193. }
  194. }
  195. // no matches!
  196. return
  197. }
  198. func (dm *DLineManager) loadFromDatastore() {
  199. dlinePrefix := fmt.Sprintf(keyDlineEntry, "")
  200. dm.server.store.View(func(tx *buntdb.Tx) error {
  201. tx.AscendGreaterOrEqual("", dlinePrefix, func(key, value string) bool {
  202. if !strings.HasPrefix(key, dlinePrefix) {
  203. return false
  204. }
  205. // get address name
  206. key = strings.TrimPrefix(key, dlinePrefix)
  207. // load addr/net
  208. hostNet, err := flatip.ParseToNormalizedNet(key)
  209. if err != nil {
  210. dm.server.logger.Error("internal", "bad dline cidr", err.Error())
  211. return true
  212. }
  213. // load ban info
  214. var info IPBanInfo
  215. err = json.Unmarshal([]byte(value), &info)
  216. if err != nil {
  217. dm.server.logger.Error("internal", "bad dline data", err.Error())
  218. return true
  219. }
  220. // set opername if it isn't already set
  221. if info.OperName == "" {
  222. info.OperName = dm.server.name
  223. }
  224. // add to the server
  225. dm.addNetworkInternal(hostNet, info)
  226. return true
  227. })
  228. return nil
  229. })
  230. }
  231. func (s *Server) loadDLines() {
  232. s.dlines = NewDLineManager(s)
  233. }