Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

exempt a configurable number of MARKREAD commands from fakelag #1978

Merged
merged 4 commits into from
Aug 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,14 @@ fakelag:
# sending any commands:
cooldown: 2s

# exempt a certain number of command invocations per session from fakelag;
# this is to speed up "resynchronization" of client state during reattach
command-budgets:
"CHATHISTORY": 16
"MARKREAD": 16
"MONITOR": 1
"WHO": 4

# the roleplay commands are semi-standardized extensions to IRC that allow
# sending and receiving messages from pseudo-nicknames. this can be used either
# for actual roleplaying, or for bridging IRC with other protocols.
Expand Down
16 changes: 12 additions & 4 deletions irc/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -668,12 +668,21 @@ func (client *Client) run(session *Session) {
}
}

msg, err := ircmsg.ParseLineStrict(line, true, MaxLineLen)
// XXX defer processing of command error parsing until after fakelag

if client.registered {
touches := session.deferredFakelagCount + 1
// apply deferred fakelag
for i := 0; i < session.deferredFakelagCount; i++ {
session.fakelag.Touch("")
}
session.deferredFakelagCount = 0
for i := 0; i < touches; i++ {
session.fakelag.Touch()
// touch for the current command
var command string
if err == nil {
command = msg.Command
}
session.fakelag.Touch(command)
} else {
// DoS hardening, #505
session.registrationMessages++
Expand All @@ -683,7 +692,6 @@ func (client *Client) run(session *Session) {
}
}

msg, err := ircmsg.ParseLineStrict(line, true, MaxLineLen)
if err == ircmsg.ErrorLineIsEmpty {
continue
} else if err == ircmsg.ErrorTagsTooLong {
Expand Down
12 changes: 12 additions & 0 deletions irc/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ type FakelagConfig struct {
BurstLimit uint `yaml:"burst-limit"`
MessagesPerWindow uint `yaml:"messages-per-window"`
Cooldown time.Duration
CommandBudgets map[string]int `yaml:"command-budgets"`
}

type TorListenersConfig struct {
Expand Down Expand Up @@ -1428,6 +1429,17 @@ func LoadConfig(filename string) (config *Config, err error) {
}
config.Server.capValues[caps.Languages] = config.languageManager.CapValue()

if len(config.Fakelag.CommandBudgets) != 0 {
// normalize command names to uppercase:
commandBudgets := make(map[string]int, len(config.Fakelag.CommandBudgets))
for command, budget := range config.Fakelag.CommandBudgets {
commandBudgets[strings.ToUpper(command)] = budget
}
config.Fakelag.CommandBudgets = commandBudgets
} else {
config.Fakelag.CommandBudgets = nil
}

if config.Server.Relaymsg.Enabled {
for _, char := range protocolBreakingNameCharacters {
if strings.ContainsRune(config.Server.Relaymsg.Separators, char) {
Expand Down
13 changes: 12 additions & 1 deletion irc/fakelag.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package irc

import (
"time"

"github.com/ergochat/ergo/irc/utils"
)

// fakelag is a system for artificially delaying commands when a user issues
Expand Down Expand Up @@ -36,6 +38,10 @@ type Fakelag struct {

func (fl *Fakelag) Initialize(config FakelagConfig) {
fl.config = config
// XXX don't share mutable member CommandBudgets:
if config.CommandBudgets != nil {
fl.config.CommandBudgets = utils.CopyMap(config.CommandBudgets)
}
fl.nowFunc = time.Now
fl.sleepFunc = time.Sleep
fl.state = FakelagBursting
Expand All @@ -58,11 +64,16 @@ func (fl *Fakelag) Unsuspend() {
}

// register a new command, sleep if necessary to delay it
func (fl *Fakelag) Touch() {
func (fl *Fakelag) Touch(command string) {
if !fl.config.Enabled {
return
}

if budget, ok := fl.config.CommandBudgets[command]; ok && budget > 0 {
fl.config.CommandBudgets[command] = budget - 1
return
}

now := fl.nowFunc()
// XXX if lastTouch.IsZero(), treat it as "very far in the past", which is fine
elapsed := now.Sub(fl.lastTouch)
Expand Down
12 changes: 6 additions & 6 deletions irc/fakelag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func TestFakelag(t *testing.T) {
window, _ := time.ParseDuration("1s")
fl, mt := newFakelagForTesting(window, 3, 2, window)

fl.Touch()
fl.Touch("")
slept, _ := mt.lastSleep()
if slept {
t.Fatalf("should not have slept")
Expand All @@ -69,15 +69,15 @@ func TestFakelag(t *testing.T) {
interval, _ := time.ParseDuration("100ms")
for i := 0; i < 2; i++ {
mt.pause(interval)
fl.Touch()
fl.Touch("")
slept, _ := mt.lastSleep()
if slept {
t.Fatalf("should not have slept")
}
}

mt.pause(interval)
fl.Touch()
fl.Touch("")
if fl.state != FakelagThrottled {
t.Fatalf("should be throttled")
}
Expand All @@ -91,7 +91,7 @@ func TestFakelag(t *testing.T) {
}

// send another message without a pause; we should have to sleep for 500 msec
fl.Touch()
fl.Touch("")
if fl.state != FakelagThrottled {
t.Fatalf("should be throttled")
}
Expand All @@ -102,7 +102,7 @@ func TestFakelag(t *testing.T) {
}

mt.pause(interval * 6)
fl.Touch()
fl.Touch("")
if fl.state != FakelagThrottled {
t.Fatalf("should still be throttled")
}
Expand All @@ -112,7 +112,7 @@ func TestFakelag(t *testing.T) {
}

mt.pause(window * 2)
fl.Touch()
fl.Touch("")
if fl.state != FakelagBursting {
t.Fatalf("should be bursting again")
}
Expand Down
8 changes: 8 additions & 0 deletions traditional.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,14 @@ fakelag:
# sending any commands:
cooldown: 2s

# exempt a certain number of command invocations per session from fakelag;
# this is to speed up "resynchronization" of client state during reattach
command-budgets:
"CHATHISTORY": 16
"MARKREAD": 16
"MONITOR": 1
"WHO": 4

# the roleplay commands are semi-standardized extensions to IRC that allow
# sending and receiving messages from pseudo-nicknames. this can be used either
# for actual roleplaying, or for bridging IRC with other protocols.
Expand Down