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.3KB

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