Added mtr-packet subprocess

The mtr-packet tool isolates the raw socket sending/receiving of
packets from the mtr user interface.  By isolating the socket
interactions to a separate process, we can be sure that any security
flaws in the user-interface code doesn't expose a raw socket
interface to an attacker attempting to escalate privileges.

This is a bare-bones implementation, only support ICMP,
only support IP version 4, and missing many of the probe customization
features available in mtr.  It will require some more work to reach
feature parity with the current mtr implementation.  But it's a start.

The include mtr-packet man page explains the protocol format
used to communicate with this new process.

Included is an automated test for mtr-packet, implemented using
Python's unittest module.  Though the code actually being tested
is implemented in C, Python make it easy to write test cases.
'make check' will test the current build.

An alternate code-path for Windows is included in the mtr-packet
tool.  The mechanism for sending and receiving network probes
is significantly different for Windows, as compared to Unix-like
operating systems, but the interface provided by mtr-packet is
the same.  'make dist-windows-bin' will make a Windows binary
distribution.  A Cygwin build environment is required, but the
resulting binary distribution doesn't require that Cygwin be
already installed.

Tested on:

Ubuntu 16.10, FreeBSD 11.0, MacOS 10.12.1 (Sierra), Windows 7

Since the code changes are significant, more esoteric operating
systems may require changes.
This commit is contained in:
Matt Kimball 2016-11-30 17:14:43 -08:00
parent 7e13a55af9
commit 5d26cb0c05
37 changed files with 3503 additions and 1182 deletions

2
.gitignore vendored
View File

@ -19,9 +19,11 @@ stamp-h1*
/autom4te.cache/
/.deps/
/packet/.deps/
/ChangeLog
/INSTALL
/mtr
/mtr-packet
/mtr.8
/mtr-*.tar.gz

View File

@ -1,6 +1,12 @@
EXTRA_DIST = SECURITY img/mtr_icon.xpm
EXTRA_DIST = \
SECURITY \
mtr.bat \
img/mtr_icon.xpm \
packet/testpacket.py \
packet/lint.sh
sbin_PROGRAMS = mtr
sbin_PROGRAMS = mtr mtr-packet
TESTS = packet/testpacket.py
PATHFILES =
CLEANFILES = $(PATHFILES)
@ -8,7 +14,7 @@ EXTRA_DIST += $(PATHFILES:=.in)
edit_cmd = sed \
-e 's|@VERSION[@]|$(VERSION)|g'
$(PATHFILES): Makefile
%.8: $(srcdir)/%.8.in
@ rm -f $@ $@.tmp
$(AM_V_at) $(MKDIR_P) $$(dirname $@)
$(AM_V_GEN) srcdir=''; \
@ -16,12 +22,14 @@ $(PATHFILES): Makefile
$(edit_cmd) $${srcdir}$@.in >$@.tmp
@ mv $@.tmp $@
dist_man_MANS = mtr.8
PATHFILES += mtr.8
$(PATHFILES): Makefile
dist_man_MANS = mtr.8 mtr-packet.8
PATHFILES += mtr.8 mtr-packet.8
install-exec-hook:
`setcap cap_net_raw+ep $(DESTDIR)$(sbindir)/mtr` \
|| chmod u+s $(DESTDIR)$(sbindir)/mtr
`setcap cap_net_raw+ep $(DESTDIR)$(sbindir)/mtr-packet` \
|| chmod u+s $(DESTDIR)$(sbindir)/mtr-packet
mtr_SOURCES = mtr.c mtr.h \
net.c net.h \
@ -32,6 +40,7 @@ mtr_SOURCES = mtr.c mtr.h \
report.c report.h \
select.c select.h \
utils.c utils.h \
packet/cmdparse.c packet/cmdparse.h \
mtr-curses.h \
img/mtr_icon.xpm \
mtr-gtk.h
@ -65,6 +74,60 @@ mtr_INCLUDES = $(GLIB_CFLAGS) -I$(top_builddir) -I$(top_srcdir)
mtr_CFLAGS = $(GTK_CFLAGS) $(NCURSES_CFLAGS)
mtr_LDADD = $(GTK_LIBS) $(NCURSES_LIBS) $(RESOLV_LIBS)
mtr_packet_SOURCES = \
packet/packet.c \
packet/cmdparse.c packet/cmdparse.h \
packet/command.c packet/command.h \
packet/platform.h \
packet/probe.c packet/probe.h \
packet/protocols.h \
packet/timeval.c packet/timeval.h \
packet/wait.h
if CYGWIN
mtr_packet_SOURCES += \
packet/command_cygwin.c packet/command_cygwin.h \
packet/probe_cygwin.c packet/probe_cygwin.h \
packet/wait_cygwin.c
mtr_packet_LDADD = -lcygwin -licmp -lws2_32
dist_windows_aux = \
$(srcdir)/mtr.bat \
$(srcdir)/AUTHORS \
$(srcdir)/COPYING \
$(srcdir)/README \
$(srcdir)/NEWS
distwindir = $(distdir)-win-$(host_cpu)
# Bundle necessary files for a Windows binary distribution
distdir-win: $(dist_windows_aux) mtr.exe mtr-packet.exe
rm -fr $(distwindir)
mkdir -p $(distwindir) $(distwindir)/bin $(distwindir)/terminfo
cp $(dist_windows_aux) -t $(distwindir)
cp mtr.exe mtr-packet.exe -t $(distwindir)/bin
ldd mtr.exe | grep -v cygdrive | awk '{ print $$3 }' | xargs cp -t $(distwindir)/bin
cp `find /usr/share/terminfo -name cygwin | xargs dirname` -r $(distwindir)/terminfo
# Zip up a Windows binary distribution
dist-windows-bin: distdir-win
rm -f $(distwindir).zip
zip -rq $(distwindir).zip $(distwindir)
rm -fr $(distwindir)
else # if CYGWIN
mtr_packet_SOURCES += \
packet/command_unix.c packet/command_unix.h \
packet/probe_unix.c packet/probe_unix.h \
packet/wait_unix.c
endif # if CYGWIN
if BUILD_BASH_COMPLETION
dist_bashcompletion_DATA = bash-completion/mtr
endif

View File

@ -3,5 +3,4 @@
aclocal $ACLOCAL_OPTS
autoheader
automake --add-missing --copy --foreign
autoconf
autoconf --force

View File

@ -17,6 +17,7 @@ m4_ifdef([AM_SILENT_RULES],
[AM_SILENT_RULES([yes])],
[AC_SUBST([AM_DEFAULT_VERBOSITY], [1])])
AC_CANONICAL_HOST
AC_PROG_CC
# Check pkg-config availability.
@ -29,6 +30,8 @@ before running ./bootstrap.sh again.])
])
PKG_PROG_PKG_CONFIG
AM_CONDITIONAL([CYGWIN], [test "$host_os" = cygwin])
# Check bytes in types.
AC_CHECK_SIZEOF([unsigned char], [1])
AC_CHECK_SIZEOF([unsigned short], [2])

View File

