]> git.ipfire.org Git - thirdparty/mtr.git/commitdiff
mtr-packet: IPv6 support
authorMatt Kimball <matt.kimball@gmail.com>
Fri, 9 Dec 2016 19:14:06 +0000 (11:14 -0800)
committerMatt Kimball <matt.kimball@gmail.com>
Fri, 9 Dec 2016 19:14:06 +0000 (11:14 -0800)
mtr-packet can now send and receive ICMPv6 probes.

We now determine the source address for an outgoing probe by
opening a UDP socket to the destination, though no outgoing packets
are sent through this UDP socket.  IPv6 made this necessary, but
it now occurs for IPv4, too.

mtr-packet now uses sockaddr_storage for passing around addresses,
rather than sockaddr_in, as it can work for either IPv4 or IPv6.

To improve the maintainability of the code, the packet construction
and interpetation has been moved from probe_unix.c to construct_unix.c
and deconstruct_unix.c.

The way that Windows reads from the command stream has been changed to
avoid the possibility of hanging in a Sleep without an active read
of the command stream.

"send-probe" will now respond with "no-route" or "network-down"
when those conditions apply.

The test code has been moved to a test directory and split into
the following modules:

probe.py - tests for sending probes
cmdparse.py - tests for command parsing
mtrpacket.py - infrastructure for testing mtr-packet

The mtr-packet man page has been updated to describe ip-6 options
and now has an example of tracing a route to a remote host.

The mtr-packet network initialization has been split into two halves
to minimize the operations which occur with elevated privileges.

28 files changed:
.gitignore
Makefile.am
mtr-packet.8.in
net.c
net.h
packet/command.c
packet/command_cygwin.c
packet/command_cygwin.h
packet/construct_unix.c [new file with mode: 0644]
packet/construct_unix.h [new file with mode: 0644]
packet/deconstruct_unix.c [new file with mode: 0644]
packet/deconstruct_unix.h [new file with mode: 0644]
packet/packet.c
packet/probe.c
packet/probe.h
packet/probe_cygwin.c
packet/probe_cygwin.h
packet/probe_unix.c
packet/probe_unix.h
packet/protocols.h
packet/testpacket.py [deleted file]
packet/wait.h
packet/wait_cygwin.c
packet/wait_unix.c
test/cmdparse.py [new file with mode: 0755]
test/lint.sh [moved from packet/lint.sh with 84% similarity]
test/mtrpacket.py [new file with mode: 0644]
test/probe.py [new file with mode: 0755]

index 83b2660c6af1214e5ef1881ecaae585557cea01d..3cfd97ed01dd4383da7dcd7e4e599398b3283c0a 100644 (file)
@@ -1,5 +1,6 @@
 # .gitignore
 *.o
+*.pyc
 
 Makefile
 Makefile.in
index 249a474f6b8f1cb09a623ffa353403ce0c13232f..97ce4a1b884ac6d8bb289df66a22fe47af78d3d3 100644 (file)
@@ -1,12 +1,20 @@
 EXTRA_DIST = \
        SECURITY \
        mtr.bat \
-       img/mtr_icon.xpm \
-       packet/testpacket.py \
-       packet/lint.sh
+       img/mtr_icon.xpm
+       $(TEST_FILES)
 
 sbin_PROGRAMS = mtr mtr-packet
-TESTS = packet/testpacket.py
+TESTS = \
+       test/cmdparse.py \
+       test/probe.py
+
+TEST_FILES = \
+       test/cmdparse.py \
+       test/mtrpacket.py \
+       test/probe.py \
+       test/lint.sh
+EXTRA_DIST += $(TEST_FILES)
 
 PATHFILES =
 CLEANFILES = $(PATHFILES)
@@ -92,7 +100,7 @@ mtr_packet_SOURCES += \
        packet/command_cygwin.c packet/command_cygwin.h \
        packet/probe_cygwin.c packet/probe_cygwin.h \
        packet/wait_cygwin.c
-mtr_packet_LDADD = -lcygwin -licmp -lws2_32
+mtr_packet_LDADD = -lcygwin -liphlpapi -lws2_32
 
 dist_windows_aux = \
        $(srcdir)/mtr.bat \
@@ -122,6 +130,8 @@ else  # if CYGWIN
 
 mtr_packet_SOURCES += \
        packet/command_unix.c packet/command_unix.h \
+       packet/construct_unix.c packet/construct_unix.h \
+       packet/deconstruct_unix.c packet/deconstruct_unix.h \
        packet/probe_unix.c packet/probe_unix.h \
        packet/wait_unix.c
 
index 9908015679d8f461f8c640b7670bd77b1e87f613..7ea5c05deaaa1a4b71fe6c022c8138c1d7fb3ed8 100644 (file)
@@ -51,10 +51,14 @@ being used.
 .SH REQUESTS
 .TP
 .B send-probe
-Send a network probe to a particular IP address.  An IP address must be
-provided as an argument.
+Send a network probe to a particular IP address.  Either an
+.B ip-4
+or
+.B ip-6
+argument must be provided.
+A valid
 .B send-probe
-will reply with
+command will reply with
 .BR reply ,
 .BR no-reply ,
 or
@@ -69,6 +73,13 @@ The following arguments may be used:
 The Internet Protocol version 4 address to probe.
 .HP 7
 .IP
+.B ip-6
+.I IP-ADDRESS
+.HP 14
+.IP
+The Internet Protocol version 6 address to probe.
+.HP 7
+.IP
 .B timeout
 .I TIMEOUT-SECONDS
 .HP 14
@@ -105,10 +116,14 @@ argument is required.
 .I FEATURE-NAME
 .HP 14
 .IP
-The name of a feature requested.  Some features which can be checked are
-.B send-probe
+The name of a feature requested.
+.HP 7
+.IP
+Some features which can be checked are
+.BR send-probe ,
+.BR ip-4 ,
 and
-.BR ip-4 .
+.BR ip-6 .
 The feature
 .B version
 can be checked to retrieve the version of
@@ -118,15 +133,25 @@ can be checked to retrieve the version of
 .B reply
 The destination host received the
 .B send-probe
-probe and replied.  Arguments of the reply are the following:
+probe and replied.  Arguments of
+.B reply
+are:
 .HP 7
 .IP
 .B ip-4
 .I IP-ADDRESS
 .HP 14
 .IP
-The Internet Protocol address of the host which replied to the
-probe.
+The Internet Protocol version 4 address of the host which replied
+to the probe.
+.HP 7
+.IP
+.B ip-6
+.I IP-ADDRESS
+.HP 14
+.IP
+The Internet Protocol version 6 address of the host which replied
+to the probe.
 .HP 7
 .IP
 .B round-trip-time
@@ -153,8 +178,16 @@ are:
 .I IP-ADDRESS
 .HP 14
 .IP
-The Internet Protocol address of the host at which the time-to-live value
-expired.
+The Internet Protocol version 4 address of the host at which the
+time-to-live value expired.
+.HP 7
+.IP
+.B ip-6
+.I IP-ADDRESS
+.HP 14
+.IP
+The Internet Protocol version 6 address of the host at which the
+time-to-live value expired.
 .HP 7
 .IP
 .B round-trip-time
@@ -166,6 +199,21 @@ response.  The time is provided as a integral number of microseconds
 elapsed.
 .HP 7
 .TP
+.B no-route
+There was no route to the host used in a
+.B send-probe
+request.
+.TP
+.B network-down
+A probe could not be sent because the network is down.
+.TP
+.B probes-exhausted
+A probe could not be sent because there are already too many unresolved
+probes in flight.
+.TP
+.B invalid-argument
+The command request contained arguments which are invalid.
+.TP
 .B feature-support
 A reply to provided to
 .B check-support
@@ -195,7 +243,8 @@ is provided as the
 .I PRESENT
 value.
 .HP 7
-.SH EXAMPLE
+.IP
+.SH EXAMPLES
 A controlling program may start
 .B mtr-packet
 as a child process and issue the following command on
@@ -219,6 +268,53 @@ such as the following:
 .LP
 This indicates that the loopback address replied to the probe, and the
 round-trip time of the probe was 126 microseconds.
+.LP
+In order to trace the route to a remote host, multiple
+.B send-probe
+commands, each with a different
+.B ttl
+value, are used.
+.LP
+.RS
+11 send-probe ip-4 8.8.8.8 ttl 1
+.RS 0
+12 send-probe ip-4 8.8.8.8 ttl 2
+.RS 0
+13 send-probe ip-4 8.8.8.8 ttl 3
+.RS 0
+\&...
+.RE 0
+.LP
+Each interemediate host would respond with a
+.B ttl-expired
+message, and the destination host would respond with a
+.BR reply :
+.LP
+.RS
+11 ttl-expired ip-4 192.168.254.254 round-trip-time 1634
+.RS 0
+12 ttl-expired ip-4 184.19.243.240 round-trip-time 7609
+.RS 0
+13 ttl-expired ip-4 172.76.20.169 round-trip-time 8643
+.RS 0
+14 ttl-expired ip-4 74.40.1.101 round-trip-time 9755
+.RS 0
+15 ttl-expired ip-4 74.40.5.126 round-trip-time 10695
+.RS 0
+17 ttl-expired ip-4 108.170.245.97 round-trip-time 14077
+.RS 0
+16 ttl-expired ip-4 74.40.26.131 round-trip-time 15253
+.RS 0
+18 ttl-expired ip-4 209.85.245.101 round-trip-time 17080
+.RS 0
+19 reply ip-4 8.8.8.8 round-trip-time 17039
+.RE 0
+.LP
+Note that the replies in this example are printed out of order.
+(The reply to probe 17 arrives prior to the reply to probe 16.)
+This is the reason that it is important to send commands with unique
+token values, and to use those token values to match replies with
+their originating commands.
 .SH CONTACT INFORMATION
 .PP
 For the latest version, see the mtr web page at
diff --git a/net.c b/net.c
index c6288e749e087153a7ed5c3c50449e9ce6b16d80..7852921e179749658bdd40e163218009c376c935 100644 (file)
--- a/net.c
+++ b/net.c
@@ -140,7 +140,6 @@ static struct sockaddr_in * rsa4 = (struct sockaddr_in *) &remotesockaddr_struct
 static ip_t * sourceaddress;
 static ip_t * remoteaddress;
 
-/* XXX How do I code this to be IPV6 compatible??? */
 #ifdef ENABLE_IPV6
 static char localaddr[INET6_ADDRSTRLEN];
 #else
