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.
# .gitignore
*.o
+*.pyc
Makefile
Makefile.in
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)
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 \
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
.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
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
.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
.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
.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
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
.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
.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
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
{
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");
}
+/*
+ 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
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 */
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;
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;
#include <sys/socket.h>
#ifdef ENABLE_IPV6
#include <netinet/ip6.h>
-#include <netinet/icmp6.h>
#endif
#include <stdint.h>
return "ok";
}
+ if (!strcmp(feature, "ip-6")) {
+ return "ok";
+ }
+
if (!strcmp(feature, "send-probe")) {
return "ok";
}
/* 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 */
}
/* Start a new overlapped I/O read from the command stream */
-static
void start_read_command(
struct command_buffer_t *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;
}
char overlapped_buffer[COMMAND_BUFFER_SIZE];
};
+struct command_buffer_t;
+
+void start_read_command(
+ struct command_buffer_t *buffer);
+
#endif
--- /dev/null
+/*
+ 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;
+}
--- /dev/null
+/*
+ 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
--- /dev/null
+/*
+ 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);
+}
--- /dev/null
+/*
+ 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
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));
#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;
}
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";
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;
+}
/* 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;
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);
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,
int icmp_id,
int icmp_sequence);
+int find_source_addr(
+ struct sockaddr_storage *srcaddr,
+ const struct sockaddr_storage *destaddr);
+
#endif
#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);
}
}
{
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();
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);
}
}
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);
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) {
/*
#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
#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, ¶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);
}
/*
{
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");
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;
}
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);
}
*/
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);
}
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;
}
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;
}
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);
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);
/*
}
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);
}
/*
/* 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
#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 */
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
+++ /dev/null
-#!/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()
#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
#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);
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;
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) {
--- /dev/null
+#!/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()
# 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
--- /dev/null
+#
+# 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")
--- /dev/null
+#!/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()