@ -21,6 +21,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <time.h>
#include "mtr.h"
#include "display.h"
@ -107,8 +108,12 @@ extern void display_open(struct mtr_ctl *ctl)
}
extern void display_close(struct mtr_ctl *ctl, time_t now)
extern void display_close(struct mtr_ctl *ctl)
{
time_t now;
now = time(NULL);
switch(ctl->DisplayMode) {
case DisplayReport:
report_close(ctl);

View File

@ -53,7 +53,7 @@ enum {
/* Prototypes for display.c */
extern void display_detect(struct mtr_ctl *ctl, int *argc, char ***argv);
extern void display_open(struct mtr_ctl *ctl);
extern void display_close(struct mtr_ctl *ctl, time_t now);
extern void display_close(struct mtr_ctl *ctl);
extern void display_redraw(struct mtr_ctl *ctl);
extern void display_rawxmit(struct mtr_ctl *ctl, int hostnum, int seq);
extern void display_rawping(struct mtr_ctl *ctl, int hostnum, int msec, int seq);

226
mtr-packet.8.in Normal file
View File

@ -0,0 +1,226 @@
.TH MTR-PACKET 8 "@VERSION@" "mtr-packet" "System Administration"
.SH NAME
mtr-packet - send and receive network probes
.SH DESCRIPTION
.B mtr-packet
reads command requests from
.I stdin,
each separated by a newline character, and responds with command replies
to
.I stdout\c
, also each separated by a newline character. The syntactic structure of
requests and replies are the same. The following format is used:
.LP
.RS
.I TOKEN
.I COMMAND
[\c
.I ARGUMENT-NAME
.I ARGUMENT-VALUE
\&...]
.RE
.LP
.I TOKEN
is a unique integer value. The same value will be used as the
.I TOKEN
for the response. This is necessary for associating replies with
requests, as commands may be completed in a different order than they are
requested. The invoker of
.B mtr-packet
should always use the
.I TOKEN
value to determine which command request has completed.
.LP
.I COMMAND
is a string identifying the command request type. A common command
is
.B send-probe\c
, which will transmit one network probe.
.LP
.I ARGUMENT-NAME
strings and
.I ARGUMENT-VALUE
strings always come in pairs. It is a syntactic error to provide an
.I ARGUMENT-NAME
without a corresponding
.I ARGUMENT-VALUE\c
\&. Valid
.I ARGUMENT-NAME
strings depend on the
.I COMMAND
being used.
.SH REQUESTS
.TP
.B send-probe
Send a network probe to a particular IP address. An IP address must
be provided as an argument.
.B send-probe
will reply with
.B reply\c
,
.B no-reply\c
, or
.B ttl-expired\c
\&.
The following arguments may be used:
.B ip-4
.I IP-ADDRESS
The Internet Protocol version 4 address to probe.
.B timeout
.I TIMEOUT-SECONDS
The number of seconds to wait for a response to the probe before
discarding the probe as lost, and generating a
.B no-reply
command reply.
.B ttl
.I TIME-TO-LIVE
The time-to-live value for the Internet Protocol packet header used
in constructing the probe. This value determines the number of
network hops through which the probe will travel before a response
is generated by an intermediate network host.
.TP
.B check-support
Check for support for a particular feature in this version of
.B mtr-packet
and in this particular operating environment.
.B check-support
will reply with
.B feature-supported\c
\&. A
.B feature
argument is required.
.B feature
.I FEATURE-NAME
The name of a feature requested. Some features which can be checked
are
.B send-probe
and
.B ip-4\c
\&. The feature
.B version
can be checked to retrieve the version of
.B mtr-packet\c
\&.
.SH REPLIES
.TP
.B reply
The destination host received the
.B send-probe
probe and replied. Arguments of the reply are the following:
.B ip-4
.I IP-ADDRESS
The Internet Protocol address of the host which replied to the
probe.
.B round-trip-time
.I TIME
The time which passed between the transmission of the probe and its
response. The time is provided as a integral number of
microseconds elapsed.
.TP
.B no-reply
No response to the probe request was received before the timeout
expired.
.TP
.B ttl-expired
The time-to-live value of the transmitted probe expired before
the probe arrived at its intended destination. Arguments of
.B ttl-expired
are:
.B ip-4
.I IP-ADDRESS
The Internet Protocol address of the host at which the time-to-live
value expired.
.B round-trip-time
.I TIME
The time which passed between the transmission of the probe and its
response. The time is provided as a integral number of
microseconds elapsed.
.TP
.B feature-support
A reply to provided to
.B check-support
indicating the availability of a particular feature. The argument
provided is:
.B support
.I PRESENT
In most cases, the
.I PRESENT
value will be either
.B ok\c
, indicating the feature is supported, or
.B no\c
, indicating no support for the feature.
In the case that
.B version
is the requested
.I FEATURE-NAME\c
, the version of
.B mtr-packet
is provided as the
.I PRESENT
value.
.SH EXAMPLE
A controlling program may start
.B mtr-packet
as a child process and issue the following command on
.I stdin\c
:
.LP
.RS
42 send-probe ip-4 127.0.0.1
.RE
.LP
This will send a network probe to the loopback interface. When the probe
completes,
.B
mtr-packet
will provide a response on
.I stdout
such as the following:
.LP
.RS
42 reply ip-4 127.0.0.1 round-trip-time 126
.RE
.LP
This indicates that the loopback address replied to the probe, and the
round-trip time of the probe was 126 microseconds.
.SH CONTACT INFORMATION
.PP
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),
TCP/IP Illustrated (Stevens, ISBN 0201633469).

View File

@ -448,8 +448,19 @@ passed in
.B MTR_OPTIONS\c
).
.TP
.B MTR_PACKET
A path to the
.I mtr-packet
executable, to be used for sending and receiving network probes. If
.B MTR_PACKET
is unset, the
.B PATH
will be used to search for an
.I mtr-packet
executable.
.TP
.B DISPLAY
Used for the GTK+ frontend.
Specifies an X11 server for the GTK+ frontend.
.SH BUGS
Some modern routers give a lower priority to ICMP ECHO packets than
to other network traffic. Consequently, the reliability of these
@ -470,6 +481,7 @@ GitHub at:
.UR https://\:github\:.com/\:traviscross/\:mtr
.UE .
.SH "SEE ALSO"
.BR mtr-packet (8),
.BR traceroute (8),
.BR ping (8),
.BR socket (7),

32
mtr.bat Executable file
View File

@ -0,0 +1,32 @@
@echo off
rem
rem mtr -- a network diagnostic tool
rem Copyright (C) 2016 Matt Kimball
rem
rem This program is free software; you can redistribute it and/or modify
rem it under the terms of the GNU General Public License version 2 as
rem published by the Free Software Foundation.
rem
rem This program is distributed in the hope that it will be useful,
rem but WITHOUT ANY WARRANTY; without even the implied warranty of
rem MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
rem GNU General Public License for more details.
rem
rem You should have received a copy of the GNU General Public License
rem along with this program; if not, write to the Free Software
rem Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
rem
rem Assume the path of this batch file is the mtr installation location
set MTR_DIR=%~dp0
set MTR_BIN=%MTR_DIR%\bin
rem ncurses needs to locate the cygwin terminfo file
set TERMINFO=%MTR_DIR%\terminfo
rem mtr needs to know the location to the packet generator
set MTR_PACKET=%MTR_BIN%\mtr-packet.exe
rem Pass along commandline arguments
%MTR_BIN%\mtr %*

50
mtr.c
View File

@ -41,7 +41,6 @@
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <time.h>
#include <ctype.h>
#include <assert.h>
#include <fcntl.h>
@ -623,7 +622,6 @@ static void init_rand(void)
extern int main(int argc, char **argv)
{
struct hostent * host = NULL;
int net_preopen_result;
struct addrinfo hints, *res;
int gai_error;
struct hostent trhost;
@ -632,7 +630,6 @@ extern int main(int argc, char **argv)
#ifdef ENABLE_IPV6
struct sockaddr_in6 * sa6;
#endif
time_t now;
names_t *names_root = NULL;
names_t **names_head = &names_root;
@ -656,20 +653,13 @@ extern int main(int argc, char **argv)
ctl.ipinfo_max = -1;
xstrncpy(ctl.fld_active, "LS NABWV", 2 * MAXFLD);
/* Get the raw sockets first thing, so we can drop to user euid immediately */
if ( ( net_preopen_result = net_preopen () ) ) {
error(EXIT_FAILURE, errno, "Unable to get raw sockets");
}
/* Now drop to user permissions */
if (setgid(getgid()) || setuid(getuid())) {
error(EXIT_FAILURE, errno, "Unable to drop permissions");
}
/* Double check, just in case */
/*
mtr used to be suid root. It should not be with this version.
We'll check so that we can notify people using installation
mechanisms with obsolete assumptions.
*/
if ((geteuid() != getuid()) || (getegid() != getgid())) {
error(EXIT_FAILURE, errno, "Unable to drop permissions");
error(EXIT_FAILURE, errno, "mtr should not run suid");
}
/* This will check if stdout/stderr writing is successful */
@ -694,11 +684,6 @@ extern int main(int argc, char **argv)
append_to_names(names_head, name);
}
/* Now that we know mtrtype we can select which socket to use */
if (net_selectsocket(&ctl) != 0) {
error(EXIT_FAILURE, 0, "Couldn't determine raw socket type");
}
if (!names_root) append_to_names (names_head, "localhost"); /* default: localhost. */
names_head = &names_root;
@ -709,16 +694,6 @@ extern int main(int argc, char **argv)
xstrncpy(ctl.LocalHostname, "UNKNOWNHOST", sizeof(ctl.LocalHostname));
}
if (net_preopen_result != 0) {
error(0, 0, "Unable to get raw socket. (Executable not suid?)");
if (ctl.DisplayMode != DisplayCSV)
exit(EXIT_FAILURE);
else {
names_root = names_root->next;
continue;
}
}
/* gethostbyname2() is deprecated so we'll use getaddrinfo() instead. */
memset( &hints, 0, sizeof hints );
hints.ai_family = ctl.af;
@ -777,16 +752,6 @@ extern int main(int argc, char **argv)
}
}
if (net_set_interfaceaddress (&ctl) != 0) {
error(0, 0, "Couldn't set interface address: %s", ctl.InterfaceAddress);
if (ctl.DisplayMode != DisplayCSV)
exit(EXIT_FAILURE);
else {
names_root = names_root->next;
continue;
}
}
lock(stdout);
dns_open(&ctl);
@ -795,8 +760,7 @@ extern int main(int argc, char **argv)
display_loop(&ctl);
net_end_transit();
now = time(NULL);
display_close(&ctl, now);
display_close(&ctl);
unlock(stdout);
if (ctl.DisplayMode != DisplayCSV)

