]> git.netwichtig.de Git - user/henk/code/inspircd.git/blob - src/modules/m_dnsbl.cpp
Add support for blocking tag messages with the deaf mode.
[user/henk/code/inspircd.git] / src / modules / m_dnsbl.cpp
1 /*
2  * InspIRCd -- Internet Relay Chat Daemon
3  *
4  *   Copyright (C) 2018-2020 Matt Schatz <genius3000@g3k.solutions>
5  *   Copyright (C) 2018-2019 linuxdaemon <linuxdaemon.irc@gmail.com>
6  *   Copyright (C) 2013, 2017-2021 Sadie Powell <sadie@witchery.services>
7  *   Copyright (C) 2013, 2015-2016 Adam <Adam@anope.org>
8  *   Copyright (C) 2012-2016 Attila Molnar <attilamolnar@hush.com>
9  *   Copyright (C) 2012, 2018 Robby <robby@chatbelgie.be>
10  *   Copyright (C) 2009-2010 Daniel De Graaf <danieldg@inspircd.org>
11  *   Copyright (C) 2007, 2010 Craig Edwards <brain@inspircd.org>
12  *   Copyright (C) 2007 Dennis Friis <peavey@inspircd.org>
13  *   Copyright (C) 2006-2009 Robin Burchell <robin+git@viroteck.net>
14  *
15  * This file is part of InspIRCd.  InspIRCd is free software: you can
16  * redistribute it and/or modify it under the terms of the GNU General Public
17  * License as published by the Free Software Foundation, version 2.
18  *
19  * This program is distributed in the hope that it will be useful, but WITHOUT
20  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
21  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
22  * details.
23  *
24  * You should have received a copy of the GNU General Public License
25  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
26  */
27
28
29 #include "inspircd.h"
30 #include "xline.h"
31 #include "modules/dns.h"
32 #include "modules/stats.h"
33
34 /* Class holding data for a single entry */
35 class DNSBLConfEntry : public refcountbase
36 {
37         public:
38                 enum EnumBanaction { I_UNKNOWN, I_KILL, I_ZLINE, I_KLINE, I_GLINE, I_MARK };
39                 enum EnumType { A_RECORD, A_BITMASK };
40                 std::string name, ident, host, domain, reason;
41                 EnumBanaction banaction;
42                 EnumType type;
43                 unsigned long duration;
44                 unsigned int bitmask;
45                 unsigned char records[256];
46                 unsigned long stats_hits, stats_misses, stats_errors;
47                 DNSBLConfEntry()
48                         : type(A_BITMASK)
49                         , duration(86400)
50                         , bitmask(0)
51                         , stats_hits(0)
52                         , stats_misses(0)
53                         , stats_errors(0)
54                 {
55                 }
56 };
57
58
59 /** Resolver for CGI:IRC hostnames encoded in ident/real name
60  */
61 class DNSBLResolver : public DNS::Request
62 {
63  private:
64         irc::sockets::sockaddrs theirsa;
65         std::string theiruid;
66         LocalStringExt& nameExt;
67         LocalIntExt& countExt;
68         reference<DNSBLConfEntry> ConfEntry;
69
70  public:
71         DNSBLResolver(DNS::Manager *mgr, Module *me, LocalStringExt& match, LocalIntExt& ctr, const std::string &hostname, LocalUser* u, reference<DNSBLConfEntry> conf)
72                 : DNS::Request(mgr, me, hostname, DNS::QUERY_A, true)
73                 , theirsa(u->client_sa)
74                 , theiruid(u->uuid)
75                 , nameExt(match)
76                 , countExt(ctr)
77                 , ConfEntry(conf)
78         {
79         }
80
81         /* Note: This may be called multiple times for multiple A record results */
82         void OnLookupComplete(const DNS::Query *r) CXX11_OVERRIDE
83         {
84                 /* Check the user still exists */
85                 LocalUser* them = IS_LOCAL(ServerInstance->FindUUID(theiruid));
86                 if (!them || them->client_sa != theirsa)
87                 {
88                         ConfEntry->stats_misses++;
89                         return;
90                 }
91
92                 int i = countExt.get(them);
93                 if (i)
94                         countExt.set(them, i - 1);
95
96                 // The DNSBL reply must contain an A result.
97                 const DNS::ResourceRecord* const ans_record = r->FindAnswerOfType(DNS::QUERY_A);
98                 if (!ans_record)
99                 {
100                         ConfEntry->stats_errors++;
101                         ServerInstance->SNO->WriteGlobalSno('d', "%s returned an result with no IPv4 address.",
102                                 ConfEntry->name.c_str());
103                         return;
104                 }
105
106                 // The DNSBL reply must be a valid IPv4 address.
107                 in_addr resultip;
108                 if (inet_pton(AF_INET, ans_record->rdata.c_str(), &resultip) != 1)
109                 {
110                         ConfEntry->stats_errors++;
111                         ServerInstance->SNO->WriteGlobalSno('d', "%s returned an invalid IPv4 address: %s",
112                                 ConfEntry->name.c_str(), ans_record->rdata.c_str());
113                         return;
114                 }
115
116                 // The DNSBL reply should be in the 127.0.0.0/8 range.
117                 if ((resultip.s_addr & 0xFF) != 127)
118                 {
119                         ConfEntry->stats_errors++;
120                         ServerInstance->SNO->WriteGlobalSno('d', "%s returned an IPv4 address which is outside of the 127.0.0.0/8 subnet: %s",
121                                 ConfEntry->name.c_str(), ans_record->rdata.c_str());
122                         return;
123                 }
124
125                 bool match = false;
126                 unsigned int result = 0;
127                 switch (ConfEntry->type)
128                 {
129                         case DNSBLConfEntry::A_BITMASK:
130                         {
131                                 result = (resultip.s_addr >> 24) & ConfEntry->bitmask;
132                                 match = (result != 0);
133                                 break;
134                         }
135                         case DNSBLConfEntry::A_RECORD:
136                         {
137                                 result = resultip.s_addr >> 24;
138                                 match = (ConfEntry->records[result] == 1);
139                                 break;
140                         }
141                 }
142
143                 if (match)
144                 {
145                         std::string reason = ConfEntry->reason;
146                         std::string::size_type x = reason.find("%ip%");
147                         while (x != std::string::npos)
148                         {
149                                 reason.erase(x, 4);
150                                 reason.insert(x, them->GetIPString());
151                                 x = reason.find("%ip%");
152                         }
153
154                         ConfEntry->stats_hits++;
155
156                         switch (ConfEntry->banaction)
157                         {
158                                 case DNSBLConfEntry::I_KILL:
159                                 {
160                                         ServerInstance->Users->QuitUser(them, "Killed (" + reason + ")");
161                                         break;
162                                 }
163                                 case DNSBLConfEntry::I_MARK:
164                                 {
165                                         if (!ConfEntry->ident.empty())
166                                         {
167                                                 them->WriteNotice("Your ident has been set to " + ConfEntry->ident + " because you matched " + reason);
168                                                 them->ChangeIdent(ConfEntry->ident);
169                                         }
170
171                                         if (!ConfEntry->host.empty())
172                                         {
173                                                 them->WriteNotice("Your host has been set to " + ConfEntry->host + " because you matched " + reason);
174                                                 them->ChangeDisplayedHost(ConfEntry->host);
175                                         }
176
177                                         nameExt.set(them, ConfEntry->name);
178                                         break;
179                                 }
180                                 case DNSBLConfEntry::I_KLINE:
181                                 {
182                                         KLine* kl = new KLine(ServerInstance->Time(), ConfEntry->duration, ServerInstance->Config->ServerName.c_str(), reason.c_str(),
183                                                         "*", them->GetIPString());
184                                         if (ServerInstance->XLines->AddLine(kl,NULL))
185                                         {
186                                                 ServerInstance->SNO->WriteToSnoMask('x', "K-line added due to DNSBL match on *@%s to expire in %s (on %s): %s",
187                                                         them->GetIPString().c_str(), InspIRCd::DurationString(kl->duration).c_str(),
188                                                         InspIRCd::TimeString(kl->expiry).c_str(), reason.c_str());
189                                                 ServerInstance->XLines->ApplyLines();
190                                         }
191                                         else
192                                         {
193                                                 delete kl;
194                                                 return;
195                                         }
196                                         break;
197                                 }
198                                 case DNSBLConfEntry::I_GLINE:
199                                 {
200                                         GLine* gl = new GLine(ServerInstance->Time(), ConfEntry->duration, ServerInstance->Config->ServerName.c_str(), reason.c_str(),
201                                                         "*", them->GetIPString());
202                                         if (ServerInstance->XLines->AddLine(gl,NULL))
203                                         {
204                                                 ServerInstance->SNO->WriteToSnoMask('x', "G-line added due to DNSBL match on *@%s to expire in %s (on %s): %s",
205                                                         them->GetIPString().c_str(), InspIRCd::DurationString(gl->duration).c_str(),
206                                                         InspIRCd::TimeString(gl->expiry).c_str(), reason.c_str());
207                                                 ServerInstance->XLines->ApplyLines();
208                                         }
209                                         else
210                                         {
211                                                 delete gl;
212                                                 return;
213                                         }
214                                         break;
215                                 }
216                                 case DNSBLConfEntry::I_ZLINE:
217                                 {
218                                         ZLine* zl = new ZLine(ServerInstance->Time(), ConfEntry->duration, ServerInstance->Config->ServerName.c_str(), reason.c_str(),
219                                                         them->GetIPString());
220                                         if (ServerInstance->XLines->AddLine(zl,NULL))
221                                         {
222                                                 ServerInstance->SNO->WriteToSnoMask('x', "Z-line added due to DNSBL match on %s to expire in %s (on %s): %s",
223                                                         them->GetIPString().c_str(), InspIRCd::DurationString(zl->duration).c_str(),
224                                                         InspIRCd::TimeString(zl->expiry).c_str(), reason.c_str());
225                                                 ServerInstance->XLines->ApplyLines();
226                                         }
227                                         else
228                                         {
229                                                 delete zl;
230                                                 return;
231                                         }
232                                         break;
233                                 }
234                                 case DNSBLConfEntry::I_UNKNOWN:
235                                 default:
236                                         break;
237                         }
238
239                         ServerInstance->SNO->WriteGlobalSno('d', "Connecting user %s (%s) detected as being on the '%s' DNS blacklist with result %d",
240                                 them->GetFullRealHost().c_str(), them->GetIPString().c_str(), ConfEntry->name.c_str(), result);
241                 }
242                 else
243                         ConfEntry->stats_misses++;
244         }
245
246         void OnError(const DNS::Query *q) CXX11_OVERRIDE
247         {
248                 bool is_miss = true;
249                 switch (q->error)
250                 {
251                         case DNS::ERROR_NO_RECORDS:
252                         case DNS::ERROR_DOMAIN_NOT_FOUND:
253                                 ConfEntry->stats_misses++;
254                                 break;
255
256                         default:
257                                 ConfEntry->stats_errors++;
258                                 is_miss = false;
259                                 break;
260                 }
261
262                 LocalUser* them = IS_LOCAL(ServerInstance->FindUUID(theiruid));
263                 if (!them || them->client_sa != theirsa)
264                         return;
265
266                 int i = countExt.get(them);
267                 if (i)
268                         countExt.set(them, i - 1);
269
270                 if (is_miss)
271                         return;
272
273                 ServerInstance->SNO->WriteGlobalSno('d', "An error occurred whilst checking whether %s (%s) is on the '%s' DNS blacklist: %s",
274                         them->GetFullRealHost().c_str(), them->GetIPString().c_str(), ConfEntry->name.c_str(), this->manager->GetErrorStr(q->error).c_str());
275         }
276 };
277
278 typedef std::vector<reference<DNSBLConfEntry> > DNSBLConfList;
279
280 class ModuleDNSBL : public Module, public Stats::EventListener
281 {
282         DNSBLConfList DNSBLConfEntries;
283         dynamic_reference<DNS::Manager> DNS;
284         LocalStringExt nameExt;
285         LocalIntExt countExt;
286
287         /*
288          *      Convert a string to EnumBanaction
289          */
290         DNSBLConfEntry::EnumBanaction str2banaction(const std::string &action)
291         {
292                 if (stdalgo::string::equalsci(action, "kill"))
293                         return DNSBLConfEntry::I_KILL;
294                 if (stdalgo::string::equalsci(action, "kline"))
295                         return DNSBLConfEntry::I_KLINE;
296                 if (stdalgo::string::equalsci(action, "zline"))
297                         return DNSBLConfEntry::I_ZLINE;
298                 if (stdalgo::string::equalsci(action, "gline"))
299                         return DNSBLConfEntry::I_GLINE;
300                 if (stdalgo::string::equalsci(action, "mark"))
301                         return DNSBLConfEntry::I_MARK;
302                 return DNSBLConfEntry::I_UNKNOWN;
303         }
304  public:
305         ModuleDNSBL()
306                 : Stats::EventListener(this)
307                 , DNS(this, "DNS")
308                 , nameExt("dnsbl_match", ExtensionItem::EXT_USER, this)
309                 , countExt("dnsbl_pending", ExtensionItem::EXT_USER, this)
310         {
311         }
312
313         void init() CXX11_OVERRIDE
314         {
315                 ServerInstance->SNO->EnableSnomask('d', "DNSBL");
316         }
317
318         void Prioritize() CXX11_OVERRIDE
319         {
320                 Module* corexline = ServerInstance->Modules->Find("core_xline");
321                 ServerInstance->Modules->SetPriority(this, I_OnSetUserIP, PRIORITY_AFTER, corexline);
322         }
323
324         Version GetVersion() CXX11_OVERRIDE
325         {
326                 return Version("Allows the server administrator to check the IP address of connecting users against a DNSBL.", VF_VENDOR);
327         }
328
329         /** Fill our conf vector with data
330          */
331         void ReadConfig(ConfigStatus& status) CXX11_OVERRIDE
332         {
333                 DNSBLConfList newentries;
334
335                 ConfigTagList dnsbls = ServerInstance->Config->ConfTags("dnsbl");
336                 for(ConfigIter i = dnsbls.first; i != dnsbls.second; ++i)
337                 {
338                         ConfigTag* tag = i->second;
339                         reference<DNSBLConfEntry> e = new DNSBLConfEntry();
340
341                         e->name = tag->getString("name");
342                         e->ident = tag->getString("ident");
343                         e->host = tag->getString("host");
344                         e->reason = tag->getString("reason", "Your IP has been blacklisted.", 1);
345                         e->domain = tag->getString("domain");
346
347                         if (stdalgo::string::equalsci(tag->getString("type"), "bitmask"))
348                         {
349                                 e->type = DNSBLConfEntry::A_BITMASK;
350                                 e->bitmask = tag->getUInt("bitmask", 0, 0, UINT_MAX);
351                         }
352                         else
353                         {
354                                 memset(e->records, 0, sizeof(e->records));
355                                 e->type = DNSBLConfEntry::A_RECORD;
356                                 irc::portparser portrange(tag->getString("records"), false);
357                                 long item = -1;
358                                 while ((item = portrange.GetToken()))
359                                         e->records[item] = 1;
360                         }
361
362                         e->banaction = str2banaction(tag->getString("action"));
363                         e->duration = tag->getDuration("duration", 60, 1);
364
365                         /* Use portparser for record replies */
366
367                         /* yeah, logic here is a little messy */
368                         if ((e->bitmask <= 0) && (DNSBLConfEntry::A_BITMASK == e->type))
369                         {
370                                 throw ModuleException("Invalid <dnsbl:bitmask> at " + tag->getTagLocation());
371                         }
372                         else if (e->name.empty())
373                         {
374                                 throw ModuleException("Empty <dnsbl:name> at " + tag->getTagLocation());
375                         }
376                         else if (e->domain.empty())
377                         {
378                                 throw ModuleException("Empty <dnsbl:domain> at " + tag->getTagLocation());
379                         }
380                         else if (e->banaction == DNSBLConfEntry::I_UNKNOWN)
381                         {
382                                 throw ModuleException("Unknown <dnsbl:action> at " + tag->getTagLocation());
383                         }
384                         else
385                         {
386                                 /* add it, all is ok */
387                                 newentries.push_back(e);
388                         }
389                 }
390
391                 DNSBLConfEntries.swap(newentries);
392         }
393
394         void OnSetUserIP(LocalUser* user) CXX11_OVERRIDE
395         {
396                 if (user->exempt || user->quitting || !DNS)
397                         return;
398
399                 // Clients can't be in a DNSBL if they aren't connected via IPv4 or IPv6.
400                 if (user->client_sa.family() != AF_INET && user->client_sa.family() != AF_INET6)
401                         return;
402
403                 if (user->MyClass)
404                 {
405                         if (!user->MyClass->config->getBool("usednsbl", true))
406                                 return;
407                 }
408                 else
409                 {
410                         ServerInstance->Logs->Log(MODNAME, LOG_DEBUG, "User has no connect class in OnSetUserIP");
411                         return;
412                 }
413
414                 std::string reversedip;
415                 if (user->client_sa.family() == AF_INET)
416                 {
417                         unsigned int a, b, c, d;
418                         d = (unsigned int) (user->client_sa.in4.sin_addr.s_addr >> 24) & 0xFF;
419                         c = (unsigned int) (user->client_sa.in4.sin_addr.s_addr >> 16) & 0xFF;
420                         b = (unsigned int) (user->client_sa.in4.sin_addr.s_addr >> 8) & 0xFF;
421                         a = (unsigned int) user->client_sa.in4.sin_addr.s_addr & 0xFF;
422
423                         reversedip = ConvToStr(d) + "." + ConvToStr(c) + "." + ConvToStr(b) + "." + ConvToStr(a);
424                 }
425                 else if (user->client_sa.family() == AF_INET6)
426                 {
427                         const unsigned char* ip = user->client_sa.in6.sin6_addr.s6_addr;
428
429                         std::string buf = BinToHex(ip, 16);
430                         for (std::string::const_reverse_iterator it = buf.rbegin(); it != buf.rend(); ++it)
431                         {
432                                 reversedip.push_back(*it);
433                                 reversedip.push_back('.');
434                         }
435                         reversedip.erase(reversedip.length() - 1, 1);
436                 }
437                 else
438                         return;
439
440                 ServerInstance->Logs->Log(MODNAME, LOG_DEBUG, "Reversed IP %s -> %s", user->GetIPString().c_str(), reversedip.c_str());
441
442                 countExt.set(user, DNSBLConfEntries.size());
443
444                 // For each DNSBL, we will run through this lookup
445                 for (unsigned i = 0; i < DNSBLConfEntries.size(); ++i)
446                 {
447                         // Fill hostname with a dnsbl style host (d.c.b.a.domain.tld)
448                         std::string hostname = reversedip + "." + DNSBLConfEntries[i]->domain;
449
450                         /* now we'd need to fire off lookups for `hostname'. */
451                         DNSBLResolver *r = new DNSBLResolver(*this->DNS, this, nameExt, countExt, hostname, user, DNSBLConfEntries[i]);
452                         try
453                         {
454                                 this->DNS->Process(r);
455                         }
456                         catch (DNS::Exception &ex)
457                         {
458                                 delete r;
459                                 ServerInstance->Logs->Log(MODNAME, LOG_DEBUG, ex.GetReason());
460                         }
461
462                         if (user->quitting)
463                                 break;
464                 }
465         }
466
467         ModResult OnSetConnectClass(LocalUser* user, ConnectClass* myclass) CXX11_OVERRIDE
468         {
469                 std::string dnsbl;
470                 if (!myclass->config->readString("dnsbl", dnsbl))
471                         return MOD_RES_PASSTHRU;
472
473                 std::string* match = nameExt.get(user);
474                 if (!match)
475                 {
476                         ServerInstance->Logs->Log("CONNECTCLASS", LOG_DEBUG, "The %s connect class is not suitable as it requires a DNSBL mark",
477                                         myclass->GetName().c_str());
478                         return MOD_RES_DENY;
479                 }
480
481                 if (!InspIRCd::Match(*match, dnsbl))
482                 {
483                         ServerInstance->Logs->Log("CONNECTCLASS", LOG_DEBUG, "The %s connect class is not suitable as the DNSBL mark (%s) does not match %s",
484                                         myclass->GetName().c_str(), match->c_str(), dnsbl.c_str());
485                         return MOD_RES_DENY;
486                 }
487
488                 return MOD_RES_PASSTHRU;
489         }
490
491         ModResult OnCheckReady(LocalUser *user) CXX11_OVERRIDE
492         {
493                 if (countExt.get(user))
494                         return MOD_RES_DENY;
495                 return MOD_RES_PASSTHRU;
496         }
497
498         ModResult OnStats(Stats::Context& stats) CXX11_OVERRIDE
499         {
500                 if (stats.GetSymbol() != 'd')
501                         return MOD_RES_PASSTHRU;
502
503                 unsigned long total_hits = 0;
504                 unsigned long total_misses = 0;
505                 unsigned long total_errors = 0;
506                 for (std::vector<reference<DNSBLConfEntry> >::const_iterator i = DNSBLConfEntries.begin(); i != DNSBLConfEntries.end(); ++i)
507                 {
508                         total_hits += (*i)->stats_hits;
509                         total_misses += (*i)->stats_misses;
510                         total_errors += (*i)->stats_errors;
511
512                         stats.AddRow(304, InspIRCd::Format("DNSBLSTATS \"%s\" had %lu hits, %lu misses, and %lu errors",
513                                 (*i)->name.c_str(), (*i)->stats_hits, (*i)->stats_misses, (*i)->stats_errors));
514                 }
515
516                 stats.AddRow(304, "DNSBLSTATS Total hits: " + ConvToStr(total_hits));
517                 stats.AddRow(304, "DNSBLSTATS Total misses: " + ConvToStr(total_misses));
518                 stats.AddRow(304, "DNSBLSTATS Total errors: " + ConvToStr(total_errors));
519                 return MOD_RES_PASSTHRU;
520         }
521 };
522
523 MODULE_INIT(ModuleDNSBL)