1package Net::DNS::Lite; 2 3use 5.008_001; 4 5use strict; 6use warnings; 7 8use Carp (); 9use Exporter qw(import); 10use List::MoreUtils qw(uniq); 11use List::Util qw(min); 12use Socket qw(AF_INET SOCK_DGRAM inet_ntoa sockaddr_in unpack_sockaddr_in); 13use Time::HiRes qw(time); 14 15our $VERSION = '0.12'; 16 17our @EXPORT = qw(); 18our @EXPORT_OK = qw(inet_aton); 19our %EXPORT_TAGS = ( 20 'all' => [ @EXPORT_OK ], 21); 22 23sub DOMAIN_PORT () { 53 } 24 25our %opcode_id = ( 26 query => 0, 27 iquery => 1, 28 status => 2, 29 notify => 4, 30 update => 5, 31 map +($_ => $_), 3, 6..15 32); 33 34our %opcode_str = reverse %opcode_id; 35 36our %rcode_id = ( 37 noerror => 0, 38 formerr => 1, 39 servfail => 2, 40 nxdomain => 3, 41 notimp => 4, 42 refused => 5, 43 yxdomain => 6, # Name Exists when it should not [RFC 2136] 44 yxrrset => 7, # RR Set Exists when it should not [RFC 2136] 45 nxrrset => 8, # RR Set that should exist does not [RFC 2136] 46 notauth => 9, # Server Not Authoritative for zone [RFC 2136] 47 notzone => 10, # Name not contained in zone [RFC 2136] 48# EDNS0 16 BADVERS Bad OPT Version [RFC 2671] 49# EDNS0 16 BADSIG TSIG Signature Failure [RFC 2845] 50# EDNS0 17 BADKEY Key not recognized [RFC 2845] 51# EDNS0 18 BADTIME Signature out of time window [RFC 2845] 52# EDNS0 19 BADMODE Bad TKEY Mode [RFC 2930] 53# EDNS0 20 BADNAME Duplicate key name [RFC 2930] 54# EDNS0 21 BADALG Algorithm not supported [RFC 2930] 55 map +($_ => $_), 11..15 56); 57 58our %rcode_str = reverse %rcode_id; 59 60our %type_id = ( 61 a => 1, 62 ns => 2, 63 md => 3, 64 mf => 4, 65 cname => 5, 66 soa => 6, 67 mb => 7, 68 mg => 8, 69 mr => 9, 70 null => 10, 71 wks => 11, 72 ptr => 12, 73 hinfo => 13, 74 minfo => 14, 75 mx => 15, 76 txt => 16, 77 aaaa => 28, 78 srv => 33, 79 naptr => 35, # rfc2915 80 dname => 39, # rfc2672 81 opt => 41, 82 spf => 99, 83 tkey => 249, 84 tsig => 250, 85 ixfr => 251, 86 axfr => 252, 87 mailb => 253, 88 "*" => 255, 89); 90 91our %type_str = reverse %type_id; 92 93our %class_id = ( 94 in => 1, 95 ch => 3, 96 hs => 4, 97 none => 254, 98 "*" => 255, 99); 100 101our %class_str = reverse %class_id; 102 103our $TIMEOUT = 10; 104our $CACHE; 105our $CACHE_TTL = 600; 106our $PID; 107 108sub new { 109 my ($class, %arg) = @_; 110 111 my $self = bless { 112 server => [], 113 timeout => [2, 5, 5], 114 search => [], 115 ndots => 1, 116 reuse => 300, 117 %arg, 118 }, $class; 119 120 if (@{$self->{server}} == 0) { 121 if (-e '/etc/resolv.conf') { 122 $self->_parse_resolv_conf_file('/etc/resolv.conf'); 123 } else { 124 Carp::croak "server was not specified and there is no /etc/resolv.conf"; 125 } 126 } 127 128 $self->_compile; 129 130 $self 131} 132 133sub _compile { 134 my $self = shift; 135 136 $self->{search} = [ grep { length($_) } uniq @{$self->{search}} ]; 137 138 $self->{server} = [ 139 map { 140 Socket::inet_aton($_) or Carp::croak "invalid server address: $_" 141 } grep { 142 ! /:/ # ignore ipv6 address (for now) 143 } grep { length($_) } uniq @{$self->{server}}, 144 ]; 145 146 my @retry; 147 148 for my $timeout (@{$self->{timeout}}) { 149 for my $server (@{$self->{server}}) { 150 push @retry, [ $server, $timeout ]; 151 } 152 } 153 154 $self->{retry} = \@retry; 155} 156 157sub resolve { 158 my ($self, $qname, $qtype, %opt) = @_; 159 160 my @search = $qname =~ s/\.$// 161 ? "" 162 : $opt{search} 163 ? @{ $opt{search} } 164 : ($qname =~ y/.//) >= $self->{ndots} 165 ? ("", @{ $self->{search} }) 166 : (@{ $self->{search} }, ""); 167 168 my $class = $opt{class} || "in"; 169 170 my %atype = $opt{accept} 171 ? map { +($_ => 1) } @{$opt{accept}} 172 : ($qtype => 1); 173 174 # use some big value as default so that all servers and retries will be 175 # performed before total_timeout 176 my $timeout_at = time + (defined $opt{timeout} ? $opt{timeout} : $TIMEOUT); 177 178 # advance in searchlist 179 my ($do_search, $do_req); 180 181 $do_search = sub { 182 @search 183 or (undef $do_search), (undef $do_req), return (); 184 185 (my $name = lc "$qname." . shift @search) =~ s/\.$//; 186 my $depth = 10; 187 188 # advance in cname-chain 189 $do_req = sub { 190 my $res = $self->request($name, $qtype, $class, $timeout_at) 191 or return $do_search->(); 192 193 my $cname; 194 195 while (1) { 196 # results found? 197 my @rr = grep { 198 $name eq lc $_->[0] && ($atype{"*"} || $atype{$_->[1]}) 199 } @{$res->{an}}; 200 201 if (@rr) { 202 (undef $do_search), (undef $do_req), return @rr; 203 } 204 205 # see if there is a cname we can follow 206 @rr = grep { 207 $name eq lc $_->[0] && $_->[1] eq "cname" 208 } @{$res->{an}}; 209 210 if (@rr) { 211 $depth-- 212 or return $do_search->(); # cname chain too long 213 214 $cname = 1; 215 $name = lc $rr[0][4]; 216 217 } elsif ($cname) { 218 # follow the cname 219 return $do_req->(); 220 221 } else { 222 # no, not found anything 223 return $do_search->(); 224 } 225 } 226 }; 227 228 $do_req->(); 229 }; 230 231 $do_search->(); 232} 233 234sub request { 235 my ($self, $name, $qtype, $class, $total_timeout_at) = @_; 236 237 my $cache = $self->{cache}; 238 if (! defined $self->{cache}) { 239 $cache = $CACHE; 240 } 241 my $cache_key = "$class $qtype $name"; 242 243 if ($cache) { 244 if (my $value = $cache->get($cache_key)) { 245 my ($res, $expires_at) = @$value; 246 return $res if time < $expires_at; 247 $cache->remove($cache_key); 248 } 249 } 250 251 $self->_open_socket() 252 if ! $self->{sock_v4} || $self->{pid} != $$; 253 254 my $req = { 255 id => $self->_new_id(), 256 rd => 1, 257 qd => [[$name, $qtype, $class]], 258 }; 259 260 my $req_pkt = dns_pack($req); 261 my $pkt_sent; 262 263 for (my $retry = 0; $retry < @{$self->{retry}}; $retry++) { 264 my ($server, $server_timeout) = @{$self->{retry}->[$retry]}; 265 266 my $now = time; 267 my $server_timeout_at = $now + $server_timeout; 268 $server_timeout_at = $total_timeout_at 269 if $total_timeout_at < $server_timeout_at; 270 if ($server_timeout_at <= $now) { 271 goto FAIL; 272 } 273 274 # send request 275 $pkt_sent = 1; 276 send( 277 $self->{sock_v4}, $req_pkt, 0, 278 scalar sockaddr_in(DOMAIN_PORT, $server), 279 ) or do { 280 warn "failed to send packet to @{[inet_ntoa($server)]}:$!"; 281 next; 282 }; 283 284 # wait for the response (or the timeout) 285 my $res; 286 for (; ; undef($res), $now = time) { 287 my $select_timeout = $server_timeout_at - $now; 288 if ($select_timeout <= 0) { 289 goto FAIL if $total_timeout_at <= $now; 290 last; 291 } 292 last if $select_timeout <= 0; 293 my $rfd = ''; 294 vec($rfd, fileno($self->{sock_v4}), 1) = 1; 295 my $nfound = select( 296 $rfd, my $wfd = '', my $efd = '', $select_timeout); 297 next unless $nfound > 0; 298 my $from = recv($self->{sock_v4}, my $res_pkt, 1024, 0) 299 or next; 300 my ($from_port, $from_addr) = unpack_sockaddr_in($from); 301 if (! ($from_port == DOMAIN_PORT 302 && grep { $from_addr eq $_ } @{$self->{server}})) { 303 next; 304 } 305 $res = dns_unpack($res_pkt) 306 or next; 307 if ($res->{id} == $req->{id}) { 308 $self->_register_unusable_id($req->{id}) 309 if $retry != 0; 310 if ($cache) { 311 my $ttl = min( 312 $self->{cache_ttl} || $CACHE_TTL, 313 map { 314 $_->[3] 315 } (@{$res->{an}} ? @{$res->{an}} : @{$res->{ns}}), 316 ); 317 $cache->set( 318 $cache_key => [ $res, time + $ttl + 0.5 ], 319 ); 320 } 321 return $res; 322 } 323 } 324 } 325 326 FAIL: 327 $self->_register_unusable_id($req->{id}) 328 if $pkt_sent; 329 return; 330} 331 332sub _open_socket { 333 my $self = shift; 334 335 my $got_socket = 0; 336 socket($self->{sock_v4}, AF_INET, SOCK_DGRAM, 0) 337 and $got_socket++; 338 # if (AF_INET6) { 339 # socket($self->{sock_v6}, AF_INET6, SOCK_DGRAM, 0) 340 # and $got_socket++; 341 # } 342 343 $got_socket 344 or Carp::croak "unable to create either an IPv4 or an IPv6 socket"; 345 346 $self->{reuse_q} = []; 347 $self->{reuse_h} = +{}; 348 $self->{pid} = $$; 349} 350 351sub _new_id { 352 my $self = shift; 353 my $id; 354 355 my $now = time; 356 357 if (@{$self->{reuse_q}} >= 30000) { 358 $self->_open_socket(); 359 } else { 360 delete $self->{reuse_h}{(shift @{$self->{reuse_q}})->[1]} 361 while @{$self->{reuse_q}} && $self->{reuse_q}[0][0] <= $now; 362 } 363 364 while (1) { 365 $id = int rand(65536); 366 last if not defined $self->{reuse_h}{$id}; 367 } 368 369 $id; 370} 371 372sub _register_unusable_id { 373 my ($self, $id) = @_; 374 375 push @{$self->{reuse_q}}, [ time + $self->{reuse}, $id ]; 376 $self->{reuse_h}{$id} = 1; 377} 378 379sub parse_resolv_conf { 380 my ($self, $resolvconf) = @_; 381 382 $self->{server} = []; 383 $self->{search} = []; 384 385 my $attempts; 386 my $timeout; 387 388 for (split /\n/, $resolvconf) { 389 s/\s*[;#].*$//; # not quite legal, but many people insist 390 391 if (/^\s*nameserver\s+(\S+)\s*$/i) { 392 my $ip = $1; 393 if (my $ipn = parse_address($ip)) { 394 push @{ $self->{server} }, $ip; 395 } else { 396 warn "nameserver $ip invalid and ignored\n"; 397 } 398 } elsif (/^\s*domain\s+(\S*)\s*$/i) { 399 $self->{search} = [$1]; 400 } elsif (/^\s*search\s+(.*?)\s*$/i) { 401 $self->{search} = [split /\s+/, $1]; 402 } elsif (/^\s*sortlist\s+(.*?)\s*$/i) { 403 # ignored, NYI 404 } elsif (/^\s*options\s+(.*?)\s*$/i) { 405 for (split /\s+/, $1) { 406 if (/^timeout:(\d+)$/) { 407 $timeout = $1; 408 } elsif (/^attempts:(\d+)$/) { 409 $attempts = $1; 410 } elsif (/^ndots:(\d+)$/) { 411 $self->{ndots} = $1; 412 } else { 413 # debug, rotate, no-check-names, inet6 414 } 415 } 416 } 417 } 418 419 if ( $timeout || $attempts ) { 420 $timeout ||= 5; 421 $attempts ||= 2; 422 $self->{timeout} = [ map { $timeout } 1..$attempts ]; 423 } 424} 425 426sub _parse_resolv_conf_file { 427 my ($self, $resolv_conf) = @_; 428 429 open my $fh, '<', $resolv_conf 430 or Carp::croak "could not open file: $resolv_conf: $!"; 431 432 $self->parse_resolv_conf(do { local $/; join '', <$fh> }); 433} 434 435sub _enc_name($) { 436 pack "(C/a*)*", (split /\./, shift), "" 437} 438 439sub _enc_qd() { 440 no warnings; 441 (_enc_name $_->[0]) . pack "nn", 442 ($_->[1] > 0 ? $_->[1] : $type_id {$_->[1]}), 443 ($_->[2] > 0 ? $_->[2] : $class_id{$_->[2] || "in"}) 444} 445 446sub _enc_rr() { 447 die "encoding of resource records is not supported"; 448} 449 450sub dns_pack { 451 no warnings; 452 my ($req) = @_; 453 454 pack "nn nnnn a* a* a* a*", 455 $req->{id}, 456 457 ! !$req->{qr} * 0x8000 458 + $opcode_id{$req->{op}} * 0x0800 459 + ! !$req->{aa} * 0x0400 460 + ! !$req->{tc} * 0x0200 461 + ! !$req->{rd} * 0x0100 462 + ! !$req->{ra} * 0x0080 463 + ! !$req->{ad} * 0x0020 464 + ! !$req->{cd} * 0x0010 465 + $rcode_id{$req->{rc}} * 0x0001, 466 467 scalar @{ $req->{qd} || [] }, 468 scalar @{ $req->{an} || [] }, 469 scalar @{ $req->{ns} || [] }, 470 scalar @{ $req->{ar} || [] }, 471 472 (join "", map _enc_qd, @{ $req->{qd} || [] }), 473 (join "", map _enc_rr, @{ $req->{an} || [] }), 474 (join "", map _enc_rr, @{ $req->{ns} || [] }), 475 (join "", map _enc_rr, @{ $req->{ar} || [] }) 476} 477 478our $ofs; 479our $pkt; 480 481# bitches 482sub _dec_name { 483 my @res; 484 my $redir; 485 my $ptr = $ofs; 486 my $cnt; 487 488 while () { 489 return undef if ++$cnt >= 256; # to avoid DoS attacks 490 491 my $len = ord substr $pkt, $ptr++, 1; 492 493 if ($len >= 0xc0) { 494 $ptr++; 495 $ofs = $ptr if $ptr > $ofs; 496 $ptr = (unpack "n", substr $pkt, $ptr - 2, 2) & 0x3fff; 497 } elsif ($len) { 498 push @res, substr $pkt, $ptr, $len; 499 $ptr += $len; 500 } else { 501 $ofs = $ptr if $ptr > $ofs; 502 return join ".", @res; 503 } 504 } 505} 506 507sub _dec_qd { 508 my $qname = _dec_name; 509 my ($qt, $qc) = unpack "nn", substr $pkt, $ofs; $ofs += 4; 510 [$qname, $type_str{$qt} || $qt, $class_str{$qc} || $qc] 511} 512 513our %dec_rr = ( 514 1 => sub { join ".", unpack "C4", $_ }, # a 2 => sub { local $ofs = $ofs - length; _dec_name }, # ns 515 5 => sub { local $ofs = $ofs - length; _dec_name }, # cname 516 6 => sub { 517 local $ofs = $ofs - length; my $mname = _dec_name; 518 my $rname = _dec_name; 519 ($mname, $rname, unpack "NNNNN", substr $pkt, $ofs) 520 }, # soa 521 11 => sub { ((join ".", unpack "C4", $_), unpack "C a*", substr $_, 4) }, # wks 522 12 => sub { local $ofs = $ofs - length; _dec_name }, # ptr 523 13 => sub { unpack "C/a* C/a*", $_ }, # hinfo 524 15 => sub { local $ofs = $ofs + 2 - length; ((unpack "n", $_), _dec_name) }, # mx 525 16 => sub { unpack "(C/a*)*", $_ }, # txt 526 28 => sub { format_ipv6 ($_) }, # aaaa 527 33 => sub { local $ofs = $ofs + 6 - length; ((unpack "nnn", $_), _dec_name) }, # srv 528 35 => sub { # naptr 529 # requires perl 5.10, sorry 530 my ($order, $preference, $flags, $service, $regexp, $offset) = unpack "nn C/a* C/a* C/a* .", $_; 531 local $ofs = $ofs + $offset - length; 532 ($order, $preference, $flags, $service, $regexp, _dec_name) 533 }, 534 39 => sub { local $ofs = $ofs - length; _dec_name }, # dname 535 99 => sub { unpack "(C/a*)*", $_ }, # spf 536); 537 538sub _dec_rr { 539 my $name = _dec_name; 540 541 my ($rt, $rc, $ttl, $rdlen) = unpack "nn N n", substr $pkt, $ofs; $ofs += 10; 542 local $_ = substr $pkt, $ofs, $rdlen; $ofs += $rdlen; 543 544 [ 545 $name, 546 $type_str{$rt} || $rt, 547 $class_str{$rc} || $rc, 548 $ttl, 549 ($dec_rr{$rt} || sub { $_ })->(), 550 ] 551} 552 553sub dns_unpack { 554 local $pkt = shift; 555 my ($id, $flags, $qd, $an, $ns, $ar) 556 = unpack "nn nnnn A*", $pkt; 557 558 local $ofs = 6 * 2; 559 560 { 561 id => $id, 562 qr => ! ! ($flags & 0x8000), 563 aa => ! ! ($flags & 0x0400), 564 tc => ! ! ($flags & 0x0200), 565 rd => ! ! ($flags & 0x0100), 566 ra => ! ! ($flags & 0x0080), 567 ad => ! ! ($flags & 0x0020), 568 cd => ! ! ($flags & 0x0010), 569 op => $opcode_str{($flags & 0x001e) >> 11}, 570 rc => $rcode_str{($flags & 0x000f)}, 571 572 qd => [map _dec_qd, 1 .. $qd], 573 an => [map _dec_rr, 1 .. $an], 574 ns => [map _dec_rr, 1 .. $ns], 575 ar => [map _dec_rr, 1 .. $ar], 576 } 577} 578 579sub parse_address { 580 my $text = shift; 581 if (my $addr = parse_ipv6($text)) { 582 $addr =~ s/^\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff//; 583 return $addr; 584 } else { 585 return parse_ipv4($text); 586 } 587} 588 589sub parse_ipv4 { 590 $_[0] =~ /^ (?: 0x[0-9a-fA-F]+ | 0[0-7]* | [1-9][0-9]* ) 591 (?:\. (?: 0x[0-9a-fA-F]+ | 0[0-7]* | [1-9][0-9]* ) ){0,3}$/x 592 or return undef; 593 594 @_ = map /^0/ ? oct : $_, split /\./, $_[0]; 595 596 # check leading parts against range 597 return undef if grep $_ >= 256, @_[0 .. @_ - 2]; 598 599 # check trailing part against range 600 return undef if $_[-1] >= 2 ** (8 * (4 - $#_)); 601 602 pack "N", (pop) 603 + ($_[0] << 24) 604 + ($_[1] << 16) 605 + ($_[2] << 8); 606} 607 608sub parse_ipv6 { 609 # quick test to avoid longer processing 610 my $n = $_[0] =~ y/://; 611 return undef if $n < 2 || $n > 8; 612 613 my ($h, $t) = split /::/, $_[0], 2; 614 615 unless (defined $t) { 616 ($h, $t) = (undef, $h); 617 } 618 619 my @h = defined $h ? (split /:/, $h) : (); 620 my @t = split /:/, $t; 621 622 # check for ipv4 tail 623 if (@t && $t[-1]=~ /\./) { 624 return undef if $n > 6; 625 626 my $ipn = parse_ipv4(pop @t) 627 or return undef; 628 629 push @t, map +(sprintf "%x", $_), unpack "nn", $ipn; 630 } 631 632 # no :: then we need to have exactly 8 components 633 return undef unless @h + @t == 8 || $_[0] =~ /::/; 634 635 # now check all parts for validity 636 return undef if grep !/^[0-9a-fA-F]{1,4}$/, @h, @t; 637 638 # now pad... 639 push @h, 0 while @h + @t < 8; 640 641 # and done 642 pack "n*", map hex, @h, @t 643} 644 645our $resolver; 646 647sub RESOLVER() { 648 $resolver ||= Net::DNS::Lite->new; 649} 650 651sub inet_aton { 652 my $name = shift; 653 if (my $address = parse_address($name)) { 654 return $address; 655 } 656 my @rr = RESOLVER->resolve( 657 $name, 'a', 658 (@_ ? (timeout => $_[0]) : ()), 659 ); 660 while (@rr) { 661 my $idx = int rand @rr; 662 my $address = parse_ipv4($rr[$idx][4]); 663 return $address if defined $address; 664 splice @rr, $idx, 1; 665 } 666 return undef; 667} 668 669sub format_ipv4($) { 670 join ".", unpack "C4", $_[0] 671} 672 673sub format_ipv6($) { 674 if ($_[0] =~ /^\x00\x00\x00\x00\x00\x00\x00\x00/) { 675 if (v0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0 eq $_[0]) { 676 return "::"; 677 } elsif (v0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1 eq $_[0]) { 678 return "::1"; 679 } elsif (v0.0.0.0.0.0.0.0.0.0.0.0 eq substr $_[0], 0, 12) { 680 # v4compatible 681 return "::" . format_ipv4 substr $_[0], 12; 682 } elsif (v0.0.0.0.0.0.0.0.0.0.255.255 eq substr $_[0], 0, 12) { 683 # v4mapped 684 return "::ffff:" . format_ipv4 substr $_[0], 12; 685 } elsif (v0.0.0.0.0.0.0.0.255.255.0.0 eq substr $_[0], 0, 12) { 686 # v4translated 687 return "::ffff:0:" . format_ipv4 substr $_[0], 12; 688 } 689 } 690 691 my $ip = sprintf "%x:%x:%x:%x:%x:%x:%x:%x", unpack "n8", $_[0]; 692 693 # this is admittedly rather sucky 694 $ip =~ s/(?:^|:) 0:0:0:0:0:0:0 (?:$|:)/::/x 695 or $ip =~ s/(?:^|:) 0:0:0:0:0:0 (?:$|:)/::/x 696 or $ip =~ s/(?:^|:) 0:0:0:0:0 (?:$|:)/::/x 697 or $ip =~ s/(?:^|:) 0:0:0:0 (?:$|:)/::/x 698 or $ip =~ s/(?:^|:) 0:0:0 (?:$|:)/::/x 699 or $ip =~ s/(?:^|:) 0:0 (?:$|:)/::/x 700 or $ip =~ s/(?:^|:) 0 (?:$|:)/::/x; 701 702 $ip 703} 704 7051; 706__END__ 707 708=head1 NAME 709 710Net::DNS::Lite - a pure-perl DNS resolver with support for timeout 711 712=head1 SYNOPSIS 713 714 use Net::DNS::Lite qw(inet_aton); 715 716 # drop-in replacement for Socket::inet_aton 717 $Net::DNS::Lite::TIMEOUT = 5; # global timeout variable 718 my $addr = inet_aton("www.google.com"); 719 720 # or per-query timeout 721 my $addr = inet_aton("www.google.com", $timeout_in_seconds); 722 723=head1 DESCRIPTION 724 725This module provides a replacement function for L<Socket::inet_aton>, with support for timeouts. 726 727=head1 CONFIGURATION VARIABLES 728 729=head2 $Net::DNS::Lite::TIMEOUT 730 731maximum time (in seconds) inet_aton will block (default: 10) 732 733=head2 $Net::DNS::Lite::CACHE 734 735if set, Net::DNS::Lite will cache the DNS responses internally using the supplied cache object. The cache object should support C<get>, C<set>, and C<remove> functions (default: none) 736 737=head2 $Net::DNS::Lite::CACHE_TTL 738 739maximum ttl of the cached entries (in seconds). Only has effect when $Net::DNS::Lite::CACHE is set. 740 741=head1 AUTHOR 742 743Kazuho Oku 744 745The module is based on the excellent L<AnyEvent::DNS> by mlehmann. 746 747=head1 LICENSE 748 749This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. 750 751See <http://www.perl.com/perl/misc/Artistic.html> 752 753=cut 754