diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a600d6f..aa059872 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ New release of Oragono! ### Security ### 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). ### Changed diff --git a/irc/client.go b/irc/client.go index 11156523..4b099188 100644 --- a/irc/client.go +++ b/irc/client.go @@ -372,6 +372,15 @@ func (client *Client) destroy() { friends := client.Friends() 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 _, exists := client.server.currentOpers[client] if exists { diff --git a/irc/config.go b/irc/config.go index e28ca0d9..93aedce6 100644 --- a/irc/config.go +++ b/irc/config.go @@ -89,6 +89,13 @@ func (conf *OperConfig) PasswordBytes() []byte { 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 { Network struct { Name string @@ -96,14 +103,15 @@ type Config struct { Server struct { PassConfig - Password string - Name string - Listen []string - Wslisten string `yaml:"ws-listen"` - TLSListeners map[string]*TLSListenConfig `yaml:"tls-listeners"` - CheckIdent bool `yaml:"check-ident"` - Log string - MOTD string + Password string + Name string + Listen []string + Wslisten string `yaml:"ws-listen"` + TLSListeners map[string]*TLSListenConfig `yaml:"tls-listeners"` + CheckIdent bool `yaml:"check-ident"` + Log string + MOTD string + ConnectionLimits ConnectionLimitsConfig `yaml:"connection-limits"` } Datastore struct { diff --git a/irc/connection_limits.go b/irc/connection_limits.go new file mode 100644 index 00000000..332a3737 --- /dev/null +++ b/irc/connection_limits.go @@ -0,0 +1,111 @@ +// Copyright (c) 2016- Daniel Oaks +// 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 +} diff --git a/irc/server.go b/irc/server.go index 2876406d..91d0c385 100644 --- a/irc/server.go +++ b/irc/server.go @@ -25,6 +25,12 @@ import ( "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 type Limits struct { AwayLen int @@ -67,6 +73,8 @@ type Server struct { clients *ClientLookupSet commands chan Command 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 currentOpers map[*Client]bool idle chan *Client @@ -135,6 +143,11 @@ func NewServer(configFilename string, config *Config) *Server { 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{ accounts: make(map[string]*ClientAccount), authenticationEnabled: config.AuthenticationEnabled, @@ -142,6 +155,7 @@ func NewServer(configFilename string, config *Config) *Server { clients: NewClientLookupSet(), commands: make(chan Command), configFilename: configFilename, + connectionLimits: connectionLimits, ctime: time.Now(), currentOpers: make(map[*Client]bool), idle: make(chan *Client), @@ -318,7 +332,21 @@ func (server *Server) Run() { } 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: client.Idle() @@ -946,6 +974,12 @@ func (server *Server) rehash() 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 operclasses, err := config.OperatorClasses() 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 addedCaps := make(CapabilitySet) removedCaps := make(CapabilitySet) diff --git a/oragono.yaml b/oragono.yaml index 953a0e80..8ff4acb5 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -41,6 +41,23 @@ server: # if you change the motd, you should move it to ircd.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 authentication-enabled: true