Add initial automated connection throttling

This commit is contained in:
Daniel Oaks 2017-01-12 17:40:01 +10:00
parent ddba5af265
commit 91d59575ce
6 changed files with 301 additions and 53 deletions

View File

@ -11,15 +11,17 @@ New release of Oragono!
### Added ### Added
* Added ARM build (for Raspberry PIs and similar). * Added ARM build (for Raspberry PIs and similar).
* Added `KLINE` and `UNDLINE` commands. Complementing `KLINE`, this lets you ban masks from the server. * Added automated connection throttling! To enable this, copy the `connection-throttling` section from the config.
* Added `KLINE` and `UNDLINE` commands. Complementing `DLINE`'s per-IP and per-network bans, this lets you ban masks from the server.
### Changed ### Changed
* Connection limits can now be freely enabled or disabled. To enable automated limit handling, see the new `enabled` flag in the config, under `connection-limits`.
### Removed ### Removed
### Fixed ### Fixed
* Fixed an issue where `UNDLINE` didn't save across server launches. * Fixed an issue where `UNDLINE` didn't save across server launches.
* Removed several race conditions and made the server more resiliant to these bugs. * Removed several race conditions which could result in server panics.
## [0.5.0] - 2016-12-10 ## [0.5.0] - 2016-12-10

View File

