1package POE::Component::Server::DNS;
2$POE::Component::Server::DNS::VERSION = '0.32';
3#ABSTRACT: A non-blocking, concurrent DNS server POE component
4
5use strict;
6use warnings;
7use POE qw(Component::Client::DNS Wheel::ReadWrite Component::Client::DNS::Recursive Wheel::SocketFactory Filter::DNS::TCP);
8use Socket;
9use Net::DNS::RR;
10use IO::Socket::INET;
11
12sub spawn {
13  my $package = shift;
14  my %args = @_;
15  $args{lc $_} = delete $args{$_} for keys %args;
16
17  my $options = delete $args{options};
18
19  my $self = bless \%args, $package;
20
21  $self->{_handlers} = [ ];
22
23  unless ( $self->{no_clients} ) {
24      $self->{_localhost} = Net::DNS::RR->new('localhost. 0 A 127.0.0.1');
25  }
26
27  $self->{session_id} = POE::Session->create(
28	object_states => [
29		$self => { shutdown => '_shutdown', _sock_err_tcp => '_sock_err', },
30		$self => [ qw(_start _dns_incoming _dns_err _dns_response _dns_recursive add_handler del_handler _handled_req _sock_up _sock_err log_event _sock_up_tcp) ],
31	],
32	heap => $self,
33	options => ( $options && ref $options eq 'HASH' ? $options : { } ),
34  )->ID();
35
36  return $self;
37}
38
39sub _start {
40  my ($kernel,$self,$session) = @_[KERNEL,OBJECT,SESSION];
41
42  $self->{session_id} = $session->ID();
43
44  if ( $self->{alias} ) {
45     $kernel->alias_set($self->{alias});
46  }
47  else {
48     $kernel->alias_set("$self");
49     $self->{alias} = "$self";
50  }
51
52  unless ( $self->{no_clients} ) {
53      $self->{resolver_opts} = { } unless $self->{resolver_opts} and ref $self->{resolver_opts} eq 'HASH';
54      delete $self->{resolver_opts}->{Alias};
55      $self->{resolver} = POE::Component::Client::DNS->spawn( Alias => "resolver" . $self->session_id(), %{ $self->{resolver_opts} } );
56#      $self->{recursive}->hints( { event => '_hints' } );
57  }
58
59  $self->{factory} = POE::Wheel::SocketFactory->new(
60	SocketProtocol => 'udp',
61	BindAddress => $self->{address} || INADDR_ANY,
62	BindPort => ( defined $self->{port} ? $self->{port} : 53 ),
63	SuccessEvent   => '_sock_up',
64	FailureEvent   => '_sock_err',
65  );
66
67  $self->{factory_tcp} = POE::Wheel::SocketFactory->new(
68    SocketProtocol => 'tcp',
69    Reuse => 1,
70    BindAddress => $self->{address} || INADDR_ANY,
71    BindPort => ( defined $self->{port} ? $self->{port} : 53 ),
72    SuccessEvent   => '_sock_up_tcp',
73    FailureEvent   => '_sock_err_tcp',
74  );
75
76  undef;
77}
78
79sub _sock_up {
80  my ($kernel,$self,$dns_socket) = @_[KERNEL,OBJECT,ARG0];
81  $self->{_sockport} = ( sockaddr_in( getsockname($dns_socket) ) )[0];
82  delete $self->{factory};
83  $self->{dnsrw} = POE::Wheel::ReadWrite->new(
84	    Handle => $dns_socket,
85	    Driver => DNS::Driver::SendRecv->new(),
86	    Filter => DNS::Filter::UDPDNS->new(),
87	    InputEvent => '_dns_incoming',
88	    ErrorEvent => '_dns_err',
89	);
90  undef;
91}
92
93sub _sock_up_tcp {
94  my ($kernel,$self,$dns_socket, $address, $port) = @_[KERNEL,OBJECT,ARG0, ARG1, ARG2];
95  $address = inet_ntoa($address);
96
97  POE::Session->create(
98	object_states => [
99		$self => { _start => '_socket_success', _stop => '_socket_death' },
100		$self => [ qw( _sock_err _socket_input _socket_death _handled_req _dns_incoming _dns_recursive _dns_response) ],
101	],
102    args => [$dns_socket],
103    heap => { _tcp_sockport => "$address:$port", },
104  );
105
106  undef;
107}
108
109
110sub _socket_death {
111  my $heap = $_[HEAP];
112  if ($heap->{socket_wheel}) {
113    delete $heap->{socket_wheel};
114  }
115}
116
117sub _socket_success {
118  my ($heap,$kernel,$connected_socket) = @_[HEAP, KERNEL, ARG0];
119
120  $heap->{socket_wheel} = POE::Wheel::ReadWrite->new(
121        Handle => $connected_socket,
122        Filter => POE::Filter::DNS::TCP->new(),
123        InputEvent => '_dns_incoming',
124        ErrorEvent => '_sock_err',
125  );
126}
127
128sub _socket_input {
129  my ($heap, $buf) = @_[HEAP, ARG0];
130  warn Dumper $buf;
131  delete $heap->{socket_wheel};
132}
133
134sub _sock_err {
135  my ($operation, $errnum, $errstr, $wheel_id) = @_[ARG0..ARG3];
136  # ErrorEvent may also indicate EOF on a FileHandle by returning operation "read" error 0. For sockets, this means the remote end has closed the connection.
137  return undef if ($operation eq "read" and $errnum == 0);
138  delete $_[OBJECT]->{factory};
139  delete $_[OBJECT]->{"factory_tcp"};
140  die "Wheel $wheel_id generated $operation error $errnum: $errstr\n";
141  undef;
142}
143
144sub session_id {
145  return $_[0]->{session_id};
146}
147
148sub resolver {
149  return $_[0]->{resolver};
150}
151
152sub sockport {
153  return $_[0]->{_sockport};
154}
155
156sub shutdown {
157  my $self = shift;
158  $poe_kernel->post( $self->session_id() => 'shutdown' );
159}
160
161sub _shutdown {
162  my ($kernel,$self) = @_[KERNEL,OBJECT];
163  $kernel->alarm_remove_all();
164  $kernel->alias_remove( $_ ) for $kernel->alias_list( $_[SESSION] );
165  delete $self->{dnsrw};
166  delete $self->{'factory'};
167  delete $self->{'factory_tcp'};
168  unless ( $self->{no_clients} ) {
169      $self->{resolver}->shutdown();
170      #$self->{recursive}->shutdown();
171  }
172  $kernel->refcount_decrement( $_->{session}, __PACKAGE__ ) for @{ $self->{_handlers} };
173  $kernel->refcount_decrement( $_, __PACKAGE__ ) for keys %{ $self->{_sessions} };
174  delete $self->{_handlers};
175  undef;
176}
177
178sub log_event {
179  my ($kernel,$self,$sender,$event) = @_[KERNEL,OBJECT,SENDER,ARG0];
180  $sender = $sender->ID();
181
182  if ( exists $self->{_sessions}->{ $sender } and !$event ) {
183	delete $self->{_sessions}->{ $sender };
184	$kernel->refcount_decrement( $sender => __PACKAGE__ );
185	return;
186  }
187
188  if ( exists $self->{_sessions}->{ $sender } ) {
189	$self->{_sessions}->{ $sender } = $event;
190	return;
191  }
192
193  $self->{_sessions}->{ $sender } = $event;
194  $kernel->refcount_increment( $sender => __PACKAGE__ );
195  return;
196}
197
198sub add_handler {
199  my ($kernel,$self,$sender) = @_[KERNEL,OBJECT,SENDER];
200  $sender = $sender->ID();
201
202  # Get the arguments
203  my $args;
204  if (ref($_[ARG0]) eq 'HASH') {
205	$args = { %{ $_[ARG0] } };
206  }
207  else {
208	warn "first parameter must be a ref hash, trying to adjust. "
209		."(fix this to get rid of this message)";
210	$args = { @_[ARG0 .. $#_ ] };
211  }
212
213  $args->{ lc $_ } = delete $args->{$_} for keys %{ $args };
214
215  unless ( $args->{label} ) {
216	warn "you must supply a label argument, to make it unique\n";
217	return;
218  }
219
220  if ( grep { $_->{label} eq $args->{label} } @{ $self->{_handlers} } ) {
221	warn "you must supply a unique label argument, I already have that one\n";
222	return;
223  }
224
225  unless ( $args->{event} ) {
226	warn "you must supply an event argument, otherwise where do I send the replies to\n";
227	return;
228  }
229
230  unless ( $args->{match} ) {
231	warn "you must supply a match argument, otherwise what's the point\n";
232	return;
233  }
234
235  my $regex;
236  eval { $regex = qr/$args->{match}/ };
237
238  if ( $@ ) {
239	warn "The match argument you supplied was fubar, please try harder\n";
240	return;
241  }
242  else {
243	$args->{match} = $regex;
244  }
245
246  $args->{session} = $sender unless $args->{session};
247  if ( my $ref = $kernel->alias_resolve( $args->{session} ) ) {
248	$args->{session} = $ref->ID();
249  }
250  else {
251	$args->{session} = $sender->ID();
252  }
253
254  $kernel->refcount_increment( $args->{session}, __PACKAGE__ );
255
256  push @{ $self->{_handlers} }, $args;
257
258  undef;
259}
260
261sub del_handler {
262  my ($kernel,$self,$label) = @_[KERNEL,OBJECT,ARG0];
263  return unless $label;
264
265  my $i = 0; my $rec;
266  for ( @{ $self->{_handlers} } ) {
267    if ( $_->{label} eq $label ) {
268      splice( @{ $self->{_handlers} }, $i, 1 );
269      $rec = $_;
270      last;
271    }
272  }
273
274  $kernel->refcount_decrement( $rec->{session}, __PACKAGE__ );
275  undef;
276}
277
278sub _dns_incoming {
279  my($kernel,$self,$heap,$session,$dnsq) = @_[KERNEL,OBJECT,HEAP,SESSION,ARG0];
280
281  # TCP remote address is handled differently than UDP, so fix that here.
282  if (defined($heap->{_tcp_sockport})) {
283    $dnsq->answerfrom($heap->{_tcp_sockport});
284  }
285
286  my($q) = $dnsq->question();
287  return unless $q;
288
289  foreach my $handler ( @{ $self->{_handlers} } ) {
290	next unless $q->qname =~ $handler->{match};
291	my $callback = $session->callback( '_handled_req', $dnsq );
292	$kernel->post(
293			$handler->{session},
294			$handler->{event},
295			$q->qname,
296			$q->qclass,
297			$q->qtype,
298			$callback,
299			$dnsq->answerfrom, $dnsq, $handler->{'label'} );
300	return;
301  }
302
303  if ( $self->{no_clients} ) {
304    # Refuse unhandled requests, like an authoritative-only
305    #  BIND server would.
306    $dnsq->header->rcode('REFUSED');
307    $dnsq->header->qr(1);
308    $dnsq->header->aa(0);
309    $dnsq->header->ra(0);
310    $dnsq->header->ad(0);
311    my $str = $dnsq->string(); # Doesn't work without this, fucked if I know why.
312    $self->_dispatch_log( $dnsq );
313	#  $self->{dnsrw}->put( $dnsq ) if $self->{dnsrw};
314    $self->{"dnsrw"}->put( $dnsq ) if (!(defined($heap) && defined($heap->{socket_wheel})) && $self->{"dnsrw"});
315    $heap->{socket_wheel}->put($dnsq) if $heap->{socket_wheel};
316
317    return;
318  }
319
320  if ( $q->qname =~ /^localhost\.*$/i ) {
321	$dnsq->push( answer => $self->{_localhost} );
322	$self->_dispatch_log( $dnsq );
323    $self->{"dnsrw"}->put( $dnsq ) if (!(defined($heap) && defined($heap->{socket_wheel})) && $self->{"dnsrw"});
324	$heap->{socket_wheel}->put($dnsq) if $heap->{socket_wheel};
325	return;
326  }
327
328  if ( $self->{forward_only} ) {
329    my %query = (
330      class   => $q->qclass,
331      type    => $q->qtype,
332      host    => $q->qname,
333      context => [ $dnsq->answerfrom, $dnsq->header->id ],
334      event   => '_dns_response',
335    );
336
337    my $response = $self->{resolver}->resolve( %query );
338    $kernel->yield( '_dns_response', $response ) if $response;
339
340  }
341  else {
342#    $self->{recursive}->query_dorecursion( { event => '_dns_recursive', data => [ $dnsq, $dnsq->answerfrom, $dnsq->header->id ], }, $q->qname, $q->qtype, $q->qclass );
343    POE::Component::Client::DNS::Recursive->resolve(
344	event   => '_dns_recursive',
345	context => [ $dnsq, $dnsq->answerfrom, $dnsq->header->id ],
346	host    => $q->qname,
347	type    => $q->qtype,
348	class   => $q->qclass,
349    );
350  }
351
352  undef;
353}
354
355sub _handled_req {
356  my ($kernel,$self,$passthru,$passback,$heap) = @_[KERNEL,OBJECT,ARG0,ARG1,HEAP];
357  my $reply = $passthru->[0];
358  my ($rcode, $ans, $auth, $add, $headermask) = @{ $passback };
359  $reply->header->rcode($rcode);
360  $reply->push("answer",     @$ans)  if $ans;
361  $reply->push("authority",  @$auth) if $auth;
362  $reply->push("additional", @$add)  if $add;
363  if (!defined ($headermask)) {
364	$reply->header->ra($self->{no_clients} ? 0 : 1);
365	$reply->header->ad(0);
366  }
367  else {
368	$reply->header->aa(1) if $headermask->{'aa'};
369	$reply->header->ra(1) if $headermask->{'ra'};
370	$reply->header->ad(1) if $headermask->{'ad'};
371  }
372
373  $reply->header->qr(1);
374  $self->_dispatch_log( $reply );
375
376  $self->{"dnsrw"}->put( $reply ) if (!(defined($heap) && defined($heap->{socket_wheel})) && $self->{"dnsrw"});
377  $heap->{socket_wheel}->put($reply) if $heap->{socket_wheel};
378  undef;
379}
380
381sub _dns_err {
382  my($kernel,$self, $op, $errnum, $errstr) = @_[KERNEL,OBJECT, ARG0..ARG2];
383  warn "DNS readwrite: $op generated error $errnum: $errstr\n";
384  if (!($op eq "read" and ($errnum == 0 ||  $errnum == 104)))
385  {
386	warn "SHUTDOWN";
387	$kernel->yield('shutdown');
388  }
389  undef;
390}
391
392sub _dns_recursive {
393  my ($kernel,$heap,$self,$data) = @_[KERNEL,HEAP,OBJECT,ARG0];
394  return if $data->{error};
395  my ($dnsq,$answerfrom,$id) = @{ $data->{context} };
396
397  my $socket = $heap->{socket_wheel};
398
399  my $response = $data->{response};
400  if ( $response ) {
401    $response->header->id( $id );
402    $response->answerfrom( $answerfrom );
403    $self->_dispatch_log( $response );
404    $self->{"dnsrw"}->put( $response ) if (!(defined($socket)) && $self->{"dnsrw"});
405    $socket->put($response) if $socket;
406    return;
407  }
408  $dnsq->header->rcode('NXDOMAIN');
409  $self->_dispatch_log( $dnsq );
410#  $self->{dnsrw}->put( $dnsq ) if $self->{dnsrw};
411  $self->{"dnsrw"}->put( $dnsq ) if (!(defined($socket)) && $self->{"dnsrw"});
412  $socket->put($dnsq) if $socket;
413
414  undef;
415}
416
417sub _dns_response {
418  my ($kernel,$self,$heap,$reply) = @_[KERNEL,OBJECT,HEAP,ARG0];
419
420  my ($answerfrom,$id) = @{ $reply->{context} };
421  my $response = delete $reply->{response};
422  $response->header->id( $id );
423  $response->answerfrom( $answerfrom );
424  $self->_dispatch_log( $response );
425  $self->{"dnsrw"}->put( $response ) if (!(defined($heap) && defined($heap->{socket_wheel})) && $self->{"dnsrw"});
426  $heap->{socket_wheel}->put($response) if $heap->{socket_wheel};
427  undef;
428}
429
430sub _dispatch_log {
431  my $self = shift;
432  my $packet = shift || return;
433  my $af = $packet->answerfrom;
434  $poe_kernel->post( $_, $self->{_sessions}->{$_}, $af, $packet ) for keys %{ $self->{_sessions} };
435  return 1;
436}
437
438package DNS::Driver::SendRecv;
439$DNS::Driver::SendRecv::VERSION = '0.32';
440use strict;
441use POE::Driver;
442use Socket;
443
444sub new {
445    my $class = shift;
446    my $self = []; # the output queue
447    bless $self, $class;
448}
449
450sub get {
451    my $self = shift;
452    my $fh = shift;
453
454    my @ret;
455    while (1) {
456        my $from = recv($fh, my $buffer = '', 4096, 0 );
457        last if !$from;
458        push @ret, [ $from, $buffer ];
459    }
460    return if !@ret;
461    return \@ret;
462}
463
464sub put {
465    my $self = shift;
466    my $data = shift;
467
468    push @$self, @$data;
469    my $sum = 0;
470    $sum += length( $_->[1] ) for @$self;
471    return $sum;
472}
473
474sub flush {
475    my $self = shift;
476    my $fh = shift;
477
478    while ( @$self ) {
479        my $n = send($fh, $self->[0][1], 0, $self->[0][0])
480            or return;
481        $n == length($self->[0][1])
482            or die "Couldn't write complete message to socket: $!\n";
483        shift @$self;
484    }
485}
486
487package DNS::Filter::UDPDNS;
488$DNS::Filter::UDPDNS::VERSION = '0.32';
489use strict;
490use POE::Filter;
491use Socket;
492use Net::DNS::Packet;
493
494sub new {
495    my $class = shift;
496    bless {}, $class;
497}
498
499sub get {
500    my $self = shift;
501    my $data = shift;
502
503    my @ret;
504    for my $d ( @$data ) {
505        ref($d) eq "ARRAY"
506            or die "UDPDNS filter expected arrayrefs for input\n";
507        my($port, $inet) = sockaddr_in($d->[0]);
508        my $inetstr = inet_ntoa($inet);
509        my($p, $err) = Net::DNS::Packet->new(\$d->[1]);
510        if ( !$p ) {
511            warn "Cannot create DNS question for packet received from " .
512                "$inetstr: $err\n";
513        } else {
514            $p->answerfrom("$inetstr:$port");
515            push @ret, $p;
516        }
517    }
518    return \@ret;
519}
520
521sub put {
522    my $self = shift;
523    my $data = shift;
524
525    my @ret;
526    for my $d ( @$data ) {
527        my($inetstr, $port) = split /:/, $d->answerfrom();
528        $d->{buffer} = ''; #sigh
529        if ( !defined $port ) {
530            warn "answerfrom not set in DNS packet, no destination known\n";
531        } else {
532            push @ret,
533                [ pack_sockaddr_in($port, inet_aton($inetstr)), $d->data ];
534        }
535    }
536    return \@ret;
537}
538
5391;
540
541__END__
542
543=pod
544
545=encoding UTF-8
546
547=head1 NAME
548
549POE::Component::Server::DNS - A non-blocking, concurrent DNS server POE component
550
551=head1 VERSION
552
553version 0.32
554
555=head1 SYNOPSIS
556
557  use strict;
558  use Net::DNS::RR;
559  use POE qw(Component::Server::DNS);
560
561  my $dns_server = POE::Component::Server::DNS->spawn( alias => 'dns_server' );
562
563  POE::Session->create(
564        package_states => [ 'main' => [ qw(_start handler log) ], ],
565  );
566
567  $poe_kernel->run();
568  exit 0;
569
570  sub _start {
571    my ($kernel,$heap) = @_[KERNEL,HEAP];
572
573    # Tell the component that we want log events to go to 'log'
574    $kernel->post( 'dns_server', 'log_event', 'log' );
575
576    # register a handler for any foobar.com suffixed domains
577    $kernel->post( 'dns_server', 'add_handler',
578	{
579	  event => 'handler',
580	  label => 'foobar',
581	  match => 'foobar\.com$',
582        }
583    );
584    undef;
585  }
586
587  sub handler {
588    my ($qname,$qclass,$qtype,$callback) = @_[ARG0..ARG3];
589    my ($rcode, @ans, @auth, @add);
590
591    if ($qtype eq "A") {
592      my ($ttl, $rdata) = (3600, "10.1.2.3");
593      push @ans, Net::DNS::RR->new("$qname $ttl $qclass $qtype $rdata");
594      $rcode = "NOERROR";
595    } else {
596      $rcode = "NXDOMAIN";
597    }
598
599    $callback->($rcode, \@ans, \@auth, \@add, { aa => 1 });
600    undef;
601  }
602
603  sub log {
604    my ($ip_port,$net_dns_packet) = @_[ARG0..ARG1];
605    $net_dns_packet->print();
606    undef;
607  }
608
609=head1 DESCRIPTION
610
611POE::Component::Server::DNS is a L<POE> component that implements a DNS server.
612
613It uses L<POE::Component::Client::DNS> to handle resolving when configured as 'forward_only' and
614L<Net::DNS::Resolver::Recurse> wrapped by L<POE::Component::Generic> to perform recursion.
615
616One may add handlers to massage and manipulate responses to particular queries which is vaguely modelled
617after L<Net::DNS::Nameserver>.
618
619=head1 CONSTRUCTOR
620
621=over
622
623=item spawn
624
625Starts a POE::Component::Server::DNS component session and returns an object. Takes a number of optional arguments:
626
627  "alias", an alias to address the component by;
628  "port", which udp port to listen on. Default is 53, which requires 'root' privilege on UN*X type systems;
629  "address", which local IP address to listen on.  Default is INADDR_ANY;
630  "resolver_opts", a set of options to pass to the POE::Component::Client::DNS constructor;
631  "forward_only", be a forwarding only DNS server. Default is 0, be recursive.
632  "no_clients", do not spawn client code (See following notes);
633
634"no_clients" disables the spawning of client code (PoCo::Client::DNS, Net::DNS::Resolver::Recursive), and doesn't attempt to forward or recurse inbound requests.  Any request not handled by one of your handlers will be C<REFUSED>.  Saves some resources when you intend your server to be authoritative only (as opposed to a general resolver for DNS client software to point at directly).  Additionally, this argument changes the default "Recursion Available" flag in responses to off instead of on.
635
636=back
637
638=head1 METHODS
639
640These are methods that may be used with the object returned by spawn().
641
642=over
643
644=item session_id
645
646Returns the L<POE::Session> ID of the component's session.
647
648=item resolver
649
650Returns a reference to the L<POE::Component::Client::DNS> object.
651
652=item shutdown
653
654Terminates the component and associated resolver.
655
656=item sockport
657
658Returns the port of the socket that the component is listening on.
659
660=back
661
662=head1 INPUT EVENTS
663
664These are states that the component will accept:
665
666=over
667
668=item add_handler
669
670Accepts a hashref as an argument with the following keys:
671
672  "event", the event the component will post to, mandatory;
673  "label", a unique name for this handler, mandatory;
674  "match", a regex expression ( without // ) to match against the host part of queries, mandatory;
675  "session", the session where this handler event should be sent to, defaults to SENDER;
676
677See OUTPUT EVENTS for details of what happens when a handler is triggered.
678
679=item del_handler
680
681Accepts a handler label to remove.
682
683=item log_event
684
685Tells the component that a session wishes to receive or stop receiving DNS log events. Specify the event you
686wish to receive log events as the first argument. If no event is specified you stop receiving log events.
687
688=item shutdown
689
690Terminates the component and associated resolver.
691
692=back
693
694=head1 HANDLER EVENTS
695
696These events are triggered by a DNS query matching a handler. The applicable event is fired in the requested session
697with the following paramters:
698
699  ARG0, query name
700  ARG1, query class
701  ARG2, query type
702  ARG3, a callback coderef
703  ARG4, the IP address and port of the requestor, 'IPaddr:port'
704
705Do your manipulating then use the callback to fire the response back to the component, returning a
706response code and references to the answer, authority, and additional sections of the response. For advanced
707usage there is an optional argument containing an hashref with the settings for the aa, ra, and ad header bits.
708The argument is of the form { ad => 1, aa => 0, ra => 1 }.
709
710  $callback->( $rcode, \@ans, \@auth, \@add, { aa => 1 } );
711
712=head1 LOG EVENTS
713
714These events are triggered whenever a DNS response is sent to a client.
715
716  ARG0, the IP address and port of the requestor, 'IPaddr:port';
717  ARG1, the Net::DNS::Packet object;
718
719See L<Net::DNS::Packet> for details.
720
721=head1 HISTORY
722
723The component's genesis was inspired by Jan-Pieter's 'Fun with POE' talk at YAPC::EU 2006, which lay much of the
724ground-work code such as the L<POE::Driver> and L<POE::Filter> used internally. BinGOs wrapped it all up in a
725component, added the tests ( borrowed shamelessly from L<POE::Component::Client::DNS>'s testsuite ) and documentation.
726
727Other suggestions as to the API were provided by Ben 'integral' Smith.
728
729Rocco Caputo brought L<POE::Component::Client::DNS> to the party.
730
731=head1 SEE ALSO
732
733L<POE::Component::Client::DNS>
734
735L<POE::Component::Generic>
736
737L<Net::DNS>
738
739L<Net::DNS::Packet>
740
741=head1 AUTHORS
742
743=over 4
744
745=item *
746
747Chris Williams <chris@bingosnet.co.uk>
748
749=item *
750
751Jan-Pieter Cornet <johnpc@xs4all.nl>
752
753=item *
754
755Brandon Black <blblack@gmail.com>
756
757=item *
758
759Richard Harman <richard@richardharman.com>
760
761=item *
762
763Stephan Jauernick <stephan@stejau.de>
764
765=back
766
767=head1 COPYRIGHT AND LICENSE
768
769This software is copyright (c) 2015 by Chris Williams, Jan-Pieter Cornet, Brandon Black, Richard Harman and Stephan Jauernick.
770
771This is free software; you can redistribute it and/or modify it under
772the same terms as the Perl 5 programming language system itself.
773
774=cut
775