mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-10 22:19:31 +01:00
Merge pull request #1191 from slingamn/moderation.3
enhancements to moderation (#1134, #1135)
This commit is contained in:
commit
358c85e697
@ -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.)
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user