From 533b625585f2fd433e97781b9a4a51ff50b3ed35 Mon Sep 17 00:00:00 2001
From: Erik Auerswald <auerswal@unix-ag.uni-kl.de>
Date: Mon, 18 Apr 2022 16:19:39 +0200
Subject: [PATCH 6/8] IPv6 support for -g, --generate

Implementation of and tests for IPv6 support for the -g,
--generate option.  Existing tests for the absence of IPv6
support for -g, --generate are adjusted.

The implementation uses two unsigned 64 bit integers to
represent a single IPv6 address.  The required arithmetic
is open-coded and limited to what is needed to implement
IPv6 support for -g, --generate.

Tests generating IPv6 addresses use either the IPv6 Loopback
Address ::1 or addresses from the Link-Local Unicast range
fe80::/10.  Reachability of Link-local Unicast IPv6 addresses
is ignored, the tests check if they are generated.

The IPv6 tests take the open-coded 128-bit arithmetic into
consideration, i.e., edge cases are tested, too.
---
 ci/test-06-options-f-h.pl | 212 +++++++++++++++++++++++++++++++++++---
 ci/test-issue-58.pl       |   2 +-
 src/fping.c               | 140 +++++++++++++++++++++++--
 3 files changed, 335 insertions(+), 19 deletions(-)

diff --git a/ci/test-06-options-f-h.pl b/ci/test-06-options-f-h.pl
index 70f04f7..529a33e 100755
--- a/ci/test-06-options-f-h.pl
+++ b/ci/test-06-options-f-h.pl
@@ -1,6 +1,6 @@
 #!/usr/bin/perl -w
 
-use Test::Command tests => 84;
+use Test::Command tests => 136;
 use Test::More;
 use File::Temp;
 