1515
net.c

File diff suppressed because it is too large Load Diff

4
net.h
View File

@ -30,11 +30,8 @@
#include "mtr.h"
extern int net_preopen(void);
extern int net_selectsocket(struct mtr_ctl *ctl);
extern int net_open(struct mtr_ctl *ctl, struct hostent *host);
extern void net_reopen(struct mtr_ctl *ctl, struct hostent *address);
extern int net_set_interfaceaddress (struct mtr_ctl *ctl);
extern void net_reset(struct mtr_ctl *ctl);
extern void net_close(void);
extern int net_waitfd(void);
@ -79,4 +76,3 @@ extern int addrcmp( char * a, char * b, int af );
extern void addrcpy( char * a, char * b, int af );
extern void net_add_fds(fd_set *writefd, int *maxfd);
extern void net_process_fds(struct mtr_ctl *ctl, fd_set *writefd);

125
packet/cmdparse.c Normal file
View File

@ -0,0 +1,125 @@
/*
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 "cmdparse.h"
#include <ctype.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
/*
NUL terminate the whitespace separated tokens in the command string.
This modifies command_string in-place with NUL characters.
Fill the tokens array with pointers to the tokens, and return the
number of tokens found.
*/
static
int tokenize_command(
char **tokens,
int max_tokens,
char *command_string)
{
int token_count = 0;
int on_space = 1;
int i;
for (i = 0; command_string[i]; i++) {
if (on_space) {
if (!isspace(command_string[i])) {
/* Take care not to exceed the token array length */
if (token_count >= max_tokens) {
return -1;
}
tokens[token_count++] = &command_string[i];
on_space = 0;
}
} else {
if (isspace(command_string[i])) {
command_string[i] = 0;
on_space = 1;
}
}
}
return token_count;
}
/*
Parse a command string (or command reply string) into a command_t
structure for later semantic interpretation. Returns EINVAL if the
command string is unparseable or zero for success.
comamnd_string will be modified in-place with NUL characters terminating
tokens, and the command_t will use pointers to the conents of
command_string without copying, so any interpretation of the
command_t structure requires that the command_string memory has not yet
been freed or otherwise reused.
*/
int parse_command(
struct command_t *command,
char *command_string)
{
const int max_tokens = MAX_COMMAND_ARGUMENTS * 2 + 2;
char *tokens[max_tokens];
int token_count;
int i;
memset(command, 0, sizeof(struct command_t));
/* Tokenize the string using whitespace */
token_count = tokenize_command(tokens, max_tokens, command_string);
if (token_count < 2) {
return EINVAL;
}
/* Expect the command token to be a numerical value */
errno = 0;
command->token = strtol(tokens[0], NULL, 10);
if (errno) {
return EINVAL;
}
command->command_name = tokens[1];
/*
The tokens beyond the command name are expected to be in
name, value pairs.
*/
i = 2;
command->argument_count = 0;
while (i < token_count) {
/* It's an error if we get a name without a key */
if (i + 1 >= token_count) {
return EINVAL;
}
/* It's an error if we get more arguments than we have space for */
if (command->argument_count >= MAX_COMMAND_ARGUMENTS) {
return EINVAL;
}
command->argument_name[command->argument_count] = tokens[i];
command->argument_value[command->argument_count] = tokens[i + 1];
command->argument_count++;
i += 2;
}
return 0;
}

47
packet/cmdparse.h Normal file
View File

@ -0,0 +1,47 @@
/*
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 CMDPARSE_H
#define CMDPARSE_H
#define MAX_COMMAND_ARGUMENTS 16
/* Parsed commands, or command replies, ready for semantic interpretation */
struct command_t
{
/* A unique value for matching command requests with replies */
int token;
/* Text indiciating the command type, or reply type */
char *command_name;
/* The number of key, value argument pairs used */
int argument_count;
/* Names for each argument */
char *argument_name[MAX_COMMAND_ARGUMENTS];
/* Values for each argument, parallel to the argument_name array */
char *argument_value[MAX_COMMAND_ARGUMENTS];
};
int parse_command(
struct command_t *command,
char *command_string);
#endif

242
packet/command.c Normal file
View File

