Files
Utilities/Networking/udp2file.c

448 lines
17 KiB
C

/*============================================================================*/
//
// Title: udp2file.c
// Author: RH
// Last modified: 13/09/2025
//
//============================================================================
//
// Description:
// ------------
// Tool that takes a file path, ip address, port, and capture size. The tool
// then listens on the supplied address and port for UDP datagrams and captures
// messages in chunks up to the capture size at a time, before then writing
// the binary data to the specified filepath.
//
// Compilation:
// ------------
// gcc -Wall -Wextra ../Logging/logging_lib.c udp2file.c -o udp2file
//
/*============================================================================*/
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdint.h>
#include <signal.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include "../Logging/logging_lib.h"
// program, verbose (optional), buffer check (optional), file path, ip address,
// port, capture size
#define MIN_NUM_ARGS 4
#define MAX_NUM_ARGS 6
#define NUM_REQ_ARGS 3
#define MAX_PAYLOAD_SIZE 65507
// ----------------------------------------------------------------------------
// print usage information to the terminal to provide information on how the
// program expects to be run
// ----------------------------------------------------------------------------
void usage (void)
{
printf("\n");
printf("---------------------------------------------------------------------------------------------\n");
printf(" Usage:\n");
printf(" ./udp2file [options] <file path> <port> <capture size>\n");
printf("---------------------------------------------------------------------------------------------\n");
printf(" Required Arguments:\n");
printf(" <file path>:\n");
printf(" \tThe path to the file to write the captured binary data to. Will create the file if it\n");
printf(" \tdoes not already exist and will overwite it if it does.\n");
printf(" <port>:\n");
printf(" \tThe port to listen on for the UDP datagrams.\n");
printf(" <capture size>:\n");
printf(" \tThe capture size (in bytes) to read from the system UDP buffer. This is the size of\n");
printf(" \tthe datagram payload, so an MTU of 9000 (jumbo frames) means a capture size of 8972\n");
printf(" \tbytes. [Range: 1 to 65507.]\n");
printf("\n");
printf(" Optional Arguments:\n");
printf(" -h, --help\n");
printf(" \tDisplay this usage information then exit.\n");
printf(" -v, --verbose\n");
printf(" \tVerbose flag to enable additional debug output messages - at high data rate this can\n");
printf(" \tbe very noisy and so is not recommended for operational use.\n");
printf(" -b, --buffer-check\n");
printf(" \tEnable additional checking of the system UDP buffer that warns if the queue is\n");
printf(" \tgrowing and reports an error when datagrams are dropped. This is not recommended for\n");
printf(" \tvery high data rates (>512 mbps).\n");
printf("---------------------------------------------------------------------------------------------\n");
printf(" Example:\n");
printf(" ./udp_receiver ~/udp_captures/catpure_6.bin 8080 8972\n");
printf("---------------------------------------------------------------------------------------------\n");
}
// ----------------------------------------------------------------------------
// signal handler to allow application to exit cleanly
// ----------------------------------------------------------------------------
static int run = 1;
// the handler just sets 'run' to 0, which allows the main loop to exit
// and the cleanup code after the main loop to run
static void SignalHandler ( int dummy )
{
dummy++ ;
LOGMSG_INFO(">>>>>>>>>>>> Signal received, terminating...")
run = 0;
}
// ----------------------------------------------------------------------------
// main program - parse arguments, open output file, create listener
// socket, then enter a loop waiting for datagrams and writing them to file
// ----------------------------------------------------------------------------
int main(int argc, char **argv)
{
// initiliase logger to print messages to stdout
int Verbose = 0;
LOGGING_INIT_STDOUT
// signal handlers to ensure program exits cleanly
(void) signal(SIGINT , SignalHandler) ;
(void) signal(SIGTERM, SignalHandler) ;
// -------------------------------------------------------------------------
// parse input arguments
// -------------------------------------------------------------------------
// check for help argument
if ( (argc == 2)
&& ( (strncmp ( argv[1], "-h", strlen("-h")) == 0)
|| (strncmp ( argv[1], "--help", strlen("--help")) == 0)
)
)
{
usage();
return -1;
}
if ( (argc < MIN_NUM_ARGS) || (argc > MAX_NUM_ARGS) )
{
LOGMSG_ERR("Error: incorrect number of arguments supplied. Received %d, minimum %d -> maximum %d",
argc, MIN_NUM_ARGS, MAX_NUM_ARGS)
usage();
return -1;
}
// check optional arguments (excluding --help)
int current_arg = 1;
int buffer_check = 0;
// check combinations first: -vb, -bv
if ( (strncmp ( argv[current_arg], "-vb", strlen("-vb")) == 0)
|| (strncmp ( argv[current_arg], "-bv", strlen("-bv")) == 0)
)
{
Verbose = 1;
buffer_check = 1;
current_arg++;
}
// check for verbose supplied individually - Verbose == 0 used to prevent
// '-v' from incrementing current_args if '-vb' above has already been set
if ( ( Verbose == 0 )
&& ( (strncmp ( argv[current_arg], "-v", strlen("-v")) == 0)
|| (strncmp ( argv[current_arg], "--verbose", strlen("--verbose")) == 0) )
)
{
Verbose = 1;
current_arg++;
}
// check buffer flag supplied individually - buffer_check == 0 used to prevent
// '-b' from incrementing current_args if '-bv' above has already been set
if ( ( buffer_check == 0 )
&& ( (strncmp ( argv[current_arg], "-b", strlen("-b")) == 0)
|| (strncmp ( argv[current_arg], "--buffer-check", strlen("--buffer-check")) == 0) )
)
{
buffer_check = 1;
current_arg++;
}
// check required arguments (file name, port, payload size)
// there should only be 3 arguments left to process
char *pFileName = NULL;
uint32_t port = 0;
size_t payloadSize = 0;
if ( (argc - current_arg) != NUM_REQ_ARGS )
{
LOGMSG_ERR("Error parsing input arguments: Received %d, minimum %d -> maximum %d",
argc, MIN_NUM_ARGS, MAX_NUM_ARGS)
usage();
return -1;
}
else
{
pFileName = argv[current_arg++];
port = atoi(argv[current_arg++]);
payloadSize = atoi(argv[current_arg]);
}
if ( (payloadSize > MAX_PAYLOAD_SIZE) || (payloadSize == 0) )
{
LOGMSG_ERR("Error: supplied payload size [%d] must be greater than 0 and not"
" more than the maximum possible UDP datagram payload size [%d]",
payloadSize, MAX_PAYLOAD_SIZE)
usage();
return -1;
}
LOGMSG_INFO("Running udp_receiver | Verbose: %s, Buffer checking: %s, Output"
" file: %s, Port: %d, Payload size: %d",
(Verbose == 1) ? "Yes" : "No", (buffer_check == 1) ? "Yes" : "No",
pFileName, port, payloadSize)
// -------------------------------------------------------------------------
// open the file to write the binary data to
// -------------------------------------------------------------------------
FILE *pDestFile = NULL;
pDestFile = fopen(pFileName, "w");
size_t bytes_written = 0;
if (pDestFile == NULL)
{
LOGMSG_ERR("Error: unable to open the requested file %s. Errno: %s [%d]",
pFileName, strerror(errno), errno)
return -1;
}
else
{
LOGMSG_DBG("Opened file: %s", pFileName)
}
// -------------------------------------------------------------------------
// create the socket and bind to the requested port
// -------------------------------------------------------------------------
// the buffer to read the datagram payload, size set by program argument
char udpRecvBuffer[payloadSize];
// socket and interface information
int rx_fd = 0;
ssize_t bytes_received = 0;
struct sockaddr_in local_addr;
socklen_t sock_length = sizeof(struct sockaddr);
// clear local_addr
memset(&local_addr, '\0', sizeof(local_addr));
// populate information about the remote and the port to listen on
local_addr.sin_family = AF_INET;
local_addr.sin_port = htons(port);
local_addr.sin_addr.s_addr = INADDR_ANY;
// create socket file descriptor
if ( (rx_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0 )
{
LOGMSG_ERR("Error: unable to create UDP listener socket. Errno: %s [%d]",
strerror(errno), errno)
fclose(pDestFile);
return -1;
}
// set the socket REUSEADDR option
int option = 1 ;
if ( setsockopt(rx_fd, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option)) < 0 )
{
LOGMSG_ERR("Error: unable to set socket options. Errno: %s [%d]",
strerror(errno), errno)
fclose(pDestFile);
close(rx_fd);
return -1;
}
// bind the socket to the local address
if ( bind(rx_fd, (const struct sockaddr *) &local_addr, sizeof(local_addr)) < 0 )
{
LOGMSG_ERR("Error: unable to bind to listener socket. Errno: %s [%d]",
strerror(errno), errno)
fclose(pDestFile);
close(rx_fd);
return -1;
}
LOGMSG_DBG("Socket %d opened for port: %d", rx_fd, port)
// -------------------------------------------------------------------------
// buffer checking variables for use if it is enabled
// -------------------------------------------------------------------------
// variables to store buffer stats into
uint32_t local_port = 0;
uint32_t rx_buffer_used_space = 0;
uint32_t prev_rx_buffer_used_space = 0;
uint32_t prev_dopped_dgram_count = 0;
uint32_t curr_dopped_dgram_count = 0;
// variables for reading the buffer stats file
ssize_t buffer_stats_bytes_read = 0;
size_t line_len = 0;
char *stat_line = NULL;
char udp_buffer_stats_path[] = "/proc/net/udp";
FILE *pBufStatFile = NULL;
// check the file can be opened
if (buffer_check == 1)
{
pBufStatFile = fopen(udp_buffer_stats_path, "r");
if (pBufStatFile == NULL)
{
LOGMSG_ERR("Error: unable to open UDP buffer statistics file at %s. Errno: %s [%d]",
udp_buffer_stats_path, strerror(errno), errno)
buffer_check = 0;
}
}
// variables to flag bad buffer states so we don't spam the log
int buffer_overflow = 0;
int rx_queue_growing = 0;
// -------------------------------------------------------------------------
// main loop
// -------------------------------------------------------------------------
LOGMSG_DBG("Capturing UDP datagrams - payload size: %d port: %d output file: %s",
payloadSize, port, pFileName)
while(run == 1)
{
// non-blocking function to loop until a message appears in the UDP buffer,
// then read bytes from the system buffer up to the amount specified by
// payloadSize.
// non-blocking means that the while-loop can be exited when receiving
// a system signal
bytes_received = recvfrom(rx_fd, udpRecvBuffer, payloadSize, MSG_DONTWAIT, (struct sockaddr *) &local_addr, &sock_length);
// we have read in some number of bytes successfully
if ( bytes_received > 0 )
{
LOGMSG_DBG("Successfully received %d bytes (buffer size %d)", bytes_received, payloadSize)
// attempt to write the captured bytes to file
bytes_written = fwrite(udpRecvBuffer, sizeof(char), bytes_received, pDestFile);
// we have written the data to file successfully
if ( bytes_written == ((size_t)bytes_received) )
{
LOGMSG_DBG("Successfully written %d bytes to file: %s", bytes_written, pFileName)
}
else // error
{
LOGMSG_ERR("Error: Failed to write all captured data to the output file %s: Errno: %s [%d] Bytes read: %d Bytes written: %d",
pFileName, strerror(errno), errno, bytes_received, bytes_written)
}
}
else
{
if (bytes_received == 0)
{
LOGMSG_WARN("Received 0 byte message")
}
else if ( (errno != EAGAIN) && (errno != EWOULDBLOCK) ) // error
{
LOGMSG_ERR("Error: UDP receive function failed: Errno: %s [%d] Bytes read: %d",
strerror(errno), errno, bytes_received)
}
}
// check the system buffer and report if we are dropping datagrams
if (buffer_check == 1)
{
// need to use freopen as existing lines in the file have changed
freopen(udp_buffer_stats_path, "r", pBufStatFile);
while( (buffer_stats_bytes_read = getline(&stat_line, &line_len, pBufStatFile)) != -1 )
{
// onl process the line if it is not the header
if ( strstr(stat_line, "sl local_address rem_address") == NULL )
{
// parse the line and store into variables
// %*d allows the fomratting to skip over an element (the * means to not assign to destination)
sscanf(stat_line, "%*d: %*X:%4X %*X:%*X %*d %*X:%8X %*d:%*X %*X %*d %*d %*d %*d %*X %d",
&local_port, &rx_buffer_used_space, &curr_dopped_dgram_count );
// check if we have dropped any datagrams on the port we are listening to
if (local_port == port)
{
//TODO: include with a logging interval
// LOGMSG_DBG("port: %04X tx_queue: %X rx_queue: %X dropped_datagrams: %d",
// local_port, tx_queue, rx_queue, dropped_datagrams);
// log once if a buffer overflow occurs, until the rx queue is cleared
if ( (curr_dopped_dgram_count != prev_dopped_dgram_count)
&& (buffer_overflow == 0)
)
{
LOGMSG_ERR("Error: UDP BUFFER OVERFLOW ON PORT %X - Rx Buffer: %X | Dropped datagrams: %d (previously %d)",
local_port, rx_buffer_used_space, curr_dopped_dgram_count, prev_dopped_dgram_count)
buffer_overflow = 1;
}
// warn if the rx queue is growing (impending overflow) - at high UDP data rates there will likely be
// no warning if the system buffer is small as a gigabit link could fill the buffer within a check period
if ( (rx_buffer_used_space > prev_rx_buffer_used_space)
&& (rx_queue_growing == 0)
)
{
LOGMSG_WARN("UDP RX BUFFER QUEUE GROWING ON PORT %X - Rx Buffer: %X (previously %X) | Dropped datagrams: %d (previously %d)",
local_port, rx_buffer_used_space, prev_rx_buffer_used_space, curr_dopped_dgram_count, prev_dopped_dgram_count )
rx_queue_growing = 1;
}
// clear the buffer_overflow flag so we can log further unique instances
if ( (rx_buffer_used_space == 0)
&& ( (rx_queue_growing == 1) || (buffer_overflow == 1) )
)
{
// clear flags as buffer has returned to a nominal state
buffer_overflow = 0;
rx_queue_growing = 0;
LOGMSG_INFO("UDP RX Buffer on port %X has returned to nominal conditions - Rx Buffer: %X (previously %X) | Total dropped datagrams: %d",
rx_buffer_used_space, prev_rx_buffer_used_space, curr_dopped_dgram_count)
}
// update variables used to detect error conditions
prev_dopped_dgram_count = curr_dopped_dgram_count;
prev_rx_buffer_used_space = rx_buffer_used_space;
// we have parsed the line relevant to our port, so we can skip the rest of the file
break;
}
}
}
}
// short sleep to not hog resource - 1000 us at 8972 bytes gives a max
// throughput of 8972000 Bytes/s (71776000 b/s)
// a usleep of 50 us is recommended for gigabit throughput using jumbo
// frames: 8972 bytes per 50 us means 179440000 Bytes/s
// (1435520000 b/s [1.4 Gbps])
// also beware disk usage limitations when attempting gigabit
// captures (~178 MBps)
usleep(1000);
}
// -------------------------------------------------------------------------
// clean up
// -------------------------------------------------------------------------
if (buffer_check == 1)
{
fclose(pBufStatFile);
}
fclose(pDestFile);
close(rx_fd);
LOGMSG_INFO("\n############################\n\tProgram Exit\n############################")
return 0;
}