Browse Source

add support for login throttling

tags/v1.0.0-rc1
Shivaram Lingamneni 5 years ago
parent
commit
f94f737b31
8 changed files with 220 additions and 37 deletions
  1. 1
    0
      Makefile
  2. 14
    8
      irc/client.go
  3. 13
    4
      irc/config.go
  4. 50
    13
      irc/connection_limits/throttler.go
  5. 86
    0
      irc/connection_limits/throttler_test.go
  6. 27
    11
      irc/handlers.go
  7. 18
    1
      irc/nickserv.go
  8. 11
    0
      oragono.yaml

+ 1
- 0
Makefile View File

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

+ 14
- 8
irc/client.go View File

@@ -19,6 +19,7 @@ import (
19 19
 	"github.com/goshuirc/irc-go/ircmsg"
20 20
 	ident "github.com/oragono/go-ident"
21 21
 	"github.com/oragono/oragono/irc/caps"
22
+	"github.com/oragono/oragono/irc/connection_limits"
22 23
 	"github.com/oragono/oragono/irc/history"
23 24
 	"github.com/oragono/oragono/irc/modes"
24 25
 	"github.com/oragono/oragono/irc/sno"
@@ -73,6 +74,7 @@ type Client struct {
73 74
 	isDestroyed        bool
74 75
 	isQuitting         bool
75 76
 	languages          []string
77
+	loginThrottle      connection_limits.GenericThrottle
76 78
 	maxlenTags         uint32
77 79
 	maxlenRest         uint32
78 80
 	nick               string
@@ -126,14 +128,18 @@ func NewClient(server *Server, conn net.Conn, isTLS bool) {
126 128
 	fullLineLenLimit := config.Limits.LineLen.Tags + config.Limits.LineLen.Rest
127 129
 	socket := NewSocket(conn, fullLineLenLimit*2, config.Server.MaxSendQBytes)
128 130
 	client := &Client{
129
-		atime:          now,
130
-		authorized:     server.Password() == nil,
131
-		capabilities:   caps.NewSet(),
132
-		capState:       caps.NoneState,
133
-		capVersion:     caps.Cap301,
134
-		channels:       make(ChannelSet),
135
-		ctime:          now,
136
-		flags:          modes.NewModeSet(),
131
+		atime:        now,
132
+		authorized:   server.Password() == nil,
133
+		capabilities: caps.NewSet(),
134
+		capState:     caps.NoneState,
135
+		capVersion:   caps.Cap301,
136
+		channels:     make(ChannelSet),
137
+		ctime:        now,
138
+		flags:        modes.NewModeSet(),
139
+		loginThrottle: connection_limits.GenericThrottle{
140
+			Duration: config.Accounts.LoginThrottling.Duration,
141
+			Limit:    config.Accounts.LoginThrottling.MaxAttempts,
142
+		},
137 143
 		server:         server,
138 144
 		socket:         socket,
139 145
 		accountName:    "*",

+ 13
- 4
irc/config.go View File

@@ -54,10 +54,15 @@ func (conf *TLSListenConfig) Config() (*tls.Config, error) {
54 54
 
55 55
 type AccountConfig struct {
56 56
 	Registration          AccountRegistrationConfig
57
-	AuthenticationEnabled bool                  `yaml:"authentication-enabled"`
58
-	SkipServerPassword    bool                  `yaml:"skip-server-password"`
59
-	NickReservation       NickReservationConfig `yaml:"nick-reservation"`
60
-	VHosts                VHostConfig
57
+	AuthenticationEnabled bool `yaml:"authentication-enabled"`
58
+	LoginThrottling       struct {
59
+		Enabled     bool
60
+		Duration    time.Duration
61
+		MaxAttempts int `yaml:"max-attempts"`
62
+	} `yaml:"login-throttling"`
63
+	SkipServerPassword bool                  `yaml:"skip-server-password"`
64
+	NickReservation    NickReservationConfig `yaml:"nick-reservation"`
65
+	VHosts             VHostConfig
61 66
 }
62 67
 
63 68
 // AccountRegistrationConfig controls account registration.
@@ -558,6 +563,10 @@ func LoadConfig(filename string) (config *Config, err error) {
558 563
 		config.Accounts.VHosts.ValidRegexp = defaultValidVhostRegex
559 564
 	}
560 565
 
566
+	if !config.Accounts.LoginThrottling.Enabled {
567
+		config.Accounts.LoginThrottling.MaxAttempts = 0 // limit of 0 means disabled
568
+	}
569
+
561 570
 	maxSendQBytes, err := bytefmt.ToBytes(config.Server.MaxSendQString)
562 571
 	if err != nil {
563 572
 		return nil, fmt.Errorf("Could not parse maximum SendQ size (make sure it only contains whole numbers): %s", err.Error())

+ 50
- 13
irc/connection_limits/throttler.go View File

@@ -26,8 +26,45 @@ type ThrottlerConfig struct {
26 26
 
27 27
 // ThrottleDetails holds the connection-throttling details for a subnet/IP.
28 28
 type ThrottleDetails struct {
29
-	Start       time.Time
30
-	ClientCount int
29
+	Start time.Time
30
+	Count int
31
+}
32
+
33
+// GenericThrottle allows enforcing limits of the form
34
+// "at most X events per time window of duration Y"
35
+type GenericThrottle struct {
36
+	ThrottleDetails // variable state: what events have been seen
37
+	// these are constant after creation:
38
+	Duration time.Duration // window length to consider
39
+	Limit    int           // number of events allowed per window
40
+}
41
+
42
+// Touch checks whether an additional event is allowed:
43
+// it either denies it (by returning false) or allows it (by returning true)
44
+// and records it
45
+func (g *GenericThrottle) Touch() (throttled bool, remainingTime time.Duration) {
46
+	return g.touch(time.Now())
47
+}
48
+
49
+func (g *GenericThrottle) touch(now time.Time) (throttled bool, remainingTime time.Duration) {
50
+	if g.Limit == 0 {
51
+		return // limit of 0 disables throttling
52
+	}
53
+
54
+	elapsed := now.Sub(g.Start)
55
+	if elapsed > g.Duration {
56
+		// reset window, record the operation
57
+		g.Start = now
58
+		g.Count = 1
59
+		return false, 0
60
+	} else if g.Count >= g.Limit {
61
+		// we are throttled
62
+		return true, g.Start.Add(g.Duration).Sub(now)
63
+	} else {
64
+		// we are not throttled, record the operation
65
+		g.Count += 1
66
+		return false, 0
67
+	}
31 68
 }
32 69
 
33 70
 // Throttler manages automated client connection throttling.
@@ -102,21 +139,21 @@ func (ct *Throttler) AddClient(addr net.IP) error {
102 139
 	ct.maskAddr(addr)
103 140
 	addrString := addr.String()
104 141
 
105
-	details, exists := ct.population[addrString]
106
-	if !exists || details.Start.Add(ct.duration).Before(time.Now()) {
107
-		details = ThrottleDetails{
108
-			Start: time.Now(),
109
-		}
142
+	details := ct.population[addrString] // retrieve mutable throttle state from the map
143
+	// add in constant state to process the limiting operation
144
+	g := GenericThrottle{
145
+		ThrottleDetails: details,
146
+		Duration:        ct.duration,
147
+		Limit:           ct.subnetLimit,
110 148
 	}
149
+	throttled, _ := g.Touch()                     // actually check the limit
150
+	ct.population[addrString] = g.ThrottleDetails // store modified mutable state
111 151
 
112
-	if details.ClientCount+1 > ct.subnetLimit {
152
+	if throttled {
113 153
 		return errTooManyClients
154
+	} else {
155
+		return nil
114 156
 	}
115
-
116
-	details.ClientCount++
117
-	ct.population[addrString] = details
118
-
119
-	return nil
120 157
 }
121 158
 
122 159
 func (ct *Throttler) BanDuration() time.Duration {

+ 86
- 0
irc/connection_limits/throttler_test.go View File

@@ -0,0 +1,86 @@
1
+// Copyright (c) 2018 Shivaram Lingamneni
2
+// released under the MIT license
3
+
4
+package connection_limits
5
+
6
+import (
7
+	"net"
8
+	"reflect"
9
+	"testing"
10
+	"time"
11
+)
12
+
13
+func assertEqual(supplied, expected interface{}, t *testing.T) {
14
+	if !reflect.DeepEqual(supplied, expected) {
15
+		t.Errorf("expected %v but got %v", expected, supplied)
16
+	}
17
+}
18
+
19
+func TestGenericThrottle(t *testing.T) {
20
+	minute, _ := time.ParseDuration("1m")
21
+	second, _ := time.ParseDuration("1s")
22
+	zero, _ := time.ParseDuration("0s")
23
+
24
+	throttler := GenericThrottle{
25
+		Duration: minute,
26
+		Limit:    2,
27
+	}
28
+
29
+	now := time.Now()
30
+	throttled, remaining := throttler.touch(now)
31
+	assertEqual(throttled, false, t)
32
+	assertEqual(remaining, zero, t)
33
+
34
+	now = now.Add(second)
35
+	throttled, remaining = throttler.touch(now)
36
+	assertEqual(throttled, false, t)
37
+	assertEqual(remaining, zero, t)
38
+
39
+	now = now.Add(second)
40
+	throttled, remaining = throttler.touch(now)
41
+	assertEqual(throttled, true, t)
42
+	assertEqual(remaining, 58*second, t)
43
+
44
+	now = now.Add(minute)
45
+	throttled, remaining = throttler.touch(now)
46
+	assertEqual(throttled, false, t)
47
+	assertEqual(remaining, zero, t)
48
+}
49
+
50
+func TestGenericThrottleDisabled(t *testing.T) {
51
+	minute, _ := time.ParseDuration("1m")
52
+	throttler := GenericThrottle{
53
+		Duration: minute,
54
+		Limit:    0,
55
+	}
56
+
57
+	for i := 0; i < 1024; i += 1 {
58
+		throttled, _ := throttler.Touch()
59
+		if throttled {
60
+			t.Error("disabled throttler should not throttle")
61
+		}
62
+	}
63
+}
64
+
65
+func TestConnectionThrottle(t *testing.T) {
66
+	minute, _ := time.ParseDuration("1m")
67
+	maxConnections := 3
68
+	config := ThrottlerConfig{
69
+		Enabled:            true,
70
+		CidrLenIPv4:        32,
71
+		CidrLenIPv6:        64,
72
+		ConnectionsPerCidr: maxConnections,
73
+		Duration:           minute,
74
+	}
75
+	throttler := NewThrottler()
76
+	throttler.ApplyConfig(config)
77
+
78
+	addr := net.ParseIP("8.8.8.8")
79
+
80
+	for i := 0; i < maxConnections; i += 1 {
81
+		err := throttler.AddClient(addr)
82
+		assertEqual(err, nil, t)
83
+	}
84
+	err := throttler.AddClient(addr)
85
+	assertEqual(err, errTooManyClients, t)
86
+}

+ 27
- 11
irc/handlers.go View File

@@ -83,9 +83,10 @@ func parseCallback(spec string, config *AccountConfig) (callbackNamespace string
83 83
 
84 84
 // ACC REGISTER <accountname> [callback_namespace:]<callback> [cred_type] :<credential>
85 85
 func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
86
+	nick := client.Nick()
86 87
 	// clients can't reg new accounts if they're already logged in
87 88
 	if client.LoggedIntoAccount() {
88
-		rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, "*", client.t("You're already logged into an account"))
89
+		rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, nick, "*", client.t("You're already logged into an account"))
89 90
 		return false
90 91
 	}
91 92
 
@@ -94,12 +95,12 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
94 95
 	casefoldedAccount, err := CasefoldName(account)
95 96
 	// probably don't need explicit check for "*" here... but let's do it anyway just to make sure
96 97
 	if err != nil || msg.Params[1] == "*" {
97
-		rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, account, client.t("Account name is not valid"))
98
+		rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, nick, account, client.t("Account name is not valid"))
98 99
 		return false
99 100
 	}
100 101
 
101 102
 	if len(msg.Params) < 4 {
102
-		rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.nick, msg.Command, client.t("Not enough parameters"))
103
+		rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, nick, msg.Command, client.t("Not enough parameters"))
103 104
 		return false
104 105
 	}
105 106
 
@@ -107,7 +108,7 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
107 108
 	callbackNamespace, callbackValue := parseCallback(callbackSpec, server.AccountConfig())
108 109
 
109 110
 	if callbackNamespace == "" {
110
-		rb.Add(nil, server.name, ERR_REG_INVALID_CALLBACK, client.nick, account, callbackSpec, client.t("Callback namespace is not supported"))
111
+		rb.Add(nil, server.name, ERR_REG_INVALID_CALLBACK, nick, account, callbackSpec, client.t("Callback namespace is not supported"))
111 112
 		return false
112 113
 	}
113 114
 
@@ -131,12 +132,12 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
131 132
 		}
132 133
 	}
133 134
 	if credentialType == "certfp" && client.certfp == "" {
134
-		rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, client.nick, credentialType, callbackNamespace, client.t("You are not using a TLS certificate"))
135
+		rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, nick, credentialType, callbackNamespace, client.t("You are not using a TLS certificate"))
135 136
 		return false
136 137
 	}
137 138
 
138 139
 	if !credentialValid {
139
-		rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, client.nick, credentialType, callbackNamespace, client.t("Credential type is not supported"))
140
+		rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, nick, credentialType, callbackNamespace, client.t("Credential type is not supported"))
140 141
 		return false
