src/common/tcpdump.c
/* $Id$ */
/*
* Copyright (c) 2001-2010 Aaron Turner <aturner at synfin dot net>
* Copyright (c) 2013-2022 Fred Klassen <tcpreplay at appneta dot com> - AppNeta
*
* The Tcpreplay Suite of tools is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or with the authors permission any later version.
*
* The Tcpreplay Suite 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 the Tcpreplay Suite. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* This code allows us to use tcpdump to print packet decodes.
* Basically, we create a local AF_UNIX socketpair, fork a copy
* of ourselves, link 1/2 of the pair to STDIN of the child and
* replace the child with tcpdump. We then send a "pcap" file
* over the socket so that tcpdump can print it's decode to STDOUT.
*
* Idea and a lot of code stolen from Christain Kreibich's
* <christian@whoop.org> libnetdude 0.4 code. Any bugs are mine. :)
*
* This product includes software developed by the University of California,
* Lawrence Berkeley Laboratory and its contributors
*/
#include "defines.h"
#include "config.h"
#include "common.h"
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#ifdef HAVE_SIGNAL_H
#include <signal.h>
#endif
#include "tcpdump.h"
char *options_vec[OPTIONS_VEC_SIZE];
static int tcpdump_fill_in_options(char *opt);
static int can_exec(const char *filename);
/**
* given a packet, print a decode of via tcpdump
*/
int
tcpdump_print(tcpdump_t *tcpdump, struct pcap_pkthdr *pkthdr, const u_char *data)
{
struct pollfd poller;
int res, total;
char decode[TCPDUMP_DECODE_LEN];
struct compact_pkthdr {
struct {
uint32_t ts_sec;
uint32_t ts_usec;
} ts;
uint32_t caplen; /* length of portion present */
uint32_t len; /* length this packet (off wire) */
} actual_pkthdr;
assert(tcpdump);
assert(pkthdr);
assert(data);
/* convert header to file-format packet header */
actual_pkthdr.ts.ts_sec = (uint32_t)pkthdr->ts.tv_sec;
actual_pkthdr.ts.ts_usec = (uint32_t)pkthdr->ts.tv_sec;
actual_pkthdr.caplen = pkthdr->caplen;
actual_pkthdr.len = pkthdr->len;
total = 0;
header_again:
poller.fd = PARENT_WRITE_FD;
poller.events = POLLOUT;
poller.revents = 0;
/* wait until we can write the header to the tcpdump pipe */
res = poll(&poller, 1, TCPDUMP_POLL_TIMEOUT);
if (res < 0)
errx(-1,
"Error writing header to fd %d during poll() to write to tcpdump\n%s",
PARENT_WRITE_FD,
strerror(errno));
if (res == 0)
err(-1,
"poll() timeout... tcpdump seems to be having a problem keeping up\n"
"Try increasing TCPDUMP_POLL_TIMEOUT");
#ifdef DEBUG
if (debug >= 5) {
if (write(tcpdump->debugfd, (char *)&actual_pkthdr, sizeof(actual_pkthdr)) != sizeof(actual_pkthdr))
errx(-1, "Error writing pcap file header to tcpdump debug\n%s", strerror(errno));
}
#endif
/* res > 0 if we get here */
while (total != sizeof(actual_pkthdr) &&
(res = (int)write(PARENT_WRITE_FD, &actual_pkthdr + total, sizeof(actual_pkthdr) - total))) {
if (res < 0) {
if (errno == EAGAIN)
goto header_again;
errx(-1, "Error writing pcap file header to tcpdump\n%s", strerror(errno));
}
total += res;
}
total = 0;
data_again:
/* wait until we can write data to the tcpdump pipe */
poller.fd = PARENT_WRITE_FD;
poller.events = POLLOUT;
poller.revents = 0;
res = poll(&poller, 1, TCPDUMP_POLL_TIMEOUT);
if (res < 0)
errx(-1, "Error writing to fd %d during poll() to write to tcpdump\n%s", PARENT_WRITE_FD, strerror(errno));
if (res == 0)
err(-1,
"poll() timeout... tcpdump seems to be having a problem keeping up\n"
"Try increasing TCPDUMP_POLL_TIMEOUT");
#ifdef DEBUG
if (debug >= 5) {
if (write(tcpdump->debugfd, data, pkthdr->caplen) != (ssize_t)pkthdr->caplen)
errx(-1, "Error writing packet data to tcpdump debug\n%s", strerror(errno));
}
#endif
while (total != (ssize_t)pkthdr->caplen &&
(res = (int)write(PARENT_WRITE_FD, data + total, pkthdr->caplen - total))) {
if (res < 0) {
if (errno == EAGAIN)
goto data_again;
errx(-1, "Error writing packet data to tcpdump\n%s", strerror(errno));
}
total += res;
}
/* Wait for output from tcpdump */
poller.fd = PARENT_READ_FD;
poller.events = POLLIN;
poller.revents = 0;
res = poll(&poller, 1, TCPDUMP_POLL_TIMEOUT);
if (res < 0)
errx(-1, "Error out to fd %d during poll() to read from tcpdump\n%s", PARENT_READ_FD, strerror(errno));
if (res == 0)
err(-1,
"poll() timeout... tcpdump seems to be having a problem keeping up\n"
"Try increasing TCPDUMP_POLL_TIMEOUT");
while ((res = (int)read(PARENT_READ_FD, decode, TCPDUMP_DECODE_LEN))) {
if (res < 0) {
if (errno == EAGAIN)
break;
errx(-1, "Error reading tcpdump decode: %s", strerror(errno));
}
decode[min(res, TCPDUMP_DECODE_LEN - 1)] = 0;
dbgx(4, "read %d byte from tcpdump", res);
printf("%s", decode);
}
return TRUE;
}
/**
* init our tcpdump handle using the given pcap handle
* Basically, this starts up tcpdump as a child and communicates
* to it via a pair of sockets (stdout/stdin)
*/
int
tcpdump_open(tcpdump_t *tcpdump, pcap_t *pcap)
{
assert(tcpdump);
assert(pcap);
if (tcpdump->pid != 0) {
warn("tcpdump process already running");
return FALSE;
}
/* is tcpdump executable? */
if (!can_exec(TCPDUMP_BINARY)) {
errx(-1, "Unable to execute tcpdump binary: %s", TCPDUMP_BINARY);
}
#ifdef DEBUG
strlcpy(tcpdump->debugfile, TCPDUMP_DEBUG, sizeof(tcpdump->debugfile));
if (debug >= 5) {
dbgx(5, "Opening tcpdump debug file: %s", tcpdump->debugfile);
if ((tcpdump->debugfd =
open(tcpdump->debugfile, O_WRONLY | O_CREAT | O_TRUNC, S_IREAD | S_IWRITE | S_IRGRP | S_IROTH)) ==
-1) {
errx(-1, "Error opening tcpdump debug file: %s\n%s", tcpdump->debugfile, strerror(errno));
}
}
#endif
/* copy over the args */
dbg(2, "Prepping tcpdump options...");
tcpdump_fill_in_options(tcpdump->args);
dbg(2, "Starting tcpdump...");
/* create our pipe to send packet data to tcpdump via */
if (pipe(tcpdump->pipes[PARENT_READ_PIPE]) < 0 || pipe(tcpdump->pipes[PARENT_WRITE_PIPE]) < 0)
errx(-1, "Unable to create pipe: %s", strerror(errno));
if ((tcpdump->pid = fork()) < 0)
errx(-1, "Fork failed: %s", strerror(errno));
dbgx(2, "tcpdump pid: %d", tcpdump->pid);
if (tcpdump->pid > 0) {
/* parent - we're still in tcpreplay */
/* close fds not required by parent */
dbgx(2, "[parent] closing child read/write fd %d/%d", CHILD_READ_FD, CHILD_WRITE_FD);
close(CHILD_READ_FD);
close(CHILD_WRITE_FD);
CHILD_READ_FD = 0;
CHILD_WRITE_FD = 0;
/* send the pcap file header to tcpdump */
FILE *writer = fdopen(PARENT_WRITE_FD, "w");
if ((pcap_dump_fopen(pcap, writer)) == NULL) {
warnx("[parent] pcap_dump_fopen(): %s", pcap_geterr(pcap));
return FALSE;
}
pcap_dump_flush((pcap_dumper_t *)writer);
if (fcntl(PARENT_WRITE_FD, F_SETFL, O_NONBLOCK) < 0)
warnx("[parent] Unable to fcntl write pipe:\n%s", strerror(errno));
if (fcntl(PARENT_READ_FD, F_SETFL, O_NONBLOCK) < 0)
warnx("[parent] Unable to fnctl read pip:\n%s", strerror(errno));
} else {
dbg(2, "[child] started the kid");
/* we're in the child process - run "tcpdump <options> -r -" */
if (dup2(CHILD_READ_FD, STDIN_FILENO) != STDIN_FILENO) {
errx(-1, "[child] Unable to duplicate socket to stdin: %s", strerror(errno));
}
if (dup2(CHILD_WRITE_FD, STDOUT_FILENO) != STDOUT_FILENO) {
errx(-1, "[child] Unable to duplicate socket to stdout: %s", strerror(errno));
}
/*
* Close sockets not required by child. The exec'ed program must
* not know that they ever existed.
*/
dbgx(2, "[child] closing in fds %d/%d/%d/%d", CHILD_READ_FD, CHILD_WRITE_FD, PARENT_READ_FD, PARENT_WRITE_FD);
close(CHILD_READ_FD);
close(CHILD_WRITE_FD);
close(PARENT_READ_FD);
close(PARENT_WRITE_FD);
/* exec tcpdump */
dbg(2, "[child] Exec'ing tcpdump...");
if (execv(TCPDUMP_BINARY, options_vec) < 0)
errx(-1, "Unable to exec tcpdump: %s", strerror(errno));
dbg(2, "[child] tcpdump done!");
}
return TRUE;
}
/**
* shutdown tcpdump
*/
void
tcpdump_close(tcpdump_t *tcpdump)
{
if (!tcpdump)
return;
if (tcpdump->pid <= 0)
return;
dbgx(2, "[parent] killing tcpdump pid: %d", tcpdump->pid);
kill(tcpdump->pid, SIGKILL);
close(PARENT_READ_FD);
close(PARENT_WRITE_FD);
if (waitpid(tcpdump->pid, NULL, 0) != tcpdump->pid)
errx(-1, "[parent] Error in waitpid: %s", strerror(errno));
tcpdump->pid = 0;
PARENT_READ_FD = 0;
PARENT_WRITE_FD = 0;
}
/**
* copy the string of args (*opt) to the vector (**opt_vec)
* for a max of opt_len. Returns the number of options
* in the vector
*/
static int
tcpdump_fill_in_options(char *opt)
{
char options[256];
char *arg, *newarg;
int i = 1, arglen;
char *token = NULL;
/* zero out our options_vec for execv() */
memset(options_vec, '\0', sizeof(options_vec));
/* first arg should be the binary (by convention) */
options_vec[0] = TCPDUMP_BINARY;
/* prep args */
memset(options, '\0', 256);
if (opt != NULL) {
strlcat(options, opt, sizeof(options));
}
strlcat(options, TCPDUMP_ARGS, sizeof(options));
dbgx(2, "[child] Will execute: tcpdump %s", options);
/* process args */
/* process the first argument */
arg = strtok_r(options, OPT_DELIM, &token);
arglen = (int)strlen(arg) + 2; /* -{arg}\0 */
newarg = (char *)safe_malloc(arglen);
strlcat(newarg, "-", arglen);
strlcat(newarg, arg, arglen);
options_vec[i++] = newarg;
/* process the remaining args
* note that i < OPTIONS_VEC_SIZE - 1
* because: a) we need to add '-' as an option to the end
* b) because the array has to be null terminated
*/
while (((arg = strtok_r(NULL, OPT_DELIM, &token)) != NULL) && (i < OPTIONS_VEC_SIZE - 1)) {
arglen = (int)strlen(arg) + 2;
newarg = (char *)safe_malloc(arglen);
strlcat(newarg, "-", arglen);
strlcat(newarg, arg, arglen);
options_vec[i++] = newarg;
}
/* tell -r to read from stdin */
options_vec[i] = "-";
return i;
}
/**
* can we exec the given file?
*/
static int
can_exec(const char *filename)
{
struct stat st;
assert (filename);
if (filename[0] == '\0')
return FALSE;
/* Stat the file to see if it's executable and
if the user may run it.
*/
if (lstat(filename, &st) < 0)
return FALSE;
if ((st.st_mode & S_IXUSR) || (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH))
return TRUE;
return FALSE;
}