From: Matt Kimball Date: Wed, 14 Dec 2016 17:18:42 +0000 (-0800) Subject: mtr-packet: UDP probe support X-Git-Tag: v0.88~15^2~7 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=88d1a95087185339e439918a24923d5e0e816451;p=thirdparty%2Fmtr.git mtr-packet: UDP probe support Added support send using UDP as the protocol for sending probes, rather than ICMP. Both IPv4 and IPv6 UDP probes are supported. We are using the source port in the UDP packet for identifying the particular probe transmitted. This is a bit less reliable than ICMP, where we are also able to store our PID for verifying the probe has been transmitted by this instance of mtr-packet, but space is limited, and it is what the pre-existing mtr implementation does. We report no-route and network-down errors in response to errors from sendto(), in addition to reporting them from errors in the connect() used to determine the source address of an outgoing probe. The mtr-packet tests now properly parse the replies from mtr-packet, as opposed to simply matching regular expressions against the output. This is better because it give us future compatibility with additional reply arguments from mtr-packet. A better introduction to mtr-packet is now included in the mtr-packet man page. The dual code paths for sending IPv6 probes between Linux and non-Linux Unix-likes has been eliminated. The Linux path gave us direct control over the IP header, but wasn't necessary and would make maintainence more difficult, so now Linux uses the more indirect setsockopt() method of setting IPv6 header fields. --- diff --git a/mtr-packet.8.in b/mtr-packet.8.in index 7ea5c05..463e962 100644 --- a/mtr-packet.8.in +++ b/mtr-packet.8.in @@ -4,8 +4,19 @@ mtr-packet - send and receive network probes .SH DESCRIPTION .B mtr-packet +is a tool for sending network probes to measure network connectivity and +performance. Many network probes can be sent simultaneously by a single +process instance of +.B mtr-packet +and additional probes can be generated by an instance of +.B mtr-packet +which already has network probes in flight. It is intended to be used +by programs which invoke it with Unix pipes attached to its standard input +and output streams. +.LP +.B mtr-packet reads command requests from -.I stdin, +.IR stdin , each separated by a newline character, and responds with command replies to .IR stdout , also each separated by a newline character. The syntactic structure of @@ -80,6 +91,27 @@ The Internet Protocol version 4 address to probe. The Internet Protocol version 6 address to probe. .HP 7 .IP +.B protocol +.I PROTOCOL +.HP 14 +.IP +The protocol to use for the network probe. +.B icmp +and +.B udp +may be used. The default protocol is +.BR icmp. +.HP 7 +.IP +.B port +.I PORT-NUMBER +.HP 14 +.IP +The destination port to use for +.B udp +probes. +.HP 7 +.IP .B timeout .I TIMEOUT-SECONDS .HP 14 @@ -321,12 +353,12 @@ For the latest version, see the mtr web page at .UR http://\:www.\:bitwizard.\:nl/\:mtr/ .UE .PP -The mtr mailinglist was little used and is no longer active. -.PP For patches, bug reports, or feature requests, please open an issue on GitHub at: .UR https://\:github\:.com/\:traviscross/\:mtr .UE . .SH "SEE ALSO" .BR mtr (8), +.BR icmp (7), +.BR udp (7), TCP/IP Illustrated (Stevens, ISBN 0201633469). diff --git a/mtr.8.in b/mtr.8.in index 58cd081..eb9fbe8 100644 --- a/mtr.8.in +++ b/mtr.8.in @@ -474,8 +474,6 @@ For the latest version, see the mtr web page at .UR http://\:www.\:bitwizard.\:nl/\:mtr/ .UE .PP -The mtr mailinglist was little used and is no longer active. -.PP For patches, bug reports, or feature requests, please open an issue on GitHub at: .UR https://\:github\:.com/\:traviscross/\:mtr diff --git a/net.c b/net.c index 7852921..f413f89 100644 --- a/net.c +++ b/net.c @@ -62,6 +62,7 @@ #define MinSequence 33000 #define MaxSequence 65536 +#define COMMAND_BUFFER_SIZE 4096 #define PACKET_REPLY_BUFFER_SIZE 4096 static int packetsize; /* packet size used by ping */ @@ -197,7 +198,11 @@ 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[INET6_ADDRSTRLEN]; + char command[COMMAND_BUFFER_SIZE]; + char argument[COMMAND_BUFFER_SIZE]; + int remaining_size; const char *ip_type; + const char *protocol = NULL; /* Conver the remote IP address to a string */ if (inet_ntop( @@ -213,12 +218,32 @@ static void net_send_query(struct mtr_ctl *ctl, int index) ip_type = "ip-4"; } - /* Send a probe using the mtr-packet subprocess */ - if (dprintf( - packet_command_pipe.write_fd, - "%d send-probe %s %s ttl %d\n", - seq, ip_type, ip_string, time_to_live) < 0) { + if (ctl->mtrtype == IPPROTO_ICMP) { + protocol = "icmp"; + } else if (ctl->mtrtype == IPPROTO_UDP) { + protocol = "udp"; + } else { + display_close(ctl); + error(EXIT_FAILURE, 0, "protocol unsupported by mtr-packet interface"); + } + + snprintf( + command, COMMAND_BUFFER_SIZE, + "%d send-probe %s %s protocol %s ttl %d", + seq, ip_type, ip_string, protocol, time_to_live); + + if (ctl->remoteport) { + remaining_size = COMMAND_BUFFER_SIZE - strlen(command) - 1; + + snprintf(argument, COMMAND_BUFFER_SIZE, " port %d", ctl->remoteport); + strncat(command, argument, remaining_size); + } + + remaining_size = COMMAND_BUFFER_SIZE - strlen(command) - 1; + strncat(command, "\n", remaining_size); + /* Send a probe using the mtr-packet subprocess */ + if (write(packet_command_pipe.write_fd, command, strlen(command)) == -1) { display_close(ctl); error(EXIT_FAILURE, errno, "mtr-packet command pipe write failure"); } @@ -817,12 +842,15 @@ static void set_fd_nonblock(int fd) } } - -/* Ensure we can communicate with the mtr-packet subprocess */ -static int net_command_pipe_check(struct mtr_ctl *ctl) +/* + Send a command synchronously to mtr-packet, blocking until a result + is available. This is intended to be used at start-up to check the + capabilities of mtr-packet, but probes should be sent asynchronously + to avoid blocking other probes and the user interface. +*/ +static int net_synchronous_command( + struct mtr_ctl *ctl, const char *cmd, struct command_t *result) { - const char *check_command = "1 check-support feature send-probe\n"; - struct command_t command; char reply[PACKET_REPLY_BUFFER_SIZE]; int command_length; int write_length; @@ -830,9 +858,9 @@ static int net_command_pipe_check(struct mtr_ctl *ctl) int parse_result; /* Query send-probe support */ - command_length = strlen(check_command); + command_length = strlen(cmd); write_length = write( - packet_command_pipe.write_fd, check_command, command_length); + packet_command_pipe.write_fd, cmd, command_length); if (write_length == -1) { return -1; @@ -853,17 +881,35 @@ static int net_command_pipe_check(struct mtr_ctl *ctl) /* Parse the query reply */ reply[read_length] = 0; - parse_result = parse_command(&command, reply); + parse_result = parse_command(result, reply); if (parse_result) { errno = parse_result; return -1; } - /* Check that send-probe is supported */ - if (!strcmp(command.command_name, "feature-support") - && command.argument_count >= 1 - && !strcmp(command.argument_name[0], "support") - && !strcmp(command.argument_value[0], "ok")) { + return 0; +} + + +/* Check support for a particular feature with the mtr-packet we invoked */ +static int net_check_feature(struct mtr_ctl *ctl, const char *feature) +{ + char check_command[COMMAND_BUFFER_SIZE]; + struct command_t reply; + + snprintf( + check_command, COMMAND_BUFFER_SIZE, + "1 check-support feature %s\n", feature); + + if (net_synchronous_command(ctl, check_command, &reply) == -1) { + return -1; + } + + /* Check that the feature is supported */ + if (!strcmp(reply.command_name, "feature-support") + && reply.argument_count >= 1 + && !strcmp(reply.argument_name[0], "support") + && !strcmp(reply.argument_value[0], "ok")) { /* Looks good */ return 0; @@ -874,6 +920,24 @@ static int net_command_pipe_check(struct mtr_ctl *ctl) } +/* + Check the protocol selected against the mtr-packet we are using. + Returns zero if everything is fine, or -1 with errno for either + a failure during the check, or for an unsupported feature. +*/ +static int net_packet_feature_check(struct mtr_ctl *ctl) +{ + if (ctl->mtrtype == IPPROTO_ICMP) { + return net_check_feature(ctl, "icmp"); + } else if (ctl->mtrtype == IPPROTO_UDP) { + return net_check_feature(ctl, "udp"); + } else { + errno = EINVAL; + return -1; + } +} + + /* Create the command pipe to a new mtr-packet subprocess */ static int net_command_pipe_open(struct mtr_ctl *ctl) { @@ -954,10 +1018,14 @@ static int net_command_pipe_open(struct mtr_ctl *ctl) Check that we can communicate with the client. If we failed to execute the mtr-packet binary, we will discover that here. */ - if (net_command_pipe_check(ctl)) { + if (net_check_feature(ctl, "send-probe")) { error(EXIT_FAILURE, errno, "Failure to start mtr-packet"); } + if (net_packet_feature_check(ctl)) { + error(EXIT_FAILURE, errno, "Packet type unsupported"); + } + /* We will need non-blocking reads from the child */ set_fd_nonblock(packet_command_pipe.read_fd); } diff --git a/packet/command.c b/packet/command.c index df3ca96..5118f93 100644 --- a/packet/command.c +++ b/packet/command.c @@ -54,10 +54,24 @@ const char *find_parameter( return NULL; } +/* Returns a feature support string for a particular probe protocol */ +static +const char *check_protocol_support( + struct net_state_t *net_state, + int protocol) +{ + if (is_protocol_supported(net_state, protocol)) { + return "ok"; + } else { + return "no"; + } +} + /* Given a feature name, return a string for the check-support reply */ static const char *check_support( - const char *feature) + const char *feature, + struct net_state_t *net_state) { if (!strcmp(feature, "version")) { return PACKAGE_VERSION; @@ -75,13 +89,22 @@ const char *check_support( return "ok"; } + if (!strcmp(feature, "icmp")) { + return check_protocol_support(net_state, IPPROTO_ICMP); + } + + if (!strcmp(feature, "udp")) { + return check_protocol_support(net_state, IPPROTO_UDP); + } + return "no"; } /* Handle a check-support request by checking for a particular feature */ static void check_support_command( - const struct command_t *command) + const struct command_t *command, + struct net_state_t *net_state) { const char *feature; const char *support; @@ -92,7 +115,7 @@ void check_support_command( return; } - support = check_support(feature); + support = check_support(feature, net_state); printf("%d feature-support support %s\n", command->token, support); } @@ -120,6 +143,25 @@ bool decode_probe_argument( param->address = value; } + /* Protocol for the probe */ + if (!strcmp(name, "protocol")) { + if (!strcmp(value, "icmp")) { + param->protocol = IPPROTO_ICMP; + } else if (!strcmp(value, "udp")) { + param->protocol = IPPROTO_UDP; + } else { + return false; + } + } + + /* Destination port for the probe */ + if (!strcmp(name, "port")) { + param->dest_port = strtol(value, &endstr, 10); + if (*endstr != 0) { + return false; + } + } + /* Time-to-live values */ if (!strcmp(name, "ttl")) { param->ttl = strtol(value, &endstr, 10); @@ -153,6 +195,8 @@ void send_probe_command( /* We will prepare a probe_param_t for send_probe. */ memset(¶m, 0, sizeof(struct probe_param_t)); param.command_token = command->token; + param.protocol = IPPROTO_ICMP; + param.dest_port = 7; /* Use the 'echo' port as the default destination */ param.ttl = 255; param.timeout = 10; @@ -180,7 +224,7 @@ void dispatch_command( struct net_state_t *net_state) { if (!strcmp(command->command_name, "check-support")) { - check_support_command(command); + check_support_command(command, net_state); } else if (!strcmp(command->command_name, "send-probe")) { send_probe_command(command, net_state); } else { diff --git a/packet/construct_unix.c b/packet/construct_unix.c index 4eed0d7..8b36cde 100644 --- a/packet/construct_unix.c +++ b/packet/construct_unix.c @@ -20,6 +20,7 @@ #include #include +#include #include #include "protocols.h" @@ -31,31 +32,21 @@ struct checksum_source_t 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. -*/ +/* Compute the IP checksum (or ICMP checksum) of a packet. */ static uint16_t compute_checksum( - struct checksum_source_t *source, - int source_count) + const void *packet, + int size) { - int i, j; - const uint8_t *bytes; - size_t size; + const uint8_t *packet_bytes = (uint8_t *)packet; uint32_t sum = 0; + int i; - 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]; - } + for (i = 0; i < size; i++) { + if ((i & 1) == 0) { + sum += packet_bytes[i] << 8; + } else { + sum += packet_bytes[i]; } } @@ -74,50 +65,6 @@ uint16_t compute_checksum( 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( @@ -150,41 +97,11 @@ void construct_ip4_header( ip->version = 0x45; ip->len = length_byte_swap(net_state, packet_size); ip->ttl = param->ttl; - ip->protocol = IPPROTO_ICMP; + ip->protocol = param->protocol; 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( @@ -202,36 +119,99 @@ void construct_icmp4_header( icmp->type = ICMP_ECHO; icmp->id = htons(getpid()); icmp->sequence = htons(param->command_token); - icmp->checksum = htons(simple_checksum(icmp, icmp_size)); + icmp->checksum = htons(compute_checksum(icmp, icmp_size)); } /* Construct an ICMP header for IPv6 */ static -void construct_icmp6_header( +int construct_icmp6_packet( 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 = (struct ICMPHeader *)packet_buffer; 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)); + if (setsockopt( + net_state->platform.icmp6_send_socket, IPPROTO_IPV6, + IPV6_UNICAST_HOPS, ¶m->ttl, sizeof(int))) { + return -errno; + } + + return 0; +} + +/* + Construct a header for UDP probes. We'll use the source port + as the command token, so that we can identify the probe when its + header is returned embedded in an ICMP reply. +*/ +static +void construct_udp4_header( + const struct net_state_t *net_state, + char *packet_buffer, + int packet_size, + const struct probe_param_t *param) +{ + struct UDPHeader *udp; + int udp_size; + + udp = (struct UDPHeader *)&packet_buffer[sizeof(struct IPHeader)]; + udp_size = packet_size - sizeof(struct IPHeader); + + udp->srcport = htons(param->command_token); + udp->dstport = htons(param->dest_port); + udp->length = htons(udp_size); + udp->checksum = 0; +} + +/* Construct a header for UDPv6 probes */ +static +int construct_udp6_packet( + const struct net_state_t *net_state, + char *packet_buffer, + int packet_size, + const struct probe_param_t *param) +{ + int udp_socket = net_state->platform.udp6_send_socket; + struct UDPHeader *udp; + int udp_size; + + udp = (struct UDPHeader *)packet_buffer; + udp_size = packet_size; + + udp->srcport = htons(param->command_token); + udp->dstport = htons(param->dest_port); + udp->length = htons(udp_size); + udp->checksum = 0; + + /* Set the TTL via setsockopt */ + if (setsockopt( + udp_socket, IPPROTO_IPV6, + IPV6_UNICAST_HOPS, ¶m->ttl, sizeof(int))) { + + return -errno; } + + /* + Instruct the kernel to put the pseudoheader checksum into the + UDP header. + */ + int chksum_offset = (char *)&udp->checksum - (char *)udp; + if (setsockopt( + udp_socket, IPPROTO_IPV6, + IPV6_CHECKSUM, &chksum_offset, sizeof(int))) { + + return -errno; + } + + return 0; } /* @@ -246,18 +226,23 @@ int compute_packet_size( const struct net_state_t *net_state, const struct probe_param_t *param) { - int packet_size = 0; + int packet_size; if (param->ip_version == 6) { - if (net_state->platform.ipv6_header_constructed) { - packet_size = sizeof(struct IP6Header); - } + packet_size = 0; } else if (param->ip_version == 4) { packet_size = sizeof(struct IPHeader); } else { return -EINVAL; } - packet_size += sizeof(struct ICMPHeader); + + if (param->protocol == IPPROTO_ICMP) { + packet_size += sizeof(struct ICMPHeader); + } else if (param->protocol == IPPROTO_UDP) { + packet_size += sizeof(struct UDPHeader); + } else { + return -EINVAL; + } return packet_size; } @@ -290,18 +275,35 @@ int construct_packet( memset(packet_buffer, 0, packet_size); + err = 0; 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); + if (param->protocol == IPPROTO_ICMP) { + err = construct_icmp6_packet( + net_state, packet_buffer, packet_size, param); + } else if (param->protocol == IPPROTO_UDP) { + err = construct_udp6_packet( + net_state, packet_buffer, packet_size, param); + } else { + return -EINVAL; + } } 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); + + if (param->protocol == IPPROTO_ICMP) { + construct_icmp4_header( + net_state, packet_buffer, packet_size, param); + } else if (param->protocol == IPPROTO_UDP) { + construct_udp4_header( + net_state, packet_buffer, packet_size, param); + } else { + return -EINVAL; + } + } + + if (err) { + return err; } return packet_size; diff --git a/packet/deconstruct_unix.c b/packet/deconstruct_unix.c index 80fcfff..c3a087d 100644 --- a/packet/deconstruct_unix.c +++ b/packet/deconstruct_unix.c @@ -53,12 +53,13 @@ void find_and_receive_probe( const struct sockaddr_storage *remote_addr, struct timeval timestamp, int icmp_type, + int protocol, int icmp_id, int icmp_sequence) { struct probe_t *probe; - probe = find_probe(net_state, icmp_id, icmp_sequence); + probe = find_probe(net_state, protocol, icmp_id, icmp_sequence); if (probe == NULL) { return; } @@ -66,51 +67,148 @@ void find_and_receive_probe( receive_probe(probe, icmp_type, remote_addr, timestamp); } +/* + We've received an ICMP message with an embedded IP packet. + We will try to determine which of our outgoing probes + corresponds to the embedded IP packet and record the response. +*/ +static +void handle_inner_ip4_packet( + struct net_state_t *net_state, + const struct sockaddr_storage *remote_addr, + int icmp_result, + const struct IPHeader *ip, + int packet_length, + struct timeval timestamp) +{ + const int ip_icmp_size = + sizeof(struct IPHeader) + sizeof(struct ICMPHeader); + const int ip_udp_size = + sizeof(struct IPHeader) + sizeof(struct UDPHeader); + const struct ICMPHeader *icmp; + const struct UDPHeader *udp; + + if (ip->protocol == IPPROTO_ICMP) { + if (packet_length < ip_icmp_size) { + return; + } + + icmp = (struct ICMPHeader *)(ip + 1); + + find_and_receive_probe( + net_state, remote_addr, timestamp, icmp_result, + IPPROTO_ICMP, icmp->id, icmp->sequence); + } else if (ip->protocol == IPPROTO_UDP) { + if (packet_length < ip_udp_size) { + return; + } + + udp = (struct UDPHeader *)(ip + 1); + + find_and_receive_probe( + net_state, remote_addr, timestamp, icmp_result, + IPPROTO_UDP, 0, udp->srcport); + } +} + +/* + Examine the IPv6 header embedded in a returned ICMPv6 packet + in order to match it with a probe which we previously sent. +*/ +static +void handle_inner_ip6_packet( + struct net_state_t *net_state, + const struct sockaddr_storage *remote_addr, + int icmp_result, + const struct IP6Header *ip, + int packet_length, + struct timeval timestamp) +{ + const int ip_icmp_size = + sizeof(struct IP6Header) + sizeof(struct ICMPHeader); + const int ip_udp_size = + sizeof(struct IP6Header) + sizeof(struct UDPHeader); + const struct ICMPHeader *icmp; + const struct UDPHeader *udp; + + if (ip->protocol == IPPROTO_ICMPV6) { + if (packet_length < ip_icmp_size) { + return; + } + + icmp = (struct ICMPHeader *)(ip + 1); + + find_and_receive_probe( + net_state, remote_addr, timestamp, icmp_result, + IPPROTO_ICMP, icmp->id, icmp->sequence); + } else if (ip->protocol == IPPROTO_UDP) { + if (packet_length < ip_udp_size) { + return; + } + + udp = (struct UDPHeader *)(ip + 1); + + find_and_receive_probe( + net_state, remote_addr, timestamp, icmp_result, + IPPROTO_UDP, 0, udp->srcport); + } +} + /* Decode the ICMP header received and try to find a probe which it is in response to. */ static -void handle_received_icmpv4_packet( +void handle_received_icmp4_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 int icmp_ip_size = + sizeof(struct ICMPHeader) + sizeof(struct IPHeader); const struct IPHeader *inner_ip; - const struct ICMPHeader *inner_icmp; + int inner_size = packet_length - sizeof(struct ICMPHeader); /* 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); + ICMP_ECHOREPLY, IPPROTO_ICMP, icmp->id, icmp->sequence); } + if (packet_length < icmp_ip_size) { + return; + } + inner_ip = (struct IPHeader *)(icmp + 1); + /* 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); + handle_inner_ip4_packet( + net_state, remote_addr, + ICMP_TIME_EXCEEDED, inner_ip, inner_size, timestamp); + } - find_and_receive_probe( - net_state, remote_addr, timestamp, - ICMP_TIME_EXCEEDED, inner_icmp->id, inner_icmp->sequence); + if (icmp->type == ICMP_DEST_UNREACH) { + /* + We'll get a ICMP_PORT_UNREACH when a non-ICMP probe + reaches its final destination. (Assuming that port isn't + open on the destination host.) + */ + if (icmp->code == ICMP_PORT_UNREACH) { + handle_inner_ip4_packet( + net_state, remote_addr, + ICMP_ECHOREPLY, inner_ip, inner_size, timestamp); + } } } @@ -120,36 +218,41 @@ void handle_received_icmpv4_packet( constants differ. */ static -void handle_received_icmpv6_packet( +void handle_received_icmp6_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 int icmp_ip_size = + sizeof(struct ICMPHeader) + sizeof(struct IP6Header); const struct IP6Header *inner_ip; - const struct ICMPHeader *inner_icmp; + int inner_size = packet_length - sizeof(struct ICMPHeader); if (icmp->type == ICMP6_ECHOREPLY) { find_and_receive_probe( - net_state, remote_addr, timestamp, - ICMP_ECHOREPLY, icmp->id, icmp->sequence); + net_state, remote_addr, timestamp, ICMP_ECHOREPLY, + IPPROTO_ICMP, icmp->id, icmp->sequence); } - if (icmp->type == ICMP6_TIME_EXCEEDED) { - if (packet_length < icmp_ip_icmp_size) { - return; - } + if (packet_length < icmp_ip_size) { + return; + } + inner_ip = (struct IP6Header *)(icmp + 1); - inner_ip = (struct IP6Header *)(icmp + 1); - inner_icmp = (struct ICMPHeader *)(inner_ip + 1); + if (icmp->type == ICMP6_TIME_EXCEEDED) { + handle_inner_ip6_packet( + net_state, remote_addr, + ICMP_TIME_EXCEEDED, inner_ip, inner_size, timestamp); + } - find_and_receive_probe( - net_state, remote_addr, timestamp, - ICMP_TIME_EXCEEDED, inner_icmp->id, inner_icmp->sequence); + if (icmp->type == ICMP6_DEST_UNREACH) { + if (icmp->code == ICMP6_PORT_UNREACH) { + handle_inner_ip6_packet( + net_state, remote_addr, + ICMP_ECHOREPLY, inner_ip, inner_size, timestamp); + } } } @@ -158,7 +261,7 @@ void handle_received_icmpv6_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( +void handle_received_ip4_packet( struct net_state_t *net_state, const struct sockaddr_storage *remote_addr, const void *packet, @@ -184,7 +287,7 @@ void handle_received_ipv4_packet( icmp = (struct ICMPHeader *)(ip + 1); icmp_length = packet_length - sizeof(struct IPHeader); - handle_received_icmpv4_packet( + handle_received_icmp4_packet( net_state, remote_addr, icmp, icmp_length, timestamp); } @@ -193,7 +296,7 @@ void handle_received_ipv4_packet( in received packets, so we can assume the packet we got starts with the ICMP packet. */ -void handle_received_ipv6_packet( +void handle_received_ip6_packet( struct net_state_t *net_state, const struct sockaddr_storage *remote_addr, const void *packet, @@ -204,6 +307,6 @@ void handle_received_ipv6_packet( icmp = (struct ICMPHeader *)packet; - handle_received_icmpv6_packet( + handle_received_icmp6_packet( net_state, remote_addr, icmp, packet_length, timestamp); } diff --git a/packet/deconstruct_unix.h b/packet/deconstruct_unix.h index c591318..616d19c 100644 --- a/packet/deconstruct_unix.h +++ b/packet/deconstruct_unix.h @@ -28,14 +28,14 @@ typedef void (*received_packet_func_t)( int packet_length, struct timeval timestamp); -void handle_received_ipv4_packet( +void handle_received_ip4_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( +void handle_received_ip6_packet( struct net_state_t *net_state, const struct sockaddr_storage *remote_addr, const void *packet, diff --git a/packet/probe.c b/packet/probe.c index 4c98bcc..b4d4ad9 100644 --- a/packet/probe.c +++ b/packet/probe.c @@ -136,6 +136,7 @@ int count_in_flight_probes( */ struct probe_t *find_probe( struct net_state_t *net_state, + int protocol, int icmp_id, int icmp_sequence) { @@ -143,11 +144,17 @@ struct probe_t *find_probe( struct probe_t *probe; /* - If the ICMP id doesn't match our process ID, it wasn't a - probe generated by this process, so ignore it. + ICMP has room for an id to check against our process, but + UDP doesn't. */ - if (icmp_id != htons(getpid())) { - return NULL; + if (protocol == IPPROTO_ICMP) { + /* + If the ICMP id doesn't match our process ID, it wasn't a + probe generated by this process, so ignore it. + */ + if (icmp_id != htons(getpid())) { + return NULL; + } } for (i = 0; i < MAX_PROBES; i++) { @@ -229,6 +236,8 @@ int find_source_addr( struct sockaddr_in *destaddr4; struct sockaddr_in6 *destaddr6; struct sockaddr_storage dest_with_port; + struct sockaddr_in *srcaddr4; + struct sockaddr_in6 *srcaddr6; dest_with_port = *destaddr; @@ -267,5 +276,19 @@ int find_source_addr( close(sock); + /* + Zero the port, as we may later use this address to finding, and + we don't want to use the port from the socket we just created. + */ + if (destaddr->ss_family == AF_INET6) { + srcaddr6 = (struct sockaddr_in6 *)srcaddr; + + srcaddr6->sin6_port = 0; + } else { + srcaddr4 = (struct sockaddr_in *)srcaddr; + + srcaddr4->sin_port = 0; + } + return 0; } diff --git a/packet/probe.h b/packet/probe.h index c99c852..6b66150 100644 --- a/packet/probe.h +++ b/packet/probe.h @@ -45,6 +45,12 @@ struct probe_param_t /* The IP address to probe */ const char *address; + /* Protocol for the probe, using the IPPROTO_* defines */ + int protocol; + + /* The destination port for non-ICMP probes */ + int dest_port; + /* Time to live for the transmited probe */ int ttl; @@ -81,6 +87,10 @@ void init_net_state_privileged( void init_net_state( struct net_state_t *net_state); +bool is_protocol_supported( + struct net_state_t *net_state, + int protocol); + bool get_next_probe_timeout( const struct net_state_t *net_state, struct timeval *timeout); @@ -117,6 +127,7 @@ int count_in_flight_probes( struct probe_t *find_probe( struct net_state_t *net_state, + int protocol, int icmp_id, int icmp_sequence); diff --git a/packet/probe_cygwin.c b/packet/probe_cygwin.c index b00efd5..771d101 100644 --- a/packet/probe_cygwin.c +++ b/packet/probe_cygwin.c @@ -48,6 +48,18 @@ void init_net_state( } } +/* On Windows, we only support ICMP probes */ +bool is_protocol_supported( + struct net_state_t *net_state, + int protocol) +{ + if (protocol == IPPROTO_ICMP) { + return true; + } + + return false; +} + /* The overlapped I/O style completion routine to be called by Windows during an altertable wait when an ICMP probe has diff --git a/packet/probe_unix.c b/packet/probe_unix.c index 35b3d9d..e840a01 100644 --- a/packet/probe_unix.c +++ b/packet/probe_unix.c @@ -35,21 +35,6 @@ /* Use the "jumbo" frame size as the max packet size */ #define PACKET_BUFFER_SIZE 9000 -/* Set the IPv6 options affecting and outgoing IPv6 packet */ -static -void set_ipv6_socket_options( - int socket, - const struct probe_param_t *param) -{ - if (setsockopt( - socket, IPPROTO_IPV6, - IPV6_UNICAST_HOPS, ¶m->ttl, sizeof(int))) { - - perror("Failure to set IPV6_UNICAST_HOPS"); - exit(1); - } -} - /* A wrapper around sendto for mixed IPv4 and IPv6 sending */ static int send_packet( @@ -59,20 +44,26 @@ int send_packet( int packet_size, const struct sockaddr_storage *sockaddr) { - int send_socket; + int send_socket = 0; int sockaddr_length; if (sockaddr->ss_family == AF_INET6) { - send_socket = net_state->platform.ipv6_send_socket; sockaddr_length = sizeof(struct sockaddr_in6); - if (!net_state->platform.ipv6_header_constructed) { - set_ipv6_socket_options(send_socket, param); + if (param->protocol == IPPROTO_ICMP) { + send_socket = net_state->platform.icmp6_send_socket; + } else if (param->protocol == IPPROTO_UDP) { + send_socket = net_state->platform.udp6_send_socket; } - } else { - assert(sockaddr->ss_family == AF_INET); - send_socket = net_state->platform.ipv4_send_socket; + } else if (sockaddr->ss_family == AF_INET) { sockaddr_length = sizeof(struct sockaddr_in); + + send_socket = net_state->platform.ip4_send_socket; + } + + if (send_socket == 0) { + errno = EINVAL; + return -1; } return sendto( @@ -103,6 +94,7 @@ void check_length_order( memset(¶m, 0, sizeof(struct probe_param_t)); param.ip_version = 4; + param.protocol = IPPROTO_ICMP; param.ttl = 255; param.address = "127.0.0.1"; @@ -168,7 +160,7 @@ void set_socket_nonblocking( /* Open the raw sockets for sending/receiving IPv4 packets */ static -void open_ipv4_sockets( +void open_ip4_sockets( struct net_state_t *net_state) { int send_socket; @@ -202,42 +194,28 @@ void open_ipv4_sockets( exit(1); } - net_state->platform.ipv4_send_socket = send_socket; - net_state->platform.ipv4_recv_socket = recv_socket; + net_state->platform.ip4_send_socket = send_socket; + net_state->platform.ip4_recv_socket = recv_socket; } /* Open the raw sockets for sending/receiving IPv6 packets */ static -void open_ipv6_sockets( +void open_ip6_sockets( struct net_state_t *net_state) { - int send_socket; + int send_socket_icmp; + int send_socket_udp; 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_icmp = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6); + if (send_socket_icmp == -1) { + perror("Failure opening ICMPv6 send socket"); + exit(1); } - send_socket = socket(AF_INET6, SOCK_RAW, send_protocol); - if (send_socket == -1) { - perror("Failure opening IPv6 send socket"); + send_socket_udp = socket(AF_INET6, SOCK_RAW, IPPROTO_UDP); + if (send_socket_udp == -1) { + perror("Failure opening UDPv6 send socket"); exit(1); } @@ -249,8 +227,9 @@ void open_ipv6_sockets( set_socket_nonblocking(recv_socket); - net_state->platform.ipv6_send_socket = send_socket; - net_state->platform.ipv6_recv_socket = recv_socket; + net_state->platform.icmp6_send_socket = send_socket_icmp; + net_state->platform.udp6_send_socket = send_socket_udp; + net_state->platform.ip6_recv_socket = recv_socket; } /* @@ -263,8 +242,8 @@ void init_net_state_privileged( { memset(net_state, 0, sizeof(struct net_state_t)); - open_ipv4_sockets(net_state); - open_ipv6_sockets(net_state); + open_ip4_sockets(net_state); + open_ip6_sockets(net_state); } /* @@ -274,12 +253,28 @@ void init_net_state_privileged( 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); + set_socket_nonblocking(net_state->platform.ip4_recv_socket); + set_socket_nonblocking(net_state->platform.ip6_recv_socket); check_length_order(net_state); } +/* Returns true if we can transmit probes using the specified protocol */ +bool is_protocol_supported( + struct net_state_t *net_state, + int protocol) +{ + if (protocol == IPPROTO_ICMP) { + return true; + } + + if (protocol == IPPROTO_UDP) { + return true; + } + + return false; +} + /* Craft a custom ICMP packet for a network probe. */ void send_probe( struct net_state_t *net_state, @@ -330,8 +325,19 @@ void send_probe( if (send_packet( net_state, param, packet, packet_size, &dest_sockaddr) == -1) { - perror("Failure sending probe"); - exit(1); + if (errno == ENETDOWN) { + printf("%d network-down\n", param->command_token); + } else if (errno == ENETUNREACH) { + printf("%d no-route\n", param->command_token); + } else if (errno == EINVAL) { + printf("%d invalid-argument\n", param->command_token); + } else { + perror("Failure sending probe"); + exit(1); + } + + free_probe(probe); + return; } probe->platform.timeout_time = probe->platform.departure_time; @@ -401,12 +407,12 @@ 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); + net_state, net_state->platform.ip4_recv_socket, + handle_received_ip4_packet); receive_replies_from_socket( - net_state, net_state->platform.ipv6_recv_socket, - handle_received_ipv6_packet); + net_state, net_state->platform.ip6_recv_socket, + handle_received_ip6_packet); } /* diff --git a/packet/probe_unix.h b/packet/probe_unix.h index d0080a4..75c9775 100644 --- a/packet/probe_unix.h +++ b/packet/probe_unix.h @@ -34,28 +34,25 @@ struct probe_platform_t struct net_state_platform_t { /* Socket used to send raw IPv4 packets */ - int ipv4_send_socket; + int ip4_send_socket; /* Socket used to receive IPv4 ICMP replies */ - int ipv4_recv_socket; + int ip4_recv_socket; - /* Send socket for IPv6 packets */ - int ipv6_send_socket; + /* Send socket for ICMPv6 packets */ + int icmp6_send_socket; + + /* Send socket for UDPv6 packets */ + int udp6_send_socket; /* Receive socket for IPv6 packets */ - int ipv6_recv_socket; + int ip6_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 13046c8..84d51a1 100644 --- a/packet/protocols.h +++ b/packet/protocols.h @@ -25,11 +25,18 @@ #define ICMP_ECHO 8 #define ICMP_TIME_EXCEEDED 11 +/* ICMP_DEST_UNREACH codes */ +#define ICMP_PORT_UNREACH 3 + /* ICMPv6 type codes */ +#define ICMP6_DEST_UNREACH 1 #define ICMP6_TIME_EXCEEDED 3 #define ICMP6_ECHO 128 #define ICMP6_ECHOREPLY 129 +/* ICMP6_DEST_UNREACH codes */ +#define ICMP6_PORT_UNREACH 4 + /* We can't rely on header files to provide this information, because the fields have different names between, for instance, Linux and Solaris */ @@ -64,7 +71,7 @@ struct SCTPHeader { }; /* Structure of an IPv4 UDP pseudoheader. */ -struct UDPv4PHeader { +struct UDPPseudoHeader { uint32_t saddr; uint32_t daddr; uint8_t zero; diff --git a/packet/wait_unix.c b/packet/wait_unix.c index 348831d..2fd23ef 100644 --- a/packet/wait_unix.c +++ b/packet/wait_unix.c @@ -41,21 +41,21 @@ void wait_for_activity( struct timeval *select_timeout; int ready_count; int command_stream = command_buffer->command_stream; - int ipv4_socket = net_state->platform.ipv4_recv_socket; - int ipv6_socket = net_state->platform.ipv6_recv_socket; + int ip4_socket = net_state->platform.ip4_recv_socket; + int ip6_socket = net_state->platform.ip6_recv_socket; FD_ZERO(&read_set); FD_SET(command_stream, &read_set); nfds = command_stream + 1; - FD_SET(ipv4_socket, &read_set); - if (ipv4_socket >= nfds) { - nfds = ipv4_socket + 1; + FD_SET(ip4_socket, &read_set); + if (ip4_socket >= nfds) { + nfds = ip4_socket + 1; } - FD_SET(ipv6_socket, &read_set); - if (ipv6_socket >= nfds) { - nfds = ipv6_socket + 1; + FD_SET(ip6_socket, &read_set); + if (ip6_socket >= nfds) { + nfds = ip6_socket + 1; } while (true) { diff --git a/test/cmdparse.py b/test/cmdparse.py index 8c90ec5..7a807c6 100755 --- a/test/cmdparse.py +++ b/test/cmdparse.py @@ -20,7 +20,6 @@ '''Test mtr-packet's command parsing.''' -import re import time import unittest @@ -58,8 +57,6 @@ class TestCommandParse(mtrpacket.MtrPacketTest): 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', @@ -69,29 +66,30 @@ class TestCommandParse(mtrpacket.MtrPacketTest): for cmd in bad_commands: self.write_command(cmd) - reply = self.read_reply() - match = re.match(invalid_argument_regex, reply) - self.assertIsNotNone(match) + reply = self.parse_reply() + self.assertEqual(reply.command_name, 'invalid-argument') 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$') + ('31 check-support feature ip-4', 'ok'), + ('32 check-support feature send-probe', 'ok'), + ('33 check-support feature bogus-feature', 'no') ] - for (request, regex) in feature_tests: + self.write_command('30 check-support feature version') + reply = self.parse_reply() + self.assertEqual(reply.token, 30) + self.assertEqual(reply.command_name, 'feature-support') + self.assertIn('support', reply.argument) + + for (request, expected) in feature_tests: self.write_command(request) - reply = self.read_reply() - match = re.match(regex, reply) - self.assertIsNotNone(match) + reply = self.parse_reply() + self.assertEqual(reply.command_name, 'feature-support') + self.assertIn('support', reply.argument) + self.assertEqual(reply.argument['support'], expected) def test_command_overflow(self): 'Test overflowing the incoming command buffer' diff --git a/test/mtrpacket.py b/test/mtrpacket.py index d9bbf6c..9ffc69c 100644 --- a/test/mtrpacket.py +++ b/test/mtrpacket.py @@ -26,6 +26,21 @@ import sys import time import unittest +# +# typing is used for mypy type checking, but isn't required to run, +# so it's okay if we can't import it. +# +try: + # pylint: disable=locally-disabled, unused-import + from typing import Dict, List +except ImportError: + pass + + +class MtrPacketExecuteError(Exception): + "Exception raised when MtrPacketTest can't execute mtr-packet" + pass + class ReadReplyTimeout(Exception): 'Exception raised by TestProbe.read_reply upon timeout' @@ -39,6 +54,12 @@ class WriteCommandTimeout(Exception): pass +class MtrPacketReplyParseError(Exception): + "Exception raised when MtrPacketReply can't parse the reply string" + + pass + + def set_nonblocking(file_descriptor): # type: (int) -> None 'Put a file descriptor into non-blocking mode' @@ -48,6 +69,40 @@ def set_nonblocking(file_descriptor): # type: (int) -> None fcntl.fcntl(file_descriptor, fcntl.F_SETFL, flags | os.O_NONBLOCK) +# pylint: disable=locally-disabled, too-few-public-methods +class MtrPacketReply(object): + 'A parsed reply from mtr-packet' + + def __init__(self, reply): # type: (unicode) -> None + self.token = 0 # type: int + self.command_name = None # type: unicode + self.argument = {} # type: Dict[unicode, unicode] + + self.parse_reply(reply) + + def parse_reply(self, reply): # type (unicode) -> None + 'Parses a reply string into members for the instance of this class' + + tokens = reply.split() # type List[unicode] + + try: + self.token = int(tokens[0]) + self.command_name = tokens[1] + except IndexError: + raise MtrPacketReplyParseError(reply) + + i = 2 + while i < len(tokens): + try: + name = tokens[i] + value = tokens[i + 1] + except IndexError: + raise MtrPacketReplyParseError(reply) + + self.argument[name] = value + i += 2 + + class MtrPacketTest(unittest.TestCase): '''Base class for tests invoking mtr-packet. @@ -70,10 +125,13 @@ class MtrPacketTest(unittest.TestCase): 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) + try: + self.packet_process = subprocess.Popen( + [packet_path], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + except OSError: + raise MtrPacketExecuteError(packet_path) # Put the mtr-packet process's stdout in non-blocking mode # so that we can read from it without a timeout when @@ -95,6 +153,14 @@ class MtrPacketTest(unittest.TestCase): except OSError: return + def parse_reply(self, timeout=10.0): # type: (float) -> MtrPacketReply + '''Read the next reply from mtr-packet and parse it into + an MtrPacketReply object.''' + + reply_str = self.read_reply(timeout) + + return MtrPacketReply(reply_str) + def read_reply(self, timeout=10.0): # type: (float) -> unicode '''Read the next reply from mtr-packet. @@ -184,4 +250,4 @@ def check_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") + 'Warning: many tests require running as root\n') diff --git a/test/probe.py b/test/probe.py old mode 100755 new mode 100644 index 8919fa6..b4345b8 --- a/test/probe.py +++ b/test/probe.py @@ -19,7 +19,6 @@ '''Test sending probes and receiving respones.''' -import re import socket import sys import time @@ -31,25 +30,73 @@ import mtrpacket IPV6_TEST_HOST = 'google-public-dns-a.google.com' -class TestProbeIPv4(mtrpacket.MtrPacketTest): +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 + + +HAVE_IPV6 = check_for_local_ipv6() + + +class TestProbeICMPv4(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) + reply = self.parse_reply() + self.assertEqual(reply.token, 14) + self.assertEqual(reply.command_name, 'reply') + self.assertIn('ip-4', reply.argument) + self.assertEqual(reply.argument['ip-4'], '8.8.8.8') + self.assertIn('round-trip-time', reply.argument) 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 # @@ -65,16 +112,13 @@ class TestProbeIPv4(mtrpacket.MtrPacketTest): # 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) + reply = self.parse_reply() + self.assertEqual(reply.token, 15) + self.assertEqual(reply.command_name, 'no-reply') 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 @@ -86,16 +130,16 @@ class TestProbeIPv4(mtrpacket.MtrPacketTest): reply = None try: - reply = self.read_reply(0) + reply = self.parse_reply(0) except mtrpacket.ReadReplyTimeout: pass if reply: - match = re.match(exhausted_regex, reply) - if match: + if reply.command_name == 'probes-exhausted': break - self.assertIsNotNone(match) + self.assertIsNotNone(reply) + self.assertEqual(reply.command_name, 'probes-exhausted') def test_timeout_values(self): '''Test that timeout values wait the right amount of time @@ -105,36 +149,34 @@ class TestProbeIPv4(mtrpacket.MtrPacketTest): begin = time.time() self.write_command('19 send-probe ip-4 8.8.254.254 timeout 0') - self.read_reply() + self.parse_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() + self.parse_reply() elapsed = time.time() - begin - self.assertGreaterEqual(elapsed, 1.0) + self.assertGreaterEqual(elapsed, 0.9) 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() + self.parse_reply() elapsed = time.time() - begin - self.assertGreaterEqual(elapsed, 3.0) + self.assertGreaterEqual(elapsed, 2.9) 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) + reply = self.parse_reply() + self.assertEqual(reply.command_name, 'ttl-expired') + self.assertIn('ip-4', reply.argument) + self.assertIn('round-trip-time', reply.argument) def test_parallel_probes(self): '''Test sending multiple probes in parallel @@ -143,9 +185,6 @@ class TestProbeIPv4(mtrpacket.MtrPacketTest): 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 @@ -155,17 +194,25 @@ class TestProbeIPv4(mtrpacket.MtrPacketTest): 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: + reply = self.parse_reply() + if reply.command_name == 'no-reply': continue - first_time = int(match.group(1)) - reply = self.read_reply() - match = re.match(reply_regex, reply) - if not match: + self.assertEqual(reply.command_name, 'reply') + self.assertIn('ip-4', reply.argument) + self.assertEqual(reply.argument['ip-4'], '127.0.0.1') + self.assertIn('round-trip-time', reply.argument) + first_time = int(reply.argument['round-trip-time']) + + reply = self.parse_reply() + if reply.command_name == 'no-reply': continue - second_time = int(match.group(1)) + + self.assertEqual(reply.command_name, 'reply') + self.assertIn('ip-4', reply.argument) + self.assertEqual(reply.argument['ip-4'], '8.8.8.8') + self.assertIn('round-trip-time', reply.argument) + second_time = int(reply.argument['round-trip-time']) # Ensure we got a reply from the host with the lowest latency # first. @@ -173,107 +220,98 @@ class TestProbeIPv4(mtrpacket.MtrPacketTest): success_count += 1 - # We need 95% success to pass. This allows a few probes to be + # We need 90% 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) + required_success = int(loop_count * 0.90) 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): +class TestProbeICMPv6(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) + super(TestProbeICMPv6, self).__init__(*args) - @unittest.skipIf(not have_ipv6, 'No IPv6') + @unittest.skipUnless(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) + reply = self.parse_reply() + self.assertEqual(reply.command_name, 'reply') + self.assertIn('ip-6', reply.argument) + self.assertIn('round-trip-time', reply.argument) # 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) + reply = self.parse_reply() + self.assertEqual(reply.command_name, 'reply') + self.assertIn('ip-6', reply.argument) + self.assertIn('round-trip-time', reply.argument) + self.assertEqual(reply.argument['ip-6'], '::1') - @unittest.skipIf(not have_ipv6, 'No IPv6') + @unittest.skipUnless(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) + reply = self.parse_reply() + self.assertEqual('ttl-expired', reply.command_name) + self.assertIn('ip-6', reply.argument) + self.assertIn('round-trip-time', reply.argument) + + +class TestProbeUDP(mtrpacket.MtrPacketTest): + 'Test transmitting probes using UDP' + + def test_udp_v4(self): + 'Test IPv4 UDP probes' + + cmd = '60 send-probe protocol udp ip-4 8.8.8.8 port 164 ttl 1' + self.write_command(cmd) + + reply = self.parse_reply() + self.assertEqual(reply.command_name, 'ttl-expired') + + cmd = '61 send-probe protocol udp ip-4 127.0.0.1 port 164' + self.write_command(cmd) + + reply = self.parse_reply() + self.assertEqual(reply.command_name, 'reply') + self.assertIn('ip-4', reply.argument) + self.assertEqual(reply.argument['ip-4'], '127.0.0.1') + + @unittest.skipUnless(HAVE_IPV6, 'No IPv6') + def test_udp_v6(self): + 'Test IPv6 UDP probes' + + test_addr = resolve_ipv6_address(IPV6_TEST_HOST) + + cmd = '62 send-probe protocol udp ip-6 ' + test_addr + \ + ' port 164 ttl 1' + self.write_command(cmd) + + reply = self.parse_reply() + self.assertEqual(reply.command_name, 'ttl-expired') + + cmd = '63 send-probe protocol udp ip-6 ::1 port 164' + self.write_command(cmd) + reply = self.parse_reply() + self.assertEqual(reply.command_name, 'reply') + self.assertIn('ip-6', reply.argument) + self.assertEqual(reply.argument['ip-6'], '::1') if __name__ == '__main__': mtrpacket.check_running_as_root()