initial UBAN implementation

This commit is contained in:
Shivaram Lingamneni 2021-01-19 08:49:45 -05:00
parent 64bc363cf1
commit bb5276553d
14 changed files with 598 additions and 44 deletions

View File

@ -175,6 +175,17 @@ SET modifies a channel's settings. The following settings are available:`,
enabled: chanregEnabled, enabled: chanregEnabled,
minParams: 3, minParams: 3,
}, },
"howtoban": {
handler: csHowToBanHandler,
helpShort: `$bHOWTOBAN$b suggests the best available way of banning a user`,
help: `Syntax: $bHOWTOBAN #channel <nick>
The best way to ban a user from a channel will depend on how they are
connected to the server. $bHOWTOBAN$b suggests a ban command that will
(ideally) prevent the user from returning to the channel.`,
enabled: chanregEnabled,
minParams: 2,
},
} }
) )
@ -809,3 +820,83 @@ func csSetHandler(service *ircService, server *Server, client *Client, command s
service.Notice(rb, client.t("An error occurred")) service.Notice(rb, client.t("An error occurred"))
} }
} }
func csHowToBanHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
success := false
defer func() {
if success {
service.Notice(rb, client.t("Note that if the user is currently in the channel, you must /KICK them after you ban them"))
}
}()
chname, nick := params[0], params[1]
channel := server.channels.Get(chname)
if channel == nil {
service.Notice(rb, client.t("No such channel"))
return
}
if !channel.ClientIsAtLeast(client, modes.Operator) || client.HasRoleCapabs("samode") {
service.Notice(rb, client.t("Insufficient privileges"))
return
}
var details WhoWas
target := server.clients.Get(nick)
if target == nil {
whowasList := server.whoWas.Find(nick, 1)
if len(whowasList) == 0 {
service.Notice(rb, client.t("No such nick"))
return
}
service.Notice(rb, fmt.Sprintf(client.t("Warning: %s is not currently connected to the server. Using WHOWAS data, which may be inaccurate:"), nick))
details = whowasList[0]
} else {
details = target.Details().WhoWas
}
if details.account != "" {
if channel.getAmode(details.account) != modes.Mode(0) {
service.Notice(rb, fmt.Sprintf(client.t("Warning: account %s currently has a persistent channel privilege granted with CS AMODE. If this mode is not removed, bans will not be respected"), details.accountName))
return
} else if details.account == channel.Founder() {
service.Notice(rb, fmt.Sprintf(client.t("Warning: account %s is the channel founder and cannot be banned"), details.accountName))
return
}
}
config := server.Config()
if !config.Server.Cloaks.EnabledForAlwaysOn {
service.Notice(rb, client.t("Warning: server.ip-cloaking.enabled-for-always-on is disabled. This reduces the precision of channel bans."))
}
if details.account != "" {
if config.Accounts.NickReservation.ForceNickEqualsAccount || target.AlwaysOn() {
service.Notice(rb, fmt.Sprintf(client.t("User %[1]s is authenticated and can be banned by nickname: /MODE %[2]s +b %[3]s!*@*"), details.nick, channel.Name(), details.nick))
success = true
return
}
}
ban := fmt.Sprintf("*!*@%s", strings.ToLower(details.hostname))
banRe, err := utils.CompileGlob(ban, false)
if err != nil {
server.logger.Error("internal", "couldn't compile ban regex", ban, err.Error())
service.Notice(rb, "An error occurred")
return
}
var collateralDamage []string
for _, mcl := range channel.Members() {
if mcl != target && banRe.MatchString(mcl.NickMaskCasefolded()) {
collateralDamage = append(collateralDamage, mcl.Nick())
}
}
service.Notice(rb, fmt.Sprintf(client.t("User %[1]s can be banned by hostname: /MODE %[2]s +b %[3]s"), details.nick, channel.Name(), ban))
success = true
if len(collateralDamage) != 0 {
service.Notice(rb, fmt.Sprintf(client.t("Warning: this ban will affect %d other users:"), len(collateralDamage)))
for _, line := range utils.BuildTokenLines(400, collateralDamage, " ") {
service.Notice(rb, line)
}
}
}

View File

