3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-12-12 13:59:25 +01:00
ergo/irc/connection_limits/limiter.go

137 lines
3.6 KiB
Go
Raw Normal View History

2017-03-27 14:15:02 +02:00
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
// released under the MIT license
package connection_limits
import (
"errors"
"fmt"
"net"
2017-10-09 07:47:04 +02:00
"sync"
2019-02-05 06:19:03 +01:00
"github.com/oragono/oragono/irc/utils"
)
// LimiterConfig controls the automated connection limits.
type LimiterConfig struct {
2018-02-25 11:17:39 +01:00
Enabled bool
CidrLenIPv4 int `yaml:"cidr-len-ipv4"`
CidrLenIPv6 int `yaml:"cidr-len-ipv6"`
ConnsPerSubnet int `yaml:"connections-per-subnet"`
IPsPerSubnet int `yaml:"ips-per-subnet"` // legacy name for ConnsPerSubnet
Exempted []string
}
var (
errTooManyClients = errors.New("Too many clients in subnet")
)
// Limiter manages the automated client connection limits.
type Limiter struct {
2017-10-09 07:47:04 +02:00
sync.Mutex
enabled bool
ipv4Mask net.IPMask
ipv6Mask net.IPMask
// subnetLimit is the maximum number of clients per subnet
subnetLimit int
// population holds IP -> count of clients connected from there
population map[string]int
// exemptedNets holds networks that are exempt from limits
exemptedNets []net.IPNet
}
// addrToKey canonicalizes `addr` to a string key.
func addrToKey(addr net.IP, v4Mask net.IPMask, v6Mask net.IPMask) string {
if addr.To4() != nil {
addr = addr.Mask(v4Mask) // IP.Mask() handles the 4-in-6 mapping for us
} else {
addr = addr.Mask(v6Mask)
}
return addr.String()
}
// AddClient adds a client to our population if possible. If we can't, throws an error instead.
// 'force' is used to add already-existing clients (i.e. ones that are already on the network).
func (cl *Limiter) AddClient(addr net.IP, force bool) error {
2017-10-09 07:47:04 +02:00
cl.Lock()
defer cl.Unlock()
if !cl.enabled {
return nil
}
// check exempted lists
// we don't track populations for exempted addresses or nets - this is by design
2019-02-05 06:19:03 +01:00
if utils.IPInNets(addr, cl.exemptedNets) {
return nil
}
// check population
addrString := addrToKey(addr, cl.ipv4Mask, cl.ipv6Mask)
if cl.population[addrString]+1 > cl.subnetLimit && !force {
return errTooManyClients
}
cl.population[addrString] = cl.population[addrString] + 1
return nil
}
// RemoveClient removes the given address from our population
func (cl *Limiter) RemoveClient(addr net.IP) {
2017-10-09 07:47:04 +02:00
cl.Lock()
defer cl.Unlock()
if !cl.enabled {
return
}
addrString := addrToKey(addr, cl.ipv4Mask, cl.ipv6Mask)
cl.population[addrString] = cl.population[addrString] - 1
// safety limiter
if cl.population[addrString] < 0 {
cl.population[addrString] = 0
}
}
// NewLimiter returns a new connection limit handler.
2017-10-09 07:47:04 +02:00
// The handler is functional, but disabled; it can be enabled via `ApplyConfig`.
func NewLimiter() *Limiter {
var cl Limiter
2017-10-09 07:47:04 +02:00
// initialize empty population; all other state is configurable
cl.population = make(map[string]int)
2017-10-09 07:47:04 +02:00
return &cl
}
2017-10-09 07:47:04 +02:00
// ApplyConfig atomically applies a config update to a connection limit handler
func (cl *Limiter) ApplyConfig(config LimiterConfig) error {
// assemble exempted nets
2019-02-05 06:19:03 +01:00
exemptedNets, err := utils.ParseNetList(config.Exempted)
if err != nil {
return fmt.Errorf("Could not parse limiter exemption list: %v", err.Error())
}
2017-10-09 07:47:04 +02:00
cl.Lock()
defer cl.Unlock()
cl.enabled = config.Enabled
cl.ipv4Mask = net.CIDRMask(config.CidrLenIPv4, 32)
cl.ipv6Mask = net.CIDRMask(config.CidrLenIPv6, 128)
// subnetLimit is explicitly NOT capped at a minimum of one.
// this is so that CL config can be used to allow ONLY clients from exempted IPs/nets
2018-02-25 11:17:39 +01:00
cl.subnetLimit = config.ConnsPerSubnet
// but: check if the current key was left unset, but the legacy was set:
if cl.subnetLimit == 0 && config.IPsPerSubnet != 0 {
cl.subnetLimit = config.IPsPerSubnet
}
2017-10-09 07:47:04 +02:00
cl.exemptedNets = exemptedNets
return nil
}