E-MailRelay
gdnsblock.cpp
Go to the documentation of this file.
1//
2// Copyright (C) 2001-2023 Graeme Walker <graeme_walker@users.sourceforge.net>
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program. If not, see <http://www.gnu.org/licenses/>.
16// ===
17///
18/// \file gdnsblock.cpp
19///
20
21#include "gdef.h"
22#include "gdnsblock.h"
23#include "gdnsmessage.h"
24#include "gresolver.h"
25#include "gnameservers.h"
26#include "glocal.h"
27#include "gstr.h"
28#include "gtest.h"
29#include "gassert.h"
30#include "glog.h"
31#include <sstream>
32#include <algorithm>
33#include <cstdlib>
34
35namespace GNet
36{
37 namespace DnsBlockImp
38 {
39 static constexpr G::string_view default_timeout_ms {"5000",4U} ;
40 static constexpr unsigned int default_threshold {1U} ;
41
42 struct HostList /// A streamable adaptor for a list of addresses.
43 {
44 const std::vector<Address> & m_list ;
45 friend inline std::ostream & operator<<( std::ostream & stream , const HostList & list )
46 {
47 const char * sep = "" ;
48 for( auto p = list.m_list.begin() ; p != list.m_list.end() ; ++p , sep = " " )
49 {
50 stream << sep << (*p).hostPartString() ;
51 }
52 return stream ;
53 }
54 } ;
55 template <typename T, typename P>
56 G::StringArray server_names_if( T p , T end , P pred )
57 {
58 G::StringArray result ;
59 for( ; p != end ; ++p )
60 {
61 if( pred(*p) )
62 result.push_back( (*p).server() ) ;
63 }
64 return result ;
65 }
66 }
67}
68
70 m_callback(callback) ,
71 m_es(es) ,
72 m_timer(*this,&DnsBlock::onTimeout,es) ,
73 m_threshold(1U) ,
74 m_allow_on_timeout(true) ,
75 m_dns_server(Address::defaultAddress()) ,
76 m_timeout(0) ,
77 m_id_base(0U)
78{
79 if( !config.empty() )
80 configure( config ) ;
81}
82
83void GNet::DnsBlock::checkConfig( const std::string & config )
84{
85 try
86 {
87 configureImp( config , nullptr ) ;
88 }
89 catch( std::exception & e )
90 {
91 throw ConfigError( e.what() ) ;
92 }
93}
94
96{
97 configureImp( config , this ) ;
98}
99
100void GNet::DnsBlock::configureImp( G::string_view config , DnsBlock * dnsblock_p )
101{
102 // allow old format
103 // tcp-address,timeout,threshold,domain[,domain...]
104 // or new
105 // domain[,domain...[,threshold[,timeout[,tcp-address]]]]
106
107 G::StringArray list = G::Str::splitIntoFields( config , ',' ) ;
108 if( list.empty() )
109 throw BadFieldCount() ;
110
111 if( list[0].empty() || !isDomain(list[0]) )
112 {
113 // old format...
114 if( list.size() < 4U )
115 throw BadFieldCount() ;
116
117 Address dns_server = nameServerAddress( list[0] ) ;
118 unsigned int threshold = G::Str::toUInt( list[2] ) ;
119 bool allow_on_timeout = threshold == 0U || isPositive( list[1] ) ;
120 unsigned int timeout_ms = ms( list[1] ) ;
121
122 list.erase( list.begin() , list.begin()+3U ) ;
123 if( dnsblock_p )
124 dnsblock_p->configure( dns_server , threshold , allow_on_timeout , G::TimeInterval(0U,timeout_ms*1000U) , list ) ;
125 }
126 else
127 {
128 // new format...
129 namespace imp = DnsBlockImp ;
130 std::size_t domains = 0U ;
131 for( std::size_t i = 0U ; i < list.size() && isDomain(list[i]) ; i++ )
132 domains++ ;
133
134 auto p = std::next( list.begin() , domains ) ;
135 unsigned int threshold = p == list.end() ? imp::default_threshold : G::Str::toUInt(*p++) ;
136 bool positive_timeout = isPositive( p == list.end() ? imp::default_timeout_ms : *p ) ;
137 unsigned int timeout_ms = ms( p == list.end() ? imp::default_timeout_ms : *p++ ) ;
138 Address dns_server = nameServerAddress( p == list.end() ? std::string() : *p++ ) ;
139 bool allow_on_timeout = positive_timeout || threshold == 0U ;
140 if( p != list.end() )
141 throw ConfigError( "unused fields" ) ;
142
143 list.resize( domains ) ;
144 if( dnsblock_p )
145 dnsblock_p->configure( dns_server , threshold , allow_on_timeout , G::TimeInterval(0U,timeout_ms*1000U) , list ) ;
146 }
147}
148
149void GNet::DnsBlock::configure( const Address & dns_server , unsigned int threshold ,
150 bool allow_on_timeout , G::TimeInterval timeout , const G::StringArray & servers )
151{
152 m_servers = servers ;
153 m_threshold = static_cast<std::size_t>(threshold) ;
154 m_allow_on_timeout = allow_on_timeout ;
155 m_dns_server = dns_server ;
156 m_timeout = timeout ;
157}
158
159GNet::Address GNet::DnsBlock::nameServerAddress()
160{
161 std::vector<Address> list = GNet::nameservers() ;
162 return list.empty() ? Address::loopback( Address::Family::ipv4 , 53U ) : list[0] ;
163}
164
165GNet::Address GNet::DnsBlock::nameServerAddress( const std::string & s )
166{
167 return s.empty() ? nameServerAddress() : Address::parse(s,Address::NotLocal()) ;
168}
169
170bool GNet::DnsBlock::isDomain( G::string_view s ) noexcept
171{
172 // we need to distinguish between eg. "127.0.0.1" as an IP address and
173 // "127.0.0.com" as a domain -- all top-level domains are non-numeric
174 if( G::Str::isNumeric(s,true) ) return false ;
175 G::string_view tld = G::Str::tailView( s , s.rfind('.') ) ;
176 return tld.empty() || ( G::Str::isSimple(tld) && !G::Str::isNumeric(tld) ) ;
177}
178
179bool GNet::DnsBlock::isPositive( G::string_view s ) noexcept
180{
181 return s.empty() || s[0] != '-' ;
182}
183
184unsigned int GNet::DnsBlock::ms( G::string_view s )
185{
186 if( !s.empty() && s[s.size()-1U] == 's' )
187 return 1000U * static_cast<unsigned int>( std::abs(G::Str::toInt(s.substr(0U,s.size()-1U))) ) ;
188 else
189 return static_cast<unsigned int>( std::abs(G::Str::toInt(s)) ) ;
190}
191
192void GNet::DnsBlock::start( const Address & address )
193{
194 G_DEBUG( "GNet::DnsBlock::start: dns-server=" << m_dns_server.displayString() << " "
195 << "threshold=" << m_threshold << " "
196 << "timeout=" << m_timeout << "(" << m_allow_on_timeout << ") "
197 << "address=" << address.hostPartString() << " "
198 << "servers=[" << G::Str::join(",",m_servers) << "]" ) ;
199
200 m_result.reset( m_threshold , address ) ;
201
202 // dont block connections from local addresses
203 bool is_local = address.isLoopback() || address.isUniqueLocal() || address.isLinkLocal() ;
204 if( G::Test::enabled("dns-block-allow-local") ) is_local = false ;
205 if( m_servers.empty() || is_local )
206 {
207 m_timer.startTimer( 0 ) ;
208 return ;
209 }
210
211 // re-base the sequence number if necessary
212 static unsigned int id_generator = 10 ;
213 if( (id_generator+m_servers.size()) > 65535U )
214 id_generator = 10 ;
215
216 // create a socket to receive responses
217 int protocol = 0 ;
218 DatagramSocket::Config datagram_socket_config ;
219 m_socket_ptr = std::make_unique<DatagramSocket>( m_dns_server.family() , protocol , datagram_socket_config ) ;
220 m_socket_ptr->addReadHandler( *this , m_es ) ;
221
222 // send a DNS query to each configured server
223 std::string prefix = queryString( address ) ; // eg. "1.0.0.127"
224 unsigned int id = m_id_base = id_generator ;
225 for( G::StringArray::const_iterator server_p = m_servers.begin() ; server_p != m_servers.end() ;
226 ++server_p , id++ , id_generator++ )
227 {
228 std::string server = G::Str::trimmed( *server_p , G::Str::ws() ) ;
229
230 m_result.add( DnsBlockServerResult(server) ) ;
231
232 const char * type = address.family() == Address::Family::ipv4 ? "A" : "AAAA" ;
233 DnsMessage message = DnsMessage::request( type , std::string(prefix).append(1U,'.').append(server) , id ) ;
234 G_DEBUG( "GNet::DnsBlock::start: sending [" << prefix << "."
235 << server << "] to [" << m_dns_server.displayString() << "]: id " << id ) ;
236
237 ssize_t rc = m_socket_ptr->writeto( message.p() , message.n() , m_dns_server ) ;
238 if( rc < 0 || static_cast<std::size_t>(rc) != message.n() )
239 throw SendError( m_socket_ptr->reason() ) ;
240 }
241 m_timer.startTimer( m_timeout ) ;
242}
243
245{
246 return m_timer.active() ;
247}
248
249void GNet::DnsBlock::readEvent()
250{
251 static std::vector<char> buffer;
252 buffer.resize( 4096U ) ; // 512 in RFC-1035 4.2.1
253 ssize_t rc = m_socket_ptr->read( &buffer[0] , buffer.size() ) ;
254 if( rc <= 0 || static_cast<std::size_t>(rc) >= buffer.size() )
255 throw BadDnsResponse() ;
256 buffer.resize( static_cast<std::size_t>(rc) ) ;
257
258 DnsMessage message( buffer ) ;
259 if( !message.valid() || !message.QR() || message.ID() < m_id_base ||
260 message.ID() >= (m_id_base+m_servers.size()) || message.RCODE() > 5 )
261 {
262 G_WARNING( "GNet::DnsBlock::readEvent: invalid dns response: qr=" << message.QR()
263 << " rcode=" << message.RCODE() << " id=" << message.ID() ) ;
264 return ;
265 }
266
267 m_result.at(message.ID()-m_id_base).set( message.addresses() ) ;
268
269 std::size_t server_count = m_result.list().size() ;
270 std::size_t responder_count = countResponders( m_result.list() ) ;
271 std::size_t laggard_count = server_count - responder_count ;
272 std::size_t deny_count = countDeniers( m_result.list() ) ;
273
274 G_ASSERT( laggard_count < server_count ) ;
275
276 G_DEBUG( "GNet::DnsBlock::readEvent: id=" << message.ID() << " rcode=" << message.RCODE()
277 << (message.ANCOUNT()?" deny ":" allow ")
278 << "got=" << responder_count << "/" << server_count << " deny-count=" << deny_count << "/" << m_threshold ) ;
279
280 bool finished = m_timer.active() && (
281 responder_count == server_count ||
282 ( m_threshold && deny_count >= m_threshold ) ||
283 ( m_threshold && (deny_count+laggard_count) < m_threshold ) ) ;
284
285 if( finished )
286 {
287 m_socket_ptr.reset() ;
288 m_timer.cancelTimer() ;
289 m_result.type() = ( m_threshold && deny_count >= m_threshold ) ?
290 DnsBlockResult::Type::Deny :
291 DnsBlockResult::Type::Allow ;
292 m_callback.onDnsBlockResult( m_result ) ;
293 }
294}
295
296void GNet::DnsBlock::onTimeout()
297{
298 m_socket_ptr.reset() ;
299 m_result.type() = m_result.list().empty() ?
300 ( m_servers.empty() ? DnsBlockResult::Type::Inactive : DnsBlockResult::Type::Local ) :
301 ( m_allow_on_timeout ? DnsBlockResult::Type::TimeoutAllow : DnsBlockResult::Type::TimeoutDeny ) ;
302 m_callback.onDnsBlockResult( m_result ) ;
303}
304
305std::string GNet::DnsBlock::queryString( const Address & address )
306{
307 return address.queryString() ;
308}
309
311{
312 using namespace DnsBlockImp ;
313 if( m_type == Type::Local )
314 {
315 G_LOG( "GNet::DnsBlockResult::log: dnsbl: not checking local address [" << m_address.hostPartString() << "]" ) ;
316 }
317 else if( m_type != Type::Inactive )
318 {
319 for( const auto & result : m_list )
320 {
321 std::ostringstream ss ;
322 ss << "address [" << m_address.hostPartString() << "] " ;
323 if( result.valid() && result.addresses().empty() )
324 ss << "allowed by [" << result.server() << "]" ;
325 else if( result.valid() )
326 ss << "denied by [" << result.server() << "]: " << HostList{result.addresses()} ;
327 else
328 ss << "not checked by [" << result.server() << "]" ;
329 G_LOG( "GNet::DnsBlockResult::log: dnsbl: " << ss.str() ) ;
330 }
331 }
332}
333
335{
336 if( m_type == Type::Deny || m_type == Type::TimeoutDeny || m_type == Type::TimeoutAllow )
337 {
338 std::ostringstream ss ;
339 ss << "client address [" << m_address.hostPartString() << "]" ;
340 if( m_type == Type::Deny || m_type == Type::TimeoutDeny )
341 ss << " blocked" ;
342 if( m_type == Type::TimeoutDeny || m_type == Type::TimeoutAllow )
343 ss << ": timeout: no answer from [" << G::Str::join("] [",laggards()) << "]" ;
344 else
345 ss << " by [" << G::Str::join("] [",deniers()) << "]" ;
346 G_WARNING( "GNet::DnsBlockResult::log: dnsbl: " << ss.str() ) ;
347 }
348}
349
351{
352 return m_type == Type::Inactive || m_type == Type::Local || m_type == Type::TimeoutAllow || m_type == Type::Allow ;
353}
354
355#ifndef G_LIB_SMALL
357{
358 return !allow() ;
359}
360#endif
361
362std::size_t GNet::DnsBlock::countResponders( const ResultList & list )
363{
364 return static_cast<std::size_t>( std::count_if( list.begin() , list.end() ,
365 [](const DnsBlockServerResult & r_){return r_.valid();} ) ) ;
366}
367
368std::size_t GNet::DnsBlock::countDeniers( const ResultList & list )
369{
370 return static_cast<std::size_t>( std::count_if( list.begin() , list.end() ,
371 [](const DnsBlockServerResult & r_){return r_.valid() && !r_.addresses().empty();} ) ) ;
372}
373
375{
376 return DnsBlockImp::server_names_if( m_list.begin() , m_list.end() ,
377 [](const DnsBlockServerResult & r_){return r_.valid() && !r_.addresses().empty();} ) ;
378}
379
381{
382 return DnsBlockImp::server_names_if( m_list.begin() , m_list.end() ,
383 [](const DnsBlockServerResult & r_){return !r_.valid();} ) ;
384}
385
The GNet::Address class encapsulates a TCP/UDP transport address.
Definition: gaddress.h:62
bool isLinkLocal() const
Returns true if this is a link-local address.
Definition: gaddress.cpp:276
static Address loopback(Family, unsigned int port=0U)
Returns a loopback address.
Definition: gaddress.cpp:210
bool isUniqueLocal() const
Returns true if this is a locally administered address.
Definition: gaddress.cpp:296
std::string hostPartString() const
Returns a printable string that represents the network address.
Definition: gaddress.cpp:367
bool isLoopback() const
Returns true if this is a loopback address.
Definition: gaddress.cpp:258
Family family() const noexcept
Returns the address family enumeration.
Definition: gaddress.cpp:476
A callback interface for GNet::DnsBlock.
Definition: gdnsblock.h:218
void warn() const
Emits warnings.
Definition: gdnsblock.cpp:334
bool allow() const
Returns true if the type is Inactive, Local, TimeoutAllow or Allow.
Definition: gdnsblock.cpp:350
G::StringArray laggards() const
Returns the list of slow or unresponsive servers.
Definition: gdnsblock.cpp:380
G::StringArray deniers() const
Returns the list of denying servers.
Definition: gdnsblock.cpp:374
void log() const
Logs the results.
Definition: gdnsblock.cpp:310
bool deny() const
Returns true if the type is TimeoutDeny or Deny.
Definition: gdnsblock.cpp:356
A result structure for one DNSBL server.
Definition: gdnsblock.h:49
Implements DNS blocklisting, as per RFC-5782.
Definition: gdnsblock.h:146
static void checkConfig(const std::string &)
Checks the configure() string, throwing on error.
Definition: gdnsblock.cpp:83
void start(const Address &)
Starts an asychronous check on the given address.
Definition: gdnsblock.cpp:192
void configure(const Address &dns_server, unsigned int threshold, bool allow_on_timeout, G::TimeInterval timeout, const G::StringArray &servers)
Configures the object after construction.
Definition: gdnsblock.cpp:149
DnsBlock(DnsBlockCallback &, ExceptionSink, G::string_view config={})
Constructor.
Definition: gdnsblock.cpp:69
bool busy() const
Returns true after start() and before the completion callback.
Definition: gdnsblock.cpp:244
A DNS message parser, with static factory functions for message composition.
Definition: gdnsmessage.h:56
static DnsMessage request(const std::string &type, const std::string &hostname, unsigned int id=0U)
Factory function for a request message of the give type ("A", "AAAA", etc).
A tuple containing an ExceptionHandler interface pointer and a bound 'exception source' pointer.
static string_view tailView(string_view in, std::size_t pos, string_view default_={}) noexcept
Like tail() but returning a view into the input string.
Definition: gstr.cpp:1340
static unsigned int toUInt(string_view s)
Converts string 's' to an unsigned int.
Definition: gstr.cpp:651
static bool isNumeric(string_view s, bool allow_minus_sign=false) noexcept
Returns true if every character is a decimal digit.
Definition: gstr.cpp:403
static int toInt(string_view s)
Converts string 's' to an int.
Definition: gstr.cpp:541
static std::string join(string_view sep, const StringArray &strings)
Concatenates an array of strings with separators.
Definition: gstr.cpp:1224
static bool isSimple(string_view s) noexcept
Returns true if every character is alphanumeric or "-" or "_".
Definition: gstr.cpp:426
static void splitIntoFields(string_view in, StringArray &out, char sep, char escape='\0', bool remove_escapes=true)
Splits the string into fields.
Definition: gstr.cpp:1194
static std::string trimmed(const std::string &s, string_view ws)
Returns a trim()med version of s.
Definition: gstr.cpp:343
static string_view ws() noexcept
Returns a string of standard whitespace characters.
Definition: gstr.cpp:1268
static bool enabled() noexcept
Returns true if test features are enabled.
Definition: gtest.cpp:79
An interval between two G::SystemTime values or two G::TimerTime values.
Definition: gdatetime.h:299
A class like c++17's std::string_view.
Definition: gstringview.h:51
Network classes.
Definition: gdef.h:1144
std::vector< std::string > StringArray
A std::vector of std::strings.
Definition: gstringarray.h:30
A configuration structure for GNet::DatagramSocket.
Definition: gsocket.h:418