E-MailRelay
gdnsblock.cpp
Go to the documentation of this file.
1//
2// Copyright (C) 2001-2024 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 std::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 ; // NOLINT cppcoreguidelines-avoid-const-or-ref-data-members
45 friend 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
69GNet::DnsBlock::DnsBlock( DnsBlockCallback & callback , EventState es , std::string_view config ) :
70 m_callback(callback) ,
71 m_es(es) ,
72 m_timer(*this,&DnsBlock::onTimeout,es) ,
73 m_dns_server(Address::defaultAddress())
74{
75 if( !config.empty() )
76 configure( config ) ;
77}
78
79void GNet::DnsBlock::checkConfig( const std::string & config )
80{
81 try
82 {
83 configureImp( config , nullptr ) ;
84 }
85 catch( std::exception & e )
86 {
87 throw ConfigError( e.what() ) ;
88 }
89}
90
91void GNet::DnsBlock::configure( std::string_view config )
92{
93 configureImp( config , this ) ;
94}
95
96void GNet::DnsBlock::configureImp( std::string_view config , DnsBlock * dnsblock_p )
97{
98 // allow old format
99 // tcp-address,timeout,threshold,domain[,domain...]
100 // or new
101 // domain[,domain...[,threshold[,timeout[,tcp-address]]]]
102
103 G::StringArray list = G::Str::splitIntoFields( config , ',' ) ;
104 if( list.empty() )
105 throw BadFieldCount() ;
106
107 if( list[0].empty() || !isDomain(list[0]) )
108 {
109 // old format...
110 if( list.size() < 4U )
111 throw BadFieldCount() ;
112
113 Address dns_server = nameServerAddress( list[0] ) ;
114 unsigned int threshold = G::Str::toUInt( list[2] ) ;
115 bool allow_on_timeout = threshold == 0U || isPositive( list[1] ) ;
116 unsigned int timeout_ms = ms( list[1] ) ;
117
118 list.erase( list.begin() , list.begin()+3U ) ;
119 if( dnsblock_p )
120 dnsblock_p->configure( dns_server , threshold , allow_on_timeout , G::TimeInterval(0U,timeout_ms*1000U) , list ) ;
121 }
122 else
123 {
124 // new format...
125 namespace imp = DnsBlockImp ;
126 std::size_t domains = 0U ;
127 for( std::size_t i = 0U ; i < list.size() && isDomain(list[i]) ; i++ )
128 domains++ ;
129
130 auto p = std::next( list.begin() , domains ) ; // NOLINT narrowing
131 unsigned int threshold = p == list.end() ? imp::default_threshold : G::Str::toUInt(*p++) ;
132 bool positive_timeout = isPositive( p == list.end() ? imp::default_timeout_ms : *p ) ;
133 unsigned int timeout_ms = ms( p == list.end() ? imp::default_timeout_ms : *p++ ) ;
134 Address dns_server = nameServerAddress( p == list.end() ? std::string() : *p++ ) ;
135 bool allow_on_timeout = positive_timeout || threshold == 0U ;
136 if( p != list.end() )
137 throw ConfigError( "unused fields" ) ;
138
139 list.resize( domains ) ;
140 if( dnsblock_p )
141 dnsblock_p->configure( dns_server , threshold , allow_on_timeout , G::TimeInterval(0U,timeout_ms*1000U) , list ) ;
142 }
143}
144
145void GNet::DnsBlock::configure( const Address & dns_server , unsigned int threshold ,
146 bool allow_on_timeout , G::TimeInterval timeout , const G::StringArray & servers )
147{
148 m_servers = servers ;
149 m_threshold = static_cast<std::size_t>(threshold) ;
150 m_allow_on_timeout = allow_on_timeout ;
151 m_dns_server = dns_server ;
152 m_timeout = timeout ;
153}
154
155GNet::Address GNet::DnsBlock::nameServerAddress()
156{
157 std::vector<Address> list = GNet::nameservers() ;
158 return list.empty() ? Address::loopback( Address::Family::ipv4 , 53U ) : list[0] ;
159}
160
161GNet::Address GNet::DnsBlock::nameServerAddress( const std::string & s )
162{
163 return s.empty() ? nameServerAddress() : Address::parse(s,Address::NotLocal()) ;
164}
165
166bool GNet::DnsBlock::isDomain( std::string_view s ) noexcept
167{
168 // we need to distinguish between eg. "127.0.0.1" as an IP address and
169 // "127.0.0.com" as a domain -- all top-level domains are non-numeric
170 if( G::Str::isNumeric(s,true) ) return false ;
171 std::string_view tld = G::Str::tailView( s , s.rfind('.') ) ;
172 return tld.empty() || ( G::Str::isSimple(tld) && !G::Str::isNumeric(tld) ) ;
173}
174
175bool GNet::DnsBlock::isPositive( std::string_view s ) noexcept
176{
177 return s.empty() || s[0] != '-' ;
178}
179
180unsigned int GNet::DnsBlock::ms( std::string_view s )
181{
182 if( !s.empty() && s[s.size()-1U] == 's' )
183 return 1000U * static_cast<unsigned int>( std::abs(G::Str::toInt(s.substr(0U,s.size()-1U))) ) ;
184 else
185 return static_cast<unsigned int>( std::abs(G::Str::toInt(s)) ) ;
186}
187
188void GNet::DnsBlock::start( const Address & address )
189{
190 G_DEBUG( "GNet::DnsBlock::start: dns-server=" << m_dns_server.displayString() << " "
191 << "threshold=" << m_threshold << " "
192 << "timeout=" << m_timeout << "(" << m_allow_on_timeout << ") "
193 << "address=" << address.hostPartString() << " "
194 << "servers=[" << G::Str::join(",",m_servers) << "]" ) ;
195
196 m_result.reset( m_threshold , address ) ;
197
198 // dont block connections from local addresses
199 bool is_local = address.isLoopback() || address.isUniqueLocal() || address.isLinkLocal() ;
200 if( G::Test::enabled("dns-block-allow-local") ) is_local = false ;
201 if( m_servers.empty() || is_local )
202 {
203 m_timer.startTimer( 0 ) ;
204 return ;
205 }
206
207 // re-base the sequence number if necessary
208 static unsigned int id_generator = 10 ;
209 if( (id_generator+m_servers.size()) > 65535U )
210 id_generator = 10 ;
211
212 // create a socket to receive responses
213 int protocol = 0 ;
214 DatagramSocket::Config datagram_socket_config ;
215 m_socket_ptr = std::make_unique<DatagramSocket>( m_dns_server.family() , protocol , datagram_socket_config ) ;
216 m_socket_ptr->addReadHandler( *this , m_es ) ;
217
218 // send a DNS query to each configured server
219 std::string prefix = queryString( address ) ; // eg. "1.0.0.127"
220 unsigned int id = m_id_base = id_generator ;
221 for( G::StringArray::const_iterator server_p = m_servers.begin() ; server_p != m_servers.end() ;
222 ++server_p , id++ , id_generator++ )
223 {
224 std::string server = G::Str::trimmed( *server_p , G::Str::ws() ) ;
225
226 m_result.add( DnsBlockServerResult(server) ) ;
227
228 const char * type = address.family() == Address::Family::ipv4 ? "A" : "AAAA" ;
229 DnsMessage message = DnsMessage::request( type , std::string(prefix).append(1U,'.').append(server) , id ) ;
230 G_DEBUG( "GNet::DnsBlock::start: sending [" << prefix << "."
231 << server << "] to [" << m_dns_server.displayString() << "]: id " << id ) ;
232
233 ssize_t rc = m_socket_ptr->writeto( message.p() , message.n() , m_dns_server ) ;
234 if( rc < 0 || static_cast<std::size_t>(rc) != message.n() )
235 throw SendError( m_socket_ptr->reason() ) ;
236 }
237 m_timer.startTimer( m_timeout ) ;
238}
239
241{
242 return m_timer.active() ;
243}
244
245void GNet::DnsBlock::readEvent()
246{
247 static std::vector<char> buffer;
248 buffer.resize( 4096U ) ; // 512 in RFC-1035 4.2.1
249 ssize_t rc = m_socket_ptr->read( buffer.data() , buffer.size() ) ;
250 if( rc <= 0 || static_cast<std::size_t>(rc) >= buffer.size() )
251 throw BadDnsResponse() ;
252 buffer.resize( static_cast<std::size_t>(rc) ) ;
253
254 DnsMessage message( buffer ) ;
255 if( !message.valid() || !message.QR() || message.ID() < m_id_base ||
256 message.ID() >= (m_id_base+m_servers.size()) || message.RCODE() > 5 )
257 {
258 G_WARNING( "GNet::DnsBlock::readEvent: invalid dns response: qr=" << message.QR()
259 << " rcode=" << message.RCODE() << " id=" << message.ID() ) ;
260 return ;
261 }
262
263 m_result.at(message.ID()-m_id_base).set( message.addresses() ) ;
264
265 std::size_t server_count = m_result.list().size() ;
266 std::size_t responder_count = countResponders( m_result.list() ) ;
267 std::size_t laggard_count = server_count - responder_count ;
268 std::size_t deny_count = countDeniers( m_result.list() ) ;
269
270 G_ASSERT( laggard_count < server_count ) ;
271
272 G_DEBUG( "GNet::DnsBlock::readEvent: id=" << message.ID() << " rcode=" << message.RCODE()
273 << (message.ANCOUNT()?" deny ":" allow ")
274 << "got=" << responder_count << "/" << server_count << " deny-count=" << deny_count << "/" << m_threshold ) ;
275
276 bool finished = m_timer.active() && (
277 responder_count == server_count ||
278 ( m_threshold && deny_count >= m_threshold ) ||
279 ( m_threshold && (deny_count+laggard_count) < m_threshold ) ) ;
280
281 if( finished )
282 {
283 m_socket_ptr.reset() ;
284 m_timer.cancelTimer() ;
285 m_result.type() = ( m_threshold && deny_count >= m_threshold ) ?
286 DnsBlockResult::Type::Deny :
287 DnsBlockResult::Type::Allow ;
288 m_callback.onDnsBlockResult( m_result ) ;
289 }
290}
291
292void GNet::DnsBlock::onTimeout()
293{
294 m_socket_ptr.reset() ;
295 m_result.type() = m_result.list().empty() ?
296 ( m_servers.empty() ? DnsBlockResult::Type::Inactive : DnsBlockResult::Type::Local ) :
297 ( m_allow_on_timeout ? DnsBlockResult::Type::TimeoutAllow : DnsBlockResult::Type::TimeoutDeny ) ;
298 m_callback.onDnsBlockResult( m_result ) ;
299}
300
301std::string GNet::DnsBlock::queryString( const Address & address )
302{
303 return address.queryString() ;
304}
305
307{
308 using namespace DnsBlockImp ;
309 if( m_type == Type::Local )
310 {
311 G_LOG( "GNet::DnsBlockResult::log: dnsbl: not checking local address [" << m_address.hostPartString() << "]" ) ;
312 }
313 else if( m_type != Type::Inactive )
314 {
315 for( const auto & result : m_list )
316 {
317 std::ostringstream ss ;
318 ss << "address [" << m_address.hostPartString() << "] " ;
319 if( result.valid() && result.addresses().empty() )
320 ss << "allowed by [" << result.server() << "]" ;
321 else if( result.valid() )
322 ss << "denied by [" << result.server() << "]: " << HostList{result.addresses()} ;
323 else
324 ss << "not checked by [" << result.server() << "]" ;
325 G_LOG( "GNet::DnsBlockResult::log: dnsbl: " << ss.str() ) ;
326 }
327 }
328}
329
331{
332 if( m_type == Type::Deny || m_type == Type::TimeoutDeny || m_type == Type::TimeoutAllow )
333 {
334 std::ostringstream ss ;
335 ss << "client address [" << m_address.hostPartString() << "]" ;
336 if( m_type == Type::Deny || m_type == Type::TimeoutDeny )
337 ss << " blocked" ;
338 if( m_type == Type::TimeoutDeny || m_type == Type::TimeoutAllow )
339 ss << ": timeout: no answer from [" << G::Str::join("] [",laggards()) << "]" ;
340 else
341 ss << " by [" << G::Str::join("] [",deniers()) << "]" ;
342 G_WARNING( "GNet::DnsBlockResult::log: dnsbl: " << ss.str() ) ;
343 }
344}
345
347{
348 return m_type == Type::Inactive || m_type == Type::Local || m_type == Type::TimeoutAllow || m_type == Type::Allow ;
349}
350
351#ifndef G_LIB_SMALL
353{
354 return !allow() ;
355}
356#endif
357
358std::size_t GNet::DnsBlock::countResponders( const ResultList & list )
359{
360 return static_cast<std::size_t>( std::count_if( list.begin() , list.end() ,
361 [](const DnsBlockServerResult & r_){return r_.valid();} ) ) ;
362}
363
364std::size_t GNet::DnsBlock::countDeniers( const ResultList & list )
365{
366 return static_cast<std::size_t>( std::count_if( list.begin() , list.end() ,
367 [](const DnsBlockServerResult & r_){return r_.valid() && !r_.addresses().empty();} ) ) ;
368}
369
371{
372 return DnsBlockImp::server_names_if( m_list.begin() , m_list.end() ,
373 [](const DnsBlockServerResult & r_){return r_.valid() && !r_.addresses().empty();} ) ;
374}
375
377{
378 return DnsBlockImp::server_names_if( m_list.begin() , m_list.end() ,
379 [](const DnsBlockServerResult & r_){return !r_.valid();} ) ;
380}
381
The GNet::Address class encapsulates a TCP/UDP transport address.
Definition: gaddress.h:63
bool isLinkLocal() const
Returns true if this is a link-local address.
Definition: gaddress.cpp:274
static Address loopback(Family, unsigned int port=0U)
Returns a loopback address.
Definition: gaddress.cpp:208
bool isUniqueLocal() const
Returns true if this is a locally administered address.
Definition: gaddress.cpp:294
std::string hostPartString() const
Returns a printable string that represents the network address.
Definition: gaddress.cpp:365
bool isLoopback() const
Returns true if this is a loopback address.
Definition: gaddress.cpp:256
Family family() const noexcept
Returns the address family enumeration.
Definition: gaddress.cpp:474
A callback interface for GNet::DnsBlock.
Definition: gdnsblock.h:218
void warn() const
Emits warnings.
Definition: gdnsblock.cpp:330
bool allow() const
Returns true if the type is Inactive, Local, TimeoutAllow or Allow.
Definition: gdnsblock.cpp:346
G::StringArray laggards() const
Returns the list of slow or unresponsive servers.
Definition: gdnsblock.cpp:376
G::StringArray deniers() const
Returns the list of denying servers.
Definition: gdnsblock.cpp:370
void log() const
Logs the results.
Definition: gdnsblock.cpp:306
bool deny() const
Returns true if the type is TimeoutDeny or Deny.
Definition: gdnsblock.cpp:352
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:79
void start(const Address &)
Starts an asychronous check on the given address.
Definition: gdnsblock.cpp:188
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:145
DnsBlock(DnsBlockCallback &, EventState, std::string_view config={})
Constructor.
Definition: gdnsblock.cpp:69
bool busy() const
Returns true after start() and before the completion callback.
Definition: gdnsblock.cpp:240
A DNS message parser, with static factory functions for message composition.
Definition: gdnsmessage.h:56
const char * p() const noexcept
Returns the raw data.
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).
std::size_t n() const noexcept
Returns the raw data size.
A lightweight object containing an ExceptionHandler pointer, optional ExceptionSource pointer and opt...
Definition: geventstate.h:131
static int toInt(std::string_view s)
Converts string 's' to an int.
Definition: gstr.cpp:538
static std::string join(std::string_view sep, const StringArray &strings)
Concatenates an array of strings with separators.
Definition: gstr.cpp:1221
static bool isSimple(std::string_view s) noexcept
Returns true if every character is alphanumeric or "-" or "_".
Definition: gstr.cpp:423
static void splitIntoFields(std::string_view in, StringArray &out, char sep, char escape='\0', bool remove_escapes=true)
Splits the string into fields.
Definition: gstr.cpp:1191
static bool isNumeric(std::string_view s, bool allow_minus_sign=false) noexcept
Returns true if every character is a decimal digit.
Definition: gstr.cpp:400
static std::string_view tailView(std::string_view in, std::size_t pos, std::string_view default_={}) noexcept
Like tail() but returning a view into the input string.
Definition: gstr.cpp:1337
static unsigned int toUInt(std::string_view s)
Converts string 's' to an unsigned int.
Definition: gstr.cpp:648
static std::string_view ws() noexcept
Returns a string of standard whitespace characters.
Definition: gstr.cpp:1265
static std::string trimmed(const std::string &s, std::string_view ws)
Returns a trim()med version of s.
Definition: gstr.cpp:343
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:305
Network classes.
Definition: gdef.h:1243
std::vector< std::string > StringArray
A std::vector of std::strings.
Definition: gstringarray.h:30
A configuration structure for GNet::DatagramSocket.
Definition: gsocket.h:433