Merge pull request #916 from slingamn/issue913_accountthrottle.1

fix #913
This commit is contained in:
Shivaram Lingamneni 2020-03-29 18:00:23 -07:00 committed by GitHub
commit 7bdf4441cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 87 additions and 29 deletions

@ -15,6 +15,7 @@ import (
"unicode"
"github.com/oragono/oragono/irc/caps"
"github.com/oragono/oragono/irc/connection_limits"
"github.com/oragono/oragono/irc/ldap"
"github.com/oragono/oragono/irc/passwd"
"github.com/oragono/oragono/irc/utils"
@ -62,6 +63,7 @@ type AccountManager struct {
nickToAccount map[string]string
skeletonToAccount map[string]string
accountToMethod map[string]NickEnforcementMethod
registerThrottle connection_limits.GenericThrottle
}
func (am *AccountManager) Initialize(server *Server) {
@ -75,6 +77,24 @@ func (am *AccountManager) Initialize(server *Server) {
am.buildNickToAccountIndex(config)
am.initVHostRequestQueue(config)
am.createAlwaysOnClients(config)
am.resetRegisterThrottle(config)
}
func (am *AccountManager) resetRegisterThrottle(config *Config) {
am.Lock()
defer am.Unlock()
am.registerThrottle = connection_limits.GenericThrottle{
Duration: config.Accounts.Registration.Throttling.Duration,
Limit: config.Accounts.Registration.Throttling.MaxAttempts,
}
}
func (am *AccountManager) touchRegisterThrottle() (throttled bool) {
am.Lock()
defer am.Unlock()
throttled, _ = am.registerThrottle.Touch()
return
}
func (am *AccountManager) createAlwaysOnClients(config *Config) {
@ -363,6 +383,15 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
return errFeatureDisabled
}
if client != nil && client.Account() != "" {
return errAccountAlreadyLoggedIn
}
if client != nil && am.touchRegisterThrottle() {
am.server.logger.Warning("accounts", "global registration throttle exceeded by client", client.Nick())
return errLimitExceeded
}
// if nick reservation is enabled, you can only register your current nickname
// as an account; this prevents "land-grab" situations where someone else
// registers your nick out from under you and then NS GHOSTs you
@ -725,6 +754,10 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
return errAccountVerificationFailed
}
if client != nil && client.Account() != "" {
return errAccountAlreadyLoggedIn
}
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)

