mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-11 06:29:29 +01:00
Merge pull request #222 from slingamn/fakelag.3
implement fakelag (#189)
This commit is contained in:
commit
65f2e95d2b
@ -49,6 +49,7 @@ type Client struct {
|
|||||||
class *OperClass
|
class *OperClass
|
||||||
ctime time.Time
|
ctime time.Time
|
||||||
exitedSnomaskSent bool
|
exitedSnomaskSent bool
|
||||||
|
fakelag *Fakelag
|
||||||
flags map[modes.Mode]bool
|
flags map[modes.Mode]bool
|
||||||
hasQuit bool
|
hasQuit bool
|
||||||
hops int
|
hops int
|
||||||
@ -145,6 +146,26 @@ func NewClient(server *Server, conn net.Conn, isTLS bool) *Client {
|
|||||||
return 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, flc.Cooldown)
|
||||||
|
}()
|
||||||
|
|
||||||
|
client.stateMutex.Lock()
|
||||||
|
defer client.stateMutex.Unlock()
|
||||||
|
client.fakelag = fakelag
|
||||||
|
}
|
||||||
|
|
||||||
// IP returns the IP address of this client.
|
// IP returns the IP address of this client.
|
||||||
func (client *Client) IP() net.IP {
|
func (client *Client) IP() net.IP {
|
||||||
if client.proxiedIP != nil {
|
if client.proxiedIP != nil {
|
||||||
@ -221,6 +242,8 @@ func (client *Client) run() {
|
|||||||
|
|
||||||
client.nickTimer = NewNickTimer(client)
|
client.nickTimer = NewNickTimer(client)
|
||||||
|
|
||||||
|
client.resetFakelag()
|
||||||
|
|
||||||
// Set the hostname for this client
|
// Set the hostname for this client
|
||||||
// (may be overridden by a later PROXY command from stunnel)
|
// (may be overridden by a later PROXY command from stunnel)
|
||||||
client.rawHostname = utils.AddrLookupHostname(client.socket.conn.RemoteAddr())
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if client.registered {
|
||||||
|
client.fakelag.Touch()
|
||||||
|
}
|
||||||
|
|
||||||
rb := NewResponseBuffer(client)
|
rb := NewResponseBuffer(client)
|
||||||
rb.Label = GetLabel(msg)
|
rb.Label = GetLabel(msg)
|
||||||
|
|
||||||
exiting := cmd.handler(server, client, msg, rb)
|
exiting := cmd.handler(server, client, msg, rb)
|
||||||
|
|
||||||
rb.Send()
|
rb.Send()
|
||||||
|
|
||||||
// after each command, see if we can send registration to the client
|
// after each command, see if we can send registration to the client
|
||||||
|
@ -189,6 +189,14 @@ type StackImpactConfig struct {
|
|||||||
AppName string `yaml:"app-name"`
|
AppName string `yaml:"app-name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FakelagConfig struct {
|
||||||
|
Enabled bool
|
||||||
|
Window time.Duration
|
||||||
|
BurstLimit uint `yaml:"burst-limit"`
|
||||||
|
MessagesPerWindow uint `yaml:"messages-per-window"`
|
||||||
|
Cooldown time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
// Config defines the overall configuration.
|
// Config defines the overall configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Network struct {
|
Network struct {
|
||||||
@ -255,6 +263,8 @@ type Config struct {
|
|||||||
LineLen LineLenConfig `yaml:"linelen"`
|
LineLen LineLenConfig `yaml:"linelen"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Fakelag FakelagConfig
|
||||||
|
|
||||||
Filename string
|
Filename string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
93
irc/fakelag.go
Normal file
93
irc/fakelag.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
// 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
|
||||||
|
cooldown time.Duration
|
||||||
|
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, cooldown time.Duration) *Fakelag {
|
||||||
|
return &Fakelag{
|
||||||
|
window: window,
|
||||||
|
burstLimit: burstLimit,
|
||||||
|
throttleMessagesPerWindow: throttleMessagesPerWindow,
|
||||||
|
cooldown: cooldown,
|
||||||
|
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
|
||||||
|
if elapsed > fl.cooldown {
|
||||||
|
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.cooldown {
|
||||||
|
// let them burst again
|
||||||
|
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, cooldown time.Duration) (*Fakelag, *mockTime) {
|
||||||
|
fl := NewFakelag(window, burstLimit, throttleMessagesPerWindow, cooldown)
|
||||||
|
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, window)
|
||||||
|
|
||||||
|
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 {
|
func (server *Server) AccountConfig() *AccountConfig {
|
||||||
server.configurableStateMutex.RLock()
|
server.configurableStateMutex.RLock()
|
||||||
defer server.configurableStateMutex.RUnlock()
|
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 {
|
func (client *Client) Nick() string {
|
||||||
|
@ -1757,7 +1757,6 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
client.flags[modes.Operator] = true
|
|
||||||
client.operName = name
|
client.operName = name
|
||||||
client.class = oper.Class
|
client.class = oper.Class
|
||||||
client.whoisLine = oper.WhoisLine
|
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())
|
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))
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +87,6 @@ type ListenerWrapper struct {
|
|||||||
|
|
||||||
// Server is the main Oragono server.
|
// Server is the main Oragono server.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
accountConfig *AccountConfig
|
|
||||||
accounts *AccountManager
|
accounts *AccountManager
|
||||||
batches *BatchManager
|
batches *BatchManager
|
||||||
channelRegistrationEnabled bool
|
channelRegistrationEnabled bool
|
||||||
@ -95,6 +94,7 @@ type Server struct {
|
|||||||
channelRegistry *ChannelRegistry
|
channelRegistry *ChannelRegistry
|
||||||
checkIdent bool
|
checkIdent bool
|
||||||
clients *ClientManager
|
clients *ClientManager
|
||||||
|
config *Config
|
||||||
configFilename string
|
configFilename string
|
||||||
configurableStateMutex sync.RWMutex // tier 1; generic protection for server state modified by rehash()
|
configurableStateMutex sync.RWMutex // tier 1; generic protection for server state modified by rehash()
|
||||||
connectionLimiter *connection_limits.Limiter
|
connectionLimiter *connection_limits.Limiter
|
||||||
@ -214,10 +214,10 @@ func (server *Server) setISupport() {
|
|||||||
isupport.Add("UTF8MAPPING", casemappingName)
|
isupport.Add("UTF8MAPPING", casemappingName)
|
||||||
|
|
||||||
// account registration
|
// account registration
|
||||||
if server.accountConfig.Registration.Enabled {
|
if server.config.Accounts.Registration.Enabled {
|
||||||
// 'none' isn't shown in the REGCALLBACKS vars
|
// 'none' isn't shown in the REGCALLBACKS vars
|
||||||
var enabledCallbacks []string
|
var enabledCallbacks []string
|
||||||
for _, name := range server.accountConfig.Registration.EnabledCallbacks {
|
for _, name := range server.config.Accounts.Registration.EnabledCallbacks {
|
||||||
if name != "*" {
|
if name != "*" {
|
||||||
enabledCallbacks = append(enabledCallbacks, name)
|
enabledCallbacks = append(enabledCallbacks, name)
|
||||||
}
|
}
|
||||||
@ -830,10 +830,6 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
|
|||||||
removedCaps.Add(caps.SASL)
|
removedCaps.Add(caps.SASL)
|
||||||
}
|
}
|
||||||
|
|
||||||
server.configurableStateMutex.Lock()
|
|
||||||
server.accountConfig = &config.Accounts
|
|
||||||
server.configurableStateMutex.Unlock()
|
|
||||||
|
|
||||||
nickReservationPreviouslyDisabled := oldAccountConfig != nil && !oldAccountConfig.NickReservation.Enabled
|
nickReservationPreviouslyDisabled := oldAccountConfig != nil && !oldAccountConfig.NickReservation.Enabled
|
||||||
nickReservationNowEnabled := config.Accounts.NickReservation.Enabled
|
nickReservationNowEnabled := config.Accounts.NickReservation.Enabled
|
||||||
if nickReservationPreviouslyDisabled && nickReservationNowEnabled {
|
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)
|
server.loadMOTD(config.Server.MOTD, config.Server.MOTDFormatting)
|
||||||
|
|
||||||
// reload logging config
|
// reload logging config
|
||||||
@ -963,6 +951,11 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
|
|||||||
sendRawOutputNotice := !initial && !server.loggingRawIO && nowLoggingRawIO
|
sendRawOutputNotice := !initial && !server.loggingRawIO && nowLoggingRawIO
|
||||||
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.storeFilename = config.Datastore.Path
|
||||||
server.logger.Info("rehash", "Using datastore", server.storeFilename)
|
server.logger.Info("rehash", "Using datastore", server.storeFilename)
|
||||||
if initial {
|
if initial {
|
||||||
@ -973,6 +966,14 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
|
|||||||
|
|
||||||
server.setupPprofListener(config)
|
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
|
// we are now open for business
|
||||||
server.setupListeners(config)
|
server.setupListeners(config)
|
||||||
|
|
||||||
|
20
oragono.yaml
20
oragono.yaml
@ -222,6 +222,7 @@ oper-classes:
|
|||||||
- "oper:local_kill"
|
- "oper:local_kill"
|
||||||
- "oper:local_ban"
|
- "oper:local_ban"
|
||||||
- "oper:local_unban"
|
- "oper:local_unban"
|
||||||
|
- "nofakelag"
|
||||||
|
|
||||||
# network operator
|
# network operator
|
||||||
"network-oper":
|
"network-oper":
|
||||||
@ -387,3 +388,22 @@ limits:
|
|||||||
|
|
||||||
# rest of the message
|
# rest of the message
|
||||||
rest: 2048
|
rest: 2048
|
||||||
|
|
||||||
|
# fakelag: prevents clients from spamming commands too rapidly
|
||||||
|
fakelag:
|
||||||
|
# whether to enforce fakelag
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
# time unit for counting command rates
|
||||||
|
window: 1s
|
||||||
|
|
||||||
|
# clients can send this many commands without fakelag being imposed
|
||||||
|
burst-limit: 5
|
||||||
|
|
||||||
|
# once clients have exceeded their burst allowance, they can send only
|
||||||
|
# this many commands per `window`:
|
||||||
|
messages-per-window: 2
|
||||||
|
|
||||||
|
# client status resets to the default state if they go this long without
|
||||||
|
# sending any commands:
|
||||||
|
cooldown: 1s
|
||||||
|
Loading…
Reference in New Issue
Block a user