File Coverage

blib/lib/Metabrik/Network/Address.pm
Criterion Covered Total %
statement 9 318 2.8
branch 0 154 0.0
condition 0 60 0.0
subroutine 3 36 8.3
pod 2 33 6.0
total 14 601 2.3


line stmt bran cond sub pod time code
1             #
2             # $Id$
3             #
4             # network::address Brik
5             #
6             package Metabrik::Network::Address;
7 1     1   644 use strict;
  1         3  
  1         29  
8 1     1   6 use warnings;
  1         1  
  1         27  
9              
10 1     1   7 use base qw(Metabrik);
  1         3  
  1         540  
11              
12             sub brik_properties {
13             return {
14 0     0 1   revision => '$Revision$',
15             tags => [ qw(unstable netmask convert ascii) ],
16             author => 'GomoR ',
17             license => 'http://opensource.org/licenses/BSD-3-Clause',
18             attributes => {
19             subnet => [ qw(subnet) ],
20             _ipv4_re => [ qw(INTERNAL) ],
21             _ipv6_re => [ qw(INTERNAL) ],
22             },
23             commands => {
24             match => [ qw(ip_address subnet|OPTIONAL) ],
25             network_address => [ qw(subnet|OPTIONAL) ],
26             broadcast_address => [ qw(subnet|OPTIONAL) ],
27             netmask_address => [ qw(subnet|OPTIONAL) ],
28             netmask_to_cidr => [ qw(netmask) ],
29             range_to_cidr => [ qw(first_ip_address last_ip_address) ],
30             is_ipv4 => [ qw(ipv4_address) ],
31             is_ipv6 => [ qw(ipv6_address) ],
32             is_ip => [ qw(ip_address) ],
33             is_rfc1918 => [ qw(ip_address) ],
34             ipv4_list => [ qw(subnet|OPTIONAL) ],
35             ipv6_list => [ qw(subnet|OPTIONAL) ],
36             count_ipv4 => [ qw(subnet|OPTIONAL) ],
37             count_ipv6 => [ qw(subnet|OPTIONAL) ],
38             get_ipv4_cidr => [ qw(subnet|OPTIONAL) ],
39             get_ipv6_cidr => [ qw(subnet|OPTIONAL) ],
40             is_ipv4_subnet => [ qw(subnet|OPTIONAL) ],
41             merge_cidr => [ qw($cidr_list) ],
42             ipv4_to_integer => [ qw(ipv4_address) ],
43             ipv6_to_integer => [ qw(ipv6_address) ],
44             integer_to_ipv4 => [ qw(integer) ],
45             ipv4_reserved_subnets => [ ],
46             ipv6_reserved_subnets => [ ],
47             is_ipv4_reserved => [ qw(ipv4_address) ],
48             is_ipv6_reserved => [ qw(ipv6_address) ],
49             is_ip_reserved => [ qw(ip_address) ],
50             ipv6_to_string_preferred => [ qw(ipv6_address) ],
51             ipv4_first_address => [ qw(ipv4_address) ],
52             ipv4_last_address => [ qw(ipv4_address) ],
53             ipv6_first_address => [ qw(ipv6_address) ],
54             ipv6_last_address => [ qw(ipv6_address) ],
55             },
56             require_modules => {
57             'Bit::Vector' => [ ],
58             'Net::Netmask' => [ ],
59             'Net::IPv4Addr' => [ ],
60             'Net::IPv6Addr' => [ ],
61             'IPv6::Address' => [ ],
62             'NetAddr::IP' => [ ],
63             'Net::CIDR' => [ ],
64             'Socket' => [ ],
65             'Regexp::IPv4' => [ qw($IPv4_re) ],
66             'Regexp::IPv6' => [ qw($IPv6_re) ],
67             },
68             };
69             }
70              
71             sub brik_init {
72 0     0 1   my $self = shift;
73              
74 0 0         my $init = $self->SUPER::brik_init or return;
75              
76 0           my $ipv4_re = qr/^${Regexp::IPv4::IPv4_re}$/;
77 0           my $ipv6_re = qr/^${Regexp::IPv6::IPv6_re}$/;
78              
79 0           $self->_ipv4_re($ipv4_re);
80 0           $self->_ipv6_re($ipv6_re);
81              
82 0           return $init;
83             }
84              
85             sub match {
86 0     0 0   my $self = shift;
87 0           my ($ip, $subnet) = @_;
88              
89 0   0       $subnet ||= $self->subnet;
90 0 0         $self->brik_help_run_undef_arg('match', $subnet) or return;
91              
92 0 0 0       if (! $self->is_ip($ip) || ! $self->is_ip($subnet)) {
93 0           return $self->log->error("match: invalid format for ip [$ip] or subnet [$subnet]");
94             }
95              
96 0 0 0       if ($self->is_ipv4($ip) && ! $self->is_ipv4($subnet)) {
97 0           return $self->log->error("match: cannot match IPv4 [$ip] against IPv6 ".
98             "subnet [$subnet]");
99             }
100              
101 0 0 0       if ($self->is_ipv6($ip) && ! $self->is_ipv6($subnet)) {
102 0           return $self->log->error("match: cannot match IPv6 [$ip] against IPv4 ".
103             "subnet [$subnet]");
104             }
105              
106 0           my $r;
107 0           eval {
108 0           $r = Net::CIDR::cidrlookup($ip, $subnet);
109             };
110 0 0         if ($@) {
111 0           chomp($@);
112 0           return $self->log->error("match: cidrlookup failed with ".
113             "ip [$ip] subnet [$subnet] with error [$@]");
114             }
115              
116 0 0         if ($r) {
117 0           $self->log->debug("match: $ip is in the same subnet as $subnet");
118 0           return 1;
119             }
120             else {
121 0           $self->log->debug("match: $ip is NOT in the same subnet as $subnet");
122 0           return 0;
123             }
124              
125 0           return 0;
126             }
127              
128             sub network_address {
129 0     0 0   my $self = shift;
130 0           my ($subnet) = @_;
131              
132 0   0       $subnet ||= $self->subnet;
133 0 0         $self->brik_help_run_undef_arg('network_address', $subnet) or return;
134              
135 0 0         if (! $self->is_ipv4($subnet)) {
136 0           return $self->log->error("network_address: invalid format [$subnet], not an IPv4 address");
137             }
138              
139 0           my ($address) = Net::IPv4Addr::ipv4_network($subnet);
140              
141 0           return $address;
142             }
143              
144             sub broadcast_address {
145 0     0 0   my $self = shift;
146 0           my ($subnet) = @_;
147              
148 0   0       $subnet ||= $self->subnet;
149 0 0         $self->brik_help_run_undef_arg('broadcast_address', $subnet) or return;
150              
151 0 0         if (! $self->is_ipv4($subnet)) {
152 0           return $self->log->error("broadcast_address: invalid format [$subnet], not an IPv4 address");
153             }
154              
155 0           my ($address) = Net::IPv4Addr::ipv4_broadcast($subnet);
156              
157 0           return $address;
158             }
159              
160             sub netmask_address {
161 0     0 0   my $self = shift;
162 0           my ($subnet) = @_;
163              
164 0   0       $subnet ||= $self->subnet;
165 0 0         $self->brik_help_run_undef_arg('netmask_address', $subnet) or return;
166              
167             # XXX: Not IPv6 compliant
168 0           my $block = Net::Netmask->new($subnet);
169 0           my $mask = $block->mask;
170              
171 0           return $mask;
172             }
173              
174             sub range_to_cidr {
175 0     0 0   my $self = shift;
176 0           my ($first, $last) = @_;
177              
178 0 0         $self->brik_help_run_undef_arg('range_to_cidr', $first) or return;
179 0 0         $self->brik_help_run_undef_arg('range_to_cidr', $last) or return;
180              
181 0 0 0       if ($self->is_ip($first) && $self->is_ip($last)) {
182             # IPv4 and IPv6 compliant
183 0           my @list;
184 0           eval {
185 0           @list = Net::CIDR::range2cidr("$first-$last");
186             };
187 0 0         if ($@) {
188 0           chomp($@);
189 0           return $self->log->error("range_to_cidr: range2cidr failed with ".
190             "first [$first] last [$last] with error [$@]");
191             }
192              
193 0           return \@list;
194             }
195              
196 0           return $self->log->error("range_to_cidr: first [$first] or last [$last] not a valid IP address");
197             }
198              
199             sub is_ip {
200 0     0 0   my $self = shift;
201 0           my ($ip) = @_;
202              
203 0 0         $self->brik_help_run_undef_arg('is_ip', $ip) or return;
204              
205 0 0 0       if ($self->is_ipv4($ip) || $self->is_ipv6($ip)) {
206 0           return 1;
207             }
208              
209 0           return 0;
210             }
211              
212             sub is_rfc1918 {
213 0     0 0   my $self = shift;
214 0           my ($ip) = @_;
215              
216 0 0         $self->brik_help_run_undef_arg('is_rfc1918', $ip) or return;
217              
218 0 0         if (! $self->is_ipv4($ip)) {
219 0           return $self->log->error("is_rfc1918: invalid format [$ip]");
220             }
221              
222 0           (my $local = $ip) =~ s/\/\d+$//;
223              
224 0           my $new = NetAddr::IP->new($local);
225 0           my $is;
226 0           eval {
227 0           $is = $new->is_rfc1918;
228             };
229 0 0         if ($@) {
230 0           chomp($@);
231 0           return $self->log->error("is_rfc1918: is_rfc1918 failed for [$local] with error [$@]");
232             }
233              
234 0 0         return $is ? 1 : 0;
235             }
236              
237             sub is_ipv4 {
238 0     0 0   my $self = shift;
239 0           my ($ip) = @_;
240              
241 0 0         $self->brik_help_run_undef_arg('is_ipv4', $ip) or return;
242              
243 0           (my $local = $ip) =~ s/\/\d+$//;
244              
245 0           my $ipv4_re = $self->_ipv4_re;
246              
247 0 0         if ($local =~ $ipv4_re) {
248 0           return 1;
249             }
250              
251 0           return 0;
252             }
253              
254             sub is_ipv6 {
255 0     0 0   my $self = shift;
256 0           my ($ip) = @_;
257              
258 0 0         $self->brik_help_run_undef_arg('is_ipv6', $ip) or return;
259              
260 0           (my $local = $ip) =~ s/\/\d+$//;
261              
262 0           my $ipv6_re = $self->_ipv6_re;
263              
264 0 0         if ($local =~ $ipv6_re) {
265 0           return 1;
266             }
267              
268 0           return 0;
269             }
270              
271             sub netmask_to_cidr {
272 0     0 0   my $self = shift;
273 0           my ($netmask) = @_;
274              
275 0 0         $self->brik_help_run_undef_arg('netmask_to_cidr', $netmask) or return;
276              
277             # We use a fake address, cause we are only interested in netmask
278 0           my $cidr;
279 0           eval {
280 0           $cidr = Net::CIDR::addrandmask2cidr("127.0.0.0", $netmask);
281             };
282 0 0         if ($@) {
283 0           chomp($@);
284 0           return $self->log->error("netmask_to_cidr: addrandmask2cidr failed ".
285             "with netmask [$netmask] with error [$@]");
286             }
287              
288 0           my ($size) = $cidr =~ m{/(\d+)$};
289              
290 0           return $size;
291             }
292              
293             sub ipv4_list {
294 0     0 0   my $self = shift;
295 0           my ($subnet) = @_;
296              
297 0   0       $subnet ||= $self->subnet;
298 0 0         $self->brik_help_run_undef_arg('ipv4_list', $subnet) or return;
299              
300 0 0         if (! $self->is_ipv4($subnet)) {
301 0           return $self->log->error("ipv4_list: invalid format [$subnet], not IPv4");
302             }
303              
304             # This will allow handling of IPv4 /12 networks (~ 1_000_000 IP addresses)
305 0           NetAddr::IP::netlimit(20);
306              
307 0 0         my $a = $self->network_address($subnet) or return;
308 0 0         my $m = $self->netmask_address($subnet) or return;
309              
310 0           my $ip = NetAddr::IP->new($a, $m);
311 0           my $r;
312 0           eval {
313 0           $r = $ip->hostenumref;
314             };
315 0 0         if ($@) {
316 0           chomp($@);
317 0           return $self->log->error("ipv4_list: hostenumref failed for [$a] [$m] with error [$@]");
318             }
319              
320 0           my @list = ();
321 0           for my $ip (@$r) {
322 0           push @list, $ip->addr;
323             }
324              
325 0           return \@list;
326             }
327              
328             sub ipv6_list {
329 0     0 0   my $self = shift;
330 0           my ($subnet) = @_;
331              
332 0   0       $subnet ||= $self->subnet;
333 0 0         $self->brik_help_run_undef_arg('ipv6_list', $subnet) or return;
334              
335 0 0         if (! $self->is_ipv6($subnet)) {
336 0           return $self->log->error("ipv6_list: invalid format [$subnet], not IPv6");
337             }
338              
339             # Makes IPv6 fully lowercase
340 0           eval("use NetAddr::IP qw(:lower);");
341              
342             # Will allow building a list of ~ 1_000_000 IP addresses
343 0           NetAddr::IP::netlimit(20);
344              
345 0           my $ip = NetAddr::IP->new($subnet);
346 0           my $r;
347 0           eval {
348 0           $r = $ip->hostenumref;
349             };
350 0 0         if ($@) {
351 0           chomp($@);
352 0           return $self->log->error("ipv6_list: hostenumref failed for [$subnet] with error [$@]");
353             }
354              
355 0           my @list = ();
356 0           for my $ip (@$r) {
357 0           push @list, $ip->addr;
358             }
359              
360 0           return \@list;
361             }
362              
363             sub get_ipv4_cidr {
364 0     0 0   my $self = shift;
365 0           my ($subnet) = @_;
366              
367 0   0       $subnet ||= $self->subnet;
368 0 0         $self->brik_help_run_undef_arg('get_ipv4_cidr', $subnet) or return;
369              
370 0           my ($cidr) = $subnet =~ m{/(\d+)$};
371 0 0         if (! defined($cidr)) {
372 0           return $self->log->error("get_ipv4_cidr: no CIDR mask found");
373             }
374              
375 0 0 0       if ($cidr < 0 || $cidr > 32) {
376 0           return $self->log->error("get_ipv4_cidr: invalid CIDR mask [$cidr]");
377             }
378              
379 0           return $cidr;
380             }
381              
382             sub get_ipv6_cidr {
383 0     0 0   my $self = shift;
384 0           my ($subnet) = @_;
385              
386 0   0       $subnet ||= $self->subnet;
387 0 0         $self->brik_help_run_undef_arg('get_ipv6_cidr', $subnet) or return;
388              
389 0           my ($cidr) = $subnet =~ m{/(\d+)$};
390 0 0         if (! defined($cidr)) {
391 0           return $self->log->error("get_ipv6_cidr: no CIDR mask found");
392             }
393              
394 0 0 0       if ($cidr < 0 || $cidr > 128) {
395 0           return $self->log->error("get_ipv6_cidr: invalid CIDR mask [$cidr]");
396             }
397              
398 0           return $cidr;
399             }
400              
401             sub count_ipv4 {
402 0     0 0   my $self = shift;
403 0           my ($subnet) = @_;
404              
405 0   0       $subnet ||= $self->subnet;
406 0 0         $self->brik_help_run_undef_arg('count_ipv4', $subnet) or return;
407              
408 0 0         if (! $self->is_ipv4($subnet)) {
409 0           return $self->log->error("count_ipv4: invalid format [$subnet], not IPv4");
410             }
411              
412 0 0         my $cidr = $self->get_ipv4_cidr($subnet) or return;
413              
414 0           return 2 ** (32 - $cidr);
415             }
416              
417             sub count_ipv6 {
418 0     0 0   my $self = shift;
419 0           my ($subnet) = @_;
420              
421 0   0       $subnet ||= $self->subnet;
422 0 0         $self->brik_help_run_undef_arg('count_ipv6', $subnet) or return;
423              
424 0 0         if (! $self->is_ipv6($subnet)) {
425 0           return $self->log->error("count_ipv6: invalid format [$subnet], not IPv6");
426             }
427              
428 0 0         my $cidr = $self->get_ipv6_cidr($subnet) or return;
429              
430 0           return 2 ** (128 - $cidr);
431             }
432              
433             sub is_ipv4_subnet {
434 0     0 0   my $self = shift;
435 0           my ($subnet) = @_;
436              
437 0   0       $subnet ||= $self->subnet;
438 0 0         $self->brik_help_run_undef_arg('is_ipv4_subnet', $subnet) or return;
439              
440 0           my ($address, $cidr) = $subnet =~ m{^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/(\d+)$};
441 0 0 0       if (! defined($address) || ! defined($cidr)) {
442 0           $self->log->debug("is_ipv4_subnet: not a subnet [$subnet]");
443 0           return 0;
444             }
445              
446 0 0 0       if ($cidr < 0 || $cidr > 32) {
447 0           $self->log->debug("is_ipv4_subnet: not a valid CIDR mask [$cidr]");
448 0           return 0;
449             }
450              
451 0           return 1;
452             }
453              
454             sub merge_cidr {
455 0     0 0   my $self = shift;
456 0           my ($list) = @_;
457              
458 0 0         $self->brik_help_run_undef_arg('merge_cidr', $list) or return;
459 0 0         $self->brik_help_run_invalid_arg('merge_cidr', $list, 'ARRAY') or return;
460              
461 0           my @list;
462 0           eval {
463 0 0         @list = Net::CIDR::cidradd(@$list) or return;
464             };
465 0 0         if ($@) {
466 0           chomp($@);
467 0           return $self->log->error("merge_cidr: cidradd failed with error [$@]");
468             }
469              
470 0           return \@list;
471             }
472              
473             sub ipv4_to_integer {
474 0     0 0   my $self = shift;
475 0           my ($ipv4_address) = @_;
476              
477 0 0         $self->brik_help_run_undef_arg('ipv4_to_integer', $ipv4_address) or return;
478              
479 0 0         if (! $self->is_ipv4($ipv4_address)) {
480 0           return $self->log->error("ipv4_to_integer: invalid IPv4 address [$ipv4_address]");
481             }
482              
483 0           ($ipv4_address) =~ s/\/\d+$//; # Remove /CIDR if any
484              
485 0           return CORE::unpack('N', Socket::inet_aton($ipv4_address));
486             }
487              
488             sub ipv6_to_integer {
489 0     0 0   my $self = shift;
490 0           my ($ipv6_address) = @_;
491              
492 0 0         $self->brik_help_run_undef_arg('ipv6_to_integer', $ipv6_address) or return;
493              
494 0 0         if (! $self->is_ipv6($ipv6_address)) {
495 0           return $self->log->error("ipv6_to_integer: invalid IPv6 address [$ipv6_address]");
496             }
497              
498 0           ($ipv6_address) =~ s/\/\d+$//; # Remove /CIDR if any
499              
500 0           my $f = IPv6::Address->new("$ipv6_address/128")->get_bitstr;
501              
502 0           my ($b) = CORE::unpack('B128', $f);
503 0           return Bit::Vector->new_Bin(128, $b)->to_Dec;
504             }
505              
506             sub integer_to_ipv4 {
507 0     0 0   my $self = shift;
508 0           my ($integer) = @_;
509              
510 0 0         $self->brik_help_run_undef_arg('integer_to_ipv4', $integer) or return;
511              
512 0           return Socket::inet_ntoa(pack('N', $integer));
513             }
514              
515             #
516             # https://metacpan.org/source/MAXMIND/MaxMind-DB-Writer-0.202000/lib/MaxMind/DB/Writer/Tree.pm
517             #
518             sub ipv4_reserved_subnets {
519 0     0 0   my $self = shift;
520              
521 0           return [ qw(
522             0.0.0.0/8
523             10.0.0.0/8
524             100.64.0.0/10
525             127.0.0.0/8
526             169.254.0.0/16
527             172.16.0.0/12
528             192.0.0.0/29
529             192.0.2.0/24
530             192.88.99.0/24
531             192.168.0.0/16
532             198.18.0.0/15
533             198.51.100.0/24
534             203.0.113.0/24
535             224.0.0.0/4
536             240.0.0.0/4
537             ) ];
538             }
539              
540             sub ipv6_reserved_subnets {
541 0     0 0   my $self = shift;
542              
543 0           return [ qw(
544             0::/8
545             100::/64
546             2001:1::/32
547             2001:2::/31
548             2001:4::/30
549             2001:8::/29
550             2001:10::/28
551             2001:20::/27
552             2001:40::/26
553             2001:80::/25
554             2001:100::/24
555             2001:db8::/32
556             fc00::/7
557             fe80::/10
558             ff00::/8
559             ) ];
560             }
561              
562             sub is_ipv4_reserved {
563 0     0 0   my $self = shift;
564 0           my ($ip) = @_;
565              
566 0 0         $self->brik_help_run_undef_arg('is_ipv4_reserved', $ip) or return;
567              
568 0 0         if (! $self->is_ipv4($ip)) {
569 0           return $self->log->error("is_ipv4_reserved: ip[$ip] is not IPv4");
570             }
571              
572 0           my $list = $self->ipv4_reserved_subnets;
573 0           my $is_reserved = 0;
574 0           for (@$list) {
575 0 0         if ($self->match($ip, $_)) {
576 0           $is_reserved = 1;
577 0           last;
578             }
579             }
580              
581 0           return $is_reserved;
582             }
583              
584             sub is_ipv6_reserved {
585 0     0 0   my $self = shift;
586 0           my ($ip) = @_;
587              
588 0 0         $self->brik_help_run_undef_arg('is_ipv6_reserved', $ip) or return;
589              
590 0 0         if (! $self->is_ipv6($ip)) {
591 0           return $self->log->error("is_ipv6_reserved: ip[$ip] is not IPv6");
592             }
593              
594 0           my $list = $self->ipv6_reserved_subnets;
595 0           my $is_reserved = 0;
596 0           for (@$list) {
597 0 0         if ($self->match($ip, $_)) {
598 0           $is_reserved = 1;
599 0           last;
600             }
601             }
602              
603 0           return $is_reserved;
604             }
605              
606             sub is_ip_reserved {
607 0     0 0   my $self = shift;
608 0           my ($ip) = @_;
609              
610 0 0         $self->brik_help_run_undef_arg('is_ip_reserved', $ip) or return;
611              
612 0 0         if (! $self->is_ip($ip)) {
613 0           return $self->log->error("is_ip_reserved: ip[$ip] is not IPv4 nor IPv6");
614             }
615              
616 0           my $list;
617 0 0         if ($self->is_ipv4($ip)) {
618 0           $list = $self->ipv4_reserved_subnets;
619             }
620             else {
621 0           $list = $self->ipv6_reserved_subnets;
622             }
623              
624 0           my $is_reserved = 0;
625 0           for (@$list) {
626 0 0         if ($self->match($ip, $_)) {
627 0           $is_reserved = 1;
628 0           last;
629             }
630             }
631              
632 0           return $is_reserved;
633             }
634              
635             sub ipv6_to_string_preferred {
636 0     0 0   my $self = shift;
637 0           my ($ip) = @_;
638              
639 0 0         $self->brik_help_run_undef_arg('ipv6_to_string_preferred', $ip) or return;
640              
641 0 0         if (! $self->is_ipv6($ip)) {
642 0           return $self->log->error("ipv6_to_string_preferred: not an IPv6 address");
643             }
644              
645 0           my $pref;
646 0           eval {
647 0           $pref = Net::IPv6Addr::to_string_preferred($ip);
648             };
649 0 0         if ($@) {
650 0           return $self->log->error("ipv6_to_string_preferred: unable to convert IPv6 ".
651             "address: [$ip]");
652             }
653              
654 0           return $pref;
655             }
656              
657             sub ipv4_first_address {
658 0     0 0   my $self = shift;
659 0           my ($ip) = @_;
660              
661 0           return $self->network_address($ip);
662             }
663              
664             sub ipv4_last_address {
665 0     0 0   my $self = shift;
666 0           my ($ip) = @_;
667              
668 0           return $self->broadcast_address($ip);
669             }
670              
671             sub ipv6_first_address {
672 0     0 0   my $self = shift;
673 0           my ($ip) = @_;
674              
675 0 0         $self->brik_help_run_undef_arg('ipv6_first_address', $ip) or return;
676              
677 0 0         if (! $self->is_ipv6($ip)) {
678 0           return $self->log->error("ipv6_first_address: not a valid IPv6 address: [$ip]");
679             }
680              
681 0           my $ipv6 = IPv6::Address->new($ip);
682 0           my $string = $ipv6->first_address->to_string;
683 0           $string =~ s{/\d+$}{};
684              
685 0           return $string;
686             }
687              
688             sub ipv6_last_address {
689 0     0 0   my $self = shift;
690 0           my ($ip) = @_;
691              
692 0 0         $self->brik_help_run_undef_arg('ipv6_last_address', $ip) or return;
693              
694 0 0         if (! $self->is_ipv6($ip)) {
695 0           return $self->log->error("ipv6_last_address: not a valid IPv6 address: [$ip]");
696             }
697              
698 0           my $ipv6 = IPv6::Address->new($ip);
699 0           my $string = $ipv6->last_address->to_string;
700 0           $string =~ s{/\d+$}{};
701              
702 0           return $string;
703             }
704              
705             1;
706              
707             __END__