141 142
 	}
142 143
 
@@ -146,6 +147,13 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
146 147
 	} else if credentialType == "passphrase" {
147 148
 		passphrase = credentialValue
148 149
 	}
150
+
151
+	throttled, remainingTime := client.loginThrottle.Touch()
152
+	if throttled {
153
+		rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, nick, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
154
+		return false
155
+	}
156
+
149 157
 	err = server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, certfp)
150 158
 	if err != nil {
151 159
 		msg := "Unknown"
@@ -161,7 +169,7 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
161 169
 		if err == errAccountAlreadyRegistered || err == errAccountCreation || err == errCertfpAlreadyExists {
162 170
 			msg = err.Error()
163 171
 		}
164
-		rb.Add(nil, server.name, code, client.nick, "ACC", "REGISTER", client.t(msg))
172
+		rb.Add(nil, server.name, code, nick, "ACC", "REGISTER", client.t(msg))
165 173
 		return false
166 174
 	}
167 175
 
@@ -175,7 +183,7 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
175 183
 	} else {
176 184
 		messageTemplate := client.t("Account created, pending verification; verification code has been sent to %s:%s")
177 185
 		message := fmt.Sprintf(messageTemplate, callbackNamespace, callbackValue)
178
-		rb.Add(nil, server.name, RPL_REG_VERIFICATION_REQUIRED, client.nick, casefoldedAccount, message)
186
+		rb.Add(nil, server.name, RPL_REG_VERIFICATION_REQUIRED, nick, casefoldedAccount, message)
179 187
 	}
