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

Merge pull request #1191 from slingamn/moderation.3

enhancements to moderation (#1134, #1135)
This commit is contained in:
Shivaram Lingamneni 2020-07-12 11:03:34 -07:00 committed by GitHub
commit 358c85e697
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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") - Multiclient ("Bouncer")
- History - History
- IP cloaking - IP cloaking
- Moderation
- Frequently Asked Questions - Frequently Asked Questions
- IRC over TLS - IRC over TLS
- Modes - 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`.) 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 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 { func (am *AccountManager) Unregister(account string, erase bool) error {
config := am.server.Config() config := am.server.Config()
casefoldedAccount, err := CasefoldName(account) casefoldedAccount, err := CasefoldName(account)
@ -1248,6 +1311,9 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
modesKey := fmt.Sprintf(keyAccountModes, casefoldedAccount) modesKey := fmt.Sprintf(keyAccountModes, casefoldedAccount)
var clients []*Client var clients []*Client
defer func() {
am.killClients(clients)
}()
var registeredChannels []string var registeredChannels []string
// on our way out, unregister all the account's channels and delete them from the db // 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) additionalSkel, _ := Skeleton(nick)
delete(am.skeletonToAccount, additionalSkel) 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 { if err != nil && !erase {
return errAccountDoesNotExist return errAccountDoesNotExist

View File

@ -59,7 +59,6 @@ type Client struct {
channels ChannelSet channels ChannelSet
ctime time.Time ctime time.Time
destroyed bool destroyed bool
exitedSnomaskSent bool
modes modes.ModeSet modes modes.ModeSet
hostname string hostname string
invitedTo StringSet invitedTo StringSet
@ -1281,7 +1280,6 @@ func (client *Client) destroy(session *Session) {
if saveLastSeen { if saveLastSeen {
client.dirtyBits |= IncludeLastSeen client.dirtyBits |= IncludeLastSeen
} }
exitedSnomaskSent := client.exitedSnomaskSent
autoAway := false autoAway := false
var awayMessage string 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) 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)) 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 return
} }
func (client *Client) SetExitedSnomaskSent() {
client.stateMutex.Lock()
client.exitedSnomaskSent = true
client.stateMutex.Unlock()
}
func (client *Client) AlwaysOn() (alwaysOn bool) { func (client *Client) AlwaysOn() (alwaysOn bool) {
client.stateMutex.Lock() client.stateMutex.Lock()
alwaysOn = client.alwaysOn alwaysOn = client.alwaysOn

View File

@ -83,7 +83,7 @@ func sendSuccessfulRegResponse(client *Client, rb *ResponseBuffer, forNS bool) {
} else { } else {
rb.Add(nil, client.server.name, RPL_REG_SUCCESS, details.nick, details.accountName, client.t("Account created")) 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) sendSuccessfulAccountAuth(client, rb, forNS, false)
} }
@ -867,7 +867,7 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
return false 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>")) 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 return false
} }
@ -906,24 +906,30 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
var killClient bool var killClient bool
if andKill { if andKill {
var clientsToKill []*Client var sessionsToKill []*Session
var killedClientNicks []string var killedClientNicks []string
for _, mcl := range server.clients.AllClients() { for _, mcl := range server.clients.AllClients() {
if hostNet.Contains(mcl.IP()) { nickKilled := false
clientsToKill = append(clientsToKill, mcl) for _, session := range mcl.Sessions() {
killedClientNicks = append(killedClientNicks, mcl.nick) if hostNet.Contains(session.IP()) {
sessionsToKill = append(sessionsToKill, session)
if !nickKilled {
killedClientNicks = append(killedClientNicks, mcl.Nick())
nickKilled = true
}
}
} }
} }
for _, mcl := range clientsToKill { for _, session := range sessionsToKill {
mcl.SetExitedSnomaskSent() mcl := session.client
mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason), nil) mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason), session)
if mcl == client { if session == rb.session {
killClient = true killClient = true
} else { } else {
// if mcl == client, we kill them below // 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) target := server.clients.Get(nickname)
if target == nil { 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 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) 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)) 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.Quit(quitMsg, nil)
target.destroy(nil) target.destroy(nil)
@ -1435,7 +1442,6 @@ func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
} }
for _, mcl := range clientsToKill { for _, mcl := range clientsToKill {
mcl.SetExitedSnomaskSent()
mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason), nil) mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason), nil)
if mcl == client { if mcl == client {
killClient = true killClient = true

View File

@ -316,6 +316,24 @@ example with $bCERT ADD <account> <fingerprint>$b.`,
enabled: servCmdRequiresAuthEnabled, enabled: servCmdRequiresAuthEnabled,
minParams: 1, 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")) 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"))
}
}