File Coverage

lib/Net/DNS/Dynamic/Proxyserver.pm
Criterion Covered Total %
statement 85 155 54.8
branch 16 62 25.8
condition 4 19 21.0
subroutine 20 29 68.9
pod 0 13 0.0
total 125 278 44.9


line stmt bran cond sub pod time code
1             package Net::DNS::Dynamic::Proxyserver;
2              
3             our $VERSION = '1.2';
4              
5 3     3   43886 use strict;
  3         6  
  3         75  
6 3     3   15 use warnings;
  3         3  
  3         87  
7              
8 3     3   2300 use Perl6::Junction qw( all any none one );
  3         26467  
  3         230  
9              
10 3     3   3165 use Net::DNS;
  3         330533  
  3         324  
11 3     3   2918 use Net::DNS::Nameserver;
  3         20009  
  3         98  
12              
13 3     3   2897 use POSIX qw( strftime );
  3         25189  
  3         21  
14 3     3   4051 use Carp;
  3         17  
  3         146  
15              
16 3     3   7672 use Moose;
  3         1861837  
  3         37  
17 3     3   25635 use Moose::Util::TypeConstraints;
  3         9  
  3         35  
18              
19             =head1 NAME
20              
21             Net::DNS::Dynamic::Proxyserver - A dynamic DNS proxy-server
22              
23             =head1 DESCRIPTION
24              
25             This proxy-server is able to resolve incoming DNS queries by asking the /etc/hosts file
26             and/or a SQL database. You could run it as a simple proxy-server which just loops
27             through the DNS question/answer to other nameservers. However, it could also be
28             used to build your own dynamic DNS service, share one /etc/hosts file with all PC's in
29             your local network or to deliver different answers for hosts at different locations.
30              
31             For example, if you have a home or office server behind NAT with port-forwarding and
32             want to connect to this server always with the same hostname based on your notebooks
33             location, you could run this dns-proxy to answer to your servers hostname with it's
34             internal RFC1918 IP when you are at home or in the office. When you're at a different
35             location and use an external nameserver, the hostname of your server will be resolved
36             with the "external" IP address (of your router).
37              
38             If you like to build your own dynamic DNS service, you need to write your dynamic IP
39             addresses into a SQL databases and let your DNS proxy-server answer queries from it.
40              
41             =head1 SYNOPSIS
42              
43             my $proxy = Net::DNS::Dynamic::Proxyserver->new(
44              
45             debug => 1,
46              
47             host => '*',
48             port => 53,
49              
50             uid => 65534,
51             gid => 65534,
52            
53             nameservers => [ '127.0.0.1', '192.168.1.110' ],
54             nameservers_port => 53,
55              
56             ask_etc_hosts => { ttl => 3600 },
57              
58             ask_sql => {
59            
60             ttl => 60,
61              
62             dsn => 'DBI:mysql:database=my_database;host=localhost;port=3306',
63             user => 'my_user',
64             pass => 'my_password',
65              
66             statement => "SELECT ip FROM hosts WHERE hostname='{qname}' AND type='{qtype}'"
67             },
68             );
69              
70             $proxy->run();
71              
72             =head1 WORKFLOW
73              
74             At startup, the file /etc/resolv.conf will be read and parsed. All defined nameservers will
75             be used to proxy through queries that can not be answered locally. If you define the 'ask_etc_hosts'
76             argument, then also the file /etc/hosts will be read at startup and will be used as the first
77             resource to answer DNS questions. If you make changes to /etc/hosts, you can send a kernel
78             signal HUP to your script, which will trigger a re-read of this file at run-time. The hosts-file
79             will only answer queries for type 'A' (name to IP) and 'PTR' (IP to name).
80              
81             If you specify the 'ask_sql' argument, the SQL database will be asked in second order, right
82             after a look into the hosts file. The SQL statement will be parsed for every query with the
83             given query name and type. Your statement should return the IP address as the first column
84             in the result-set. Right now, only "forward lookups" are supported (PTR records can not
85             be resolved yet because we'd need a second, different SQL statement for that).
86              
87             Then, if the query could not be answered from the hosts-file and/or the database, the question
88             will be handed over to the nameserves from your /etc/resolv.conf and the answer will be looped
89             trough to the caller.
90              
91             =head1 Arguments to new()
92              
93             The following options may be passed over when creating a new object:
94              
95             =head2 debug Int
96              
97             When the debug option is set to 1 or higher (1-3), this module will print out some
98             helpful debug informations to STDOUT. If you like, redirect the output to a
99             log-file, like so
100              
101             ./my-dns-proxy.pl >>/var/log/my_dns_proxy.log
102              
103             A debug value of 1 prints out some basic action logging. A value of 2 and
104             higher turns on nameserver verbosity, a value of 3 and higher turns on resolver
105             debug output.
106              
107             =head2 host String
108              
109             You can specify the IP address to bind to with this option. If not defined, the
110             server binds to all interfaces.
111              
112             Examples:
113              
114             my $proxy = Net::DNS::Dynamic::Proxyserver->new( host => '127.0.0.1' );
115              
116             my $proxy = Net::DNS::Dynamic::Proxyserver->new( host => '192.168.1.1' );
117              
118             my $proxy = Net::DNS::Dynamic::Proxyserver->new( host => '*' );
119              
120             =head2 port Int
121              
122             The tcp & udp port to run the DNS server under. Default is port 53, which means
123             that you need to start your script as user root (all ports below 1000 need root
124             rights).
125              
126             my $proxy = Net::DNS::Dynamic::Proxyserver->new( port => 5353 );
127              
128             =head2 uid Int
129              
130             The user id to switch to, after the socket has been created. Could be set to
131             the uid of 'nobody' (65534 on some systems).
132              
133             my $proxy = Net::DNS::Dynamic::Proxyserver->new( uid => 65534 );
134              
135             =head2 gid Int
136              
137             The group id to switch to, after the socket has been created. Could be set to
138             the gid of 'nogroup' (65534 on some systems).
139              
140             my $proxy = Net::DNS::Dynamic::Proxyserver->new( gid => 65534 );
141              
142             =head2 nameservers ArrayRef
143              
144             This argument allows to defined one or more nameservers to forward any DNS question
145             which can not be locally answered. Must be an Arrayref of IP addresses.
146              
147             If you do not specify nameservers this way, the file /etc/resolv.conf will be read
148             instead and any nameserver defined there will be used.
149              
150             my $proxy = Net::DNS::Dynamic::Proxyserver->new( nameservers => [ '127.0.0.1', '192.168.1.110' ] );
151              
152             =head2 nameservers_port Int
153              
154             Specify the port of the remote nameservers. By default, this is set to 53 (the standard port),
155             but you can ovewrite it if you run a nameserver on a different port. This port will be used
156             for every nameserver - due to a limitation of Net::DNS::Resolver which cant deal with ports
157             for each individual nameserver.
158              
159             my $proxy = Net::DNS::Dynamic::Proxyserver->new( nameservers_port => 5353 );
160              
161             =head2 ask_etc_hosts HashRef
162              
163             If you'd like to anwer DNS queries from entries in your /etc/hosts file, then
164             define this argument like so:
165              
166             my $proxy = Net::DNS::Dynamic::Proxyserver->new( ask_etc_hosts => { ttl => 3600 } );
167              
168             The only argument that can be passed to 'ask_etc_hosts' is the TTL (time to life) for
169             the response.
170              
171             If 'ask_etc_hosts' is not defined, no queries to /etc/hosts will be made.
172              
173             If you make changes to your /etc/hosts file, you can send your script a
174             signal HUP and it will re-read the file on the fly.
175              
176             =head2 ask_sql HashRef
177              
178             If you'd like to answer DNS queries from entries in your SQL database, then define
179             this argument like so:
180              
181             my $proxy = Net::DNS::Dynamic::Proxyserver->new( ask_sql => {
182            
183             ttl => 60,
184             dsn => 'DBI:mysql:database=db_name;host=localhost;port=3306',
185             user => 'my_user',
186             pass => 'my_password',
187             statement => "SELECT ip FROM hosts WHERE hostname='{qname}' AND type='{qtype}'"
188             } );
189              
190             The 'ttl' specifies the TTL (time to life) for the DNS response. Setting this to a
191             low value will tell the client to ask you again after the TTL time has passed by;
192             which also means some higher load for your dns-proxy-server.
193              
194             The 'dsn' is the 'data source name' for the DBI module. This information is used
195             to connect to your SQL database. You can use every flavour of SQL database that
196             is supported by DBI and a DBD::* module, like MySQL, PostgreSQL, SQLite, Oracle, etc...
197             Please have a look at the manual page of DBI and DBD::* to see how a dsn looks like
198             and which options it could contain.
199              
200             The 'user' and 'pass' is the username and password for the connection to the database. If
201             you use SQLite, just leave the values empty (user => '', pass => ''). Also make sure, the
202             SQLite database file can be accessed (read/write) with the defined uid/gid!
203              
204             The 'statement' is a SELECT statement, which must return the IP address for the
205             given query name (qname) and query type (qtype, like 'A' or 'MX'). The placeholders
206             {qname} and {qtype} will be replaced by the actual query name and type. Your statement
207             must return the IP address as the first column in the result.
208              
209             If 'ask_sql' is not defined, no queries to a database will be made.
210              
211             =cut
212              
213             subtype 'Net.DNS.Dynamic.Proxyserver.ValidSQLArguments'
214             => as 'HashRef'
215             => where { $_->{dsn} && $_->{user} && $_->{pass} && $_->{statement} }
216             => message { "Mandatory elements missing in argument 'ask_sql': dsn, user, pass, statement" };
217              
218             has debug => ( is => 'ro', isa => 'Int', required => 0, default => 0 );
219             has host => ( is => 'ro', isa => 'Str', required => 0, default => '*' );
220             has port => ( is => 'ro', isa => 'Int', required => 0, default => 53 );
221             has uid => ( is => 'ro', isa => 'Int', required => 0 );
222             has gid => ( is => 'ro', isa => 'Int', required => 0 );
223             has ask_etc_hosts => ( is => 'ro', isa => 'HashRef', required => 0 );
224             has ask_sql => ( is => 'ro', isa => 'Net.DNS.Dynamic.Proxyserver.ValidSQLArguments', required => 0 );
225              
226             has addrs => ( is => 'rw', isa => 'HashRef', init_arg => undef );
227             has forwarders => ( is => 'rw', isa => 'ArrayRef', required => 0, init_arg => 'nameservers' );
228             has forwarders_port => ( is => 'ro', isa => 'Int', required => 0, init_arg => 'nameservers_port' );
229             has dbh => ( is => 'rw', isa => 'Object', init_arg => undef );
230              
231             has nameserver => ( is => 'rw', isa => 'Net::DNS::Nameserver', init_arg => undef );
232             has resolver => ( is => 'rw', isa => 'Net::DNS::Resolver', init_arg => undef );
233              
234             sub BUILD {
235 2     2 0 22 my ( $self ) = shift;
236              
237             # initialize signal handlers
238             #
239 2     0   80 $SIG{KILL} = sub { $self->signal_handler(@_) };
  0         0  
240 2     1   34 $SIG{QUIT} = sub { $self->signal_handler(@_) };
  1         7292  
241 2     0   28 $SIG{TERM} = sub { $self->signal_handler(@_) };
  0         0  
242 2     0   28 $SIG{INT} = sub { $self->signal_handler(@_) };
  0         0  
243 2     0   26 $SIG{HUP} = sub { $self->read_config() };
  0         0  
244              
245             # slurp in /etc/hosts and /etc/resolv.conf
246             #
247 2         14 $self->read_config();
248              
249             # initialize nameserver object
250             #
251             my $ns = Net::DNS::Nameserver->new(
252              
253             LocalAddr => $self->host,
254             LocalPort => $self->port,
255 1     1   19376 ReplyHandler => sub { $self->reply_handler(@_); },
256 2 50       74 Verbose => ($self->debug > 1 ? 1 : 0)
257             );
258              
259 2         3412 $self->nameserver( $ns );
260              
261             # initialize resolver object
262             #
263 2         90 my $res = Net::DNS::Resolver->new(
264              
265 2 50 50     4 nameservers => [ @{$self->forwarders} ],
266             port => $self->forwarders_port || 53,
267             recurse => 1,
268             debug => ($self->debug > 2 ? 1 : 0),
269             );
270              
271 2         448 $self->resolver( $res );
272              
273             # change the effective user id and group id
274             #
275 2 50       74 $> = $self->uid if $self->uid;
276 2 50       62 $) = $self->gid if $self->gid;
277             }
278              
279             sub run {
280 1     1 0 2420 my ( $self ) = shift;
281            
282 1         419 $self->log("listening for DNS queries on address " . $self->host . " and port " . $self->port, 1);
283              
284 1 50       49 $self->log("Try a DNS query to your server: dig @" . ($self->host eq '*' ? '127.0.0.1' : $self->host ) . " -p " . $self->port . " -q hostname.domain.com");
285              
286 1         91 $self->nameserver->main_loop;
287             }
288              
289             sub reply_handler {
290 1     1 0 13 my ($self, $qname, $qclass, $qtype, $peerhost,$query,$conn) = @_;
291              
292 1         21 my ($rcode, @ans, @auth, @add);
293              
294 1         281 $self->log("received query from $peerhost: qtype '$qtype', qname '$qname'");
295              
296             # see if we can answer the question from /etc/hosts
297             #
298 1 0 0     73 if ($self->ask_etc_hosts && ($qtype eq 'A' || $qtype eq 'PTR')) {
      33        
299            
300 0 0       0 if (my $ip = $self->query_etc_hosts( $qname, $qtype )) {
301              
302 0         0 $self->log("[/etc/hosts] resolved $qname to $ip NOERROR");
303              
304 0 0       0 my ($ttl, $rdata) = (($self->ask_etc_hosts->{ttl} ? $self->ask_etc_hosts->{ttl} : 3600), $ip );
305            
306 0         0 push @ans, Net::DNS::RR->new("$qname $ttl $qclass $qtype $rdata");
307              
308 0         0 $rcode = "NOERROR";
309            
310 0         0 return ($rcode, \@ans, \@auth, \@add, { aa => 1, ra => 1 });
311             }
312             }
313              
314             # see if we can answer the question from the SQL database
315             #
316 1 50       53 if ($self->ask_sql) {
317            
318 0 0       0 if (my $ip = $self->query_sql( $qname, $qtype )) {
319            
320 0         0 $self->log("[SQL] resolved $qname to $ip NOERROR");
321              
322 0 0       0 my ($ttl, $rdata) = (($self->ask_sql->{ttl} ? $self->ask_sql->{ttl} : 3600), $ip );
323            
324 0         0 push @ans, Net::DNS::RR->new("$qname $ttl $qclass $qtype $rdata");
325              
326 0         0 $rcode = "NOERROR";
327            
328 0         0 return ($rcode, \@ans, \@auth, \@add, { aa => 1, ra => 1 });
329             }
330             }
331            
332             # forward to remote nameserver and loop through the result
333             #
334 1         73 my $answer = $self->resolver->send($qname, $qtype, $qclass);
335              
336 1 50       42188 if ($answer) {
337              
338 1         7 $rcode = $answer->header->rcode;
339 1         50 @ans = $answer->answer;
340 1         19 @auth = $answer->authority;
341 1         11 @add = $answer->additional;
342            
343 1         21 $self->log("[proxy] response from remote resolver: $qname $rcode");
344              
345 1         40 return ($rcode, \@ans, \@auth, \@add);
346             }
347             else {
348              
349 0         0 $self->log("[proxy] can not resolve $qtype $qname - no answer from remote resolver. Sending NXDOMAIN response.");
350              
351 0         0 $rcode = "NXDOMAIN";
352              
353 0         0 return ($rcode, \@ans, \@auth, \@add, { aa => 1, ra => 1 });
354             }
355             }
356              
357             sub log {
358 7     7 0 77 my ( $self, $msg, $force_flag ) = @_;
359            
360 7 100 66     427 print "[" . strftime('%Y-%m-%d %H:%M:%S', localtime(time)) . "] " . $msg . "\n" if $self->debug || $force_flag;
361             }
362              
363             sub read_config {
364 2     2 0 6 my ( $self ) = shift;
365              
366 2         10 $self->forwarders([ $self->parse_resolv_conf() ]); # /etc/resolv.conf
367 2         10 $self->addrs({ $self->parse_etc_hosts() }); # /etc/hosts
368             }
369              
370             sub signal_handler {
371 1     1 0 6 my ( $self, $signal ) = @_;
372              
373 1         24 $self->log("shutting down because of signal $signal");
374              
375 1 50       82 $self->dbh->disconnect() if $self->dbh;
376              
377 1         157 exit;
378             }
379              
380             sub query_etc_hosts {
381 0     0 0 0 my ( $self, $qname, $qtype ) = @_;
382            
383 0 0       0 return $self->search_ip_by_hostname( $qname ) if $qtype eq 'A';
384 0 0       0 return $self->search_hostname_by_ip( $qname ) if $qtype eq 'PTR';
385             }
386              
387             sub search_ip_by_hostname {
388 0     0 0 0 my ( $self, $hostname ) = @_;
389              
390 0         0 foreach my $ip (keys %{$self->addrs}) {
  0         0  
391            
392 0 0       0 if ( any(@{$self->addrs->{$ip}}) eq $hostname ) {
  0         0  
393            
394 0         0 return $ip;
395             }
396             }
397              
398 0         0 return;
399             }
400              
401             sub search_hostname_by_ip {
402 0     0 0 0 my ( $self, $ip ) = @_;
403              
404 0   0     0 $ip = $self->get_in_addr_arpa( $ip ) || return;
405              
406 0 0       0 return $self->addrs->{$ip}->[0] if $self->addrs->{$ip};
407              
408 0         0 return;
409             }
410              
411             sub get_in_addr_arpa {
412 0     0 0 0 my ( $self, $ptr ) = @_;
413              
414             # convert ipv4 -> 10.1.168.192.in-addr.arpa -> 192.168.1.10
415             #
416 0         0 my ($reverse_ip) = ($ptr =~ m!^([\d\.]+)\.in-addr\.arpa$!);
417              
418 0 0       0 return unless $reverse_ip;
419              
420 0         0 my @octets = reverse split(/\./, $reverse_ip);
421              
422 0         0 return join('.', @octets);
423             }
424              
425             sub parse_etc_hosts {
426 2     2 0 8 my ( $self ) = shift;
427              
428 2 50       84 return unless $self->ask_etc_hosts;
429              
430 0         0 $self->log('reading /etc/hosts file');
431              
432 0         0 my %addrs;
433             my %names;
434              
435 0 0       0 open(HOSTS, "/etc/hosts") or croak "cant open /etc/hosts file: $!";
436              
437 0         0 while (<HOSTS>) {
438            
439 0 0       0 next if /^\s*#/; # skip comments
440 0 0       0 next if /^$/; # skip empty lines
441 0         0 s/\s*#.*$//; # delete in-line comments and preceding whitespace
442              
443 0         0 my ($ip, @names) = split;
444              
445 0 0       0 next unless $ip =~ /^[\d\.]+$/; # skip ipv6 adresses
446              
447 0         0 push @{$addrs{$ip}}, @names;
  0         0  
448              
449 0         0 foreach (@names) {
450              
451 0 0       0 croak "The hostname $_ has been defined for more then one IP address!\n" if exists $names{$_};
452              
453 0         0 $names{$_} = $ip;
454             }
455             }
456              
457 0         0 close(HOSTS);
458              
459 0         0 return %addrs;
460             }
461              
462             sub parse_resolv_conf {
463 2     2 0 4 my ( $self ) = shift;
464            
465 2 50       90 return @{$self->forwarders} if $self->forwarders;
  0         0  
466              
467 2         12 $self->log('reading /etc/resolv.conf file');
468              
469 2         4 my @dns_servers;
470            
471 2 50       118 open (RESOLV, "/etc/resolv.conf") || croak "cant open /etc/resolv.conf file: $!";
472            
473 2         68 while (<RESOLV>) {
474            
475 12 100       62 if (/^nameserver\s+([\d\.]+)/) {
476            
477 4         20 push @dns_servers, $1;
478             }
479             }
480              
481 2         18 close (RESOLV);
482            
483 2 50       10 croak "you have not specified a nameserver in /etc/resolv.conf!" unless @dns_servers;
484            
485 2         92 return @dns_servers;
486             }
487              
488             sub query_sql {
489 0     0 0   my ( $self, $qname, $qtype ) = @_;
490              
491 3     3   45692 use DBI;
  3         83151  
  3         1132  
492            
493 0           my $args = $self->ask_sql;
494              
495             # see if we have an open database handle already, which we can re-use
496             #
497 0 0 0       unless ($self->dbh && $self->dbh->ping()) {
498            
499             # connect
500             #
501 0   0       my $dbh = DBI->connect( $args->{dsn}, $args->{user}, $args->{pass} ) || croak "can not connect to database $args->{dsn} $!";
502              
503 0           $self->dbh( $dbh );
504             }
505            
506 0 0         $qname = $self->get_in_addr_arpa( $qname ) if $qtype eq 'PTR';
507              
508             # parse the statement variables
509             #
510 0           $qname =~ s!'!!g;
511 0           $qtype =~ s!'!!g;
512            
513 0           my $statement = $args->{statement};
514            
515 0           $statement =~ s!{qname}!$qname!g;
516 0           $statement =~ s!{qtype}!$qtype!g;
517              
518 0           my $sth = $self->dbh->prepare( $statement );
519              
520 0           $sth->execute();
521              
522             # we expect exact one column to come back from the SQL statement - the IP address of the given hostname and query type
523             #
524 0           my $result = $sth->fetchrow_arrayref();
525            
526 0           return $result->[0];
527             }
528              
529             =head1 AUTHOR
530              
531             Marc Sebastian Jakobs <maja@cpan.org>
532              
533             =head1 COPYRIGHT AND LICENSE
534              
535             Copyright 2009 by Marc Sebastian Jakobs
536              
537             This library is free software, you can redistribute it and/or modify
538             it under the same terms as Perl itself.
539              
540             =cut
541              
542             __PACKAGE__->meta->make_immutable;
543              
544             1;
545