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