# # Shorewall 4.4 -- /usr/share/shorewall/Shorewall/IPAddrs.pm # # This program is under GPL [http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt] # # (c) 2007,2008,2009,2010,2011 - Tom Eastep (teastep@shorewall.net) # # Complete documentation is available at http://shorewall.net # # This program is free software; you can redistribute it and/or modify # it under the terms of Version 2 of the GNU General Public License # as published by the Free Software Foundation. # # 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, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # This module provides interfaces for dealing with IPv4 addresses, protocol names, and # port names. It also exports functions for validating protocol- and port- (service) # related constructs. # package Shorewall::IPAddrs; require Exporter; use Shorewall::Config qw( :DEFAULT split_list require_capability in_hex8 numeric_value F_IPV4 F_IPV6 :protocols %config ); use Socket; use strict; our @ISA = qw(Exporter); our @EXPORT = ( qw( ALLIPv4 ALLIPv6 NILIPv4 NILIPv6 IPv4_MULTICAST IPv6_MULTICAST IPv6_LINKLOCAL IPv6_SITELOCAL IPv6_LOOPBACK IPv6_LINK_ALLNODES IPv6_LINK_ALLRTRS IPv6_SITE_ALLNODES IPv6_SITE_ALLRTRS ALLIP NILIP ALL valid_address validate_address validate_net decompose_net compare_nets validate_host validate_range ip_range_explicit expand_port_range allipv4 allipv6 allip nilipv4 nilipv6 nilip rfc1918_networks resolve_proto resolve_dnsname proto_name validate_port validate_portpair validate_portpair1 validate_port_list validate_icmp validate_icmp6 ) ); our @EXPORT_OK = qw( ); our $VERSION = 'MODULEVERSION'; # # Some IPv4/6 useful stuff # our @allipv4 = ( '0.0.0.0/0' ); our @allipv6 = ( '::/0' ); our $allip; our @allip; our @nilipv4 = ( '0.0.0.0' ); our @nilipv6 = ( '::' ); our $nilip; our @nilip; our $valid_address; our $validate_address; our $validate_net; our $resolve_dnsname; our $validate_range; our $validate_host; our $family; use constant { ALLIPv4 => '0.0.0.0/0' , ALLIPv6 => '::/0' , NILIPv4 => '0.0.0.0' , NILIPv6 => '::' , IPv4_MULTICAST => '224.0.0.0/4' , IPv6_MULTICAST => 'ff00::/8' , IPv6_LINKLOCAL => 'fe80::/10' , IPv6_SITELOCAL => 'feC0::/10' , IPv6_LOOPBACK => '::1' , IPv6_LINK_ALLNODES => 'ff01::1' , IPv6_LINK_ALLRTRS => 'ff01::2' , IPv6_SITE_ALLNODES => 'ff02::1' , IPv6_SITE_ALLRTRS => 'ff02::2' , }; our @rfc1918_networks = ( "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16" ); # # Note: initialize() is declared at the bottom of the file # sub vlsm_to_mask( $ ) { my $vlsm = $_[0]; in_hex8 ( ( 0xFFFFFFFF << ( 32 - $vlsm ) ) & 0xFFFFFFFF ); } sub valid_4address( $ ) { my $address = $_[0]; my @address = split /\./, $address; return 0 unless @address == 4; for ( @address ) { return 0 unless /^\d+$/ && $_ < 256; } 1; } sub validate_4address( $$ ) { my ( $addr, $allow_name ) = @_; my @addrs = ( $addr ); unless ( valid_4address $addr ) { fatal_error "Invalid IP Address ($addr)" unless $allow_name; fatal_error "Unknown Host ($addr)" unless @addrs = gethostbyname( $addr ); if ( defined wantarray ) { shift @addrs for (1..4); for ( @addrs ) { $_ = ( inet_ntoa( $_ ) ); } } } defined wantarray ? wantarray ? @addrs : $addrs[0] : undef; } sub resolve_4dnsname( $ ) { my $net = $_[0]; my @addrs; fatal_error "Unknown Host ($net)" unless @addrs = gethostbyname( $net ); shift @addrs for (1..4); for ( @addrs ) { $_ = ( inet_ntoa( $_ ) ); } @addrs; } sub decodeaddr( $ ) { my $address = $_[0]; my @address = split /\./, $address; my $result = shift @address; for ( @address ) { $result = ( $result << 8 ) | $_; } $result; } sub encodeaddr( $ ) { my $addr = $_[0]; my $result = $addr & 0xff; for my $i ( 1..3 ) { my $a = ($addr = $addr >> 8) & 0xff; $result = "$a.$result"; } $result; } sub validate_4net( $$ ) { my ($net, $vlsm, $rest) = split( '/', $_[0], 3 ); my $allow_name = $_[1]; $net = '' unless defined $net; fatal_error "Missing address" if $net eq ''; if ( $net =~ /\+(\[?)/ ) { if ( $1 ) { fatal_error "An ipset list ($net) is not allowed in this context"; } elsif ( $net =~ /^\+[a-zA-Z][-\w]*$/ ) { fatal_error "An ipset name ($net) is not allowed in this context"; } else { fatal_error "Invalid ipset name ($net)"; } } if ( defined $vlsm ) { fatal_error "Invalid VLSM ($vlsm)" unless $vlsm =~ /^\d+$/ && $vlsm <= 32; fatal_error "Invalid Network address ($_[0])" if defined $rest; fatal_error "Invalid IP address ($net)" unless valid_4address $net; } else { fatal_error "Invalid Network address ($_[0])" if $_[0] =~ '/' || ! defined $net; my $net1 = validate_4address $net, $allow_name; $net = $net1 unless $config{DEFER_DNS_RESOLUTION}; $vlsm = 32; } if ( defined wantarray ) { if ( wantarray ) { assert( ! $allow_name ); ( decodeaddr( $net ) , $vlsm ); } elsif ( valid_4address $net ) { $vlsm == 32 ? $net : "$net/$vlsm"; } else { $net; } } } sub validate_4range( $$ ) { my ( $low, $high ) = @_; validate_4address $low, 0; validate_4address $high, 0; my $first = decodeaddr $low; my $last = decodeaddr $high; fatal_error "Invalid IP Range ($low-$high)" unless $first <= $last; "$low-$high"; } sub validate_4host( $$ ) { my ( $host, $allow_name ) = $_[0]; if ( $host =~ /^(\d+\.\d+\.\d+\.\d+)-(\d+\.\d+\.\d+\.\d+)$/ ) { validate_4range $1, $2; } else { validate_4net( $host, $allow_name ); } } sub ip_range_explicit( $ ) { my $range = $_[0]; my @result; my ( $low, $high ) = split /-/, $range; validate_4address $low, 0; push @result, $low; if ( defined $high ) { validate_4address $high, 0; my $first = decodeaddr $low; my $last = decodeaddr $high; my $diff = $last - $first; fatal_error "Invalid IP Range ($range)" unless $diff >= 0 && $diff <= 256; while ( ++$first <= $last ) { push @result, encodeaddr( $first ); } } @result; } sub decompose_net( $ ) { my $net = $_[0]; ( $net, my $vlsm ) = validate_net( $net , 0 ); ( ( $family == F_IPV4 ? encodeaddr( $net) : normalize_6addr( $net ) ) , $vlsm ); } sub compare_nets( $$ ) { my ( @net1, @net2 ); @net1 = decompose_net( $_[0] ); @net2 = decompose_net( $_[1] ); $net1[0] eq $net2[0] && $net1[1] == $net2[1]; } sub allipv4() { @allipv4; } sub allipv6() { @allipv6; } sub nilipv4() { @nilipv4; } sub nilipv6() { @nilipv6; } sub rfc1918_networks() { @rfc1918_networks } # # Protocol/port validation # our %nametoproto = ( all => 0, ALL => 0, icmp => 1, ICMP => 1, tcp => 6, TCP => 6, udp => 17, UDP => 17 ); our @prototoname = ( 'all', 'icmp', '', '', '', '', 'tcp', '', '', '', '', '', '', '', '', '', '', 'udp' ); # # Returns the protocol number if the passed argument is a valid protocol number or name. Returns undef otherwise # sub resolve_proto( $ ) { my $proto = $_[0]; my $number; if ( $proto =~ /^\d+$/ || $proto =~ /^0x/ ) { $number = numeric_value ( $proto ); defined $number && $number <= 255 ? $number : undef; } else { fatal_error "A protocol list ($proto) is not allowed in this context" if $proto =~ /,/; # # Allow 'icmp' as a synonym for 'ipv6-icmp' in IPv6 compilations # $proto= 'ipv6-icmp' if $proto eq 'icmp' && $family == F_IPV6; defined( $number = $nametoproto{$proto} ) ? $number : scalar getprotobyname $proto; } } sub proto_name( $ ) { my $proto = $_[0]; $proto =~ /^(\d+)$/ ? $prototoname[ $proto ] || scalar getprotobynumber $proto : $proto } sub validate_port( $$ ) { my ($proto, $port) = @_; my $value; if ( $port =~ /^(\d+)$/ || $port =~ /^0x/ ) { $port = numeric_value $port; return $port if defined $port && $port && $port <= 65535; } else { $proto = proto_name $proto if $proto =~ /^(\d+)$/; $value = getservbyname( $port, $proto ); } return $value if defined $value; fatal_error "The separator for a port range is ':', not '-' ($port)" if $port =~ /^\d+-\d+$/; fatal_error "Invalid/Unknown $proto port/service ($_[1])" unless defined $value; } sub validate_portpair( $$ ) { my ($proto, $portpair) = @_; my $what; fatal_error "Invalid port range ($portpair)" if $portpair =~ tr/:/:/ > 1; $portpair = "0$portpair" if substr( $portpair, 0, 1 ) eq ':'; $portpair = "${portpair}65535" if substr( $portpair, -1, 1 ) eq ':'; my @ports = split /:/, $portpair, 2; my $protonum = resolve_proto( $proto ) || 0; $_ = validate_port( $protonum, $_) for grep $_, @ports; if ( @ports == 2 ) { $what = 'port range'; fatal_error "Invalid port range ($portpair)" unless $ports[0] < $ports[1]; } else { $what = 'port'; } fatal_error "Using a $what ( $portpair ) requires PROTO TCP, UDP, UDPLITE, SCTP or DCCP" unless defined $protonum && ( $protonum == TCP || $protonum == UDP || $protonum == UDPLITE || $protonum == SCTP || $protonum == DCCP ); join ':', @ports; } sub validate_portpair1( $$ ) { my ($proto, $portpair) = @_; my $what; fatal_error "Invalid port range ($portpair)" if $portpair =~ tr/-/-/ > 1; $portpair = "0$portpair" if substr( $portpair, 0, 1 ) eq ':'; $portpair = "${portpair}65535" if substr( $portpair, -1, 1 ) eq ':'; my @ports = split /-/, $portpair, 2; my $protonum = resolve_proto( $proto ) || 0; $_ = validate_port( $protonum, $_) for grep $_, @ports; if ( @ports == 2 ) { $what = 'port range'; fatal_error "Invalid port range ($portpair)" unless $ports[0] < $ports[1]; } else { $what = 'port'; } fatal_error "Using a $what ( $portpair ) requires PROTO TCP, UDP, SCTP or DCCP" unless defined $protonum && ( $protonum == TCP || $protonum == UDP || $protonum == SCTP || $protonum == DCCP ); join '-', @ports; } sub validate_port_list( $$ ) { my $result = ''; my ( $proto, $list ) = @_; my @list = split_list( $list, 'port' ); if ( @list > 1 && $list =~ /:/ ) { require_capability( 'XMULTIPORT' , 'Port ranges in a port list', '' ); } $proto = proto_name $proto; for ( @list ) { my $value = validate_portpair( $proto , $_ ); $result = $result ? join ',', $result, $value : $value; } $result; } my %icmp_types = ( any => 'any', 'echo-reply' => 0, 'destination-unreachable' => 3, 'network-unreachable' => '3/0', 'host-unreachable' => '3/1', 'protocol-unreachable' => '3/2', 'port-unreachable' => '3/3', 'fragmentation-needed' => '3/4', 'source-route-failed' => '3/5', 'network-unknown' => '3/6', 'host-unknown' => '3/7', 'network-prohibited' => '3/9', 'host-prohibited' => '3/10', 'TOS-network-unreachable' => '3/11', 'TOS-host-unreachable' => '3/12', 'communication-prohibited' => '3/13', 'host-precedence-violation' => '3/14', 'precedence-cutoff' => '3/15', 'source-quench' => 4, 'redirect' => 5, 'network-redirect' => '5/0', 'host-redirect' => '5/1', 'TOS-network-redirect' => '5/2', 'TOS-host-redirect' => '5/3', 'echo-request' => '8', 'router-advertisement' => 9, 'router-solicitation' => 10, 'time-exceeded' => 11, 'ttl-zero-during-transit' => '11/0', 'ttl-zero-during-reassembly' => '11/1', 'parameter-problem' => 12, 'ip-header-bad' => '12/0', 'required-option-missing' => '12/1', 'timestamp-request' => 13, 'timestamp-reply' => 14, 'address-mask-request' => 17, 'address-mask-reply' => 18 ); sub validate_icmp( $ ) { my $type = $_[0]; my $value = $icmp_types{$type}; return $value if defined $value; if ( $type =~ /^(\d+)(\/(\d+))?$/ ) { return $type if $1 < 256 && ( ! $2 || $3 < 256 ); } fatal_error "Invalid ICMP Type ($type)" } # # Expands a port range into a minimal list of ( port, mask ) pairs. # Each port and mask are expressed as 4 hex nibbles without a leading '0x'. # # Example: # # DB<3> @foo = Shorewall::IPAddrs::expand_port_range( 6, '110:' ); print "@foo\n" # 006e fffe 0070 fff0 0080 ff80 0100 ff00 0200 fe00 0400 fc00 0800 f800 1000 f000 2000 e000 4000 c000 8000 8000 # sub expand_port_range( $$ ) { my ( $proto, $range ) = @_; if ( $range =~ /^(.*):(.*)$/ ) { my ( $first, $last ) = ( $1, $2); my @result; fatal_error "Invalid port range ($range)" unless $first ne '' or $last ne ''; # # Supply missing first/last port number # $first = 0 if $first eq ''; $last = 65535 if $last eq ''; # # Validate the ports # ( $first , $last ) = ( validate_port( $proto, $first || 1 ) , validate_port( $proto, $last ) ); $last++; #Increment last address for limit testing. # # Break the range into groups: # # - If the first port in the remaining range is odd, then the next group is ( , ffff ). # - Otherwise, find the largest power of two P that divides the first address such that # the remaining range has less than or equal to P ports. The next group is # ( , ~( P-1 ) ). # while ( ( my $ports = ( $last - $first ) ) > 0 ) { my $mask = 0xffff; #Mask for current ports in group. my $y = 2; #Next power of two to test my $z = 1; #Number of ports in current group (Previous value of $y). while ( ( ! ( $first % $y ) ) && ( $y <= $ports ) ) { $mask <<= 1; $z = $y; $y <<= 1; } # # push @result, sprintf( '%04x', $first ) , sprintf( '%04x' , $mask & 0xffff ); $first += $z; } fatal_error "Invalid port range ($range)" unless @result; # first port > last port @result; } else { ( sprintf( '%04x' , validate_port( $proto, $range ) ) , 'ffff' ); } } sub valid_6address( $ ) { my $address = $_[0]; my @address = split /:/, $address; my $max; if ( $address[-1] && $address[-1] =~ /^\d+\.\d+\.\d+\.\d+$/ ) { return 0 unless valid_4address pop @address; $max = 6; $address = join ':', @address; return 1 if $address eq ':'; } else { $max = 8; } return 0 if @address > $max; return 0 unless $address =~ /^[a-fA-F:\d]+$/; return 0 unless ( @address == $max ) || $address =~ /::/; return 0 if $address =~ /:::/ || $address =~ /::.*::/; unless ( $address =~ /^::/ ) { return 0 if $address =~ /^:/; } unless ( $address =~ /::$/ ) { return 0 if $address =~ /:$/; } for my $a ( @address ) { return 0 unless $a eq '' || ( $a =~ /^[a-fA-f\d]+$/ && length $a < 5 ); } 1; } sub validate_6address( $$ ) { my ( $addr, $allow_name ) = @_; my @addrs = ( $addr ); unless ( valid_6address $addr ) { fatal_error "Invalid IPv6 Address ($addr)" unless $allow_name; require Socket6; fatal_error "Unknown Host ($addr)" unless (@addrs = Socket6::gethostbyname2( $addr, Socket6::AF_INET6())); if ( defined wantarray ) { shift @addrs for (1..4); for ( @addrs ) { $_ = Socket6::inet_ntop( Socket6::AF_INET6(), $_ ); } } } defined wantarray ? wantarray ? @addrs : $addrs[0] : undef; } sub resolve_6dnsname( $ ) { my $net = $_[0]; my @addrs; require Socket6; fatal_error "Unknown Host ($net)" unless (@addrs = Socket6::gethostbyname2( $net, Socket6::AF_INET6())); shift @addrs for (1..4); for ( @addrs ) { $_ = Socket6::inet_ntop( Socket6::AF_INET6(), $_ ); } @addrs; } sub validate_6net( $$ ) { my ( $net, $allow_name ) = @_; if ( $net =~ /^\[(.+)]$/ ) { $net = $1; } elsif ( $net =~ /^\[(.+)\]\/(\d+)$/ ) { $net = join( '/', $1, $2 ); } fatal_error "Invalid Network Address($net)" if $net =~ /\[/; ($net, my $vlsm, my $rest) = split( '/', $net, 3 ); fatal_error 'Invalid Network Address(' . join( '/', $net, $vlsm, $rest ) if defined $rest; if ( $net =~ /\+(\[?)/ ) { if ( $1 ) { fatal_error "An ipset list ($net) is not allowed in this context"; } elsif ( $net =~ /^\+[a-zA-Z][-\w]*$/ ) { fatal_error "An ipset name ($net) is not allowed in this context"; } else { fatal_error "Invalid ipset name ($net)"; } } fatal_error "Invalid Network address ($_[0])" unless supplied $net; if ( defined $vlsm ) { fatal_error "Invalid VLSM ($vlsm)" unless $vlsm =~ /^\d+$/ && $vlsm <= 128; fatal_error "Invalid Network address ($_[0])" if defined $rest; fatal_error "Invalid IPv6 address ($net)" unless valid_6address $net; } else { fatal_error "Invalid Network address ($_[0])" if $_[0] =~ '/'; my $net1 = validate_6address $net, $allow_name; $net = $net1 unless $config{DEFER_DNS_RESOLUTION}; $vlsm = 128; } if ( defined wantarray ) { if ( wantarray ) { assert( ! $allow_name ); ( $net , $vlsm ); } elsif ( valid_6address ( $net ) ) { $vlsm == 32 ? $net : "$net/$vlsm"; } else { $net; } } } # # Note: the input is assumed to be a valid IPv6 address # sub normalize_6addr( $ ) { my $addr = shift; if ( $addr eq '::' ) { '0:0:0:0:0:0:0:0'; } else { # # Suppress leading zeros # $addr =~ s/^0+//; $addr =~ s/:0+/:/g; $addr =~ s/^:/0:/; $addr =~ s/:$/:0/; $addr =~ s/::/:0::/ while $addr =~ tr/:/:/ < 7; # # Note: "s/::/:0:/g" doesn't work here # 1 while $addr =~ s/::/:0:/; $addr =~ s/^0+:/0:/; $addr; } } sub validate_6range( $$ ) { my ( $low, $high ) = @_; validate_6address $low, 0; validate_6address $high, 0; my @low = split ":", normalize_6addr( $low ); my @high = split ":", normalize_6addr( $high ); while ( @low ) { my ( $l, $h) = ( shift @low, shift @high ); next if hex "0x$l" == hex "0x$h"; return "$low-$high" if hex "0x$l" < hex "0x$h"; last; } fatal_error "Invalid IPv6 Range ($low-$high)"; } sub validate_6host( $$ ) { my ( $host, $allow_name ) = @_; if ( $host =~ /^(.*:.*)-(.*:.*)$/ ) { validate_6range $1, $2; } else { validate_6net( $host, $allow_name ); } } my %ipv6_icmp_types = ( any => 'any', 'destination-unreachable' => 1, 'no-route' => '1/0', 'communication-prohibited' => '1/1', 'address-unreachable' => '1/2', 'port-unreachable' => '1/3', 'packet-too-big' => 2, 'time-exceeded' => 3, 'ttl-exceeded' => 3, 'ttl-zero-during-transit' => '3/0', 'ttl-zero-during-reassembly' => '3/1', 'parameter-problem' => 4, 'bad-header' => '4/0', 'unknown-header-type' => '4/1', 'unknown-option' => '4/2', 'echo-request' => 128, 'echo-reply' => 129, 'router-solicitation' => 133, 'router-advertisement' => 134, 'neighbour-solicitation' => 135, 'neighbour-advertisement' => 136, redirect => 137 ); sub validate_icmp6( $ ) { my $type = $_[0]; my $value = $ipv6_icmp_types{$type}; return $value if defined $value; if ( $type =~ /^(\d+)(\/(\d+))?$/ ) { return $type if $1 < 256 && ( ! $2 || $3 < 256 ); } fatal_error "Invalid IPv6 ICMP Type ($type)" } sub ALLIP() { $allip; } sub allip() { @allip; } sub NILIP() { $nilip; } sub nilip() { @nilip; } sub valid_address ( $ ) { $valid_address->(@_); } sub validate_address ( $$ ) { $validate_address->(@_); } sub validate_net ( $$ ) { $validate_net->(@_); } sub resolve_dnsname( $ ) { $resolve_dnsname->(@_); } sub validate_range ($$ ) { $validate_range->(@_); } sub validate_host ($$ ) { $validate_host->(@_); } # # Rather than initializing globals in an INIT block or during declaration, # we initialize them in a function. This is done for two reasons: # # 1. Proper initialization depends on the address family which isn't # known until the compiler has started. # # 2. The compiler can run multiple times in the same process so it has to be # able to re-initialize its dependent modules' state. # sub initialize( $ ) { $family = shift; if ( $family == F_IPV4 ) { $allip = ALLIPv4; @allip = @allipv4; $nilip = NILIPv4; @nilip = @nilipv4; $valid_address = \&valid_4address; $validate_address = \&validate_4address; $validate_net = \&validate_4net; $validate_range = \&validate_4range; $validate_host = \&validate_4host; $resolve_dnsname = \&resolve_4dnsname; } else { $allip = ALLIPv6; @allip = @allipv6; $nilip = NILIPv6; @nilip = @nilipv6; $valid_address = \&valid_6address; $validate_address = \&validate_6address; $validate_net = \&validate_6net; $validate_range = \&validate_6range; $validate_host = \&validate_6host; $resolve_dnsname = \&resolve_6dnsname; } } 1;