@@ -197,19 +196,28 @@ static void net_send_query(struct mtr_ctl *ctl, int index)
 {
   int seq = new_sequence(ctl, index);
   int time_to_live = index + 1;
-  char ip_string[INET_ADDRSTRLEN];
+  char ip_string[INET6_ADDRSTRLEN];
+  const char *ip_type;
 
   /*  Conver the remote IP address to a string  */
-  if (inet_ntop(AF_INET, remoteaddress, ip_string, INET_ADDRSTRLEN) == NULL) {
+  if (inet_ntop(
+      ctl->af, remoteaddress, ip_string, INET6_ADDRSTRLEN) == NULL) {
+
     display_close(ctl);
     error(EXIT_FAILURE, errno, "failure stringifying remote IP address");
   }
 
+  if (ctl->af == AF_INET6) {
+    ip_type = "ip-6";
+  } else {
+    ip_type = "ip-4";
+  }
+
   /*  Send a probe using the mtr-packet subprocess  */
   if (dprintf(
     packet_command_pipe.write_fd,
-    "%d send-probe ip-4 %s ttl %d\n",
-    seq, ip_string, time_to_live) < 0) {
+    "%d send-probe %s %s ttl %d\n",
+    seq, ip_type, ip_string, time_to_live) < 0) {
 
     display_close(ctl);
     error(EXIT_FAILURE, errno, "mtr-packet command pipe write failure");
@@ -313,6 +321,94 @@ static void net_process_ping(struct mtr_ctl *ctl, int seq, struct mplslen mpls,
 }
 
 
+/*
+  Extract the IP address and round trip time from a reply to a probe.
+  Returns true if both arguments are found in the reply, false otherwise.
+*/
+static bool parse_reply_arguments(
+  struct mtr_ctl *ctl, struct command_t *reply,
+  ip_t *fromaddress, int *round_trip_time)
+{
+  bool found_round_trip;
+  bool found_ip;
+  char *arg_name;
+  char *arg_value;
+  int i;
+
+  *round_trip_time = 0;
+  memset(fromaddress, 0, sizeof(ip_t));
+
+  found_ip = false;
+  found_round_trip = false;
+
+  /*  Examine the reply arguments for known values  */
+  for (i = 0; i < reply->argument_count; i++) {
+    arg_name = reply->argument_name[i];
+    arg_value = reply->argument_value[i];
+
+    if (ctl->af == AF_INET6) {
+      /*  IPv6 address of the responding host  */
+      if (!strcmp(arg_name, "ip-6")) {
+        if (inet_pton(AF_INET6, arg_value, fromaddress)) {
+          found_ip = true;
+        }
+      }
+    } else {
+      /*  IPv4 address of the responding host  */
+      if (!strcmp(arg_name, "ip-4")) {
+        if (inet_pton(AF_INET, arg_value, fromaddress)) {
+          found_ip = true;
+        }
+      }
+    }
+
+    /*  The round trip time in microseconds  */
+    if (!strcmp(arg_name, "round-trip-time")) {
+      errno = 0;
+      *round_trip_time = strtol(arg_value, NULL, 10);
+      if (!errno) {
+        found_round_trip = true;
+      }
+    }
+  }
+
+  return found_ip && found_round_trip;
+}
+
+
+/*
+    If an mtr-packet command has returned an error result,
+    report the error and exit.
+*/
+static void net_handle_command_reply_errors(
+  struct mtr_ctl *ctl, struct command_t *reply)
+{
+  char *reply_name;
+
+  reply_name = reply->command_name;
+
+  if (!strcmp(reply_name, "no-route")) {
+    display_close(ctl);
+    error(EXIT_FAILURE, 0, "No route to host");
+  }
+
+  if (!strcmp(reply_name, "network-down")) {
+    display_close(ctl);
+    error(EXIT_FAILURE, 0, "Network down");
+  }
+
+  if (!strcmp(reply_name, "probes-exhausted")) {
+    display_close(ctl);
+    error(EXIT_FAILURE, 0, "Probes exhausted");
+  }
+
+  if (!strcmp(reply_name, "invalid-argument")) {
+    display_close(ctl);
+    error(EXIT_FAILURE, 0, "mtr-packet reported invalid argument");
+  }
+}
+
+
 /*
     A complete mtr-packet reply line has arrived.  Parse it and record
     the responding IP and round trip time, if it is a reply that we
@@ -322,15 +418,10 @@ static void net_process_command_reply(
   struct mtr_ctl *ctl, char *reply_str)
 {
   struct command_t reply;
-  struct in_addr fromaddress;
+  ip_t fromaddress;
   int seq_num;
-  int i;
-  int round_trip_time = 0;
-  bool found_round_trip;
-  bool found_ip;
+  int round_trip_time;
   char *reply_name;
-  char *arg_name;
-  char *arg_value;
   struct mplslen mpls;
 
   /*  Parse the reply string  */
@@ -340,10 +431,13 @@ static void net_process_command_reply(
         wrong, as we might as well exit.  Even if the reply is of an
         unknown type, it should still parse.
     */
+    display_close(ctl);
     error(EXIT_FAILURE, errno, "reply parse failure");
     return;
   }
 
+  net_handle_command_reply_errors(ctl, &reply);
+
   seq_num = reply.token;
   reply_name = reply.command_name;
 
@@ -352,36 +446,11 @@ static void net_process_command_reply(
     return;
   }
 
-  found_ip = false;
-  found_round_trip = false;
-
-  /*  Examine the reply arguments for known values  */
-  for (i = 0; i < reply.argument_count; i++) {
-    arg_name = reply.argument_name[i];
-    arg_value = reply.argument_value[i];
-
-    /*  IPv4 address of the responding host  */
-    if (!strcmp(arg_name, "ip-4")) {
-      if (inet_pton(AF_INET, arg_value, &fromaddress)) {
-        found_ip = true;
-      }
-    }
-
-    /*  The round trip time in microseconds  */
-    if (!strcmp(arg_name, "round-trip-time")) {
-      errno = 0;
-      round_trip_time = strtol(arg_value, NULL, 10);
-      if (!errno) {
-        found_round_trip = true;
-      }
-    }
-  }
-
   /*
       If the reply had an IP address and a round trip time, we can
       record the result.
   */
-  if (found_ip && found_round_trip) {
+  if (parse_reply_arguments(ctl, &reply, &fromaddress, &round_trip_time)) {
     /* MPLS decoding */
     memset(&mpls, 0, sizeof(struct mplslen));
     mpls.labels = 0;
diff --git a/net.h b/net.h
index a6ef141602876e4fed5f903b28949d21c9d8c476..420ef542517f62dd819e2d0532613fc293ce2aa9 100644 (file)
--- a/net.h
+++ b/net.h
@@ -23,7 +23,6 @@
 #include <sys/socket.h>
 #ifdef ENABLE_IPV6
 #include <netinet/ip6.h>
-#include <netinet/icmp6.h>
 #endif
 
 #include <stdint.h>
index 5175d256934caead326fa126e537f5a6cda0b728..df3ca964d7da38a775e093004d2645913e92f759 100644 (file)
@@ -67,6 +67,10 @@ const char *check_support(
         return "ok";
     }
 
+    if (!strcmp(feature, "ip-6")) {
+        return "ok";
+    }
+
     if (!strcmp(feature, "send-probe")) {
         return "ok";
     }
@@ -106,7 +110,14 @@ bool decode_probe_argument(
 
     /*  Pass IPv4 addresses as string values  */
     if (!strcmp(name, "ip-4")) {
-        param->ipv4_address = value;
+        param->ip_version = 4;
+        param->address = value;
+    }
+
+    /*  IPv6 address  */
+    if (!strcmp(name, "ip-6")) {
+        param->ip_version = 6;
+        param->address = value;
     }
 
     /*  Time-to-live values  */
index 4c0857e9580227caa9810bdf73072422c0b62354..3ad7cecb2b5790a26229e2946ff46fd14b587c34 100644 (file)
@@ -82,7 +82,6 @@ void queue_empty_apc(void)
 }
 
 /*  Start a new overlapped I/O read from the command stream  */
-static
 void start_read_command(
     struct command_buffer_t *buffer)
 {
@@ -132,19 +131,15 @@ void init_command_buffer(
     memset(command_buffer, 0, sizeof(struct command_buffer_t));
     command_buffer->command_stream = command_stream;
     command_buffer->platform.pipe_open = true;
-
-    start_read_command(command_buffer);
 }
 
 /*
-    Start the next incoming read, or return EPIPE if the command stream
-    has been closed.
+    Return EPIPE if the command stream has been closed.  Otherwise, not much
+    to do for Cygwin, since we are using Overlapped I/O to read commands.
 */
 int read_commands(
     struct command_buffer_t *buffer)
 {
-    start_read_command(buffer);
-
     if (!buffer->platform.pipe_open) {
         return EPIPE;
     }
index d1a25fb898654fa8dfbf9d5f9a62c220ccc8b0cb..9a63ebadb7d70abd93167e1dcc6921176165433a 100644 (file)
@@ -45,4 +45,9 @@ struct command_buffer_platform_t
     char overlapped_buffer[COMMAND_BUFFER_SIZE];
 };
 
+struct command_buffer_t;
+
+void start_read_command(
+    struct command_buffer_t *buffer);
+
 #endif
diff --git a/packet/construct_unix.c b/packet/construct_unix.c
new file mode 100644 (file)
index 0000000..4eed0d7
--- /dev/null
@@ -0,0 +1,308 @@
+/*
+    mtr  --  a network diagnostic tool
+    Copyright (C) 2016  Matt Kimball
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+*/
+
+#include "construct_unix.h"
+
+#include <errno.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "protocols.h"
+
+/*  A source of data for computing a checksum  */
+struct checksum_source_t
+{
+    const void *data;
+    size_t size;
+};
+
+/*
+    Compute the IP checksum (or ICMP checksum) of a packet.
+    We may need to use data from multiple sources, to checksum
+    the "psuedo-header" for UDP or ICMPv6.
+*/
+static
+uint16_t compute_checksum(
+    struct checksum_source_t *source,
+    int source_count)
+{
+    int i, j;
+    const uint8_t *bytes;
+    size_t size;
+    uint32_t sum = 0;
+
+    for (i = 0; i < source_count; i++) {
+        bytes = (uint8_t *)source[i].data;
+        size = source[i].size;
+
+        for (j = 0; j < size; j++) {
+            if ((j & 1) == 0) {
+                sum += bytes[j] << 8;
+            } else {
+                sum += bytes[j];
+            }
+        }
+    }
+
+    /*
+        Sums which overflow a 16-bit value have the high bits
+        added back into the low 16 bits.
+    */
+    while (sum >> 16) {
+        sum = (sum >> 16) + (sum & 0xffff);
+    }
+
+    /*
+        The value stored is the one's complement of the
+        mathematical sum.
+    */
+    return (~sum & 0xffff);
+}
+
+/*  Compute the checksum from a single source of data  */
+static
+uint16_t simple_checksum(
+    const void *packet,
+    size_t size)
+{
+    struct checksum_source_t source;
+
+    source.data = packet;
+    source.size = size;
+
+    return compute_checksum(&source, 1);
+}
+
+/*
+    ICMPv6 and UDPv6 use a pseudo-header with a different layout
+    from the real IPv6 header for checksum purposes.  We'll fill
+    in the psuedo-header and use it to start the checksum against
+    the packet.
+*/
+static
+uint16_t pseudo6_checksum(
+    const void *ip_packet,
+    const void *packet,
+    size_t size)
+{
+    const struct IP6Header *ip = (struct IP6Header *)ip_packet;
+    struct IP6PseudoHeader pseudo;
+    struct checksum_source_t source[2];
+
+    memcpy(pseudo.saddr, ip->saddr, sizeof(struct in6_addr));
+    memcpy(pseudo.daddr, ip->daddr, sizeof(struct in6_addr));
+    pseudo.len = ip->len;
+    memset(pseudo.zero, 0, sizeof(pseudo.zero));
+    pseudo.protocol = ip->protocol;
+
+    source[0].data = &pseudo;
+    source[0].size = sizeof(struct IP6PseudoHeader);
+    source[1].data = packet;
+    source[1].size = size;
+
+    return compute_checksum(source, 2);
+}
+
+/*  Encode the IP header length field in the order required by the OS.  */
+static
+uint16_t length_byte_swap(
+    const struct net_state_t *net_state,
+    uint16_t length)
+{
+    if (net_state->platform.ip_length_host_order) {
+        return length;
+    } else {
+        return htons(length);
+    }
+}
+
+/*  Construct a header for IP version 4  */
+static
+void construct_ip4_header(
+    const struct net_state_t *net_state,
+    char *packet_buffer,
+    int packet_size,
+    const struct sockaddr_storage *srcaddr,
+    const struct sockaddr_storage *destaddr,
+    const struct probe_param_t *param)
+{
+    struct IPHeader *ip;
+    struct sockaddr_in *srcaddr4 = (struct sockaddr_in *)srcaddr;
+    struct sockaddr_in *destaddr4 = (struct sockaddr_in *)destaddr;
+
+    ip = (struct IPHeader *)&packet_buffer[0];
+
+    ip->version = 0x45;
+    ip->len = length_byte_swap(net_state, packet_size);
+    ip->ttl = param->ttl;
+    ip->protocol = IPPROTO_ICMP;
+    memcpy(&ip->saddr, &srcaddr4->sin_addr, sizeof(uint32_t));
+    memcpy(&ip->daddr, &destaddr4->sin_addr, sizeof(uint32_t));
+}
+
+/*  Construct a header for IP version 6  */
+static
+void construct_ip6_header(
+    const struct net_state_t *net_state,
+    char *packet_buffer,
+    int packet_size,
+    const struct sockaddr_storage *srcaddr,
+    const struct sockaddr_storage *destaddr,
+    const struct probe_param_t *param)
+{
+    struct IP6Header *ip;
+    int payload_size;
+    struct sockaddr_in6 *srcaddr6 = (struct sockaddr_in6 *)srcaddr;
+    struct sockaddr_in6 *destaddr6 = (struct sockaddr_in6 *)destaddr;
+
+    if (!net_state->platform.ipv6_header_constructed) {
+        return;
+    }
+
+    ip = (struct IP6Header *)&packet_buffer[0];
+    payload_size = packet_size - sizeof(struct IP6Header);
+
+    ip->version = 0x60;
+    ip->len = htons(payload_size);
+    ip->protocol = IPPROTO_ICMPV6;
+    ip->ttl = param->ttl;
+    memcpy(&ip->saddr, &srcaddr6->sin6_addr, sizeof(struct in6_addr));
+    memcpy(&ip->daddr, &destaddr6->sin6_addr, sizeof(struct in6_addr));
+}
+
+/*  Construct an ICMP header for IPv4  */
+static
+void construct_icmp4_header(
+    const struct net_state_t *net_state,
+    char *packet_buffer,
+    int packet_size,
+    const struct probe_param_t *param)
+{
+    struct ICMPHeader *icmp;
+    int icmp_size;
+
+    icmp = (struct ICMPHeader *)&packet_buffer[sizeof(struct IPHeader)];
+    icmp_size = packet_size - sizeof(struct IPHeader);
+
+    icmp->type = ICMP_ECHO;
+    icmp->id = htons(getpid());
+    icmp->sequence = htons(param->command_token);
+    icmp->checksum = htons(simple_checksum(icmp, icmp_size));
+}
+
+/*  Construct an ICMP header for IPv6  */
+static
+void construct_icmp6_header(
+    const struct net_state_t *net_state,
+    char *packet_buffer,
+    int packet_size,
+    const struct probe_param_t *param)
+{
+    struct ICMPHeader *icmp;
+    int icmp_size;
+
+    if (net_state->platform.ipv6_header_constructed) {
+        icmp = (struct ICMPHeader *)&packet_buffer[sizeof(struct IP6Header)];
+        icmp_size = packet_size - sizeof(struct IP6Header);
+    } else {
+        icmp = (struct ICMPHeader *)packet_buffer;
+        icmp_size = packet_size;
+    }
+
+    icmp->type = ICMP6_ECHO;
+    icmp->id = htons(getpid());
+    icmp->sequence = htons(param->command_token);
+
+    if (net_state->platform.ipv6_header_constructed) {
+        icmp->checksum = htons(
+            pseudo6_checksum(packet_buffer, icmp, icmp_size));
+    }
+}
+
+/*
+    Determine the size of the constructed packet based on the packet
+    parameters.  This is the amount of space the packet *we* construct
+    uses, and doesn't include any headers the operating system tacks
+    onto the packet.  (Such as the IPv6 header on non-Linux operating
+    systems.)
+*/
+static
+int compute_packet_size(
+    const struct net_state_t *net_state,
+    const struct probe_param_t *param)
+{
+    int packet_size = 0;
+
+    if (param->ip_version == 6) {
+        if (net_state->platform.ipv6_header_constructed) {
+            packet_size = sizeof(struct IP6Header);
+        }
+    } else if (param->ip_version == 4) {
+        packet_size = sizeof(struct IPHeader);
+    } else {
+        return -EINVAL;
+    }
+    packet_size += sizeof(struct ICMPHeader);
+
+    return packet_size;
+}
+
+/*  Construct a probe packet based on the probe parameters  */
+int construct_packet(
+    const struct net_state_t *net_state,
+    char *packet_buffer,
+    int packet_buffer_size,
+    const struct sockaddr_storage *dest_sockaddr,
+    const struct probe_param_t *param)
+{
+    int err;
+    int packet_size;
+    struct sockaddr_storage src_sockaddr;
+
+    packet_size = compute_packet_size(net_state, param);
+    if (packet_size < 0) {
+        return packet_size;
+    }
+
+    if (packet_buffer_size < packet_size) {
+        return -EINVAL;
+    }
+
+    err = find_source_addr(&src_sockaddr, dest_sockaddr);
+    if (err) {
+        return err;
+    }
+
+    memset(packet_buffer, 0, packet_size);
+
+    if (param->ip_version == 6) {
+        construct_ip6_header(
+            net_state, packet_buffer, packet_size,
+            &src_sockaddr, dest_sockaddr, param);
+        construct_icmp6_header(
+            net_state, packet_buffer, packet_size, param);
+    } else {
+        construct_ip4_header(
+            net_state, packet_buffer, packet_size,
+            &src_sockaddr, dest_sockaddr, param);
+        construct_icmp4_header(
+            net_state, packet_buffer, packet_size, param);
+    }
+
+    return packet_size;
+}
diff --git a/packet/construct_unix.h b/packet/construct_unix.h
new file mode 100644 (file)
index 0000000..8ed6486
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+    mtr  --  a network diagnostic tool
+    Copyright (C) 2016  Matt Kimball
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+*/
+
+#ifndef CONSTRUCT_H
+#define CONSTRUCT_H
+
+#include "probe.h"
+
+int construct_packet(
+    const struct net_state_t *net_state,
+    char *packet_buffer,
+    int packet_buffer_size,
+    const struct sockaddr_storage *dest_sockaddr,
+    const struct probe_param_t *param);
+
+#endif
diff --git a/packet/deconstruct_unix.c b/packet/deconstruct_unix.c
new file mode 100644 (file)
index 0000000..80fcfff
--- /dev/null
@@ -0,0 +1,209 @@
+/*
+    mtr  --  a network diagnostic tool
+    Copyright (C) 2016  Matt Kimball
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+*/
+
+#include "deconstruct_unix.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "protocols.h"
+
+/*
+    Compute the round trip time of a just-received probe and pass it
+    to the platform agnostic response handling.
+*/
+static
+void receive_probe(
+    struct probe_t *probe,
+    int icmp_type,
+    const struct sockaddr_storage *remote_addr,
+    struct timeval timestamp)
+{
+    unsigned int round_trip_us;
+
+    round_trip_us =
+        (timestamp.tv_sec - probe->platform.departure_time.tv_sec) * 1000000 +
+        timestamp.tv_usec - probe->platform.departure_time.tv_usec;
+
+    respond_to_probe(probe, icmp_type, remote_addr, round_trip_us);
+}
+
+/*
+    Given an ICMP id + ICMP sequence, find the match probe we've
+    transmitted and if found, respond to the command which sent it
+*/
+static
+void find_and_receive_probe(
+    struct net_state_t *net_state,
+    const struct sockaddr_storage *remote_addr,
+    struct timeval timestamp,
+    int icmp_type,
+    int icmp_id,
+    int icmp_sequence)
+{
+    struct probe_t *probe;
+
+    probe = find_probe(net_state, icmp_id, icmp_sequence);
+    if (probe == NULL) {
+        return;
+    }
+
+    receive_probe(probe, icmp_type, remote_addr, timestamp);
+}
+
+/*
+    Decode the ICMP header received and try to find a probe which it
+    is in response to.
+*/
+static
+void handle_received_icmpv4_packet(
+    struct net_state_t *net_state,
+    const struct sockaddr_storage *remote_addr,
+    const struct ICMPHeader *icmp,
+    int packet_length,
+    struct timeval timestamp)
+{
+    const int icmp_ip_icmp_size =
+        sizeof(struct ICMPHeader) +
+        sizeof(struct IPHeader) + sizeof(struct ICMPHeader);
+    const struct IPHeader *inner_ip;
+    const struct ICMPHeader *inner_icmp;
+
+    /*  If we get an echo reply, our probe reached the destination host  */
+    if (icmp->type == ICMP_ECHOREPLY) {
+        find_and_receive_probe(
+            net_state, remote_addr, timestamp,
+            ICMP_ECHOREPLY, icmp->id, icmp->sequence);
+    }
+
+    /*
+        If we get a time exceeded, we got a response from an intermediate
+        host along the path to our destination.
+    */
+    if (icmp->type == ICMP_TIME_EXCEEDED) {
+        if (packet_length < icmp_ip_icmp_size) {
+            return;
+        }
+
+        /*
+            The IP packet inside the ICMP response contains our original
+            IP header.  That's where we can get our original ID and
+            sequence number.
+        */
+        inner_ip = (struct IPHeader *)(icmp + 1);
+        inner_icmp = (struct ICMPHeader *)(inner_ip + 1);
+
+        find_and_receive_probe(
+            net_state, remote_addr, timestamp,
+            ICMP_TIME_EXCEEDED, inner_icmp->id, inner_icmp->sequence);
+    }
+}
+
+/*
+    Decode the ICMPv6 header.  The code duplication with ICMPv4 is
+    unfortunate, but small details in structure size and ICMP
+    constants differ.
+*/
+static
+void handle_received_icmpv6_packet(
+    struct net_state_t *net_state,
+    const struct sockaddr_storage *remote_addr,
+    const struct ICMPHeader *icmp,
+    int packet_length,
+    struct timeval timestamp)
+{
+    const int icmp_ip_icmp_size =
+        sizeof(struct ICMPHeader) +
+        sizeof(struct IP6Header) + sizeof(struct ICMPHeader);
+    const struct IP6Header *inner_ip;
+    const struct ICMPHeader *inner_icmp;
+
+    if (icmp->type == ICMP6_ECHOREPLY) {
+        find_and_receive_probe(
+            net_state, remote_addr, timestamp,
+            ICMP_ECHOREPLY, icmp->id, icmp->sequence);
+    }
+
+    if (icmp->type == ICMP6_TIME_EXCEEDED) {
+        if (packet_length < icmp_ip_icmp_size) {
+            return;
+        }
+
+        inner_ip = (struct IP6Header *)(icmp + 1);
+        inner_icmp = (struct ICMPHeader *)(inner_ip + 1);
+
+        find_and_receive_probe(
+            net_state, remote_addr, timestamp,
+            ICMP_TIME_EXCEEDED, inner_icmp->id, inner_icmp->sequence);
+    }
+}
+
+/*
+    We've received a new IPv4 ICMP packet.
+    We'll check to see that it is a response to one of our probes, and
+    if so, report the result of the probe to our command stream.
+*/
+void handle_received_ipv4_packet(
+    struct net_state_t *net_state,
+    const struct sockaddr_storage *remote_addr,
+    const void *packet,
+    int packet_length,
+    struct timeval timestamp)
+{
+    const int ip_icmp_size =
+        sizeof(struct IPHeader) + sizeof(struct ICMPHeader);
+    const struct IPHeader *ip;
+    const struct ICMPHeader *icmp;
+    int icmp_length;
+
+    /*  Ensure that we don't access memory beyond the bounds of the packet  */
+    if (packet_length < ip_icmp_size) {
+        return;
+    }
+
+    ip = (struct IPHeader *)packet;
+    if (ip->protocol != IPPROTO_ICMP) {
+        return;
+    }
+
+    icmp = (struct ICMPHeader *)(ip + 1);
+    icmp_length = packet_length - sizeof(struct IPHeader);
+
+    handle_received_icmpv4_packet(
+        net_state, remote_addr, icmp, icmp_length, timestamp);
+}
+
+/*
+    Unlike ICMPv6 raw sockets, unlike ICMPv4, don't include the IP header
+    in received packets, so we can assume the packet we got starts
+    with the ICMP packet.
+*/
+void handle_received_ipv6_packet(
+    struct net_state_t *net_state,
+    const struct sockaddr_storage *remote_addr,
+    const void *packet,
+    int packet_length,
+    struct timeval timestamp)
+{
+    const struct ICMPHeader *icmp;
+
+    icmp = (struct ICMPHeader *)packet;
+
+    handle_received_icmpv6_packet(
+        net_state, remote_addr, icmp, packet_length, timestamp);
+}
diff --git a/packet/deconstruct_unix.h b/packet/deconstruct_unix.h
new file mode 100644 (file)
index 0000000..c591318
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+    mtr  --  a network diagnostic tool
+    Copyright (C) 2016  Matt Kimball
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+*/
+
+#ifndef DECONSTRUCT_H
+#define DECONSTRUCT_H
+
+#include "probe.h"
+
+typedef void (*received_packet_func_t)(
+    struct net_state_t *net_state,
+    const struct sockaddr_storage *remote_addr,
+    const void *packet,
+    int packet_length,
+    struct timeval timestamp);
+
+void handle_received_ipv4_packet(
+    struct net_state_t *net_state,
+    const struct sockaddr_storage *remote_addr,
+    const void *packet,
+    int packet_length,
+    struct timeval timestamp);
+
+void handle_received_ipv6_packet(
+    struct net_state_t *net_state,
+    const struct sockaddr_storage *remote_addr,
+    const void *packet,
+    int packet_length,
+    struct timeval timestamp);
+
+#endif
index 3856ce06180bc7fdd5b934ebfe5cd578144197e8..6590ba0ffe8213ff75d054a0a75714a3e21a5a19 100644 (file)
@@ -44,14 +44,14 @@ int main(
     struct command_buffer_t command_buffer;
     struct net_state_t net_state;
 
-    init_net_state(&net_state);
-
     /*
         To minimize security risk, the only thing done prior to 
         dropping SUID should be opening the network state for
         raw sockets.
     */
+    init_net_state_privileged(&net_state);
     drop_suid_permissions();
+    init_net_state(&net_state);
 
     init_command_buffer(&command_buffer, fileno(stdin));
 
index 83028945be3770114a8ffb2a37821d4272f1b346..4c98bcc31d31cc950d93a74603b1b019cae88186 100644 (file)
 #include "protocols.h"
 #include "timeval.h"
 
-#define IP_TEXT_LENGTH 32
+#define IP_TEXT_LENGTH 64
 
 /*  Convert the destination address from text to sockaddr  */
 int decode_dest_addr(
     const struct probe_param_t *param,
-    struct sockaddr_in *dest_sockaddr)
+    struct sockaddr_storage *dest_sockaddr)
 {
-    struct in_addr dest_addr;
+    struct in_addr dest_addr4;
+    struct in6_addr dest_addr6;
+    struct sockaddr_in *sockaddr4;
+    struct sockaddr_in6 *sockaddr6;
 
-    if (param->ipv4_address == NULL) {
+    if (param->address == NULL) {
         return EINVAL;
     }
 
-    if (inet_pton(AF_INET, param->ipv4_address, &dest_addr) != 1) {
+    if (param->ip_version == 6) {
+        sockaddr6 = (struct sockaddr_in6 *)dest_sockaddr;
+
+        if (inet_pton(AF_INET6, param->address, &dest_addr6) != 1) {
+            return EINVAL;
+        }
+
+        sockaddr6->sin6_family = AF_INET6;
+        sockaddr6->sin6_port = 0;
+        sockaddr6->sin6_flowinfo = 0;
+        sockaddr6->sin6_addr = dest_addr6;
+        sockaddr6->sin6_scope_id = 0;
+    } else if (param->ip_version == 4) {
+        sockaddr4 = (struct sockaddr_in *)dest_sockaddr;
+
+        if (inet_pton(AF_INET, param->address, &dest_addr4) != 1) {
+            return EINVAL;
+        }
+
+        sockaddr4->sin_family = AF_INET;
+        sockaddr4->sin_port = 0;
+        sockaddr4->sin_addr = dest_addr4;
+    } else {
         return EINVAL;
     }
 
-    dest_sockaddr->sin_family = AF_INET;
-    dest_sockaddr->sin_port = 0;
-    dest_sockaddr->sin_addr = dest_addr;
-
     return 0;
 }
 
@@ -147,19 +168,15 @@ struct probe_t *find_probe(
 void respond_to_probe(
     struct probe_t *probe,
     int icmp_type,
-    struct sockaddr_in remote_addr,
+    const struct sockaddr_storage *remote_addr,
     unsigned int round_trip_us)
 {
     char ip_text[IP_TEXT_LENGTH];
     const char *result;
-
-    if (inet_ntop(
-            AF_INET, &remote_addr.sin_addr,
-            ip_text, IP_TEXT_LENGTH) == NULL) {
-
-        perror("inet_ntop failure");
-        exit(1);
-    }
+    const char *ip_argument;
+    struct sockaddr_in *sockaddr4;
+    struct sockaddr_in6 *sockaddr6;
+    void *addr;
 
     if (icmp_type == ICMP_TIME_EXCEEDED) {
         result = "ttl-expired";
@@ -168,9 +185,87 @@ void respond_to_probe(
         result = "reply";
     }
 
+    if (remote_addr->ss_family == AF_INET6) {
+        ip_argument = "ip-6";
+        sockaddr6 = (struct sockaddr_in6 *)remote_addr;
+        addr = &sockaddr6->sin6_addr;
+    } else {
+        ip_argument = "ip-4";
+        sockaddr4 = (struct sockaddr_in *)remote_addr;
+        addr = &sockaddr4->sin_addr;
+    }
+
+    if (inet_ntop(
+            remote_addr->ss_family, addr, ip_text, IP_TEXT_LENGTH) == NULL) {
+
+        perror("inet_ntop failure");
+        exit(1);
+    }
+
     printf(
-        "%d %s ip-4 %s round-trip-time %d\n",
-        probe->token, result, ip_text, round_trip_us);
+        "%d %s %s %s round-trip-time %d\n",
+        probe->token, result, ip_argument, ip_text, round_trip_us);
 
     free_probe(probe);
 }
+
+/*
+    Find the source address for transmitting to a particular destination
+    address.  Remember that hosts can have multiple addresses, for example
+    a unique address for each network interface.  So we will bind a UDP
+    socket to our destination and check the socket address after binding
+    to get the source for that destination, which will allow the kernel
+    to do the routing table work for us.
+
+    (connecting UDP sockets, unlike TCP sockets, doesn't transmit any packets.
+    It's just an association.)
+*/
+int find_source_addr(
+    struct sockaddr_storage *srcaddr,
+    const struct sockaddr_storage *destaddr)
+{
+    int sock;
+    int len;
+    struct sockaddr_in *destaddr4;
+    struct sockaddr_in6 *destaddr6;
+    struct sockaddr_storage dest_with_port;
+
+    dest_with_port = *destaddr;
+
+    /*
+        MacOS requires a non-zero sin_port when used as an
+        address for a UDP connect.  If we provide a zero port,
+        the connect will fail.  We aren't actually sending
+        anything to the port.
+    */
+    if (destaddr->ss_family == AF_INET6) {
+        destaddr6 = (struct sockaddr_in6 *)&dest_with_port;
+        destaddr6->sin6_port = htons(1);
+
+        len = sizeof(struct sockaddr_in6);
+    } else {
+        destaddr4 = (struct sockaddr_in *)&dest_with_port;
+        destaddr4->sin_port = htons(1);
+
+        len = sizeof(struct sockaddr_in);
+    }
+
+    sock = socket(destaddr->ss_family, SOCK_DGRAM, IPPROTO_UDP);
+    if (sock == -1) {
+        return -errno;
+    }
+
+    if (connect(sock, (struct sockaddr *)&dest_with_port, len)) {
+        close(sock);
+        return -errno;
+    }
+
+    if (getsockname(sock, (struct sockaddr *)srcaddr, &len)) {
+        close(sock);
+        return -errno;
+    }
+
+    close(sock);
+
+    return 0;
+}
index 74dabfcc38847e467bda7f221c06add7cfccafb7..c99c852e7b1563784145b558fc89409566adbfed 100644 (file)
 /*  Parameters for sending a new probe  */
 struct probe_param_t
 {
+    /*  The version of the Internet Protocol to use.  (4 or 6)  */
+    int ip_version;
+
     /*  The command token used to identify a probe when it is completed  */
     int command_token;
 
     /*  The IP address to probe  */
-    const char *ipv4_address;
+    const char *address;
 
     /*  Time to live for the transmited probe  */
     int ttl;
@@ -72,6 +75,9 @@ struct net_state_t
     struct net_state_platform_t platform;
 };
 
+void init_net_state_privileged(
+    struct net_state_t *net_state);
+
 void init_net_state(
     struct net_state_t *net_state);
 
@@ -92,12 +98,12 @@ void check_probe_timeouts(
 void respond_to_probe(
     struct probe_t *probe,
     int icmp_type,
-    struct sockaddr_in remote_addr,
+    const struct sockaddr_storage *remote_addr,
     unsigned int round_trip_us);
 
 int decode_dest_addr(
     const struct probe_param_t *param,
-    struct sockaddr_in *dest_sockaddr);
+    struct sockaddr_storage *dest_sockaddr);
 
 struct probe_t *alloc_probe(
     struct net_state_t *net_state,
@@ -114,4 +120,8 @@ struct probe_t *find_probe(
     int icmp_id,
     int icmp_sequence);
 
+int find_source_addr(
+    struct sockaddr_storage *srcaddr,
+    const struct sockaddr_storage *destaddr);
+
 #endif
index 355ca5c6fc3e735d9b4907b309c3659d9ff9f43c..b00efd5588de3a8cf839a35cb1c832f3f04bd41d 100644 (file)
 
 #include "protocols.h"
 
+/*  Windows doesn't require any initialization at a privileged level  */
+void init_net_state_privileged(
+    struct net_state_t *net_state)
+{
+}
+
 /*  Open the ICMP.DLL interface  */
 void init_net_state(
     struct net_state_t *net_state)
 {
     memset(net_state, 0, sizeof(struct net_state_t));
 
-    net_state->platform.icmp = IcmpCreateFile();
-    if (net_state->platform.icmp == INVALID_HANDLE_VALUE) {
-        fprintf(stderr, "Failure opening ICMP %d\n", GetLastError());
+    net_state->platform.icmp4 = IcmpCreateFile();
+    if (net_state->platform.icmp4 == INVALID_HANDLE_VALUE) {
+        fprintf(stderr, "Failure opening ICMPv4 %d\n", GetLastError());
+        exit(1);
+    }
+
+    net_state->platform.icmp6 = Icmp6CreateFile();
+    if (net_state->platform.icmp6 == INVALID_HANDLE_VALUE) {
+        fprintf(stderr, "Failure opening ICMPv6 %d\n", GetLastError());
         exit(1);
     }
 }
@@ -49,14 +61,51 @@ void WINAPI on_icmp_reply(
 {
     struct probe_t *probe = (struct probe_t *)context;
     int icmp_type;
-    int round_trip_us;
+    int round_trip_us = 0;
     int reply_count;
+    int reply_status = 0;
     int err;
-    struct sockaddr_in remote_addr;
-    ICMP_ECHO_REPLY32 *reply;
+    struct sockaddr_storage remote_addr;
+    struct sockaddr_in *remote_addr4;
+    struct sockaddr_in6 *remote_addr6;
+    ICMP_ECHO_REPLY32 *reply4;
+    ICMPV6_ECHO_REPLY *reply6;
+
+    if (probe->platform.ip_version == 6) {
+        reply6 = &probe->platform.reply6;
+        reply_count = Icmp6ParseReplies(reply6, sizeof(ICMPV6_ECHO_REPLY));
+
+        if (reply_count > 0) {
+            reply_status = reply6->Status;
+
+            /*  Unfortunately, ICMP.DLL only has millisecond precision  */
+            round_trip_us = reply6->RoundTripTime * 1000;
+
+            remote_addr6 = (struct sockaddr_in6 *)&remote_addr;
+            remote_addr6->sin6_family = AF_INET6;
+            remote_addr6->sin6_port = 0;
+            remote_addr6->sin6_flowinfo = 0;
+            memcpy(
+                &remote_addr6->sin6_addr, reply6->AddressBits,
+                sizeof(struct in6_addr));
+            remote_addr6->sin6_scope_id = 0;
+        }
+    } else {
+        reply4 = &probe->platform.reply4;
+        reply_count = IcmpParseReplies(reply4, sizeof(ICMP_ECHO_REPLY));
 
-    reply_count = IcmpParseReplies(
-        &probe->platform.reply, sizeof(ICMP_ECHO_REPLY));
+        if (reply_count > 0) {
+            reply_status = reply4->Status;
+
+            /*  Unfortunately, ICMP.DLL only has millisecond precision  */
+            round_trip_us = reply4->RoundTripTime * 1000;
+
+            remote_addr4 = (struct sockaddr_in *)&remote_addr;
+            remote_addr4->sin_family = AF_INET;
+            remote_addr4->sin_port = 0;
+            remote_addr4->sin_addr.s_addr = reply4->Address;
+        }
+    }
 
     if (reply_count == 0) {
         err = GetLastError();
@@ -73,25 +122,19 @@ void WINAPI on_icmp_reply(
         exit(1);
     }
 
-    reply = &probe->platform.reply;
-
-    remote_addr.sin_family = AF_INET;
-    remote_addr.sin_port = 0;
-    remote_addr.sin_addr.s_addr = reply->Address;
-
-    /*  Unfortunately, ICMP.DLL only gives us millisecond precision  */
-    round_trip_us = reply->RoundTripTime * 1000;
 
     icmp_type = -1;
-    if (reply->Status == IP_SUCCESS) {
+    if (reply_status == IP_SUCCESS) {
         icmp_type = ICMP_ECHOREPLY;
-    } else if (reply->Status == IP_TTL_EXPIRED_TRANSIT) {
+    } else if (reply_status == IP_TTL_EXPIRED_TRANSIT) {
         icmp_type = ICMP_TIME_EXCEEDED;
     }
 
     if (icmp_type != -1) {
         /*  Record probe result  */
-        respond_to_probe(probe, icmp_type, remote_addr, round_trip_us);
+        respond_to_probe(probe, icmp_type, &remote_addr, round_trip_us);
+    } else {
+        fprintf(stderr, "Unexpected ICMP result %d\n", icmp_type);
     }
 }
 
@@ -104,7 +147,11 @@ void send_probe(
     DWORD send_result;
     DWORD timeout;
     struct probe_t *probe;
-    struct sockaddr_in dest_sockaddr;
+    struct sockaddr_storage dest_sockaddr;
+    struct sockaddr_storage src_sockaddr;
+    struct sockaddr_in *dest_sockaddr4;
+    struct sockaddr_in6 *src_sockaddr6;
+    struct sockaddr_in6 *dest_sockaddr6;
 
     if (decode_dest_addr(param, &dest_sockaddr)) {
         printf("%d invalid-argument\n", param->command_token);
@@ -128,14 +175,33 @@ void send_probe(
         return;
     }
 
+    if (find_source_addr(&src_sockaddr, &dest_sockaddr)) {
+        fprintf(stderr, "error finding source address\n");
+        exit(1);
+    }
+
+    probe->platform.ip_version = param->ip_version;
+
     memset(&option, 0, sizeof(IP_OPTION_INFORMATION32));
     option.Ttl = param->ttl;
 
-    send_result = IcmpSendEcho2(
-        net_state->platform.icmp, NULL,
-        (FARPROC)on_icmp_reply, probe,
-        dest_sockaddr.sin_addr.s_addr, NULL, 0, &option,
-        &probe->platform.reply, sizeof(ICMP_ECHO_REPLY), timeout);
+    if (param->ip_version == 6) {
+        src_sockaddr6 = (struct sockaddr_in6 *)&src_sockaddr;
+        dest_sockaddr6 = (struct sockaddr_in6 *)&dest_sockaddr;
+
+        send_result = Icmp6SendEcho2(
+            net_state->platform.icmp6, NULL,
+            (FARPROC)on_icmp_reply, probe,
+            src_sockaddr6, dest_sockaddr6, NULL, 0, &option,
+            &probe->platform.reply6, sizeof(ICMPV6_ECHO_REPLY), timeout);
+    } else {
+        dest_sockaddr4 = (struct sockaddr_in *)&dest_sockaddr;
+        send_result = IcmpSendEcho2(
+            net_state->platform.icmp4, NULL,
+            (FARPROC)on_icmp_reply, probe,
+            dest_sockaddr4->sin_addr.s_addr, NULL, 0, &option,
+            &probe->platform.reply4, sizeof(ICMP_ECHO_REPLY), timeout);
+    }
 
     if (send_result == 0) {
         /*
index b8ceb08543d6c2f18ed03fd7fac5edbac1304f09..a4d5f284822147cc2ab8e280c960a61f49bf16e5 100644 (file)
 #include <iphlpapi.h>
 #include <icmpapi.h>
 
+/*
+    This should be in the Windows headers, but is missing from
+    Cygwin's Windows headers.
+*/
+typedef struct icmpv6_echo_reply_lh
+{
+    /*
+        Although Windows uses an IPV6_ADDRESS_EX here, we are using uint8_t
+        fields to avoid structure padding differences between gcc and
+        Visual C++.  (gcc wants to align the flow info to a 4 byte boundary,
+        and Windows uses it unaligned.)
+    */
+    uint8_t PortBits[2];
+    uint8_t FlowInfoBits[4];
+    uint8_t AddressBits[16];
+    uint8_t ScopeIdBits[4];
+
+    ULONG Status;
+    unsigned int RoundTripTime;
+} ICMPV6_ECHO_REPLY, *PICMPV6_ECHO_REPLY;
+
 /*
        Windows requires an echo reply structure for each in-flight
        ICMP probe.
 */
 struct probe_platform_t
 {
-    ICMP_ECHO_REPLY32 reply;
+    /*  IP version (4 or 6) used for the probe  */
+    int ip_version;
+
+    union {
+        ICMP_ECHO_REPLY32 reply4;
+        ICMPV6_ECHO_REPLY reply6;
+    };
 };
 
 /*  A Windows HANDLE for the ICMP session  */
 struct net_state_platform_t
 {
-    HANDLE icmp;
+    HANDLE icmp4;
+    HANDLE icmp6;
 };
 
 #endif
index 3a6bdee64f77b42b19def6f1577d7ffb611428f6..35b3d9ded60057f6e12183c050d7833b4898e4ca 100644 (file)
 #include <sys/socket.h>
 #include <unistd.h>
 
-#include "protocols.h"
+#include "platform.h"
+#include "construct_unix.h"
+#include "deconstruct_unix.h"
 #include "timeval.h"
 
 /*  Use the "jumbo" frame size as the max packet size  */
 #define PACKET_BUFFER_SIZE 9000
 
-/*  Compute the IP checksum (or ICMP checksum) of a packet.  */
+/*  Set the IPv6 options affecting and outgoing IPv6 packet  */
 static
-uint16_t compute_checksum(
-    const void *packet,
-    int size)
+void set_ipv6_socket_options(
+    int socket,
+    const struct probe_param_t *param)
 {
-    const uint8_t *packet_bytes = (uint8_t *)packet;
-    uint32_t sum = 0;
-    int i;
-
-    for (i = 0; i < size; i++) {
-        if ((i & 1) == 0) {
-            sum += packet_bytes[i] << 8;
-        } else {
-            sum += packet_bytes[i];
-        }
-    }
-
-    /*
-        Sums which overflow a 16-bit value have the high bits
-        added back into the low 16 bits.
-    */
-    while (sum >> 16) {
-        sum = (sum >> 16) + (sum & 0xffff);
-    }
-
-    /*
-        The value stored is the one's complement of the
-        mathematical sum.
-    */
-    return (~sum & 0xffff);
-}
+    if (setsockopt(
+            socket, IPPROTO_IPV6,
+            IPV6_UNICAST_HOPS, &param->ttl, sizeof(int))) {
 
-/*  Encode the IP header length field in the order required by the OS.  */
-static
-uint16_t length_byte_swap(
-    const struct net_state_t *net_state,
-    uint16_t length)
-{
-    if (net_state->platform.ip_length_host_order) {
-        return length;
-    } else {
-        return htons(length);
+        perror("Failure to set IPV6_UNICAST_HOPS");
+        exit(1);
     }
 }
 
-/*  Construct a probe packet based on the probe parameters  */
+/*  A wrapper around sendto for mixed IPv4 and IPv6 sending  */
 static
-int construct_packet(
+int send_packet(
     const struct net_state_t *net_state,
-    char *packet_buffer,
-    int packet_buffer_size,
-    struct sockaddr_in dest_sockaddr,
-    const struct probe_param_t *param)
+    const struct probe_param_t *param,
+    const char *packet,
+    int packet_size,
+    const struct sockaddr_storage *sockaddr)
 {
-    struct IPHeader *ip;
-    struct ICMPHeader *icmp;
-    int packet_size;
-    int icmp_size;
+    int send_socket;
+    int sockaddr_length;
 
-    ip = (struct IPHeader *)&packet_buffer[0];
-    icmp = (struct ICMPHeader *)(ip + 1);
-    packet_size = sizeof(struct IPHeader) + sizeof(struct ICMPHeader);
-    icmp_size = packet_size - sizeof(struct IPHeader);
+    if (sockaddr->ss_family == AF_INET6) {
+        send_socket = net_state->platform.ipv6_send_socket;
+        sockaddr_length = sizeof(struct sockaddr_in6);
 
-    if (packet_buffer_size < packet_size) {
-        return -EINVAL;
+        if (!net_state->platform.ipv6_header_constructed) {
+            set_ipv6_socket_options(send_socket, param);
+        }
+    } else {
+        assert(sockaddr->ss_family == AF_INET);
+        send_socket = net_state->platform.ipv4_send_socket;
+        sockaddr_length = sizeof(struct sockaddr_in);
     }
 
-    memset(packet_buffer, 0, packet_size);
-
-    /*  Fill the IP header  */
-    ip->version = 0x45;
-    ip->len = length_byte_swap(net_state, packet_size);
-    ip->ttl = param->ttl;
-    ip->protocol = IPPROTO_ICMP;
-    memcpy(&ip->daddr, &dest_sockaddr.sin_addr, sizeof(uint32_t));
-
-    /*  Fill the ICMP header  */
-    icmp->type = ICMP_ECHO;
-    icmp->id = htons(getpid());
-    icmp->sequence = htons(param->command_token);
-    icmp->checksum = htons(compute_checksum(icmp, icmp_size));
-
-    return packet_size;
+    return sendto(
+        send_socket, packet, packet_size, 0,
+        (struct sockaddr *)sockaddr, sockaddr_length);
 }
 
 /*
@@ -137,13 +97,14 @@ void check_length_order(
 {
     char packet[PACKET_BUFFER_SIZE];
     struct probe_param_t param;
-    struct sockaddr_in dest_sockaddr;
+    struct sockaddr_storage dest_sockaddr;
     ssize_t bytes_sent;
     int packet_size;
 
     memset(&param, 0, sizeof(struct probe_param_t));
+    param.ip_version = 4;
     param.ttl = 255;
-    param.ipv4_address = "127.0.0.1";
+    param.address = "127.0.0.1";
 
     if (decode_dest_addr(&param, &dest_sockaddr)) {
         fprintf(stderr, "Error decoding localhost address\n");
@@ -154,15 +115,15 @@ void check_length_order(
     net_state->platform.ip_length_host_order = false;
 
     packet_size = construct_packet(
-        net_state, packet, PACKET_BUFFER_SIZE, dest_sockaddr, &param);
-    assert(packet_size > 0);
-
-    bytes_sent = sendto(
-        net_state->platform.ipv4_send_socket,
-        packet, packet_size, 0,
-        (struct sockaddr *)&dest_sockaddr,
-        sizeof(struct sockaddr_in));
+        net_state, packet, PACKET_BUFFER_SIZE, &dest_sockaddr, &param);
+    if (packet_size < 0) {
+        errno = -packet_size;
+        perror("Unable to send to localhost");
+        exit(1);
+    }
 
+    bytes_sent = send_packet(
+        net_state, &param, packet, packet_size, &dest_sockaddr);
     if (bytes_sent > 0) {
         return;
     }
@@ -171,35 +132,52 @@ void check_length_order(
     net_state->platform.ip_length_host_order = true;
 
     packet_size = construct_packet(
-        net_state, packet, PACKET_BUFFER_SIZE, dest_sockaddr, &param);
-    assert(packet_size > 0);
-
-    bytes_sent = sendto(
-        net_state->platform.ipv4_send_socket,
-        packet, packet_size, 0,
-        (struct sockaddr *)&dest_sockaddr,
-        sizeof(struct sockaddr_in));
+        net_state, packet, PACKET_BUFFER_SIZE, &dest_sockaddr, &param);
+    if (packet_size < 0) {
+        errno = -packet_size;
+        perror("Unable to send to localhost");
+        exit(1);
+    }
 
+    bytes_sent = send_packet(
+        net_state, &param, packet, packet_size, &dest_sockaddr);
     if (bytes_sent < 0) {
         perror("Unable to send with swapped length");
         exit(1);
     }
 }
 
-/*  Open the raw sockets for transmitting custom crafted packets  */
-void init_net_state(
+/*  Set a socket to non-blocking mode  */
+static
+void set_socket_nonblocking(
+    int socket)
+{
+    int flags;
+
+    flags = fcntl(socket, F_GETFL, 0);
+    if (flags == -1) {
+        perror("Unexpected socket F_GETFL error");
+        exit(1);
+    }
+
+    if (fcntl(socket, F_SETFL, flags | O_NONBLOCK)) {
+        perror("Unexpected socket F_SETFL O_NONBLOCK error");
+        exit(1);
+    }
+}
+
+/*  Open the raw sockets for sending/receiving IPv4 packets  */
+static
+void open_ipv4_sockets(
     struct net_state_t *net_state)
 {
     int send_socket;
     int recv_socket;
-    int flags;
     int trueopt = 1;
 
-    memset(net_state, 0, sizeof(struct net_state_t));
-
     send_socket = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
     if (send_socket == -1) {
-        perror("Failure opening raw socket");
+        perror("Failure opening IPv4 send socket");
         exit(1);
     }
 
@@ -220,24 +198,84 @@ void init_net_state(
     */
     recv_socket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
     if (recv_socket == -1) {
-        perror("Failure opening raw socket");
+        perror("Failure opening IPv4 receive socket");
         exit(1);
     }
 
-    flags = fcntl(recv_socket, F_GETFL, 0);
-    if (flags == -1) {
-        perror("Unexpected socket error");
+    net_state->platform.ipv4_send_socket = send_socket;
+    net_state->platform.ipv4_recv_socket = recv_socket;
+}
+
+/*  Open the raw sockets for sending/receiving IPv6 packets  */
+static
+void open_ipv6_sockets(
+    struct net_state_t *net_state)
+{
+    int send_socket;
+    int recv_socket;
+    int send_protocol;
+
+    /*
+        Linux allows us to construct our own IPv6 header, so
+        we'll prefer that method for more explicit control.
+
+        Other OSes, such as MacOS, don't allow this, and on
+        those platforms we must use setsockopt() to control
+        fields of the IP header.
+    */
+#ifdef PLATFORM_LINUX
+    net_state->platform.ipv6_header_constructed = true;
+#else
+    net_state->platform.ipv6_header_constructed = false;
+#endif
+
+    if (net_state->platform.ipv6_header_constructed) {
+        send_protocol = IPPROTO_RAW;
+    } else {
+        send_protocol = IPPROTO_ICMPV6;
+    }
+
+    send_socket = socket(AF_INET6, SOCK_RAW, send_protocol);
+    if (send_socket == -1) {
+        perror("Failure opening IPv6 send socket");
         exit(1);
     }
 
-    /*  Set the receive socket to be non-blocking  */
-    if (fcntl(recv_socket, F_SETFL, flags | O_NONBLOCK)) {
-        perror("Unexpected socket error");
+    recv_socket = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
+    if (recv_socket == -1) {
+        perror("Failure opening IPv6 receive socket");
         exit(1);
     }
 
-    net_state->platform.ipv4_send_socket = send_socket;
-    net_state->platform.ipv4_recv_socket = recv_socket;
+    set_socket_nonblocking(recv_socket);
+
+    net_state->platform.ipv6_send_socket = send_socket;
+    net_state->platform.ipv6_recv_socket = recv_socket;
+}
+
+/*
+    The first half of the net state initialization.  Since this
+    happens with elevated privileges, this is kept as minimal
+    as possible to minimize security risk.
+*/
+void init_net_state_privileged(
+    struct net_state_t *net_state)
+{
+    memset(net_state, 0, sizeof(struct net_state_t));
+
+    open_ipv4_sockets(net_state);
+    open_ipv6_sockets(net_state);
+}
+
+/*
+    The second half of net state initialization, which is run
+    at normal privilege levels.
+*/
+void init_net_state(
+    struct net_state_t *net_state)
+{
+    set_socket_nonblocking(net_state->platform.ipv4_recv_socket);
+    set_socket_nonblocking(net_state->platform.ipv6_recv_socket);
 
     check_length_order(net_state);
 }
@@ -248,7 +286,7 @@ void send_probe(
     const struct probe_param_t *param)
 {
     char packet[PACKET_BUFFER_SIZE];
-    struct sockaddr_in dest_sockaddr;
+    struct sockaddr_storage dest_sockaddr;
     struct probe_t *probe;
     int packet_size;
 
@@ -258,9 +296,19 @@ void send_probe(
     }
 
     packet_size = construct_packet(
-        net_state, packet, PACKET_BUFFER_SIZE, dest_sockaddr, param);
+        net_state, packet, PACKET_BUFFER_SIZE, &dest_sockaddr, param);
     if (packet_size < 0) {
-        printf("%d invalid-argument\n", param->command_token);
+        if (packet_size == -EINVAL) {
+            printf("%d invalid-argument\n", param->command_token);
+        } else if (packet_size == -ENETDOWN) {
+            printf("%d network-down\n", param->command_token);
+        } else if (packet_size == -ENETUNREACH) {
+            printf("%d no-route\n", param->command_token);
+        } else {
+            errno = -packet_size;
+            perror("Failure constructing packet");
+            exit(1);
+        }
         return;
     }
 
@@ -279,11 +327,8 @@ void send_probe(
         exit(1);
     }
 
-    if (sendto(
-            net_state->platform.ipv4_send_socket,
-            packet, packet_size, 0,
-            (struct sockaddr *)&dest_sockaddr,
-            sizeof(struct sockaddr_in)) == -1) {
+    if (send_packet(
+            net_state, param, packet, packet_size, &dest_sockaddr) == -1) {
 
         perror("Failure sending probe");
         exit(1);
@@ -293,117 +338,26 @@ void send_probe(
     probe->platform.timeout_time.tv_sec += param->timeout;
 }
 
-/*
-    Compute the round trip time of a just-received probe and pass it
-    to the platform agnostic response handling.
-*/
-static
-void receive_probe(
-    struct probe_t *probe,
-    int icmp_type,
-    struct sockaddr_in remote_addr,
-    struct timeval timestamp)
-{
-    unsigned int round_trip_us;
-
-    round_trip_us =
-        (timestamp.tv_sec - probe->platform.departure_time.tv_sec) * 1000000 +
-        timestamp.tv_usec - probe->platform.departure_time.tv_usec;
-
-    respond_to_probe(probe, icmp_type, remote_addr, round_trip_us);
-}
-
-/*
-    Called when we have received a new packet through our raw socket.
-    We'll check to see that it is a response to one of our probes, and
-    if so, report the result of the probe to our command stream.
-*/
-static
-void handle_received_packet(
-    struct net_state_t *net_state,
-    struct sockaddr_in remote_addr,
-    const void *packet,
-    int packet_length,
-    struct timeval timestamp)
-{
-    const int ip_icmp_size =
-        sizeof(struct IPHeader) + sizeof(struct ICMPHeader);
-    const int ip_icmp_ip_icmp_size = 
-        sizeof(struct IPHeader) + sizeof(struct ICMPHeader) +
-        sizeof(struct IPHeader) + sizeof(struct ICMPHeader);
-    const struct IPHeader *ip;
-    const struct ICMPHeader *icmp;
-    const struct IPHeader *inner_ip;
-    const struct ICMPHeader *inner_icmp;
-    struct probe_t *probe;
-
-    /*  Ensure that we don't access memory beyond the bounds of the packet  */
-    if (packet_length < ip_icmp_size) {
-        return;
-    }
-
-    ip = (struct IPHeader *)packet;
-    if (ip->protocol != IPPROTO_ICMP) {
-        return;
-    }
-
-    icmp = (struct ICMPHeader *)(ip + 1);
-
-    /*  If we get an echo reply, our probe reached the destination host  */
-    if (icmp->type == ICMP_ECHOREPLY) {
-        probe = find_probe(net_state, icmp->id, icmp->sequence);
-        if (probe == NULL) {
-            return;
-        }
-
-        receive_probe(probe, icmp->type, remote_addr, timestamp);
-    }
-
-    /*
-        If we get a time exceeded, we got a response from an intermediate
-        host along the path to our destination.
-    */
-    if (icmp->type == ICMP_TIME_EXCEEDED) {
-        if (packet_length < ip_icmp_ip_icmp_size) {
-            return;
-        }
-
-        /*
-            The IP packet inside the ICMP response contains our original
-            IP header.  That's where we can get our original ID and
-            sequence number.
-        */
-        inner_ip = (struct IPHeader *)(icmp + 1);
-        inner_icmp = (struct ICMPHeader *)(inner_ip + 1);
-
-        probe = find_probe(net_state, inner_icmp->id, inner_icmp->sequence);
-        if (probe == NULL) {
-            return;
-        }
-
-        receive_probe(probe, icmp->type, remote_addr, timestamp);
-    }
-}
-
 /*
     Read all available packets through our receiving raw socket, and
     handle any responses to probes we have preivously sent.
 */
-void receive_replies(
-    struct net_state_t *net_state)
+void receive_replies_from_socket(
+    struct net_state_t *net_state,
+    int socket,
+    received_packet_func_t handle_received_packet)
 {
     char packet[PACKET_BUFFER_SIZE];
     int packet_length;
-    struct sockaddr_in remote_addr;
+    struct sockaddr_storage remote_addr;
     socklen_t sockaddr_length;
     struct timeval timestamp;
 
     /*  Read until no more packets are available  */
     while (true) {
-        sockaddr_length = sizeof(struct sockaddr_in);
+        sockaddr_length = sizeof(struct sockaddr_storage);
         packet_length = recvfrom(
-            net_state->platform.ipv4_recv_socket,
-            packet, PACKET_BUFFER_SIZE, 0,
+            socket, packet, PACKET_BUFFER_SIZE, 0,
             (struct sockaddr *)&remote_addr, &sockaddr_length);
 
         /*
@@ -437,8 +391,22 @@ void receive_replies(
         }
 
         handle_received_packet(
-            net_state, remote_addr, packet, packet_length, timestamp);
+            net_state, &remote_addr, packet, packet_length, timestamp);
     }
+
+}
+
+/*  Check both the IPv4 and IPv6 sockets for incoming packets  */
+void receive_replies(
+    struct net_state_t *net_state)
+{
+    receive_replies_from_socket(
+        net_state, net_state->platform.ipv4_recv_socket,
+        handle_received_ipv4_packet);
+
+    receive_replies_from_socket(
+        net_state, net_state->platform.ipv6_recv_socket,
+        handle_received_ipv6_packet);
 }
 
 /*
index a9a0dfbb527575060812eda8d13595374ac1ce34..d0080a4c0fff6b70f7ee5d6fb155af1d93e32b8f 100644 (file)
@@ -33,17 +33,29 @@ struct probe_platform_t
 /*  We'll use rack sockets to send and recieve probes on Unix systems  */
 struct net_state_platform_t
 {
-    /*  Socket used to send raw packets  */
+    /*  Socket used to send raw IPv4 packets  */
     int ipv4_send_socket;
 
-    /*  Socket used to receive ICMP replies  */
+    /*  Socket used to receive IPv4 ICMP replies  */
     int ipv4_recv_socket;
 
+    /*  Send socket for IPv6 packets  */
+    int ipv6_send_socket;
+
+    /*  Receive socket for IPv6 packets  */
+    int ipv6_recv_socket;
+
     /*
         true if we should encode the IP header length in host order.
         (as opposed to network order)
     */
     bool ip_length_host_order;
+
+    /*
+        true if we are allowed to construct the IPv6 header, false if
+        we need to let the network stack do it for us.
+    */
+    bool ipv6_header_constructed;
 };
 
 #endif
index f30c24111eb7a657c8f5acbc9697c3e998b0de13..13046c820a28a0e1729c9decbf41f33dea204de2 100644 (file)
 #ifndef PROTOCOLS_H
 #define PROTOCOLS_H
 
-/*  ICMP type codes  */
+/*  ICMPv4 type codes  */
 #define ICMP_ECHOREPLY 0
 #define ICMP_DEST_UNREACH 3
 #define ICMP_ECHO 8
 #define ICMP_TIME_EXCEEDED 11
 
+/*  ICMPv6 type codes  */
+#define ICMP6_TIME_EXCEEDED 3
+#define ICMP6_ECHO 128
+#define ICMP6_ECHOREPLY 129
+
 /*  We can't rely on header files to provide this information, because
     the fields have different names between, for instance, Linux and 
     Solaris  */
@@ -81,4 +86,24 @@ struct IPHeader {
     uint32_t daddr;
 };
 
+/*  IP version 6 header  */
+struct IP6Header {
+    uint8_t version;
+    uint8_t flow[3];
+    uint16_t len;
+    uint8_t protocol;
+    uint8_t ttl;
+    uint8_t saddr[16];
+    uint8_t daddr[16];
+};
+
+/*  The pseudo-header used for checksum computation for ICMPv6 and UDPv6  */
+struct IP6PseudoHeader {
+    uint8_t saddr[16];
+    uint8_t daddr[16];
+    uint32_t len;
+    uint8_t zero[3];
+    uint8_t protocol;
+};
+
 #endif
diff --git a/packet/testpacket.py b/packet/testpacket.py
deleted file mode 100644 (file)
index b145bb4..0000000
+++ /dev/null
@@ -1,359 +0,0 @@
-#!/usr/bin/env python
-#
-#   mtr  --  a network diagnostic tool
-#   Copyright (C) 2016  Matt Kimball
-#
-#   This program is free software; you can redistribute it and/or modify
-#   it under the terms of the GNU General Public License version 2 as
-#   published by the Free Software Foundation.
-#
-#   This program is distributed in the hope that it will be useful,
-#   but WITHOUT ANY WARRANTY; without even the implied warranty of
-#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#   GNU General Public License for more details.
-#
-#   You should have received a copy of the GNU General Public License
-#   along with this program; if not, write to the Free Software
-#   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
-#
-
-
-'''Test mtr-packet's functionality
-
-Test the ability to send probes and receive replies using mtr-packet.
-'''
-
-# pylint: disable=locally-disabled, import-error
-import fcntl
-import os
-import re
-import select
-import subprocess
-import sys
-import time
-import unittest
-
-
-class ReadReplyTimeout(Exception):
-    'Exception raised by TestProbe.read_reply upon timeout'
-
-    pass
-
-
-class TestProbe(unittest.TestCase):
-    'Test cases for sending and receiving probes'
-
-    def __init__(self, *args):
-        self.reply_buffer = None  # type: str
-        self.packet_process = None  # type: subprocess.Popen
-        self.stdout_fd = None  # type: int
-
-        super(TestProbe, self).__init__(*args)
-
-    def setUp(self):
-        'Set up a test case by spawning a mtr-packet process'
-
-        packet_path = os.environ.get('MTR_PACKET', './mtr-packet')
-
-        self.reply_buffer = ''
-        self.packet_process = subprocess.Popen(
-            [packet_path],
-            stdin=subprocess.PIPE,
-            stdout=subprocess.PIPE)
-
-        #  Put the mtr-packet process's stdout in non-blocking mode
-        #  so that we can read from it without a timeout when
-        #  no reply is available.
-        self.stdout_fd = self.packet_process.stdout.fileno()
-        flags = fcntl.fcntl(self.stdout_fd, fcntl.F_GETFL)
-
-        # pylint: disable=locally-disabled, no-member
-        fcntl.fcntl(self.stdout_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
-
-    def tearDown(self):
-        'After a test, kill the running mtr-packet instance'
-
-        try:
-            self.packet_process.kill()
-        except OSError:
-            return
-
-        self.packet_process.stdin.close()
-        self.packet_process.stdout.close()
-
-    def write_command(self, cmd):  # type: (str) -> None
-        'Send a command string to the mtr-packet instance'
-
-        command_str = cmd + '\n'
-        command_bytes = command_str.encode('utf-8')
-
-        self.packet_process.stdin.write(command_bytes)
-        self.packet_process.stdin.flush()
-
-    def read_reply(self, timeout=10.0):  # type: (float) -> str
-        '''Read the next reply from mtr-packet.
-
-        Attempt to read the next command reply from mtr-packet.  If no reply
-        is available withing the timeout time, raise ReadReplyTimeout
-        instead.'''
-
-        start_time = time.time()
-
-        #  Read from mtr-packet until either the timeout time has elapsed
-        #  or we read a newline character, which indicates a finished
-        #  reply.
-        while True:
-            now = time.time()
-            elapsed = now - start_time
-
-            select_time = timeout - elapsed
-            if select_time < 0:
-                select_time = 0
-
-            select.select([self.stdout_fd], [], [], select_time)
-
-            reply_bytes = None
-
-            try:
-                reply_bytes = os.read(self.stdout_fd, 1024)
-            except OSError:
-                pass
-
-            if reply_bytes:
-                self.reply_buffer += reply_bytes.decode('utf-8')
-
-            #  If we have read a newline character, we can stop waiting
-            #  for more input.
-            newline_ix = self.reply_buffer.find('\n')
-            if newline_ix != -1:
-                break
-
-            if elapsed >= timeout:
-                raise ReadReplyTimeout()
-
-        reply = self.reply_buffer[:newline_ix]
-        self.reply_buffer = self.reply_buffer[newline_ix + 1:]
-        return reply
-
-    def test_unknown_command(self):
-        'Test sending a command unknown to mtr-packet'
-
-        self.write_command('13 argle-bargle')
-        self.assertEqual(self.read_reply(), '13 unknown-command')
-
-    def test_malformed_command(self):
-        'Test sending a malformed command request to mtr-packet'
-
-        self.write_command('malformed')
-        self.assertEqual(self.read_reply(), '0 command-parse-error')
-
-    def test_exit_on_stdin_closed(self):
-        '''Test that the packet process terminates after stdin is closed
-
-        Test that, when outstanding requests are complete, the process
-        terminates following stdin being closed.'''
-
-        self.write_command('15 send-probe ip-4 8.8.254.254 timeout 1')
-        self.packet_process.stdin.close()
-        time.sleep(2)
-        self.read_reply()
-        exit_code = self.packet_process.poll()
-        self.assertIsNotNone(exit_code)
-
-    def test_probe(self):
-        'Test sending regular ICMP probes to known addresses'
-
-        reply_regex = r'^14 reply ip-4 8.8.8.8 round-trip-time [0-9]+$'
-
-        #  Probe Google's well-known DNS server and expect a reply
-        self.write_command('14 send-probe ip-4 8.8.8.8')
-        reply = self.read_reply()
-        match = re.match(reply_regex, reply)
-        self.assertIsNotNone(match)
-
-    def test_invalid_argument(self):
-        'Test sending invalid arguments with probe requests'
-
-        invalid_argument_regex = r'^[0-9]+ invalid-argument$'
-
-        bad_commands = [
-            '22 send-probe',
-            '23 send-probe ip-4 str-value',
-            '24 send-probe ip-4 8.8.8.8 timeout str-value',
-            '25 send-probe ip-4 8.8.8.8 ttl str-value',
-        ]
-
-        for cmd in bad_commands:
-            self.write_command(cmd)
-            reply = self.read_reply()
-            match = re.match(invalid_argument_regex, reply)
-            self.assertIsNotNone(match)
-
-    def test_timeout(self):
-        'Test timeouts when sending to a non-existant address'
-
-        no_reply_regex = r'^15 no-reply$'
-
-        #
-        #  Probe a non-existant address, and expect no reply
-        #
-        #  I'm not sure what the best way to find an address that doesn't
-        #  exist, but is still route-able.  If we use a reserved IP
-        #  address range, Windows will tell us it is non-routeable,
-        #  rather than timing out when transmitting to that address.
-        #
-        #  We're just using a currently unused address in Google's
-        #  range instead.  This is probably not the best solution.
-        #
-
-        # pylint: disable=locally-disabled, unused-variable
-        for i in range(16):
-            self.write_command('15 send-probe ip-4 8.8.254.254 timeout 1')
-            reply = self.read_reply()
-            match = re.match(no_reply_regex, reply)
-            self.assertIsNotNone(match)
-
-    def test_exhaust_probes(self):
-        'Test exhausting all available probes'
-
-        exhausted_regex = r'^[0-9]+ probes-exhausted$'
-
-        match = None
-        probe_count = 4 * 1024
-        id = 1024
-        for i in range(probe_count):
-            command = str(id) + ' send-probe ip-4 8.8.254.254 timeout 60'
-            id += 1
-            self.write_command(command)
-
-            reply = None
-            try:
-                reply = self.read_reply(0)
-            except ReadReplyTimeout:
-                pass
-
-            if reply:
-                match = re.match(exhausted_regex, reply)
-                if match:
-                    break
-
-        self.assertIsNotNone(match)
-
-    def test_timeout_values(self):
-        '''Test that timeout values wait the right amount of time
-
-        Give each probe a half-second grace period to probe a timeout
-        reply after the expected timeout time.'''
-
-        begin = time.time()
-        self.write_command('19 send-probe ip-4 8.8.254.254 timeout 0')
-        self.read_reply()
-        elapsed = time.time() - begin
-        self.assertLess(elapsed, 0.5)
-
-        begin = time.time()
-        self.write_command('20 send-probe ip-4 8.8.254.254 timeout 1')
-        self.read_reply()
-        elapsed = time.time() - begin
-        self.assertGreaterEqual(elapsed, 1.0)
-        self.assertLess(elapsed, 1.5)
-
-        begin = time.time()
-        self.write_command('21 send-probe ip-4 8.8.254.254 timeout 3')
-        self.read_reply()
-        elapsed = time.time() - begin
-        self.assertGreaterEqual(elapsed, 3.0)
-        self.assertLess(elapsed, 3.5)
-
-    def test_ttl_expired(self):
-        'Test sending a probe which will have its time-to-live expire'
-
-        ttl_expired_regex = \
-            r'^16 ttl-expired ip-4 [0-9\.]+ round-trip-time [0-9]+$'
-
-        #  Probe Goolge's DNS server, but give the probe only one hop
-        #  to live.
-        self.write_command('16 send-probe ip-4 8.8.8.8 ttl 1')
-        reply = self.read_reply()
-        match = re.match(ttl_expired_regex, reply)
-        self.assertIsNotNone(match)
-
-    def test_parallel_probes(self):
-        '''Test sending multiple probes in parallel
-
-        We will expect the probes to complete out-of-order by sending
-        a probe to a distant host immeidately followed by a probe to
-        the local host.'''
-
-        reply_regex = \
-            r'^[0-9]+ reply ip-4 [0-9\.]+ round-trip-time ([0-9]+)$'
-
-        success_count = 0
-        loop_count = 32
-
-        # pylint: disable=locally-disabled, unused-variable
-        for i in range(loop_count):
-            #  Probe the distant host before the local host.
-            self.write_command('17 send-probe ip-4 8.8.8.8 timeout 1')
-            self.write_command('18 send-probe ip-4 127.0.0.1 timeout 1')
-
-            reply = self.read_reply()
-            match = re.match(reply_regex, reply)
-            if not match:
-                continue
-            first_time = int(match.group(1))
-
-            reply = self.read_reply()
-            match = re.match(reply_regex, reply)
-            if not match:
-                continue
-            second_time = int(match.group(1))
-
-            #  Ensure we got a reply from the host with the lowest latency
-            #  first.
-            self.assertLess(first_time, second_time)
-
-            success_count += 1
-
-        #  We need 95% success to pass.  This allows a few probes to be
-        #  occasionally dropped by the network without failing the test.
-        required_success = int(loop_count * 0.95)
-        self.assertGreaterEqual(success_count, required_success)
-
-    def test_versioning(self):
-        'Test version checks and feature support checks'
-
-        feature_tests = [
-            ('30 check-support feature version',
-             r'^30 feature-support support [0-9]+\.[0-9a-z\-\.]+$'),
-            ('31 check-support feature ip-4',
-             r'^31 feature-support support ok$'),
-            ('32 check-support feature send-probe',
-             r'^32 feature-support support ok$'),
-            ('33 check-support feature bogus-feature',
-             r'^33 feature-support support no$')
-        ]
-
-        for (request, regex) in feature_tests:
-            self.write_command(request)
-            reply = self.read_reply()
-            match = re.match(regex, reply)
-            self.assertIsNotNone(match)
-
-    def test_command_overflow(self):
-        'Test overflowing the incoming command buffer'
-
-        big_buffer = 'x' * (64 * 1024)
-        self.write_command(big_buffer)
-
-        reply = self.read_reply()
-        self.assertEqual(reply, '0 command-buffer-overflow')
-
-
-if __name__ == '__main__':
-    # pylint: disable=locally-disabled, no-member
-    if sys.platform != 'cygwin' and os.getuid() > 0:
-        sys.stderr.write(
-            "Warning: Many tests require running as root\n")
-
-    unittest.main()
index 3008b6ff386b33c322521f5a89670228bc7d44ad..0d24b4a77bfa1b4031295ab68d0c1d14a53d8400 100644 (file)
@@ -23,7 +23,7 @@
 #include "probe.h"
 
 void wait_for_activity(
-    const struct command_buffer_t *command_buffer,
-    const struct net_state_t *net_state);
+    struct command_buffer_t *command_buffer,
+    struct net_state_t *net_state);
 
 #endif
index e459c7e30c30826e8e1d8154aa19e6597f92c76e..517f71f7d652b446d24bc6ff56da1794d6906a7f 100644 (file)
@@ -22,6 +22,8 @@
 #include <stdio.h>
 #include <windows.h>
 
+#include "command.h"
+
 /*
     Sleep until we receive a new probe response, a new command on the
     command stream, or a probe timeout.  On Windows, this means that
     use I/O completion routines as notifications of these events.
 */
 void wait_for_activity(
-    const struct command_buffer_t *command_buffer,
-    const struct net_state_t *net_state)
+    struct command_buffer_t *command_buffer,
+    struct net_state_t *net_state)
 {
     DWORD wait_result;
 
+    /*
+        Start the command read overlapped I/O just prior to sleeping.
+        During development of the Cygwin port, there was a bug where the
+        overlapped I/O was started earlier in the mtr-packet loop, and
+        an intermediate alertable wait could leave us in this Sleep
+        without an active command read.  So now we do this here, instead.
+    */
+    start_read_command(command_buffer);
+
     /*  Sleep until an I/O completion routine runs  */
     wait_result = SleepEx(INFINITE, TRUE);
 
index eba6fd33b5d3f24be1e5f86580ea42052ad9336f..348831d59ab04570bd0f26de3c89137ca42efe35 100644 (file)
@@ -32,8 +32,8 @@
     and the raw recieve socket.
 */
 void wait_for_activity(
-    const struct command_buffer_t *command_buffer,
-    const struct net_state_t *net_state)
+    struct command_buffer_t *command_buffer,
+    struct net_state_t *net_state)
 {
     int nfds;
     fd_set read_set;
@@ -41,14 +41,21 @@ void wait_for_activity(
     struct timeval *select_timeout;
     int ready_count;
     int command_stream = command_buffer->command_stream;
-    int socket = net_state->platform.ipv4_recv_socket;
+    int ipv4_socket = net_state->platform.ipv4_recv_socket;
+    int ipv6_socket = net_state->platform.ipv6_recv_socket;
 
     FD_ZERO(&read_set);
     FD_SET(command_stream, &read_set);
     nfds = command_stream + 1;
-    FD_SET(socket, &read_set);
-    if (socket >= nfds) {
-        nfds = socket + 1;
+
+    FD_SET(ipv4_socket, &read_set);
+    if (ipv4_socket >= nfds) {
+        nfds = ipv4_socket + 1;
+    }
+
+    FD_SET(ipv6_socket, &read_set);
+    if (ipv6_socket >= nfds) {
+        nfds = ipv6_socket + 1;
     }
 
     while (true) {
diff --git a/test/cmdparse.py b/test/cmdparse.py
new file mode 100755 (executable)
index 0000000..8c90ec5
--- /dev/null
@@ -0,0 +1,108 @@
+#!/usr/bin/env python
+#
+#   mtr  --  a network diagnostic tool
+#   Copyright (C) 2016  Matt Kimball
+#
+#   This program is free software; you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License version 2 as
+#   published by the Free Software Foundation.
+#
+#   This program is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program; if not, write to the Free Software
+#   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+
+'''Test mtr-packet's command parsing.'''
+
+
+import re
+import time
+import unittest
+
+import mtrpacket
+
+
+class TestCommandParse(mtrpacket.MtrPacketTest):
+    '''Test cases with malformed commands and version checks'''
+
+    def test_unknown_command(self):
+        'Test sending a command unknown to mtr-packet'
+
+        self.write_command('13 argle-bargle')
+        self.assertEqual(self.read_reply(), '13 unknown-command')
+
+    def test_malformed_command(self):
+        'Test sending a malformed command request to mtr-packet'
+
+        self.write_command('malformed')
+        self.assertEqual(self.read_reply(), '0 command-parse-error')
+
+    def test_exit_on_stdin_closed(self):
+        '''Test that the packet process terminates after stdin is closed
+
+        Test that, when outstanding requests are complete, the process
+        terminates following stdin being closed.'''
+
+        self.write_command('15 send-probe ip-4 8.8.254.254 timeout 1')
+        self.packet_process.stdin.close()
+        time.sleep(2)
+        self.read_reply()
+        exit_code = self.packet_process.poll()
+        self.assertIsNotNone(exit_code)
+
+    def test_invalid_argument(self):
+        'Test sending invalid arguments with probe requests'
+
+        invalid_argument_regex = r'^[0-9]+ invalid-argument$'
+
+        bad_commands = [
+            '22 send-probe',
+            '23 send-probe ip-4 str-value',
+            '24 send-probe ip-4 8.8.8.8 timeout str-value',
+            '25 send-probe ip-4 8.8.8.8 ttl str-value',
+        ]
+
+        for cmd in bad_commands:
+            self.write_command(cmd)
+            reply = self.read_reply()
+            match = re.match(invalid_argument_regex, reply)
+            self.assertIsNotNone(match)
+
+    def test_versioning(self):
+        'Test version checks and feature support checks'
+
+        feature_tests = [
+            ('30 check-support feature version',
+             r'^30 feature-support support [0-9]+\.[0-9a-z\-\.]+$'),
+            ('31 check-support feature ip-4',
+             r'^31 feature-support support ok$'),
+            ('32 check-support feature send-probe',
+             r'^32 feature-support support ok$'),
+            ('33 check-support feature bogus-feature',
+             r'^33 feature-support support no$')
+        ]
+
+        for (request, regex) in feature_tests:
+            self.write_command(request)
+            reply = self.read_reply()
+            match = re.match(regex, reply)
+            self.assertIsNotNone(match)
+
+    def test_command_overflow(self):
+        'Test overflowing the incoming command buffer'
+
+        big_buffer = 'x' * (64 * 1024)
+        self.write_command(big_buffer)
+
+        reply = self.read_reply()
+        self.assertEqual(reply, '0 command-buffer-overflow')
+
+
+if __name__ == '__main__':
+    mtrpacket.check_running_as_root()
+    unittest.main()
similarity index 84%
rename from packet/lint.sh
rename to test/lint.sh
index 0594a8dba41d586e8f7432ea0f3f77d5bc7ec237..ae9aa2a6c80132c1797d66ce25261d10b8d6c7ee 100755 (executable)
@@ -2,7 +2,7 @@
 
 #  Check the Python test source for good style
 
-PYTHON_SOURCE=testpacket.py
+PYTHON_SOURCE=*.py
 
 pep8 $PYTHON_SOURCE
 pylint --reports=n $PYTHON_SOURCE 2>/dev/null
diff --git a/test/mtrpacket.py b/test/mtrpacket.py
new file mode 100644 (file)
index 0000000..d9bbf6c
--- /dev/null
@@ -0,0 +1,187 @@
+#
+#   mtr  --  a network diagnostic tool
+#   Copyright (C) 2016  Matt Kimball
+#
+#   This program is free software; you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License version 2 as
+#   published by the Free Software Foundation.
+#
+#   This program is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program; if not, write to the Free Software
+#   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+
+'''Infrastructure for running tests which invoke mtr-packet.'''
+
+import fcntl
+import os
+import select
+import subprocess
+import sys
+import time
+import unittest
+
+
+class ReadReplyTimeout(Exception):
+    'Exception raised by TestProbe.read_reply upon timeout'
+
+    pass
+
+
+class WriteCommandTimeout(Exception):
+    'Exception raised by TestProbe.write_command upon timeout'
+
+    pass
+
+
+def set_nonblocking(file_descriptor):  # type: (int) -> None
+    'Put a file descriptor into non-blocking mode'
+
+    flags = fcntl.fcntl(file_descriptor, fcntl.F_GETFL)
+
+    # pylint: disable=locally-disabled, no-member
+    fcntl.fcntl(file_descriptor, fcntl.F_SETFL, flags | os.O_NONBLOCK)
+
+
+class MtrPacketTest(unittest.TestCase):
+    '''Base class for tests invoking mtr-packet.
+
+    Start a new mtr-packet subprocess for each test, and kill it
+    at the conclusion of the test.
+
+    Provide methods for writing commands and reading replies.
+    '''
+
+    def __init__(self, *args):
+        self.reply_buffer = None  # type: unicode
+        self.packet_process = None  # type: subprocess.Popen
+        self.stdout_fd = None  # type: int
+
+        super(MtrPacketTest, self).__init__(*args)
+
+    def setUp(self):
+        'Set up a test case by spawning a mtr-packet process'
+
+        packet_path = os.environ.get('MTR_PACKET', './mtr-packet')
+
+        self.reply_buffer = ''
+        self.packet_process = subprocess.Popen(
+            [packet_path],
+            stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE)
+
+        #  Put the mtr-packet process's stdout in non-blocking mode
+        #  so that we can read from it without a timeout when
+        #  no reply is available.
+        self.stdout_fd = self.packet_process.stdout.fileno()
+        set_nonblocking(self.stdout_fd)
+
+        self.stdin_fd = self.packet_process.stdin.fileno()
+        set_nonblocking(self.stdin_fd)
+
+    def tearDown(self):
+        'After a test, kill the running mtr-packet instance'
+
+        self.packet_process.stdin.close()
+        self.packet_process.stdout.close()
+
+        try:
+            self.packet_process.kill()
+        except OSError:
+            return
+
+    def read_reply(self, timeout=10.0):  # type: (float) -> unicode
+        '''Read the next reply from mtr-packet.
+
+        Attempt to read the next command reply from mtr-packet.  If no reply
+        is available withing the timeout time, raise ReadReplyTimeout
+        instead.'''
+
+        start_time = time.time()
+
+        #  Read from mtr-packet until either the timeout time has elapsed
+        #  or we read a newline character, which indicates a finished
+        #  reply.
+        while True:
+            now = time.time()
+            elapsed = now - start_time
+
+            select_time = timeout - elapsed
+            if select_time < 0:
+                select_time = 0
+
+            select.select([self.stdout_fd], [], [], select_time)
+
+            reply_bytes = None
+
+            try:
+                reply_bytes = os.read(self.stdout_fd, 1024)
+            except OSError:
+                pass
+
+            if reply_bytes:
+                self.reply_buffer += reply_bytes.decode('utf-8')
+
+            #  If we have read a newline character, we can stop waiting
+            #  for more input.
+            newline_ix = self.reply_buffer.find('\n')
+            if newline_ix != -1:
+                break
+
+            if elapsed >= timeout:
+                raise ReadReplyTimeout()
+
+        reply = self.reply_buffer[:newline_ix]
+        self.reply_buffer = self.reply_buffer[newline_ix + 1:]
+        return reply
+
+    def write_command(self, cmd, timeout=10.0):
+        # type: (unicode, float) -> None
+
+        '''Send a command string to the mtr-packet instance, timing out
+        if we are unable to write for an extended period of time.  The
+        timeout is to avoid deadlocks with the child process where both
+        the parent and the child are writing to their end of the pipe
+        and expecting the other end to be reading.'''
+
+        command_str = cmd + '\n'
+        command_bytes = command_str.encode('utf-8')
+
+        start_time = time.time()
+
+        while True:
+            now = time.time()
+            elapsed = now - start_time
+
+            select_time = timeout - elapsed
+            if select_time < 0:
+                select_time = 0
+
+            select.select([], [self.stdin_fd], [], select_time)
+
+            bytes_written = 0
+            try:
+                bytes_written = os.write(self.stdin_fd, command_bytes)
+            except OSError:
+                pass
+
+            command_bytes = command_bytes[bytes_written:]
+            if not len(command_bytes):
+                break
+
+            if elapsed >= timeout:
+                raise WriteCommandTimeout()
+
+
+def check_running_as_root():
+    'Print a warning to stderr if we are not running as root.'
+
+    # pylint: disable=locally-disabled, no-member
+    if sys.platform != 'cygwin' and os.getuid() > 0:
+        sys.stderr.write(
+            "Warning: Many tests require running as root\n")
diff --git a/test/probe.py b/test/probe.py
new file mode 100755 (executable)
index 0000000..8919fa6
--- /dev/null
@@ -0,0 +1,280 @@
+#!/usr/bin/env python
+#
+#   mtr  --  a network diagnostic tool
+#   Copyright (C) 2016  Matt Kimball
+#
+#   This program is free software; you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License version 2 as
+#   published by the Free Software Foundation.
+#
+#   This program is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program; if not, write to the Free Software
+#   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+
+'''Test sending probes and receiving respones.'''
+
+import re
+import socket
+import sys
+import time
+import unittest
+
+import mtrpacket
+
+
+IPV6_TEST_HOST = 'google-public-dns-a.google.com'
+
+
+class TestProbeIPv4(mtrpacket.MtrPacketTest):
+    '''Test sending probes using IP version 4'''
+
+    def test_probe(self):
+        'Test sending regular ICMP probes to known addresses'
+
+        reply_regex = r'^14 reply ip-4 8.8.8.8 round-trip-time [0-9]+$'
+
+        #  Probe Google's well-known DNS server and expect a reply
+        self.write_command('14 send-probe ip-4 8.8.8.8')
+        reply = self.read_reply()
+        match = re.match(reply_regex, reply)
+        self.assertIsNotNone(match)
+
+    def test_timeout(self):
+        'Test timeouts when sending to a non-existant address'
+
+        no_reply_regex = r'^15 no-reply$'
+
+        #
+        #  Probe a non-existant address, and expect no reply
+        #
+        #  I'm not sure what the best way to find an address that doesn't
+        #  exist, but is still route-able.  If we use a reserved IP
+        #  address range, Windows will tell us it is non-routeable,
+        #  rather than timing out when transmitting to that address.
+        #
+        #  We're just using a currently unused address in Google's
+        #  range instead.  This is probably not the best solution.
+        #
+
+        # pylint: disable=locally-disabled, unused-variable
+        for i in range(16):
+            self.write_command('15 send-probe ip-4 8.8.254.254 timeout 1')
+            reply = self.read_reply()
+            match = re.match(no_reply_regex, reply)
+            self.assertIsNotNone(match)
+
+    def test_exhaust_probes(self):
+        'Test exhausting all available probes'
+
+        exhausted_regex = r'^[0-9]+ probes-exhausted$'
+
+        match = None
+        probe_count = 4 * 1024
+        token = 1024
+
+        # pylint: disable=locally-disabled, unused-variable
+        for i in range(probe_count):
+            command = str(token) + ' send-probe ip-4 8.8.254.254 timeout 60'
+            token += 1
+            self.write_command(command)
+
+            reply = None
+            try:
+                reply = self.read_reply(0)
+            except mtrpacket.ReadReplyTimeout:
+                pass
+
+            if reply:
+                match = re.match(exhausted_regex, reply)
+                if match:
+                    break
+
+        self.assertIsNotNone(match)
+
+    def test_timeout_values(self):
+        '''Test that timeout values wait the right amount of time
+
+        Give each probe a half-second grace period to probe a timeout
+        reply after the expected timeout time.'''
+
+        begin = time.time()
+        self.write_command('19 send-probe ip-4 8.8.254.254 timeout 0')
+        self.read_reply()
+        elapsed = time.time() - begin
+        self.assertLess(elapsed, 0.5)
+
+        begin = time.time()
+        self.write_command('20 send-probe ip-4 8.8.254.254 timeout 1')
+        self.read_reply()
+        elapsed = time.time() - begin
+        self.assertGreaterEqual(elapsed, 1.0)
+        self.assertLess(elapsed, 1.5)
+
+        begin = time.time()
+        self.write_command('21 send-probe ip-4 8.8.254.254 timeout 3')
+        self.read_reply()
+        elapsed = time.time() - begin
+        self.assertGreaterEqual(elapsed, 3.0)
+        self.assertLess(elapsed, 3.5)
+
+    def test_ttl_expired(self):
+        'Test sending a probe which will have its time-to-live expire'
+
+        ttl_expired_regex = \
+            r'^16 ttl-expired ip-4 [0-9\.]+ round-trip-time [0-9]+$'
+
+        #  Probe Goolge's DNS server, but give the probe only one hop
+        #  to live.
+        self.write_command('16 send-probe ip-4 8.8.8.8 ttl 1')
+        reply = self.read_reply()
+        match = re.match(ttl_expired_regex, reply)
+        self.assertIsNotNone(match)
+
+    def test_parallel_probes(self):
+        '''Test sending multiple probes in parallel
+
+        We will expect the probes to complete out-of-order by sending
+        a probe to a distant host immeidately followed by a probe to
+        the local host.'''
+
+        reply_regex = \
+            r'^[0-9]+ reply ip-4 [0-9\.]+ round-trip-time ([0-9]+)$'
+
+        success_count = 0
+        loop_count = 32
+
+        # pylint: disable=locally-disabled, unused-variable
+        for i in range(loop_count):
+            #  Probe the distant host before the local host.
+            self.write_command('17 send-probe ip-4 8.8.8.8 timeout 1')
+            self.write_command('18 send-probe ip-4 127.0.0.1 timeout 1')
+
+            reply = self.read_reply()
+            match = re.match(reply_regex, reply)
+            if not match:
+                continue
+            first_time = int(match.group(1))
+
+            reply = self.read_reply()
+            match = re.match(reply_regex, reply)
+            if not match:
+                continue
+            second_time = int(match.group(1))
+
+            #  Ensure we got a reply from the host with the lowest latency
+            #  first.
+            self.assertLess(first_time, second_time)
+
+            success_count += 1
+
+        #  We need 95% success to pass.  This allows a few probes to be
+        #  occasionally dropped by the network without failing the test.
+        required_success = int(loop_count * 0.95)
+        self.assertGreaterEqual(success_count, required_success)
+
+
+def resolve_ipv6_address(hostname):  # type: (str) -> str
+    'Resolve a hostname to an IP version 6 address'
+
+    for addrinfo in socket.getaddrinfo(hostname, 0):
+        # pylint: disable=locally-disabled, unused-variable
+        (family, socktype, proto, name, sockaddr) = addrinfo
+
+        if family == socket.AF_INET6:
+            sockaddr6 = sockaddr  # type: tuple
+
+            (address, port, flow, scope) = sockaddr6
+            return address
+
+    raise LookupError(hostname)
+
+
+def check_for_local_ipv6():
+    '''Check for IPv6 support on the test host, to see if we should skip
+    the IPv6 tests'''
+
+    addrinfo = socket.getaddrinfo(IPV6_TEST_HOST, 1, socket.AF_INET6)
+    if len(addrinfo):
+        addr = addrinfo[0][4]
+
+    #  Create a UDP socket and check to see it can be connected to
+    #  IPV6_TEST_HOST.  (Connecting UDP requires no packets sent, just
+    #  a route present.)
+    sock = socket.socket(
+        socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
+
+    connect_success = False
+    try:
+        sock.connect(addr)
+        connect_success = True
+    except socket.error:
+        pass
+
+    sock.close()
+
+    if not connect_success:
+        sys.stderr.write(
+            'This host has no IPv6.  Skipping IPv6 tests.\n')
+
+    return connect_success
+
+
+class TestProbeIPv6(mtrpacket.MtrPacketTest):
+    '''Test sending probes using IP version 6'''
+
+    have_ipv6 = check_for_local_ipv6()
+
+    def __init__(self, *args):
+        google_addr = resolve_ipv6_address(IPV6_TEST_HOST)
+
+        self.google_addr = google_addr  # type: str
+
+        super(TestProbeIPv6, self).__init__(*args)
+
+    @unittest.skipIf(not have_ipv6, 'No IPv6')
+    def test_probe(self):
+        "Test a probe to Google's public DNS server"
+
+        reply_regex = r'^51 reply ip-6 [0-9a-f:]+ round-trip-time [0-9]+$'
+        loopback_reply_regex = r'^52 reply ip-6 ::1 round-trip-time [0-9]+$'
+
+        #  Probe Google's well-known DNS server and expect a reply
+        self.write_command('51 send-probe ip-6 ' + self.google_addr)
+        reply = self.read_reply()
+        match = re.match(reply_regex, reply)
+        self.assertIsNotNone(match, reply)
+
+        #  Probe the loopback, and check the address we get a reply from is
+        #  also the loopback.  While implementing IPv6, I had a bug where
+        #  the low bits of the received address got zeroed.  This checks for
+        #  that bug.
+        self.write_command('52 send-probe ip-6 ::1')
+        reply = self.read_reply()
+        match = re.match(loopback_reply_regex, reply)
+        self.assertIsNotNone(match, reply)
+
+    @unittest.skipIf(not have_ipv6, 'No IPv6')
+    def test_ttl_expired(self):
+        'Test sending a probe which will have its time-to-live expire'
+
+        ttl_expired_regex = \
+            r'^53 ttl-expired ip-6 [0-9a-f:]+ round-trip-time [0-9]+$'
+
+        #  Probe Goolge's DNS server, but give the probe only one hop
+        #  to live.
+        cmd = '53 send-probe ip-6 ' + self.google_addr + ' ttl 1'
+        self.write_command(cmd)
+        reply = self.read_reply()
+        match = re.match(ttl_expired_regex, reply)
+        self.assertIsNotNone(match, reply)
+
+
+if __name__ == '__main__':
+    mtrpacket.check_running_as_root()
+    unittest.main()