xref: /freebsd/contrib/sendmail/contrib/cidrexpand (revision e0c4386e)
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