diff --git a/Makefile.am b/Makefile.am index 87be3ee..265cea1 100644 --- a/Makefile.am +++ b/Makefile.am @@ -71,6 +71,7 @@ AM_PL_LOG_FLAGS = -Mstrict -w \ -MDevel::Autoflush handwritten_tests = \ t/is-and-extract-ipv4.pl \ + t/is-and-extract-ipv6.pl \ t/geturl_connectivity.pl \ t/geturl_ssl.pl \ t/parse_assignments.pl \ diff --git a/ddclient.in b/ddclient.in index 288e5c5..1cec317 100755 --- a/ddclient.in +++ b/ddclient.in @@ -2297,44 +2297,71 @@ sub extract_ipv4 { return $ip; } +###################################################################### +## Regex that matches an IPv6 address. Accepts embedded leading zeros. +## Accepts IPv4-mapped IPv6 addresses such as 64:ff9b::192.0.2.13. +###################################################################### +my $regex_ipv6 = qr/ + # Define some named groups so we can use Perl's recursive subpattern feature for shorthand: + (?[0-9A-F]{1,4}){0} # "g" matches a group of 1 to 4 hex chars + (?(?&g):){0} # "g_" matches a group of 1 to 4 hex chars followed by a colon + (?<_g>:(?&g)){0} # "_g" matches a colon followed by a group of 1 to 4 hex chars + (?(?&g)?){0} # "g0" is an optional "g" (matches a group of 0 to 4 hex chars) + (?(?&g0):){0} # "g0_" is an optional "g" followed by a colon + (?[:.0-9A-Z]){0} # "x" matches chars that should never come before or after the address + (?$regex_ipv4){0} # "ip4" matches an IPv4 address x.x.x.x + + # Now for the regex itself: + (? scope global + # This seems to be consistent accross platforms. The last line is from Ubuntu of a static + # assigned IPv6. + ["ip -6 -o addr show dev scope global", <<'EOF'], +2: ens160 inet6 fdb6:1d86:d9bd:1::8214/128 scope global dynamic noprefixroute \ valid_lft 63197sec preferred_lft 63197sec +2: ens160 inet6 2001:DB8:4341:0781::8214/128 scope global dynamic noprefixroute \ valid_lft 63197sec preferred_lft 63197sec +2: ens160 inet6 2001:DB8:4341:0781:89b9:4b1c:186c:a0c7/64 scope global temporary dynamic \ valid_lft 85954sec preferred_lft 21767sec +2: ens160 inet6 fdb6:1d86:d9bd:1:89b9:4b1c:186c:a0c7/64 scope global temporary dynamic \ valid_lft 85954sec preferred_lft 21767sec +2: ens160 inet6 fdb6:1d86:d9bd:1:34a6:c329:c52e:8ba6/64 scope global temporary deprecated dynamic \ valid_lft 85954sec preferred_lft 0sec +2: ens160 inet6 fdb6:1d86:d9bd:1:b417:fe35:166b:4816/64 scope global dynamic mngtmpaddr noprefixroute \ valid_lft 85954sec preferred_lft 85954sec +2: ens160 inet6 2001:DB8:4341:0781:34a6:c329:c52e:8ba6/64 scope global temporary deprecated dynamic \ valid_lft 85954sec preferred_lft 0sec +2: ens160 inet6 2001:DB8:4341:0781:f911:a224:7e69:d22/64 scope global dynamic mngtmpaddr noprefixroute \ valid_lft 85954sec preferred_lft 85954sec +2: ens160 inet6 2001:DB8:4341:0781::100/128 scope global noprefixroute \ valid_lft forever preferred_lft forever +EOF + # Sample output from MacOS: + # ifconfig | grep -w "inet6" + # (Yes, there is a tab at start of each line.) The last two lines are with a manually + # configured static GUA. + ["MacOS: ifconfig | grep -w \"inet6\"", <<'EOF'], + inet6 fe80::1419:abd0:5943:8bbb%en0 prefixlen 64 secured scopeid 0xa + inet6 fdb6:1d86:d9bd:1:142c:8e9e:de48:843e prefixlen 64 autoconf secured + inet6 fdb6:1d86:d9bd:1:7447:cf67:edbd:cea4 prefixlen 64 autoconf temporary + inet6 fdb6:1d86:d9bd:1::c5b3 prefixlen 64 dynamic + inet6 2001:DB8:4341:0781:141d:66b9:2ba1:b67d prefixlen 64 autoconf secured + inet6 2001:DB8:4341:0781:64e1:b68f:e8af:5d6e prefixlen 64 autoconf temporary + inet6 fe80::1419:abd0:5943:8bbb%en0 prefixlen 64 secured scopeid 0xa + inet6 2001:DB8:4341:0781::101 prefixlen 64 +EOF + ["RHEL: ifconfig | grep -w \"inet6\"", <<'EOF'], + inet6 2001:DB8:4341:0781::dc14 prefixlen 128 scopeid 0x0 + inet6 fe80::cd48:4a58:3b0f:4d30 prefixlen 64 scopeid 0x20 + inet6 2001:DB8:4341:0781:e720:3aec:a936:36d4 prefixlen 64 scopeid 0x0 + inet6 fdb6:1d86:d9bd:1:9c16:8cbf:ae33:f1cc prefixlen 64 scopeid 0x0 + inet6 fdb6:1d86:d9bd:1::dc14 prefixlen 128 scopeid 0x0 +EOF + ["Ubuntu: ifconfig | grep -w \"inet6\"", <<'EOF'], + inet6 fdb6:1d86:d9bd:1:34a6:c329:c52e:8ba6 prefixlen 64 scopeid 0x0 + inet6 fdb6:1d86:d9bd:1:89b9:4b1c:186c:a0c7 prefixlen 64 scopeid 0x0 + inet6 fdb6:1d86:d9bd:1::8214 prefixlen 128 scopeid 0x0 + inet6 fdb6:1d86:d9bd:1:b417:fe35:166b:4816 prefixlen 64 scopeid 0x0 + inet6 fe80::5b31:fc63:d353:da68 prefixlen 64 scopeid 0x20 + inet6 2001:DB8:4341:0781::8214 prefixlen 128 scopeid 0x0 + inet6 2001:DB8:4341:0781:34a6:c329:c52e:8ba6 prefixlen 64 scopeid 0x0 + inet6 2001:DB8:4341:0781:89b9:4b1c:186c:a0c7 prefixlen 64 scopeid 0x0 + inet6 2001:DB8:4341:0781:f911:a224:7e69:d22 prefixlen 64 scopeid 0x0 +EOF + ["Busybox: ifconfig | grep -w \"inet6\"", <<'EOF'], + inet6 addr: fe80::4362:31ff:fe08:61b4/64 Scope:Link + inet6 addr: 2001:DB8:4341:0781:ed44:eb63:b070:212f/128 Scope:Global +EOF +); + + +subtest "is_ipv6() with valid addresses" => sub { + foreach my $ip (@valid_ipv6) { + ok(ddclient::is_ipv6($ip), "is_ipv6('$ip')"); + } +}; + +subtest "is_ipv6() with invalid addresses" => sub { + foreach my $ip (@invalid_ipv6) { + ok(!ddclient::is_ipv6($ip), sprintf("!is_ipv6(%s)", defined($ip) ? "'$ip'" : 'undef')); + } +}; + +subtest "is_ipv6() with char adjacent to valid address" => sub { + foreach my $ch (split(//, '/.,:z @$#&%!^*()_-+'), "\n") { + subtest perlstring($ch) => sub { + foreach my $ip (@valid_ipv6) { + subtest $ip => sub { + my $test = $ch . $ip; # insert at front + ok(!ddclient::is_ipv6($test), "!is_ipv6('$test')"); + $test = $ip . $ch; # add at end + ok(!ddclient::is_ipv6($test), "!is_ipv6('$test')"); + $test = $ch . $ip . $ch; # wrap front and end + ok(!ddclient::is_ipv6($test), "!is_ipv6('$test')"); + }; + } + }; + } +}; + +subtest "extract_ipv6()" => sub { + my @test_cases = ( + {name => "undef", text => undef, want => undef}, + {name => "empty", text => "", want => undef}, + {name => "invalid", text => "::12345", want => undef}, + {name => "two addrs", text => "::1\n::2", want => "::1"}, + {name => "zone index", text => "fe80::1%0", want => "fe80::1"}, + {name => "url host+port", text => "[::1]:123", want => "::1"}, + {name => "url host+zi+port", text => "[fe80::1%250]:123", want => "fe80::1"}, + {name => "zero pad", text => "::0001", want => "::1"}, + ); + foreach my $tc (@test_cases) { + is(ddclient::extract_ipv6($tc->{text}), $tc->{want}, $tc->{name}); + } +}; + +subtest "extract_ipv6() of valid addr with adjacent non-word char" => sub { + foreach my $wb (split(//, '/, @$#&%!^*()_-+'), "\n") { + subtest perlstring($wb) => sub { + my $test = ""; + foreach my $ip (@valid_ipv6) { + $test = "foo" . $wb . $ip . $wb . "bar"; # wrap front and end + $ip =~ s/\b0+\B//g; ## remove embedded leading zeros for testing + is(ddclient::extract_ipv6($test), $ip, perlstring($test)); + } + }; + } +}; + +subtest "interface config samples" => sub { + for my $sample (@if_samples) { + my ($name, $text) = @$sample; + subtest $name => sub { + my $ip = ddclient::extract_ipv6($text); + ok(ddclient::is_ipv6($ip), "extract_ipv6(\$text) returns an IPv6 address"); + foreach my $line (split(/\n/, $text)) { + my $ip = ddclient::extract_ipv6($line); + ok(ddclient::is_ipv6($ip), + sprintf("extract_ipv6(%s) returns an IPv6 address", perlstring($line))); + } + } + } +}; + +done_testing();