diff --git a/irc/client.go b/irc/client.go index 3be6a75b..63a33379 100644 --- a/irc/client.go +++ b/irc/client.go @@ -82,6 +82,8 @@ type Client struct { username string vhost string whoisLine string + requireSasl bool + requireSaslReason string } // NewClient returns a client with all the appropriate info setup. @@ -108,6 +110,9 @@ func NewClient(server *Server, conn net.Conn, isTLS bool) *Client { } client.languages = server.languages.Default() + // Check IP towards speified DNSBLs + server.ProcessBlacklist(client) + client.recomputeMaxlens() if isTLS { client.flags[modes.TLS] = true diff --git a/irc/config.go b/irc/config.go index ada0a578..bc28b1ff 100644 --- a/irc/config.go +++ b/irc/config.go @@ -90,6 +90,25 @@ type AccountRegistrationConfig struct { AllowMultiplePerConnection bool `yaml:"allow-multiple-per-connection"` } +type DnsblConfig struct { + Enabled bool + Channel string + Lists []DnsblListEntry `yaml:"lists"` +} + +type DnsblListEntry struct { + Host string + Types []string + Reply map[string]DnsblListReply + Action string + Reason string +} + +type DnsblListReply struct { + Action string + Reason string +} + type NickReservationMethod int const ( @@ -263,6 +282,7 @@ type Config struct { LineLen LineLenConfig `yaml:"linelen"` } + Dnsbl DnsblConfig Fakelag FakelagConfig Filename string @@ -471,6 +491,26 @@ func LoadConfig(filename string) (config *Config, err error) { newWebIRC = append(newWebIRC, webirc) } config.Server.WebIRC = newWebIRC + + for id, list := range config.Dnsbl.Lists { + var action, reason = list.Action, list.Reason + + var newDnsblListReply = make(map[string]DnsblListReply) + for key, reply := range list.Reply { + if reply.Action == "" { + reply.Action = action + } + if reply.Reason == "" { + reply.Reason = reason + } + + for _, newKey := range strings.Split(key, ",") { + newDnsblListReply[newKey] = reply + } + } + config.Dnsbl.Lists[id].Reply = newDnsblListReply + } + // process limits if config.Limits.LineLen.Tags < 512 || config.Limits.LineLen.Rest < 512 { return nil, ErrLineLengthsTooSmall diff --git a/irc/dnsbl.go b/irc/dnsbl.go new file mode 100644 index 00000000..2f3d7632 --- /dev/null +++ b/irc/dnsbl.go @@ -0,0 +1,142 @@ +package irc + +import ( + "fmt" + "net" + "sort" + "strings" + + "github.com/oragono/oragono/irc/sno" +) + +func ReverseAddress(ip net.IP) string { + // This is a IPv4 address + if ip.To4() != nil { + address := strings.Split(ip.String(), ".") + + for i, j := 0, len(address)-1; i < j; i, j = i+1, j-1 { + address[i], address[j] = address[j], address[i] + } + + return strings.Join(address, ".") + } + + // fallback to returning the String of IP if it is not an IPv4 address + return ip.String() +} + +func LastIpOctet(addr string) string { + address := strings.Split(addr, ".") + + return address[len(address)-1] +} + +func (server *Server) LookupBlacklistEntry(list *DnsblListEntry, client *Client) []string { + res, err := net.LookupHost(fmt.Sprintf("%s.%s", ReverseAddress(client.IP()), list.Host)) + + var entries []string + if err != nil { + server.logger.Info("dnsbl-lookup", fmt.Sprintf("DNSBL loopup failed: %s", err)) + return entries + } + + if len(res) > 0 { + for _, addr := range res { + entries = append(entries, LastIpOctet(addr)) + } + } + + return entries +} + +func sendDnsblMessage(client *Client, message string) { + /*fmt.Printf(client.server.DnsblConfig().Channel) + if channel := client.server.DnsblConfig().Channel; channel != "" { + fmt.Printf(channel) + client.Send(nil, client.server.name, "PRIVMSG", channel, message) + } + */ + client.server.snomasks.Send(sno.Dnsbl, message) +} + +// ProcessBlacklist does +func (server *Server) ProcessBlacklist(client *Client) { + + if !server.DnsblConfig().Enabled || len(server.DnsblConfig().Lists) == 0 { + // do nothing if dnsbl is disabled, empty lists is treated as if dnsbl was disabled + return + } + + type DnsblTypeResponse struct { + Host string + Action string + Reason string + } + var items = []DnsblTypeResponse{} + for _, list := range server.DnsblConfig().Lists { + response := DnsblTypeResponse{ + Host: list.Host, + Action: list.Action, + Reason: list.Reason, + } + // update action/reason if matched with new ... + for _, entry := range server.LookupBlacklistEntry(&list, client) { + if reply, exists := list.Reply[entry]; exists { + response.Action, response.Reason = reply.Action, reply.Reason + } + items = append(items, response) + } + } + + // Sort responses so that require-sasl blocks come first. Otherwise A>B (allow>block, allow>notify, block>notify) + // so that responses come in this order: + // - require-sasl + // - allow + // - block + // - notify + sort.Slice(items, func(i, j int) bool { + if items[i].Action == "require-sasl" { + return true + } + return items[i].Action > items[j].Action + }) + + if len(items) > 0 { + item := items[0] + switch item.Action { + case "require-sasl": + sendDnsblMessage(client, fmt.Sprintf("Connecting client %s matched %s, requiring SASL to proceed", client.IP(), item.Host)) + client.SetRequireSasl(true, item.Reason) + + case "block": + sendDnsblMessage(client, fmt.Sprintf("Connecting client %s matched %s - killing", client.IP(), item.Host)) + client.Quit(strings.Replace(item.Reason, "{ip}", client.IPString(), -1)) + + case "notify": + sendDnsblMessage(client, fmt.Sprintf("Connecting client %s matched %s", client.IP(), item.Host)) + + case "allow": + sendDnsblMessage(client, fmt.Sprintf("Allowing host %s [%s]", client.IP(), item.Host)) + } + } + + return +} + +func connectionRequiresSasl(client *Client) bool { + sasl, reason := client.RequireSasl() + + if !sasl { + return false + } + + if client.Account() == "" { + sendDnsblMessage(client, fmt.Sprintf("Connecting client %s and did not authenticate through SASL - blocking connection", client.IP())) + client.Quit(strings.Replace(reason, "{ip}", client.IPString(), -1)) + return true + } + + sendDnsblMessage(client, fmt.Sprintf("Connecting client %s authenticated through SASL - allowing", client.IP())) + + return false +} diff --git a/irc/getters.go b/irc/getters.go index 9d1c6498..c8e69266 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -4,9 +4,10 @@ package irc import ( + "sync/atomic" + "github.com/oragono/oragono/irc/isupport" "github.com/oragono/oragono/irc/modes" - "sync/atomic" ) func (server *Server) MaxSendQBytes() int { @@ -74,6 +75,15 @@ func (server *Server) AccountConfig() *AccountConfig { return &server.config.Accounts } +func (server *Server) DnsblConfig() *DnsblConfig { + server.configurableStateMutex.RLock() + defer server.configurableStateMutex.RUnlock() + if server.config == nil { + return nil + } + return &server.config.Dnsbl +} + func (server *Server) FakelagConfig() *FakelagConfig { server.configurableStateMutex.RLock() defer server.configurableStateMutex.RUnlock() @@ -175,6 +185,19 @@ func (client *Client) SetAuthorized(authorized bool) { client.authorized = authorized } +func (client *Client) RequireSasl() (bool, string) { + client.stateMutex.RLock() + defer client.stateMutex.RUnlock() + return client.requireSasl, client.requireSaslReason +} + +func (client *Client) SetRequireSasl(required bool, reason string) { + client.stateMutex.Lock() + defer client.stateMutex.Unlock() + client.requireSasl = required + client.requireSaslReason = reason +} + func (client *Client) PreregNick() string { client.stateMutex.RLock() defer client.stateMutex.RUnlock() diff --git a/irc/handlers.go b/irc/handlers.go index ac753e80..7b0e1cf4 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -1785,7 +1785,7 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp server.snomasks.Send(sno.LocalOpers, fmt.Sprintf(ircfmt.Unescape("Client opered up $c[grey][$r%s$c[grey], $r%s$c[grey]]"), client.nickMaskString, client.operName)) // increase oper count - server.stats.ChangeOperators(1) + //server.stats.ChangeOperators(1) // client may now be unthrottled by the fakelag system client.resetFakelag() diff --git a/irc/modes.go b/irc/modes.go index d8cc95b8..fa11ace7 100644 --- a/irc/modes.go +++ b/irc/modes.go @@ -30,6 +30,10 @@ func (client *Client) applyUserModeChanges(force bool, changes modes.ModeChanges case modes.Bot, modes.Invisible, modes.WallOps, modes.UserRoleplaying, modes.Operator, modes.LocalOperator, modes.RegisteredOnly: switch change.Op { case modes.Add: + if change.Mode == modes.Operator || change.Mode == modes.LocalOperator { + client.server.stats.ChangeOperators(1) + } + if !force && (change.Mode == modes.Operator || change.Mode == modes.LocalOperator) { continue } diff --git a/irc/server.go b/irc/server.go index 1fa9dfb4..99629ab6 100644 --- a/irc/server.go +++ b/irc/server.go @@ -446,6 +446,11 @@ func (server *Server) tryRegister(c *Client) { return } + if connectionRequiresSasl(c) { + c.destroy(false) + return + } + // client MUST send PASS (or AUTHENTICATE, if skip-server-password is set) // before completing the other registration commands if !c.Authorized() { diff --git a/irc/sno/constants.go b/irc/sno/constants.go index 5449962e..98456acf 100644 --- a/irc/sno/constants.go +++ b/irc/sno/constants.go @@ -19,6 +19,7 @@ const ( Stats Mask = 't' LocalAccounts Mask = 'u' LocalXline Mask = 'x' + Dnsbl Mask = 'S' ) var ( @@ -34,6 +35,7 @@ var ( Stats: "STATS", LocalAccounts: "ACCOUNT", LocalXline: "XLINE", + Dnsbl: "DNSBL", } // ValidMasks contains the snomasks that we support. @@ -48,5 +50,6 @@ var ( Stats: true, LocalAccounts: true, LocalXline: true, + Dnsbl: true, } )