Add is_ipv6_global and extract_ipv6_global functions

This commit is contained in:
David Kerr 2020-07-11 15:50:35 -04:00
parent 147ee33754
commit 1ad4d6737a
5 changed files with 182 additions and 61 deletions

View file

@ -74,6 +74,7 @@ handwritten_tests = \
t/geturl_ssl.pl \ t/geturl_ssl.pl \
t/is-and-extract-ipv4.pl \ t/is-and-extract-ipv4.pl \
t/is-and-extract-ipv6.pl \ t/is-and-extract-ipv6.pl \
t/is-and-extract-ipv6-global.pl \
t/parse_assignments.pl \ t/parse_assignments.pl \
t/write_cache.pl t/write_cache.pl
generated_tests = \ 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-ca-cert.pem \
t/lib/ddclient/Test/Fake/HTTPD/dummy-server-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/Test/Fake/HTTPD/dummy-server-key.pem \
t/lib/ddclient/t.pm \
t/lib/ok.pm t/lib/ok.pm

View file

@ -2406,6 +2406,38 @@ sub extract_ipv6 {
return $ip; 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 ## group_hosts_by
###################################################################### ######################################################################

View file

@ -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();

View file

@ -1,6 +1,6 @@
use Test::More; use Test::More;
use B qw(perlstring); use B qw(perlstring);
use ddclient::t;
SKIP: { eval { require Test::Warnings; } or skip($@, 1); } SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
eval { require 'ddclient'; } or BAIL_OUT($@); eval { require 'ddclient'; } or BAIL_OUT($@);
@ -361,60 +361,6 @@ my @invalid_ipv6 = (
"::2222:3333:4444:5555:6666:7777:8888:", "::2222:3333:4444:5555:6666:7777:8888:",
); );
my @if_samples = (
# Sample output from:
# ip -6 -o addr show dev <interface> 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 <interface> 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 <interface> | 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 <interface> | 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 <interface> | grep -w \"inet6\"", <<'EOF'],
inet6 2001:DB8:4341:0781::dc14 prefixlen 128 scopeid 0x0<global>
inet6 fe80::cd48:4a58:3b0f:4d30 prefixlen 64 scopeid 0x20<link>
inet6 2001:DB8:4341:0781:e720:3aec:a936:36d4 prefixlen 64 scopeid 0x0<global>
inet6 fdb6:1d86:d9bd:1:9c16:8cbf:ae33:f1cc prefixlen 64 scopeid 0x0<global>
inet6 fdb6:1d86:d9bd:1::dc14 prefixlen 128 scopeid 0x0<global>
EOF
["Ubuntu: ifconfig <interface> | grep -w \"inet6\"", <<'EOF'],
inet6 fdb6:1d86:d9bd:1:34a6:c329:c52e:8ba6 prefixlen 64 scopeid 0x0<global>
inet6 fdb6:1d86:d9bd:1:89b9:4b1c:186c:a0c7 prefixlen 64 scopeid 0x0<global>
inet6 fdb6:1d86:d9bd:1::8214 prefixlen 128 scopeid 0x0<global>
inet6 fdb6:1d86:d9bd:1:b417:fe35:166b:4816 prefixlen 64 scopeid 0x0<global>
inet6 fe80::5b31:fc63:d353:da68 prefixlen 64 scopeid 0x20<link>
inet6 2001:DB8:4341:0781::8214 prefixlen 128 scopeid 0x0<global>
inet6 2001:DB8:4341:0781:34a6:c329:c52e:8ba6 prefixlen 64 scopeid 0x0<global>
inet6 2001:DB8:4341:0781:89b9:4b1c:186c:a0c7 prefixlen 64 scopeid 0x0<global>
inet6 2001:DB8:4341:0781:f911:a224:7e69:d22 prefixlen 64 scopeid 0x0<global>
EOF
["Busybox: ifconfig <interface> | 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 { subtest "is_ipv6() with valid addresses" => sub {
foreach my $ip (@valid_ipv6) { 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 { subtest "interface config samples" => sub {
for my $sample (@if_samples) { for my $sample (@ddclient::t::interface_samples) {
my ($name, $text) = @$sample; subtest $sample->{name} => sub {
subtest $name => sub { my $ip = ddclient::extract_ipv6($sample->{text});
my $ip = ddclient::extract_ipv6($text); ok(ddclient::is_ipv6($ip), "extract_ipv6() returns an IPv6 address");
ok(ddclient::is_ipv6($ip), "extract_ipv6(\$text) returns an IPv6 address"); foreach my $line (split(/\n/, $sample->{text})) {
foreach my $line (split(/\n/, $text)) {
my $ip = ddclient::extract_ipv6($line); my $ip = ddclient::extract_ipv6($line);
ok(ddclient::is_ipv6($ip), ok(ddclient::is_ipv6($ip),
sprintf("extract_ipv6(%s) returns an IPv6 address", perlstring($line))); sprintf("extract_ipv6(%s) returns an IPv6 address", perlstring($line)));

78
t/lib/ddclient/t.pm Normal file
View file

@ -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 <interface> 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 <interface> 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 <interface> | 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 <interface> | 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 <interface> | grep -w inet6',
text => <<'EOF',
inet6 2001:DB8:4341:0781::dc14 prefixlen 128 scopeid 0x0<global>
inet6 fe80::cd48:4a58:3b0f:4d30 prefixlen 64 scopeid 0x20<link>
inet6 2001:DB8:4341:0781:e720:3aec:a936:36d4 prefixlen 64 scopeid 0x0<global>
inet6 fdb6:1d86:d9bd:1:9c16:8cbf:ae33:f1cc prefixlen 64 scopeid 0x0<global>
inet6 fdb6:1d86:d9bd:1::dc14 prefixlen 128 scopeid 0x0<global>
EOF
want_extract_ipv6_global => '2001:DB8:4341:781::dc14',
},
{
name => 'Ubuntu: ifconfig <interface> | grep -w inet6',
text => <<'EOF',
inet6 fdb6:1d86:d9bd:1:34a6:c329:c52e:8ba6 prefixlen 64 scopeid 0x0<global>
inet6 fdb6:1d86:d9bd:1:89b9:4b1c:186c:a0c7 prefixlen 64 scopeid 0x0<global>
inet6 fdb6:1d86:d9bd:1::8214 prefixlen 128 scopeid 0x0<global>
inet6 fdb6:1d86:d9bd:1:b417:fe35:166b:4816 prefixlen 64 scopeid 0x0<global>
inet6 fe80::5b31:fc63:d353:da68 prefixlen 64 scopeid 0x20<link>
inet6 2001:DB8:4341:0781::8214 prefixlen 128 scopeid 0x0<global>
inet6 2001:DB8:4341:0781:34a6:c329:c52e:8ba6 prefixlen 64 scopeid 0x0<global>
inet6 2001:DB8:4341:0781:89b9:4b1c:186c:a0c7 prefixlen 64 scopeid 0x0<global>
inet6 2001:DB8:4341:0781:f911:a224:7e69:d22 prefixlen 64 scopeid 0x0<global>
EOF
want_extract_ipv6_global => '2001:DB8:4341:781::8214',
},
{
name => 'Busybox: ifconfig <interface> | 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',
},
);