@ -0,0 +1,242 @@
/*
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 "command.h"
#include <assert.h>
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cmdparse.h"
#include "platform.h"
#include "config.h"
/*
Find a parameter with a particular name in a command_t structure.
If no such parameter exists, return NULL.
*/
static
const char *find_parameter(
const struct command_t *command,
const char *name_request)
{
const char *name;
const char *value;
int i;
for (i = 0; i < command->argument_count; i++) {
name = command->argument_name[i];
value = command->argument_value[i];
if (!strcmp(name, name_request)) {
return value;
}
}
return NULL;
}
/* Given a feature name, return a string for the check-support reply */
static
const char *check_support(
const char *feature)
{
if (!strcmp(feature, "version")) {
return PACKAGE_VERSION;
}
if (!strcmp(feature, "ip-4")) {
return "ok";
}
if (!strcmp(feature, "send-probe")) {
return "ok";
}
return "no";
}
/* Handle a check-support request by checking for a particular feature */
static
void check_support_command(
const struct command_t *command)
{
const char *feature;
const char *support;
feature = find_parameter(command, "feature");
if (feature == NULL) {
printf("%d invalid-argument\n", command->token);
return;
}
support = check_support(feature);
printf("%d feature-support support %s\n", command->token, support);
}
/*
If a named send_probe argument is recognized, fill in the probe paramters
structure with the argument value.
*/
static
bool decode_probe_argument(
struct probe_param_t *param,
const char *name,
const char *value)
{
char *endstr = NULL;
/* Pass IPv4 addresses as string values */
if (!strcmp(name, "ip-4")) {
param->ipv4_address = value;
}
/* Time-to-live values */
if (!strcmp(name, "ttl")) {
param->ttl = strtol(value, &endstr, 10);
if (*endstr != 0) {
return false;
}
}
/* Number of seconds to wait for a reply */
if (!strcmp(name, "timeout")) {
param->timeout = strtol(value, &endstr, 10);
if (*endstr != 0) {
return false;
}
}
return true;
}
/* Handle "send-probe" commands */
static
void send_probe_command(
const struct command_t *command,
struct net_state_t *net_state)
{
struct probe_param_t param;
int i;
char *name;
char *value;
/* We will prepare a probe_param_t for send_probe. */
memset(&param, 0, sizeof(struct probe_param_t));
param.command_token = command->token;
param.ttl = 255;
param.timeout = 10;
for (i = 0; i < command->argument_count; i++) {
name = command->argument_name[i];
value = command->argument_value[i];
if (!decode_probe_argument(&param, name, value)) {
printf("%d invalid-argument\n", command->token);
return;
}
}
/* Send the probe using a platform specific mechanism */
send_probe(net_state, &param);
}
/*
Given a parsed command, dispatch to the handler for specific
command requests.
*/
static
void dispatch_command(
const struct command_t *command,
struct net_state_t *net_state)
{
if (!strcmp(command->command_name, "check-support")) {
check_support_command(command);
} else if (!strcmp(command->command_name, "send-probe")) {
send_probe_command(command, net_state);
} else {
/* For unrecognized commands, respond with an error */
printf("%d unknown-command\n", command->token);
}
}
/*
With newly read data in our command buffer, dispatch all completed
command requests.
*/
void dispatch_buffer_commands(
struct command_buffer_t *buffer,
struct net_state_t *net_state)
{
struct command_t command;
char *end_of_command;
char full_command[COMMAND_BUFFER_SIZE];
int command_length;
int remaining_count;
while (true) {
assert(buffer->incoming_read_position < COMMAND_BUFFER_SIZE);
/* Terminate the buffer string */
buffer->incoming_buffer[buffer->incoming_read_position] = 0;
/* Find the next newline, which terminates command requests */
end_of_command = index(buffer->incoming_buffer, '\n');
if (end_of_command == NULL) {
/*
No newlines found, so any data we've read so far is
not yet complete.
*/
break;
}
command_length = end_of_command - buffer->incoming_buffer;
remaining_count = buffer->incoming_read_position - command_length - 1;
/* Copy the completed command */
memmove(full_command, buffer->incoming_buffer, command_length);
full_command[command_length] = 0;
/*
Free the space used by the completed command by advancing the
remaining requests within the buffer.
*/
memmove(buffer->incoming_buffer, end_of_command + 1, remaining_count);
buffer->incoming_read_position -= command_length + 1;
if (parse_command(&command, full_command)) {
/* If the command fails to parse, respond with an error */
printf("0 command-parse-error\n");
} else {
dispatch_command(&command, net_state);
}
}
if (buffer->incoming_read_position >= COMMAND_BUFFER_SIZE - 1) {
/*
If we've filled the buffer without a complete command, the
only thing we can do is discard what we've read and hope that
new data is better formatted.
*/
printf("0 command-buffer-overflow\n");
buffer->incoming_read_position = 0;
}
}

60
packet/command.h Normal file
View File

@ -0,0 +1,60 @@
/*
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 COMMAND_H
#define COMMAND_H
#include "platform.h"
#include "probe.h"
#define COMMAND_BUFFER_SIZE 4096
#ifdef PLATFORM_CYGWIN
#include "command_cygwin.h"
#else
#include "command_unix.h"
#endif
/* Storage for incoming commands, prior to command parsing */
struct command_buffer_t
{
/* The file descriptor of the incoming command stream */
int command_stream;
/* Storage to read commands into */
char incoming_buffer[COMMAND_BUFFER_SIZE];
/* The number of bytes read so far in incoming_buffer */
int incoming_read_position;
/* Platform specific */
struct command_buffer_platform_t platform;
};
void init_command_buffer(
struct command_buffer_t *command_buffer,
int command_stream);
int read_commands(
struct command_buffer_t *buffer);
void dispatch_buffer_commands(
struct command_buffer_t *buffer,
struct net_state_t *net_state);
#endif

153
packet/command_cygwin.c Normal file
View File

