3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-22 11:59:40 +01:00

enhancements to moderation (#1134, #1135)

This commit is contained in:
Shivaram Lingamneni 2020-07-10 17:09:02 -04:00
parent 57f2857e83
commit a7ca6601c7
6 changed files with 150 additions and 29 deletions

View File

@ -35,6 +35,7 @@ _Copyright © Daniel Oaks <daniel@danieloaks.net>, Shivaram Lingamneni <slingamn
- Multiclient ("Bouncer")
- History
- IP cloaking
- Moderation
- Frequently Asked Questions
- IRC over TLS
- Modes
@ -362,6 +363,26 @@ Oragono supports cloaking, which is enabled by default (via the `server.ip-cloak
Setting `server.ip-cloaking.num-bits` to 0 gives users cloaks that don't depend on their IP address information at all, which is an option for deployments where privacy is a more pressing concern than abuse. Holders of registered accounts can also use the vhost system (for details, `/msg HostServ HELP`.)
## Moderation
Oragono's multiclient and always-on features mean that moderation (at the server operator level) requires different techniques than a traditional IRC network. Server operators have three principal tools for moderation:
1. `/NICKSERV SUSPEND`, which disables a user account and disconnects all associated clients
2. `/DLINE ANDKILL`, which bans an IP or CIDR and disconnects clients
3. `/DEFCON`, which can impose emergency restrictions on user activity in response to attacks
See the `/HELP` (or `/HELPOP`) entries for these commands for more information, but here's a rough workflow for mitigating spam or other attacks:
1. Subscribe to the `a` snomask to monitor for abusive registration attempts (this is set automatically in the default operator config, but can be added manually with `/mode mynick +s u`)
2. Given abusive traffic from a nickname, identify whether they are using an account (this should be displayed in `/WHOIS` output)
3. If they are using an account, suspend the account with `/NICKSERV SUSPEND`, which will disconnect them
4. If they are not using an account, or if they're spamming new registrations from an IP, determine the IP (either from `/WHOIS` or from account registration notices) and temporarily `/DLINE` their IP
5. When facing a flood of abusive registrations that cannot be stemmed with `/DLINE`, use `/DEFCON 4` to temporarily restrict registrations. (At `/DEFCON 2`, all new connections to the server will require SASL, but this will likely be disruptive to legitimate users as well.)
For channel operators, as opposed to server operators, most traditional moderation tools should be effective. In particular, bans on cloaked hostnames (e.g., `/mode #chan +b *!*@98rgwnst3dahu.my.network`) should work as expected. With `force-nick-equals-account` enabled, channel operators can also ban nicknames (with `/mode #chan +b nick`, which Oragono automatically expands to `/mode #chan +b nick!*@*` as a way of banning an account.)
-------------------------------------------------------------------------------------------

View File

@ -1223,6 +1223,69 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
return
}
func (am *AccountManager) Suspend(accountName string) (err error) {
account, err := CasefoldName(accountName)
if err != nil {
return errAccountDoesNotExist
}
existsKey := fmt.Sprintf(keyAccountExists, account)
verifiedKey := fmt.Sprintf(keyAccountVerified, account)
err = am.server.store.Update(func(tx *buntdb.Tx) error {
_, err := tx.Get(existsKey)
if err != nil {
return errAccountDoesNotExist
}
_, err = tx.Delete(verifiedKey)
return err
})
if err == errAccountDoesNotExist {
return err
} else if err != nil {
am.server.logger.Error("internal", "couldn't persist suspension", account, err.Error())
} // keep going
am.Lock()
clients := am.accountToClients[account]
delete(am.accountToClients, account)
am.Unlock()
am.killClients(clients)
return nil
}
func (am *AccountManager) killClients(clients []*Client) {
for _, client := range clients {
client.Logout()
client.Quit(client.t("You are no longer authorized to be on this server"), nil)
client.destroy(nil)
}
}
func (am *AccountManager) Unsuspend(account string) (err error) {
cfaccount, err := CasefoldName(account)
if err != nil {
return errAccountDoesNotExist
}
existsKey := fmt.Sprintf(keyAccountExists, cfaccount)
verifiedKey := fmt.Sprintf(keyAccountVerified, cfaccount)
err = am.server.store.Update(func(tx *buntdb.Tx) error {
_, err := tx.Get(existsKey)
if err != nil {
return errAccountDoesNotExist
}
tx.Set(verifiedKey, "1", nil)
return nil
})
if err != nil {
return errAccountDoesNotExist
}
return nil
}
func (am *AccountManager) Unregister(account string, erase bool) error {
config := am.server.Config()
casefoldedAccount, err := CasefoldName(account)
@ -1248,6 +1311,9 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
modesKey := fmt.Sprintf(keyAccountModes, casefoldedAccount)
var clients []*Client
defer func() {
am.killClients(clients)
}()
var registeredChannels []string
// on our way out, unregister all the account's channels and delete them from the db
@ -1341,12 +1407,6 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
additionalSkel, _ := Skeleton(nick)
delete(am.skeletonToAccount, additionalSkel)
}
for _, client := range clients {
client.Logout()
client.Quit(client.t("You are no longer authorized to be on this server"), nil)
// destroy acquires a semaphore so we can't call it while holding a lock
go client.destroy(nil)
}
if err != nil && !erase {
return errAccountDoesNotExist

View File

@ -59,7 +59,6 @@ type Client struct {
channels ChannelSet
ctime time.Time
destroyed bool
exitedSnomaskSent bool
modes modes.ModeSet
hostname string
invitedTo StringSet
@ -1281,7 +1280,6 @@ func (client *Client) destroy(session *Session) {
if saveLastSeen {
client.dirtyBits |= IncludeLastSeen
}
exitedSnomaskSent := client.exitedSnomaskSent
autoAway := false
var awayMessage string
@ -1423,7 +1421,7 @@ func (client *Client) destroy(session *Session) {
friend.sendFromClientInternal(false, splitQuitMessage.Time, splitQuitMessage.Msgid, details.nickMask, details.accountName, nil, "QUIT", quitMessage)
}
if !exitedSnomaskSent && registered {
if registered {
client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r exited the network"), details.nick))
}
}

View File

@ -201,12 +201,6 @@ func (client *Client) SetAway(away bool, awayMessage string) (changed bool) {
return
}
func (client *Client) SetExitedSnomaskSent() {
client.stateMutex.Lock()
client.exitedSnomaskSent = true
client.stateMutex.Unlock()
}
func (client *Client) AlwaysOn() (alwaysOn bool) {
client.stateMutex.Lock()
alwaysOn = client.alwaysOn

View File

@ -83,7 +83,7 @@ func sendSuccessfulRegResponse(client *Client, rb *ResponseBuffer, forNS bool) {
} else {
rb.Add(nil, client.server.name, RPL_REG_SUCCESS, details.nick, details.accountName, client.t("Account created"))
}
client.server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Client $c[grey][$r%s$c[grey]] registered account $c[grey][$r%s$c[grey]]"), details.nickMask, details.accountName))
client.server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Client $c[grey][$r%s$c[grey]] registered account $c[grey][$r%s$c[grey]] from IP %s"), details.nickMask, details.accountName, rb.session.IP().String()))
sendSuccessfulAccountAuth(client, rb, forNS, false)
}
@ -867,7 +867,7 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
return false
}
if !dlineMyself && hostNet.Contains(client.IP()) {
if !dlineMyself && hostNet.Contains(rb.session.IP()) {
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, client.t("This ban matches you. To DLINE yourself, you must use the command: /DLINE MYSELF <arguments>"))
return false
}
@ -906,24 +906,30 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
var killClient bool
if andKill {
var clientsToKill []*Client
var sessionsToKill []*Session
var killedClientNicks []string
for _, mcl := range server.clients.AllClients() {
if hostNet.Contains(mcl.IP()) {
clientsToKill = append(clientsToKill, mcl)
killedClientNicks = append(killedClientNicks, mcl.nick)
nickKilled := false
for _, session := range mcl.Sessions() {
if hostNet.Contains(session.IP()) {
sessionsToKill = append(sessionsToKill, session)
if !nickKilled {
killedClientNicks = append(killedClientNicks, mcl.Nick())
nickKilled = true
}
}
}
}
for _, mcl := range clientsToKill {
mcl.SetExitedSnomaskSent()
mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason), nil)
if mcl == client {
for _, session := range sessionsToKill {
mcl := session.client
mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason), session)
if session == rb.session {
killClient = true
} else {
// if mcl == client, we kill them below
mcl.destroy(nil)
mcl.destroy(session)
}
}
@ -1296,14 +1302,15 @@ func killHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
target := server.clients.Get(nickname)
if target == nil {
rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.nick, utils.SafeErrorParam(nickname), client.t("No such nick"))
rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.Nick(), utils.SafeErrorParam(nickname), client.t("No such nick"))
return false
} else if target.AlwaysOn() {
rb.Add(nil, client.server.name, ERR_UNKNOWNERROR, client.Nick(), "KILL", fmt.Sprintf(client.t("Client %s is always-on and cannot be fully removed by /KILL; consider /NS SUSPEND instead"), target.Nick()))
}
quitMsg := fmt.Sprintf("Killed (%s (%s))", client.nick, comment)
server.snomasks.Send(sno.LocalKills, fmt.Sprintf(ircfmt.Unescape("%s$r was killed by %s $c[grey][$r%s$c[grey]]"), target.nick, client.nick, comment))
target.SetExitedSnomaskSent()
target.Quit(quitMsg, nil)
target.destroy(nil)
@ -1435,7 +1442,6 @@ func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
}
for _, mcl := range clientsToKill {
mcl.SetExitedSnomaskSent()
mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason), nil)
if mcl == client {
killClient = true

View File

@ -316,6 +316,24 @@ example with $bCERT ADD <account> <fingerprint>$b.`,
enabled: servCmdRequiresAuthEnabled,
minParams: 1,
},
"suspend": {
handler: nsSuspendHandler,
help: `Syntax: $bSUSPEND <nickname>$b
SUSPEND disables an account and disconnects the associated clients.`,
helpShort: `$bSUSPEND$b disables an account and disconnects the clients`,
minParams: 1,
capabs: []string{"accreg"},
},
"unsuspend": {
handler: nsUnsuspendHandler,
help: `Syntax: $bUNSUSPEND <nickname>$b
UNSUSPEND reverses a previous SUSPEND, restoring access to the account.`,
helpShort: `$bUNSUSPEND$b restores access to a suspended account`,
minParams: 1,
capabs: []string{"accreg"},
},
}
)
@ -1177,3 +1195,27 @@ func nsCertHandler(server *Server, client *Client, command string, params []stri
nsNotice(rb, client.t("An error occurred"))
}
}
func nsSuspendHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
err := server.accounts.Suspend(params[0])
switch err {
case nil:
nsNotice(rb, fmt.Sprintf(client.t("Successfully suspended account %s"), params[0]))
case errAccountDoesNotExist:
nsNotice(rb, client.t("No such account"))
default:
nsNotice(rb, client.t("An error occurred"))
}
}
func nsUnsuspendHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
err := server.accounts.Unsuspend(params[0])
switch err {
case nil:
nsNotice(rb, fmt.Sprintf(client.t("Successfully un-suspended account %s"), params[0]))
case errAccountDoesNotExist:
nsNotice(rb, client.t("No such account"))
default:
nsNotice(rb, client.t("An error occurred"))
}
}