From 97a9f24fc3e4ccf87984bd2f028b2c356c95edf1 Mon Sep 17 00:00:00 2001 From: lohhiiccc <96543753+lohhiiccc@users.noreply.github.com> Date: Sat, 7 Feb 2026 11:15:42 +0100 Subject: [PATCH] feat: extract_offending function --- includes/icmp.h | 25 +++ sources.mk | 2 + src/recv/api/extract_offending.c | 99 +++++++++++ tests/recv/test_extract_offending.c | 255 ++++++++++++++++++++++++++++ 4 files changed, 381 insertions(+) create mode 100644 src/recv/api/extract_offending.c create mode 100644 tests/recv/test_extract_offending.c diff --git a/includes/icmp.h b/includes/icmp.h index 4b7535f..4c848d3 100644 --- a/includes/icmp.h +++ b/includes/icmp.h @@ -23,6 +23,27 @@ typedef struct icmp_reply { size_t ip_payload_len; } icmp_reply_t; +/* Offending packet structure for error messages */ +typedef struct icmp_offending_packet { + struct in_addr src; + struct in_addr dst; + uint8_t protocol; + uint8_t icmp_type; + uint8_t icmp_code; + union { + struct { + uint16_t id; + uint16_t seq; + } echo; + uint32_t gateway; + struct { + uint16_t unused; + uint16_t mtu; + } frag; + uint8_t raw[4]; + } rest; +} icmp_offending_packet_t; + /* Callback type for received packets */ typedef void (*icmp_callback_t)(const icmp_reply_t *reply, void *userdata); @@ -48,6 +69,10 @@ int icmp_process(icmp_handle_t *h, icmp_callback_t cb, void *userdata, int icmp_reply_id_seq(const icmp_reply_t *reply, uint16_t *id, uint16_t *seq); +/* Error message helpers */ +int icmp_error_extract_offending(const icmp_reply_t *reply, + icmp_offending_packet_t *offending); + /* Error handling */ const char *icmp_strerror(const icmp_handle_t *h); int icmp_should_retry(const icmp_handle_t *h); diff --git a/sources.mk b/sources.mk index 77ae9f9..7d28607 100644 --- a/sources.mk +++ b/sources.mk @@ -29,6 +29,7 @@ SRCS += $(SRC_DIR)/recv/core/parse_packet.c SRCS += $(SRC_DIR)/recv/core/process_single_packet.c SRCS += $(SRC_DIR)/recv/api/process.c SRCS += $(SRC_DIR)/recv/api/extract_id_seq.c +SRCS += $(SRC_DIR)/recv/api/extract_offending.c TESTS_DIR = tests TESTS += $(TESTS_DIR)/utils/test_checksum.c @@ -52,3 +53,4 @@ TESTS += $(TESTS_DIR)/recv/test_receive_packet.c TESTS += $(TESTS_DIR)/recv/test_parse_packet.c TESTS += $(TESTS_DIR)/recv/test_process.c TESTS += $(TESTS_DIR)/recv/test_extract_id_seq.c +TESTS += $(TESTS_DIR)/recv/test_extract_offending.c diff --git a/src/recv/api/extract_offending.c b/src/recv/api/extract_offending.c new file mode 100644 index 0000000..dee080c --- /dev/null +++ b/src/recv/api/extract_offending.c @@ -0,0 +1,99 @@ +#include "icmp.h" +#include "icmp_types.h" +#include "internal/icmp_packet.h" +#include "internal/icmp_packet_internal.h" +#include +#include + +#define MIN_ERROR_PAYLOAD_LEN 28 +#define MIN_ICMP_HEADER_LEN 8 + +/* Forward declarations */ +static int is_error_type(uint8_t type); +static int is_echo_type(uint8_t type); +static int is_frag_needed(uint8_t type, uint8_t code); +static int parse_embedded_ip(const icmp_reply_t *reply, + icmp_offending_packet_t *offending, + size_t *ip_hdr_len); +static const struct icmp_header *get_embedded_icmp(const icmp_reply_t *reply, + size_t ip_hdr_len); +static void extract_icmp_fields(const struct icmp_header *hdr, + icmp_offending_packet_t *offending); +/* -------------------- */ + +int +icmp_error_extract_offending(const icmp_reply_t *reply, + icmp_offending_packet_t *offending) +{ + size_t ip_hdr_len; + const struct icmp_header *hdr; + + if ((NULL == reply || NULL == offending) || + (!is_error_type(reply->type)) || + (reply->payload_len < MIN_ERROR_PAYLOAD_LEN) || + (parse_embedded_ip(reply, offending, &ip_hdr_len) < 0) || + (reply->payload_len < ip_hdr_len + MIN_ICMP_HEADER_LEN)) + return -1; + + hdr = get_embedded_icmp(reply, ip_hdr_len); + extract_icmp_fields(hdr, offending); + return 0; +} + +static int +is_error_type(uint8_t type) +{ + return ICMP_TYPE_DEST_UNREACHABLE == type || + ICMP_TYPE_REDIRECT == type || + ICMP_TYPE_TIME_EXCEEDED == type || + ICMP_TYPE_PARAMETER_PROBLEM == type; +} + +static int +is_echo_type(uint8_t type) +{ + return ICMP_TYPE_ECHO_REPLY == type || + ICMP_TYPE_ECHO_REQUEST == type || + ICMP_TYPE_TIMESTAMP_REQUEST == type || + ICMP_TYPE_TIMESTAMP_REPLY == type; +} + +static int +is_frag_needed(uint8_t type, uint8_t code) +{ + return ICMP_TYPE_DEST_UNREACHABLE == type && + ICMP_CODE_FRAG_NEEDED == code; +} + +static int +parse_embedded_ip(const icmp_reply_t *reply, icmp_offending_packet_t *offending, + size_t *ip_hdr_len) +{ + return icmp_parse_ip_header(reply->payload, reply->payload_len, + NULL, &offending->src, ip_hdr_len, + &offending->dst, &offending->protocol); +} + +static const struct icmp_header * +get_embedded_icmp(const icmp_reply_t *reply, size_t ip_hdr_len) +{ + return (const struct icmp_header *)((const uint8_t *)reply->payload + ip_hdr_len); +} + +static void +extract_icmp_fields(const struct icmp_header *hdr, icmp_offending_packet_t *offending) +{ + offending->icmp_type = hdr->type; + offending->icmp_code = hdr->code; + offending->rest.gateway = hdr->un.gateway; + + if (is_echo_type(hdr->type)) + { + offending->rest.echo.id = ntohs(hdr->un.echo.id); + offending->rest.echo.seq = ntohs(hdr->un.echo.seq); + } + else if (is_frag_needed(hdr->type, hdr->code)) + { + offending->rest.frag.mtu = ntohs(hdr->un.frag.mtu); + } +} diff --git a/tests/recv/test_extract_offending.c b/tests/recv/test_extract_offending.c new file mode 100644 index 0000000..bcfe53f --- /dev/null +++ b/tests/recv/test_extract_offending.c @@ -0,0 +1,255 @@ +#include +#include +#include +#include "icmp.h" +#include "icmp_types.h" +#include "internal/icmp_packet.h" + +/* Helper to build an error reply with embedded ICMP packet */ +static icmp_reply_t +make_error_reply(uint8_t error_type, uint8_t *buffer, size_t buffer_size) +{ + icmp_reply_t reply; + + memset(&reply, 0, sizeof(reply)); + memset(buffer, 0, buffer_size); + + reply.type = error_type; + reply.code = 0; + reply.payload = buffer; + reply.payload_len = buffer_size; + + return reply; +} + +/* Helper to fill IP header in buffer */ +static void +fill_ip_header(uint8_t *buffer, const char *src, const char *dst, uint8_t protocol) +{ + struct ip_header *ip = (struct ip_header *)buffer; + ip->version_ihl = 0x45; /* version 4, IHL 5 (20 bytes) */ + ip->ttl = 64; + ip->protocol = protocol; + ip->saddr = inet_addr(src); + ip->daddr = inet_addr(dst); +} + +/* Helper to fill ICMP header after IP header */ +static void +fill_icmp_header(uint8_t *buffer, uint8_t type, uint8_t code, + uint16_t id, uint16_t seq) +{ + buffer[20] = type; /* After 20-byte IP header */ + buffer[21] = code; + buffer[22] = 0; /* checksum high */ + buffer[23] = 0; /* checksum low */ + /* id and seq in network byte order */ + *(uint16_t *)(buffer + 24) = htons(id); + *(uint16_t *)(buffer + 26) = htons(seq); +} + +Test(extract_offending, time_exceeded_with_echo) +{ + uint8_t buffer[28]; + icmp_reply_t reply = make_error_reply(ICMP_TYPE_TIME_EXCEEDED, buffer, sizeof(buffer)); + + fill_ip_header(buffer, "192.168.1.100", "8.8.8.8", 1); + fill_icmp_header(buffer, ICMP_TYPE_ECHO_REQUEST, 0, 0x1234, 0x5678); + + icmp_offending_packet_t offending; + int ret = icmp_error_extract_offending(&reply, &offending); + + cr_assert_eq(ret, 0, "Should return 0 for valid Time Exceeded"); + cr_assert_eq(offending.src.s_addr, inet_addr("192.168.1.100"), + "Source address should match"); + cr_assert_eq(offending.dst.s_addr, inet_addr("8.8.8.8"), + "Destination address should match"); + cr_assert_eq(offending.protocol, 1, "Protocol should be 1 (ICMP)"); + cr_assert_eq(offending.icmp_type, ICMP_TYPE_ECHO_REQUEST, + "ICMP type should be Echo Request"); + cr_assert_eq(offending.icmp_code, 0, "ICMP code should be 0"); + cr_assert_eq(offending.rest.echo.id, 0x1234, "Echo ID should match"); + cr_assert_eq(offending.rest.echo.seq, 0x5678, "Echo seq should match"); +} + +Test(extract_offending, dest_unreachable_with_timestamp) +{ + uint8_t buffer[28]; + icmp_reply_t reply = make_error_reply(ICMP_TYPE_DEST_UNREACHABLE, buffer, sizeof(buffer)); + + fill_ip_header(buffer, "10.0.0.1", "10.0.0.2", 1); + fill_icmp_header(buffer, ICMP_TYPE_TIMESTAMP_REQUEST, 0, 0xABCD, 0xEF01); + + icmp_offending_packet_t offending; + int ret = icmp_error_extract_offending(&reply, &offending); + + cr_assert_eq(ret, 0, "Should return 0 for valid Dest Unreachable"); + cr_assert_eq(offending.protocol, 1, "Protocol should be ICMP"); + cr_assert_eq(offending.icmp_type, ICMP_TYPE_TIMESTAMP_REQUEST); + cr_assert_eq(offending.rest.echo.id, 0xABCD); + cr_assert_eq(offending.rest.echo.seq, 0xEF01); +} + +Test(extract_offending, redirect_with_gateway) +{ + uint8_t buffer[28]; + icmp_reply_t reply = make_error_reply(ICMP_TYPE_REDIRECT, buffer, sizeof(buffer)); + + fill_ip_header(buffer, "10.0.0.1", "10.0.0.2", 1); + /* For redirect, the 4 bytes after type/code/checksum are the gateway address */ + buffer[20] = ICMP_TYPE_REDIRECT; + buffer[21] = 0; + buffer[22] = 0; + buffer[23] = 0; + *(uint32_t *)(buffer + 24) = inet_addr("192.168.1.1"); /* Gateway */ + + icmp_offending_packet_t offending; + int ret = icmp_error_extract_offending(&reply, &offending); + + cr_assert_eq(ret, 0, "Should return 0 for valid Redirect"); + cr_assert_eq(offending.icmp_type, ICMP_TYPE_REDIRECT); + cr_assert_eq(offending.rest.gateway, inet_addr("192.168.1.1"), + "Gateway address should match"); +} + +Test(extract_offending, parameter_problem) +{ + uint8_t buffer[28]; + icmp_reply_t reply = make_error_reply(ICMP_TYPE_PARAMETER_PROBLEM, buffer, sizeof(buffer)); + + fill_ip_header(buffer, "1.2.3.4", "5.6.7.8", 1); + fill_icmp_header(buffer, ICMP_TYPE_ECHO_REQUEST, 0, 100, 200); + + icmp_offending_packet_t offending; + int ret = icmp_error_extract_offending(&reply, &offending); + + cr_assert_eq(ret, 0, "Should return 0 for Parameter Problem"); + cr_assert_eq(offending.rest.echo.id, 100); + cr_assert_eq(offending.rest.echo.seq, 200); +} + +Test(extract_offending, non_icmp_protocol) +{ + uint8_t buffer[28]; + icmp_reply_t reply = make_error_reply(ICMP_TYPE_TIME_EXCEEDED, buffer, sizeof(buffer)); + + /* Embedded packet is UDP (protocol 17), not ICMP */ + fill_ip_header(buffer, "10.0.0.1", "10.0.0.2", 17); + memset(buffer + 20, 0, 8); /* 8 bytes of UDP header */ + + icmp_offending_packet_t offending; + int ret = icmp_error_extract_offending(&reply, &offending); + + cr_assert_eq(ret, 0, "Should return 0 even for non-ICMP"); + cr_assert_eq(offending.protocol, 17, "Protocol should be 17 (UDP)"); + /* icmp_type and icmp_code are set but not meaningful for UDP */ +} + +Test(extract_offending, non_error_type_returns_error) +{ + uint8_t buffer[28]; + icmp_reply_t reply = make_error_reply(ICMP_TYPE_ECHO_REPLY, buffer, sizeof(buffer)); + + fill_ip_header(buffer, "10.0.0.1", "10.0.0.2", 1); + + icmp_offending_packet_t offending; + int ret = icmp_error_extract_offending(&reply, &offending); + + cr_assert_eq(ret, -1, "Should return -1 for non-error type (Echo Reply)"); +} + +Test(extract_offending, null_reply_returns_error) +{ + icmp_offending_packet_t offending; + int ret = icmp_error_extract_offending(NULL, &offending); + + cr_assert_eq(ret, -1, "Should return -1 for NULL reply"); +} + +Test(extract_offending, null_offending_returns_error) +{ + uint8_t buffer[28]; + icmp_reply_t reply = make_error_reply(ICMP_TYPE_TIME_EXCEEDED, buffer, sizeof(buffer)); + + int ret = icmp_error_extract_offending(&reply, NULL); + + cr_assert_eq(ret, -1, "Should return -1 for NULL offending"); +} + +Test(extract_offending, payload_too_short_returns_error) +{ + uint8_t buffer[27]; /* One byte short */ + icmp_reply_t reply = make_error_reply(ICMP_TYPE_TIME_EXCEEDED, buffer, sizeof(buffer)); + + icmp_offending_packet_t offending; + int ret = icmp_error_extract_offending(&reply, &offending); + + cr_assert_eq(ret, -1, "Should return -1 when payload < 28 bytes"); +} + +Test(extract_offending, ip_header_with_options) +{ + uint8_t buffer[40]; /* 20 byte IP + options + 8 byte ICMP */ + icmp_reply_t reply = make_error_reply(ICMP_TYPE_TIME_EXCEEDED, buffer, sizeof(buffer)); + + /* IP header with IHL=6 (24 bytes including 4 bytes of options) */ + struct ip_header *ip = (struct ip_header *)buffer; + ip->version_ihl = 0x46; /* version 4, IHL 6 (24 bytes) */ + ip->ttl = 64; + ip->protocol = 1; + ip->saddr = inet_addr("192.168.1.1"); + ip->daddr = inet_addr("192.168.1.2"); + + /* ICMP header starts at offset 24 */ + fill_icmp_header(buffer + 4, ICMP_TYPE_ECHO_REQUEST, 0, 0x9999, 0xAAAA); + + icmp_offending_packet_t offending; + int ret = icmp_error_extract_offending(&reply, &offending); + + cr_assert_eq(ret, 0, "Should handle IP headers with options"); + cr_assert_eq(offending.protocol, 1); + cr_assert_eq(offending.icmp_type, ICMP_TYPE_ECHO_REQUEST); + cr_assert_eq(offending.rest.echo.id, 0x9999); + cr_assert_eq(offending.rest.echo.seq, 0xAAAA); +} + +Test(extract_offending, embedded_frag_needed_mtu) +{ + uint8_t buffer[28]; + icmp_reply_t reply = make_error_reply(ICMP_TYPE_TIME_EXCEEDED, buffer, sizeof(buffer)); + + fill_ip_header(buffer, "10.0.0.1", "10.0.0.2", 1); + /* Embedded packet is itself a Dest Unreachable / Frag Needed */ + buffer[20] = ICMP_TYPE_DEST_UNREACHABLE; + buffer[21] = ICMP_CODE_FRAG_NEEDED; + buffer[22] = 0; + buffer[23] = 0; + *(uint16_t *)(buffer + 24) = 0; /* unused */ + *(uint16_t *)(buffer + 26) = htons(1400); /* mtu in network order */ + + icmp_offending_packet_t offending; + int ret = icmp_error_extract_offending(&reply, &offending); + + cr_assert_eq(ret, 0); + cr_assert_eq(offending.icmp_type, ICMP_TYPE_DEST_UNREACHABLE); + cr_assert_eq(offending.icmp_code, ICMP_CODE_FRAG_NEEDED); + cr_assert_eq(offending.rest.frag.mtu, 1400, "MTU should be in host byte order"); +} + +Test(extract_offending, redirect_with_tcp_packet) +{ + uint8_t buffer[48]; + icmp_reply_t reply = make_error_reply(ICMP_TYPE_REDIRECT, buffer, sizeof(buffer)); + + fill_ip_header(buffer, "10.0.0.1", "10.0.0.2", 6); // Protocol 6 = TCP + /* TCP header after IP */ + memset(buffer + 20, 0, 20); + buffer[20] = 0x50; + buffer[21] = 0x00; + + icmp_offending_packet_t offending; + int ret = icmp_error_extract_offending(&reply, &offending); + + cr_assert_eq(ret, 0); + cr_assert_eq(offending.protocol, 6, "Should be TCP"); +}