From 56868c675fcdc153f9102885372ccfb30cc19b2f Mon Sep 17 00:00:00 2001 From: Erik Rigtorp Date: Sun, 4 Oct 2026 22:08:20 -0400 Subject: [PATCH] Reject IPv4 fragments and invalid packet lengths --- .github/workflows/ccpp.yml | 3 +- CMakeLists.txt | 9 ++- README.md | 11 +++ src/udpreplay.cpp | 96 +++++++++++++++++++------- test/packet-validation.py | 136 +++++++++++++++++++++++++++++++++++++ 5 files changed, 227 insertions(+), 28 deletions(-) create mode 100644 test/packet-validation.py diff --git a/.github/workflows/ccpp.yml b/.github/workflows/ccpp.yml index 89e287d..a359338 100644 --- a/.github/workflows/ccpp.yml +++ b/.github/workflows/ccpp.yml @@ -36,7 +36,7 @@ jobs: os: macos-15 compiler: clang++ build_type: Release - testing: 'OFF' + testing: 'ON' env: CXX: ${{ matrix.compiler }} BUILD_TYPE: ${{ matrix.build_type }} @@ -65,7 +65,6 @@ jobs: - name: Build run: cmake --build build --config "$BUILD_TYPE" --parallel 2 - name: Test - if: runner.os == 'Linux' run: ctest --test-dir build -C "$BUILD_TYPE" --output-on-failure - name: Install run: cmake --install build --config "$BUILD_TYPE" --prefix "$RUNNER_TEMP/udpreplay-install" diff --git a/CMakeLists.txt b/CMakeLists.txt index bc2e77e..1af52c4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux") set(_testing_default ON) endif() endif() -option(BUILD_TESTING "Enable Linux timing tests (requires Expect)" ${_testing_default}) +option(BUILD_TESTING "Enable packet tests and Linux timing tests" ${_testing_default}) # Homebrew's keg-only libpcap is not on the default compiler search path. set(_pcap_prefixes) @@ -53,6 +53,13 @@ include(GNUInstallDirs) install(TARGETS udpreplay RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") include(CTest) +if(BUILD_TESTING) + find_package(Python3 COMPONENTS Interpreter REQUIRED) + add_test(NAME packet-validation + COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/test/packet-validation.py" + "$") + set_tests_properties(packet-validation PROPERTIES TIMEOUT 30) +endif() if(BUILD_TESTING AND CMAKE_SYSTEM_NAME STREQUAL "Linux") find_program(EXPECT_EXECUTABLE NAMES expect REQUIRED) find_program(TIME_EXECUTABLE NAMES time REQUIRED) diff --git a/README.md b/README.md index 4f9f753..fb5b509 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,15 @@ usage: udpreplay [-i iface] [-l] [-s speed] [-c millisec] [-r repeat] [-t ttl] p $ udpreplay -i eth0 example.pcap ``` +IPv4 fragments and invalid packet lengths stop replay with an error. +Reassemble fragmented captures first, for example with +[IPDefragUtil](https://github.com/seladb/PcapPlusPlus/tree/master/Examples/IPDefragUtil): + +```sh +IPDefragUtil input.pcap -o reassembled.pcap -a +udpreplay reassembled.pcap +``` + ## Building & Installing Requires Linux or macOS, CMake 3.20+, a C++11 compiler, and libpcap headers @@ -44,6 +53,8 @@ location, add `-DCMAKE_PREFIX_PATH=/path/to/libpcap` when configuring. Linux timing tests are enabled by default when Expect is installed. Run them with `ctest --test-dir build --output-on-failure`, or disable them with `-DBUILD_TESTING=OFF` when configuring. +Tests also require Python 3 for packet validation; on macOS, enable them with +`-DBUILD_TESTING=ON`. ## About diff --git a/src/udpreplay.cpp b/src/udpreplay.cpp index dc61f2a..2e50bba 100644 --- a/src/udpreplay.cpp +++ b/src/udpreplay.cpp @@ -146,12 +146,20 @@ int main(int argc, char *argv[]) { return 1; } + auto packet_error = [&](const char *message) { + std::cerr << message << std::endl; + pcap_close(handle); + return 1; + }; + timespec start = {-1, -1}; timespec pcap_start = {-1, -1}; - pcap_pkthdr header; + pcap_pkthdr *packet_header; const u_char *p; - while ((p = pcap_next(handle, &header))) { + int packet_status; + while ((packet_status = pcap_next_ex(handle, &packet_header, &p)) == 1) { + const auto &header = *packet_header; if (start.tv_nsec == -1) { if (clock_gettime(CLOCK_MONOTONIC, &start) == -1) { std::cerr << "clock_gettime: " << strerror(errno) << std::endl; @@ -162,27 +170,68 @@ int main(int argc, char *argv[]) { header.ts.tv_usec; // Note PCAP_TSTAMP_PRECISION_NANO } if (header.len != header.caplen) { - continue; + return packet_error("Invalid packet: captured and original lengths differ"); } - auto eth = reinterpret_cast(p); + if (header.caplen < sizeof(ether_header)) { + return packet_error("Invalid packet: truncated Ethernet header"); + } + ether_header eth; + memcpy(ð, p, sizeof(eth)); + size_t offset = sizeof(eth); + auto protocol = ntohs(eth.ether_type); - // jump over and ignore vlan tags - while (ntohs(eth->ether_type) == ETHERTYPE_VLAN) { - p += 4; - eth = reinterpret_cast(p); + // Jump over VLAN tags, checking each tag before reading its protocol. + while (protocol == ETHERTYPE_VLAN) { + if (header.caplen - offset < 4) { + return packet_error("Invalid packet: truncated VLAN header"); + } + uint16_t encapsulated_protocol; + memcpy(&encapsulated_protocol, p + offset + 2, + sizeof(encapsulated_protocol)); + protocol = ntohs(encapsulated_protocol); + offset += 4; } - if (ntohs(eth->ether_type) != ETHERTYPE_IP) { + if (protocol != ETHERTYPE_IP) { continue; } - auto ip = reinterpret_cast(p + sizeof(ether_header)); - if (ip->ip_v != 4) { + if (header.caplen - offset < sizeof(struct ip)) { + return packet_error("Invalid packet: truncated IPv4 header"); + } + // Ethernet payloads need not be aligned for native IP/UDP structs. + struct ip ip; + memcpy(&ip, p + offset, sizeof(ip)); + if (ip.ip_v != 4) { continue; } - if (ip->ip_p != IPPROTO_UDP) { + const size_t ip_header_length = ip.ip_hl * 4; + const size_t ip_length = ntohs(ip.ip_len); + if (ip_header_length < sizeof(ip) || + ip_header_length > header.caplen - offset || + ip_length < ip_header_length || ip_length > header.caplen - offset) { + return packet_error("Invalid packet: invalid IPv4 length"); + } + if (ip.ip_p != IPPROTO_UDP) { continue; } - auto udp = reinterpret_cast(p + sizeof(ether_header) + - ip->ip_hl * 4); + if (ntohs(ip.ip_off) & (IP_MF | IP_OFFMASK)) { + return packet_error("IPv4 fragments are not supported; reassemble the " + "capture before replay"); + } + if (ip_length - ip_header_length < sizeof(udphdr)) { + return packet_error("Invalid packet: truncated UDP header"); + } + udphdr udp; + memcpy(&udp, p + offset + ip_header_length, sizeof(udp)); +#ifdef __GLIBC__ + const size_t udp_length = ntohs(udp.len); +#else + const size_t udp_length = ntohs(udp.uh_ulen); +#endif + if (udp_length < sizeof(udp) || udp_length > ip_length - ip_header_length) { + return packet_error("Invalid packet: invalid UDP length"); + } + const ssize_t len = udp_length - sizeof(udp); + const u_char *d = p + offset + ip_header_length + sizeof(udp); if (interval != -1) { // Use constant packet rate deadline.tv_sec += interval / 1000L; @@ -236,23 +285,15 @@ int main(int argc, char *argv[]) { #endif } -#ifdef __GLIBC__ - ssize_t len = ntohs(udp->len) - 8; -#else - ssize_t len = ntohs(udp->uh_ulen) - 8; -#endif - const u_char *d = - &p[sizeof(ether_header) + ip->ip_hl * 4 + sizeof(udphdr)]; - sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; #ifdef __GLIBC__ - addr.sin_port = udp->dest; + addr.sin_port = udp.dest; #else - addr.sin_port = udp->uh_dport; + addr.sin_port = udp.uh_dport; #endif - addr.sin_addr = {ip->ip_dst}; + addr.sin_addr = ip.ip_dst; auto n = sendto(fd, d, len, 0, reinterpret_cast(&addr), sizeof(addr)); if (n != len) { @@ -261,6 +302,11 @@ int main(int argc, char *argv[]) { } } + if (packet_status == -1) { + std::cerr << "pcap_read: " << pcap_geterr(handle) << std::endl; + pcap_close(handle); + return 1; + } pcap_close(handle); } diff --git a/test/packet-validation.py b/test/packet-validation.py new file mode 100644 index 0000000..b9c7a73 --- /dev/null +++ b/test/packet-validation.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT + +import socket +import struct +import subprocess +import sys +import tempfile +from pathlib import Path + + +def checksum(data): + value = sum(struct.unpack('!' + 'H' * (len(data) // 2), data)) + while value >> 16: + value = (value & 65535) + (value >> 16) + return ~value & 65535 + + +def ipv4(data, *, ihl=5, length=None, flags=0, options=b'', protocol=17): + header = struct.pack('!BBHHHBBH4s4s', 0x40 | ihl, 0, + 20 + len(options) + len(data) if length is None else length, + 123, flags, 64, protocol, 0, + socket.inet_aton('127.0.0.1'), socket.inet_aton('127.0.0.1')) + header += options + return header[:10] + struct.pack('!H', checksum(header)) + header[12:] + data + + +def ethernet(data, tags=0, protocol=0x0800): + return (bytes(12) + struct.pack('!H', 0x8100 if tags else protocol) + + b''.join(struct.pack('!HH', 1, 0x8100 if i + 1 < tags else protocol) + for i in range(tags)) + data) + + +def capture(packet, *, caplen=None, wirelen=None): + caplen = len(packet) if caplen is None else caplen + wirelen = caplen if wirelen is None else wirelen + return (struct.pack('