1#!/usr/bin/perl -w 2# 3# usage: 4# cidrexpand < /etc/mail/access | makemap -r hash /etc/mail/access 5# 6# v 1.1 7# 8# 17 July 2000 Derek J. Balling (dredd@megacity.org) 9# 10# Acts as a preparser on /etc/mail/access_db to allow you to use address/bit 11# notation. 12# 13# If you have two overlapping CIDR blocks with conflicting actions 14# e.g. 10.2.3.128/25 REJECT and 10.2.3.143 ACCEPT 15# make sure that the exceptions to the more general block are specified 16# later in the access_db. 17# 18# the -r flag to makemap will make it "do the right thing" 19# 20# Modifications 21# ------------- 22# 26 Jul 2001 Derek Balling (dredd@megacity.org) 23# Now uses Net::CIDR because it makes life a lot easier. 24# 25# 5 Nov 2002 Richard Rognlie (richard@sendmail.com) 26# Added code to deal with the prefix tags that may now be included in 27# the access_db 28# 29# Added clarification in the notes for what to do if you have 30# exceptions to a larger CIDR block. 31# 32# 26 Jul 2006 Richard Rognlie (richard@sendmail.com) 33# Added code to strip "comments" (anything after a non-escaped #) 34# # characters after a \ or within quotes (single and double) are 35# left intact. 36# 37# e.g. 38# From:1.2.3.4 550 Die spammer # spammed us 2006.07.26 39# becomes 40# From:1.2.3.4 550 Die spammer 41# 42# 3 August 2006 43# Corrected a bug to have it handle the special case of "0.0.0.0/0" 44# since Net::CIDR doesn't handle it properly. 45# 46# 27 April 2016 47# Corrected IPv6 handling. Note that UseCompressedIPv6Addresses must 48# be turned off for this to work; there are three reasons for this: 49# 1) if the MTA uses compressed IPv6 addresses then CIDR 'cuts' 50# in the compressed range *cannot* be matched, as the MTA simply 51# won't look for them. E.g., there's no way to accurately 52# match "IPv6:fe80::/64" when for the address "IPv6:fe80::54ad" 53# the MTA doesn't lookup up "IPv6:fe80:0:0:0" 54# 2) cidrexpand only generates uncompressed addresses, so CIDR 55# 'cuts' to the right of the compressed range won't be matched 56# either. Why doesn't it generate compressed address output? 57# Oh, because: 58# 3) compressed addresses are ambiguous when colon-groups are 59# chopped off! You want an access map entry for 60# IPv6:fe80::0:5420 61# but not for 62# IPv6:fe80::5420:1234 63# ? Sorry, the former is really 64# IPv6:fe80::5420 65# which will also match the latter! 66# 67# 25 July 2016 68# Since cidrexpand already requires UseCompressedIPv6Addresses to be 69# turned off, it can also canonicalize non-CIDR IPv6 addresses to the 70# format that sendmail looks up, expanding compressed addresses and 71# trimming superfluous leading zeros. 72# 73# Report bugs to: <dredd@megacity.org> 74# 75 76our $VERSION = '1.1'; 77 78use strict; 79use Net::CIDR qw(cidr2octets cidrvalidate); 80use Getopt::Std; 81$Getopt::Std::STANDARD_HELP_VERSION = 1; 82 83sub VERSION_MESSAGE; 84sub HELP_MESSAGE; 85sub print_expanded_v4network; 86sub print_expanded_v6network; 87 88our %opts; 89getopts('cfhOSt:', \%opts); 90 91if ($opts{h}) { 92 HELP_MESSAGE(\*STDOUT); 93 exit 0; 94} 95 96# Delimiter between the key and value 97my $space_re = exists $opts{t} ? $opts{t} : '\s+'; 98 99# Regexp that matches IPv4 address literals 100my $ipv4_re = qr"(?:\d+\.){3}\d+"; 101 102# Regexp that matches IPv6 address literals, plus a lot more. 103# Further checks are required for verifying that it's really one 104my $ipv6_re = qr"[0-9A-Fa-f:]{2,39}(?:\.\d+\.\d+\.\d+)?"; 105 106my %pending; 107while (<>) 108{ 109 chomp; 110 my ($prefix, $network, $len, $right); 111 112 next if /^#/ && $opts{S}; 113 if ( (/\#/) && $opts{c} ) 114 { 115 # print "checking...\n"; 116 my $i; 117 my $qtype=''; 118 for ($i=0 ; $i<length($_) ; $i++) 119 { 120 my $ch = substr($_,$i,1); 121 if ($ch eq '\\') 122 { 123 $i++; 124 next; 125 } 126 elsif ($qtype eq '' && $ch eq '#') 127 { 128 substr($_,$i) = ''; 129 last; 130 } 131 elsif ($qtype ne '' && $ch eq $qtype) 132 { 133 $qtype = ''; 134 } 135 elsif ($qtype eq '' && $ch =~ /[\'\"]/) 136 { 137 $qtype = $ch; 138 } 139 } 140 } 141 142 if (($prefix, $network, $len, $right) = 143 m!^(|[^\s:]+:)(${ipv4_re})/(\d+)(${space_re}.*)$!) 144 { 145 print_expanded_v4network($network, $len, $prefix, $right); 146 } 147 elsif ((($prefix, $network, $len, $right) = 148 m!^((?:[^\s:]+:)?[Ii][Pp][Vv]6:)(${ipv6_re})(?:/(\d+))?(${space_re}.*)$!) && 149 (!defined($len) || $len <= 128) && 150 defined(cidrvalidate($network))) 151 { 152 print_expanded_v6network($network, $len // 128, $prefix, $right); 153 } 154 else 155 { 156 if (%pending && m!^(.+?)${space_re}!) 157 { 158 delete $pending{$opts{f} ? $1 : lc($1)}; 159 } 160 print "$_\n"; 161 } 162} 163print foreach values %pending; 164 165sub print_expanded_v4network 166{ 167 my ($network, $len, $prefix, $suffix) = @_; 168 my $fp = $opts{f} ? $prefix : lc($prefix); 169 170 # cidr2octets() doesn't handle a prefix-length of zero, so do 171 # that ourselves 172 foreach my $nl ($len == 0 ? (0..255) : cidr2octets("$network/$len")) 173 { 174 my $val = "$prefix$nl$suffix\n"; 175 if ($opts{O}) 176 { 177 $pending{"$fp$nl"} = $val; 178 next; 179 } 180 print $val; 181 } 182} 183 184sub print_expanded_v6network 185{ 186 my ($network, $len, $prefix, $suffix) = @_; 187 188 # cidr2octets() doesn't handle a prefix-length of zero, so do 189 # that ourselves. Easiest is to just recurse on bottom and top 190 # halves with a length of 1 191 if ($len == 0) { 192 print_expanded_v6network("::", 1, $prefix, $suffix); 193 print_expanded_v6network("8000::", 1, $prefix, $suffix); 194 } 195 else 196 { 197 my $fp = $opts{f} ? $prefix : lc($prefix); 198 foreach my $nl (cidr2octets("$network/$len")) 199 { 200 # trim leading zeros from each group 201 $nl =~ s/(^|:)0+(?=[^:])/$1/g; 202 my $val = "$prefix$nl$suffix\n"; 203 if ($opts{O}) 204 { 205 $pending{"$fp$nl"} = $val; 206 next; 207 } 208 print $val; 209 } 210 } 211} 212 213sub VERSION_MESSAGE 214{ 215 my ($fh) = @_; 216 print $fh "cidrexpand - Version $VERSION\n"; 217} 218 219sub HELP_MESSAGE 220{ 221 my ($fh) = @_; 222 print $fh <<'EOF'; 223Usage: cidrexpand [-cfhOS] [-t regexp] files... 224 225Expand CIDR format inside the keys of map entries for makemap. 226 227 -c Truncate lines at the first unquoted '#' 228 229 -f Treat keys as case-sensitive when doing override detection 230 for the -O option. By default overlap detection is 231 case-insensitive. 232 233 -h Print this usage 234 235 -O When a CIDR expansion would generate a partial conflict 236 with a later entry, suppress the overlap from the earlier 237 expansion 238 239 -S Skip lines that start with '#' 240 241 -t regexp 242 Use 'regexp' to match the delimiter between key and value, 243 defaulting to \s+ 244 245EOF 246} 247