Przeglądaj źródła

add support for login throttling

tags/v1.0.0-rc1
Shivaram Lingamneni 5 lat temu
rodzic
commit
f94f737b31

+ 1
- 0
Makefile Wyświetl plik

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

+ 14
- 8
irc/client.go Wyświetl plik

19
 	"github.com/goshuirc/irc-go/ircmsg"
19
 	"github.com/goshuirc/irc-go/ircmsg"
20
 	ident "github.com/oragono/go-ident"
20
 	ident "github.com/oragono/go-ident"
21
 	"github.com/oragono/oragono/irc/caps"
21
 	"github.com/oragono/oragono/irc/caps"
22
+	"github.com/oragono/oragono/irc/connection_limits"
22
 	"github.com/oragono/oragono/irc/history"
23
 	"github.com/oragono/oragono/irc/history"
23
 	"github.com/oragono/oragono/irc/modes"
24
 	"github.com/oragono/oragono/irc/modes"
24
 	"github.com/oragono/oragono/irc/sno"
25
 	"github.com/oragono/oragono/irc/sno"
73
 	isDestroyed        bool
74
 	isDestroyed        bool
74
 	isQuitting         bool
75
 	isQuitting         bool
75
 	languages          []string
76
 	languages          []string
77
+	loginThrottle      connection_limits.GenericThrottle
76
 	maxlenTags         uint32
78
 	maxlenTags         uint32
77
 	maxlenRest         uint32
79
 	maxlenRest         uint32
78
 	nick               string
80
 	nick               string
126
 	fullLineLenLimit := config.Limits.LineLen.Tags + config.Limits.LineLen.Rest
128
 	fullLineLenLimit := config.Limits.LineLen.Tags + config.Limits.LineLen.Rest
127
 	socket := NewSocket(conn, fullLineLenLimit*2, config.Server.MaxSendQBytes)
129
 	socket := NewSocket(conn, fullLineLenLimit*2, config.Server.MaxSendQBytes)
128
 	client := &Client{
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
 		server:         server,
143
 		server:         server,
138
 		socket:         socket,
144
 		socket:         socket,
139
 		accountName:    "*",
145
 		accountName:    "*",

+ 13
- 4
irc/config.go Wyświetl plik

54
 
54
 
55
 type AccountConfig struct {
55
 type AccountConfig struct {
56
 	Registration          AccountRegistrationConfig
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
 // AccountRegistrationConfig controls account registration.
68
 // AccountRegistrationConfig controls account registration.
558
 		config.Accounts.VHosts.ValidRegexp = defaultValidVhostRegex
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
 	maxSendQBytes, err := bytefmt.ToBytes(config.Server.MaxSendQString)
570
 	maxSendQBytes, err := bytefmt.ToBytes(config.Server.MaxSendQString)
562
 	if err != nil {
571
 	if err != nil {
563
 		return nil, fmt.Errorf("Could not parse maximum SendQ size (make sure it only contains whole numbers): %s", err.Error())
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 Wyświetl plik

26
 
26
 
27
 // ThrottleDetails holds the connection-throttling details for a subnet/IP.
27
 // ThrottleDetails holds the connection-throttling details for a subnet/IP.
28
 type ThrottleDetails struct {
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
 // Throttler manages automated client connection throttling.
70
 // Throttler manages automated client connection throttling.
102
 	ct.maskAddr(addr)
139
 	ct.maskAddr(addr)
103
 	addrString := addr.String()
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
 		return errTooManyClients
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
 func (ct *Throttler) BanDuration() time.Duration {
159
 func (ct *Throttler) BanDuration() time.Duration {

+ 86
- 0
irc/connection_limits/throttler_test.go Wyświetl plik

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 Wyświetl plik

83
 
83
 
84
 // ACC REGISTER <accountname> [callback_namespace:]<callback> [cred_type] :<credential>
84
 // ACC REGISTER <accountname> [callback_namespace:]<callback> [cred_type] :<credential>
85
 func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
85
 func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
86
+	nick := client.Nick()
86
 	// clients can't reg new accounts if they're already logged in
87
 	// clients can't reg new accounts if they're already logged in
87
 	if client.LoggedIntoAccount() {
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
 		return false
90
 		return false
90
 	}
91
 	}
91
 
92
 
94
 	casefoldedAccount, err := CasefoldName(account)
95
 	casefoldedAccount, err := CasefoldName(account)
95
 	// probably don't need explicit check for "*" here... but let's do it anyway just to make sure
96
 	// probably don't need explicit check for "*" here... but let's do it anyway just to make sure
96
 	if err != nil || msg.Params[1] == "*" {
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
 		return false
99
 		return false
99
 	}
100
 	}
100
 
101
 
101
 	if len(msg.Params) < 4 {
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
 		return false
104
 		return false
104
 	}
105
 	}
105
 
106
 
107
 	callbackNamespace, callbackValue := parseCallback(callbackSpec, server.AccountConfig())
108
 	callbackNamespace, callbackValue := parseCallback(callbackSpec, server.AccountConfig())
108
 
109
 
109
 	if callbackNamespace == "" {
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
 		return false
112
 		return false
112
 	}
113
 	}
113
 
114
 
131
 		}
132
 		}
132
 	}
133
 	}