@ -12,6 +12,7 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"strings" "strings"
"time"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
@ -95,12 +96,26 @@ type RestAPIConfig struct {
} }
type ConnectionLimitsConfig struct { type ConnectionLimitsConfig struct {
Enabled bool
CidrLenIPv4 int `yaml:"cidr-len-ipv4"` CidrLenIPv4 int `yaml:"cidr-len-ipv4"`
CidrLenIPv6 int `yaml:"cidr-len-ipv6"` CidrLenIPv6 int `yaml:"cidr-len-ipv6"`
IPsPerCidr int `yaml:"ips-per-subnet"` IPsPerCidr int `yaml:"ips-per-subnet"`
Exempted []string Exempted []string
} }
type ConnectionThrottleConfig struct {
Enabled bool
CidrLenIPv4 int `yaml:"cidr-len-ipv4"`
CidrLenIPv6 int `yaml:"cidr-len-ipv6"`
ConnectionsPerCidr int `yaml:"max-connections"`
DurationString string `yaml:"duration"`
Duration time.Duration `yaml:"duration-time"`
BanDurationString string `yaml:"ban-duration"`
BanDuration time.Duration
BanMessage string `yaml:"ban-message"`
Exempted []string
}
type Config struct { type Config struct {
Network struct { Network struct {
Name string Name string
@ -108,16 +123,17 @@ type Config struct {
Server struct { Server struct {
PassConfig PassConfig
Password string Password string
Name string Name string
Listen []string Listen []string
Wslisten string `yaml:"ws-listen"` Wslisten string `yaml:"ws-listen"`
TLSListeners map[string]*TLSListenConfig `yaml:"tls-listeners"` TLSListeners map[string]*TLSListenConfig `yaml:"tls-listeners"`
RestAPI RestAPIConfig `yaml:"rest-api"` RestAPI RestAPIConfig `yaml:"rest-api"`
CheckIdent bool `yaml:"check-ident"` CheckIdent bool `yaml:"check-ident"`
Log string Log string
MOTD string MOTD string
ConnectionLimits ConnectionLimitsConfig `yaml:"connection-limits"` ConnectionLimits ConnectionLimitsConfig `yaml:"connection-limits"`
ConnectionThrottle ConnectionThrottleConfig `yaml:"connection-throttling"`
} }
Datastore struct { Datastore struct {
@ -309,6 +325,16 @@ func LoadConfig(filename string) (config *Config, err error) {
if config.Limits.NickLen < 1 || config.Limits.ChannelLen < 2 || config.Limits.AwayLen < 1 || config.Limits.KickLen < 1 || config.Limits.TopicLen < 1 { if config.Limits.NickLen < 1 || config.Limits.ChannelLen < 2 || config.Limits.AwayLen < 1 || config.Limits.KickLen < 1 || config.Limits.TopicLen < 1 {
return nil, errors.New("Limits aren't setup properly, check them and make them sane") return nil, errors.New("Limits aren't setup properly, check them and make them sane")
} }
if config.Server.ConnectionThrottle.Enabled {
config.Server.ConnectionThrottle.Duration, err = time.ParseDuration(config.Server.ConnectionThrottle.DurationString)
if err != nil {
return nil, fmt.Errorf("Could not parse connection-throttle duration: %s", err.Error())
}
config.Server.ConnectionThrottle.BanDuration, err = time.ParseDuration(config.Server.ConnectionThrottle.BanDurationString)
if err != nil {
return nil, fmt.Errorf("Could not parse connection-throttle ban-duration: %s", err.Error())
}
}
return config, nil return config, nil
} }

View File

@ -15,6 +15,7 @@ var (
// ConnectionLimits manages the automated client connection limits. // ConnectionLimits manages the automated client connection limits.
type ConnectionLimits struct { type ConnectionLimits struct {
enabled bool
ipv4Mask net.IPMask ipv4Mask net.IPMask
ipv6Mask net.IPMask ipv6Mask net.IPMask
// subnetLimit is the maximum number of clients per subnet // subnetLimit is the maximum number of clients per subnet
@ -44,6 +45,10 @@ func (cl *ConnectionLimits) maskAddr(addr net.IP) net.IP {
// AddClient adds a client to our population if possible. If we can't, throws an error instead. // 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). // 'force' is used to add already-existing clients (i.e. ones that are already on the network).
func (cl *ConnectionLimits) AddClient(addr net.IP, force bool) error { func (cl *ConnectionLimits) AddClient(addr net.IP, force bool) error {
if !cl.enabled {
return nil
}
// check exempted lists // check exempted lists
// we don't track populations for exempted addresses or nets - this is by design // we don't track populations for exempted addresses or nets - this is by design
if cl.exemptedIPs[addr.String()] { if cl.exemptedIPs[addr.String()] {
@ -70,6 +75,10 @@ func (cl *ConnectionLimits) AddClient(addr net.IP, force bool) error {
// RemoveClient removes the given address from our population // RemoveClient removes the given address from our population
func (cl *ConnectionLimits) RemoveClient(addr net.IP) { func (cl *ConnectionLimits) RemoveClient(addr net.IP) {
if !cl.enabled {
return
}
addrString := addr.String() addrString := addr.String()
cl.population[addrString] = cl.population[addrString] - 1 cl.population[addrString] = cl.population[addrString] - 1
@ -82,6 +91,8 @@ func (cl *ConnectionLimits) RemoveClient(addr net.IP) {
// NewConnectionLimits returns a new connection limit handler. // NewConnectionLimits returns a new connection limit handler.
func NewConnectionLimits(config ConnectionLimitsConfig) (*ConnectionLimits, error) { func NewConnectionLimits(config ConnectionLimitsConfig) (*ConnectionLimits, error) {
var cl ConnectionLimits var cl ConnectionLimits
cl.enabled = config.Enabled
cl.population = make(map[string]int) cl.population = make(map[string]int)
cl.exemptedIPs = make(map[string]bool) cl.exemptedIPs = make(map[string]bool)

View File

@ -0,0 +1,142 @@
// Copyright (c) 2016- Daniel Oaks <daniel@danieloaks.net>
// released under the MIT license
package irc
import (
"fmt"
"net"
"time"
"github.com/DanielOaks/girc-go/ircmsg"
)
// ThrottleDetails holds the connection-throttling details for a subnet/IP.
type ThrottleDetails struct {
Start time.Time
ClientCount int
}
// ConnectionThrottle manages automated client connection throttling.
type ConnectionThrottle struct {
enabled bool
ipv4Mask net.IPMask
ipv6Mask net.IPMask
subnetLimit int
duration time.Duration
population map[string]ThrottleDetails
// used by the server to ban clients that go over this limit
BanDuration time.Duration
BanMessage string
BanMessageBytes []byte
// exemptedIPs holds IPs that are exempt from limits
exemptedIPs map[string]bool
// exemptedNets holds networks that are exempt from limits
exemptedNets []net.IPNet
}
// maskAddr masks the given IPv4/6 address with our cidr limit masks.
func (ct *ConnectionThrottle) maskAddr(addr net.IP) net.IP {
if addr.To4() == nil {
// IPv6 addr
addr = addr.Mask(ct.ipv6Mask)
} else {
// IPv4 addr
addr = addr.Mask(ct.ipv4Mask)
}
return addr
}
// ResetFor removes any existing count for the given address.
func (ct *ConnectionThrottle) ResetFor(addr net.IP) {
if !ct.enabled {
return
}
// remove
ct.maskAddr(addr)
addrString := addr.String()
delete(ct.population, addrString)
}
// AddClient introduces a new client connection if possible. If we can't, throws an error instead.
func (ct *ConnectionThrottle) AddClient(addr net.IP) error {
if !ct.enabled {
return nil
}
// check exempted lists
if ct.exemptedIPs[addr.String()] {
return nil
}
for _, ex := range ct.exemptedNets {
if ex.Contains(addr) {
return nil
}
}
// check throttle
ct.maskAddr(addr)
addrString := addr.String()
details, exists := ct.population[addrString]
if !exists || details.Start.Add(ct.duration).Before(time.Now()) {
details = ThrottleDetails{
Start: time.Now(),
}
}
if details.ClientCount+1 > ct.subnetLimit {
return errTooManyClients
}
details.ClientCount++
ct.population[addrString] = details
return nil
}
// NewConnectionThrottle returns a new client connection throttler.
func NewConnectionThrottle(config ConnectionThrottleConfig) (*ConnectionThrottle, error) {
var ct ConnectionThrottle
ct.enabled = config.Enabled
ct.population = make(map[string]ThrottleDetails)
ct.exemptedIPs = make(map[string]bool)
ct.ipv4Mask = net.CIDRMask(config.CidrLenIPv4, 32)
ct.ipv6Mask = net.CIDRMask(config.CidrLenIPv6, 128)
ct.subnetLimit = config.ConnectionsPerCidr
ct.duration = config.Duration
ct.BanDuration = config.BanDuration
ct.BanMessage = config.BanMessage
ircmsgOutput := ircmsg.MakeMessage(nil, "", "ERROR", ct.BanMessage)
msg, err := ircmsgOutput.Line()
if err != nil {
return nil, fmt.Errorf("Could not make error message: %s", err.Error())
}
ct.BanMessageBytes = []byte(msg)
// assemble exempted nets
for _, cidr := range config.Exempted {
ipaddr := net.ParseIP(cidr)
_, netaddr, err := net.ParseCIDR(cidr)
if ipaddr == nil && err != nil {
return nil, fmt.Errorf("Could not parse exempted IP/network [%s]", cidr)
}
if ipaddr != nil {
ct.exemptedIPs[ipaddr.String()] = true
} else {
ct.exemptedNets = append(ct.exemptedNets, *netaddr)
}
}
return &ct, nil
}

View File

@ -28,7 +28,7 @@ import (
var ( var (
// cached because this may be used lots // cached because this may be used lots
tooManyClientsMsg = ircmsg.MakeMessage(nil, "", "ERROR", "Too many clients from your IP or network") tooManyClientsMsg = ircmsg.MakeMessage(nil, "", "ERROR", "Too many clients from your network")
tooManyClientsBytes, _ = tooManyClientsMsg.Line() tooManyClientsBytes, _ = tooManyClientsMsg.Line()
bannedFromServerMsg = ircmsg.MakeMessage(nil, "", "ERROR", "You are banned from this server (%s)") bannedFromServerMsg = ircmsg.MakeMessage(nil, "", "ERROR", "You are banned from this server (%s)")
@ -72,42 +72,44 @@ type ListenerEvent struct {
// Server is the main Oragono server. // Server is the main Oragono server.
type Server struct { type Server struct {
accountRegistration *AccountRegistration accountRegistration *AccountRegistration
accounts map[string]*ClientAccount accounts map[string]*ClientAccount
authenticationEnabled bool authenticationEnabled bool
channels ChannelNameMap channels ChannelNameMap
checkIdent bool checkIdent bool
clients *ClientLookupSet clients *ClientLookupSet
commands chan Command commands chan Command
configFilename string configFilename string
connectionLimits *ConnectionLimits connectionThrottle *ConnectionThrottle
connectionLimitsMutex sync.Mutex // used when affecting the connection limiter, to make sure rehashing doesn't make things go out-of-whack connectionThrottleMutex sync.Mutex // used when affecting the connection limiter, to make sure rehashing doesn't make things go out-of-whack
ctime time.Time connectionLimits *ConnectionLimits
currentOpers map[*Client]bool connectionLimitsMutex sync.Mutex // used when affecting the connection limiter, to make sure rehashing doesn't make things go out-of-whack
dlines *DLineManager ctime time.Time
idle chan *Client currentOpers map[*Client]bool
isupport *ISupportList dlines *DLineManager
klines *KLineManager idle chan *Client
limits Limits isupport *ISupportList
listenerEventActMutex sync.Mutex klines *KLineManager
listeners map[string]ListenerInterface limits Limits
listenerUpdateMutex sync.Mutex listenerEventActMutex sync.Mutex
monitoring map[string][]Client listeners map[string]ListenerInterface
motdLines []string listenerUpdateMutex sync.Mutex
name string monitoring map[string][]Client
nameCasefolded string motdLines []string
networkName string name string
newConns chan clientConn nameCasefolded string
operators map[string]Oper networkName string
operclasses map[string]OperClass newConns chan clientConn
password []byte operators map[string]Oper
passwords *PasswordManager operclasses map[string]OperClass
rehashMutex sync.Mutex password []byte
rehashSignal chan os.Signal passwords *PasswordManager
restAPI *RestAPIConfig rehashMutex sync.Mutex
signals chan os.Signal rehashSignal chan os.Signal
store *buntdb.DB restAPI *RestAPIConfig
whoWas *WhoWasList signals chan os.Signal
store *buntdb.DB
whoWas *WhoWasList
} }
var ( var (
@ -157,6 +159,10 @@ func NewServer(configFilename string, config *Config) *Server {
if err != nil { if err != nil {
log.Fatal("Error loading connection limits:", err.Error()) log.Fatal("Error loading connection limits:", err.Error())
} }
connectionThrottle, err := NewConnectionThrottle(config.Server.ConnectionThrottle)
if err != nil {
log.Fatal("Error loading connection throttler:", err.Error())
}
server := &Server{ server := &Server{
accounts: make(map[string]*ClientAccount), accounts: make(map[string]*ClientAccount),
@ -166,6 +172,7 @@ func NewServer(configFilename string, config *Config) *Server {
commands: make(chan Command), commands: make(chan Command),
configFilename: configFilename, configFilename: configFilename,
connectionLimits: connectionLimits, connectionLimits: connectionLimits,
connectionThrottle: connectionThrottle,
ctime: time.Now(), ctime: time.Now(),
currentOpers: make(map[*Client]bool), currentOpers: make(map[*Client]bool),
idle: make(chan *Client), idle: make(chan *Client),
@ -403,6 +410,27 @@ func (server *Server) Run() {
continue continue
} }
// check connection throttle
server.connectionThrottleMutex.Lock()
err = server.connectionThrottle.AddClient(ipaddr)
server.connectionThrottleMutex.Unlock()
if err != nil {
// too many connections too quickly from client, tell them and close the connection
length := &IPRestrictTime{
Duration: server.connectionThrottle.BanDuration,
Expires: time.Now().Add(server.connectionThrottle.BanDuration),
}
server.dlines.AddIP(ipaddr, length, server.connectionThrottle.BanMessage, "Exceeded automated connection throttle")
// reset ban on connectionThrottle
server.connectionThrottle.ResetFor(ipaddr)
// this might not show up properly on some clients, but our objective here is just to close it out before it has a load impact on us
conn.Conn.Write([]byte(server.connectionThrottle.BanMessageBytes))
conn.Conn.Close()
continue
}
go NewClient(server, conn.Conn, conn.IsTLS) go NewClient(server, conn.Conn, conn.IsTLS)
continue continue
} }
@ -1066,23 +1094,29 @@ func (server *Server) rehash() error {
config, err := LoadConfig(server.configFilename) config, err := LoadConfig(server.configFilename)
if err != nil { if err != nil {
return fmt.Errorf("Error rehashing config file: %s", err.Error()) return fmt.Errorf("Error rehashing config file config: %s", err.Error())
} }
// confirm connectionLimits are fine // confirm connectionLimits are fine
connectionLimits, err := NewConnectionLimits(config.Server.ConnectionLimits) connectionLimits, err := NewConnectionLimits(config.Server.ConnectionLimits)
if err != nil { if err != nil {
return fmt.Errorf("Error rehashing config file: %s", err.Error()) return fmt.Errorf("Error rehashing config file connection-limits: %s", err.Error())
}
// confirm connectionThrottler is fine
connectionThrottle, err := NewConnectionThrottle(config.Server.ConnectionThrottle)
if err != nil {
return fmt.Errorf("Error rehashing config file connection-throttle: %s", err.Error())
} }
// confirm operator stuff all exists and is fine // confirm operator stuff all exists and is fine
operclasses, err := config.OperatorClasses() operclasses, err := config.OperatorClasses()
if err != nil { if err != nil {
return fmt.Errorf("Error rehashing config file: %s", err.Error()) return fmt.Errorf("Error rehashing config file operclasses: %s", err.Error())
} }
opers, err := config.Operators(operclasses) opers, err := config.Operators(operclasses)
if err != nil { if err != nil {
return fmt.Errorf("Error rehashing config file: %s", err.Error()) return fmt.Errorf("Error rehashing config file opers: %s", err.Error())
} }
for client := range server.currentOpers { for client := range server.currentOpers {
_, exists := opers[client.operName] _, exists := opers[client.operName]
@ -1094,6 +1128,8 @@ func (server *Server) rehash() error {
// apply new connectionlimits // apply new connectionlimits
server.connectionLimitsMutex.Lock() server.connectionLimitsMutex.Lock()
server.connectionLimits = connectionLimits server.connectionLimits = connectionLimits
server.connectionThrottleMutex.Lock()
server.connectionThrottle = connectionThrottle
server.clients.ByNickMutex.RLock() server.clients.ByNickMutex.RLock()
for _, client := range server.clients.ByNick { for _, client := range server.clients.ByNick {

View File

@ -51,6 +51,9 @@ server:
# maximum number of connections per subnet # maximum number of connections per subnet
connection-limits: connection-limits:
# whether to throttle limits or not
enabled: true
# how wide the cidr should be for IPv4 # how wide the cidr should be for IPv4
cidr-len-ipv4: 24 cidr-len-ipv4: 24
@ -66,6 +69,34 @@ server:
- "127.0.0.1/8" - "127.0.0.1/8"
- "::1/128" - "::1/128"
# automated connection throttling
connection-throttling:
# whether to throttle connections or not
enabled: true
# how wide the cidr should be for IPv4
cidr-len-ipv4: 32
# how wide the cidr should be for IPv6
cidr-len-ipv6: 128
# how long to keep track of connections for
duration: 10m
# maximum number of connections, per subnet, within the given duration
max-connections: 12
# how long to ban offenders for, and the message to use
# after banning them, the number of connections is reset (which lets you use UNDLINE to unban people)
ban-duration: 10m
ban-message: You have attempted to connect too many times within a short duration. Wait a while, and you will be able to connect.
# IPs/networks which are exempted from connection limits
exempted:
- "127.0.0.1"
- "127.0.0.1/8"
- "::1/128"
# account/channel registration # account/channel registration
registration: registration:
# account registration # account registration