diff --git a/Makefile.am b/Makefile.am index 97f3b41..636f379 100644 --- a/Makefile.am +++ b/Makefile.am @@ -74,6 +74,7 @@ handwritten_tests = \ t/geturl_ssl.pl \ t/is-and-extract-ipv4.pl \ t/is-and-extract-ipv6.pl \ + t/is-and-extract-ipv6-global.pl \ t/parse_assignments.pl \ t/write_cache.pl generated_tests = \ @@ -150,4 +151,5 @@ EXTRA_DIST += $(handwritten_tests) \ t/lib/ddclient/Test/Fake/HTTPD/dummy-ca-cert.pem \ t/lib/ddclient/Test/Fake/HTTPD/dummy-server-cert.pem \ t/lib/ddclient/Test/Fake/HTTPD/dummy-server-key.pem \ + t/lib/ddclient/t.pm \ t/lib/ok.pm diff --git a/ddclient.in b/ddclient.in index fb596b1..4303197 100755 --- a/ddclient.in +++ b/ddclient.in @@ -2406,6 +2406,38 @@ sub extract_ipv6 { return $ip; } +my $regex_ipv6_global = qr{ + (?! # Is not one of the following addresses: + 0{0,4}: # ::/16 is assumed to never contain globaly routable addresses + | f[cd][0-9a-f]{2}: # fc00::/7 RFC4193 ULA + | fe[89ab][0-9a-f]: # fe80::/10 link local + | ff[0-9a-f]{2}: # ff00::/8 multicast + ) + $regex_ipv6 # And is a valid IPv6 address +}xi; + +###################################################################### +## is_ipv6_global() returns true if the string is a valid IPv6 address +## that is probably globally routable, with no preceding or trailing +## characters. All addresses in the ::/16 block are assumed to not be +## globally routable. +###################################################################### +sub is_ipv6_global { + return (shift // '') =~ /\A$regex_ipv6_global\z/ +} + +###################################################################### +## extract_ipv6_global() finds the first IPv6 address in the given +## string that satisfies is_ipv6_global(), removes embedded leading +## zeros, and returns the result. Returns undef if no such address is +## found. +###################################################################### +sub extract_ipv6_global { + (shift // '') =~ /($regex_ipv6_global)/ or return undef; + (my $ip = $1) =~ s/\b0+\B//g; ## remove embedded leading zeros + return $ip; +} + ###################################################################### ## group_hosts_by ###################################################################### diff --git a/t/is-and-extract-ipv6-global.pl b/t/is-and-extract-ipv6-global.pl new file mode 100644 index 0000000..de7a991 --- /dev/null +++ b/t/is-and-extract-ipv6-global.pl @@ -0,0 +1,64 @@ +use Test::More; +use ddclient::t; +SKIP: { eval { require Test::Warnings; } or skip($@, 1); } +eval { require 'ddclient'; } or BAIL_OUT($@); + +subtest "is_ipv6_global() with valid but non-globally-routable addresses" => sub { + foreach my $ip ( + # The entirety of ::/16 is assumed to never contain globally routable addresses + "::", + "::1", + "0:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + # fc00::/7 unique local addresses (ULA) + "fc00::", + "fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + # fe80::/10 link-local unicast addresses + "fe80::", + "febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + # ff00::/8 multicast addresses + "ff00::", + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + # Case insensitivity of the negative lookahead + "FF00::", + ) { + ok(!ddclient::is_ipv6_global($ip), "!is_ipv6_global('$ip')"); + } +}; + +subtest "is_ipv6_global() with valid, globally routable addresses" => sub { + foreach my $ip ( + "1::", # just after ::/16 assumed non-global block + "fbff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", # just before fc00::/7 ULA block + "fe00::", # just after fc00::/7 ULA block + "fe7f:ffff:ffff:ffff:ffff:ffff:ffff:ffff", # just before fe80::/10 link-local block + "fec0::", # just after fe80::/10 link-local block + "feff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", # just before ff00::/8 multicast block + ) { + ok(ddclient::is_ipv6_global($ip), "is_ipv6_global('$ip')"); + } +}; + +subtest "extract_ipv6_global()" => sub { + my @test_cases = ( + {name => "undef", text => undef, want => undef}, + {name => "empty", text => "", want => undef}, + {name => "only non-global", text => "foo fe80:: bar", want => undef}, + {name => "single global", text => "foo 2000:: bar", want => "2000::"}, + {name => "multiple globals", text => "2000:: 3000::", want => "2000::"}, + {name => "global before non-global", text => "2000:: fe80::", want => "2000::"}, + {name => "non-global before global", text => "fe80:: 2000::", want => "2000::"}, + {name => "zero pad", text => "2001::0001", want => "2001::1"}, + ); + foreach my $tc (@test_cases) { + is(ddclient::extract_ipv6_global($tc->{text}), $tc->{want}, $tc->{name}); + } +}; + +subtest "interface config samples" => sub { + for my $sample (@ddclient::t::interface_samples) { + my $got = ddclient::extract_ipv6_global($sample->{text}); + is($got, $sample->{want_extract_ipv6_global}, $sample->{name}); + } +}; + +done_testing(); diff --git a/t/is-and-extract-ipv6.pl b/t/is-and-extract-ipv6.pl index bb33bc9..a7d97b9 100644 --- a/t/is-and-extract-ipv6.pl +++ b/t/is-and-extract-ipv6.pl @@ -1,6 +1,6 @@ use Test::More; use B qw(perlstring); - +use ddclient::t; SKIP: { eval { require Test::Warnings; } or skip($@, 1); } eval { require 'ddclient'; } or BAIL_OUT($@); @@ -361,60 +361,6 @@ my @invalid_ipv6 = ( "::2222:3333:4444:5555:6666:7777:8888:", ); -my @if_samples = ( - # Sample output from: - # ip -6 -o addr show dev 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) { @@ -475,12 +421,11 @@ subtest "extract_ipv6() of valid addr with adjacent non-word char" => sub { }; 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)) { + for my $sample (@ddclient::t::interface_samples) { + subtest $sample->{name} => sub { + my $ip = ddclient::extract_ipv6($sample->{text}); + ok(ddclient::is_ipv6($ip), "extract_ipv6() returns an IPv6 address"); + foreach my $line (split(/\n/, $sample->{text})) { my $ip = ddclient::extract_ipv6($line); ok(ddclient::is_ipv6($ip), sprintf("extract_ipv6(%s) returns an IPv6 address", perlstring($line))); diff --git a/t/lib/ddclient/t.pm b/t/lib/ddclient/t.pm new file mode 100644 index 0000000..2468b73 --- /dev/null +++ b/t/lib/ddclient/t.pm @@ -0,0 +1,78 @@ +package ddclient::t; +require v5.10.1; +use strict; +use warnings; + +our @interface_samples = ( + # Sample output from: + # ip -6 -o addr show dev scope global + # This seems to be consistent accross platforms. The last line is from Ubuntu of a static + # assigned IPv6. + { + name => 'ip -6 -o addr show dev scope global', + text => <<'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 + want_extract_ipv6_global => '2001:DB8:4341:781::8214', + }, + # 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. + { + name => 'MacOS: ifconfig | grep -w inet6', + text => <<'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 + want_extract_ipv6_global => '2001:DB8:4341:781:141d:66b9:2ba1:b67d', + }, + { + name => 'RHEL: ifconfig | grep -w inet6', + text => <<'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 + want_extract_ipv6_global => '2001:DB8:4341:781::dc14', + }, + { + name => 'Ubuntu: ifconfig | grep -w inet6', + text => <<'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 + want_extract_ipv6_global => '2001:DB8:4341:781::8214', + }, + { + name => 'Busybox: ifconfig | grep -w inet6', + text => <<'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 + want_extract_ipv6_global => '2001:DB8:4341:781:ed44:eb63:b070:212f', + }, +);