133
 	if credentialType == "certfp" && client.certfp == "" {
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
 		return false
136
 		return false
136
 	}
137
 	}
137
 
138
 
138
 	if !credentialValid {
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
 		return false
141
 		return false
141
 	}
142
 	}
142
 
143
 
146
 	} else if credentialType == "passphrase" {
147
 	} else if credentialType == "passphrase" {
147
 		passphrase = credentialValue
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
 	err = server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, certfp)
157
 	err = server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, certfp)
150
 	if err != nil {
158
 	if err != nil {
151
 		msg := "Unknown"
159
 		msg := "Unknown"
161
 		if err == errAccountAlreadyRegistered || err == errAccountCreation || err == errCertfpAlreadyExists {
169
 		if err == errAccountAlreadyRegistered || err == errAccountCreation || err == errCertfpAlreadyExists {
162
 			msg = err.Error()
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
 		return false
173
 		return false
166
 	}
174
 	}
167
 
175
 
175
 	} else {
183
 	} else {
176
 		messageTemplate := client.t("Account created, pending verification; verification code has been sent to %s:%s")
184
 		messageTemplate := client.t("Account created, pending verification; verification code has been sent to %s:%s")
177
 		message := fmt.Sprintf(messageTemplate, callbackNamespace, callbackValue)
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
 	return false
189
 	return false
336
 
344
 
337
 	var accountKey, authzid string
345
 	var accountKey, authzid string
338
 
346
 
347
+	nick := client.Nick()
348
+
339
 	if len(splitValue) == 3 {
349
 	if len(splitValue) == 3 {
340
 		accountKey = string(splitValue[0])
350
 		accountKey = string(splitValue[0])
341
 		authzid = string(splitValue[1])
351
 		authzid = string(splitValue[1])
343
 		if accountKey == "" {
353
 		if accountKey == "" {
344
 			accountKey = authzid
354
 			accountKey = authzid
345
 		} else if accountKey != authzid {
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
 			return false
357
 			return false
348
 		}
358
 		}
349
 	} else {
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
 		return false
367
 		return false
352
 	}
368
 	}
353
 
369
 
355
 	err := server.accounts.AuthenticateByPassphrase(client, accountKey, password)
371
 	err := server.accounts.AuthenticateByPassphrase(client, accountKey, password)
356
 	if err != nil {
372
 	if err != nil {
357
 		msg := authErrorToMessage(server, err)
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
 		return false
375
 		return false
360
 	}
376
 	}
361
 
377
 

+ 18
- 1
irc/nickserv.go Wyświetl plik

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
 func nsIdentifyHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
212
 func nsIdentifyHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
204
 	loginSuccessful := false
213
 	loginSuccessful := false
205
 
214
 
207
 
216
 
208
 	// try passphrase
217
 	// try passphrase
209
 	if username != "" && passphrase != "" {
218
 	if username != "" && passphrase != "" {
219
+		if !nsLoginThrottleCheck(client, rb) {
220
+			return
221
+		}
210
 		err := server.accounts.AuthenticateByPassphrase(client, username, passphrase)
222
 		err := server.accounts.AuthenticateByPassphrase(client, username, passphrase)
211
 		loginSuccessful = (err == nil)
223
 		loginSuccessful = (err == nil)
212
 	}
224
 	}
407
 	var newPassword string
419
 	var newPassword string
408
 	var errorMessage string
420
 	var errorMessage string
409
 
421
 
422
+	hasPrivs := client.HasRoleCapabs("accreg")
423
+	if !hasPrivs && !nsLoginThrottleCheck(client, rb) {
424
+		return
425
+	}
426
+
410
 	fields := strings.Fields(params)
427
 	fields := strings.Fields(params)
411
 	switch len(fields) {
428
 	switch len(fields) {
412
 	case 2:
429
 	case 2:
413
-		if !client.HasRoleCapabs("accreg") {
430
+		if !hasPrivs {
414
 			errorMessage = "Insufficient privileges"
431
 			errorMessage = "Insufficient privileges"
415
 		} else {
432
 		} else {
416
 			target, newPassword = fields[0], fields[1]
433
 			target, newPassword = fields[0], fields[1]

+ 11
- 0
oragono.yaml Wyświetl plik

179
     # is account authentication enabled?
179
     # is account authentication enabled?
180
     authentication-enabled: true
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
     # some clients (notably Pidgin and Hexchat) offer only a single password field,
193
     # some clients (notably Pidgin and Hexchat) offer only a single password field,
183
     # which makes it impossible to specify a separate server password (for the PASS
194
     # which makes it impossible to specify a separate server password (for the PASS
184
     # command) and SASL password. if this option is set to true, a client that
195
     # command) and SASL password. if this option is set to true, a client that

Ładowanie…
Anuluj
Zapisz