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:
parent
1092caa190
commit
a404bf83d1
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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
111
irc/connection_limits.go
Normal 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
|
||||||
|
}
|
@ -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)
|
||||||
|
17
oragono.yaml
17
oragono.yaml
@ -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
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user