Browse Source

enhancements to moderation (#1134, #1135)

tags/v2.2.0-rc1
Shivaram Lingamneni 3 years ago
parent
commit
a7ca6601c7
6 changed files with 150 additions and 29 deletions
  1. 21
    0
      docs/MANUAL.md
  2. 66
    6
      irc/accounts.go
  3. 1
    3
      irc/client.go
  4. 0
    6
      irc/getters.go
  5. 20
    14
      irc/handlers.go
  6. 42
    0
      irc/nickserv.go

+ 21
- 0
docs/MANUAL.md View File

@@ -35,6 +35,7 @@ _Copyright © Daniel Oaks <daniel@danieloaks.net>, Shivaram Lingamneni <slingamn
35 35
     - Multiclient ("Bouncer")
36 36
     - History
37 37
     - IP cloaking
38
+    - Moderation
38 39
 - Frequently Asked Questions
39 40
 - IRC over TLS
40 41
 - Modes
@@ -362,6 +363,26 @@ Oragono supports cloaking, which is enabled by default (via the `server.ip-cloak
362 363
 
363 364
 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`.)
364 365
 
366
+
367
+## Moderation
368
+
369
+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:
370
+
371
+1. `/NICKSERV SUSPEND`, which disables a user account and disconnects all associated clients
372
+2. `/DLINE ANDKILL`, which bans an IP or CIDR and disconnects clients
373
+3. `/DEFCON`, which can impose emergency restrictions on user activity in response to attacks
374
+
375
+See the `/HELP` (or `/HELPOP`) entries for these commands for more information, but here's a rough workflow for mitigating spam or other attacks:
376
+
377
+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`)
378
+2. Given abusive traffic from a nickname, identify whether they are using an account (this should be displayed in `/WHOIS` output)
379
+3. If they are using an account, suspend the account with `/NICKSERV SUSPEND`, which will disconnect them
380
+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
381
+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.)
382
+
383
+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.)
384
+
385
+
365 386
 -------------------------------------------------------------------------------------------
366 387
 
367 388
 

+ 66
- 6
irc/accounts.go View File

@@ -1223,6 +1223,69 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
1223 1223
 	return
1224 1224
 }
1225 1225
 
1226
+func (am *AccountManager) Suspend(accountName string) (err error) {
1227
+	account, err := CasefoldName(accountName)
1228
+	if err != nil {
1229
+		return errAccountDoesNotExist
1230
+	}
1231
+
1232
+	existsKey := fmt.Sprintf(keyAccountExists, account)
1233
+	verifiedKey := fmt.Sprintf(keyAccountVerified, account)
1234
+	err = am.server.store.Update(func(tx *buntdb.Tx) error {
1235
+		_, err := tx.Get(existsKey)
1236
+		if err != nil {
1237
+			return errAccountDoesNotExist
1238
+		}
1239
+		_, err = tx.Delete(verifiedKey)
1240
+		return err
1241
+	})
1242
+
1243
+	if err == errAccountDoesNotExist {
1244
+		return err
1245
+	} else if err != nil {
1246
+		am.server.logger.Error("internal", "couldn't persist suspension", account, err.Error())
1247
+	} // keep going
1248
+
1249
+	am.Lock()
1250
+	clients := am.accountToClients[account]
1251
+	delete(am.accountToClients, account)
1252
+	am.Unlock()
1253
+
1254
+	am.killClients(clients)
1255
+	return nil
1256
+}
1257
+
1258
+func (am *AccountManager) killClients(clients []*Client) {
1259
+	for _, client := range clients {
1260
+		client.Logout()
1261
+		client.Quit(client.t("You are no longer authorized to be on this server"), nil)
1262
+		client.destroy(nil)
1263
+	}
1264
+}
1265
+
1266
+func (am *AccountManager) Unsuspend(account string) (err error) {
1267
+	cfaccount, err := CasefoldName(account)
1268
+	if err != nil {
1269
+		return errAccountDoesNotExist
1270
+	}
1271
+
1272
+	existsKey := fmt.Sprintf(keyAccountExists, cfaccount)
1273
+	verifiedKey := fmt.Sprintf(keyAccountVerified, cfaccount)
1274
+	err = am.server.store.Update(func(tx *buntdb.Tx) error {
1275
+		_, err := tx.Get(existsKey)
1276
+		if err != nil {
1277
+			return errAccountDoesNotExist
1278
+		}
1279
+		tx.Set(verifiedKey, "1", nil)
1280
+		return nil
1281
+	})
1282
+
1283
+	if err != nil {
1284
+		return errAccountDoesNotExist
1285
+	}
1286
+	return nil
1287
+}
1288
+
1226 1289
 func (am *AccountManager) Unregister(account string, erase bool) error {
1227 1290
 	config := am.server.Config()
1228 1291
 	casefoldedAccount, err := CasefoldName(account)
@@ -1248,6 +1311,9 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
1248 1311
 	modesKey := fmt.Sprintf(keyAccountModes, casefoldedAccount)
1249 1312
 
1250 1313
 	var clients []*Client
1314
+	defer func() {
1315
+		am.killClients(clients)
1316
+	}()
1251 1317
 
1252 1318
 	var registeredChannels []string
1253 1319
 	// 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 {
1341 1407
 		additionalSkel, _ := Skeleton(nick)
1342 1408
 		delete(am.skeletonToAccount, additionalSkel)
1343 1409
 	}
1344
-	for _, client := range clients {
1345
-		client.Logout()
1346
-		client.Quit(client.t("You are no longer authorized to be on this server"), nil)
1347
-		// destroy acquires a semaphore so we can't call it while holding a lock
1348
-		go client.destroy(nil)
1349
-	}
1350 1410
 
1351 1411
 	if err != nil && !erase {
1352 1412
 		return errAccountDoesNotExist

+ 1
- 3
irc/client.go View File

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

+ 0
- 6
irc/getters.go View File

@@ -201,12 +201,6 @@ func (client *Client) SetAway(away bool, awayMessage string) (changed bool) {
201 201
 	return
202 202
 }
203 203
 
204
-func (client *Client) SetExitedSnomaskSent() {
205
-	client.stateMutex.Lock()
206
-	client.exitedSnomaskSent = true
207
-	client.stateMutex.Unlock()
208
-}
209
-
210 204
 func (client *Client) AlwaysOn() (alwaysOn bool) {
211 205
 	client.stateMutex.Lock()
212 206
 	alwaysOn = client.alwaysOn

+ 20
- 14
irc/handlers.go View File

@@ -83,7 +83,7 @@ func sendSuccessfulRegResponse(client *Client, rb *ResponseBuffer, forNS bool) {
83 83
 	} else {
84 84
 		rb.Add(nil, client.server.name, RPL_REG_SUCCESS, details.nick, details.accountName, client.t("Account created"))
85 85
 	}
86
-	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))
86
+	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()))
87 87
 	sendSuccessfulAccountAuth(client, rb, forNS, false)
88 88
 }
89 89
 
@@ -867,7 +867,7 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
867 867
 		return false
868 868
 	}
869 869
 
870
-	if !dlineMyself && hostNet.Contains(client.IP()) {
870
+	if !dlineMyself && hostNet.Contains(rb.session.IP()) {
871 871
 		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>"))
872 872
 		return false
873 873
 	}
@@ -906,24 +906,30 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
906 906
 
907 907
 	var killClient bool
908 908
 	if andKill {
909
-		var clientsToKill []*Client
909
+		var sessionsToKill []*Session
910 910
 		var killedClientNicks []string
911 911
 
912 912
 		for _, mcl := range server.clients.AllClients() {
913
-			if hostNet.Contains(mcl.IP()) {
914
-				clientsToKill = append(clientsToKill, mcl)
915
-				killedClientNicks = append(killedClientNicks, mcl.nick)
913
+			nickKilled := false
914
+			for _, session := range mcl.Sessions() {
915
+				if hostNet.Contains(session.IP()) {
916
+					sessionsToKill = append(sessionsToKill, session)
917
+					if !nickKilled {
918
+						killedClientNicks = append(killedClientNicks, mcl.Nick())
919
+						nickKilled = true
920
+					}
921
+				}
916 922
 			}
917 923
 		}
918 924
 
919
-		for _, mcl := range clientsToKill {
920
-			mcl.SetExitedSnomaskSent()
921
-			mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason), nil)
922
-			if mcl == client {
925
+		for _, session := range sessionsToKill {
926
+			mcl := session.client
927
+			mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason), session)
928
+			if session == rb.session {
923 929
 				killClient = true
924 930
 			} else {
925 931
 				// if mcl == client, we kill them below
926
-				mcl.destroy(nil)
932
+				mcl.destroy(session)
927 933
 			}
928 934
 		}
929 935
 
@@ -1296,14 +1302,15 @@ func killHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
1296 1302
 
1297 1303
 	target := server.clients.Get(nickname)
1298 1304
 	if target == nil {
1299
-		rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.nick, utils.SafeErrorParam(nickname), client.t("No such nick"))
1305
+		rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.Nick(), utils.SafeErrorParam(nickname), client.t("No such nick"))
1300 1306
 		return false
1307
+	} else if target.AlwaysOn() {
1308
+		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()))
1301 1309
 	}
1302 1310
 
1303 1311
 	quitMsg := fmt.Sprintf("Killed (%s (%s))", client.nick, comment)
1304 1312
 
1305 1313
 	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))
1306
-	target.SetExitedSnomaskSent()
1307 1314
 
1308 1315
 	target.Quit(quitMsg, nil)
1309 1316
 	target.destroy(nil)
@@ -1435,7 +1442,6 @@ func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
1435 1442
 		}
1436 1443
 
1437 1444
 		for _, mcl := range clientsToKill {
1438
-			mcl.SetExitedSnomaskSent()
1439 1445
 			mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason), nil)
1440 1446
 			if mcl == client {
1441 1447
 				killClient = true

+ 42
- 0
irc/nickserv.go View File

@@ -316,6 +316,24 @@ example with $bCERT ADD <account> <fingerprint>$b.`,
316 316
 			enabled:   servCmdRequiresAuthEnabled,
317 317
 			minParams: 1,
318 318
 		},