@ -0,0 +1,153 @@
/*
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 "command.h"
#include <io.h>
#include <stdio.h>
/*
A completion routine to be called by Windows when a read from
the command stream has completed.
*/
static
void CALLBACK finish_read_command(
DWORD status,
DWORD size_read,
OVERLAPPED *overlapped)
{
struct command_buffer_t *buffer;
char *read_position;
/*
hEvent is unusuaed by ReadFileEx, so we use it to pass
our command_buffer structure.
*/
buffer = (struct command_buffer_t *)overlapped->hEvent;
if (status) {
/* When the stream is closed ERROR_BROKEN_PIPE will be the result */
if (status == ERROR_BROKEN_PIPE) {
buffer->platform.pipe_open = false;
return;
}
fprintf(stderr, "ReadFileEx completion failure %d\n", status);
exit(1);
}
/* Copy from the overlapped I/O buffer to the incoming command buffer */
read_position = &buffer->incoming_buffer[buffer->incoming_read_position];
memcpy(read_position, buffer->platform.overlapped_buffer, size_read);
/* Account for the newly read data */
buffer->incoming_read_position += size_read;
buffer->platform.read_active = false;
}
/*
An APC which does nothing, to be used only to wake from the current
alertable wait.
*/
static
void CALLBACK empty_apc(
ULONG *param)
{
}
/* Wake from the next alertable wait without waiting for newly read data */
static
void queue_empty_apc(void)
{
if (QueueUserAPC((PAPCFUNC)empty_apc, GetCurrentThread(), 0) == 0) {
fprintf(
stderr, "Unexpected QueueUserAPC failure %d\n", GetLastError());
exit(1);
}
}
/* Start a new overlapped I/O read from the command stream */
static
void start_read_command(
struct command_buffer_t *buffer)
{
HANDLE command_stream =
(HANDLE)get_osfhandle(buffer->command_stream);
int space_remaining =
COMMAND_BUFFER_SIZE - buffer->incoming_read_position - 1;
int err;
/* If a read is already active, or the pipe is closed, do nothing */
if (!buffer->platform.pipe_open || buffer->platform.read_active) {
return;
}
memset(&buffer->platform.overlapped, 0, sizeof(OVERLAPPED));
buffer->platform.overlapped.hEvent = (HANDLE)buffer;
if (!ReadFileEx(
command_stream, buffer->platform.overlapped_buffer, space_remaining,
&buffer->platform.overlapped, finish_read_command)) {
err = GetLastError();
if (err == ERROR_BROKEN_PIPE) {
/* If the command stream has been closed, we need to wake from
the next altertable wait to exit the main loop */
buffer->platform.pipe_open = false;
queue_empty_apc();
return;
} else if (err != WAIT_IO_COMPLETION) {
fprintf(
stderr, "Unexpected ReadFileEx failure %d\n", GetLastError());
exit(1);
}
}
/* Remember that we have started an overlapped read already */
buffer->platform.read_active = true;
}
/* Initialize the command buffer, and start the first overlapped read */
void init_command_buffer(
struct command_buffer_t *command_buffer,
int command_stream)
{
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.
*/
int read_commands(
struct command_buffer_t *buffer)
{
start_read_command(buffer);
if (!buffer->platform.pipe_open) {
return EPIPE;
}
return 0;
}

48
packet/command_cygwin.h Normal file
View File

@ -0,0 +1,48 @@
/*
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 COMMAND_CYGWIN_H
#define COMMAND_CYGWIN_H
/*
Though Cygwin supports the usual Unix non-blocking reads on
the command stream, we've got to use Overlapped I/O instead because
ICMP.DLL's support for sending probes requires Overlapped I/O
and alertable waits for notification of replies. Since we need
alertable waits, we can't use Cygwin's select to determine when
command stream data is available, but Overlapped I/O completion
will work.
*/
/* Overlapped I/O manament for Windows command buffer reads */
struct command_buffer_platform_t
{
/* true if an overlapped I/O read is active */
bool read_active;
/* true if the command pipe is still open */
bool pipe_open;
/* Windows OVERLAPPED I/O data */
OVERLAPPED overlapped;
/* The buffer which active OVERLAPPED reads read into */
char overlapped_buffer[COMMAND_BUFFER_SIZE];
};
#endif

89
packet/command_unix.c Normal file
View File

@ -0,0 +1,89 @@
/*
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 "command.h"
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
/*
Initialize the command buffer and put the command stream in
non-blocking mode.
*/
void init_command_buffer(
struct command_buffer_t *command_buffer,
int command_stream)
{
int flags;
memset(command_buffer, 0, sizeof(struct command_buffer_t));
command_buffer->command_stream = command_stream;
/* Get the current command stream flags */
flags = fcntl(command_stream, F_GETFL, 0);
if (flags == -1) {
perror("Unexpected command stream error");
exit(1);
}
/* Set the O_NONBLOCK bit */
if (fcntl(command_stream, F_SETFL, flags | O_NONBLOCK)) {
perror("Unexpected command stream error");
exit(1);
}
}
/* Read currently available data from the command stream */
int read_commands(
struct command_buffer_t *buffer)
{
int space_remaining =
COMMAND_BUFFER_SIZE - buffer->incoming_read_position - 1;
char *read_position =
&buffer->incoming_buffer[buffer->incoming_read_position];
int read_count;
int command_stream = buffer->command_stream;
read_count = read(command_stream, read_position, space_remaining);
/* If the command stream has been closed, read will return zero. */
if (read_count == 0)
{
return EPIPE;
}
if (read_count > 0) {
/* Account for the newly read data */
buffer->incoming_read_position += read_count;
}
if (read_count < 0) {
/* EAGAIN simply means there is no available data to read */
/* EINTR indicates we received a signal during read */
if (errno != EINTR && errno != EAGAIN) {
perror("Unexpected command buffer read error");
exit(1);
}
}
return 0;
}

27
packet/command_unix.h Normal file
View File

@ -0,0 +1,27 @@
/*
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 COMMAND_UNIX_H
#define COMMAND_UNIX_H
/* No platform specific data is required for Unix command streams */
struct command_buffer_platform_t
{
};
#endif

9
packet/lint.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/sh
# Check the Python test source for good style
PYTHON_SOURCE=testpacket.py
pep8 $PYTHON_SOURCE
pylint --reports=n $PYTHON_SOURCE 2>/dev/null
mypy --py2 $PYTHON_SOURCE

103
packet/packet.c Normal file
View File

@ -0,0 +1,103 @@
/*
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 <errno.h>
#include <stdio.h>
#include <unistd.h>
#include "wait.h"
/* Drop SUID privileges. To be used after accquiring raw sockets. */
static
void drop_suid_permissions(void)
{
if (setgid(getgid()) || setuid(getuid())) {
perror("Unable to drop suid permissions");
}
if (geteuid() != getuid() || getegid() != getgid()) {
perror("Unable to drop suid permissions");
}
}
int main(
int argc,
char **argv)
{
bool command_pipe_open;
int err;
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.
*/
drop_suid_permissions();
init_command_buffer(&command_buffer, fileno(stdin));
command_pipe_open = true;
/*
Dispatch commands and respond to probe replies until the
command stream is closed.
*/
while (true) {
/* Ensure any responses are written before waiting */
fflush(stdout);
wait_for_activity(&command_buffer, &net_state);
/*
Receive replies first so that the timestamps are as
close to the response arrival time as possible.
*/
receive_replies(&net_state);
if (command_pipe_open) {
err = read_commands(&command_buffer);
if (err == EPIPE)
{
command_pipe_open = false;
}
}
check_probe_timeouts(&net_state);
/*
Dispatch commands late so that the window between probe
departure and arriving replies is as small as possible.
*/
dispatch_buffer_commands(&command_buffer, &net_state);
/*
If the command pipe has been closed, exit after all
in-flight probes have reported their status.
*/
if (!command_pipe_open) {
if (count_in_flight_probes(&net_state) == 0) {
break;
}
}
}
return 0;
}

53
packet/platform.h Normal file
View File

@ -0,0 +1,53 @@
/*
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 PLATFORM_H
#define PLATFORM_H
/*
Determine the most appropriate PLATFORM_* define for our
current target.
*/
#if defined(__CYGWIN__)
#define PLATFORM_CYGWIN
#elif defined(__APPLE__) && defined(__MACH__)
#define PLATFORM_OS_X
#elif defined(__gnu_linux__)
#define PLATFORM_LINUX
#elif defined (__FreeBSD__)
#define PLATFORM_FREEBSD
#elif defined(__unix__)
#define PLATFORM_UNIX_UNKNOWN
#else
#error Unsupported platform
#endif
#endif

176
packet/probe.c Normal file
View File

@ -0,0 +1,176 @@
/*
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 "probe.h"
#include <arpa/inet.h>
#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include "platform.h"
#include "protocols.h"
#include "timeval.h"
#define IP_TEXT_LENGTH 32
/* Convert the destination address from text to sockaddr */
int decode_dest_addr(
const struct probe_param_t *param,
struct sockaddr_in *dest_sockaddr)
{
struct in_addr dest_addr;
if (param->ipv4_address == NULL) {
return EINVAL;
}
if (inet_pton(AF_INET, param->ipv4_address, &dest_addr) != 1) {
return EINVAL;
}
dest_sockaddr->sin_family = AF_INET;
dest_sockaddr->sin_port = 0;
dest_sockaddr->sin_addr = dest_addr;
return 0;
}
/* Allocate a structure for tracking a new probe */
struct probe_t *alloc_probe(
struct net_state_t *net_state,
int token)
{
int i;
struct probe_t *probe;
for (i = 0; i < MAX_PROBES; i++) {
probe = &net_state->probes[i];
if (!probe->used) {
memset(probe, 0, sizeof(struct probe_t));
probe->used = true;
probe->token = token;
return probe;
}
}
return NULL;
}
/* Mark a probe tracking structure as unused */
void free_probe(
struct probe_t *probe)
{
probe->used = false;
}
/*
Return the number of probes which haven't yet received a reply
and haven't yet timed out.
*/
int count_in_flight_probes(
struct net_state_t *net_state)
{
int i;
int count;
struct probe_t *probe;
count = 0;
for (i = 0; i < MAX_PROBES; i++) {
probe = &net_state->probes[i];
if (probe->used) {
count++;
}
}
return count;
}
/*
Find an existing probe structure by ICMP id and sequence number.
Returns NULL if non is found.
*/
struct probe_t *find_probe(
struct net_state_t *net_state,
int icmp_id,
int icmp_sequence)
{
int i;
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.
*/
if (icmp_id != htons(getpid())) {
return NULL;
}
for (i = 0; i < MAX_PROBES; i++) {
probe = &net_state->probes[i];
if (probe->used && htons(probe->token) == icmp_sequence) {
return probe;
}
}
return NULL;
}
/*
After a probe reply has arrived, respond to the command request which
sent the probe.
*/
void respond_to_probe(
struct probe_t *probe,
int icmp_type,
struct sockaddr_in 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);
}
if (icmp_type == ICMP_TIME_EXCEEDED) {
result = "ttl-expired";
} else {
assert(icmp_type == ICMP_ECHOREPLY);
result = "reply";
}
printf(
"%d %s ip-4 %s round-trip-time %d\n",
probe->token, result, ip_text, round_trip_us);
free_probe(probe);
}