180 188
 
181 189
 	return false
@@ -336,6 +344,8 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
336 344
 
337 345
 	var accountKey, authzid string
338 346
 
347
+	nick := client.Nick()
348
+
339 349
 	if len(splitValue) == 3 {
340 350
 		accountKey = string(splitValue[0])
341 351
 		authzid = string(splitValue[1])
@@ -343,11 +353,17 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
343 353
 		if accountKey == "" {
344 354
 			accountKey = authzid
345 355
 		} else if accountKey != authzid {
346
-			rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed: authcid and authzid should be the same"))
356
+			rb.Add(nil, server.name, ERR_SASLFAIL, nick, client.t("SASL authentication failed: authcid and authzid should be the same"))
347 357
 			return false
348 358
 		}
349 359
 	} else {
350
-		rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed: Invalid auth blob"))
360
+		rb.Add(nil, server.name, ERR_SASLFAIL, nick, client.t("SASL authentication failed: Invalid auth blob"))
361
+		return false
362
+	}
363
+
364
+	throttled, remainingTime := client.loginThrottle.Touch()
365
+	if throttled {
366
+		rb.Add(nil, server.name, ERR_SASLFAIL, nick, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
351 367
 		return false
352 368
 	}
353 369
 
@@ -355,7 +371,7 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
355 371
 	err := server.accounts.AuthenticateByPassphrase(client, accountKey, password)
356 372
 	if err != nil {
357 373
 		msg := authErrorToMessage(server, err)
358
-		rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, fmt.Sprintf("%s: %s", client.t("SASL authentication failed"), client.t(msg)))
374
+		rb.Add(nil, server.name, ERR_SASLFAIL, nick, fmt.Sprintf("%s: %s", client.t("SASL authentication failed"), client.t(msg)))
359 375
 		return false
360 376
 	}