@@ -115,26 +115,113 @@ SKIP: {
     $cmd->stderr_like(qr{can't parse address 127\.0\.0\.1:.*(not supported|not known)});
 }
 
-# fping -g (range - no IPv6 generator)
+# fping -6 -g (IPv6 address family with one address in IPv6 range)
 SKIP: {
     if($ENV{SKIP_IPV6}) {
         skip 'Skip IPv6 tests', 3;
     }
     my $cmd = Test::Command->new(cmd => "fping -6 -g ::1 ::1");
+    $cmd->exit_is_num(0);
+    $cmd->stdout_is_eq("::1 is alive\n");
+    $cmd->stderr_is_eq("");
+}
+
+# fping -g (IPv6 range - low value in lower 64 bits)
+SKIP: {
+    if($ENV{SKIP_IPV6}) {
+        skip 'Skip IPv6 tests', 3;
+    }
+    my $cmd = Test::Command->new(cmd => "fping -g ::1 ::1");
+    $cmd->exit_is_num(0);
+    $cmd->stdout_is_eq("::1 is alive\n");
+    $cmd->stderr_is_eq("");
+}
+
+# fping -g (IPv6 range - low values in lower 64 bits)
+SKIP: {
+    if($ENV{SKIP_IPV6}) {
+        skip 'Skip IPv6 tests', 3;
+    }
+    my $cmd = Test::Command->new(cmd => "fping -t 100 -r 0 -g fe80::1 fe80::2");
+    $cmd->stdout_like(qr{fe80::1 is (alive|unreachable)\n});
+    $cmd->stdout_like(qr{fe80::2 is (alive|unreachable)\n});
+    $cmd->stderr_is_eq("");
+}
+
+# fping -g (IPv6 range - high value in lower 64 bits)
+SKIP: {
+    if($ENV{SKIP_IPV6}) {
+        skip 'Skip IPv6 tests', 2;
+    }
+    my $cmd = Test::Command->new(cmd => "fping -t 100 -r 0 -g fe80::ffff:ffff:ffff:ffff fe80::ffff:ffff:ffff:ffff");
+    $cmd->stdout_like(qr{fe80::ffff:ffff:ffff:ffff is (alive|unreachable)\n});
+    $cmd->stderr_is_eq("");
+}
+
+# fping -g (IPv6 range - high values in lower 64 bits)
+SKIP: {
+    if($ENV{SKIP_IPV6}) {
+        skip 'Skip IPv6 tests', 3;
+    }
+    my $cmd = Test::Command->new(cmd => "fping -t 100 -r 0 -g fe80::ffff:ffff:ffff:fffe fe80::ffff:ffff:ffff:ffff");
+    $cmd->stdout_like(qr{fe80::ffff:ffff:ffff:fffe is (alive|unreachable)\n});
+    $cmd->stdout_like(qr{fe80::ffff:ffff:ffff:ffff is (alive|unreachable)\n});
+    $cmd->stderr_is_eq("");
+}
+
+# fping -g (IPv6 range - higher and lower 64 bits in use)
+SKIP: {
+    if($ENV{SKIP_IPV6}) {
+        skip 'Skip IPv6 tests', 3;
+    }
+    my $cmd = Test::Command->new(cmd => "fping -t 100 -r 0 -g fe80::ffff:ffff:ffff:ffff fe80:0:0:1::");
+    $cmd->stdout_like(qr{fe80::ffff:ffff:ffff:ffff is (alive|unreachable)\n});
+    $cmd->stdout_like(qr{fe80:0:0:1:: is (alive|unreachable)\n});
+    $cmd->stderr_is_eq("");
+}
+
+# fping -g (empty IPv6 range)
+SKIP: {
+    if($ENV{SKIP_IPV6}) {
+        skip 'Skip IPv6 tests', 3;
+    }
+    my $cmd = Test::Command->new(cmd => "fping -g fe80::2 fe80::1");
     $cmd->exit_is_num(1);
     $cmd->stdout_is_eq("");
-    $cmd->stderr_is_eq("fping: -g works only with IPv4 addresses\n");
+    $cmd->stderr_is_eq("");
 }
 
-# fping -6 -g (range - no IPv6 generator)
+# fping -g (too large IPv6 range - in most significant 64 bits)
 SKIP: {
     if($ENV{SKIP_IPV6}) {
         skip 'Skip IPv6 tests', 3;
     }
-    my $cmd = Test::Command->new(cmd => "fping -g ::1 ::1");
+    my $cmd = Test::Command->new(cmd => "fping -g fe80:: fe80:0:1::");
+    $cmd->exit_is_num(1);
+    $cmd->stdout_is_eq("");
+    $cmd->stderr_is_eq("fping: -g parameter generates too many addresses\n");
+}
+
+# fping -g (too large IPv6 range - in least significant 64 bits)
+SKIP: {
+    if($ENV{SKIP_IPV6}) {
+        skip 'Skip IPv6 tests', 3;
+    }
+    my $cmd = Test::Command->new(cmd => "fping -g fe80:: fe80::1:0:0");
     $cmd->exit_is_num(1);
     $cmd->stdout_is_eq("");
-    $cmd->stderr_is_eq("fping: -g works only with IPv4 addresses\n");
+    $cmd->stderr_is_eq("fping: -g parameter generates too many addresses\n");
+}
+
+# fping -g (too large IPv6 range - need to check both 64 bit parts)
+SKIP: {
+    if($ENV{SKIP_IPV6}) {
+        skip 'Skip IPv6 tests', 3;
+    }
+    my $cmd = Test::Command->new(cmd => "fping -g fe80::ffff:ffff:fff0:0 fe80:0:0:1::");
+    $cmd->exit_is_num(1);
+    $cmd->stdout_is_eq("");
+    $cmd->stderr_is_eq("fping: -g parameter generates too many addresses\n");
 }
 
 # fping -g (range - mixed address families: start IPv4, end IPv6)
@@ -156,7 +243,40 @@ SKIP: {
     my $cmd = Test::Command->new(cmd => "fping -g ::1 127.0.0.1");
     $cmd->exit_is_num(1);
     $cmd->stdout_is_eq("");
-    $cmd->stderr_is_eq("fping: -g works only with IPv4 addresses\n");
+    $cmd->stderr_like(qr{can't parse address 127\.0\.0\.1:.*(not supported|not known)});
+}
+
+# fping -g (no scoped IPv6 addresses in range - both)
+SKIP: {
+    if($ENV{SKIP_IPV6}) {
+        skip 'Skip IPv6 tests', 3;
+    }
+    my $cmd = Test::Command->new(cmd => "fping -g fe80::1%eth42 fe80::2%eth42");
+    $cmd->exit_is_num(1);
+    $cmd->stdout_is_eq("");
+    $cmd->stderr_is_eq("fping: -g does not support scoped IPv6 addresses\n");
+}
+
+# fping -g (no scoped IPv6 addresses in range - start)
+SKIP: {
+    if($ENV{SKIP_IPV6}) {
+         skip 'Skip IPv6 tests', 3;
+    }
+    my $cmd = Test::Command->new(cmd => "fping -g fe80::1%eth42 fe80::2");
+    $cmd->exit_is_num(1);
+    $cmd->stdout_is_eq("");
+    $cmd->stderr_is_eq("fping: -g does not support scoped IPv6 addresses\n");
+}
+
+# fping -g (no scoped IPv6 addresses in range - end)
+SKIP: {
+    if($ENV{SKIP_IPV6}) {
+        skip 'Skip IPv6 tests', 3;
+    }
+    my $cmd = Test::Command->new(cmd => "fping -g fe80::1 fe80::2%eth42");
+    $cmd->exit_is_num(1);
+    $cmd->stdout_is_eq("");
+    $cmd->stderr_is_eq("fping: -g does not support scoped IPv6 addresses\n");
 }
 
 # fping -g (cidr)
@@ -237,26 +357,94 @@ SKIP: {
     $cmd->stderr_like(qr{can't parse address 127\.0\.0\.1:.*(not supported|not known)});
 }
 
-# fping -g (cidr - no IPv6 generator)
+# fping -g (cidr IPv6)
 SKIP: {
     if($ENV{SKIP_IPV6}) {
         skip 'Skip IPv6 tests', 3;
     }
     my $cmd = Test::Command->new(cmd => "fping -g ::1/128");
+    $cmd->exit_is_num(0);
+    $cmd->stdout_is_eq("::1 is alive\n");
+    $cmd->stderr_is_eq("");
+}
+
+# fping -6 -g (cidr IPv6)
+SKIP: {
+    if($ENV{SKIP_IPV6}) {
+        skip 'Skip IPv6 tests', 3;
+    }
+    my $cmd = Test::Command->new(cmd => "fping -6 -g ::1/128");
+    $cmd->exit_is_num(0);
+    $cmd->stdout_is_eq("::1 is alive\n");
+    $cmd->stderr_is_eq("");
+}
+
+# fping -g (cidr IPv6)
+SKIP: {
+    if($ENV{SKIP_IPV6}) {
+        skip 'Skip IPv6 tests', 3;
+    }
+    my $cmd = Test::Command->new(cmd => "fping -t 100 -r 0 -g fe80::/127");
+    $cmd->stdout_like(qr{fe80:: is (alive|unreachable)\n});
+    $cmd->stdout_like(qr{fe80::1 is (alive|unreachable)\n});
+    $cmd->stderr_is_eq("");
+}
+
+# fping -g (cidr IPv6)
+SKIP: {
+    if($ENV{SKIP_IPV6}) {
+        skip 'Skip IPv6 tests', 5;
+    }
+    my $cmd = Test::Command->new(cmd => "fping -t 100 -r 0 -g fe80::cafe:1:2:30/126");
+    $cmd->stdout_like(qr{fe80::cafe:1:2:30 is (alive|unreachable)\n});
+    $cmd->stdout_like(qr{fe80::cafe:1:2:31 is (alive|unreachable)\n});
+    $cmd->stdout_like(qr{fe80::cafe:1:2:32 is (alive|unreachable)\n});
+    $cmd->stdout_like(qr{fe80::cafe:1:2:33 is (alive|unreachable)\n});
+    $cmd->stderr_is_eq("");
+}
+
+# fping -g (too short cidr IPv6 prefix)
+SKIP: {
+    if($ENV{SKIP_IPV6}) {
+        skip 'Skip IPv6 tests', 3;
+    }
+    my $cmd = Test::Command->new(cmd => "fping -g fe80::/64");
     $cmd->exit_is_num(1);
     $cmd->stdout_is_eq("");
-    $cmd->stderr_is_eq("fping: -g works only with IPv4 addresses\n");
+    $cmd->stderr_is_eq("fping: netmask must be between 65 and 128 (is: 64)\n");
 }
 
-# fping -6 -g (cidr - no IPv6 generator)
+# fping -g (too long cidr IPv6 prefix)
 SKIP: {
     if($ENV{SKIP_IPV6}) {
         skip 'Skip IPv6 tests', 3;
     }
-    my $cmd = Test::Command->new(cmd => "fping -6 -g ::1/128");
+    my $cmd = Test::Command->new(cmd => "fping -g ::1/129");
+    $cmd->exit_is_num(1);
+    $cmd->stdout_is_eq("");
+    $cmd->stderr_is_eq("fping: netmask must be between 65 and 128 (is: 129)\n");
+}
+
+# fping -g (too many addresses in cidr IPv6 prefix)
+SKIP: {
+    if($ENV{SKIP_IPV6}) {
+        skip 'Skip IPv6 tests', 3;
+    }
+    my $cmd = Test::Command->new(cmd => "fping -g fe80::/65");
+    $cmd->exit_is_num(1);
+    $cmd->stdout_is_eq("");
+    $cmd->stderr_is_eq("fping: -g parameter generates too many addresses\n");
+}
+
+# fping -g (no scoped IPv6 addresses in cidr IPv6 prefix)
+SKIP: {
+    if($ENV{SKIP_IPV6}) {
+        skip 'Skip IPv6 tests', 3;
+    }
+    my $cmd = Test::Command->new(cmd => "fping -g fe80::1%eth42/128");
     $cmd->exit_is_num(1);
     $cmd->stdout_is_eq("");
-    $cmd->stderr_is_eq("fping: -g works only with IPv4 addresses\n");
+    $cmd->stderr_is_eq("fping: -g does not support scoped IPv6 addresses\n");
 }
 
 # fping -H
diff --git a/ci/test-issue-58.pl b/ci/test-issue-58.pl
index db5161a..c3944f5 100755
--- a/ci/test-issue-58.pl
+++ b/ci/test-issue-58.pl
@@ -7,4 +7,4 @@ use Test::Command tests => 3;
 my $cmd1 = Test::Command->new(cmd => "fping -a -g 2001:db8:120:4161::4/64");
 $cmd1->exit_is_num(1);
 $cmd1->stdout_is_eq("");
-$cmd1->stderr_is_eq("fping: -g works only with IPv4 addresses\n");
+$cmd1->stderr_like(qr{fping[:,] (netmask must be between 65 and 128 \(is: 64\)|-g does not support this address family|can't parse address 2001:db8:120:4161::4: Address family.*not (supported|known))\n});
diff --git a/src/fping.c b/src/fping.c
index 4ca4b41..3c9f11d 100644
--- a/src/fping.c
+++ b/src/fping.c
@@ -390,6 +390,12 @@ void add_cidr(char*);
 void add_cidr_ipv4(unsigned long, unsigned long);
 void add_range(char*, char*);
 void add_addr_range_ipv4(unsigned long, unsigned long);
+#ifdef IPV6
+uint64_t be_octets_to_uint64(uint8_t*);
+void uint64_to_be_octets(uint64_t, uint8_t*);
+void add_cidr_ipv6(uint64_t, uint64_t, unsigned long);
+void add_addr_range_ipv6(uint64_t, uint64_t, uint64_t, uint64_t);
+#endif
 void print_warning(char* fmt, ...);
 int addr_cmp(struct sockaddr* a, struct sockaddr* b);
 void host_add_ping_event(HOST_ENTRY *h, int index, int64_t ev_time);
@@ -1182,6 +1188,14 @@ void add_cidr(char* addr)
     struct addrinfo addr_hints;
     struct addrinfo* addr_res;
     unsigned long net_addr;
+#ifdef IPV6
+    uint64_t net_upper, net_lower;
+
+    if (strchr(addr, '%')) {
+        fprintf(stderr, "%s: -g does not support scoped IPv6 addresses\n", prog);
+        exit(1);
+    }
+#endif /* IPV6 */
 
     /* Split address from mask */
     addr_end = strchr(addr, '/');
@@ -1192,7 +1206,7 @@ void add_cidr(char* addr)
     mask_str = addr_end + 1;
     mask = atoi(mask_str);
 
-    /* parse address (IPv4 only) */
+    /* parse address */
     memset(&addr_hints, 0, sizeof(struct addrinfo));
     addr_hints.ai_family = hints_ai_family;
     addr_hints.ai_flags = AI_NUMERICHOST;
@@ -1205,9 +1219,17 @@ void add_cidr(char* addr)
         net_addr = ntohl(((struct sockaddr_in*)addr_res->ai_addr)->sin_addr.s_addr);
         freeaddrinfo(addr_res);
         add_cidr_ipv4(net_addr, mask);
+#ifdef IPV6
+    } else if (addr_res->ai_family == AF_INET6) {
+        uint8_t *ipv6_addr = ((struct sockaddr_in6*)addr_res->ai_addr)->sin6_addr.s6_addr;
+        net_upper = be_octets_to_uint64(ipv6_addr);
+        net_lower = be_octets_to_uint64(ipv6_addr + 8);
+        freeaddrinfo(addr_res);
+        add_cidr_ipv6(net_upper, net_lower, mask);
+#endif /* IPV6 */
     } else {
         freeaddrinfo(addr_res);
-        fprintf(stderr, "%s: -g works only with IPv4 addresses\n", prog);
+        fprintf(stderr, "%s: -g does not support this address family\n", prog);
         exit(1);
     }
 }
@@ -1239,6 +1261,29 @@ void add_cidr_ipv4(unsigned long net_addr, unsigned long mask)
     add_addr_range_ipv4(net_addr, net_last);
 }
 
+#ifdef IPV6
+void add_cidr_ipv6(uint64_t net_upper, uint64_t net_lower, unsigned long mask)
+{
+    uint64_t bitmask_lower;
+    uint64_t last_lower;
+
+    /* check mask -- 2^63 addresses should suffice for now */
+    if (mask < 65 || mask > 128) {
+        fprintf(stderr, "%s: netmask must be between 65 and 128 (is: %lu)\n", prog, mask);
+        exit(1);
+    }
+
+    /* convert mask integer from 65 to 128 to the lower part of a bitmask */
+    bitmask_lower = ((uint64_t)-1) << (64 - mask);
+
+    /* calculate network range */
+    net_lower &= bitmask_lower;
+    last_lower = net_lower + ((uint64_t)1 << (64 - mask)) - 1;
+
+    add_addr_range_ipv6(net_upper, net_lower, net_upper, last_lower);
+}
+#endif /* IPV6 */
+
 void add_range(char* start, char* end)
 {
     struct addrinfo addr_hints;
@@ -1246,8 +1291,30 @@ void add_range(char* start, char* end)
     unsigned long start_long;
     unsigned long end_long;
     int ret;
+#ifdef IPV6
+    uint64_t start_upper, start_lower;
+    uint64_t end_upper, end_lower;
+
+    /*
+     * The compiler does not know that setting the address family hint to
+     * ensure that start and end are from the same address family also
+     * ensures that either start_long and end_long are initialized and used,
+     * or start_upper, start_lower, end_upper, and end_lower are initialized
+     * and used.  Thus initialize all variables when both IPv4 and IPv6 are
+     * supported to suppress compiler warnings.
+    */
+    start_long = -1;
+    end_long = 0;
+    start_upper = start_lower = -1;
+    end_upper = end_lower = 0;
+
+    if (strchr(start, '%') || strchr(end, '%')) {
+        fprintf(stderr, "%s: -g does not support scoped IPv6 addresses\n", prog);
+        exit(1);
+    }
+#endif /* IPV6 */
 
-    /* parse start address (IPv4 only) */
+    /* parse start address */
     memset(&addr_hints, 0, sizeof(struct addrinfo));
     addr_hints.ai_family = hints_ai_family;
     addr_hints.ai_flags = AI_NUMERICHOST;
@@ -1256,16 +1323,23 @@ void add_range(char* start, char* end)
         fprintf(stderr, "%s: can't parse address %s: %s\n", prog, start, gai_strerror(ret));
         exit(1);
     }
+    /* start and end must be from the same address family */
     hints_ai_family = addr_res->ai_family;
     if (addr_res->ai_family == AF_INET) {
         start_long = ntohl(((struct sockaddr_in*)addr_res->ai_addr)->sin_addr.s_addr);
+#ifdef IPV6
+    } else if (addr_res->ai_family == AF_INET6) {
+        uint8_t *ipv6_addr= ((struct sockaddr_in6*)addr_res->ai_addr)->sin6_addr.s6_addr;
+        start_upper = be_octets_to_uint64(ipv6_addr);
+        start_lower = be_octets_to_uint64(ipv6_addr + 8);
+#endif /* IPV6 */
     } else {
         freeaddrinfo(addr_res);
-        fprintf(stderr, "%s: -g works only with IPv4 addresses\n", prog);
+        fprintf(stderr, "%s: -g does not support this address family\n", prog);
         exit(1);
     }
 
-    /* parse end address (IPv4 only) */
+    /* parse end address */
     memset(&addr_hints, 0, sizeof(struct addrinfo));
     addr_hints.ai_family = hints_ai_family;
     addr_hints.ai_flags = AI_NUMERICHOST;
@@ -1278,9 +1352,17 @@ void add_range(char* start, char* end)
         end_long = ntohl(((struct sockaddr_in*)addr_res->ai_addr)->sin_addr.s_addr);
         freeaddrinfo(addr_res);
         add_addr_range_ipv4(start_long, end_long);
+#ifdef IPV6
+    } else if (addr_res->ai_family == AF_INET6) {
+        uint8_t *ipv6_addr= ((struct sockaddr_in6*)addr_res->ai_addr)->sin6_addr.s6_addr;
+        end_upper = be_octets_to_uint64(ipv6_addr);
+        end_lower = be_octets_to_uint64(ipv6_addr + 8);
+        freeaddrinfo(addr_res);
+        add_addr_range_ipv6(start_upper, start_lower, end_upper, end_lower);
+#endif /* IPV6 */
     } else {
         freeaddrinfo(addr_res);
-        fprintf(stderr, "%s: -g works only with IPv4 addresses\n", prog);
+        fprintf(stderr, "%s: -g does not support this address family\n", prog);
         exit(1);
     }
 }
@@ -1302,6 +1384,52 @@ void add_addr_range_ipv4(unsigned long start_long, unsigned long end_long)
     }
 }
 
+#ifdef IPV6
+uint64_t be_octets_to_uint64(uint8_t *be_octets)
+{
+    return ((uint64_t)be_octets[7]<<0 ) | ((uint64_t)be_octets[6]<<8)  |
+           ((uint64_t)be_octets[5]<<16) | ((uint64_t)be_octets[4]<<24) |
+           ((uint64_t)be_octets[3]<<32) | ((uint64_t)be_octets[2]<<40) |
+           ((uint64_t)be_octets[1]<<48) | ((uint64_t)be_octets[0]<<56);
+}
+
+void uint64_to_be_octets(uint64_t num, uint8_t *be_octets)
+{
+    int i;
+    for (i = 0; i < 8; i++) {
+        be_octets[7 - i] = (uint8_t)((num >> (i * 8)) & 0xff);
+    }
+}
+
+void add_addr_range_ipv6(uint64_t start_upper, uint64_t start_lower,
+                         uint64_t end_upper, uint64_t end_lower)
+{
+    /* prevent generating too many addresses */
+    if ((start_upper + 1 < end_upper) ||
+        (start_upper + 1 == end_upper && end_lower >= start_lower) ||
+        (start_upper + 1 == end_upper && end_lower - MAX_GENERATE > start_lower) ||
+        (start_upper == end_upper && end_lower - MAX_GENERATE > start_lower &&
+                                     start_lower + MAX_GENERATE < end_lower)) {
+        fprintf(stderr, "%s: -g parameter generates too many addresses\n", prog);
+        exit(1);
+    }
+
+    while ((start_upper < end_upper) ||
+           (start_upper == end_upper && start_lower <= end_lower)) {
+        struct in6_addr in6_addr_tmp;
+        char buffer[50];
+        uint64_to_be_octets(start_upper, in6_addr_tmp.s6_addr);
+        uint64_to_be_octets(start_lower, in6_addr_tmp.s6_addr + 8);
+        inet_ntop(AF_INET6, &in6_addr_tmp, buffer, sizeof(buffer));
+        add_name(buffer);
+        start_lower++;
+        if (start_lower == 0) {
+            start_upper++;
+        }
+    }
+}
+#endif /* IPv6 */
+
 void main_loop()
 {
     int64_t lt;
-- 
2.25.1