@ -292,6 +292,9 @@ type WhoWas struct {
username string username string
hostname string hostname string
realname string realname string
// technically not required for WHOWAS:
account string
accountName string
} }
// ClientDetails is a standard set of details about a client // ClientDetails is a standard set of details about a client
@ -300,8 +303,6 @@ type ClientDetails struct {
nickMask string nickMask string
nickMaskCasefolded string nickMaskCasefolded string
account string
accountName string
} }
// RunClient sets up a new client and runs its goroutine. // RunClient sets up a new client and runs its goroutine.

View File

@ -319,6 +319,11 @@ func init() {
handler: topicHandler, handler: topicHandler,
minParams: 1, minParams: 1,
}, },
"UBAN": {
handler: ubanHandler,
minParams: 1,
capabs: []string{"ban"},
},
"UNDLINE": { "UNDLINE": {
handler: unDLineHandler, handler: unDLineHandler,
minParams: 1, minParams: 1,

View File

@ -209,6 +209,38 @@ func (cl *Limiter) RemoveClient(addr flatip.IP) {
cl.limiter[addrString] = count cl.limiter[addrString] = count
} }
type LimiterStatus struct {
Exempt bool
Count int
MaxCount int
Throttle int
MaxPerWindow int
ThrottleDuration time.Duration
}
func (cl *Limiter) Status(addr flatip.IP) (status LimiterStatus) {
cl.Lock()
defer cl.Unlock()
if flatip.IPInNets(addr, cl.config.exemptedNets) {
status.Exempt = true
return
}
status.ThrottleDuration = cl.config.Window
addrString, maxConcurrent, maxPerWindow := cl.addrToKey(addr)
status.MaxCount = maxConcurrent
status.MaxPerWindow = maxPerWindow
status.Count = cl.limiter[addrString]
status.Throttle = cl.throttler[addrString].Count
return
}
// ResetThrottle resets the throttle count for an IP // ResetThrottle resets the throttle count for an IP
func (cl *Limiter) ResetThrottle(addr flatip.IP) { func (cl *Limiter) ResetThrottle(addr flatip.IP) {
cl.Lock() cl.Lock()

View File

@ -6,13 +6,11 @@ package irc
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/oragono/oragono/irc/flatip" "github.com/oragono/oragono/irc/flatip"
"github.com/oragono/oragono/irc/utils"
"github.com/tidwall/buntdb" "github.com/tidwall/buntdb"
) )
@ -48,7 +46,11 @@ func (info IPBanInfo) TimeLeft() string {
// BanMessage returns the ban message. // BanMessage returns the ban message.
func (info IPBanInfo) BanMessage(message string) string { func (info IPBanInfo) BanMessage(message string) string {
message = fmt.Sprintf(message, info.Reason) reason := info.Reason
if reason == "" {
reason = "No reason given"
}
message = fmt.Sprintf(message, reason)
if info.Duration != 0 { if info.Duration != 0 {
message += fmt.Sprintf(" [%s]", info.TimeLeft()) message += fmt.Sprintf(" [%s]", info.TimeLeft())
} }
@ -86,14 +88,14 @@ func (dm *DLineManager) AllBans() map[string]IPBanInfo {
defer dm.RUnlock() defer dm.RUnlock()
for key, info := range dm.networks { for key, info := range dm.networks {
allb[key.String()] = info allb[key.HumanReadableString()] = info
} }
return allb return allb
} }
// AddNetwork adds a network to the blocked list. // AddNetwork adds a network to the blocked list.
func (dm *DLineManager) AddNetwork(network net.IPNet, duration time.Duration, reason, operReason, operName string) error { func (dm *DLineManager) AddNetwork(network flatip.IPNet, duration time.Duration, reason, operReason, operName string) error {
dm.persistenceMutex.Lock() dm.persistenceMutex.Lock()
defer dm.persistenceMutex.Unlock() defer dm.persistenceMutex.Unlock()
@ -110,8 +112,7 @@ func (dm *DLineManager) AddNetwork(network net.IPNet, duration time.Duration, re
return dm.persistDline(id, info) return dm.persistDline(id, info)
} }
func (dm *DLineManager) addNetworkInternal(network net.IPNet, info IPBanInfo) (id flatip.IPNet) { func (dm *DLineManager) addNetworkInternal(flatnet flatip.IPNet, info IPBanInfo) (id flatip.IPNet) {
flatnet := flatip.FromNetIPNet(network)
id = flatnet id = flatnet
var timeLeft time.Duration var timeLeft time.Duration
@ -193,11 +194,11 @@ func (dm *DLineManager) unpersistDline(id flatip.IPNet) error {
} }
// RemoveNetwork removes a network from the blocked list. // RemoveNetwork removes a network from the blocked list.
func (dm *DLineManager) RemoveNetwork(network net.IPNet) error { func (dm *DLineManager) RemoveNetwork(network flatip.IPNet) error {
dm.persistenceMutex.Lock() dm.persistenceMutex.Lock()
defer dm.persistenceMutex.Unlock() defer dm.persistenceMutex.Unlock()
id := flatip.FromNetIPNet(network) id := network
present := func() bool { present := func() bool {
dm.Lock() dm.Lock()
@ -215,22 +216,8 @@ func (dm *DLineManager) RemoveNetwork(network net.IPNet) error {
return dm.unpersistDline(id) return dm.unpersistDline(id)
} }
// AddIP adds an IP address to the blocked list.
func (dm *DLineManager) AddIP(addr net.IP, duration time.Duration, reason, operReason, operName string) error {
return dm.AddNetwork(utils.NormalizeIPToNet(addr), duration, reason, operReason, operName)
}
// RemoveIP removes an IP address from the blocked list.
func (dm *DLineManager) RemoveIP(addr net.IP) error {
return dm.RemoveNetwork(utils.NormalizeIPToNet(addr))
}
// CheckIP returns whether or not an IP address was banned, and how long it is banned for. // CheckIP returns whether or not an IP address was banned, and how long it is banned for.
func (dm *DLineManager) CheckIP(addr flatip.IP) (isBanned bool, info IPBanInfo) { func (dm *DLineManager) CheckIP(addr flatip.IP) (isBanned bool, info IPBanInfo) {
if addr.IsLoopback() {
return // #671
}
dm.RLock() dm.RLock()
defer dm.RUnlock() defer dm.RUnlock()
@ -257,7 +244,7 @@ func (dm *DLineManager) loadFromDatastore() {
key = strings.TrimPrefix(key, dlinePrefix) key = strings.TrimPrefix(key, dlinePrefix)
// load addr/net // load addr/net
hostNet, err := utils.NormalizedNetFromString(key) hostNet, err := flatip.ParseToNormalizedNet(key)
if err != nil { if err != nil {
dm.server.logger.Error("internal", "bad dline cidr", err.Error()) dm.server.logger.Error("internal", "bad dline cidr", err.Error())
return true return true

View File

@ -183,6 +183,16 @@ func (cidr IPNet) String() string {
return ipnet.String() return ipnet.String()
} }
// HumanReadableString returns a string representation of an IPNet;
// if the network contains only a single IP address, it returns
// a representation of that address.
func (cidr IPNet) HumanReadableString() string {
if cidr.PrefixLen == 128 {
return cidr.IP.String()
}
return cidr.String()
}
// IsZero tests whether ipnet is the zero value of an IPNet, 0::0/0. // IsZero tests whether ipnet is the zero value of an IPNet, 0::0/0.
// Although this is a valid subnet, it can still be used as a sentinel // Although this is a valid subnet, it can still be used as a sentinel
// value in some contexts. // value in some contexts.

View File

@ -314,6 +314,12 @@ func (client *Client) setCloakedHostname(cloak string) {
client.updateNickMaskNoMutex() client.updateNickMaskNoMutex()
} }
func (client *Client) CloakedHostname() string {
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
return client.cloakedHostname
}
func (client *Client) historyCutoff() (cutoff time.Time) { func (client *Client) historyCutoff() (cutoff time.Time) {
client.stateMutex.Lock() client.stateMutex.Lock()
if client.account != "" { if client.account != "" {
@ -553,3 +559,9 @@ func (channel *Channel) Ctime() (ctime time.Time) {
channel.stateMutex.RUnlock() channel.stateMutex.RUnlock()
return return
} }
func (channel *Channel) getAmode(cfaccount string) (result modes.Mode) {
channel.stateMutex.RLock()
defer channel.stateMutex.RUnlock()
return channel.accountToUMode[cfaccount]
}

View File

@ -906,7 +906,7 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
operName = server.name operName = server.name
} }
err = server.dlines.AddNetwork(hostNet, duration, reason, operReason, operName) err = server.dlines.AddNetwork(flatip.FromNetIPNet(hostNet), duration, reason, operReason, operName)
if err != nil { if err != nil {
rb.Notice(fmt.Sprintf(client.t("Could not successfully save new D-LINE: %s"), err.Error())) rb.Notice(fmt.Sprintf(client.t("Could not successfully save new D-LINE: %s"), err.Error()))
@ -2833,13 +2833,8 @@ func unDLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
// get host // get host
hostString := msg.Params[0] hostString := msg.Params[0]
// TODO(#1447) consolidate this into the "unban" command
if flatip, ipErr := flatip.ParseIP(hostString); ipErr == nil {
server.connectionLimiter.ResetThrottle(flatip)
}
// check host // check host
hostNet, err := utils.NormalizedNetFromString(hostString) hostNet, err := flatip.ParseToNormalizedNet(hostString)
if err != nil { if err != nil {
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, client.t("Could not parse IP address or CIDR network")) rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, client.t("Could not parse IP address or CIDR network"))
@ -2853,7 +2848,7 @@ func unDLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
return false return false
} }
hostString = utils.NetToNormalizedString(hostNet) hostString = hostNet.String()
rb.Notice(fmt.Sprintf(client.t("Removed D-Line for %s"), hostString)) rb.Notice(fmt.Sprintf(client.t("Removed D-Line for %s"), hostString))
server.snomasks.Send(sno.LocalXline, fmt.Sprintf(ircfmt.Unescape("%s$r removed D-Line for %s"), client.nick, hostString)) server.snomasks.Send(sno.LocalXline, fmt.Sprintf(ircfmt.Unescape("%s$r removed D-Line for %s"), client.nick, hostString))
return false return false

View File

@ -520,6 +520,19 @@ Shows the time of the current, or the given, server.`,
If [topic] is given, sets the topic in the channel to that. If [topic] is not If [topic] is given, sets the topic in the channel to that. If [topic] is not
given, views the current topic on the channel.`, given, views the current topic on the channel.`,
},
"uban": {
text: `UBAN <subcommand> [arguments]
Oragono's "unified ban" system. Accepts the following subcommands:
1. UBAN ADD <target> [DURATION <duration>] [REASON...]
2. UBAN DEL <target>
3. UBAN LIST
4. UBAN INFO <target>
<target> may be an IP, a CIDR, a nickmask with wildcards, or the name of an
account to suspend.`,
}, },
"undline": { "undline": {
oper: true, oper: true,

View File

@ -1,3 +1,6 @@
// Copyright (c) 2020 Shivaram Lingamneni
// released under the MIT license
package irc package irc
import ( import (

View File

@ -189,6 +189,17 @@ func (km *KLineManager) RemoveMask(mask string) error {
return km.unpersistKLine(mask) return km.unpersistKLine(mask)
} }
func (km *KLineManager) ContainsMask(mask string) (isBanned bool, info IPBanInfo) {
km.RLock()
defer km.RUnlock()
klineInfo, isBanned := km.entries[mask]
if isBanned {
info = klineInfo.Info
}
return
}
// CheckMasks returns whether or not the hostmask(s) are banned, and how long they are banned for. // CheckMasks returns whether or not the hostmask(s) are banned, and how long they are banned for.
func (km *KLineManager) CheckMasks(masks ...string) (isBanned bool, info IPBanInfo) { func (km *KLineManager) CheckMasks(masks ...string) (isBanned bool, info IPBanInfo) {
km.RLock() km.RLock()

View File

@ -1361,11 +1361,16 @@ func (a ByCreationTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByCreationTime) Less(i, j int) bool { return a[i].TimeCreated.After(a[j].TimeCreated) } func (a ByCreationTime) Less(i, j int) bool { return a[i].TimeCreated.After(a[j].TimeCreated) }
func nsSuspendListHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { func nsSuspendListHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
suspensions := server.accounts.ListSuspended() listAccountSuspensions(client, rb, service.prefix)
}
func listAccountSuspensions(client *Client, rb *ResponseBuffer, source string) {
suspensions := client.server.accounts.ListSuspended()
sort.Sort(ByCreationTime(suspensions)) sort.Sort(ByCreationTime(suspensions))
service.Notice(rb, fmt.Sprintf(client.t("There are %d active suspensions."), len(suspensions))) nick := client.Nick()
rb.Add(nil, source, "NOTICE", nick, fmt.Sprintf(client.t("There are %d active account suspensions."), len(suspensions)))
for _, suspension := range suspensions { for _, suspension := range suspensions {
service.Notice(rb, suspensionToString(client, suspension)) rb.Add(nil, source, "NOTICE", nick, suspensionToString(client, suspension))
} }
} }

View File

@ -162,8 +162,14 @@ func (server *Server) Run() {
} }
func (server *Server) checkBans(config *Config, ipaddr net.IP, checkScripts bool) (banned bool, requireSASL bool, message string) { func (server *Server) checkBans(config *Config, ipaddr net.IP, checkScripts bool) (banned bool, requireSASL bool, message string) {
// #671: do not enforce bans against loopback, as a failsafe
// note that this function is not used for Tor connections (checkTorLimits is used instead)
if ipaddr.IsLoopback() {
return
}
if server.Defcon() == 1 { if server.Defcon() == 1 {
if !(ipaddr.IsLoopback() || utils.IPInNets(ipaddr, server.Config().Server.secureNets)) { if !utils.IPInNets(ipaddr, server.Config().Server.secureNets) {
return true, false, "New connections to this server are temporarily restricted" return true, false, "New connections to this server are temporarily restricted"
} }
} }
@ -198,7 +204,7 @@ func (server *Server) checkBans(config *Config, ipaddr net.IP, checkScripts bool
} }
// TODO: currently no way to cache results other than IPBanned // TODO: currently no way to cache results other than IPBanned
if output.Result == IPBanned && output.CacheSeconds != 0 { if output.Result == IPBanned && output.CacheSeconds != 0 {
network, err := utils.NormalizedNetFromString(output.CacheNet) network, err := flatip.ParseToNormalizedNet(output.CacheNet)
if err != nil { if err != nil {
server.logger.Error("internal", "invalid dline net from IP ban script", ipaddr.String(), output.CacheNet) server.logger.Error("internal", "invalid dline net from IP ban script", ipaddr.String(), output.CacheNet)
} else { } else {
@ -339,12 +345,14 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) {
// count new user in statistics (before checking KLINEs, see #1303) // count new user in statistics (before checking KLINEs, see #1303)
server.stats.Register(c.HasMode(modes.Invisible)) server.stats.Register(c.HasMode(modes.Invisible))
// check KLINEs // check KLINEs (#671: ignore KLINEs for loopback connections)
if !session.IP().IsLoopback() || session.isTor {
isBanned, info := server.klines.CheckMasks(c.AllNickmasks()...) isBanned, info := server.klines.CheckMasks(c.AllNickmasks()...)
if isBanned { if isBanned {
c.Quit(info.BanMessage(c.t("You are banned from this server (%s)")), nil) c.Quit(info.BanMessage(c.t("You are banned from this server (%s)")), nil)
return true return true
} }
}
server.playRegistrationBurst(session) server.playRegistrationBurst(session)
return false return false

381
irc/uban.go Normal file
View File

@ -0,0 +1,381 @@
// Copyright (c) 2021 Shivaram Lingamneni
// released under the MIT license
package irc
import (
"fmt"
"regexp"
"strings"
"time"
"github.com/goshuirc/irc-go/ircmsg"
"github.com/oragono/oragono/irc/custime"
"github.com/oragono/oragono/irc/flatip"
"github.com/oragono/oragono/irc/utils"
)
func consumeDuration(params []string, rb *ResponseBuffer) (duration time.Duration, remainingParams []string, err error) {
remainingParams = params
if 2 <= len(remainingParams) && strings.ToLower(remainingParams[0]) == "duration" {
duration, err = custime.ParseDuration(remainingParams[1])
if err != nil {
rb.Notice(rb.session.client.t("Invalid time duration for NS SUSPEND"))
return
}
remainingParams = remainingParams[2:]
}
return
}
// a UBAN target is one of these syntactically unambiguous entities:
// an IP, a CIDR, a NUH mask, or an account name
type ubanType uint
const (
ubanCIDR ubanType = iota
ubanNickmask
ubanNick
)
// tagged union, i guess
type ubanTarget struct {
banType ubanType
cidr flatip.IPNet
matcher *regexp.Regexp
nickOrMask string
}
func parseUbanTarget(param string) (target ubanTarget, err error) {
if utils.SafeErrorParam(param) == "*" {
err = errInvalidParams
return
}
ipnet, ipErr := flatip.ParseToNormalizedNet(param)
if ipErr == nil {
target.banType = ubanCIDR
target.cidr = ipnet
return
}
if strings.IndexByte(param, '!') != -1 || strings.IndexByte(param, '@') != -1 {
canonicalized, cErr := CanonicalizeMaskWildcard(param)
if cErr != nil {
err = errInvalidParams
return
}
re, reErr := utils.CompileGlob(canonicalized, false)
if reErr != nil {
err = errInvalidParams
return
}
target.banType = ubanNickmask
target.nickOrMask = canonicalized
target.matcher = re
return
}
if _, cErr := CasefoldName(param); cErr == nil {
target.banType = ubanNick
target.nickOrMask = param
return
}
err = errInvalidParams
return
}
// UBAN <subcommand> [target] [DURATION <duration>] [reason...]
func ubanHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
subcommand := strings.ToLower(msg.Params[0])
params := msg.Params[1:]
var target ubanTarget
if subcommand != "list" {
if len(msg.Params) == 1 {
rb.Add(nil, client.server.name, "FAIL", "UBAN", "INVALID_PARAMS", client.t("Not enough parameters"))
return false
}
var parseErr error
target, parseErr = parseUbanTarget(params[0])
if parseErr != nil {
rb.Add(nil, client.server.name, "FAIL", "UBAN", "INVALID_PARAMS", client.t("Couldn't parse ban target"))
return false
}
params = params[1:]
}
switch subcommand {
case "add":
return ubanAddHandler(client, target, params, rb)
case "del", "remove", "rm":
return ubanDelHandler(client, target, params, rb)
case "list":
return ubanListHandler(client, params, rb)
case "info":
return ubanInfoHandler(client, target, params, rb)
default:
rb.Add(nil, server.name, "FAIL", "UBAN", "UNKNOWN_COMMAND", client.t("Unknown command"))
return false
}
}
func sessionsForCIDR(server *Server, cidr flatip.IPNet, exclude *Session) (sessions []*Session, nicks []string) {
for _, client := range server.clients.AllClients() {
for _, session := range client.Sessions() {
seen := false
if session != exclude && cidr.Contains(flatip.FromNetIP(session.IP())) {
sessions = append(sessions, session)
if !seen {
seen = true
nicks = append(nicks, session.client.Nick())
}
}
}
}
return
}
func ubanAddHandler(client *Client, target ubanTarget, params []string, rb *ResponseBuffer) bool {
duration, params, err := consumeDuration(params, rb)
if err != nil {
return false
}
operReason := strings.Join(params, " ")
switch target.banType {
case ubanCIDR:
ubanAddCIDR(client, target, duration, operReason, rb)
case ubanNickmask:
ubanAddNickmask(client, target, duration, operReason, rb)
case ubanNick:
account := target.nickOrMask
err := client.server.accounts.Suspend(account, duration, client.Oper().Name, "UBAN")
switch err {
case nil:
rb.Notice(fmt.Sprintf(client.t("Successfully suspended account %s"), account))
case errAccountDoesNotExist:
rb.Notice(client.t("No such account"))
default:
rb.Notice(client.t("An error occurred"))
}
}
return false
}
func ubanAddCIDR(client *Client, target ubanTarget, duration time.Duration, operReason string, rb *ResponseBuffer) {
err := client.server.dlines.AddNetwork(target.cidr, duration, "", operReason, client.Oper().Name)
if err == nil {
rb.Notice(fmt.Sprintf(client.t("Successfully added UBAN for %s"), target.cidr.HumanReadableString()))
} else {
client.server.logger.Error("internal", "ubanAddCIDR failed", err.Error())
rb.Notice(client.t("An error occurred"))
return
}
sessions, nicks := sessionsForCIDR(client.server, target.cidr, rb.session)
for _, session := range sessions {
session.client.Quit("You have been banned from this server", session)
session.client.destroy(session)
}
if len(sessions) != 0 {
rb.Notice(fmt.Sprintf(client.t("Killed %[1]d active client(s) from %[2]s, associated with %[3]d nickname(s):"), len(sessions), target.cidr.String(), len(nicks)))
for _, line := range utils.BuildTokenLines(400, nicks, " ") {
rb.Notice(line)
}
}
}
func ubanAddNickmask(client *Client, target ubanTarget, duration time.Duration, operReason string, rb *ResponseBuffer) {
err := client.server.klines.AddMask(target.nickOrMask, duration, "", operReason, client.Oper().Name)
if err == nil {
rb.Notice(fmt.Sprintf(client.t("Successfully added UBAN for %s"), target.nickOrMask))
} else {
client.server.logger.Error("internal", "ubanAddNickmask failed", err.Error())
rb.Notice(client.t("An error occurred"))
return
}
var killed []string
var alwaysOn []string
for _, mcl := range client.server.clients.AllClients() {
if mcl != client && target.matcher.MatchString(client.NickMaskCasefolded()) {
if !mcl.AlwaysOn() {
killed = append(killed, mcl.Nick())
mcl.destroy(nil)
} else {
alwaysOn = append(alwaysOn, mcl.Nick())
}
}
}
if len(killed) != 0 {
rb.Notice(fmt.Sprintf(client.t("Killed %d clients:"), len(killed)))
for _, line := range utils.BuildTokenLines(400, killed, " ") {
rb.Notice(line)
}
}
if len(alwaysOn) != 0 {
rb.Notice(fmt.Sprintf(client.t("Warning: %d clients matched this rule, but were not killed due to being always-on:"), len(alwaysOn)))
for _, line := range utils.BuildTokenLines(400, alwaysOn, " ") {
rb.Notice(line)
}
rb.Notice(client.t("You can suspend their accounts instead; try /UBAN ADD <nickname>"))
}
}
func ubanDelHandler(client *Client, target ubanTarget, params []string, rb *ResponseBuffer) bool {
var err error
var targetString string
switch target.banType {
case ubanCIDR:
if target.cidr.PrefixLen == 128 {
client.server.connectionLimiter.ResetThrottle(target.cidr.IP)
rb.Notice(fmt.Sprintf(client.t("Reset throttle for IP: %s"), target.cidr.IP.String()))
}
targetString = target.cidr.HumanReadableString()
err = client.server.dlines.RemoveNetwork(target.cidr)
case ubanNickmask:
targetString = target.nickOrMask
err = client.server.klines.RemoveMask(target.nickOrMask)
case ubanNick:
targetString = target.nickOrMask
err = client.server.accounts.Unsuspend(target.nickOrMask)
}
if err == nil {
rb.Notice(fmt.Sprintf(client.t("Successfully removed ban on %s"), targetString))
} else {
rb.Notice(fmt.Sprintf(client.t("Could not remove ban: %v"), err))
}
return false
}
func ubanListHandler(client *Client, params []string, rb *ResponseBuffer) bool {
allDlines := client.server.dlines.AllBans()
rb.Notice(fmt.Sprintf(client.t("There are %d active IP/network ban(s) (DLINEs)"), len(allDlines)))
for key, info := range allDlines {
rb.Notice(formatBanForListing(client, key, info))
}
rb.Notice(client.t("Some IPs may also be prevented from connecting by the connection limiter and/or throttler"))
allKlines := client.server.klines.AllBans()
rb.Notice(fmt.Sprintf(client.t("There are %d active ban(s) on nick-user-host masks (KLINEs)"), len(allKlines)))
for key, info := range allKlines {
rb.Notice(formatBanForListing(client, key, info))
}
listAccountSuspensions(client, rb, client.server.name)
return false
}
func ubanInfoHandler(client *Client, target ubanTarget, params []string, rb *ResponseBuffer) bool {
switch target.banType {
case ubanCIDR:
ubanInfoCIDR(client, target, rb)
case ubanNickmask:
ubanInfoNickmask(client, target, rb)
case ubanNick:
ubanInfoNick(client, target, rb)
}
return false
}
func ubanInfoCIDR(client *Client, target ubanTarget, rb *ResponseBuffer) {
if target.cidr.PrefixLen == 128 {
status := client.server.connectionLimiter.Status(target.cidr.IP)
str := target.cidr.IP.String()
if status.Exempt {
rb.Notice(fmt.Sprintf(client.t("IP %s is exempt from connection limits"), str))
} else {
rb.Notice(fmt.Sprintf(client.t("IP %[1]s has %[2]d active connections out of a maximum of %[3]d"), str, status.Count, status.MaxCount))
rb.Notice(fmt.Sprintf(client.t("IP %[1]s has had %[2]d connection attempts in the past %[3]v, out of a maximum of %[4]d"), str, status.Throttle, status.ThrottleDuration, status.MaxPerWindow))
}
}
str := target.cidr.HumanReadableString()
isBanned, banInfo := client.server.dlines.CheckIP(target.cidr.IP)
if isBanned {
rb.Notice(formatBanForListing(client, str, banInfo))
} else {
rb.Notice(fmt.Sprintf(client.t("There is no active IP ban against %s"), str))
}
sessions, nicks := sessionsForCIDR(client.server, target.cidr, nil)
if len(sessions) != 0 {
rb.Notice(fmt.Sprintf(client.t("There are %[1]d active client(s) from %[2]s, associated with %[3]d nickname(s):"), len(sessions), target.cidr.String(), len(nicks)))
for _, line := range utils.BuildTokenLines(400, nicks, " ") {
rb.Notice(line)
}
}
}
func ubanInfoNickmask(client *Client, target ubanTarget, rb *ResponseBuffer) {
isBanned, info := client.server.klines.ContainsMask(target.nickOrMask)
if isBanned {
rb.Notice(formatBanForListing(client, target.nickOrMask, info))
} else {
rb.Notice(fmt.Sprintf(client.t("No ban exists for %[1]s"), target.nickOrMask))
}
affectedCount := 0
alwaysOnCount := 0
for _, mcl := range client.server.clients.AllClients() {
matches := false
for _, mask := range mcl.AllNickmasks() {
if target.matcher.MatchString(mask) {
matches = true
break
}
}
if matches {
if mcl.AlwaysOn() {
alwaysOnCount++
} else {
affectedCount++
}
}
}
rb.Notice(fmt.Sprintf(client.t("Adding this mask would affect %[1]d clients (an additional %[2]d clients are exempt due to always-on)"), affectedCount, alwaysOnCount))
}
func ubanInfoNick(client *Client, target ubanTarget, rb *ResponseBuffer) {
mcl := client.server.clients.Get(target.nickOrMask)
if mcl != nil {
details := mcl.Details()
if details.account == "" {
rb.Notice(fmt.Sprintf(client.t("Client %[1]s is unauthenticated and connected from %[2]s"), details.nick, client.IP().String()))
} else {
rb.Notice(fmt.Sprintf(client.t("Client %[1]s is logged into account %[2]s and has %[3]d active clients (see /NICKSERV CLIENTS LIST %[4]s for more info"), details.nick, details.accountName, len(mcl.Sessions()), details.nick))
ip := client.IP()
if !ip.IsLoopback() {
rb.Notice(fmt.Sprintf(client.t("Client %[1]s is associated with IP %[2]s; you can ban this IP with /UBAN ADD"), details.nick, ip.String()))
}
}
} else {
rb.Notice(fmt.Sprintf(client.t("No client is currently using that nickname")))
}
account, err := client.server.accounts.LoadAccount(target.nickOrMask)
if err != nil {
if err == errAccountDoesNotExist {
rb.Notice(fmt.Sprintf(client.t("There is no account registered for %s"), target.nickOrMask))
} else {
rb.Notice(fmt.Sprintf(client.t("Couldn't load account: %v"), err.Error()))
}
return
}
if account.Verified {
if account.Suspended == nil {
rb.Notice(fmt.Sprintf(client.t("Account %[1]s is in good standing; see /NICKSERV INFO %[2]s for more details"), target.nickOrMask, target.nickOrMask))
} else {
rb.Notice(fmt.Sprintf(client.t("Account %[1]s has been suspended: %[2]s"), target.nickOrMask, suspensionToString(client, *account.Suspended)))
}
} else {
rb.Notice(fmt.Sprintf(client.t("Account %[1]s was created, but has not been verified"), target.nickOrMask))
}
}