117
packet/probe.h Normal file
View File

@ -0,0 +1,117 @@
/*
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 PROBE_H
#define PROBE_H
#include "platform.h"
#include <netinet/in.h>
#include <stdbool.h>
#include <sys/time.h>
#ifdef PLATFORM_CYGWIN
#include "probe_cygwin.h"
#else
#include "probe_unix.h"
#endif
#define MAX_PROBES 1024
/* Parameters for sending a new probe */
struct probe_param_t
{
/* The command token used to identify a probe when it is completed */
int command_token;
/* The IP address to probe */
const char *ipv4_address;
/* Time to live for the transmited probe */
int ttl;
/* The number of seconds to wait before assuming the probe was lost */
int timeout;
};
/* Tracking information for an outstanding probe */
struct probe_t
{
/* true if this entry is in use */
bool used;
/* Command token of the probe request */
int token;
/* Platform specific probe tracking */
struct probe_platform_t platform;
};
/* Global state for interacting with the network */
struct net_state_t
{
/* Tracking information for in-flight probes */
struct probe_t probes[MAX_PROBES];
/* Platform specific tracking information */
struct net_state_platform_t platform;
};
void init_net_state(
struct net_state_t *net_state);
bool get_next_probe_timeout(
const struct net_state_t *net_state,
struct timeval *timeout);
void send_probe(
struct net_state_t *net_state,
const struct probe_param_t *param);
void receive_replies(
struct net_state_t *net_state);
void check_probe_timeouts(
struct net_state_t *net_state);
void respond_to_probe(
struct probe_t *probe,
int icmp_type,
struct sockaddr_in remote_addr,
unsigned int round_trip_us);
int decode_dest_addr(
const struct probe_param_t *param,
struct sockaddr_in *dest_sockaddr);
struct probe_t *alloc_probe(
struct net_state_t *net_state,
int token);
void free_probe(
struct probe_t *probe);
int count_in_flight_probes(
struct net_state_t *net_state);
struct probe_t *find_probe(
struct net_state_t *net_state,
int icmp_id,
int icmp_sequence);
#endif

181
packet/probe_cygwin.c Normal file
View File

@ -0,0 +1,181 @@
/*
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 "probe.h"
#include <stdio.h>
#include <winternl.h>
#include "protocols.h"
/* 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());
exit(1);
}
}
/*
The overlapped I/O style completion routine to be called by
Windows during an altertable wait when an ICMP probe has
completed, either by reply, or by ICMP.DLL timeout.
*/
static
void WINAPI on_icmp_reply(
PVOID context,
PIO_STATUS_BLOCK status,
ULONG reserved)
{
struct probe_t *probe = (struct probe_t *)context;
int icmp_type;
int round_trip_us;
int reply_count;
int err;
struct sockaddr_in remote_addr;
ICMP_ECHO_REPLY32 *reply;
reply_count = IcmpParseReplies(
&probe->platform.reply, sizeof(ICMP_ECHO_REPLY));
if (reply_count == 0) {
err = GetLastError();
/* It could be that we got no reply because of timeout */
if (err == IP_REQ_TIMED_OUT) {
printf("%d no-reply\n", probe->token);
free_probe(probe);
return;
}
fprintf(stderr, "IcmpParseReplies failure %d\n", err);
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) {
icmp_type = ICMP_ECHOREPLY;
} 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);
}
}
/* Send a new probe using ICMP.DLL's send echo mechanism */
void send_probe(
struct net_state_t *net_state,
const struct probe_param_t *param)
{
IP_OPTION_INFORMATION option;
DWORD send_result;
DWORD timeout;
struct probe_t *probe;
struct sockaddr_in dest_sockaddr;
if (decode_dest_addr(param, &dest_sockaddr)) {
printf("%d invalid-argument\n", param->command_token);
return;
}
if (param->timeout > 0) {
timeout = 1000 * param->timeout;
} else {
/*
IcmpSendEcho2 will return invalid argument on a timeout of
zero. Our Unix implementation allows it. Bump up the timeout
to 1 millisecond.
*/
timeout = 1;
}
probe = alloc_probe(net_state, param->command_token);
if (probe == NULL) {
printf("%d probes-exhausted\n", param->command_token);
return;
}
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 (send_result == 0) {
/*
ERROR_IO_PENDING is expected for asynchronous probes,
but any other error is unexpected.
*/
if (GetLastError() != ERROR_IO_PENDING) {
fprintf(stderr, "IcmpSendEcho2 failure %d\n", GetLastError());
exit(1);
}
}
}
/*
On Windows, an implementation of receive_replies is unnecessary, because,
unlike Unix, replies are completed using Overlapped I/O during an
alertable wait, and don't require explicit reads.
*/
void receive_replies(
struct net_state_t *net_state)
{
}
/*
On Windows, an implementation of check_probe_timeout is unnecesary because
timeouts are managed by ICMP.DLL, including a call to the I/O completion
routine when the time fully expires.
*/
void check_probe_timeouts(
struct net_state_t *net_state)
{
}
/*
As in the case of check_probe_timeout, getting the next probe timeout is
unnecessary under Windows, as ICMP.DLL manages timeouts for us.
*/
bool get_next_probe_timeout(
const struct net_state_t *net_state,
struct timeval *timeout)
{
return false;
}

42
packet/probe_cygwin.h Normal file
View File

@ -0,0 +1,42 @@
/*
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 PROBE_CYGWIN_H
#define PROBE_CYGWIN_H
#include <arpa/inet.h>
#include <windows.h>
#include <iphlpapi.h>
#include <icmpapi.h>
/*
Windows requires an echo reply structure for each in-flight
ICMP probe.
*/
struct probe_platform_t
{
ICMP_ECHO_REPLY32 reply;
};
/* A Windows HANDLE for the ICMP session */
struct net_state_platform_t
{
HANDLE icmp;
};
#endif

