diff --git a/default.yaml b/default.yaml index 84911b9b..227fd0e6 100644 --- a/default.yaml +++ b/default.yaml @@ -584,9 +584,8 @@ oper-classes: # capability names capabilities: - - "local_kill" - - "local_ban" - - "local_unban" + - "kill" + - "ban" - "nofakelag" - "roleplay" - "relaymsg" diff --git a/docs/MANUAL.md b/docs/MANUAL.md index 26134b36..badbf075 100644 --- a/docs/MANUAL.md +++ b/docs/MANUAL.md @@ -930,9 +930,8 @@ oper-classes: # capability names capabilities: - - "local_kill" - - "local_ban" - - "local_unban" + - "kill" + - "ban" - "nofakelag" # ircd operators diff --git a/irc/chanserv.go b/irc/chanserv.go index bcd8b3f9..70ea1231 100644 --- a/irc/chanserv.go +++ b/irc/chanserv.go @@ -175,6 +175,17 @@ SET modifies a channel's settings. The following settings are available:`, enabled: chanregEnabled, minParams: 3, }, + "howtoban": { + handler: csHowToBanHandler, + helpShort: `$bHOWTOBAN$b suggests the best available way of banning a user`, + help: `Syntax: $bHOWTOBAN #channel + +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, + }, } ) @@ -502,10 +513,13 @@ func csTransferHandler(service *ircService, server *Server, client *Client, comm chname = regInfo.Name account := client.Account() isFounder := account != "" && account == regInfo.Founder - hasPrivs := client.HasRoleCapabs("chanreg") - if !(isFounder || hasPrivs) { - service.Notice(rb, client.t("Insufficient privileges")) - return + var oper *Oper + if !isFounder { + oper = client.Oper() + if !oper.HasRoleCapab("chanreg") { + service.Notice(rb, client.t("Insufficient privileges")) + return + } } target := params[1] targetAccount, err := server.accounts.LoadAccount(params[1]) @@ -522,7 +536,12 @@ func csTransferHandler(service *ircService, server *Server, client *Client, comm return } } - status, err := channel.Transfer(client, target, hasPrivs) + if !isFounder { + message := fmt.Sprintf("Operator %s ran CS TRANSFER on %s to account %s", oper.Name, chname, target) + server.snomasks.Send(sno.LocalOpers, message) + server.logger.Info("opers", message) + } + status, err := channel.Transfer(client, target, oper != nil) if err == nil { switch status { case channelTransferComplete: @@ -801,3 +820,83 @@ func csSetHandler(service *ircService, server *Server, client *Client, command s 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) + } + } +} diff --git a/irc/client.go b/irc/client.go index 0f8b2895..67ceb7f6 100644 --- a/irc/client.go +++ b/irc/client.go @@ -292,6 +292,9 @@ type WhoWas struct { username string hostname string realname string + // technically not required for WHOWAS: + account string + accountName string } // ClientDetails is a standard set of details about a client @@ -300,8 +303,6 @@ type ClientDetails struct { nickMask string nickMaskCasefolded string - account string - accountName string } // RunClient sets up a new client and runs its goroutine. diff --git a/irc/commands.go b/irc/commands.go index cb81fd2f..9d0ae58d 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -170,7 +170,7 @@ func init() { handler: killHandler, minParams: 1, oper: true, - capabs: []string{"local_kill"}, //TODO(dan): when we have S2S, this will be checked in the command handler itself + capabs: []string{"kill"}, }, "KLINE": { handler: klineHandler, @@ -319,6 +319,11 @@ func init() { handler: topicHandler, minParams: 1, }, + "UBAN": { + handler: ubanHandler, + minParams: 1, + capabs: []string{"ban"}, + }, "UNDLINE": { handler: unDLineHandler, minParams: 1, diff --git a/irc/config.go b/irc/config.go index de1a7ca8..159977e5 100644 --- a/irc/config.go +++ b/irc/config.go @@ -649,7 +649,7 @@ type OperClass struct { // OperatorClasses returns a map of assembled operator classes from the given config. func (conf *Config) OperatorClasses() (map[string]*OperClass, error) { fixupCapability := func(capab string) string { - return strings.TrimPrefix(capab, "oper:") // #868 + return strings.TrimPrefix(strings.TrimPrefix(capab, "oper:"), "local_") // #868, #1442 } ocs := make(map[string]*OperClass) @@ -733,6 +733,10 @@ type Oper struct { Modes []modes.ModeChange } +func (oper *Oper) HasRoleCapab(capab string) bool { + return oper != nil && oper.Class.Capabilities.Has(capab) +} + // Operators returns a map of operator configs from the given OperClass and config. func (conf *Config) Operators(oc map[string]*OperClass) (map[string]*Oper, error) { operators := make(map[string]*Oper) diff --git a/irc/connection_limits/limiter.go b/irc/connection_limits/limiter.go index 867cb0f8..a3fcec90 100644 --- a/irc/connection_limits/limiter.go +++ b/irc/connection_limits/limiter.go @@ -209,6 +209,38 @@ func (cl *Limiter) RemoveClient(addr flatip.IP) { 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 func (cl *Limiter) ResetThrottle(addr flatip.IP) { cl.Lock() diff --git a/irc/dline.go b/irc/dline.go index 3de5a621..419ad612 100644 --- a/irc/dline.go +++ b/irc/dline.go @@ -6,13 +6,11 @@ package irc import ( "encoding/json" "fmt" - "net" "strings" "sync" "time" "github.com/oragono/oragono/irc/flatip" - "github.com/oragono/oragono/irc/utils" "github.com/tidwall/buntdb" ) @@ -48,7 +46,11 @@ func (info IPBanInfo) TimeLeft() string { // BanMessage returns the ban message. 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 { message += fmt.Sprintf(" [%s]", info.TimeLeft()) } @@ -86,14 +88,14 @@ func (dm *DLineManager) AllBans() map[string]IPBanInfo { defer dm.RUnlock() for key, info := range dm.networks { - allb[key.String()] = info + allb[key.HumanReadableString()] = info } return allb } // 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() defer dm.persistenceMutex.Unlock() @@ -110,8 +112,7 @@ func (dm *DLineManager) AddNetwork(network net.IPNet, duration time.Duration, re return dm.persistDline(id, info) } -func (dm *DLineManager) addNetworkInternal(network net.IPNet, info IPBanInfo) (id flatip.IPNet) { - flatnet := flatip.FromNetIPNet(network) +func (dm *DLineManager) addNetworkInternal(flatnet flatip.IPNet, info IPBanInfo) (id flatip.IPNet) { id = flatnet var timeLeft time.Duration @@ -193,11 +194,11 @@ func (dm *DLineManager) unpersistDline(id flatip.IPNet) error { } // 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() defer dm.persistenceMutex.Unlock() - id := flatip.FromNetIPNet(network) + id := network present := func() bool { dm.Lock() @@ -215,22 +216,8 @@ func (dm *DLineManager) RemoveNetwork(network net.IPNet) error { 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. func (dm *DLineManager) CheckIP(addr flatip.IP) (isBanned bool, info IPBanInfo) { - if addr.IsLoopback() { - return // #671 - } - dm.RLock() defer dm.RUnlock() @@ -257,7 +244,7 @@ func (dm *DLineManager) loadFromDatastore() { key = strings.TrimPrefix(key, dlinePrefix) // load addr/net - hostNet, err := utils.NormalizedNetFromString(key) + hostNet, err := flatip.ParseToNormalizedNet(key) if err != nil { dm.server.logger.Error("internal", "bad dline cidr", err.Error()) return true diff --git a/irc/flatip/flatip.go b/irc/flatip/flatip.go index 7ebdbb50..7c603350 100644 --- a/irc/flatip/flatip.go +++ b/irc/flatip/flatip.go @@ -183,6 +183,16 @@ func (cidr IPNet) String() 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. // Although this is a valid subnet, it can still be used as a sentinel // value in some contexts. diff --git a/irc/getters.go b/irc/getters.go index 2be4dca2..bb14869b 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -314,6 +314,12 @@ func (client *Client) setCloakedHostname(cloak string) { client.updateNickMaskNoMutex() } +func (client *Client) CloakedHostname() string { + client.stateMutex.Lock() + defer client.stateMutex.Unlock() + return client.cloakedHostname +} + func (client *Client) historyCutoff() (cutoff time.Time) { client.stateMutex.Lock() if client.account != "" { @@ -553,3 +559,9 @@ func (channel *Channel) Ctime() (ctime time.Time) { channel.stateMutex.RUnlock() return } + +func (channel *Channel) getAmode(cfaccount string) (result modes.Mode) { + channel.stateMutex.RLock() + defer channel.stateMutex.RUnlock() + return channel.accountToUMode[cfaccount] +} diff --git a/irc/handlers.go b/irc/handlers.go index 3aec6edc..22b5a112 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -826,7 +826,7 @@ func formatBanForListing(client *Client, key string, info IPBanInfo) string { func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { // check oper permissions oper := client.Oper() - if oper == nil || !oper.Class.Capabilities.Has("local_ban") { + if !oper.HasRoleCapab("ban") { rb.Add(nil, server.name, ERR_NOPRIVS, client.nick, msg.Command, client.t("Insufficient oper privs")) return false } @@ -906,7 +906,7 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res 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 { rb.Notice(fmt.Sprintf(client.t("Could not successfully save new D-LINE: %s"), err.Error())) @@ -1273,6 +1273,10 @@ func sajoinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re } } + message := fmt.Sprintf("Operator %s ran SAJOIN %s", client.Oper().Name, strings.Join(msg.Params, " ")) + server.snomasks.Send(sno.LocalOpers, message) + server.logger.Info("opers", message) + channels := strings.Split(channelString, ",") for _, chname := range channels { err, _ := server.channels.Join(target, chname, "", true, rb) @@ -1364,7 +1368,7 @@ func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res details := client.Details() // check oper permissions oper := client.Oper() - if oper == nil || !oper.Class.Capabilities.Has("local_ban") { + if !oper.HasRoleCapab("ban") { rb.Add(nil, server.name, ERR_NOPRIVS, details.nick, msg.Command, client.t("Insufficient oper privs")) return false } @@ -1737,6 +1741,12 @@ func umodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res return false } + if msg.Command == "SAMODE" { + message := fmt.Sprintf("Operator %s ran SAMODE %s", client.Oper().Name, strings.Join(msg.Params, " ")) + server.snomasks.Send(sno.LocalOpers, message) + server.logger.Info("opers", message) + } + // applied mode changes applied := make(modes.ModeChanges, 0) @@ -2307,6 +2317,7 @@ func applyOper(client *Client, oper *Oper, rb *ResponseBuffer) { copy(modeChanges[1:], oper.Modes) applied := ApplyUserModeChanges(client, modeChanges, true, oper) + client.server.logger.Info("opers", details.nick, "opered up as", oper.Name) client.server.snomasks.Send(sno.LocalOpers, fmt.Sprintf(ircfmt.Unescape("Client opered up $c[grey][$r%s$c[grey], $r%s$c[grey]]"), newDetails.nickMask, oper.Name)) rb.Broadcast(nil, client.server.name, RPL_YOUREOPER, details.nick, client.t("You are now an IRC operator")) @@ -2814,7 +2825,7 @@ func topicHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res func unDLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { // check oper permissions oper := client.Oper() - if oper == nil || !oper.Class.Capabilities.Has("local_unban") { + if !oper.HasRoleCapab("ban") { rb.Add(nil, server.name, ERR_NOPRIVS, client.nick, msg.Command, client.t("Insufficient oper privs")) return false } @@ -2822,13 +2833,8 @@ func unDLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R // get host 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 - hostNet, err := utils.NormalizedNetFromString(hostString) + hostNet, err := flatip.ParseToNormalizedNet(hostString) if err != nil { rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, client.t("Could not parse IP address or CIDR network")) @@ -2842,7 +2848,7 @@ func unDLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R return false } - hostString = utils.NetToNormalizedString(hostNet) + hostString = hostNet.String() 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)) return false @@ -2853,7 +2859,7 @@ func unKLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R details := client.Details() // check oper permissions oper := client.Oper() - if oper == nil || !oper.Class.Capabilities.Has("local_unban") { + if !oper.HasRoleCapab("ban") { rb.Add(nil, server.name, ERR_NOPRIVS, details.nick, msg.Command, client.t("Insufficient oper privs")) return false } diff --git a/irc/help.go b/irc/help.go index 0e12d7d6..f7537463 100644 --- a/irc/help.go +++ b/irc/help.go @@ -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 given, views the current topic on the channel.`, + }, + "uban": { + text: `UBAN [arguments] + +Oragono's "unified ban" system. Accepts the following subcommands: + +1. UBAN ADD [DURATION ] [REASON...] +2. UBAN DEL +3. UBAN LIST +4. UBAN INFO + + may be an IP, a CIDR, a nickmask with wildcards, or the name of an +account to suspend.`, }, "undline": { oper: true, diff --git a/irc/ircconn.go b/irc/ircconn.go index 980b34f5..916c8655 100644 --- a/irc/ircconn.go +++ b/irc/ircconn.go @@ -1,3 +1,6 @@ +// Copyright (c) 2020 Shivaram Lingamneni +// released under the MIT license + package irc import ( diff --git a/irc/kline.go b/irc/kline.go index ca669a5e..be59482a 100644 --- a/irc/kline.go +++ b/irc/kline.go @@ -189,6 +189,17 @@ func (km *KLineManager) RemoveMask(mask string) error { 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. func (km *KLineManager) CheckMasks(masks ...string) (isBanned bool, info IPBanInfo) { km.RLock() diff --git a/irc/nickserv.go b/irc/nickserv.go index 1bdba9b2..6d8cca44 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -995,17 +995,21 @@ func nsPasswdHandler(service *ircService, server *Server, client *Client, comman var newPassword string var errorMessage string - hasPrivs := client.HasRoleCapabs("accreg") + var oper *Oper switch len(params) { case 2: - if !hasPrivs { + oper = client.Oper() + if !oper.HasRoleCapab("accreg") { errorMessage = `Insufficient privileges` } else { target, newPassword = params[0], params[1] if newPassword == "*" { newPassword = "" } + message := fmt.Sprintf("Operator %s ran NS PASSWD for account %s", oper.Name, target) + server.snomasks.Send(sno.LocalOpers, message) + server.logger.Info("opers", message) } case 3: target = client.Account() @@ -1041,7 +1045,7 @@ func nsPasswdHandler(service *ircService, server *Server, client *Client, comman return } - err := server.accounts.setPassword(target, newPassword, hasPrivs) + err := server.accounts.setPassword(target, newPassword, oper != nil) switch err { case nil: service.Notice(rb, client.t("Password changed")) @@ -1090,7 +1094,7 @@ func nsClientsHandler(service *ircService, server *Server, client *Client, comma func nsClientsListHandler(service *ircService, server *Server, client *Client, params []string, rb *ResponseBuffer) { target := client - hasPrivs := client.HasRoleCapabs("local_ban") + hasPrivs := client.HasRoleCapabs("ban") if 0 < len(params) { target = server.clients.Get(params[0]) if target == nil { @@ -1141,10 +1145,10 @@ func nsClientsLogoutHandler(service *ircService, server *Server, client *Client, service.Notice(rb, client.t("No such nick")) return } - // User must have "local_kill" privileges to logout other user sessions. + // User must have "kill" privileges to logout other user sessions. if target != client { oper := client.Oper() - if oper == nil || !oper.Class.Capabilities.Has("local_kill") { + if oper.HasRoleCapab("kill") { service.Notice(rb, client.t("Insufficient oper privs")) return } @@ -1357,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 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)) - 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 { - service.Notice(rb, suspensionToString(client, suspension)) + rb.Add(nil, source, "NOTICE", nick, suspensionToString(client, suspension)) } } diff --git a/irc/server.go b/irc/server.go index eed41a3e..cadb7c53 100644 --- a/irc/server.go +++ b/irc/server.go @@ -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) { + // #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 !(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" } } @@ -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 if output.Result == IPBanned && output.CacheSeconds != 0 { - network, err := utils.NormalizedNetFromString(output.CacheNet) + network, err := flatip.ParseToNormalizedNet(output.CacheNet) if err != nil { server.logger.Error("internal", "invalid dline net from IP ban script", ipaddr.String(), output.CacheNet) } else { @@ -339,11 +345,13 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) { // count new user in statistics (before checking KLINEs, see #1303) server.stats.Register(c.HasMode(modes.Invisible)) - // check KLINEs - isBanned, info := server.klines.CheckMasks(c.AllNickmasks()...) - if isBanned { - c.Quit(info.BanMessage(c.t("You are banned from this server (%s)")), nil) - return true + // check KLINEs (#671: ignore KLINEs for loopback connections) + if !session.IP().IsLoopback() || session.isTor { + isBanned, info := server.klines.CheckMasks(c.AllNickmasks()...) + if isBanned { + c.Quit(info.BanMessage(c.t("You are banned from this server (%s)")), nil) + return true + } } server.playRegistrationBurst(session) diff --git a/irc/uban.go b/irc/uban.go new file mode 100644 index 00000000..2e9be18d --- /dev/null +++ b/irc/uban.go @@ -0,0 +1,398 @@ +// 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 [target] [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: + ubanAddAccount(client, target, duration, operReason, rb) + + } + 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 ")) + } +} + +func ubanAddAccount(client *Client, target ubanTarget, duration time.Duration, operReason string, rb *ResponseBuffer) { + account := target.nickOrMask + // TODO this doesn't enumerate all sessions if ForceNickEqualsAccount is disabled + var sessionData []SessionData + if mcl := client.server.clients.Get(account); mcl != nil { + sessionData, _ = mcl.AllSessionData(nil, true) + } + + err := client.server.accounts.Suspend(account, duration, client.Oper().Name, operReason) + switch err { + case nil: + rb.Notice(fmt.Sprintf(client.t("Successfully suspended account %s"), account)) + if len(sessionData) != 0 { + rb.Notice(fmt.Sprintf(client.t("Disconnected %d client(s) associated with the account, using the following IPs:"), len(sessionData))) + for i, d := range sessionData { + rb.Notice(fmt.Sprintf("%d. %s", i+1, d.ip.String())) + } + } + case errAccountDoesNotExist: + rb.Notice(client.t("No such account")) + default: + rb.Notice(client.t("An error occurred")) + } +} + +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)) + } +} diff --git a/traditional.yaml b/traditional.yaml index 4976e67f..1436ff7f 100644 --- a/traditional.yaml +++ b/traditional.yaml @@ -556,9 +556,8 @@ oper-classes: # capability names capabilities: - - "local_kill" - - "local_ban" - - "local_unban" + - "kill" + - "ban" - "nofakelag" - "roleplay" - "relaymsg"