123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- // Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
- // released under the MIT license
-
- package irc
-
- import (
- "encoding/json"
- "fmt"
- "net"
- "strings"
- "sync"
- "time"
-
- "github.com/oragono/oragono/irc/utils"
- "github.com/tidwall/buntdb"
- )
-
- const (
- keyDlineEntry = "bans.dlinev2 %s"
- )
-
- // IPBanInfo holds info about an IP/net ban.
- type IPBanInfo struct {
- // Reason is the ban reason.
- Reason string `json:"reason"`
- // OperReason is an oper ban reason.
- OperReason string `json:"oper_reason"`
- // OperName is the oper who set the ban.
- OperName string `json:"oper_name"`
- // time of ban creation
- TimeCreated time.Time
- // duration of the ban; 0 means "permanent"
- Duration time.Duration
- }
-
- func (info IPBanInfo) timeLeft() time.Duration {
- return info.TimeCreated.Add(info.Duration).Sub(time.Now())
- }
-
- func (info IPBanInfo) TimeLeft() string {
- if info.Duration == 0 {
- return "indefinite"
- } else {
- return info.timeLeft().Truncate(time.Second).String()
- }
- }
-
- // BanMessage returns the ban message.
- func (info IPBanInfo) BanMessage(message string) string {
- message = fmt.Sprintf(message, info.Reason)
- if info.Duration != 0 {
- message += fmt.Sprintf(" [%s]", info.TimeLeft())
- }
- return message
- }
-
- // dLineNet contains the net itself and expiration time for a given network.
- type dLineNet struct {
- // Network is the network that is blocked.
- // This is always an IPv6 CIDR; IPv4 CIDRs are translated with the 4-in-6 prefix,
- // individual IPv4 and IPV6 addresses are translated to the relevant /128.
- Network net.IPNet
- // Info contains information on the ban.
- Info IPBanInfo
- }
-
- // DLineManager manages and dlines.
- type DLineManager struct {
- sync.RWMutex // tier 1
- persistenceMutex sync.Mutex // tier 2
- // networks that are dlined:
- // XXX: the keys of this map (which are also the database persistence keys)
- // are the human-readable representations returned by NetToNormalizedString
- networks map[string]dLineNet
- // this keeps track of expiration timers for temporary bans
- expirationTimers map[string]*time.Timer
- server *Server
- }
-
- // NewDLineManager returns a new DLineManager.
- func NewDLineManager(server *Server) *DLineManager {
- var dm DLineManager
- dm.networks = make(map[string]dLineNet)
- dm.expirationTimers = make(map[string]*time.Timer)
- dm.server = server
-
- dm.loadFromDatastore()
-
- return &dm
- }
-
- // AllBans returns all bans (for use with APIs, etc).
- func (dm *DLineManager) AllBans() map[string]IPBanInfo {
- allb := make(map[string]IPBanInfo)
-
- dm.RLock()
- defer dm.RUnlock()
-
- // map keys are already the human-readable forms, just return a copy of the map
- for key, info := range dm.networks {
- allb[key] = info.Info
- }
-
- return allb
- }
-
- // AddNetwork adds a network to the blocked list.
- func (dm *DLineManager) AddNetwork(network net.IPNet, duration time.Duration, reason, operReason, operName string) error {
- dm.persistenceMutex.Lock()
- defer dm.persistenceMutex.Unlock()
-
- // assemble ban info
- info := IPBanInfo{
- Reason: reason,
- OperReason: operReason,
- OperName: operName,
- TimeCreated: time.Now(),
- Duration: duration,
- }
-
- id := dm.addNetworkInternal(network, info)
- return dm.persistDline(id, info)
- }
-
- func (dm *DLineManager) addNetworkInternal(network net.IPNet, info IPBanInfo) (id string) {
- network = utils.NormalizeNet(network)
- id = utils.NetToNormalizedString(network)
-
- var timeLeft time.Duration
- if info.Duration != 0 {
- timeLeft = info.timeLeft()
- if timeLeft <= 0 {
- return
- }
- }
-
- dm.Lock()
- defer dm.Unlock()
-
- dm.networks[id] = dLineNet{
- Network: network,
- Info: info,
- }
-
- dm.cancelTimer(id)
-
- if info.Duration == 0 {
- return
- }
-
- // set up new expiration timer
- timeCreated := info.TimeCreated
- processExpiration := func() {
- dm.Lock()
- defer dm.Unlock()
-
- netBan, ok := dm.networks[id]
- if ok && netBan.Info.TimeCreated.Equal(timeCreated) {
- delete(dm.networks, id)
- // TODO(slingamn) here's where we'd remove it from the radix tree
- delete(dm.expirationTimers, id)
- }
- }
- dm.expirationTimers[id] = time.AfterFunc(timeLeft, processExpiration)
-
- return
- }
-
- func (dm *DLineManager) cancelTimer(id string) {
- oldTimer := dm.expirationTimers[id]
- if oldTimer != nil {
- oldTimer.Stop()
- delete(dm.expirationTimers, id)
- }
- }
-
- func (dm *DLineManager) persistDline(id string, info IPBanInfo) error {
- // save in datastore
- dlineKey := fmt.Sprintf(keyDlineEntry, id)
- // assemble json from ban info
- b, err := json.Marshal(info)
- if err != nil {
- dm.server.logger.Error("internal", "couldn't marshal d-line", err.Error())
- return err
- }
- bstr := string(b)
- var setOptions *buntdb.SetOptions
- if info.Duration != 0 {
- setOptions = &buntdb.SetOptions{Expires: true, TTL: info.Duration}
- }
-
- err = dm.server.store.Update(func(tx *buntdb.Tx) error {
- _, _, err := tx.Set(dlineKey, bstr, setOptions)
- return err
- })
- if err != nil {
- dm.server.logger.Error("internal", "couldn't store d-line", err.Error())
- }
- return err
- }
-
- func (dm *DLineManager) unpersistDline(id string) error {
- dlineKey := fmt.Sprintf(keyDlineEntry, id)
- return dm.server.store.Update(func(tx *buntdb.Tx) error {
- _, err := tx.Delete(dlineKey)
- return err
- })
- }
-
- // RemoveNetwork removes a network from the blocked list.
- func (dm *DLineManager) RemoveNetwork(network net.IPNet) error {
- dm.persistenceMutex.Lock()
- defer dm.persistenceMutex.Unlock()
-
- id := utils.NetToNormalizedString(utils.NormalizeNet(network))
-
- present := func() bool {
- dm.Lock()
- defer dm.Unlock()
- _, ok := dm.networks[id]
- delete(dm.networks, id)
- dm.cancelTimer(id)
- return ok
- }()
-
- if !present {
- return errNoExistingBan
- }
-
- return dm.unpersistDline(id)
- }
-
- // AddIP adds an IP address to the blocked list.
- func (dm *DLineManager) AddIP(addr net.IP, duration time.Duration, reason, operReason, operName string) error {
- return dm.AddNetwork(utils.NormalizeIPToNet(addr), duration, reason, operReason, operName)
- }
-
- // RemoveIP removes an IP address from the blocked list.
- func (dm *DLineManager) RemoveIP(addr net.IP) error {
- return dm.RemoveNetwork(utils.NormalizeIPToNet(addr))
- }
-
- // CheckIP returns whether or not an IP address was banned, and how long it is banned for.
- func (dm *DLineManager) CheckIP(addr net.IP) (isBanned bool, info IPBanInfo) {
- addr = addr.To16() // almost certainly unnecessary
-
- dm.RLock()
- defer dm.RUnlock()
-
- // check networks
- // TODO(slingamn) use a radix tree as the data plane for this
- for _, netBan := range dm.networks {
- if netBan.Network.Contains(addr) {
- return true, netBan.Info
- }
- }
- // no matches!
- isBanned = false
- return
- }
-
- func (dm *DLineManager) loadFromDatastore() {
- dlinePrefix := fmt.Sprintf(keyDlineEntry, "")
- dm.server.store.View(func(tx *buntdb.Tx) error {
- tx.AscendGreaterOrEqual("", dlinePrefix, func(key, value string) bool {
- if !strings.HasPrefix(key, dlinePrefix) {
- return false
- }
-
- // get address name
- key = strings.TrimPrefix(key, dlinePrefix)
-
- // load addr/net
- hostNet, err := utils.NormalizedNetFromString(key)
- if err != nil {
- dm.server.logger.Error("internal", "bad dline cidr", err.Error())
- return true
- }
-
- // load ban info
- var info IPBanInfo
- err = json.Unmarshal([]byte(value), &info)
- if err != nil {
- dm.server.logger.Error("internal", "bad dline data", err.Error())
- return true
- }
-
- // set opername if it isn't already set
- if info.OperName == "" {
- info.OperName = dm.server.name
- }
-
- // add to the server
- dm.addNetworkInternal(hostNet, info)
-
- return true
- })
- return nil
- })
- }
-
- func (s *Server) loadDLines() {
- s.dlines = NewDLineManager(s)
- }
|