526
packet/probe_unix.c Normal file
View File

@ -0,0 +1,526 @@
/*
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 "probe.h"
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include "protocols.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. */
static
uint16_t compute_checksum(
const void *packet,
int size)
{
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);
}
/* 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 probe packet based on the probe parameters */
static
int construct_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)
{
struct IPHeader *ip;
struct ICMPHeader *icmp;
int packet_size;
int icmp_size;
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 (packet_buffer_size < packet_size) {
return -EINVAL;
}
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;
}
/*
Nearly all fields in the IP header should be encoded in network byte
order prior to passing to send(). However, the required byte order of
the length field of the IP header is inconsistent between operating
systems and operating system versions. FreeBSD 11 requires the length
field in network byte order, but some older versions of FreeBSD
require host byte order. OS X requires the length field in host
byte order. Linux will accept either byte order.
Test for a byte order which works by sending a ping to localhost.
*/
static
void check_length_order(
struct net_state_t *net_state)
{
char packet[PACKET_BUFFER_SIZE];
struct probe_param_t param;
struct sockaddr_in dest_sockaddr;
ssize_t bytes_sent;
int packet_size;
memset(&param, 0, sizeof(struct probe_param_t));
param.ttl = 255;
param.ipv4_address = "127.0.0.1";
if (decode_dest_addr(&param, &dest_sockaddr)) {
fprintf(stderr, "Error decoding localhost address\n");
exit(1);
}
/* First attempt to ping the localhost with network byte order */
net_state->platform.ip_length_host_order = false;
packet_size = construct_packet(
net_state, packet, PACKET_BUFFER_SIZE, dest_sockaddr, &param);
assert(packet_size > 0);
bytes_sent = sendto(
net_state->platform.ipv4_send_socket,
packet, packet_size, 0,
(struct sockaddr *)&dest_sockaddr,
sizeof(struct sockaddr_in));
if (bytes_sent > 0) {
return;
}
/* Since network byte order failed, try host byte order */
net_state->platform.ip_length_host_order = true;
packet_size = construct_packet(
net_state, packet, PACKET_BUFFER_SIZE, dest_sockaddr, &param);
assert(packet_size > 0);
bytes_sent = sendto(
net_state->platform.ipv4_send_socket,
packet, packet_size, 0,
(struct sockaddr *)&dest_sockaddr,
sizeof(struct sockaddr_in));
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(
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");
exit(1);
}
/*
We will be including the IP header in transmitted packets.
Linux doesn't require this, but BSD derived network stacks do.
*/
if (setsockopt(
send_socket, IPPROTO_IP, IP_HDRINCL, &trueopt, sizeof(int))) {
perror("Failure to set IP_HDRINCL");
exit(1);
}
/*
Open a second socket with IPPROTO_ICMP because we are only
interested in receiving ICMP packets, not all packets.
*/
recv_socket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (recv_socket == -1) {
perror("Failure opening raw socket");
exit(1);
}
flags = fcntl(recv_socket, F_GETFL, 0);
if (flags == -1) {
perror("Unexpected socket error");
exit(1);
}
/* Set the receive socket to be non-blocking */
if (fcntl(recv_socket, F_SETFL, flags | O_NONBLOCK)) {
perror("Unexpected socket error");
exit(1);
}
net_state->platform.ipv4_send_socket = send_socket;
net_state->platform.ipv4_recv_socket = recv_socket;
check_length_order(net_state);
}
/* Craft a custom ICMP packet for a network probe. */
void send_probe(
struct net_state_t *net_state,
const struct probe_param_t *param)
{
char packet[PACKET_BUFFER_SIZE];
struct sockaddr_in dest_sockaddr;
struct probe_t *probe;
int packet_size;
if (decode_dest_addr(param, &dest_sockaddr)) {
printf("%d invalid-argument\n", param->command_token);
return;
}
packet_size = construct_packet(
net_state, packet, PACKET_BUFFER_SIZE, dest_sockaddr, param);
if (packet_size < 0) {
printf("%d invalid-argument\n", param->command_token);
return;
}
probe = alloc_probe(net_state, param->command_token);
if (probe == NULL) {
printf("%d probes-exhausted\n", param->command_token);
return;
}
/*
We get the time just before the send call to keep the timing
as tight as possible.
*/
if (gettimeofday(&probe->platform.departure_time, NULL)) {
perror("gettimeofday failure");
exit(1);
}
if (sendto(
net_state->platform.ipv4_send_socket,
packet, packet_size, 0,
(struct sockaddr *)&dest_sockaddr,
sizeof(struct sockaddr_in)) == -1) {
perror("Failure sending probe");
exit(1);
}
probe->platform.timeout_time = probe->platform.departure_time;
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)
{
char packet[PACKET_BUFFER_SIZE];
int packet_length;
struct sockaddr_in remote_addr;
socklen_t sockaddr_length;
struct timeval timestamp;
/* Read until no more packets are available */
while (true) {
sockaddr_length = sizeof(struct sockaddr_in);
packet_length = recvfrom(
net_state->platform.ipv4_recv_socket,
packet, PACKET_BUFFER_SIZE, 0,
(struct sockaddr *)&remote_addr, &sockaddr_length);
/*
Get the time immediately after reading the packet to
keep the timing as precise as we can.
*/
if (gettimeofday(&timestamp, NULL)) {
perror("gettimeofday failure");
exit(1);
}
if (packet_length == -1) {
/*
EAGAIN will be returned if there is no current packet
available.
*/
if (errno == EAGAIN) {
return;
}
/*
EINTER will be returned if we received a signal during
receive.
*/
if (errno == EINTR) {
continue;
}
perror("Failure receiving replies");
exit(1);
}
handle_received_packet(
net_state, remote_addr, packet, packet_length, timestamp);
}
}
/*
Check for any probes for which we have not received a response
for some time, and report a time-out, assuming that we won't
receive a future reply.
*/
void check_probe_timeouts(
struct net_state_t *net_state)
{
struct timeval now;
struct probe_t *probe;
int i;
if (gettimeofday(&now, NULL)) {
perror("gettimeofday failure");
exit(1);
}
for (i = 0; i < MAX_PROBES; i++) {
probe = &net_state->probes[i];
/* Don't check probes which aren't currently outstanding */
if (!probe->used) {
continue;
}
if (compare_timeval(probe->platform.timeout_time, now) < 0) {
/* Report timeout to the command stream */
printf("%d no-reply\n", probe->token);
free_probe(probe);
}
}
}
/*
Find the remaining time until the next probe times out.
This may be a negative value if the next probe timeout has
already elapsed.
Returns false if no probes are currently outstanding, and true
if a timeout value for the next probe exists.
*/
bool get_next_probe_timeout(
const struct net_state_t *net_state,
struct timeval *timeout)
{
int i;
bool have_timeout;
const struct probe_t *probe;
struct timeval now;
struct timeval probe_timeout;
if (gettimeofday(&now, NULL)) {
perror("gettimeofday failure");
exit(1);
}
have_timeout = false;
for (i = 0; i < MAX_PROBES; i++) {
probe = &net_state->probes[i];
if (!probe->used) {
continue;
}
probe_timeout.tv_sec =
probe->platform.timeout_time.tv_sec - now.tv_sec;
probe_timeout.tv_usec =
probe->platform.timeout_time.tv_usec - now.tv_usec;
normalize_timeval(&probe_timeout);
if (have_timeout) {
if (compare_timeval(probe_timeout, *timeout) < 0) {
/* If this probe has a sooner timeout, store it instead */
*timeout = probe_timeout;
}
} else {
*timeout = probe_timeout;
have_timeout = true;
}
}
return have_timeout;
}

