From: Matt Kimball Date: Fri, 9 Dec 2016 19:14:06 +0000 (-0800) Subject: mtr-packet: IPv6 support X-Git-Tag: v0.88~15^2~8 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6df4e45df4ae1504604d5eef1b0858e1cb6e42de;p=thirdparty%2Fmtr.git mtr-packet: IPv6 support 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. --- diff --git a/.gitignore b/.gitignore index 83b2660..3cfd97e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # .gitignore *.o +*.pyc Makefile Makefile.in diff --git a/Makefile.am b/Makefile.am index 249a474..97ce4a1 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 diff --git a/mtr-packet.8.in b/mtr-packet.8.in index 9908015..7ea5c05 100644 --- a/mtr-packet.8.in +++ b/mtr-packet.8.in @@ -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 c6288e7..7852921 100644 --- 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 a6ef141..420ef54 100644 --- a/net.h +++ b/net.h @@ -23,7 +23,6 @@ #include #ifdef ENABLE_IPV6 #include -#include #endif #include diff --git a/packet/command.c b/packet/command.c index 5175d25..df3ca96 100644 --- a/packet/command.c +++ b/packet/command.c @@ -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 */ diff --git a/packet/command_cygwin.c b/packet/command_cygwin.c index 4c0857e..3ad7cec 100644 --- a/packet/command_cygwin.c +++ b/packet/command_cygwin.c @@ -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; } diff --git a/packet/command_cygwin.h b/packet/command_cygwin.h index d1a25fb..9a63eba 100644 --- a/packet/command_cygwin.h +++ b/packet/command_cygwin.h @@ -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 index 0000000..4eed0d7 --- /dev/null +++ b/packet/construct_unix.c @@ -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 +#include +#include + +#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 index 0000000..8ed6486 --- /dev/null +++ b/packet/construct_unix.h @@ -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 index 0000000..80fcfff --- /dev/null +++ b/packet/deconstruct_unix.c @@ -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 +#include + +#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 index 0000000..c591318 --- /dev/null +++ b/packet/deconstruct_unix.h @@ -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 diff --git a/packet/packet.c b/packet/packet.c index 3856ce0..6590ba0 100644 --- a/packet/packet.c +++ b/packet/packet.c @@ -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)); diff --git a/packet/probe.c b/packet/probe.c index 8302894..4c98bcc 100644 --- a/packet/probe.c +++ b/packet/probe.c @@ -31,27 +31,48 @@ #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; +} diff --git a/packet/probe.h b/packet/probe.h index 74dabfc..c99c852 100644 --- a/packet/probe.h +++ b/packet/probe.h @@ -36,11 +36,14 @@ /* 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 diff --git a/packet/probe_cygwin.c b/packet/probe_cygwin.c index 355ca5c..b00efd5 100644 --- a/packet/probe_cygwin.c +++ b/packet/probe_cygwin.c @@ -23,15 +23,27 @@ #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) { /* diff --git a/packet/probe_cygwin.h b/packet/probe_cygwin.h index b8ceb08..a4d5f28 100644 --- a/packet/probe_cygwin.h +++ b/packet/probe_cygwin.h @@ -24,19 +24,47 @@ #include #include +/* + 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 diff --git a/packet/probe_unix.c b/packet/probe_unix.c index 3a6bdee..35b3d9d 100644 --- a/packet/probe_unix.c +++ b/packet/probe_unix.c @@ -27,97 +27,57 @@ #include #include -#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, ¶m->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(¶m, 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(¶m, &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, ¶m); - 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, ¶m); + if (packet_size < 0) { + errno = -packet_size; + perror("Unable to send to localhost"); + exit(1); + } + bytes_sent = send_packet( + net_state, ¶m, 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, ¶m); - 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, ¶m); + if (packet_size < 0) { + errno = -packet_size; + perror("Unable to send to localhost"); + exit(1); + } + bytes_sent = send_packet( + net_state, ¶m, 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); } /* diff --git a/packet/probe_unix.h b/packet/probe_unix.h index a9a0dfb..d0080a4 100644 --- a/packet/probe_unix.h +++ b/packet/probe_unix.h @@ -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 diff --git a/packet/protocols.h b/packet/protocols.h index f30c241..13046c8 100644 --- a/packet/protocols.h +++ b/packet/protocols.h @@ -19,12 +19,17 @@ #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 index b145bb4..0000000 --- a/packet/testpacket.py +++ /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() diff --git a/packet/wait.h b/packet/wait.h index 3008b6f..0d24b4a 100644 --- a/packet/wait.h +++ b/packet/wait.h @@ -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 diff --git a/packet/wait_cygwin.c b/packet/wait_cygwin.c index e459c7e..517f71f 100644 --- a/packet/wait_cygwin.c +++ b/packet/wait_cygwin.c @@ -22,6 +22,8 @@ #include #include +#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 @@ -29,11 +31,20 @@ 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); diff --git a/packet/wait_unix.c b/packet/wait_unix.c index eba6fd3..348831d 100644 --- a/packet/wait_unix.c +++ b/packet/wait_unix.c @@ -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 index 0000000..8c90ec5 --- /dev/null +++ b/test/cmdparse.py @@ -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() diff --git a/packet/lint.sh b/test/lint.sh similarity index 84% rename from packet/lint.sh rename to test/lint.sh index 0594a8d..ae9aa2a 100755 --- a/packet/lint.sh +++ b/test/lint.sh @@ -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 index 0000000..d9bbf6c --- /dev/null +++ b/test/mtrpacket.py @@ -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 index 0000000..8919fa6 --- /dev/null +++ b/test/probe.py @@ -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()