implement fakelag (#189)

This commit is contained in:
Shivaram Lingamneni 2018-03-22 11:04:21 -04:00
parent e3e714059c
commit 1bf5e2a7c8
9 changed files with 293 additions and 19 deletions

@ -49,6 +49,7 @@ type Client struct {
class *OperClass
ctime time.Time
exitedSnomaskSent bool
fakelag *Fakelag
flags map[modes.Mode]bool
hasQuit bool
hops int
@ -145,6 +146,26 @@ func NewClient(server *Server, conn net.Conn, isTLS bool) *Client {
return client
}
func (client *Client) resetFakelag() {
fakelag := func() *Fakelag {
if client.HasRoleCapabs("nofakelag") {
return nil
}
flc := client.server.FakelagConfig()
if !flc.Enabled {
return nil
}
return NewFakelag(flc.Window, flc.BurstLimit, flc.MessagesPerWindow)
}()
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
client.fakelag = fakelag
}
// IP returns the IP address of this client.
func (client *Client) IP() net.IP {
if client.proxiedIP != nil {
@ -221,6 +242,8 @@ func (client *Client) run() {
client.nickTimer = NewNickTimer(client)
client.resetFakelag()
// Set the hostname for this client
// (may be overridden by a later PROXY command from stunnel)
client.rawHostname = utils.AddrLookupHostname(client.socket.conn.RemoteAddr())

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

@ -189,6 +189,13 @@ type StackImpactConfig struct {
AppName string `yaml:"app-name"`
}
type FakelagConfig struct {
Enabled bool
Window time.Duration
BurstLimit uint `yaml:"burst-limit"`
MessagesPerWindow uint `yaml:"messages-per-window"`
}
// Config defines the overall configuration.
type Config struct {
Network struct {
@ -255,6 +262,8 @@ type Config struct {
LineLen LineLenConfig `yaml:"linelen"`
}
Fakelag FakelagConfig
Filename string
}

92
irc/fakelag.go Normal file

@ -0,0 +1,92 @@
// Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
// released under the MIT license
package irc
import (
"time"
)
// fakelag is a system for artificially delaying commands when a user issues
// them too rapidly
type FakelagState uint
const (
// initially, the client is "bursting" and can send n commands without
// encountering fakelag
FakelagBursting FakelagState = iota
// after that, they're "throttled" and we sleep in between commands until
// they're spaced sufficiently far apart
FakelagThrottled
)
// this is intentionally not threadsafe, because it should only be touched
// from the loop that accepts the client's input and runs commands
type Fakelag struct {
window time.Duration
burstLimit uint
throttleMessagesPerWindow uint
nowFunc func() time.Time
sleepFunc func(time.Duration)
state FakelagState
burstCount uint // number of messages sent in the current burst
lastTouch time.Time
}
func NewFakelag(window time.Duration, burstLimit uint, throttleMessagesPerWindow uint) *Fakelag {
return &Fakelag{
window: window,
burstLimit: burstLimit,
throttleMessagesPerWindow: throttleMessagesPerWindow,
nowFunc: time.Now,
sleepFunc: time.Sleep,
state: FakelagBursting,
}
}
// register a new command, sleep if necessary to delay it
func (fl *Fakelag) Touch() {
if fl == nil {
return
}
now := fl.nowFunc()
// XXX if lastTouch.IsZero(), treat it as "very far in the past", which is fine
elapsed := now.Sub(fl.lastTouch)
fl.lastTouch = now
if fl.state == FakelagBursting {
// determine if the previous burst is over
// (we could use 2*window instead)
if elapsed > fl.window {
fl.burstCount = 0
}
fl.burstCount++
if fl.burstCount > fl.burstLimit {
// reset burst window for next time
fl.burstCount = 0
// transition to throttling
fl.state = FakelagThrottled
// continue to throttling logic
} else {
return
}
}
if fl.state == FakelagThrottled {
if elapsed > fl.window {
// let them burst again (as above, we could use 2*window instead)
fl.state = FakelagBursting
return
}
// space them out by at least window/messagesperwindow
sleepDuration := time.Duration((int64(fl.window) / int64(fl.throttleMessagesPerWindow)) - int64(elapsed))
if sleepDuration < 0 {
sleepDuration = 0
}
fl.sleepFunc(sleepDuration)
}
}

114
irc/fakelag_test.go Normal file

@ -0,0 +1,114 @@
// Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
// released under the MIT license
package irc
import (
"testing"
"time"
)
type mockTime struct {
now time.Time
sleepList []time.Duration
lastCheckedSleep int
}
func (mt *mockTime) Now() (now time.Time) {
return mt.now
}
func (mt *mockTime) Sleep(dur time.Duration) {
mt.sleepList = append(mt.sleepList, dur)
mt.pause(dur)
}
func (mt *mockTime) pause(dur time.Duration) {
mt.now = mt.now.Add(dur)
}
func (mt *mockTime) lastSleep() (slept bool, duration time.Duration) {
if mt.lastCheckedSleep == len(mt.sleepList)-1 {
slept = false
return
}
slept = true
mt.lastCheckedSleep += 1
duration = mt.sleepList[mt.lastCheckedSleep]
return
}
func newFakelagForTesting(window time.Duration, burstLimit uint, throttleMessagesPerWindow uint) (*Fakelag, *mockTime) {
fl := NewFakelag(window, burstLimit, throttleMessagesPerWindow)
mt := new(mockTime)
mt.now, _ = time.Parse("Mon Jan 2 15:04:05 -0700 MST 2006", "Mon Jan 2 15:04:05 -0700 MST 2006")
mt.lastCheckedSleep = -1
fl.nowFunc = mt.Now
fl.sleepFunc = mt.Sleep
return fl, mt
}
func TestFakelag(t *testing.T) {
window, _ := time.ParseDuration("1s")
fl, mt := newFakelagForTesting(window, 3, 2)
fl.Touch()
slept, _ := mt.lastSleep()
if slept {
t.Fatalf("should not have slept")
}
interval, _ := time.ParseDuration("100ms")
for i := 0; i < 2; i++ {
mt.pause(interval)
fl.Touch()
slept, _ := mt.lastSleep()
if slept {
t.Fatalf("should not have slept")
}
}
mt.pause(interval)
fl.Touch()
if fl.state != FakelagThrottled {
t.Fatalf("should be throttled")
}
slept, duration := mt.lastSleep()
if !slept {
t.Fatalf("should have slept due to fakelag")
}
expected, _ := time.ParseDuration("400ms")
if duration != expected {
t.Fatalf("incorrect sleep time: %v != %v", expected, duration)
}
fl.Touch()
if fl.state != FakelagThrottled {
t.Fatalf("should be throttled")
}
slept, duration = mt.lastSleep()
if duration != interval {
t.Fatalf("incorrect sleep time: %v != %v", interval, duration)
}
mt.pause(interval * 6)
fl.Touch()
if fl.state != FakelagThrottled {
t.Fatalf("should still be throttled")
}
slept, duration = mt.lastSleep()
if duration != 0 {
t.Fatalf("we paused for long enough that we shouldn't sleep here")
}
mt.pause(window * 2)
fl.Touch()
if fl.state != FakelagBursting {
t.Fatalf("should be bursting again")
}
slept, _ = mt.lastSleep()
if slept {
t.Fatalf("should not have slept")
}
}

@ -59,7 +59,19 @@ func (server *Server) ChannelRegistrationEnabled() bool {
func (server *Server) AccountConfig() *AccountConfig {
server.configurableStateMutex.RLock()
defer server.configurableStateMutex.RUnlock()
return server.accountConfig
if server.config == nil {
return nil
}
return &server.config.Accounts
}
func (server *Server) FakelagConfig() *FakelagConfig {
server.configurableStateMutex.RLock()
defer server.configurableStateMutex.RUnlock()
if server.config == nil {
return nil
}
return &server.config.Fakelag
}
func (client *Client) Nick() string {

@ -1757,7 +1757,6 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
return true
}
client.flags[modes.Operator] = true
client.operName = name
client.class = oper.Class
client.whoisLine = oper.WhoisLine
@ -1795,6 +1794,11 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
rb.Add(nil, server.name, "MODE", client.nick, applied.String())
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))
// client may now be unthrottled by the fakelag system
client.resetFakelag()
client.flags[modes.Operator] = true
return false
}

@ -87,7 +87,6 @@ type ListenerWrapper struct {
// Server is the main Oragono server.
type Server struct {
accountConfig *AccountConfig
accounts *AccountManager
batches *BatchManager
channelRegistrationEnabled bool
@ -95,6 +94,7 @@ type Server struct {
channelRegistry *ChannelRegistry
checkIdent bool
clients *ClientManager
config *Config
configFilename string
configurableStateMutex sync.RWMutex // tier 1; generic protection for server state modified by rehash()
connectionLimiter *connection_limits.Limiter
@ -214,10 +214,10 @@ func (server *Server) setISupport() {
isupport.Add("UTF8MAPPING", casemappingName)
// account registration
if server.accountConfig.Registration.Enabled {
if server.config.Accounts.Registration.Enabled {
// 'none' isn't shown in the REGCALLBACKS vars
var enabledCallbacks []string
for _, name := range server.accountConfig.Registration.EnabledCallbacks {
for _, name := range server.config.Accounts.Registration.EnabledCallbacks {
if name != "*" {
enabledCallbacks = append(enabledCallbacks, name)
}
@ -830,10 +830,6 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
removedCaps.Add(caps.SASL)
}
server.configurableStateMutex.Lock()
server.accountConfig = &config.Accounts
server.configurableStateMutex.Unlock()
nickReservationPreviouslyDisabled := oldAccountConfig != nil && !oldAccountConfig.NickReservation.Enabled
nickReservationNowEnabled := config.Accounts.NickReservation.Enabled
if nickReservationPreviouslyDisabled && nickReservationNowEnabled {
@ -943,14 +939,6 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
}
}
// set RPL_ISUPPORT
var newISupportReplies [][]string
oldISupportList := server.isupport
server.setISupport()
if oldISupportList != nil {
newISupportReplies = oldISupportList.GetDifference(server.isupport)
}
server.loadMOTD(config.Server.MOTD, config.Server.MOTDFormatting)
// reload logging config
@ -963,6 +951,11 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
sendRawOutputNotice := !initial && !server.loggingRawIO && nowLoggingRawIO
server.loggingRawIO = nowLoggingRawIO
// save a pointer to the new config
server.configurableStateMutex.Lock()
server.config = config
server.configurableStateMutex.Unlock()
server.storeFilename = config.Datastore.Path
server.logger.Info("rehash", "Using datastore", server.storeFilename)
if initial {
@ -973,6 +966,14 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
server.setupPprofListener(config)
// set RPL_ISUPPORT
var newISupportReplies [][]string
oldISupportList := server.ISupport()
server.setISupport()
if oldISupportList != nil {
newISupportReplies = oldISupportList.GetDifference(server.ISupport())
}
// we are now open for business
server.setupListeners(config)

@ -222,6 +222,7 @@ oper-classes:
- "oper:local_kill"
- "oper:local_ban"
- "oper:local_unban"
- "nofakelag"
# network operator
"network-oper":
@ -387,3 +388,19 @@ limits:
# rest of the message
rest: 2048
# fakelag: prevents clients from spamming commands too rapidly
fakelag:
# whether to enforce fakelag
enabled: true
# time unit for counting command rates
window: 1s
# clients can send this many commands without fakelag being imposed
# (resets after a period of `window` elapses without any commands)
burst-limit: 5
# once clients have exceeded their burst allowance, they can send only
# this many commands per `window`:
messages-per-window: 2