319
+		"suspend": {
320
+			handler: nsSuspendHandler,
321
+			help: `Syntax: $bSUSPEND <nickname>$b
322
+
323
+SUSPEND disables an account and disconnects the associated clients.`,
324
+			helpShort: `$bSUSPEND$b disables an account and disconnects the clients`,
325
+			minParams: 1,
326
+			capabs:    []string{"accreg"},
327
+		},
328
+		"unsuspend": {
329
+			handler: nsUnsuspendHandler,
330
+			help: `Syntax: $bUNSUSPEND <nickname>$b
331
+
332
+UNSUSPEND reverses a previous SUSPEND, restoring access to the account.`,
333
+			helpShort: `$bUNSUSPEND$b restores access to a suspended account`,
334
+			minParams: 1,
335
+			capabs:    []string{"accreg"},
336
+		},
319 337
 	}
320 338
 )
321 339
 
@@ -1177,3 +1195,27 @@ func nsCertHandler(server *Server, client *Client, command string, params []stri
1177 1195
 		nsNotice(rb, client.t("An error occurred"))
1178 1196
 	}
1179 1197
 }
1198
+
1199
+func nsSuspendHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
1200
+	err := server.accounts.Suspend(params[0])
1201
+	switch err {
1202
+	case nil:
1203
+		nsNotice(rb, fmt.Sprintf(client.t("Successfully suspended account %s"), params[0]))
1204
+	case errAccountDoesNotExist:
1205
+		nsNotice(rb, client.t("No such account"))
1206
+	default:
1207
+		nsNotice(rb, client.t("An error occurred"))
1208
+	}
1209
+}
1210
+
1211
+func nsUnsuspendHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
1212
+	err := server.accounts.Unsuspend(params[0])
1213
+	switch err {
1214
+	case nil:
1215
+		nsNotice(rb, fmt.Sprintf(client.t("Successfully un-suspended account %s"), params[0]))
1216
+	case errAccountDoesNotExist:
1217
+		nsNotice(rb, client.t("No such account"))
1218
+	default:
1219
+		nsNotice(rb, client.t("An error occurred"))
1220
+	}
1221
+}

Loading…
Cancel
Save