mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-10 22:19:31 +01:00
implement fakelag (#189)
This commit is contained in:
parent
e3e714059c
commit
1bf5e2a7c8
@ -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
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
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)
|
||||
|
||||
|
17
oragono.yaml
17
oragono.yaml
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user