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

add support for login throttling

This commit is contained in:
Shivaram Lingamneni 2019-01-01 16:45:37 -05:00
parent 3cd3601a30
commit f94f737b31
8 changed files with 220 additions and 37 deletions

View File

@ -20,6 +20,7 @@ test:
python3 ./gencapdefs.py | diff - ${capdef_file}
cd irc && go test . && go vet .
cd irc/caps && go test . && go vet .
cd irc/connection_limits && go test . && go vet .
cd irc/history && go test . && go vet .
cd irc/isupport && go test . && go vet .
cd irc/modes && go test . && go vet .

View File

@ -19,6 +19,7 @@ import (
"github.com/goshuirc/irc-go/ircmsg"
ident "github.com/oragono/go-ident"
"github.com/oragono/oragono/irc/caps"
"github.com/oragono/oragono/irc/connection_limits"
"github.com/oragono/oragono/irc/history"
"github.com/oragono/oragono/irc/modes"
"github.com/oragono/oragono/irc/sno"
@ -73,6 +74,7 @@ type Client struct {
isDestroyed bool
isQuitting bool
languages []string
loginThrottle connection_limits.GenericThrottle
maxlenTags uint32
maxlenRest uint32
nick string
@ -134,6 +136,10 @@ func NewClient(server *Server, conn net.Conn, isTLS bool) {
channels: make(ChannelSet),
ctime: now,
flags: modes.NewModeSet(),
loginThrottle: connection_limits.GenericThrottle{
Duration: config.Accounts.LoginThrottling.Duration,
Limit: config.Accounts.LoginThrottling.MaxAttempts,
},
server: server,
socket: socket,
accountName: "*",

View File

@ -55,6 +55,11 @@ func (conf *TLSListenConfig) Config() (*tls.Config, error) {
type AccountConfig struct {
Registration AccountRegistrationConfig
AuthenticationEnabled bool `yaml:"authentication-enabled"`
LoginThrottling struct {
Enabled bool
Duration time.Duration
MaxAttempts int `yaml:"max-attempts"`
} `yaml:"login-throttling"`
SkipServerPassword bool `yaml:"skip-server-password"`
NickReservation NickReservationConfig `yaml:"nick-reservation"`
VHosts VHostConfig
@ -558,6 +563,10 @@ func LoadConfig(filename string) (config *Config, err error) {
config.Accounts.VHosts.ValidRegexp = defaultValidVhostRegex
}
if !config.Accounts.LoginThrottling.Enabled {
config.Accounts.LoginThrottling.MaxAttempts = 0 // limit of 0 means disabled
}
maxSendQBytes, err := bytefmt.ToBytes(config.Server.MaxSendQString)
if err != nil {
return nil, fmt.Errorf("Could not parse maximum SendQ size (make sure it only contains whole numbers): %s", err.Error())

View File

@ -27,7 +27,44 @@ type ThrottlerConfig struct {
// ThrottleDetails holds the connection-throttling details for a subnet/IP.
type ThrottleDetails struct {
Start time.Time
ClientCount int
Count int
}
// GenericThrottle allows enforcing limits of the form
// "at most X events per time window of duration Y"
type GenericThrottle struct {
ThrottleDetails // variable state: what events have been seen
// these are constant after creation:
Duration time.Duration // window length to consider
Limit int // number of events allowed per window
}
// Touch checks whether an additional event is allowed:
// it either denies it (by returning false) or allows it (by returning true)
// and records it
func (g *GenericThrottle) Touch() (throttled bool, remainingTime time.Duration) {
return g.touch(time.Now())
}
func (g *GenericThrottle) touch(now time.Time) (throttled bool, remainingTime time.Duration) {
if g.Limit == 0 {
return // limit of 0 disables throttling
}
elapsed := now.Sub(g.Start)
if elapsed > g.Duration {
// reset window, record the operation
g.Start = now
g.Count = 1
return false, 0
} else if g.Count >= g.Limit {
// we are throttled
return true, g.Start.Add(g.Duration).Sub(now)
} else {
// we are not throttled, record the operation
g.Count += 1
return false, 0
}
}
// Throttler manages automated client connection throttling.
@ -102,21 +139,21 @@ func (ct *Throttler) AddClient(addr net.IP) error {
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(),
}
details := ct.population[addrString] // retrieve mutable throttle state from the map
// add in constant state to process the limiting operation
g := GenericThrottle{
ThrottleDetails: details,
Duration: ct.duration,
Limit: ct.subnetLimit,
}
throttled, _ := g.Touch() // actually check the limit
ct.population[addrString] = g.ThrottleDetails // store modified mutable state
if details.ClientCount+1 > ct.subnetLimit {
if throttled {
return errTooManyClients
}
details.ClientCount++
ct.population[addrString] = details
} else {
return nil
}
}
func (ct *Throttler) BanDuration() time.Duration {

View File

@ -0,0 +1,86 @@
// Copyright (c) 2018 Shivaram Lingamneni
// released under the MIT license
package connection_limits
import (
"net"
"reflect"
"testing"
"time"
)
func assertEqual(supplied, expected interface{}, t *testing.T) {
if !reflect.DeepEqual(supplied, expected) {
t.Errorf("expected %v but got %v", expected, supplied)
}
}
func TestGenericThrottle(t *testing.T) {
minute, _ := time.ParseDuration("1m")
second, _ := time.ParseDuration("1s")
zero, _ := time.ParseDuration("0s")
throttler := GenericThrottle{
Duration: minute,
Limit: 2,
}
now := time.Now()
throttled, remaining := throttler.touch(now)
assertEqual(throttled, false, t)
assertEqual(remaining, zero, t)
now = now.Add(second)
throttled, remaining = throttler.touch(now)
assertEqual(throttled, false, t)
assertEqual(remaining, zero, t)
now = now.Add(second)
throttled, remaining = throttler.touch(now)
assertEqual(throttled, true, t)
assertEqual(remaining, 58*second, t)
now = now.Add(minute)
throttled, remaining = throttler.touch(now)
assertEqual(throttled, false, t)
assertEqual(remaining, zero, t)
}
func TestGenericThrottleDisabled(t *testing.T) {
minute, _ := time.ParseDuration("1m")
throttler := GenericThrottle{
Duration: minute,
Limit: 0,
}
for i := 0; i < 1024; i += 1 {
throttled, _ := throttler.Touch()
if throttled {
t.Error("disabled throttler should not throttle")
}
}
}
func TestConnectionThrottle(t *testing.T) {
minute, _ := time.ParseDuration("1m")
maxConnections := 3
config := ThrottlerConfig{
Enabled: true,
CidrLenIPv4: 32,
CidrLenIPv6: 64,
ConnectionsPerCidr: maxConnections,
Duration: minute,
}
throttler := NewThrottler()
throttler.ApplyConfig(config)
addr := net.ParseIP("8.8.8.8")
for i := 0; i < maxConnections; i += 1 {
err := throttler.AddClient(addr)
assertEqual(err, nil, t)
}
err := throttler.AddClient(addr)
assertEqual(err, errTooManyClients, t)
}

View File

@ -83,9 +83,10 @@ func parseCallback(spec string, config *AccountConfig) (callbackNamespace string
// ACC REGISTER <accountname> [callback_namespace:]<callback> [cred_type] :<credential>
func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
nick := client.Nick()
// clients can't reg new accounts if they're already logged in
if client.LoggedIntoAccount() {
rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, "*", client.t("You're already logged into an account"))
rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, nick, "*", client.t("You're already logged into an account"))
return false
}
@ -94,12 +95,12 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
casefoldedAccount, err := CasefoldName(account)
// probably don't need explicit check for "*" here... but let's do it anyway just to make sure
if err != nil || msg.Params[1] == "*" {
rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, account, client.t("Account name is not valid"))
rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, nick, account, client.t("Account name is not valid"))
return false
}
if len(msg.Params) < 4 {
rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.nick, msg.Command, client.t("Not enough parameters"))
rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, nick, msg.Command, client.t("Not enough parameters"))
return false
}
@ -107,7 +108,7 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
callbackNamespace, callbackValue := parseCallback(callbackSpec, server.AccountConfig())
if callbackNamespace == "" {
rb.Add(nil, server.name, ERR_REG_INVALID_CALLBACK, client.nick, account, callbackSpec, client.t("Callback namespace is not supported"))
rb.Add(nil, server.name, ERR_REG_INVALID_CALLBACK, nick, account, callbackSpec, client.t("Callback namespace is not supported"))
return false
}
@ -131,12 +132,12 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
}
}
if credentialType == "certfp" && client.certfp == "" {
rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, client.nick, credentialType, callbackNamespace, client.t("You are not using a TLS certificate"))
rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, nick, credentialType, callbackNamespace, client.t("You are not using a TLS certificate"))
return false
}
if !credentialValid {
rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, client.nick, credentialType, callbackNamespace, client.t("Credential type is not supported"))
rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, nick, credentialType, callbackNamespace, client.t("Credential type is not supported"))
return false
}
@ -146,6 +147,13 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
} else if credentialType == "passphrase" {
passphrase = credentialValue
}
throttled, remainingTime := client.loginThrottle.Touch()
if throttled {
rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, nick, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
return false
}
err = server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, certfp)
if err != nil {
msg := "Unknown"
@ -161,7 +169,7 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
if err == errAccountAlreadyRegistered || err == errAccountCreation || err == errCertfpAlreadyExists {
msg = err.Error()
}
rb.Add(nil, server.name, code, client.nick, "ACC", "REGISTER", client.t(msg))
rb.Add(nil, server.name, code, nick, "ACC", "REGISTER", client.t(msg))
return false
}
@ -175,7 +183,7 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
} else {
messageTemplate := client.t("Account created, pending verification; verification code has been sent to %s:%s")
message := fmt.Sprintf(messageTemplate, callbackNamespace, callbackValue)
rb.Add(nil, server.name, RPL_REG_VERIFICATION_REQUIRED, client.nick, casefoldedAccount, message)
rb.Add(nil, server.name, RPL_REG_VERIFICATION_REQUIRED, nick, casefoldedAccount, message)
}
return false
@ -336,6 +344,8 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
var accountKey, authzid string
nick := client.Nick()
if len(splitValue) == 3 {
accountKey = string(splitValue[0])
authzid = string(splitValue[1])
@ -343,11 +353,17 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
if accountKey == "" {
accountKey = authzid
} else if accountKey != authzid {
rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed: authcid and authzid should be the same"))
rb.Add(nil, server.name, ERR_SASLFAIL, nick, client.t("SASL authentication failed: authcid and authzid should be the same"))
return false
}
} else {
rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed: Invalid auth blob"))
rb.Add(nil, server.name, ERR_SASLFAIL, nick, client.t("SASL authentication failed: Invalid auth blob"))
return false
}
throttled, remainingTime := client.loginThrottle.Touch()
if throttled {
rb.Add(nil, server.name, ERR_SASLFAIL, nick, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
return false
}
@ -355,7 +371,7 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
err := server.accounts.AuthenticateByPassphrase(client, accountKey, password)
if err != nil {
msg := authErrorToMessage(server, err)
rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, fmt.Sprintf("%s: %s", client.t("SASL authentication failed"), client.t(msg)))
rb.Add(nil, server.name, ERR_SASLFAIL, nick, fmt.Sprintf("%s: %s", client.t("SASL authentication failed"), client.t(msg)))
return false
}

