/*
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.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "cmdpipe.h"
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/wait.h>
#include <unistd.h>
#ifdef HAVE_ERROR_H
#include <error.h>
#else
#include "portability/error.h"
#endif
#include "packet/cmdparse.h"
#include "display.h"
/* Set a file descriptor to non-blocking */
static
void set_fd_nonblock(
int fd)
{
int flags;
/* Get the current flags of the file descriptor */
flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) {
error(EXIT_FAILURE, errno, "F_GETFL failure");
exit(1);
}
/* Add the O_NONBLOCK bit to the current flags */
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
error(EXIT_FAILURE, errno, "Failure to set O_NONBLOCK");
exit(1);
}
}
/*
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 send_synchronous_command(
struct mtr_ctl *ctl,
struct packet_command_pipe_t *cmdpipe,
const char *cmd,
struct command_t *result,
char *reply)
{
int command_length;
int write_length;
int read_length;
/* Query send-probe support */
command_length = strlen(cmd);
write_length = write(cmdpipe->write_fd, cmd, command_length);
if (write_length == -1) {
return -1;
}
if (write_length != command_length) {
errno = EIO;
return -1;
}
/* Read the reply to our query */
read_length =
read(cmdpipe->read_fd, reply, PACKET_REPLY_BUFFER_SIZE - 1);
if (read_length < 0) {
return -1;
}
/* Parse the query reply */
reply[read_length] = 0;
if (parse_command(result, reply)) {
return -1;
}
return 0;
}
/* Check support for a particular feature with the mtr-packet we invoked */
static
int check_feature(
struct mtr_ctl *ctl,
struct packet_command_pipe_t *cmdpipe,
const char *feature)
{
char check_command[COMMAND_BUFFER_SIZE];
struct command_t reply;
char reply_buf[PACKET_REPLY_BUFFER_SIZE];
snprintf(check_command, COMMAND_BUFFER_SIZE,
"1 check-support feature %s\n", feature);
if (send_synchronous_command(ctl, cmdpipe, check_command, &reply, reply_buf) ==
-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;
}
errno = ENOTSUP;
return -1;
}
/*
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 check_packet_features(
struct mtr_ctl *ctl,
struct packet_command_pipe_t *cmdpipe)
{
/* Check the IP protocol version */
if (ctl->af == AF_INET6) {
if (check_feature(ctl, cmdpipe, "ip-6")) {
return -1;
}
} else if (ctl->af == AF_INET) {
if (check_feature(ctl, cmdpipe, "ip-4")) {
return -1;
}
} else {
errno = EINVAL;
return -1;
}
/* Check the transport protocol */
if (ctl->mtrtype == IPPROTO_ICMP) {
if (check_feature(ctl, cmdpipe, "icmp")) {
return -1;
}
} else if (ctl->mtrtype == IPPROTO_UDP) {
if (check_feature(ctl, cmdpipe, "udp")) {
return -1;
}
} else if (ctl->mtrtype == IPPROTO_TCP) {
if (check_feature(ctl, cmdpipe, "tcp")) {
return -1;
}
#ifdef HAS_SCTP
} else if (ctl->mtrtype == IPPROTO_SCTP) {
if (check_feature(ctl, cmdpipe, "sctp")) {
return -1;
}
#endif
} else {
errno = EINVAL;
return -1;
}
#ifdef SO_MARK
if (ctl->mark) {
if (check_feature(ctl, cmdpipe, "mark")) {
return -1;
}
}
#endif
return 0;
}
extern char *myname;
/*
Execute mtr-packet, allowing the MTR_PACKET environment to override
the PATH when locating the executable.
*/
static
void execute_packet_child(
void)
{
char buf[256];
/*
Allow the MTR_PACKET environment variable to override
the path to the mtr-packet executable. This is necessary
for debugging changes for mtr-packet.
*/
char *mtr_packet_path = getenv("MTR_PACKET");
if (mtr_packet_path == NULL) {
mtr_packet_path = "mtr-packet";
}
/*
First, try to execute mtr-packet from PATH
or MTR_PACKET environment variable.
*/
execlp(mtr_packet_path, "mtr-packet", (char *) NULL);
/*
Then try to find it where WE were executed from.
*/
strncpy (buf, myname, 240);
strcat (buf, "-packet");
mtr_packet_path = buf;
execl(mtr_packet_path, "mtr-packet", (char *) NULL);
/*
If mtr-packet is not found, try to use mtr-packet from current directory
*/
execl("./mtr-packet", "./mtr-packet", (char *) NULL);
/* Both exec attempts failed, so nothing to do but exit */
exit(1);
}
/* Create the command pipe to a new mtr-packet subprocess */
int open_command_pipe(
struct mtr_ctl *ctl,
struct packet_command_pipe_t *cmdpipe)
{
int stdin_pipe[2];
int stdout_pipe[2];
pid_t child_pid;
int i;
/*
We actually need two Unix pipes. One for stdin and one for
stdout on the new process.
*/
if (pipe(stdin_pipe) || pipe(stdout_pipe)) {
return errno;
}
child_pid = fork();
if (child_pid == -1) {
return errno;
}
if (child_pid == 0) {
/*
In the child process, attach our created pipes to stdin
and stdout
*/
dup2(stdin_pipe[0], STDIN_FILENO);
dup2(stdout_pipe[1], STDOUT_FILENO);
/* Close all unnecessary fds */
for (i = STDERR_FILENO + 1; i <= stdout_pipe[1]; i++) {
close(i);
}
execute_packet_child();
} else {
memset(cmdpipe, 0, sizeof(struct packet_command_pipe_t));
/*
In the parent process, save the opposite ends of the pipes
attached as stdin and stdout in the child.
*/
cmdpipe->pid = child_pid;
cmdpipe->read_fd = stdout_pipe[0];
cmdpipe->write_fd = stdin_pipe[1];
/* We don't need the child ends of the pipe open in the parent. */
close(stdout_pipe[1]);
close(stdin_pipe[0]);
/*
Check that we can communicate with the client. If we failed to
execute the mtr-packet binary, we will discover that here.
*/
if (check_feature(ctl, cmdpipe, "send-probe")) {
error(EXIT_FAILURE, errno, "Failure to start mtr-packet");
}
if (check_packet_features(ctl, cmdpipe)) {
error(EXIT_FAILURE, errno, "Packet type unsupported");
}
/* We will need non-blocking reads from the child */
set_fd_nonblock(cmdpipe->read_fd);
}
return 0;
}
/* Kill the mtr-packet child process and close the command pipe */
void close_command_pipe(
struct packet_command_pipe_t *cmdpipe)
{
int child_exit_value;
if (cmdpipe->pid) {
close(cmdpipe->read_fd);
close(cmdpipe->write_fd);
kill(cmdpipe->pid, SIGTERM);
waitpid(cmdpipe->pid, &child_exit_value, 0);
}
memset(cmdpipe, 0, sizeof(struct packet_command_pipe_t));
}
/* Start building the command string for the "send-probe" command */
static
void construct_base_command(
struct mtr_ctl *ctl,
char *command,
int buffer_size,
int command_token,
ip_t * address,
ip_t * localaddress)
{
char ip_string[INET6_ADDRSTRLEN];
char local_ip_string[INET6_ADDRSTRLEN];
const char *ip_type;
const char *local_ip_type;
const char *protocol = NULL;
/* Convert the remote IP address to a string */
if (inet_ntop(ctl->af, address, ip_string, INET6_ADDRSTRLEN) == NULL) {
display_close(ctl);
error(EXIT_FAILURE, errno, "invalid remote IP address");
}
if (inet_ntop(ctl->af, localaddress,
local_ip_string, INET6_ADDRSTRLEN) == NULL) {
display_close(ctl);
error(EXIT_FAILURE, errno, "invalid local IP address");
}
if (ctl->af == AF_INET6) {
ip_type = "ip-6";
local_ip_type = "local-ip-6";
} else {
ip_type = "ip-4";
local_ip_type = "local-ip-4";
}
if (ctl->mtrtype == IPPROTO_ICMP) {
protocol = "icmp";
} else if (ctl->mtrtype == IPPROTO_UDP) {
protocol = "udp";
} else if (ctl->mtrtype == IPPROTO_TCP) {
protocol = "tcp";
#ifdef HAS_SCTP
} else if (ctl->mtrtype == IPPROTO_SCTP) {
protocol = "sctp";
#endif
} else {
display_close(ctl);
error(EXIT_FAILURE, 0,
"protocol unsupported by mtr-packet interface");
}
snprintf(command, buffer_size,
"%d send-probe %s %s %s %s protocol %s",
command_token,
ip_type, ip_string, local_ip_type, local_ip_string, protocol);
}
/* Append an argument to the "send-probe" command string */
static
void append_command_argument(
char *command,
int buffer_size,
char *name,
int value)
{
char argument[COMMAND_BUFFER_SIZE];
int remaining_size;
remaining_size = buffer_size - strlen(command) - 1;
snprintf(argument, buffer_size, " %s %d", name, value);
strncat(command, argument, remaining_size);
}
/* Request a new probe from the "mtr-packet" child process */
void send_probe_command(
struct mtr_ctl *ctl,
struct packet_command_pipe_t *cmdpipe,
ip_t * address,
ip_t * localaddress,
int packet_size,
int sequence,
int time_to_live)
{
char command[COMMAND_BUFFER_SIZE];
int remaining_size;
int timeout;
construct_base_command(ctl, command, COMMAND_BUFFER_SIZE, sequence,
address, localaddress);
append_command_argument(command, COMMAND_BUFFER_SIZE, "size",
packet_size);
append_command_argument(command, COMMAND_BUFFER_SIZE, "bit-pattern",
ctl->bitpattern);
append_command_argument(command, COMMAND_BUFFER_SIZE, "tos", ctl->tos);
append_command_argument(command, COMMAND_BUFFER_SIZE, "ttl",
time_to_live);
timeout = ctl->probe_timeout / 1000000;
append_command_argument(command, COMMAND_BUFFER_SIZE, "timeout",
timeout);
if (ctl->remoteport) {
append_command_argument(command, COMMAND_BUFFER_SIZE, "port",
ctl->remoteport);
}
if (ctl->localport) {
append_command_argument(command, COMMAND_BUFFER_SIZE, "local-port",
ctl->localport);
}
#ifdef SO_MARK
if (ctl->mark) {
append_command_argument(command, COMMAND_BUFFER_SIZE, "mark",
ctl->mark);
}
#endif
remaining_size = COMMAND_BUFFER_SIZE - strlen(command) - 1;
strncat(command, "\n", remaining_size);
/* Send a probe using the mtr-packet subprocess */
if (write(cmdpipe->write_fd, command, strlen(command)) == -1) {
display_close(ctl);
error(EXIT_FAILURE, errno,
"mtr-packet command pipe write failure");
}
}
/*
Parse a comma separated field of mpls values, filling out the mplslen
structure with mpls labels.
*/
static
void parse_mpls_values(
struct mplslen *mpls,
char *value_str)
{
char *next_value = value_str;
char *end_of_value;
int value;
int label_count = 0;
int label_field = 0;
while (*next_value) {
value = strtol(next_value, &end_of_value, 10);
/* Failure to advance means an invalid numeric value */
if (end_of_value == next_value) {
return;
}
/* If the next character is not a comma or a NUL, we have
an invalid string */
if (*end_of_value == ',') {
next_value = end_of_value + 1;
} else if (*end_of_value == 0) {
next_value = end_of_value;
} else {
return;
}
/*
Store the converted value in the next field of the MPLS
structure.
*/
if (label_field == 0) {
mpls->label[label_count] = value;
} else if (label_field == 1) {
mpls->tc[label_count] = value;
} else if (label_field == 2) {
mpls->s[label_count] = value;
} else if (label_field == 3) {
mpls->ttl[label_count] = value;
}
label_field++;
if (label_field > 3) {
label_field = 0;
label_count++;
}
/*
If we've used up all MPLS labels in the structure, return with
what we've got
*/
if (label_count >= MAXLABELS) {
break;
}
}
mpls->labels = label_count;
}
/*
Extract the IP address and round trip time from a reply to a probe.
Returns true if both arguments are found in the reply, false otherwise.
*/
static
bool parse_reply_arguments(
struct mtr_ctl *ctl,
struct command_t *reply,
ip_t * fromaddress,
int *round_trip_time,
struct mplslen *mpls)
{
bool found_round_trip;
bool found_ip;
char *arg_name;
char *arg_value;
int i;
*round_trip_time = 0;
memset(fromaddress, 0, sizeof(ip_t));
memset(mpls, 0, sizeof(struct mplslen));
found_ip = false;
found_round_trip = false;
/* Examine the reply arguments for known values */
for (i = 0; i < reply->argument_count; i++) {
arg_name = reply->argument_name[i];
arg_value = reply->argument_value[i];
if (ctl->af == AF_INET6) {
/* IPv6 address of the responding host */
if (!strcmp(arg_name, "ip-6")) {
if (inet_pton(AF_INET6, arg_value, fromaddress)) {
found_ip = true;
}
}
} else {
/* IPv4 address of the responding host */
if (!strcmp(arg_name, "ip-4")) {
if (inet_pton(AF_INET, arg_value, fromaddress)) {
found_ip = true;
}
}
}
/* The round trip time in microseconds */
if (!strcmp(arg_name, "round-trip-time")) {
errno = 0;
*round_trip_time = strtol(arg_value, NULL, 10);
if (!errno) {
found_round_trip = true;
}
}
/* MPLS labels */
if (!strcmp(arg_name, "mpls")) {
parse_mpls_values(mpls, arg_value);
}
}
return found_ip && found_round_trip;
}
/*
If an mtr-packet command has returned an error result,
report the error and exit.
*/
static
void handle_reply_errors(
struct mtr_ctl *ctl,
struct command_t *reply)
{
char *reply_name;
reply_name = reply->command_name;
if (!strcmp(reply_name, "probes-exhausted")) {
display_close(ctl);
error(EXIT_FAILURE, 0, "Probes exhausted");
}
if (!strcmp(reply_name, "invalid-argument")) {
display_close(ctl);
error(EXIT_FAILURE, 0, "mtr-packet reported invalid argument");
}
if (!strcmp(reply_name, "permission-denied")) {
display_close(ctl);
error(EXIT_FAILURE, 0, "Permission denied");
}
if (!strcmp(reply_name, "address-in-use")) {
display_close(ctl);
error(EXIT_FAILURE, 0, "Address in use");
}
if (!strcmp(reply_name, "address-not-available")) {
display_close(ctl);
error(EXIT_FAILURE, 0, "Address not available");
}
if (!strcmp(reply_name, "unexpected-error")) {
display_close(ctl);
error(EXIT_FAILURE, 0, "Unexpected mtr-packet error");
}
}
/*
A complete mtr-packet reply line has arrived. Parse it and record
the responding IP and round trip time, if it is a reply that we
understand.
*/
static
void handle_command_reply(
struct mtr_ctl *ctl,
char *reply_str,
probe_reply_func_t reply_func)
{
struct command_t reply;
ip_t fromaddress;
int seq_num;
int err;
int round_trip_time;
char *reply_name;
struct mplslen mpls;
/* Parse the reply string */
if (parse_command(&reply, reply_str)) {
/*
If the reply isn't well structured, something is fundamentally
wrong, as we might as well exit. Even if the reply is of an
unknown type, it should still parse.
*/
display_close(ctl);
error(EXIT_FAILURE, errno, "reply parse failure");
return;
}
handle_reply_errors(ctl, &reply);
seq_num = reply.token;
reply_name = reply.command_name;
/* Check for known reply types. */
if (!strcmp(reply_name, "reply")
|| !strcmp(reply_name, "ttl-expired")) {
err = 0;
} else if (!strcmp(reply_name, "no-route")) {
err = ENETUNREACH;
} else if (!strcmp(reply_name, "network-down")) {
err = ENETDOWN;
} else {
/* If the reply type is unknown, ignore it */
return;
}
/*
If the reply had an IP address and a round trip time, we can
record the result.
*/
if (parse_reply_arguments
(ctl, &reply, &fromaddress, &round_trip_time, &mpls)) {
reply_func(ctl, seq_num, err, &mpls, (void *) &fromaddress,
round_trip_time);
}
}
/*
Check the command pipe for completed replies to commands
we have previously sent. Record the results of those replies.
*/
static
void consume_reply_buffer(
struct mtr_ctl *ctl,
struct packet_command_pipe_t *cmdpipe,
probe_reply_func_t reply_func)
{
char *reply_buffer;
char *reply_start;
char *end_of_reply;
int used_size;
int move_size;
reply_buffer = cmdpipe->reply_buffer;
/* Terminate the string storing the replies */
assert(cmdpipe->reply_buffer_used < PACKET_REPLY_BUFFER_SIZE);
reply_buffer[cmdpipe->reply_buffer_used] = 0;
reply_start = reply_buffer;
/*
We may have multiple completed replies. Loop until we don't
have any more newlines terminating replies.
*/
while (true) {
/* If no newline is found, our reply isn't yet complete */
end_of_reply = index(reply_start, '\n');
if (end_of_reply == NULL) {
/* No complete replies remaining */
break;
}
/*
Terminate the reply string at the newline, which
is necessary in the case where we are able to read
multiple replies arriving simultaneously.
*/
*end_of_reply = 0;
/* Parse and record the reply results */
handle_command_reply(ctl, reply_start, reply_func);
reply_start = end_of_reply + 1;
}
/*
After replies have been processed, free the space used
by the replies, and move any remaining partial reply text
to the start of the reply buffer.
*/
used_size = reply_start - reply_buffer;
move_size = cmdpipe->reply_buffer_used - used_size;
memmove(reply_buffer, reply_start, move_size);
cmdpipe->reply_buffer_used -= used_size;
if (cmdpipe->reply_buffer_used >= PACKET_REPLY_BUFFER_SIZE - 1) {
/*
We've overflowed the reply buffer without a complete reply.
There's not much we can do about it but discard the data
we've got and hope new data coming in fits.
*/
cmdpipe->reply_buffer_used = 0;
}
}
/*
Read as much as we can from the reply pipe from the child process, and
process as many replies as are available.
*/
void handle_command_replies(
struct mtr_ctl *ctl,
struct packet_command_pipe_t *cmdpipe,
probe_reply_func_t reply_func)
{
int read_count;
int buffer_remaining;
char *reply_buffer;
char *read_buffer;
reply_buffer = cmdpipe->reply_buffer;
/*
Read the available reply text, up to the remaining
buffer space. (Minus one for the terminating NUL.)
*/
read_buffer = &reply_buffer[cmdpipe->reply_buffer_used];
buffer_remaining =
PACKET_REPLY_BUFFER_SIZE - cmdpipe->reply_buffer_used;
read_count = read(cmdpipe->read_fd, read_buffer, buffer_remaining - 1);
if (read_count < 0) {
/*
EAGAIN simply indicates that there is no data currently
available on our non-blocking pipe.
*/
if (errno == EAGAIN) {
return;
}
display_close(ctl);
error(EXIT_FAILURE, errno, "command reply read failure");
return;
}
if (read_count == 0) {
display_close(ctl);
errno = EPIPE;
error(EXIT_FAILURE, EPIPE, "unexpected packet generator exit");
}
cmdpipe->reply_buffer_used += read_count;
/* Handle any replies completed by this read */
consume_reply_buffer(ctl, cmdpipe, reply_func);
}
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>