@ -229,6 +229,29 @@ type MulticlientConfig struct {
AlwaysOn PersistentStatus `yaml:"always-on"`
}
type throttleConfig struct {
Enabled bool
Duration time.Duration
MaxAttempts int `yaml:"max-attempts"`
}
type ThrottleConfig struct {
throttleConfig
}
func (t *ThrottleConfig) UnmarshalYAML(unmarshal func(interface{}) error) (err error) {
// note that this technique only works if the zero value of the struct
// doesn't need any postprocessing (because if the field is omitted entirely
// from the YAML, then UnmarshalYAML won't be called at all)
if err = unmarshal(&t.throttleConfig); err != nil {
return
}
if !t.Enabled {
t.MaxAttempts = 0 // limit of 0 means disabled
}
return
}
type AccountConfig struct {
Registration AccountRegistrationConfig
AuthenticationEnabled bool `yaml:"authentication-enabled"`
@ -237,13 +260,9 @@ type AccountConfig struct {
Exempted []string
exemptedNets []net.IPNet
} `yaml:"require-sasl"`
LDAP ldap.ServerConfig
LoginThrottling struct {
Enabled bool
Duration time.Duration
MaxAttempts int `yaml:"max-attempts"`
} `yaml:"login-throttling"`
SkipServerPassword bool `yaml:"skip-server-password"`
LDAP ldap.ServerConfig
LoginThrottling ThrottleConfig `yaml:"login-throttling"`
SkipServerPassword bool `yaml:"skip-server-password"`
NickReservation struct {
Enabled bool
AdditionalNickLimit int `yaml:"additional-nick-limit"`
@ -266,6 +285,7 @@ type AccountConfig struct {
// AccountRegistrationConfig controls account registration.
type AccountRegistrationConfig struct {
Enabled bool
Throttling ThrottleConfig
EnabledCallbacks []string `yaml:"enabled-callbacks"`
EnabledCredentialTypes []string `yaml:"-"`
VerifyTimeout custime.Duration `yaml:"verify-timeout"`
@ -997,10 +1017,6 @@ func LoadConfig(filename string) (config *Config, err error) {
}
}
if !config.Accounts.LoginThrottling.Enabled {
config.Accounts.LoginThrottling.MaxAttempts = 0 // limit of 0 means disabled
}
config.Server.capValues[caps.SASL] = "PLAIN,EXTERNAL"
if !config.Accounts.AuthenticationEnabled {
config.Server.supportedCaps.Disable(caps.SASL)

@ -22,6 +22,7 @@ var (
errAccountBadPassphrase = errors.New(`Passphrase contains forbidden characters or is otherwise invalid`)
errAccountNickReservationFailed = errors.New("Could not (un)reserve nick")
errAccountNotLoggedIn = errors.New("You're not logged into an account")
errAccountAlreadyLoggedIn = errors.New("You're already logged into an account")
errAccountTooManyNicks = errors.New("Account has too many reserved nicks")
errAccountUnverified = errors.New(`Account is not yet verified`)
errAccountVerificationFailed = errors.New("Account verification failed")

@ -66,10 +66,10 @@ func registrationErrorToMessageAndCode(err error) (message, code string) {
case errAccountBadPassphrase:
code = "REG_INVALID_CREDENTIAL"
message = err.Error()
case errAccountAlreadyRegistered, errAccountAlreadyVerified, errAccountAlreadyUnregistered:
message = err.Error()
case errAccountCreation, errAccountMustHoldNick, errAccountBadPassphrase, errCertfpAlreadyExists, errFeatureDisabled:
case errAccountAlreadyRegistered, errAccountAlreadyVerified, errAccountAlreadyUnregistered, errAccountAlreadyLoggedIn, errAccountCreation, errAccountMustHoldNick, errAccountBadPassphrase, errCertfpAlreadyExists, errFeatureDisabled:
message = err.Error()
case errLimitExceeded:
message = `There have been too many registration attempts recently; try again later`
}
return
}

@ -616,7 +616,9 @@ func nsGroupHandler(server *Server, client *Client, command string, params []str
}
func nsLoginThrottleCheck(client *Client, rb *ResponseBuffer) (success bool) {
client.stateMutex.Lock()
throttled, remainingTime := client.loginThrottle.Touch()
client.stateMutex.Unlock()
if throttled {
nsNotice(rb, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
return false
@ -741,11 +743,6 @@ func nsRegisterHandler(server *Server, client *Client, command string, params []
}
}
if details.account != "" {
nsNotice(rb, client.t("You're already logged into an account"))
return
}
if !nsLoginThrottleCheck(client, rb) {
return
}
@ -793,13 +790,10 @@ func nsRegisterHandler(server *Server, client *Client, command string, params []
message := fmt.Sprintf(messageTemplate, fmt.Sprintf("%s:%s", callbackNamespace, callbackValue))
nsNotice(rb, message)
}
}
// details could not be stored and relevant numerics have been dispatched, abort
message, _ := registrationErrorToMessageAndCode(err)
if err != nil {
} else {
// details could not be stored and relevant numerics have been dispatched, abort
message, _ := registrationErrorToMessageAndCode(err)
nsNotice(rb, client.t(message))
return
}
}
@ -894,10 +888,13 @@ func nsVerifyHandler(server *Server, client *Client, command string, params []st
err := server.accounts.Verify(client, username, code)
var errorMessage string
if err == errAccountVerificationInvalidCode || err == errAccountAlreadyVerified {
errorMessage = err.Error()
} else if err != nil {
errorMessage = errAccountVerificationFailed.Error()
if err != nil {
switch err {
case errAccountAlreadyLoggedIn, errAccountVerificationInvalidCode, errAccountAlreadyVerified:
errorMessage = err.Error()
default:
errorMessage = errAccountVerificationFailed.Error()
}
}
if errorMessage != "" {

@ -634,6 +634,9 @@ func (server *Server) applyConfig(config *Config) (err error) {
client.resizeHistory(config)
}
}
if oldConfig.Accounts.Registration.Throttling != config.Accounts.Registration.Throttling {
server.accounts.resetRegisterThrottle(config)
}
}
// activate the new config

@ -266,6 +266,14 @@ accounts:
# the `accreg` capability can still create accounts with `/NICKSERV SAREGISTER`
enabled: true
# global throttle on new account creation
throttling:
enabled: true
# window
duration: 10m
# number of attempts allowed within the window
max-attempts: 30
# this is the bcrypt cost we'll use for account passwords
bcrypt-cost: 9