361 377
 

+ 18
- 1
irc/nickserv.go View File

@@ -200,6 +200,15 @@ func nsGroupHandler(server *Server, client *Client, command, params string, rb *
200 200
 	}
201 201
 }
202 202
 
203
+func nsLoginThrottleCheck(client *Client, rb *ResponseBuffer) (success bool) {
204
+	throttled, remainingTime := client.loginThrottle.Touch()
205
+	if throttled {
206
+		nsNotice(rb, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
207
+		return false
208
+	}
209
+	return true
210
+}
211
+
203 212
 func nsIdentifyHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
204 213
 	loginSuccessful := false
205 214
 
@@ -207,6 +216,9 @@ func nsIdentifyHandler(server *Server, client *Client, command, params string, r
207 216
 
208 217
 	// try passphrase
209 218
 	if username != "" && passphrase != "" {
219
+		if !nsLoginThrottleCheck(client, rb) {
220
+			return
221
+		}
210 222
 		err := server.accounts.AuthenticateByPassphrase(client, username, passphrase)
211 223
 		loginSuccessful = (err == nil)
212 224
 	}
@@ -407,10 +419,15 @@ func nsPasswdHandler(server *Server, client *Client, command, params string, rb
407 419
 	var newPassword string
408 420
 	var errorMessage string
409 421
 
422
+	hasPrivs := client.HasRoleCapabs("accreg")
423
+	if !hasPrivs && !nsLoginThrottleCheck(client, rb) {
424
+		return
425
+	}
426
+
410 427
 	fields := strings.Fields(params)
411 428
 	switch len(fields) {
412 429
 	case 2:
413
-		if !client.HasRoleCapabs("accreg") {
430
+		if !hasPrivs {
414 431
 			errorMessage = "Insufficient privileges"
415 432
 		} else {
416 433
 			target, newPassword = fields[0], fields[1]

+ 11
- 0
oragono.yaml View File

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

Loading…
Cancel
Save