3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-22 11:59:40 +01:00

Add automated client connection limiting

This commit is contained in:
Daniel Oaks 2016-10-23 23:05:00 +10:00
parent 1092caa190
commit a404bf83d1
6 changed files with 202 additions and 10 deletions

View File

@ -10,7 +10,8 @@ New release of Oragono!
### Security ### Security
### Added ### Added
* Operator classes, allowing for more finely-grained permissions for operators. * Added operator classes, allowing for more finely-grained permissions for operators.
* Added automatic client connection limiting, similar to other IRCds.
* Added support for IRCv3 capability [`chghost`](http://ircv3.net/specs/extensions/chghost-3.2.html). * Added support for IRCv3 capability [`chghost`](http://ircv3.net/specs/extensions/chghost-3.2.html).
### Changed ### Changed

View File

@ -372,6 +372,15 @@ func (client *Client) destroy() {
friends := client.Friends() friends := client.Friends()
friends.Remove(client) friends.Remove(client)
// remove from connection limits
ipaddr := net.ParseIP(IPString(client.socket.conn.RemoteAddr()))
// this check shouldn't be required but eh
if ipaddr != nil {
client.server.connectionLimitsMutex.Lock()
client.server.connectionLimits.RemoveClient(ipaddr)
client.server.connectionLimitsMutex.Unlock()
}
// remove from opers list // remove from opers list
_, exists := client.server.currentOpers[client] _, exists := client.server.currentOpers[client]
if exists { if exists {

View File

@ -89,6 +89,13 @@ func (conf *OperConfig) PasswordBytes() []byte {
return bytes return bytes
} }
type ConnectionLimitsConfig struct {
CidrLenIPv4 int `yaml:"cidr-len-ipv4"`
CidrLenIPv6 int `yaml:"cidr-len-ipv6"`
IPsPerCidr int `yaml:"ips-per-subnet"`
Exempted []string
}
type Config struct { type Config struct {
Network struct { Network struct {
Name string Name string
@ -96,14 +103,15 @@ 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"`
CheckIdent bool `yaml:"check-ident"` CheckIdent bool `yaml:"check-ident"`
Log string Log string
MOTD string MOTD string
ConnectionLimits ConnectionLimitsConfig `yaml:"connection-limits"`
} }
Datastore struct { Datastore struct {

111
irc/connection_limits.go Normal file
View File

@ -0,0 +1,111 @@
// Copyright (c) 2016- Daniel Oaks <daniel@danieloaks.net>
// released under the MIT license
package irc
import (
"errors"
"fmt"
"net"
)
var (
errTooManyClients = errors.New("Too many clients in subnet")
)
// ConnectionLimits manages the automated client connection limits.
type ConnectionLimits struct {
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
// 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 (cl *ConnectionLimits) maskAddr(addr net.IP) net.IP {
if addr.To4() == nil {
// IPv6 addr
addr = addr.Mask(cl.ipv6Mask)
} else {
// IPv4 addr
addr = addr.Mask(cl.ipv4Mask)
}
return addr
}
// 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 *ConnectionLimits) AddClient(addr net.IP, force bool) error {
// check exempted lists
// we don't track populations for exempted addresses or nets - this is by design
if cl.exemptedIPs[addr.String()] {
return nil
}
for _, ex := range cl.exemptedNets {
if ex.Contains(addr) {
return nil
}
}
// check population
cl.maskAddr(addr)
addrString := addr.String()
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 *ConnectionLimits) RemoveClient(addr net.IP) {
addrString := addr.String()
cl.population[addrString] = cl.population[addrString] - 1
// safety limiter
if cl.population[addrString] < 0 {
cl.population[addrString] = 0
}
}
// NewConnectionLimits returns a new connection limit handler.
func NewConnectionLimits(config ConnectionLimitsConfig) (*ConnectionLimits, error) {
var cl ConnectionLimits
cl.population = make(map[string]int)
cl.exemptedIPs = make(map[string]bool)
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
cl.subnetLimit = config.IPsPerCidr
// 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 {
cl.exemptedIPs[ipaddr.String()] = true
} else {
cl.exemptedNets = append(cl.exemptedNets, *netaddr)
}
}
return &cl, nil
}

View File

@ -25,6 +25,12 @@ import (
"github.com/tidwall/buntdb" "github.com/tidwall/buntdb"
) )
var (
// cached because this may be used lots
tooManyClientsMsg = ircmsg.MakeMessage(nil, "", "ERROR", "Too many clients from your IP or network")
tooManyClientsBytes, _ = tooManyClientsMsg.Line()
)
// Limits holds the maximum limits for various things such as topic lengths // Limits holds the maximum limits for various things such as topic lengths
type Limits struct { type Limits struct {
AwayLen int AwayLen int
@ -67,6 +73,8 @@ type Server struct {
clients *ClientLookupSet clients *ClientLookupSet
commands chan Command commands chan Command
configFilename string configFilename string
connectionLimits *ConnectionLimits
connectionLimitsMutex sync.Mutex // used when affecting the connection limiter, to make sure rehashing doesn't make things go out-of-whack
ctime time.Time ctime time.Time
currentOpers map[*Client]bool currentOpers map[*Client]bool
idle chan *Client idle chan *Client
@ -135,6 +143,11 @@ func NewServer(configFilename string, config *Config) *Server {
log.Fatal("Error loading operators:", err.Error()) log.Fatal("Error loading operators:", err.Error())
} }
connectionLimits, err := NewConnectionLimits(config.Server.ConnectionLimits)
if err != nil {
log.Fatal("Error loading connection limits:", err.Error())
}
server := &Server{ server := &Server{
accounts: make(map[string]*ClientAccount), accounts: make(map[string]*ClientAccount),
authenticationEnabled: config.AuthenticationEnabled, authenticationEnabled: config.AuthenticationEnabled,
@ -142,6 +155,7 @@ func NewServer(configFilename string, config *Config) *Server {
clients: NewClientLookupSet(), clients: NewClientLookupSet(),
commands: make(chan Command), commands: make(chan Command),
configFilename: configFilename, configFilename: configFilename,
connectionLimits: connectionLimits,
ctime: time.Now(), ctime: time.Now(),
currentOpers: make(map[*Client]bool), currentOpers: make(map[*Client]bool),
idle: make(chan *Client), idle: make(chan *Client),
@ -318,7 +332,21 @@ func (server *Server) Run() {
} }
case conn := <-server.newConns: case conn := <-server.newConns:
go NewClient(server, conn.Conn, conn.IsTLS) // check connection limits
ipaddr := net.ParseIP(IPString(conn.Conn.RemoteAddr()))
if ipaddr != nil {
server.connectionLimitsMutex.Lock()
err := server.connectionLimits.AddClient(ipaddr, false)
server.connectionLimitsMutex.Unlock()
if err == nil {
go NewClient(server, conn.Conn, conn.IsTLS)
continue
}
}
// too many connections from one client, tell the client and close the connection
// 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(tooManyClientsBytes))
conn.Conn.Close()
case client := <-server.idle: case client := <-server.idle:
client.Idle() client.Idle()
@ -946,6 +974,12 @@ func (server *Server) rehash() error {
return fmt.Errorf("Error rehashing config file: %s", err.Error()) return fmt.Errorf("Error rehashing config file: %s", err.Error())
} }
// confirm connectionLimits are fine
connectionLimits, err := NewConnectionLimits(config.Server.ConnectionLimits)
if err != nil {
return fmt.Errorf("Error rehashing config file: %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 {
@ -962,6 +996,18 @@ func (server *Server) rehash() error {
} }
} }
// apply new connectionlimits
server.connectionLimitsMutex.Lock()
server.connectionLimits = connectionLimits
for _, client := range server.clients.ByNick {
ipaddr := net.ParseIP(IPString(client.socket.conn.RemoteAddr()))
if ipaddr != nil {
server.connectionLimits.AddClient(ipaddr, true)
}
}
server.connectionLimitsMutex.Unlock()
// setup new and removed caps // setup new and removed caps
addedCaps := make(CapabilitySet) addedCaps := make(CapabilitySet)
removedCaps := make(CapabilitySet) removedCaps := make(CapabilitySet)

View File

@ -41,6 +41,23 @@ server:
# if you change the motd, you should move it to ircd.motd # if you change the motd, you should move it to ircd.motd
motd: oragono.motd motd: oragono.motd
# maximum number of connections per subnet
connection-limits:
# how wide the cidr should be for IPv4
cidr-len-ipv4: 24
# how wide the cidr should be for IPv6
cidr-len-ipv6: 120
# maximum number of IPs per subnet (defined above by the cird length)
ips-per-subnet: 16
# IPs/networks which are exempted from connection limits
exempted:
- "127.0.0.1"
- "127.0.0.1/8"
- "::1/128"
# whether account authentication is enabled # whether account authentication is enabled
authentication-enabled: true authentication-enabled: true