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 7.5KB

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