#!/usr/bin/perl # Auto-generating IPv6 Reverse DNS server with DNSSEC online signing # Version 1.0 # $Id: v6rev-dnssec-1rev.pl,v 1.5 2010/08/23 13:08:17 fujiwara Exp $ # # Author: Kazunori Fujiwara , my $debug = 0; use Net::DNS::SEC::Private; use Net::DNS::Nameserver; use Socket; use Socket6; use strict; use warnings; my $configfile = $ARGV[0]; STDOUT->autoflush(1); ############################################################################### # Supported queries: # any.$rev_dom PTR ----> ANY.$fwd_dom # $rev_dom NS/SOA/NSEC/DNSKEY # ANY.$fwd_dom AAAA ----> AAAA # ANY.$fwd_dom NSEC # $fwd_dom NS/SOA/NSEC/DNSKEY # Remaining work: # new configuration # multiple zone support # name error handling on reverse mapping ############################################################################### my @serveraddr = ('203.178.129.34', '2001:200:132:7::2'); my $serverport = 53; my $pid_file ="/var/run/v6rev.pid"; ############################################################################### # We need periodic re-signing of DNSKEY and other static RRSets. ############################################################################### my $enable_dnssec = 0; my $querylog = 0; my $keyfile_dir = "."; my $reconfig_interval = 3600; my $ttl = 900; my $serial = time; my @nsname = ('ns.example.jp'); my $fwd_dom = 'user.example.jp'; my $rev_dom = '8.b.d.0.1.0.0.2.ip6.arpa'; my $rev_hostdigits; my %static_ptr; my @fwd_keys; my @fwd_ksk_private; my @fwd_zsk_private; my @rev_keys; my @rev_ksk_private; my @rev_zsk_private; my @fwd_key_sigs; my $fwd_soa; my @fwd_soa_sigs; my @fwd_ns; my @fwd_ns_sigs; my $fwd_apex_nsec; my @fwd_apex_nsec_sigs; my @rev_key_sigs; my $rev_soa; my @rev_soa_sigs; my @rev_ns; my @rev_ns_sigs; my $rev_apex_nsec; my @rev_apex_nsec_sigs; my $initialized_time = time; my $reload_interval = 86400; ############################################################################### &initialize($configfile); if ($serverport eq 53 && $< != 0) { # for debug @serveraddr = ('127.0.0.1', '::1'); $serverport = 10053; $pid_file ="v6rev.pid"; } ############################################################################### sub initialize() { if (defined($configfile)) { open(F, "$configfile") || die "cannot open $configfile"; while () { chomp; next if (/^[#; \t]/); if (/^(\S+):(.*)$/) { my ($key, $value) = ($1, $2); my @value = split(/,/, $value); for (my $i = 0; $i <= $#value; $i++) { if ($value[$i] =~ /^\s*(\S+.*\S*)\s*$/) { $value[$i] = $1; } } if ($key eq "server_address" && $#value >= 0) { @serveraddr = @value; } elsif ($key eq "server_port" && $#value == 0 && $value[0] > 0) { $serverport = $value[0]; } elsif ($key eq "pid_file" && $#value == 0) { $pid_file = $value[0]; } elsif ($key eq "debug" && $#value == 0) { $debug = $value[0]; } elsif ($key eq "reconfig_interval" && $#value == 0 && $value[0] > 0) { $reconfig_interval = $value[0]; } elsif ($key eq "keyfile_dir" && $#value == 0) { $keyfile_dir = $value[0]; } elsif ($key eq "ttl" && $#value == 0 && $value[0] > 0) { $ttl = $value[0]; } elsif ($key eq "nsname" && $#value >= 0) { @nsname = @value; } elsif ($key eq "forward_domainname" && $#value == 0) { $fwd_dom = $value[0]; } elsif ($key eq "reverse_domainname" && $#value == 0 && $value[0] =~ /^([0-9a-fA-F.]+\.ip6\.arpa)\.?/i) { $rev_dom = $1; $rev_dom =~ tr / A-Z / a-z /; } elsif ($key eq "enable_dnssec" && $#value == 0) { if ($value[0] > 0 || $value[0] =~ /^(yes|on)$/i) { $enable_dnssec = 1; } else { $enable_dnssec = 0; } } elsif ($key eq "querylog" && $#value == 0) { if ($value[0] > 0 && $value[0] =~ /^(yes|on)$/i) { $querylog = 1; } else { $querylog = 0; } } elsif ($key eq "static_ptr" && $#value == 0) { my @rr = split(/ /, $value[0]); if ($#rr != 1 || &rev2host($rr[0]) eq '') { print STDERR "Invalid parameter: $_\n"; } else { $static_ptr{$rr[0]} = $rr[1]; } } else { print STDERR "Unknown parameter: $_\n"; } } } close (F); } # calculate $rev_hostdigits my @rev_dom = split(/\./, $rev_dom); $rev_hostdigits = 33 - $#rev_dom; $fwd_soa = Net::DNS::RR->new( "$fwd_dom $ttl IN SOA ".$nsname[0]." postmaster.".$nsname[0]." $serial 3600 900 86400 900"); @fwd_ns = (); for my $k (@nsname) { push @fwd_ns, Net::DNS::RR->new("$fwd_dom $ttl IN NS $k"); } $rev_soa = Net::DNS::RR->new( "$rev_dom $ttl IN SOA ".$nsname[0]." postmaster.".$nsname[0]." $serial 3600 900 86400 900"); @rev_ns = (); for my $k (@nsname) { push @rev_ns, Net::DNS::RR->new("$rev_dom $ttl IN NS $k"); } @fwd_keys = (); @fwd_ksk_private = (); @fwd_zsk_private = (); @rev_keys = (); @rev_ksk_private = (); @rev_zsk_private = (); if ($enable_dnssec) { opendir(my $dh, $keyfile_dir) || die "cannot opendir $keyfile_dir"; foreach my $file (readdir($dh)) { if ($file =~ /^K$fwd_dom\.\+\d+\+\d+\.key$/) { my $public = Net::DNS::RR->new_from_string(&load_file($keyfile_dir."/".$file)); $public->ttl($ttl); push @fwd_keys, $public; my $privatefile = $file; $privatefile =~ s/key$/private/; $privatefile = $keyfile_dir."/".$privatefile; if ($public->flags == 257) { if (-f $privatefile) { push @fwd_ksk_private, Net::DNS::SEC::Private->new($privatefile); } } else { if (-f $privatefile) { push @fwd_zsk_private, Net::DNS::SEC::Private->new($privatefile); } } } elsif ($file =~ /^K$rev_dom\.\+\d+\+\d+\.key$/) { my $public = Net::DNS::RR->new_from_string(&load_file($keyfile_dir."/".$file)); $public->ttl($ttl); push @rev_keys, $public; my $privatefile = $file; $privatefile =~ s/key$/private/; $privatefile = $keyfile_dir."/".$privatefile; if ($public->flags == 257) { if (-f $privatefile) { push @rev_ksk_private, Net::DNS::SEC::Private->new($privatefile); } } else { if (-f $privatefile) { push @rev_zsk_private, Net::DNS::SEC::Private->new($privatefile); } } } } close($dh); ############################################################################### @fwd_key_sigs = (); for my $private (@fwd_ksk_private, @fwd_zsk_private) { push @fwd_key_sigs, create Net::DNS::RR::RRSIG([@fwd_keys],$private); } @fwd_soa_sigs = (); foreach my $private (@fwd_zsk_private) { push @fwd_soa_sigs, create Net::DNS::RR::RRSIG([$fwd_soa],$private); } @fwd_ns_sigs = (); foreach my $private (@fwd_zsk_private) { push @fwd_ns_sigs, create Net::DNS::RR::RRSIG([@fwd_ns],$private); } $fwd_apex_nsec = Net::DNS::RR->new("$fwd_dom $ttl IN NSEC 00000000000000000000000000000000.$fwd_dom ( SOA NS DNSKEY NSEC RRSIG )"); @fwd_apex_nsec_sigs = (); foreach my $private (@fwd_zsk_private) { push @fwd_apex_nsec_sigs, create Net::DNS::RR::RRSIG([$fwd_apex_nsec],$private); } ############################################################################### @rev_key_sigs = (); for my $private (@rev_ksk_private, @rev_zsk_private) { push @rev_key_sigs, create Net::DNS::RR::RRSIG([@rev_keys],$private); } @rev_soa_sigs = (); foreach my $private (@rev_zsk_private) { push @rev_soa_sigs, create Net::DNS::RR::RRSIG([$rev_soa],$private); } @rev_ns_sigs = (); foreach my $private (@rev_zsk_private) { push @rev_ns_sigs, create Net::DNS::RR::RRSIG([@rev_ns],$private); } $rev_apex_nsec = Net::DNS::RR->new(&generate_rev_nsec("",$rev_dom, $rev_hostdigits)); @rev_apex_nsec_sigs = (); foreach my $private (@rev_zsk_private) { push @rev_apex_nsec_sigs, create Net::DNS::RR::RRSIG([$rev_apex_nsec],$private); } ############################################################################### } $initialized_time = time; } ################################################################### # Initialize and start main loop ################################################################### my $ns = Net::DNS::Nameserver->new( LocalAddr => [ @serveraddr ], LocalPort => $serverport, ReplyHandler => \&reply_handler, Verbose => 0, ); if ($ns) { open(F, "> $pid_file") || die "couldn't write $pid_file"; print F $$, "\n"; close(F); $ns->main_loop; } else { die "couldn't create nameserver object\n"; } ################################################################### # Load DNSKEY file ################################################################### sub load_file { my ($file) = @_; my ($line); open(F, "$file") || die "cannot open $file: $!"; $line = ; chomp($line); close(F); return $line; } ################################################################### # Reverse domainname -> hostname label ################################################################### sub rev2host { my $dom = shift; if ($dom =~ /^(([0-9a-fA-F]\.){32})ip6\.arpa\.?$/i) { return join("", reverse(split(/\./, $1))).'.'.$fwd_dom; } return ''; } ################################################################### # Forward hostname to IPv6 address ################################################################### sub host2addr { my $dom = shift; my $addr = ''; if ($dom =~ /^([0-9a-fA-F]{32})\.$fwd_dom\.?$/i) { my $t = $1; $addr = join(':', ($t =~ m/.{4}/g)); } return $addr; } ################################################################### # forward lookup NSEC support ################################################################### sub nexthostlabel { my $label = shift; return '00000000000000000000000000000000' if ($label eq ''); $label =~ tr/A-Z/a-z/; return '' unless ($label =~ /^[0-9a-f]{32}$/); my $L = substr($label, 16, 16); my $H = substr($label, 0, 16); if ($L eq 'ffffffffffffffff') { if ($H eq 'ffffffffffffffff') { return ''; } $label = sprintf("%016x0000000000000000", hex($H)+1); } else { $label = $H.sprintf("%016x", hex($L)+1); } return $label; } sub previoushostlabel { my $label = shift; $label =~ tr/A-Z/a-z/; return '' unless ($label =~ /^[0-9a-f]{32}$/); my $L = substr($label, 16, 16); my $H = substr($label, 0, 16); if ($L eq '0000000000000000') { if ($H eq '0000000000000000') { return ''; } $label = sprintf("%016xffffffffffffffff", hex($H)-1); } else { $label = $H.sprintf("%016x", hex($L)-1); } return $label; } sub nearesthostlabel { my $label = shift; $label =~ tr/A-Z/a-z/; return $label if ($label =~ /^[0-9a-f]{32}$/); return '' if ($label eq ''); my $first = substr($label, 0, 1); return '' if (ord($first) < ord('0')); return '9fffffffffffffffffffffffffffffff' if (ord($first) > ord('9') && ord($first) < ord('a')); return 'ffffffffffffffffffffffffffffffff' if (ord($first) > ord('f')); if ($label =~ /^([0-9a-f]+)([^0-9a-f]?.*)$/) { my ($H, $L) = ($1, $2); if (length($H) >= 32) { return substr($H, 0, 32); } my $stub = ''; for (my $i = 32-length($H); $i > 0; $i--) { $stub .= '0'; } if ($L ne '') { return $H . $stub; } return &previoushostlabel($H.$stub); } else { $label = ''; } } sub generate_fwd_nsec { my $labels = shift; my $base = shift; my @labels = split(/\./, $labels); my $label = $labels[$#labels]; my $nearest = &nearesthostlabel($label); my $nextlabel = &nexthostlabel($nearest); if ($nextlabel eq '') { $nextlabel = $base; } else { $nextlabel = $nextlabel.'.'.$base; } if ($nearest ne '') { return "$nearest.$base $ttl IN NSEC $nextlabel ( AAAA NSEC RRSIG )"; } return ''; } ################################################################### # reverse lookup NSEC support ################################################################### sub rev_decrement { my @a = @_; my $carry = 1; my $i = $#a; while ($i >= 0 && $carry > 0) { my $k = hex($a[$i]); if ($k == 0) { $carry = 1; $a[$i] = 'f'; } else { $carry = 0; $a[$i] = sprintf("%1x", $k-1); } $i--; } return ($carry > 0) ? () : @a; } sub rev_increment { my @a = @_; my $carry = 1; my $i = $#a; while ($i >= 0 && $carry > 0) { my $k = hex($a[$i]); if ($k >= 15) { $carry = 1; $a[$i] = '0'; } else { $carry = 0; $a[$i] = sprintf("%1x", $k+1); } $i--; } return ($carry > 0) ? () : @a; } sub generate_rev_nsec { my $labels = shift; my $base = shift; my $hostdigits = shift; my $apex = shift; my @labels = reverse split(/\./, $labels); my @result; my $i = $#labels; if ($labels eq '') { $labels = ""; for ($i = 0; $i < $hostdigits; $i++) { $labels .= "0."; } return "$base $ttl IN NSEC $labels$base ( SOA NS DNSKEY NSEC RRSIG )"; } for ($i = 0; $i < $hostdigits; $i++) { if ($i > $#labels) { for (; $i < $hostdigits; $i++) { push @result, '0'; } @result = &rev_decrement(@result); } else { my $label = $labels[$i]; my $f = substr($label, 0, 1); if (ord($f) < ord('0')) { for (; $i < $hostdigits; $i++) { push @result, '0'; } @result = &rev_decrement(@result); } elsif (ord($f) > ord('9') && (ord($f) < ord('a'))) { push @result, '9'; $i++; for (; $i < $hostdigits; $i++) { push @result, 'f'; } } elsif (ord($f) > ord('f')) { push @result, 'f'; $i++; for (; $i < $hostdigits; $i++) { push @result, 'f'; } } elsif (length($label) > 1) { push @result, $f; $i++; for (; $i < $hostdigits; $i++) { push @result, 'f'; } } else { push @result, $label; } } } if ($#result < 0) { return ''; } my $newlabel = join(".", reverse @result).".".$base; my $newnext = ""; my @next = &rev_increment(@result); if ($#next < 0) { $newnext = $base; } else { $newnext = join(".", reverse @next).".".$base; } return "$newlabel $ttl IN NSEC $newnext ( PTR NSEC RRSIG )"; } ################################################################### # Main reply handler ################################################################### sub reply_handler { my ($qname, $qclass, $qtype, $peerhost,$query,$conn) = @_; my ($rcode, @ans, @auth, @add); my (@qadd, $rr, $nsec, $addr, $host, $qnames, $short); my ($opt, $opt_do) = ('', 0); my $do_dnssec; my $now = time; if ($initialized_time + $reload_interval < $now) { print "$now initialize\n"; &initialize; } $qname = '.' if ($qname eq ''); ($qnames = $qname) =~ y/A-Z/a-z/; $rcode = 'REFUSED'; @qadd = $query->additional; if ($#qadd > 0) { return ("SERVFAIL", \@ans, \@auth, \@add, { aa => 1 }); } elsif ($#qadd == 0) { $opt = $qadd[0]; if ($opt->type ne "OPT") { return ("SERVFAIL", \@ans, \@auth, \@add, { aa => 1 }); } $opt_do = $opt->do ? 1 : 0; push @add, $opt; } print "$now $peerhost $qname $qclass $qtype $opt_do\n" if ($querylog); $do_dnssec = $enable_dnssec && $opt_do; if ($qnames =~ /^(.*)\.$rev_dom.?$/i) { $short = $1; $host = &rev2host($qname); if ($host ne '') { if (defined($static_ptr{$qnames})) { $host = $static_ptr{$qnames}; } if ($qtype eq "PTR") { $rr = Net::DNS::RR->new("$qname $ttl $qclass $qtype $host"); push @ans, $rr; if ($do_dnssec) { foreach my $private (@rev_zsk_private) { push @ans, create Net::DNS::RR::RRSIG([$rr],$private); } } push @auth, @rev_ns; print "added auth, rev_ns\n"; push @auth, @rev_ns_sigs if ($do_dnssec); $rcode = "NOERROR"; } else { push @auth, $rev_soa; if ($do_dnssec) { push @auth, @rev_soa_sigs; $nsec = &generate_rev_nsec($short, $rev_dom, $rev_hostdigits); if ($nsec ne "") { $nsec = Net::DNS::RR->new($nsec); foreach my $private (@rev_zsk_private) { push @auth, $nsec, create Net::DNS::RR::RRSIG([$nsec], $private); } } push @auth, $rev_apex_nsec, @rev_apex_nsec_sigs; } $rcode = "NOERROR"; } } else { $rcode = "NXDOMAIN"; push @auth, $rev_soa; if ($do_dnssec) { push @auth, @rev_soa_sigs, $rev_apex_nsec, @rev_apex_nsec_sigs; $nsec = &generate_rev_nsec($short, $rev_dom, $rev_hostdigits); if ($nsec ne "") { $nsec = Net::DNS::RR->new($nsec); foreach my $private (@rev_zsk_private) { push @auth, $nsec, create Net::DNS::RR::RRSIG([$nsec],$private); } } } } } elsif ($qnames =~ /^(.*)\.$fwd_dom\.?$/i) { $short = $1; $addr = &host2addr($qname); if ($addr ne '') { if ($qtype eq 'AAAA') { $rr = Net::DNS::RR->new("$qname $ttl $qclass $qtype $addr"); push @ans, $rr; if ($do_dnssec) { foreach my $private (@fwd_zsk_private) { push @ans, create Net::DNS::RR::RRSIG([$rr],$private); } } push @auth, @fwd_ns; push @auth, @fwd_ns_sigs if ($do_dnssec); $rcode = "NOERROR"; } elsif ($qtype eq 'NSEC' && $do_dnssec) { $nsec = &generate_fwd_nsec($short,$fwd_dom); if ($nsec != '') { my $nsecRR = Net::DNS::RR->new($nsec); push @auth, $nsecRR; foreach my $private (@fwd_zsk_private) { push @auth, create Net::DNS::RR::RRSIG([$nsecRR],$private); } } } else { # Generate existing NSEC RR push @auth, $fwd_soa; if ($do_dnssec) { push @auth, @fwd_soa_sigs; $nsec = &generate_fwd_nsec($short,$fwd_dom); if ($nsec != '') { my $nsecRR = Net::DNS::RR->new($nsec); push @auth, $nsecRR; foreach my $private (@fwd_zsk_private) { push @auth, create Net::DNS::RR::RRSIG([$nsecRR],$private); } } } $rcode = "NOERROR"; } } else { push @auth, $fwd_soa; if ($do_dnssec) { push @auth, $fwd_apex_nsec, @fwd_apex_nsec_sigs, @fwd_soa_sigs; $nsec = &generate_fwd_nsec($short, $fwd_dom); if ($nsec ne '') { my $nsecRR = Net::DNS::RR->new($nsec); foreach my $private (@fwd_zsk_private) { push @auth, $nsecRR, create Net::DNS::RR::RRSIG([$nsecRR],$private); } } } $rcode = "NXDOMAIN"; } } elsif ($qnames eq $rev_dom) { if ($qtype eq "NS") { push @ans, @rev_ns; push @ans, @rev_ns_sigs if ($do_dnssec); } elsif ($qtype eq "SOA") { push @ans, $rev_soa; push @ans, @rev_soa_sigs if ($do_dnssec); } elsif ($qtype eq "DNSKEY" && $do_dnssec) { push @ans, @rev_keys, @rev_key_sigs; } elsif ($qtype eq "NSEC" && $do_dnssec) { push @ans, $rev_apex_nsec, @rev_apex_nsec_sigs; } else { push @auth, $rev_soa; push @auth, @rev_soa_sigs, $rev_apex_nsec, @rev_apex_nsec_sigs if ($do_dnssec); } $rcode = "NOERROR"; } elsif ($qnames eq $fwd_dom) { if ($qtype eq "NS") { push @ans, @fwd_ns; push @ans, @fwd_ns_sigs if ($do_dnssec); } elsif ($qtype eq "SOA") { push @ans, $fwd_soa; push @ans, @fwd_soa_sigs if ($do_dnssec); } elsif ($qtype eq "DNSKEY" && $do_dnssec) { push @ans, @fwd_keys, @fwd_key_sigs; } elsif ($qtype eq "NSEC" && $do_dnssec) { push @ans, $fwd_apex_nsec, @fwd_apex_nsec_sigs; } else { push @auth, $fwd_soa; push @auth, @fwd_soa_sigs, $fwd_apex_nsec, @fwd_apex_nsec_sigs if ($do_dnssec); } $rcode = "NOERROR"; } else { $rcode = "REFUSED"; } if ($debug) { print "Return RCODE=$rcode\n"; print "Answer:\n"; for my $a (@ans) { $a->print; }; print "Authority:\n"; for my $a (@auth) { $a->print; }; print "Additional:\n"; for my $a (@add) { $a->print; }; } return ($rcode, \@ans, \@auth, \@add, { aa => 1 }); } =head1 NAME Auto-generating IPv6 Reverse DNS server with DNSSEC online signing =head1 DESCRIPTION v6rsec2.pl is a DNS server which acts a master of specified IPv6 reverse zone and its forward zone. It support DNSSEC online signing. =head1 USAGE v6rsec2.pl requires one argument, a configuration file path. =head2 configuration file It contains colon sparated field name and field value. The value field may contain multiple value separated by comma. A line starts with '#', ';', ' ' and TAB character is treated as a comment. server_address: 127.0.0.1, ::1 # DNS server IP address server_port: 53 # DNS server port number pid_file: /var/run/v6rev.pid # process ID file reconfig_interval: 3600 # The DNS server reloads the config file periodically. reverse_domainname: 6.0.0.0.2.3.1.0.0.0.2.0.1.0.0.2.ip6.arpa # Reverse zone name forward_domainname: user.dnslab.jp # Forward zone name keyfile_dir: . # The directory which the DNSSEC key files are put. ttl: 3600 # TTL nsname: v6rev.dnslab.jp, v6rev2.dnslab.jp # DNS server names. They must be out-bailiwick. enable_dnssec: 1 # Enable DNSSEC or not querylog: 1 # Enable querylog to stdout static_ptr: REVERSE_DOMAIN_FQDNHOSTNAME_FQDN # Define fixed reverse hostname. # For example, static_ptr: 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa rev.example.jp # Multiple definitions are allowed. # Forward mapping is not generated. Reverse only. =head1 Limitations This version does not support multiple IPv6 prefix. This program runs in the foreground and outputs messages to STDOUT and STDERR. Some wrapper script may be required. =head1 Author Kazunori Fujiwara =head1 Copyright and License Copyright(c) 2009,2010 Japan Registry Services Co., Ltd. All rights reserved. By using this file, you agree to the terms and conditions set forth bellow. LICENSE TERMS AND CONDITIONS The following License Terms and Conditions apply, unless a different license is obtained from Japan Registry Services Co., Ltd. ("JPRS"), a Japanese corporation, Chiyoda First Bldg. East 13F, 3-8-1 Nishi-Kanda, Chiyoda-ku, Tokyo 101-0065, Japan. 1. Use, Modification and Redistribution (including distribution of any modified or derived work) in source and/or binary forms is permitted under this License Terms and Conditions. 2. Redistribution of source code must retain the copyright notices as they appear in each source code file, this License Terms and Conditions. 3. Redistribution in binary form must reproduce the Copyright Notice, this License Terms and Conditions, in the documentation and/or other materials provided with the distribution. For the purposes of binary distribution the "Copyright Notice" refers to the following language: "Copyright(c) 2007,2008 Japan Registry Services Co., Ltd. All rights reserved." 4. The name of JPRS may not be used to endorse or promote products derived from this Software without specific prior written approval of JPRS. 5. Disclaimer/Limitation of Liability: THIS SOFTWARE IS PROVIDED BY JPRS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JPRS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. =cut