Browse Source

implement fakelag (#189)

tags/v0.11.0-beta
Shivaram Lingamneni 6 years ago
parent
commit
1bf5e2a7c8
9 changed files with 293 additions and 19 deletions
  1. 23
    0
      irc/client.go
  2. 4
    2
      irc/commands.go
  3. 9
    0
      irc/config.go
  4. 92
    0
      irc/fakelag.go
  5. 114
    0
      irc/fakelag_test.go
  6. 13
    1
      irc/getters.go
  7. 5
    1
      irc/handlers.go
  8. 16
    15
      irc/server.go
  9. 17
    0
      oragono.yaml

+ 23
- 0
irc/client.go View File

@@ -49,6 +49,7 @@ type Client struct {
49 49
 	class              *OperClass
50 50
 	ctime              time.Time
51 51
 	exitedSnomaskSent  bool
52
+	fakelag            *Fakelag
52 53
 	flags              map[modes.Mode]bool
53 54
 	hasQuit            bool
54 55
 	hops               int
@@ -145,6 +146,26 @@ func NewClient(server *Server, conn net.Conn, isTLS bool) *Client {
145 146
 	return client
146 147
 }
147 148
 
149
+func (client *Client) resetFakelag() {
150
+	fakelag := func() *Fakelag {
151
+		if client.HasRoleCapabs("nofakelag") {
152
+			return nil
153
+		}
154
+
155
+		flc := client.server.FakelagConfig()
156
+
157
+		if !flc.Enabled {
158
+			return nil
159
+		}
160
+
161
+		return NewFakelag(flc.Window, flc.BurstLimit, flc.MessagesPerWindow)
162
+	}()
163
+
164
+	client.stateMutex.Lock()
165
+	defer client.stateMutex.Unlock()
166
+	client.fakelag = fakelag
167
+}
168
+
148 169
 // IP returns the IP address of this client.
149 170
 func (client *Client) IP() net.IP {
150 171
 	if client.proxiedIP != nil {
@@ -221,6 +242,8 @@ func (client *Client) run() {
221 242
 
222 243
 	client.nickTimer = NewNickTimer(client)
223 244
 
245
+	client.resetFakelag()
246
+
224 247
 	// Set the hostname for this client
225 248
 	// (may be overridden by a later PROXY command from stunnel)
226 249
 	client.rawHostname = utils.AddrLookupHostname(client.socket.conn.RemoteAddr())

+ 4
- 2
irc/commands.go View File

@@ -40,11 +40,13 @@ func (cmd *Command) Run(server *Server, client *Client, msg ircmsg.IrcMessage) b
40 40
 		return false
41 41
 	}
42 42
 
43
+	if client.registered {
44
+		client.fakelag.Touch()
45
+	}
46
+
43 47
 	rb := NewResponseBuffer(client)
44 48
 	rb.Label = GetLabel(msg)
45
-
46 49
 	exiting := cmd.handler(server, client, msg, rb)
47
-
48 50
 	rb.Send()
49 51
 
50 52
 	// after each command, see if we can send registration to the client

+ 9
- 0
irc/config.go View File

@@ -189,6 +189,13 @@ type StackImpactConfig struct {
189 189
 	AppName  string `yaml:"app-name"`
190 190
 }
191 191
 
192
+type FakelagConfig struct {
193
+	Enabled           bool
194
+	Window            time.Duration
195
+	BurstLimit        uint `yaml:"burst-limit"`
196
+	MessagesPerWindow uint `yaml:"messages-per-window"`
197
+}
198
+
192 199
 // Config defines the overall configuration.
193 200
 type Config struct {
194 201
 	Network struct {
@@ -255,6 +262,8 @@ type Config struct {
255 262
 		LineLen        LineLenConfig `yaml:"linelen"`
256 263
 	}
257 264
 
265
+	Fakelag FakelagConfig
266
+
258 267
 	Filename string
259 268
 }
260 269
 

+ 92
- 0
irc/fakelag.go View File

@@ -0,0 +1,92 @@
1
+// Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// released under the MIT license
3
+
4
+package irc
5
+
6
+import (
7
+	"time"
8
+)
9
+
10
+// fakelag is a system for artificially delaying commands when a user issues
11
+// them too rapidly
12
+
13
+type FakelagState uint
14
+
15
+const (
16
+	// initially, the client is "bursting" and can send n commands without
17
+	// encountering fakelag
18
+	FakelagBursting FakelagState = iota
19
+	// after that, they're "throttled" and we sleep in between commands until
20
+	// they're spaced sufficiently far apart
21
+	FakelagThrottled
22
+)
23
+
24
+// this is intentionally not threadsafe, because it should only be touched
25
+// from the loop that accepts the client's input and runs commands
26
+type Fakelag struct {
27
+	window                    time.Duration
28
+	burstLimit                uint
29
+	throttleMessagesPerWindow uint
30
+	nowFunc                   func() time.Time
31
+	sleepFunc                 func(time.Duration)
32
+
33
+	state      FakelagState
34
+	burstCount uint // number of messages sent in the current burst
35
+	lastTouch  time.Time
36
+}
37
+
38
+func NewFakelag(window time.Duration, burstLimit uint, throttleMessagesPerWindow uint) *Fakelag {
39
+	return &Fakelag{
40
+		window:                    window,
41
+		burstLimit:                burstLimit,
42
+		throttleMessagesPerWindow: throttleMessagesPerWindow,
43
+		nowFunc:                   time.Now,
44
+		sleepFunc:                 time.Sleep,
45
+		state:                     FakelagBursting,
46
+	}
47
+}
48
+
49
+// register a new command, sleep if necessary to delay it
50
+func (fl *Fakelag) Touch() {
51
+	if fl == nil {
52
+		return
53
+	}
54
+
55
+	now := fl.nowFunc()
56
+	// XXX if lastTouch.IsZero(), treat it as "very far in the past", which is fine
57
+	elapsed := now.Sub(fl.lastTouch)
58
+	fl.lastTouch = now
59
+
60
+	if fl.state == FakelagBursting {
61
+		// determine if the previous burst is over
62
+		// (we could use 2*window instead)
63
+		if elapsed > fl.window {
64
+			fl.burstCount = 0
65
+		}
66
+
67
+		fl.burstCount++
68
+		if fl.burstCount > fl.burstLimit {
69
+			// reset burst window for next time
70
+			fl.burstCount = 0
71
+			// transition to throttling
72
+			fl.state = FakelagThrottled
73
+			// continue to throttling logic
74
+		} else {
75
+			return
76
+		}
77
+	}
78
+
79
+	if fl.state == FakelagThrottled {
80
+		if elapsed > fl.window {
81
+			// let them burst again (as above, we could use 2*window instead)
82
+			fl.state = FakelagBursting
83
+			return
84
+		}
85
+		// space them out by at least window/messagesperwindow
86
+		sleepDuration := time.Duration((int64(fl.window) / int64(fl.throttleMessagesPerWindow)) - int64(elapsed))
87
+		if sleepDuration < 0 {
88
+			sleepDuration = 0
89
+		}
90
+		fl.sleepFunc(sleepDuration)
91
+	}
92
+}

+ 114
- 0
irc/fakelag_test.go View File

@@ -0,0 +1,114 @@
1
+// Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// released under the MIT license
3
+
4
+package irc
5
+
6
+import (
7
+	"testing"
8
+	"time"
9
+)
10
+
11
+type mockTime struct {
12
+	now              time.Time
13
+	sleepList        []time.Duration
14
+	lastCheckedSleep int
15
+}
16
+
17
+func (mt *mockTime) Now() (now time.Time) {
18
+	return mt.now
19
+}
20
+
21
+func (mt *mockTime) Sleep(dur time.Duration) {
22
+	mt.sleepList = append(mt.sleepList, dur)
23
+	mt.pause(dur)
24
+}
25
+
26
+func (mt *mockTime) pause(dur time.Duration) {
27
+	mt.now = mt.now.Add(dur)
28
+}
29
+
30
+func (mt *mockTime) lastSleep() (slept bool, duration time.Duration) {
31
+	if mt.lastCheckedSleep == len(mt.sleepList)-1 {
32
+		slept = false
33
+		return
34
+	}
35
+
36
+	slept = true
37
+	mt.lastCheckedSleep += 1
38
+	duration = mt.sleepList[mt.lastCheckedSleep]
39
+	return
40
+}
41
+
42
+func newFakelagForTesting(window time.Duration, burstLimit uint, throttleMessagesPerWindow uint) (*Fakelag, *mockTime) {
43
+	fl := NewFakelag(window, burstLimit, throttleMessagesPerWindow)
44
+	mt := new(mockTime)
45
+	mt.now, _ = time.Parse("Mon Jan 2 15:04:05 -0700 MST 2006", "Mon Jan 2 15:04:05 -0700 MST 2006")
46
+	mt.lastCheckedSleep = -1
47
+	fl.nowFunc = mt.Now
48
+	fl.sleepFunc = mt.Sleep
49
+	return fl, mt
50
+}
51
+
52
+func TestFakelag(t *testing.T) {
53
+	window, _ := time.ParseDuration("1s")
54
+	fl, mt := newFakelagForTesting(window, 3, 2)
55
+
56
+	fl.Touch()
57
+	slept, _ := mt.lastSleep()
58
+	if slept {
59
+		t.Fatalf("should not have slept")
60
+	}
61
+
62
+	interval, _ := time.ParseDuration("100ms")
63
+	for i := 0; i < 2; i++ {
64
+		mt.pause(interval)
65
+		fl.Touch()
66
+		slept, _ := mt.lastSleep()
67
+		if slept {
68
+			t.Fatalf("should not have slept")
69
+		}
70
+	}
71
+
72
+	mt.pause(interval)
73
+	fl.Touch()
74
+	if fl.state != FakelagThrottled {
75
+		t.Fatalf("should be throttled")
76
+	}
77
+	slept, duration := mt.lastSleep()
78
+	if !slept {
79
+		t.Fatalf("should have slept due to fakelag")
80
+	}
81
+	expected, _ := time.ParseDuration("400ms")
82
+	if duration != expected {
83
+		t.Fatalf("incorrect sleep time: %v != %v", expected, duration)
84
+	}
85
+
86
+	fl.Touch()
87
+	if fl.state != FakelagThrottled {
88
+		t.Fatalf("should be throttled")
89
+	}
90
+	slept, duration = mt.lastSleep()
91
+	if duration != interval {
92
+		t.Fatalf("incorrect sleep time: %v != %v", interval, duration)
93
+	}
94
+
95
+	mt.pause(interval * 6)
96
+	fl.Touch()
97
+	if fl.state != FakelagThrottled {
98
+		t.Fatalf("should still be throttled")
99
+	}
100
+	slept, duration = mt.lastSleep()
101
+	if duration != 0 {
102
+		t.Fatalf("we paused for long enough that we shouldn't sleep here")
103
+	}
104
+
105
+	mt.pause(window * 2)
106
+	fl.Touch()
107
+	if fl.state != FakelagBursting {
108
+		t.Fatalf("should be bursting again")
109
+	}
110
+	slept, _ = mt.lastSleep()
111
+	if slept {
112
+		t.Fatalf("should not have slept")
113
+	}
114
+}

+ 13
- 1
irc/getters.go View File

@@ -59,7 +59,19 @@ func (server *Server) ChannelRegistrationEnabled() bool {
59 59
 func (server *Server) AccountConfig() *AccountConfig {
60 60
 	server.configurableStateMutex.RLock()
61 61
 	defer server.configurableStateMutex.RUnlock()
62
-	return server.accountConfig
62
+	if server.config == nil {
63
+		return nil
64
+	}
65
+	return &server.config.Accounts
66
+}
67
+
68
+func (server *Server) FakelagConfig() *FakelagConfig {
69
+	server.configurableStateMutex.RLock()
70
+	defer server.configurableStateMutex.RUnlock()
71
+	if server.config == nil {
72
+		return nil
73
+	}
74
+	return &server.config.Fakelag
63 75
 }
64 76
 
65 77
 func (client *Client) Nick() string {

+ 5
- 1
irc/handlers.go View File

@@ -1757,7 +1757,6 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
1757 1757
 		return true
1758 1758
 	}
1759 1759
 
1760
-	client.flags[modes.Operator] = true
1761 1760
 	client.operName = name
1762 1761
 	client.class = oper.Class
1763 1762
 	client.whoisLine = oper.WhoisLine
@@ -1795,6 +1794,11 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
1795 1794
 	rb.Add(nil, server.name, "MODE", client.nick, applied.String())
1796 1795
 
1797 1796
 	server.snomasks.Send(sno.LocalOpers, fmt.Sprintf(ircfmt.Unescape("Client opered up $c[grey][$r%s$c[grey], $r%s$c[grey]]"), client.nickMaskString, client.operName))
1797
+
1798
+	// client may now be unthrottled by the fakelag system
1799
+	client.resetFakelag()
1800
+
1801
+	client.flags[modes.Operator] = true
1798 1802
 	return false
1799 1803
 }
1800 1804
 

+ 16
- 15
irc/server.go View File

@@ -87,7 +87,6 @@ type ListenerWrapper struct {
87 87
 
88 88
 // Server is the main Oragono server.
89 89
 type Server struct {
90
-	accountConfig              *AccountConfig
91 90
 	accounts                   *AccountManager
92 91
 	batches                    *BatchManager
93 92
 	channelRegistrationEnabled bool
@@ -95,6 +94,7 @@ type Server struct {
95 94
 	channelRegistry            *ChannelRegistry
96 95
 	checkIdent                 bool
97 96
 	clients                    *ClientManager
97
+	config                     *Config
98 98
 	configFilename             string
99 99
 	configurableStateMutex     sync.RWMutex // tier 1; generic protection for server state modified by rehash()
100 100
 	connectionLimiter          *connection_limits.Limiter
@@ -214,10 +214,10 @@ func (server *Server) setISupport() {
214 214
 	isupport.Add("UTF8MAPPING", casemappingName)
215 215
 
216 216
 	// account registration
217
-	if server.accountConfig.Registration.Enabled {
217
+	if server.config.Accounts.Registration.Enabled {
218 218
 		// 'none' isn't shown in the REGCALLBACKS vars
219 219
 		var enabledCallbacks []string
220
-		for _, name := range server.accountConfig.Registration.EnabledCallbacks {
220
+		for _, name := range server.config.Accounts.Registration.EnabledCallbacks {
221 221
 			if name != "*" {
222 222
 				enabledCallbacks = append(enabledCallbacks, name)
223 223
 			}
@@ -830,10 +830,6 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
830 830
 		removedCaps.Add(caps.SASL)
831 831
 	}
832 832
 
833
-	server.configurableStateMutex.Lock()
834
-	server.accountConfig = &config.Accounts
835
-	server.configurableStateMutex.Unlock()
836
-
837 833
 	nickReservationPreviouslyDisabled := oldAccountConfig != nil && !oldAccountConfig.NickReservation.Enabled
838 834
 	nickReservationNowEnabled := config.Accounts.NickReservation.Enabled
839 835
 	if nickReservationPreviouslyDisabled && nickReservationNowEnabled {
@@ -943,14 +939,6 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
943 939
 		}
944 940
 	}
945 941
 
946
-	// set RPL_ISUPPORT
947
-	var newISupportReplies [][]string
948
-	oldISupportList := server.isupport
949
-	server.setISupport()
950
-	if oldISupportList != nil {
951
-		newISupportReplies = oldISupportList.GetDifference(server.isupport)
952
-	}
953
-
954 942
 	server.loadMOTD(config.Server.MOTD, config.Server.MOTDFormatting)
955 943
 
956 944
 	// reload logging config
@@ -963,6 +951,11 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
963 951
 	sendRawOutputNotice := !initial && !server.loggingRawIO && nowLoggingRawIO
964 952
 	server.loggingRawIO = nowLoggingRawIO
965 953
 
954
+	// save a pointer to the new config
955
+	server.configurableStateMutex.Lock()
956
+	server.config = config
957
+	server.configurableStateMutex.Unlock()
958
+
966 959
 	server.storeFilename = config.Datastore.Path
967 960
 	server.logger.Info("rehash", "Using datastore", server.storeFilename)
968 961
 	if initial {
@@ -973,6 +966,14 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
973 966
 
974 967
 	server.setupPprofListener(config)
975 968
 
969
+	// set RPL_ISUPPORT
970
+	var newISupportReplies [][]string
971
+	oldISupportList := server.ISupport()
972
+	server.setISupport()
973
+	if oldISupportList != nil {
974
+		newISupportReplies = oldISupportList.GetDifference(server.ISupport())
975
+	}
976
+
976 977
 	// we are now open for business
977 978
 	server.setupListeners(config)
978 979
 

+ 17
- 0
oragono.yaml View File

@@ -222,6 +222,7 @@ oper-classes:
222 222
             - "oper:local_kill"
223 223
             - "oper:local_ban"
224 224
             - "oper:local_unban"
225
+            - "nofakelag"
225 226
 
226 227
     # network operator
227 228
     "network-oper":
@@ -387,3 +388,19 @@ limits:
387 388
 
388 389
         # rest of the message
389 390
         rest: 2048
391
+
392
+# fakelag: prevents clients from spamming commands too rapidly
393
+fakelag:
394
+    # whether to enforce fakelag
395
+    enabled: true
396
+
397
+    # time unit for counting command rates
398
+    window: 1s
399
+
400
+    # clients can send this many commands without fakelag being imposed
401
+    # (resets after a period of `window` elapses without any commands)
402
+    burst-limit: 5
403
+
404
+    # once clients have exceeded their burst allowance, they can send only
405
+    # this many commands per `window`:
406
+    messages-per-window: 2

Loading…
Cancel
Save