View File

@ -200,6 +200,15 @@ func nsGroupHandler(server *Server, client *Client, command, params string, rb *
}
}
func nsLoginThrottleCheck(client *Client, rb *ResponseBuffer) (success bool) {
throttled, remainingTime := client.loginThrottle.Touch()
if throttled {
nsNotice(rb, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
return false
}
return true
}
func nsIdentifyHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
loginSuccessful := false
@ -207,6 +216,9 @@ func nsIdentifyHandler(server *Server, client *Client, command, params string, r
// try passphrase
if username != "" && passphrase != "" {
if !nsLoginThrottleCheck(client, rb) {
return
}
err := server.accounts.AuthenticateByPassphrase(client, username, passphrase)
loginSuccessful = (err == nil)
}
@ -407,10 +419,15 @@ func nsPasswdHandler(server *Server, client *Client, command, params string, rb
var newPassword string
var errorMessage string
hasPrivs := client.HasRoleCapabs("accreg")
if !hasPrivs && !nsLoginThrottleCheck(client, rb) {
return
}
fields := strings.Fields(params)
switch len(fields) {
case 2:
if !client.HasRoleCapabs("accreg") {
if !hasPrivs {
errorMessage = "Insufficient privileges"
} else {
target, newPassword = fields[0], fields[1]

View File

@ -179,6 +179,17 @@ accounts:
# is account authentication enabled?
authentication-enabled: true
# throttle account login attempts (to prevent either password guessing, or DoS
# attacks on the server aimed at forcing repeated expensive bcrypt computations)
login-throttling:
enabled: true
# window
duration: 1m
# number of attempts allowed within the window
max-attempts: 3
# some clients (notably Pidgin and Hexchat) offer only a single password field,
# which makes it impossible to specify a separate server password (for the PASS
# command) and SASL password. if this option is set to true, a client that