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:
commit
358c85e697
@ -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.)
|
||||||
|
|
||||||
|
|
||||||
-------------------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user