|
| 1 | +#!/usr/bin/env perl |
| 2 | + |
| 3 | +use strict; |
| 4 | +use warnings; |
| 5 | + |
| 6 | +use FindBin qw($Bin); |
| 7 | +use Test::More; |
| 8 | +use lib "$Bin/../lib"; |
| 9 | + |
| 10 | +use TestBootstrap (); |
| 11 | +use ConfigServer::CheckIP qw(checkip cccheckip); |
| 12 | + |
| 13 | +sub assert_cases { |
| 14 | + my ( $fn, $cases ) = @_; |
| 15 | + |
| 16 | + for my $case ( @{$cases} ) { |
| 17 | + my ( $input, $expected, $label ) = @{$case}; |
| 18 | + is( $fn->($input), $expected, $label ); |
| 19 | + } |
| 20 | + |
| 21 | + return; |
| 22 | +} |
| 23 | + |
| 24 | +subtest 'checkip accepts canonical, boundary, and alternate address forms' => sub { |
| 25 | + assert_cases( |
| 26 | + \&checkip, |
| 27 | + [ |
| 28 | + [ '192.168.1.1', 4, 'accepts a private IPv4 address' ], |
| 29 | + [ '8.8.8.8', 4, 'accepts a public IPv4 address' ], |
| 30 | + [ '8.8.8.8/24', 4, 'accepts an IPv4 CIDR block' ], |
| 31 | + [ '8.8.8.8/32', 4, 'accepts the IPv4 upper CIDR boundary' ], |
| 32 | + [ '0.0.0.0', 4, 'accepts the IPv4 all-zero address' ], |
| 33 | + [ '255.255.255.255', 4, 'accepts the IPv4 all-ones address' ], |
| 34 | + [ '2001:4860:4860::8888', 6, 'accepts a compressed IPv6 address' ], |
| 35 | + [ '2001:4860:4860::8888/64', 6, 'accepts an IPv6 CIDR block' ], |
| 36 | + [ '2001:4860:4860::8888/128', 6, 'accepts the IPv6 upper CIDR boundary' ], |
| 37 | + [ '2001:0db8:0000:0000:0000:ff00:0042:8329', 6, 'accepts a fully expanded IPv6 address' ], |
| 38 | + [ 'fe80::1', 6, 'accepts a link-local IPv6 address' ], |
| 39 | + [ '::ffff:192.0.2.1', 6, 'accepts an IPv4-mapped IPv6 address' ], |
| 40 | + ] |
| 41 | + ); |
| 42 | +}; |
| 43 | + |
| 44 | +subtest 'checkip rejects malformed, polluted, or out-of-range input' => sub { |
| 45 | + assert_cases( |
| 46 | + \&checkip, |
| 47 | + [ |
| 48 | + [ '', 0, 'rejects an empty string' ], |
| 49 | + [ 'not-an-ip', 0, 'rejects a non-IP string' ], |
| 50 | + [ '999.999.999.999', 0, 'rejects an out-of-range IPv4 address' ], |
| 51 | + [ '192.168.1', 0, 'rejects an IPv4 address with too few octets' ], |
| 52 | + [ '192.168.1.1.1', 0, 'rejects an IPv4 address with too many octets' ], |
| 53 | + [ '8.8.8.8/not-a-mask', 0, 'rejects a non-numeric IPv4 mask' ], |
| 54 | + [ '8.8.8.8/33', 0, 'rejects an IPv4 mask above 32' ], |
| 55 | + [ '8.8.8.8/999', 0, 'rejects a clearly invalid IPv4 mask' ], |
| 56 | + [ '2001:4860:4860::8888/129', 0, 'rejects an IPv6 mask above 128' ], |
| 57 | + [ ' 8.8.8.8', 0, 'rejects a leading-space IPv4 string' ], |
| 58 | + [ '8.8.8.8 ', 0, 'rejects a trailing-space IPv4 string' ], |
| 59 | + [ '8.8.8.8; echo hacked', 0, 'rejects IPv4 input polluted with shell text' ], |
| 60 | + [ '8.8.8.8|cat', 0, 'rejects IPv4 input polluted with pipe characters' ], |
| 61 | + ] |
| 62 | + ); |
| 63 | +}; |
| 64 | + |
| 65 | +subtest 'checkip rejects loopback addresses in both families' => sub { |
| 66 | + assert_cases( |
| 67 | + \&checkip, |
| 68 | + [ |
| 69 | + [ '127.0.0.1', 0, 'rejects IPv4 loopback' ], |
| 70 | + [ '127.0.0.1/8', 0, 'rejects IPv4 loopback with CIDR' ], |
| 71 | + [ '::1', 0, 'rejects compressed IPv6 loopback' ], |
| 72 | + [ '0000:0000:0000:0000:0000:0000:0000:0001', 0, 'rejects expanded IPv6 loopback' ], |
| 73 | + ] |
| 74 | + ); |
| 75 | +}; |
| 76 | + |
| 77 | +subtest 'checkip handles undefined and empty scalar inputs without warnings' => sub { |
| 78 | + my @warnings; |
| 79 | + local $SIG{__WARN__} = sub { push @warnings, @_ }; |
| 80 | + |
| 81 | + my $undef_scalar; |
| 82 | + my $empty_scalar = q{}; |
| 83 | + |
| 84 | + is( checkip(undef), 0, 'rejects undef scalar input' ); |
| 85 | + is( checkip(\$undef_scalar), 0, 'rejects undef scalar reference input' ); |
| 86 | + is( checkip(\$empty_scalar), 0, 'rejects empty scalar reference input' ); |
| 87 | + is( scalar @warnings, 0, 'does not emit warnings for undefined or empty input' ); |
| 88 | +}; |
| 89 | + |
| 90 | +subtest 'IPv6 reference inputs are normalised in place and preserve CIDR suffixes' => sub { |
| 91 | + my $ipv6 = '2001:4860:0000:0000:0000:0000:0000:8888'; |
| 92 | + my $ipv6_cidr = '2001:4860:0000:0000:0000:0000:0000:8844/64'; |
| 93 | + |
| 94 | + is( checkip(\$ipv6), 6, 'plain IPv6 reference input is accepted' ); |
| 95 | + is( $ipv6, '2001:4860::8888', 'plain IPv6 reference input is shortened in place' ); |
| 96 | + |
| 97 | + is( checkip(\$ipv6_cidr), 6, 'IPv6 reference input with CIDR is accepted' ); |
| 98 | + is( $ipv6_cidr, '2001:4860::8844/64', 'IPv6 reference input keeps its CIDR suffix after normalisation' ); |
| 99 | +}; |
| 100 | + |
| 101 | +subtest 'cccheckip keeps the stricter public/private IPv4 split' => sub { |
| 102 | + assert_cases( |
| 103 | + \&cccheckip, |
| 104 | + [ |
| 105 | + [ '8.8.8.8', 4, 'accepts a public IPv4 address' ], |
| 106 | + [ '1.1.1.1', 4, 'accepts another public IPv4 address' ], |
| 107 | + [ '8.8.8.0/24', 4, 'accepts a public IPv4 CIDR block' ], |
| 108 | + [ '192.168.1.1', 0, 'rejects a 192.168.x.x private IPv4 address' ], |
| 109 | + [ '10.0.0.1', 0, 'rejects a 10.x.x.x private IPv4 address' ], |
| 110 | + [ '172.16.0.1', 0, 'rejects a 172.16.x.x private IPv4 address' ], |
| 111 | + [ '127.0.0.1', 0, 'rejects IPv4 loopback' ], |
| 112 | + [ '0.0.0.0', 0, 'rejects a non-public all-zero IPv4 address' ], |
| 113 | + [ '8.8.8.8/33', 0, 'rejects an out-of-range public IPv4 mask' ], |
| 114 | + [ 'not-an-ip', 0, 'rejects malformed scalar input' ], |
| 115 | + [ '', 0, 'rejects an empty string' ], |
| 116 | + ] |
| 117 | + ); |
| 118 | +}; |
| 119 | + |
| 120 | +subtest 'cccheckip accepts IPv6 scalars and references while still rejecting loopback' => sub { |
| 121 | + my $ipv6 = '2001:4860:0000:0000:0000:0000:0000:8844'; |
| 122 | + my $ipv6_cidr = '2001:4860:0000:0000:0000:0000:0000:8844/64'; |
| 123 | + |
| 124 | + assert_cases( |
| 125 | + \&cccheckip, |
| 126 | + [ |
| 127 | + [ '2001:4860:4860::8844', 6, 'accepts a plain IPv6 scalar' ], |
| 128 | + [ '2001:4860:4860::8844/128', 6, 'accepts an IPv6 scalar with upper CIDR boundary' ], |
| 129 | + [ 'fe80::1', 6, 'accepts a link-local IPv6 scalar' ], |
| 130 | + [ '::1', 0, 'rejects compressed IPv6 loopback' ], |
| 131 | + [ '::1/128', 0, 'rejects IPv6 loopback with CIDR' ], |
| 132 | + [ '2001:4860:4860::8844/129', 0, 'rejects an out-of-range IPv6 mask' ], |
| 133 | + ] |
| 134 | + ); |
| 135 | + |
| 136 | + is( cccheckip(\$ipv6), 6, 'accepts an IPv6 reference input' ); |
| 137 | + is( $ipv6, '2001:4860::8844', 'normalises IPv6 reference input in place' ); |
| 138 | + |
| 139 | + is( cccheckip(\$ipv6_cidr), 6, 'accepts an IPv6 reference input with CIDR' ); |
| 140 | + is( $ipv6_cidr, '2001:4860::8844/64', 'preserves CIDR on cccheckip IPv6 reference input' ); |
| 141 | +}; |
| 142 | + |
| 143 | +subtest 'legacy CIDR /0 behaviour is documented as a known quirk' => sub { |
| 144 | + local $TODO = 'Legacy truthiness logic currently allows /0 CIDR masks'; |
| 145 | + |
| 146 | + is( checkip('8.8.8.8/0'), 0, 'checkip should reject IPv4 /0 once the legacy quirk is fixed' ); |
| 147 | + is( checkip('2001:4860:4860::8888/0'), 0, 'checkip should reject IPv6 /0 once the legacy quirk is fixed' ); |
| 148 | + is( cccheckip('8.8.8.8/0'), 0, 'cccheckip should reject IPv4 /0 once the legacy quirk is fixed' ); |
| 149 | + is( cccheckip('2001:4860:4860::8888/0'), 0, 'cccheckip should reject IPv6 /0 once the legacy quirk is fixed' ); |
| 150 | +}; |
| 151 | + |
| 152 | +done_testing; |
0 commit comments