#! /usr/bin/perl # Copyright (C) 2014 Sergey Poznyakoff # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3, or (at your option) # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . use strict; use Getopt::Long qw(:config gnu_getopt no_ignore_case); use IO::Socket; use Pod::Usage; use Pod::Man; use Socket qw(:DEFAULT :crlf inet_ntoa); use Net::CIDR; use Whoseip::DB qw(:all); use constant EX_OK => 0; use constant EX_USAGE => 64; # command line usage error use constant EX_DATAERR => 65; # data format error use constant EX_NOINPUT => 66; # cannot open input file use constant EX_SOFTWARE => 70; # internal software error (not used yet) use constant EX_OSFILE => 72; # critical OS file missing use constant EX_CANTCREAT => 73; # can't create (user) output file my $progname; # This script name; ($progname = $0) =~ s/.*\///; my $progdescr = "Identifies IP addresses"; my $debug; my @ipv4list; my $ipv4rx = '\d{1,3}((\.\d{1,3}){3})'; my $delim = $LF; # Output delimiter my $dbf; my $dbfile; my %fmtab = (unix => '${status} $?{diag}{${diag}}{${country} ${cidr} ${range} ${count}} ', cgi => 'Content-Type: text/xml ${status} $?{diag}{${diag}}{${country} ${cidr} ${range} ${count}} $?{term}{${term}} ' ); sub error { my $msg = shift; local %_ = @_; print STDERR "$progname: " if defined($progname); print STDERR "$_{prefix}: " if defined($_{prefix}); print STDERR "$msg\n" } sub debug { my $l = shift; error(join(' ',@_), prefix => 'DEBUG') if $debug >= $l; } sub abend { my $code = shift; print STDERR "$progname: " if defined($progname); print STDERR "@_\n"; exit $code; } sub read_config_file($) { my $config_file = shift; print STDERR "reading $config_file\n" if ($debug); open(my $fd, "<", $config_file) or die("cannot open $config_file: $!"); while (<$fd>) { chomp; s/\s+$//; if (/\\$/) { chop; $_ .= <$fd>; redo; } s/^\s+//; s/\s+=\s+/=/; s/#.*//; next if ($_ eq ""); unshift(@ARGV, "--$_"); } close $fd; } sub read_ipv4list { my $file = shift; open(my $fd, "<", $file) or abend(EX_NOINPUT, "can't open $file for reading: $!"); my $line = 0; @ipv4list = (); while (<$fd>) { ++$line; chomp; s/#.*//; s/^\s+//; s/\s+$//; next if ($_ eq ""); unless (/^([\d\.]+)\/(\d+)\s+([\w\.]+)$/) { error("$file:$line: malformed line"); next; } my $srv = $3; next if $srv eq 'UNKNOWN'; my $msk = $2; my $ip = str2ipv4($1); $srv = "whois.$srv.net" unless ($srv =~ /\./); push @ipv4list, [ $ip, (0xffffffff^(0xffffffff>>$msk)), $srv ]; } close $fd; } sub str2ipv4 { my $ipstr = shift; my @ip = split(/\./, $ipstr); return ($ip[0] << 24) + ($ip[1] << 16) + ($ip[2] << 8) + $ip[3]; } sub range2count { my $count = 0; foreach my $arg (@_) { my @a = split /-/, shift; next unless $#a == 1; $count += str2ipv4($a[1]) - str2ipv4($a[0]) + 1; } return $count; } sub cidr_to_range { my @a; @a = sort { $a->[0] <=> $b->[0] } map { map { [ map { str2ipv4($_) } split(/-/, $_, 2) ] } Net::CIDR::cidr2range($_) } split /,/, shift; for (my $i = 1; $i <= $#a; $i++) { if ($a[$i]->[0] == $a[$i-1]->[1] + 1) { $a[$i-1]->[1] = $a[$i]->[1]; splice @a, $i, 1; } } return join ',', map { inet_ntoa(pack('N', $_->[0])) . '-' . inet_ntoa(pack('N', $_->[1])) } @a; } # ############ # ARIN # ############ sub arin_fmt { my $q = shift; return "n + $q"; } sub arin_decode { my ($input, $ref) = @_; return if ($input =~ /^#/ or $input eq ''); if ($input =~ /^NetRange:\s+(.+)/) { my $r = $1; $r =~ s/\s+//g; my $n = range2count($r); if (!defined($ref->{count}) or $ref->{count} > $n) { $ref->{range} = $r; $ref->{cidr} = join ',', Net::CIDR::range2cidr($r); $ref->{count} = $n; delete $ref->{country} } } elsif ($input =~ /^Country:\s+(.+)/ and !defined($ref->{country})) { $ref->{country} = $1; } } # ############ # RIPE # ############ use constant RIPE_INIT => 0; use constant RIPE_TEXT => 1; use constant RIPE_IGNR => 2; sub ripe_decode { my ($input, $ref) = @_; return if ($input =~ /^%/); if ($ref->{state} == RIPE_INIT) { if ($input eq '') { return; } else { $ref->{state} = RIPE_TEXT; } } if ($ref->{state} == RIPE_TEXT) { if ($input =~ /^inetnum:\s+(.+)/) { my $r = $1; $r =~ s/\s+//g; $ref->{range} = $r; $ref->{count} = range2count($r); $ref->{cidr} = join ',', Net::CIDR::range2cidr($r); } elsif ($input =~ /^country:\s+(.+)/) { $ref->{country} = $1; } elsif ($input eq '') { $ref->{state} = RIPE_IGNR; } } } # ############ # LACNIC # ############ sub lacnic_decode { my ($input, $ref) = @_; return if ($input =~ /^%/); if ($ref->{state} == RIPE_INIT) { if ($input eq '') { return; } else { $ref->{state} = RIPE_TEXT; } } if ($ref->{state} == RIPE_TEXT) { if ($input =~ /^inetnum:\s+(.+)/) { my $cidr = $1; if ($cidr =~ m#^(\d{1,3})/(\d+)#) { $cidr = "$1.0.0.0/$2"; } elsif ($cidr =~ m#^(\d{1,3}\.\d{1,3})/(\d+)#) { $cidr = "$1.0.0/$2"; } elsif ($cidr =~ m#^(\d{1,3}\.\d{1,3}\.\d{1,3})/(\d+)#) { $cidr = "$1.0/$2"; } $ref->{cidr} = $cidr; $ref->{range} = cidr_to_range($cidr); $ref->{count} = range2count($ref->{range}); } elsif ($input =~ /^country:\s+(.+)/) { $ref->{country} = $1; } elsif ($input eq '') { $ref->{state} = RIPE_IGNR; } } } # ################### # rwhois.gin.ntt.net # ################### sub ntt_decode { my ($input, $ref) = @_; if ($input =~ /^\s+(${ipv4rx}\s*-\s*${ipv4rx})/) { my $r = $1; $r =~ s/\s+//g; my $c = range2count($r); if (!defined($ref->{count}) or $ref->{count} > $c) { $ref->{count} = $c; $ref->{range} = $r; $ref->{cidr} = join ',', Net::CIDR::range2cidr($r); $ref->{country} = 'US'; } } } # ############ # TWNIC # ############ sub twnic_decode { my ($input, $ref) = @_; if ($input =~ /^\s+Netblock:\s+(.+)/) { my $r = $1; $r =~ s/\s+//g; $ref->{range} = $r; $ref->{count} = range2count($r); $ref->{cidr} = join ',', Net::CIDR::range2cidr($r); $ref->{country} = 'TW'; } } ################### # whois.nic.ad.jp ################### sub nic_ad_jp_fmt { my $q = shift; return "NET $q/e"; } sub nic_ad_jp_decode { my ($input, $ref) = @_; if ($input =~ /^a\.\s+\[Network Number\]\s+(.+)/) { $ref->{cidr} = $1; $ref->{range} = cidr_to_range($ref->{cidr}); $ref->{count} = range2count($ref->{range}); $ref->{country} = 'JP'; } } ################### # whois.nic.or.kr ################### sub nic_or_kr_decode { my ($input, $ref) = @_; if ($input =~ /^IPv4 Address\s*:\s+(${ipv4rx}\s*-\s*${ipv4rx})/) { my $r = $1; $r =~ s/\s+//g; my $c = range2count($r); if (!defined($ref->{count}) or $ref->{count} > $c) { $ref->{count} = $c; $ref->{range} = $r; $ref->{cidr} = join ',', Net::CIDR::range2cidr($r); $ref->{country} = 'KR'; } } } sub nobistech_decode { my ($input, $ref) = @_; if ($input =~ /network:IP-Network:(.+)/) { $ref->{cidr} = $1; $ref->{range} = cidr_to_range($1); $ref->{count} = range2count($ref->{range}); } elsif ($input =~ /network:Country-Code:(.+)/) { $ref->{country} = $1; } } # ####################################################################### # Server table # ####################################################################### my %srvtab = ( 'whois.arin.net' => { q => \&arin_fmt, d => \&arin_decode }, 'whois.lacnic.net' => { d => \&lacnic_decode }, 'whois.ripe.net' => { d => \&ripe_decode }, 'rwhois.gin.ntt.net' => { d => \&ntt_decode }, 'whois.twnic.net' => { d => \&twnic_decode }, 'whois.nic.ad.jp' => { q => \&nic_ad_jp_fmt, d => \&nic_ad_jp_decode }, 'whois.nic.br' => { d => \&lacnic_decode }, 'whois.nic.or.kr' => { d => \&nic_or_kr_decode }, 'rwhois.nobistech.net' => { d => \&nobistech_decode } ); sub format_query { my ($srv, $term) = @_; if (defined($srvtab{$srv}{q})) { return &{$srvtab{$srv}{q}}($term); } else { return $term; } } sub findsrv { my $ip = str2ipv4(shift); foreach my $r (@ipv4list) { debug(3, "findsrv: $ip $r->[0]/$r->[1]"); return $r->[2] if ($ip & $r->[1]) == $r->[0]; } return undef; } sub whois($$) { my $ip = shift; my $server = shift; my $port = 43; if ($server =~ /(.+):(.+)/) { $server = $1; $port = $2; } debug(1,"querying $ip from $server:$port"); my $sock = new IO::Socket::INET (PeerAddr => $server, PeerPort => $port, Proto => 'tcp'); my $expiration = undef; my @collect; unless ($sock) { error("could not connect to $server:$port: $!"); return undef; } print $sock format_query($server, $ip)."\n"; my $decode; if (defined($srvtab{$server}{d})) { $decode = $srvtab{$server}{d}; } else { $decode = \&ripe_decode; } local $/ = LF; my %res; while (<$sock>) { chomp; debug(4, "RECV: $_"); if (/%% referto: whois -h (\S+) -p (\S+)/) { $res{referto} = "$1:$2"; debug(1, "found reference to $res{referto}"); } elsif (m#ReferralServer: r?whois://(.+)#) { $res{referto} = $1; $res{referto} =~ s#/$##; debug(1, "found reference to $res{referto}"); } else { &{$decode}($_, \%res); } } close $sock; # while (my ($k,$v) = each %res) { # print "$k $v\n"; # } return %res; } sub serve { my $term = shift; my %res; if ($term =~ /^${ipv4rx}$/) { if (defined($dbf)) { %res = ipdb_lookup($dbf, $term); if (defined($res{country})) { $res{status} = 'OK'; unless (defined($res{cidr})) { $res{cidr} = Net::CIDR::addrandmask2cidr($res{network}, $res{netmask}); } $res{range} = cidr_to_range($res{cidr}); $res{count} = range2count($res{range}); return %res; } } my $srv = findsrv($term); if (defined($srv) and $srv ne 'UNKNOWN') { my %prev; while (%res = whois($term, $srv), and defined($res{referto})) { %prev = %res if $res{status} = 'OK'; $srv = $res{referto}; } %res = %prev if (!defined($res{country}) and defined($prev{country})); if (!defined($res{country})) { $res{status} = 'NO'; $res{diag} = 'IP unknown'; } else { $res{status} = 'OK'; if (defined($dbf)) { foreach my $cidr (split /,/, $res{cidr}) { ipdb_insert($dbf, $cidr, $res{country}, cidr=>$res{cidr}); } } } } else { $res{status} = 'NO'; $res{diag} = 'whois server unknown'; } } else { $res{status} = 'BAD'; $res{diag} = 'invalid input'; } $res{term} = $term; return %res; } # ####################################################################### # Create a copy of this program with ipv4list embedded # ####################################################################### sub whoseip_dump { my ($opt,$file) = @_; open(my $ifd, "<", $0) or abend(EX_NOINPUT, "can't open $0 for reading"); open(my $ofd, ">", $file) or abend(EX_CANTCREAT, "can't open $file for writing"); my $zapto; my $line = 0; while (<$ifd>) { ++$line; if (defined($zapto)) { $zapto = undef if /$zapto/; next; } if (/^my \@ipv4list\s*(.*)/) { my $tail = $1; if ($tail =~ /^=\s*\(/) { $zapto = '^\);$'; } elsif ($tail !~ /^;/) { error("$file:$line: unrecognized @ipv4list initializer"); print $ofd $_; next; } print $ofd "my \@ipv4list = (\n"; foreach my $x (@ipv4list) { print $ofd "[ $x->[0], $x->[1], '$x->[2]' ],\n"; } print $ofd ");\n"; } else { print $ofd $_; } } close $ifd; close $ofd; exit 0; } # ####################################################################### # Output functions # ####################################################################### sub read_format { my $file = shift; open(my $fd, "<", $file) or die "can't open $file for reading"; my $res; while (<$fd>) { chomp; if (/\\$/) { chop; $_ .= <$fd>; redo; } next if /^#/; $res .= "$_\n"; } close $fd; return $res; } sub getsegm { my $sref = shift; my $s = ${$sref}; my $level = 0; my $res; while ($s =~ /(.*?[{}])(.*)/s) { $res .= $1; $s = $2; if ($res =~ /[\$\?l]\{$/s) { if ($s =~ /(\w+\})(.*)/s) { $res .= $1; $s = $2; } } elsif ($res =~ /{$/) { ++$level; } elsif ($res =~ /}$/) { last if (--$level == 0); } } ${$sref} = $s; $res =~ s/^\{//s; $res =~ s/\}$//s; return $res; } sub output { my $s = shift; my %esctab = (a => "\a", b => "\b", e => "\e", f => "\f", n => "\n", r => "\r", t => "\t", v => "\v"); $s =~ s/\$l{(\w+)\}/length($_{$1})/sgex; $s =~ s/\$\{(\w+)\}/$_{$1}/sgex; $s =~ s/\\([\\abefnrtv])/$esctab{$1}/sgex; print $s; } sub format_out { my $fmt = shift; local %_ = @_; while ($fmt =~ /(.*?)\$\?\{(\w+)\}(.*)/s) { output($1); my $v = $2; $fmt = $3; my $t = getsegm(\$fmt); my $f; $f = getsegm(\$fmt) if ($fmt =~ /^\{/); if (defined($_{$v})) { format_out($t, @_); } elsif (defined($f)) { format_out($f, @_); } } output($fmt); } sub docgi { my ($fmt, $env) = @_; my $term; my %res; if ($env->{QUERY_STRING} =~ /^$ipv4rx$/) { $term = $env->{QUERY_STRING}; } else { my %q = map { /(.+?)=(.*)/ ? ($1 => $2) : ($1 => 1); } split(/\&/, $env->{QUERY_STRING}); if (defined($q{fmt})) { if (defined($fmtab{$q{fmt}})) { if ($fmtab{$q{fmt}} =~ /^Content-Type:/) { $fmt = $fmtab{$q{fmt}}; } else { %res = (status => 'BAD', diag => 'invalid format') } } else { %res = (status => 'BAD', diag => 'format undefined'); } } $term = $q{ip} if defined($q{ip}); } unless (defined($res{status})) { if (defined($term)) { %res = serve($term); } else { %res = (status => 'BAD', diag => 'search term invalid or missing'); } } format_out($fmt, %res); } # ####################################################################### # Main # ####################################################################### my $output_format; my $fastcgi; my $single_query; if (defined($ENV{WHOSEIP_CONF})) { read_config_file($ENV{WHOSEIP_CONF}); } elsif (-r "/etc/whoseip.conf") { read_config_file("/etc/whoseip.conf"); } GetOptions("h" => sub { pod2usage(-message => "$progname: $progdescr", -exitstatus => 0); }, "help" => sub { pod2usage(-exitstatus => EX_OK, -verbose => 2); }, "usage" => sub { pod2usage(-exitstatus => EX_OK, -verbose => 0); }, "debug|d+" => \$debug, "ip-list|i=s" => sub { read_ipv4list($_[1]); }, "dump|D=s" => \&whoseip_dump, "define-format=s" => sub { my @a = split /\s*=\s*/, $_[1], 2; $fmtab{$a[0]} = $a[1]; }, "format|f=s" => \$output_format, "format-file|formfile|F=s" => sub { if ($_[1] =~ /=/) { my @a = split /\s*=\s*/, $_[1], 2; $fmtab{$a[0]} = read_format($a[1]); } else { $output_format = read_format($_[1]); } }, "fastcgi:s" => \$fastcgi, "cache-file|c:s" => \$dbfile, "no-cache|N" => sub { $dbfile = undef; }, "single-query" => \$single_query ) or exit(EX_USAGE); if (defined($dbfile)) { $dbfile .= "whoseip.db" if (-d $dbfile); $dbf = ipdb_open($dbfile, debug => $debug); } if (defined($fastcgi)) { if ($fastcgi eq '') { $fastcgi = 1; } else { my @suf = split /\s+/, $fastcgi; $fastcgi = undef; foreach my $s (@suf) { if ($0 =~ /$s$/) { $fastcgi = 1; last; } } } } else { $fastcgi = $0 =~ /\.fcgi$/; } if (defined($output_format) and $output_format =~ /@(.+)/) { abend(EX_USAGE, "format $1 not defined") unless defined $fmtab{$1}; $output_format = $fmtab{$1}; } if ($fastcgi) { eval { require FCGI; 1; } or do { my $msg = $@; if ($debug) { abend(EX_OSFILE, "can't load CGI::Fast: $@"); } else { abend(EX_OSFILE, "can't load CGI::Fast"); } }; $output_format = $fmtab{cgi} unless defined($output_format); my $req = FCGI::Request(); while ($req->Accept() >= 0) { docgi($output_format, $req->GetEnvironment()); } } elsif ($ENV{GATEWAY_INTERFACE} =~ m#CGI/\d+\.\d+#) { $output_format = $fmtab{cgi} unless defined($output_format); docgi($output_format, \%ENV); } else { my $term; my %res; ipdb_locker($dbf, lock => 'shared') if (defined($dbf)); $output_format = $fmtab{unix} unless defined($output_format); if ($#ARGV == -1) { unless (-t *STDIN) { local $/ = CRLF; $delim = "$CR$LF"; } while (<>) { chomp; %res = serve($_); format_out($output_format, %res); last if $single_query; } } else { foreach my $term (@ARGV) { format_out($output_format, serve($term)); } } } ipdb_close($dbf) if defined($dbf); __END__ =head1 NAME whoseip - return information about IP address =head1 SYNOPSIS B [B<-dh>] [B<-F> I] [B<-D> I] [B<-i> I] [B<--debug>] [B<--define-format=>IB<=>I] [B<--dump=>I] [B<--fastcgi=>[I]] [B<--format=>I] [B<--format-file=>[IB<=>]I] [B<--formfile=>I] [B<--help>] [B<--ip-list=>I] [B<--single-query>] [B<--usage>] [I...] =head1 DESCRIPTION For each IP address, B returns the country it is located in (a ISO 3166-1 code), the network it belongs to and the number of addresses in the network. The program can operate in several modes. If the program name ends in B<.fcgi> the B mode is enabled. This mode is also enabled if the command line option B<--fastcgi> is given without arguments, or if the program name ends in one of the suffixes supplied in the argument to this option (a whitespace-separated list). In this mode, the the IP address to look for is taken from the parameter B. Additional parameter B can be used to supply the name of the desired output format. The format must be either one of the built-in formats, or must be defined using the B<--define-format> option (see below). As a shortcut, the invocation command line containing an IP alone is also recognized. Otherwise, when one or more IP addresses are given in the command line, B prints the data for each of them on the standard output. This is B mode. If called without arguments, the program checks if the environment variable B is defined and contains B> (where I is the version number). If so, it assumes B mode. In this mode the command line is parsed the same way as in B mode. If B is not set, the program reads IP addresses from input (one per line) and prints replies for each of them. This is B. To summarize: =over 4 =item 1. Start it from the command line with one or more IPs given as arguments, if you wish to get info about these IPs. =item 2. Add it to B if you want to query it remotely as a service, e.g.: whois stream tcp nowait nobody /usr/bin/whoseip =item 3. Copy it to your B directory to use it with a B server as a B. =item 4. Link it to B to use it as a B application (or use the B<--fastcgi> option). =back Output formats are configurable and depend on the mode B runs in. In command line and inetd modes, the default output format is: =over 4 B I I I I =back where I is country code, I is network block in CIDR notation, I is network block as a range of IP addresses, and I is number of IP address in the network block. If the specified IP address is not found, the reply is =over 4 B I =back where I is a human-readable explanatory message. If the input is invalid, the reply is: =over 4 B I =back In B and B modes, the output is represented as XML, as shown in the example below: OK US 192.0.2.0/24 192.0.2.0-192.0.2.255 255 192.0.2.10 The following example illustrates the reply if the IP is not found: NO IP unknown 43.0.0.1 See the section B below for a discussion on how to customize output formats. =head1 OPTIONS =over 4 =item B<-D>, B<--dump=>I Dump the program to I. This is normally done to update the built-in server list, e.g.: whoseip --ip-list=ip_del_list --dump=whoseip.new Note, that B<--dump> must be last option in the command line. =item B<-d>, B<--debug> Increase debugging verbosity. =item B<--define-format=>IB<=>I Define a named format I to be I. Two names are predefined: format B is used to respond to B or B requests, and format B is used when serving requests coming from command line or in inetd mode. See the section B, for a detailed discussion. =item B<--fastcgi=>[I] When used without argument, forces FastCGI mode. If an argument is given, it is treated as a whitespace-separated list of suffixes. In this case, FastCGI mode is enabled if the program name ends in one of these suffixes. If this option is not give, FastCGI is enabled if the program name ends in B<.fcgi>. =item B<-f>, B<--format=>I Sets output format string. If I begins with a B<@>, it is a reference to a named format string (either built-in one or a one created using the B<--define-format> option), and is replaced with the value of the format referred to. For example, B<--format=@cgi> instructs the program to use B format. =item B<-F>, B<--formfile>, B<--format-file=>[IB<=>]I Read output format string from I. If I is supplied, assign the format string to the named format. See the section B, for a detailed discussion. =item B<-i>, B<--ip-list=>I Read the table of B servers from I. Each line in I must have the following format: I I Comments are introduced with a B<#> sign. Empty lines are ignored. Without this option, B uses the built-in list of servers. =item B<--single-query> This option is valid only in B. It instructs B to terminate after replying to the first query. =back The following options cause the program to display informative text and exit: =over 4 =item B<-h> Show a short usage summary. =item B<--help> Show man page. =item B<--usage> Show a concise command line syntax reminder. =back =head1 CONFIGURATION FILE If the file B exists, it is read before processing command line options. If the environment variable B is set, its value is used as the file name, instead of B. The file is read line by line. Long lines can be split over several physical lines by inserting a backslash followed by a newline. Empty lines are ignored. Comments are introduced with the B<#> character. Anything following it up to the logical line is ignored. Each non-empty line must contain a single long command line option, without the leading B<-->. Arguments must be separated from options with an equals sign, optionally surrounded with whitespace. For example: # Assume FastCGI if the program name ends in one of these # suffixes fastcgi = .fcgi .pl # Define output format for CGI and FastCGI modes define-format = cgi=Content-Type: application/json\n\ \n\ { "status": "${status}", \ $?{diag}{"diag": "${diag}"}{\ "country": "${country}",\ "cidr": "${cidr}",\ "range": "${range}",\ "count": "${count}"}\ $?{term}{, "term": "${term}" } }\n =head1 FORMAT Output formats can be redefined using B<--define-format>, B<--format>, and B<--format-file> command line options, or corresponding configuration file keywords. The format string supplied with this options (or in the input file, in case of the B<--format-file> option) can contain the following macro references, which are replaced with the corresponding piece of information on output: =over 4 =item B<${status}> The reply status: B, if the information has been retrieved, B, if it was not, and B, if the input was invalid. =item B<${diag}> Contains explanatory text if B<${status}> is B or B. If it is B, this macro is not defined. =item B<${term}> The input IP address. =item B<${cidr}> The network IP belongs to, as a B. =item B<${range}> The network, as a range of IP addresses. =item B<${count}> Number of IP addresses in the network. =item B<${country}> ISO 3166-1 code of the country where IP address is located. =back If a macro is not defined, the corresponding reference expands to empty string. Conditional expressions evaluate depending on whether a macro is defined. The syntax of a conditional expression is: =over 4 B<$?{I}>B<{>IB<}>B<{>IB<}> =back Its effect is as follows: if the macro I is defined, the I is substituted, otherwise the I is substituted. Conditional expressions can be nested. The escape sequences B<\a>, B<\b>, B<\e>, B<\f>, B<\n>, B<\r>, B<\t>, and B<\v> are replaced according to their traditional meaning. =head1 BUGS Only IPv4 is supported. =head1 AUTHOR Sergey Poznyakoff =cut