49
packet/probe_unix.h Normal file
View File

@ -0,0 +1,49 @@
/*
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 PROBE_UNIX_H
#define PROBE_UNIX_H
/* We need to track the transmission and timeouts on Unix systems */
struct probe_platform_t
{
/* The time at which the probe is considered lost */
struct timeval timeout_time;
/* The time at which the probe was sent */
struct timeval departure_time;
};
/* We'll use rack sockets to send and recieve probes on Unix systems */
struct net_state_platform_t
{
/* Socket used to send raw packets */
int ipv4_send_socket;
/* Socket used to receive ICMP replies */
int ipv4_recv_socket;
/*
true if we should encode the IP header length in host order.
(as opposed to network order)
*/
bool ip_length_host_order;
};
#endif

84
packet/protocols.h Normal file
View File

@ -0,0 +1,84 @@
/*
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 PROTOCOLS_H
#define PROTOCOLS_H
/* ICMP type codes */
#define ICMP_ECHOREPLY 0
#define ICMP_DEST_UNREACH 3
#define ICMP_ECHO 8
#define ICMP_TIME_EXCEEDED 11
/* We can't rely on header files to provide this information, because
the fields have different names between, for instance, Linux and
Solaris */
struct ICMPHeader {
uint8_t type;
uint8_t code;
uint16_t checksum;
uint16_t id;
uint16_t sequence;
};
/* Structure of an UDP header. */
struct UDPHeader {
uint16_t srcport;
uint16_t dstport;
uint16_t length;
uint16_t checksum;
};
/* Structure of an TCP header, as far as we need it. */
struct TCPHeader {
uint16_t srcport;
uint16_t dstport;
uint32_t seq;
};
/* Structure of an SCTP header */
struct SCTPHeader {
uint16_t srcport;
uint16_t dstport;
uint32_t veri_tag;
};
/* Structure of an IPv4 UDP pseudoheader. */
struct UDPv4PHeader {
uint32_t saddr;
uint32_t daddr;
uint8_t zero;
uint8_t protocol;
uint16_t len;
};
/* Structure of an IP header. */
struct IPHeader {
uint8_t version;
uint8_t tos;
uint16_t len;
uint16_t id;
uint16_t frag;
uint8_t ttl;
uint8_t protocol;
uint16_t check;
uint32_t saddr;
uint32_t daddr;
};
#endif

348
packet/testpacket.py Executable file
View File

@ -0,0 +1,348 @@
#!/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
def write_command(self, cmd): # type: (str) -> None
'Send a command string to the mtr-packet instance'
self.packet_process.stdin.write(cmd + '\n')
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)
try:
self.reply_buffer += os.read(self.stdout_fd, 1024)
except OSError:
pass
# 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()

77
packet/timeval.c Normal file
View File

@ -0,0 +1,77 @@
/*
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 "timeval.h"
/*
Ensure that a timevalue has a microsecond value in the range
[0.0, 1.0e6) microseconds by converting microseconds to full seconds.
*/
void normalize_timeval(
struct timeval *timeval)
{
int full_sec;
/*
If tv_usec has overflowed a full second, convert the overflow
to tv_sec.
*/
full_sec = timeval->tv_usec / 1000000;
timeval->tv_sec += full_sec;
timeval->tv_usec -= 1000000 * full_sec;
/* If tv_usec is negative, make it positive by rolling tv_sec back */
if (timeval->tv_usec < 0) {
timeval->tv_sec--;
timeval->tv_usec += 1000000;
}
/* If the entire time value is negative, clamp to zero */
if (timeval->tv_sec < 0) {
timeval->tv_sec = 0;
timeval->tv_usec = 0;
}
}
/*
Compare two time values. Return:
-1 if a < b
0 if a == b
1 if a > b
*/
int compare_timeval(
struct timeval a,
struct timeval b)
{
if (a.tv_sec > b.tv_sec) {
return 1;
}
if (a.tv_sec < b.tv_sec) {
return -1;
}
if (a.tv_usec > b.tv_usec) {
return 1;
}
if (a.tv_usec < b.tv_usec) {
return -1;
}
return 0;
}

31
packet/timeval.h Normal file
View File

@ -0,0 +1,31 @@
/*
mtr -- a network diagnostic tool
Copyright (C) 2016 Matt Kimball
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2 as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#ifndef TIMEVAL_H
#define TIMEVAL_H
#include <sys/time.h>
void normalize_timeval(
struct timeval *timeval);
int compare_timeval(
struct timeval a,
struct timeval b);
#endif

29
packet/wait.h Normal file
View File

@ -0,0 +1,29 @@
/*
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 WAIT_H
#define WAIT_H
#include "command.h"
#include "probe.h"
void wait_for_activity(
const struct command_buffer_t *command_buffer,
const struct net_state_t *net_state);
#endif

44
packet/wait_cygwin.c Normal file
View File

@ -0,0 +1,44 @@
/*
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 "wait.h"
#include <io.h>
#include <stdio.h>
#include <windows.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
we will sleep with an alertable wait, as all of these conditions
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)
{
DWORD wait_result;
/* Sleep until an I/O completion routine runs */
wait_result = SleepEx(INFINITE, TRUE);
if (wait_result == WAIT_FAILED) {
fprintf(stderr, "SleepEx failure %d\n", GetLastError());
exit(1);
}
}

86
packet/wait_unix.c Normal file
View File

@ -0,0 +1,86 @@
/*
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 "wait.h"
#include <assert.h>
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
/*
Sleep until we receive a new probe response, a new command on the
command stream, or a probe timeout. On Unix systems, this means
we use select to wait on file descriptors for the command stream
and the raw recieve socket.
*/
void wait_for_activity(
const struct command_buffer_t *command_buffer,
const struct net_state_t *net_state)
{
int nfds;
fd_set read_set;
struct timeval probe_timeout;
struct timeval *select_timeout;
int ready_count;
int command_stream = command_buffer->command_stream;
int socket = net_state->platform.ipv4_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;
}
while (true) {
select_timeout = NULL;
/* Use the soonest probe timeout time as our maximum wait time */
if (get_next_probe_timeout(net_state, &probe_timeout)) {
assert(probe_timeout.tv_sec >= 0);
select_timeout = &probe_timeout;
}
ready_count = select(nfds, &read_set, NULL, NULL, select_timeout);
/*
If we didn't have an error, either one of our descriptors is
readable, or we timed out. So we can now return.
*/
if (ready_count != -1) {
break;
}
/*
We will get EINTR if we received a signal during the select, so
retry in that case. We may get EAGAIN if "the kernel was
(perhaps temporarily) unable to allocate the requested number of
file descriptors." I haven't seen this in practice, but selecting
again seems like the right thing to do.
*/
if (errno != EINTR && errno != EAGAIN) {
/* We don't expect other errors, so report them */
perror("unexpected select error");
exit(1);
}
}
}

View File

@ -254,10 +254,6 @@ extern void select_loop(struct mtr_ctl *ctl){
}
anyset = 1;
}
/* Check for activity on open sockets */
if (ctl->mtrtype == IPPROTO_TCP)
net_process_fds(ctl, &writefd);
}
return;
}