]> git.netwichtig.de Git - user/henk/code/inspircd.git/blob - src/modules/m_dnsbl.cpp
6712e267422077935c185add906e85c3a6b1a3b6
[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                         return;
88
89                 int i = countExt.get(them);
90                 if (i)
91                         countExt.set(them, i - 1);
92
93                 // The DNSBL reply must contain an A result.
94                 const DNS::ResourceRecord* const ans_record = r->FindAnswerOfType(DNS::QUERY_A);
95                 if (!ans_record)
96                 {
97                         ConfEntry->stats_errors++;
98                         ServerInstance->SNO->WriteGlobalSno('d', "%s returned an result with no IPv4 address.",
99                                 ConfEntry->name.c_str());
100                         return;
101                 }
102
103                 // The DNSBL reply must be a valid IPv4 address.
104                 in_addr resultip;
105                 if (inet_pton(AF_INET, ans_record->rdata.c_str(), &resultip) != 1)
106                 {
107                         ConfEntry->stats_errors++;
108                         ServerInstance->SNO->WriteGlobalSno('d', "%s returned an invalid IPv4 address: %s",
109                                 ConfEntry->name.c_str(), ans_record->rdata.c_str());
110                         return;
111                 }
112
113                 // The DNSBL reply should be in the 127.0.0.0/8 range.
114                 if ((resultip.s_addr & 0xFF) != 127)
115                 {
116                         ConfEntry->stats_errors++;
117                         ServerInstance->SNO->WriteGlobalSno('d', "%s returned an IPv4 address which is outside of the 127.0.0.0/8 subnet: %s",
118                                 ConfEntry->name.c_str(), ans_record->rdata.c_str());
119                         return;
120                 }
121
122                 bool match = false;
123                 unsigned int result = 0;
124                 switch (ConfEntry->type)
125                 {
126                         case DNSBLConfEntry::A_BITMASK:
127                         {
128                                 result = (resultip.s_addr >> 24) & ConfEntry->bitmask;
129                                 match = (result != 0);
130                                 break;
131                         }
132                         case DNSBLConfEntry::A_RECORD:
133                         {
134                                 result = resultip.s_addr >> 24;
135                                 match = (ConfEntry->records[result] == 1);
136                                 break;
137                         }
138                 }
139
140                 if (match)
141                 {
142                         std::string reason = ConfEntry->reason;
143                         std::string::size_type x = reason.find("%ip%");
144                         while (x != std::string::npos)
145                         {
146                                 reason.erase(x, 4);
147                                 reason.insert(x, them->GetIPString());
148                                 x = reason.find("%ip%");
149                         }
150
151                         ConfEntry->stats_hits++;
152
153                         switch (ConfEntry->banaction)
154                         {
155                                 case DNSBLConfEntry::I_KILL:
156                                 {
157                                         ServerInstance->Users->QuitUser(them, "Killed (" + reason + ")");
158                                         break;
159                                 }
160                                 case DNSBLConfEntry::I_MARK:
161                                 {
162                                         if (!ConfEntry->ident.empty())
163                                         {
164                                                 them->WriteNotice("Your ident has been set to " + ConfEntry->ident + " because you matched " + reason);
165                                                 them->ChangeIdent(ConfEntry->ident);
166                                         }
167
168                                         if (!ConfEntry->host.empty())
169                                         {
170                                                 them->WriteNotice("Your host has been set to " + ConfEntry->host + " because you matched " + reason);
171                                                 them->ChangeDisplayedHost(ConfEntry->host);
172                                         }
173
174                                         nameExt.set(them, ConfEntry->name);
175                                         break;
176                                 }
177                                 case DNSBLConfEntry::I_KLINE:
178                                 {
179                                         KLine* kl = new KLine(ServerInstance->Time(), ConfEntry->duration, ServerInstance->Config->ServerName.c_str(), reason.c_str(),
180                                                         "*", them->GetIPString());
181                                         if (ServerInstance->XLines->AddLine(kl,NULL))
182                                         {
183                                                 ServerInstance->SNO->WriteToSnoMask('x', "K-line added due to DNSBL match on *@%s to expire in %s (on %s): %s",
184                                                         them->GetIPString().c_str(), InspIRCd::DurationString(kl->duration).c_str(),
185                                                         InspIRCd::TimeString(kl->expiry).c_str(), reason.c_str());
186                                                 ServerInstance->XLines->ApplyLines();
187                                         }
188                                         else
189                                         {
190                                                 delete kl;
191                                                 return;
192                                         }
193                                         break;
194                                 }
195                                 case DNSBLConfEntry::I_GLINE:
196                                 {
197                                         GLine* gl = new GLine(ServerInstance->Time(), ConfEntry->duration, ServerInstance->Config->ServerName.c_str(), reason.c_str(),
198                                                         "*", them->GetIPString());
199                                         if (ServerInstance->XLines->AddLine(gl,NULL))
200                                         {
201                                                 ServerInstance->SNO->WriteToSnoMask('x', "G-line added due to DNSBL match on *@%s to expire in %s (on %s): %s",
202                                                         them->GetIPString().c_str(), InspIRCd::DurationString(gl->duration).c_str(),
203                                                         InspIRCd::TimeString(gl->expiry).c_str(), reason.c_str());
204                                                 ServerInstance->XLines->ApplyLines();
205                                         }
206                                         else
207                                         {
208                                                 delete gl;
209                                                 return;
210                                         }
211                                         break;
212                                 }
213                                 case DNSBLConfEntry::I_ZLINE:
214                                 {
215                                         ZLine* zl = new ZLine(ServerInstance->Time(), ConfEntry->duration, ServerInstance->Config->ServerName.c_str(), reason.c_str(),
216                                                         them->GetIPString());
217                                         if (ServerInstance->XLines->AddLine(zl,NULL))
218                                         {
219                                                 ServerInstance->SNO->WriteToSnoMask('x', "Z-line added due to DNSBL match on %s to expire in %s (on %s): %s",
220                                                         them->GetIPString().c_str(), InspIRCd::DurationString(zl->duration).c_str(),
221                                                         InspIRCd::TimeString(zl->expiry).c_str(), reason.c_str());
222                                                 ServerInstance->XLines->ApplyLines();
223                                         }
224                                         else
225                                         {
226                                                 delete zl;
227                                                 return;
228                                         }
229                                         break;
230                                 }
231                                 case DNSBLConfEntry::I_UNKNOWN:
232                                 default:
233                                         break;
234                         }
235
236                         ServerInstance->SNO->WriteGlobalSno('d', "Connecting user %s (%s) detected as being on the '%s' DNS blacklist with result %d",
237                                 them->GetFullRealHost().c_str(), them->GetIPString().c_str(), ConfEntry->name.c_str(), result);
238                 }
239                 else
240                         ConfEntry->stats_misses++;
241         }
242
243         void OnError(const DNS::Query *q) CXX11_OVERRIDE
244         {
245                 LocalUser* them = IS_LOCAL(ServerInstance->FindUUID(theiruid));
246                 if (!them || them->client_sa != theirsa)
247                         return;
248
249                 int i = countExt.get(them);
250                 if (i)
251                         countExt.set(them, i - 1);
252
253                 if (q->error == DNS::ERROR_NO_RECORDS || q->error == DNS::ERROR_DOMAIN_NOT_FOUND)
254                 {
255                         ConfEntry->stats_misses++;
256                         return;
257                 }
258
259                 ConfEntry->stats_errors++;
260                 ServerInstance->SNO->WriteGlobalSno('d', "An error occurred whilst checking whether %s (%s) is on the '%s' DNS blacklist: %s",
261                         them->GetFullRealHost().c_str(), them->GetIPString().c_str(), ConfEntry->name.c_str(), this->manager->GetErrorStr(q->error).c_str());
262         }
263 };
264
265 typedef std::vector<reference<DNSBLConfEntry> > DNSBLConfList;
266
267 class ModuleDNSBL : public Module, public Stats::EventListener
268 {
269         DNSBLConfList DNSBLConfEntries;
270         dynamic_reference<DNS::Manager> DNS;
271         LocalStringExt nameExt;
272         LocalIntExt countExt;
273
274         /*
275          *      Convert a string to EnumBanaction
276          */
277         DNSBLConfEntry::EnumBanaction str2banaction(const std::string &action)
278         {
279                 if (stdalgo::string::equalsci(action, "kill"))
280                         return DNSBLConfEntry::I_KILL;
281                 if (stdalgo::string::equalsci(action, "kline"))
282                         return DNSBLConfEntry::I_KLINE;
283                 if (stdalgo::string::equalsci(action, "zline"))
284                         return DNSBLConfEntry::I_ZLINE;
285                 if (stdalgo::string::equalsci(action, "gline"))
286                         return DNSBLConfEntry::I_GLINE;
287                 if (stdalgo::string::equalsci(action, "mark"))
288                         return DNSBLConfEntry::I_MARK;
289                 return DNSBLConfEntry::I_UNKNOWN;
290         }
291  public:
292         ModuleDNSBL()
293                 : Stats::EventListener(this)
294                 , DNS(this, "DNS")
295                 , nameExt("dnsbl_match", ExtensionItem::EXT_USER, this)
296                 , countExt("dnsbl_pending", ExtensionItem::EXT_USER, this)
297         {
298         }
299
300         void init() CXX11_OVERRIDE
301         {
302                 ServerInstance->SNO->EnableSnomask('d', "DNSBL");
303         }
304
305         void Prioritize() CXX11_OVERRIDE
306         {
307                 Module* corexline = ServerInstance->Modules->Find("core_xline");
308                 ServerInstance->Modules->SetPriority(this, I_OnSetUserIP, PRIORITY_AFTER, corexline);
309         }
310
311         Version GetVersion() CXX11_OVERRIDE
312         {
313                 return Version("Allows the server administrator to check the IP address of connecting users against a DNSBL.", VF_VENDOR);
314         }
315
316         /** Fill our conf vector with data
317          */
318         void ReadConfig(ConfigStatus& status) CXX11_OVERRIDE
319         {
320                 DNSBLConfList newentries;
321
322                 ConfigTagList dnsbls = ServerInstance->Config->ConfTags("dnsbl");
323                 for(ConfigIter i = dnsbls.first; i != dnsbls.second; ++i)
324                 {
325                         ConfigTag* tag = i->second;
326                         reference<DNSBLConfEntry> e = new DNSBLConfEntry();
327
328                         e->name = tag->getString("name");
329                         e->ident = tag->getString("ident");
330                         e->host = tag->getString("host");
331                         e->reason = tag->getString("reason", "Your IP has been blacklisted.", 1);
332                         e->domain = tag->getString("domain");
333
334                         if (stdalgo::string::equalsci(tag->getString("type"), "bitmask"))
335                         {
336                                 e->type = DNSBLConfEntry::A_BITMASK;
337                                 e->bitmask = tag->getUInt("bitmask", 0, 0, UINT_MAX);
338                         }
339                         else
340                         {
341                                 memset(e->records, 0, sizeof(e->records));
342                                 e->type = DNSBLConfEntry::A_RECORD;
343                                 irc::portparser portrange(tag->getString("records"), false);
344                                 long item = -1;
345                                 while ((item = portrange.GetToken()))
346                                         e->records[item] = 1;
347                         }
348
349                         e->banaction = str2banaction(tag->getString("action"));
350                         e->duration = tag->getDuration("duration", 60, 1);
351
352                         /* Use portparser for record replies */
353
354                         /* yeah, logic here is a little messy */
355                         if ((e->bitmask <= 0) && (DNSBLConfEntry::A_BITMASK == e->type))
356                         {
357                                 throw ModuleException("Invalid <dnsbl:bitmask> at " + tag->getTagLocation());
358                         }
359                         else if (e->name.empty())
360                         {
361                                 throw ModuleException("Empty <dnsbl:name> at " + tag->getTagLocation());
362                         }
363                         else if (e->domain.empty())
364                         {
365                                 throw ModuleException("Empty <dnsbl:domain> at " + tag->getTagLocation());
366                         }
367                         else if (e->banaction == DNSBLConfEntry::I_UNKNOWN)
368                         {
369                                 throw ModuleException("Unknown <dnsbl:action> at " + tag->getTagLocation());
370                         }
371                         else
372                         {
373                                 /* add it, all is ok */
374                                 newentries.push_back(e);
375                         }
376                 }
377
378                 DNSBLConfEntries.swap(newentries);
379         }
380
381         void OnSetUserIP(LocalUser* user) CXX11_OVERRIDE
382         {
383                 if (user->exempt || user->quitting || !DNS)
384                         return;
385
386                 // Clients can't be in a DNSBL if they aren't connected via IPv4 or IPv6.
387                 if (user->client_sa.family() != AF_INET && user->client_sa.family() != AF_INET6)
388                         return;
389
390                 if (user->MyClass)
391                 {
392                         if (!user->MyClass->config->getBool("usednsbl", true))
393                                 return;
394                 }
395                 else
396                 {
397                         ServerInstance->Logs->Log(MODNAME, LOG_DEBUG, "User has no connect class in OnSetUserIP");
398                         return;
399                 }
400
401                 std::string reversedip;
402                 if (user->client_sa.family() == AF_INET)
403                 {
404                         unsigned int a, b, c, d;
405                         d = (unsigned int) (user->client_sa.in4.sin_addr.s_addr >> 24) & 0xFF;
406                         c = (unsigned int) (user->client_sa.in4.sin_addr.s_addr >> 16) & 0xFF;
407                         b = (unsigned int) (user->client_sa.in4.sin_addr.s_addr >> 8) & 0xFF;
408                         a = (unsigned int) user->client_sa.in4.sin_addr.s_addr & 0xFF;
409
410                         reversedip = ConvToStr(d) + "." + ConvToStr(c) + "." + ConvToStr(b) + "." + ConvToStr(a);
411                 }
412                 else if (user->client_sa.family() == AF_INET6)
413                 {
414                         const unsigned char* ip = user->client_sa.in6.sin6_addr.s6_addr;
415
416                         std::string buf = BinToHex(ip, 16);
417                         for (std::string::const_reverse_iterator it = buf.rbegin(); it != buf.rend(); ++it)
418                         {
419                                 reversedip.push_back(*it);
420                                 reversedip.push_back('.');
421                         }
422                         reversedip.erase(reversedip.length() - 1, 1);
423                 }
424                 else
425                         return;
426
427                 ServerInstance->Logs->Log(MODNAME, LOG_DEBUG, "Reversed IP %s -> %s", user->GetIPString().c_str(), reversedip.c_str());
428
429                 countExt.set(user, DNSBLConfEntries.size());
430
431                 // For each DNSBL, we will run through this lookup
432                 for (unsigned i = 0; i < DNSBLConfEntries.size(); ++i)
433                 {
434                         // Fill hostname with a dnsbl style host (d.c.b.a.domain.tld)
435                         std::string hostname = reversedip + "." + DNSBLConfEntries[i]->domain;
436
437                         /* now we'd need to fire off lookups for `hostname'. */
438                         DNSBLResolver *r = new DNSBLResolver(*this->DNS, this, nameExt, countExt, hostname, user, DNSBLConfEntries[i]);
439                         try
440                         {
441                                 this->DNS->Process(r);
442                         }
443                         catch (DNS::Exception &ex)
444                         {
445                                 delete r;
446                                 ServerInstance->Logs->Log(MODNAME, LOG_DEBUG, ex.GetReason());
447                         }
448
449                         if (user->quitting)
450                                 break;
451                 }
452         }
453
454         ModResult OnSetConnectClass(LocalUser* user, ConnectClass* myclass) CXX11_OVERRIDE
455         {
456                 std::string dnsbl;
457                 if (!myclass->config->readString("dnsbl", dnsbl))
458                         return MOD_RES_PASSTHRU;
459
460                 std::string* match = nameExt.get(user);
461                 if (!match)
462                 {
463                         ServerInstance->Logs->Log("CONNECTCLASS", LOG_DEBUG, "The %s connect class is not suitable as it requires a DNSBL mark",
464                                         myclass->GetName().c_str());
465                         return MOD_RES_DENY;
466                 }
467
468                 if (!InspIRCd::Match(*match, dnsbl))
469                 {
470                         ServerInstance->Logs->Log("CONNECTCLASS", LOG_DEBUG, "The %s connect class is not suitable as the DNSBL mark (%s) does not match %s",
471                                         myclass->GetName().c_str(), match->c_str(), dnsbl.c_str());
472                         return MOD_RES_DENY;
473                 }
474
475                 return MOD_RES_PASSTHRU;
476         }
477
478         ModResult OnCheckReady(LocalUser *user) CXX11_OVERRIDE
479         {
480                 if (countExt.get(user))
481                         return MOD_RES_DENY;
482                 return MOD_RES_PASSTHRU;
483         }
484
485         ModResult OnStats(Stats::Context& stats) CXX11_OVERRIDE
486         {
487                 if (stats.GetSymbol() != 'd')
488                         return MOD_RES_PASSTHRU;
489
490                 unsigned long total_hits = 0;
491                 unsigned long total_misses = 0;
492                 unsigned long total_errors = 0;
493                 for (std::vector<reference<DNSBLConfEntry> >::const_iterator i = DNSBLConfEntries.begin(); i != DNSBLConfEntries.end(); ++i)
494                 {
495                         total_hits += (*i)->stats_hits;
496                         total_misses += (*i)->stats_misses;
497                         total_errors += (*i)->stats_errors;
498
499                         stats.AddRow(304, InspIRCd::Format("DNSBLSTATS \"%s\" had %lu hits, %lu misses, and %lu errors",
500                                 (*i)->name.c_str(), (*i)->stats_hits, (*i)->stats_misses, (*i)->stats_errors));
501                 }
502
503                 stats.AddRow(304, "DNSBLSTATS Total hits: " + ConvToStr(total_hits));
504                 stats.AddRow(304, "DNSBLSTATS Total misses: " + ConvToStr(total_misses));
505                 stats.AddRow(304, "DNSBLSTATS Total errors: " + ConvToStr(total_errors));
506                 return MOD_RES_PASSTHRU;
507         }
508 };
509
510 MODULE_INIT(ModuleDNSBL)