From ebb064018dc366c054d63eb3513e8f0e6643fc82 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 19 Aug 2026 09:59:43 +0200 Subject: [PATCH 01/16] F-10278: pop one root entry from the timer heap --- src/test/unit/unit.c | 4 ++- src/test/unit/unit_tests_branches.c | 56 +++++++++++++++++++++++++++++ src/test/unit/unit_tests_proto.c | 37 ++++++++++++------- src/wolfip.c | 38 ++++++++++---------- 4 files changed, 103 insertions(+), 32 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 529eadf3..bc2d00f1 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -132,7 +132,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_pop_timer); tcase_add_test(tc_utils, test_is_timer_expired); tcase_add_test(tc_utils, test_cancel_timer); - tcase_add_test(tc_utils, test_timer_pop_skips_zero_expires); + tcase_add_test(tc_utils, test_timer_pop_removes_zero_head_first); tcase_add_test(tc_utils, test_timer_pop_reorders_heap); tcase_add_test(tc_utils, test_timer_pop_right_child_swap); tcase_add_test(tc_utils, test_timer_pop_break_when_root_small); @@ -1047,6 +1047,8 @@ Suite *wolf_suite(void) #endif tcase_add_test(tc_core, test_poll_dispatches_socket_callback); tcase_add_test(tc_core, test_poll_fires_expired_timer); + tcase_add_test(tc_core, test_timer_binheap_drain_keeps_live_timer); + tcase_add_test(tc_core, test_poll_keeps_timer_armed_after_earlier_cancel); tcase_add_test(tc_core, test_poll_arp_pending_when_nexthop_unresolved); tcase_add_test(tc_core, test_poll_filter_block_holds_tx); tcase_add_test(tc_core, test_poll_drains_icmp_tx); diff --git a/src/test/unit/unit_tests_branches.c b/src/test/unit/unit_tests_branches.c index 367ce533..935fd96e 100644 --- a/src/test/unit/unit_tests_branches.c +++ b/src/test/unit/unit_tests_branches.c @@ -1118,6 +1118,62 @@ START_TEST(test_poll_fires_expired_timer) } END_TEST +START_TEST(test_timer_binheap_drain_keeps_live_timer) +{ + struct timers_binheap heap; + struct wolfIP_timer a; + struct wolfIP_timer b; + struct wolfIP_timer got; + int ida; + int idb; + + memset(&heap, 0, sizeof(heap)); + memset(&a, 0, sizeof(a)); + memset(&b, 0, sizeof(b)); + a.expires = 100; + b.expires = 200; + ida = timers_binheap_insert(&heap, a); + idb = timers_binheap_insert(&heap, b); + ck_assert_int_gt(ida, 0); + ck_assert_int_gt(idb, 0); + + /* Cancel the earliest timer, leaving a tombstone at the heap root. */ + timer_binheap_cancel(&heap, (uint32_t)ida); + ck_assert_int_eq(is_timer_expired(&heap, 50), 0); + + /* Draining the tombstone must leave the next live timer in place. */ + ck_assert_int_eq((int)heap.size, 1); + got = timers_binheap_pop(&heap); + ck_assert_uint_eq(got.id, (uint32_t)idb); + ck_assert_uint_eq(got.expires, 200); +} +END_TEST + +START_TEST(test_poll_keeps_timer_armed_after_earlier_cancel) +{ + struct wolfIP s; + struct wolfIP_timer tmr; + int id_first; + int id_second; + + wolfIP_init(&s); + mock_link_init(&s); + memset(&tmr, 0, sizeof(tmr)); + tmr.expires = 100; + id_first = timers_binheap_insert(&s.timers, tmr); + ck_assert_int_gt(id_first, 0); + memset(&tmr, 0, sizeof(tmr)); + tmr.expires = 200; + tmr.cb = test_timer_cb; + id_second = timers_binheap_insert(&s.timers, tmr); + ck_assert_int_gt(id_second, 0); + timer_binheap_cancel(&s.timers, (uint32_t)id_first); + timer_cb_calls = 0; + ck_assert_int_eq(wolfIP_poll(&s, 250), 0); + ck_assert_int_eq(timer_cb_calls, 1); +} +END_TEST + START_TEST(test_poll_arp_pending_when_nexthop_unresolved) { struct wolfIP s; diff --git a/src/test/unit/unit_tests_proto.c b/src/test/unit/unit_tests_proto.c index fce2bb7c..0d2ffd36 100644 --- a/src/test/unit/unit_tests_proto.c +++ b/src/test/unit/unit_tests_proto.c @@ -2425,12 +2425,14 @@ START_TEST(test_cancel_timer) { ck_assert_int_eq(heap.timers[0].expires, 0); // tmr1 canceled popped = timers_binheap_pop(&heap); - ck_assert_int_eq(popped.expires, 200); // Only tmr2 should remain + ck_assert_uint_eq(popped.expires, 0); /* the tombstone itself */ + popped = timers_binheap_pop(&heap); + ck_assert_uint_eq(popped.expires, 200); /* tmr2 survived the drain */ ck_assert_int_eq(heap.size, 0); } END_TEST -START_TEST(test_timer_pop_skips_zero_expires) +START_TEST(test_timer_pop_removes_zero_head_first) { struct timers_binheap h; struct wolfIP_timer tmr1 = { .expires = 50 }; @@ -2442,8 +2444,11 @@ START_TEST(test_timer_pop_skips_zero_expires) tmr2.id = timers_binheap_insert(&h, tmr2); timer_binheap_cancel(&h, tmr2.id); + /* One root per pop: the tombstone first, then the live timer. */ + popped = timers_binheap_pop(&h); + ck_assert_uint_eq(popped.expires, 0); popped = timers_binheap_pop(&h); - ck_assert_uint_ne(popped.expires, 0); + ck_assert_uint_eq(popped.expires, 50); } END_TEST @@ -2512,16 +2517,18 @@ START_TEST(test_is_timer_expired_skips_zero_head) h.timers[0].expires = 0; h.timers[1].expires = 50; + /* The tombstone head is drained; the live timer survives. */ ck_assert_int_eq(is_timer_expired(&h, 10), 0); - ck_assert_uint_eq(h.size, 0); + ck_assert_uint_eq(h.size, 1); + ck_assert_uint_eq(h.timers[0].expires, 50); } END_TEST -/* Regression: when timers_binheap_pop skips multiple cancelled timers - * (expires==0) in its do-while loop, the sift-down cursor must reset to 0 - * on each iteration. Without the fix the cursor stays at a leaf position - * from the previous sift-down, so the replacement element at index 0 is - * never sifted down, breaking the min-heap invariant. */ +/* Regression: when consecutive pops drain a run of cancelled timers + * (expires==0), the sift-down cursor must start at the root each time. + * Without the reset the cursor stays at a leaf position from the previous + * sift-down, so the replacement element at index 0 is never sifted down, + * breaking the min-heap invariant. Each pop removes exactly one root. */ START_TEST(test_timer_pop_siftdown_resets_after_cancelled) { struct timers_binheap h; @@ -2541,13 +2548,19 @@ START_TEST(test_timer_pop_siftdown_resets_after_cancelled) timer_binheap_cancel(&h, id1); timer_binheap_cancel(&h, id2); - /* Pop must skip both cancelled timers and return 50 */ + /* Pops remove one root each: both tombstones, then the live timers + * in order -- verifies the heap invariant held through the run. */ + popped = timers_binheap_pop(&h); + ck_assert_uint_eq(popped.expires, 0); + popped = timers_binheap_pop(&h); + ck_assert_uint_eq(popped.expires, 0); popped = timers_binheap_pop(&h); ck_assert_uint_eq(popped.expires, 50); - - /* Next pop must return 100 -- verifies the heap invariant held */ popped = timers_binheap_pop(&h); ck_assert_uint_eq(popped.expires, 100); + popped = timers_binheap_pop(&h); + ck_assert_uint_eq(popped.expires, 200); + ck_assert_uint_eq(h.size, 0); } END_TEST diff --git a/src/wolfip.c b/src/wolfip.c index e5f898b5..0dfb178e 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -2422,30 +2422,30 @@ void wolfIP_register_callback(struct wolfIP *s, int sock_fd, tsocket_cb cb, } /* Timers */ +/* Removes exactly one root entry. Drain sites pop in a loop that only + * continues while the root is a tombstone (expires == 0), so a live + * timer is never removed by a drain. */ static struct wolfIP_timer timers_binheap_pop(struct timers_binheap *heap) { uint32_t i = 0; struct wolfIP_timer tmr = {0}; - do { - i = 0; - tmr = heap->timers[0]; - heap->size--; - heap->timers[0] = heap->timers[heap->size]; - while (2*i+1 < heap->size) { - struct wolfIP_timer tmp; - uint32_t j = 2*i+1; - if (j+1 < heap->size && heap->timers[j+1].expires < heap->timers[j].expires) { - j++; - } - if (heap->timers[i].expires <= heap->timers[j].expires) { - break; - } - tmp = heap->timers[i]; - heap->timers[i] = heap->timers[j]; - heap->timers[j] = tmp; - i = j; + tmr = heap->timers[0]; + heap->size--; + heap->timers[0] = heap->timers[heap->size]; + while (2*i+1 < heap->size) { + struct wolfIP_timer tmp; + uint32_t j = 2*i+1; + if (j+1 < heap->size && heap->timers[j+1].expires < heap->timers[j].expires) { + j++; + } + if (heap->timers[i].expires <= heap->timers[j].expires) { + break; } - } while ((tmr.expires == 0) && (heap->size > 0)); + tmp = heap->timers[i]; + heap->timers[i] = heap->timers[j]; + heap->timers[j] = tmp; + i = j; + } return tmr; } From eb4cd4059f586dc22246471464f7ece7a24e5cb2 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 19 Aug 2026 17:17:05 +0200 Subject: [PATCH 02/16] fix: ignore stale timer fires in the DHCP BOUND state --- src/wolfip.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/wolfip.c b/src/wolfip.c index 0dfb178e..95e41efa 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -8070,6 +8070,15 @@ static void dhcp_timer_cb(void *arg) dhcp_send_discover(s); break; } + if (s->dhcp_renew_at != 0 && s->last_tick < s->dhcp_renew_at) { + /* A stale timer from an earlier lease cycle fired early + * (e.g. a renewal timer left pending across a lease drop + * and re-DORA). The current lease's renew time is still + * ahead, so its timer is the one that should drive the + * renewal; stay BOUND instead of starting a spurious + * RENEWING transaction. */ + break; + } s->dhcp_state = DHCP_RENEWING; s->dhcp_start_tick = s->last_tick; s->dhcp_timeout_count = 0; From a4532065bf7867c0a26ad9760b893b57eafad1be Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 19 Aug 2026 10:09:51 +0200 Subject: [PATCH 03/16] F-10267: send TTL-exceeded for short datagrams, quoting what exists --- src/test/unit/unit.c | 7 +-- src/test/unit/unit_tests_ip_arp_recv.c | 64 ++++++++++++++++++++++---- src/test/unit/unit_tests_proto.c | 6 ++- src/test/unit/unit_tests_tcp_ack.c | 22 ++++++--- src/wolfip.c | 23 +++++---- 5 files changed, 91 insertions(+), 31 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index bc2d00f1..4cc14a3a 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -760,8 +760,8 @@ Suite *wolf_suite(void) tcase_add_test(tc_proto, test_send_ttl_exceeded_non_ethernet_skips_eth_filter); tcase_add_test(tc_proto, test_send_ttl_exceeded_sets_df); #if WOLFIP_ENABLE_FORWARDING - tcase_add_test(tc_proto, test_wolfip_forward_ttl_exceeded_short_len_does_not_send); - tcase_add_test(tc_proto, test_regression_forward_ttl_exceeded_short_len_with_options_no_send); + tcase_add_test(tc_proto, test_wolfip_forward_ttl_exceeded_truncated_header_no_send); + tcase_add_test(tc_proto, test_regression_forward_ttl_exceeded_options_header_only_quotes_full_header); #endif tcase_add_test(tc_proto, test_arp_request_filter_drop); tcase_add_test(tc_proto, test_arp_request_invalid_interface); @@ -1450,7 +1450,8 @@ Suite *wolf_suite(void) tcase_add_test(tc_core, test_ip_recv_loopback_dst_on_non_loopback_dropped); tcase_add_test(tc_core, test_ip_recv_loopback_src_on_non_loopback_dropped); tcase_add_test(tc_core, test_ip_recv_forward_ttl_normal_decremented); - tcase_add_test(tc_core, test_ip_recv_forward_ttl1_short_frame_dropped); + tcase_add_test(tc_core, test_ip_recv_forward_ttl1_short_frame_sends_ttl_exceeded); + tcase_add_test(tc_core, test_ip_recv_forward_ttl1_partial_payload_quoted); tcase_add_test(tc_core, test_ip_recv_dest_matches_secondary_iface_ip_is_local); tcase_add_test(tc_core, test_ip_recv_multicast_dst_not_forwarded); tcase_add_test(tc_core, test_arp_recv_htype_not_ethernet_dropped); diff --git a/src/test/unit/unit_tests_ip_arp_recv.c b/src/test/unit/unit_tests_ip_arp_recv.c index 522070d8..412f4cb4 100644 --- a/src/test/unit/unit_tests_ip_arp_recv.c +++ b/src/test/unit/unit_tests_ip_arp_recv.c @@ -1052,14 +1052,14 @@ START_TEST(test_ip_recv_forward_ttl_normal_decremented) END_TEST /* ========================================================================= - * ip_recv: TTL=1 short-frame — dropped before TTL-exceeded sent + * ip_recv: TTL=1 header-only datagram — TTL-exceeded sent, header quoted * ========================================================================= - * Branch: ip->ttl <= 1 && len < ETH_HEADER_LEN + ip_hlen + 8 → return + * Branch: ip->ttl <= 1 → wolfIP_send_ttl_exceeded. The quoted packet is the + * original header plus up to 8 payload bytes, or as much as exists. */ -START_TEST(test_ip_recv_forward_ttl1_short_frame_dropped) +START_TEST(test_ip_recv_forward_ttl1_short_frame_sends_ttl_exceeded) { struct wolfIP s; - /* Frame too short: ETH + IP only, missing the required 8 transport bytes */ uint8_t frame[ETH_HEADER_LEN + IP_HEADER_LEN]; struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)frame; ip4 primary_ip = 0x0A000001U; @@ -1076,19 +1076,65 @@ START_TEST(test_ip_recv_forward_ttl1_short_frame_dropped) memcpy(ip->eth.src, "\x01\x02\x03\x04\x05\x06", 6); ip->eth.type = ee16(ETH_TYPE_IP); ip->ver_ihl = 0x45; - ip->ttl = 1; /* TTL == 1 → would send TTL exceeded if frame ok */ + ip->ttl = 1; ip->proto = WI_IPPROTO_UDP; ip->len = ee16(IP_HEADER_LEN); ip->src = ee32(src_ip); ip->dst = ee32(dest_ip); fix_ip_checksum(ip); - /* Pass only ETH+IP — total 34 bytes, missing the 8 transport bytes - * required by wolfIP_send_ttl_exceeded */ ip_recv(&s, TEST_PRIMARY_IF, ip, (uint32_t)sizeof(frame)); - /* Short frame: TTL-exceeded must NOT have been sent */ - ck_assert_uint_eq(last_frame_sent_size, 0); + /* Time-exceeded quoting the original header only (no payload exists). */ + ck_assert_uint_eq(last_frame_sent_size, + (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + 8 + IP_HEADER_LEN)); + ck_assert_uint_eq(last_frame_sent[ETH_HEADER_LEN + IP_HEADER_LEN], + ICMP_TTL_EXCEEDED); + ck_assert_uint_eq(last_frame_sent[ETH_HEADER_LEN + IP_HEADER_LEN + 8], + 0x45); /* quoted version/IHL */ + ck_assert_uint_eq(last_frame_sent[ETH_HEADER_LEN + IP_HEADER_LEN + 20], + (src_ip >> 24) & 0xFF); /* quoted source address */ +} +END_TEST + +/* ========================================================================= + * ip_recv: TTL=1 datagram with partial payload — quoted as it exists + * ========================================================================= + * Branch: ip->ttl <= 1 → quote = original header + min(8, payload present) + */ +START_TEST(test_ip_recv_forward_ttl1_partial_payload_quoted) +{ + struct wolfIP s; + uint8_t frame[ETH_HEADER_LEN + IP_HEADER_LEN + 4]; + struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)frame; + ip4 primary_ip = 0x0A000001U; + ip4 secondary_ip = 0xC0A80101U; + ip4 dest_ip = 0xC0A80155U; + ip4 src_ip = 0x0A000002U; + + setup_stack_with_two_ifaces(&s, primary_ip, secondary_ip); + wolfIP_filter_set_callback(NULL, NULL); + last_frame_sent_size = 0; + + memset(frame, 0, sizeof(frame)); + memcpy(ip->eth.dst, s.ll_dev[TEST_PRIMARY_IF].mac, 6); + memcpy(ip->eth.src, "\x01\x02\x03\x04\x05\x06", 6); + ip->eth.type = ee16(ETH_TYPE_IP); + ip->ver_ihl = 0x45; + ip->ttl = 1; + ip->proto = WI_IPPROTO_UDP; + ip->len = ee16(IP_HEADER_LEN + 4); + ip->src = ee32(src_ip); + ip->dst = ee32(dest_ip); + fix_ip_checksum(ip); + + ip_recv(&s, TEST_PRIMARY_IF, ip, (uint32_t)sizeof(frame)); + + /* Time-exceeded quoting header + the 4 payload bytes present. */ + ck_assert_uint_eq(last_frame_sent_size, + (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + 8 + IP_HEADER_LEN + 4)); + ck_assert_uint_eq(last_frame_sent[ETH_HEADER_LEN + IP_HEADER_LEN], + ICMP_TTL_EXCEEDED); } END_TEST diff --git a/src/test/unit/unit_tests_proto.c b/src/test/unit/unit_tests_proto.c index 0d2ffd36..32501bc5 100644 --- a/src/test/unit/unit_tests_proto.c +++ b/src/test/unit/unit_tests_proto.c @@ -3865,8 +3865,10 @@ START_TEST(test_wolfip_forwarding_ttl_expired) wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, ETH_HEADER_LEN + IP_HEADER_LEN + 8); + /* The quote is the declared total length (20), below the 28-byte + * default. */ ck_assert_uint_eq(last_frame_sent_size, - (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + ICMP_TTL_EXCEEDED_SIZE)); + (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + 8 + ee16(frame->len))); icmp = (struct wolfIP_icmp_ttl_exceeded_packet *)last_frame_sent; ck_assert_uint_eq(icmp->type, ICMP_TTL_EXCEEDED); ck_assert_uint_eq(icmp->code, 0); @@ -3875,7 +3877,7 @@ START_TEST(test_wolfip_forwarding_ttl_expired) ck_assert_mem_eq(icmp->ip.eth.src, s.ll_dev[TEST_PRIMARY_IF].mac, 6); ck_assert_uint_eq(icmp->ip.ttl, 64); ck_assert_uint_eq(ee16(icmp->ip.len), - (uint16_t)(IP_HEADER_LEN + ICMP_TTL_EXCEEDED_SIZE)); + (uint16_t)(IP_HEADER_LEN + 8 + ee16(frame->len))); ck_assert_uint_eq(ee32(icmp->ip.src), s.ipconf[TEST_PRIMARY_IF].ip); ck_assert_uint_eq(ee32(icmp->ip.dst), ee32(frame->src)); ck_assert_mem_eq(icmp->orig_packet, diff --git a/src/test/unit/unit_tests_tcp_ack.c b/src/test/unit/unit_tests_tcp_ack.c index 83a364bc..aeceac81 100644 --- a/src/test/unit/unit_tests_tcp_ack.c +++ b/src/test/unit/unit_tests_tcp_ack.c @@ -2772,12 +2772,14 @@ START_TEST(test_send_ttl_exceeded_non_ethernet_skips_eth_filter) memset(ip_buf, 0, sizeof(ip_buf)); memcpy(ip->eth.src, "\x01\x02\x03\x04\x05\x06", 6); + ip->ver_ihl = 0x45; + ip->len = ee16(IP_HEADER_LEN); /* header-only datagram */ ip->src = ee32(0x0A000002U); ip->dst = ee32(0x0A000001U); wolfIP_send_ttl_exceeded(&s, TEST_PRIMARY_IF, ip); ck_assert_uint_eq(last_frame_sent_size, - (uint32_t)(IP_HEADER_LEN + ICMP_TTL_EXCEEDED_SIZE)); + (uint32_t)(IP_HEADER_LEN + 8 + IP_HEADER_LEN)); wolfIP_filter_set_callback(NULL, NULL); wolfIP_filter_set_eth_mask(0); @@ -2811,10 +2813,12 @@ START_TEST(test_send_ttl_exceeded_sets_df) END_TEST #if WOLFIP_ENABLE_FORWARDING -START_TEST(test_wolfip_forward_ttl_exceeded_short_len_does_not_send) +START_TEST(test_wolfip_forward_ttl_exceeded_truncated_header_no_send) { struct wolfIP s; - uint8_t ip_buf[ETH_HEADER_LEN + IP_HEADER_LEN]; + /* Frame shorter than the full IP header: dropped in validation, no + * Time Exceeded can be originated without the quoted header. */ + uint8_t ip_buf[ETH_HEADER_LEN + 10]; struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)ip_buf; ip4 primary_ip = 0x0A000001U; ip4 secondary_ip = 0xC0A80101U; @@ -2840,7 +2844,7 @@ START_TEST(test_wolfip_forward_ttl_exceeded_short_len_does_not_send) } END_TEST -START_TEST(test_regression_forward_ttl_exceeded_short_len_with_options_no_send) +START_TEST(test_regression_forward_ttl_exceeded_options_header_only_quotes_full_header) { struct wolfIP s; /* IHL=10 (40-byte IP header), no transport bytes after it. */ @@ -2868,10 +2872,14 @@ START_TEST(test_regression_forward_ttl_exceeded_short_len_with_options_no_send) memset(((uint8_t *)ip) + ETH_HEADER_LEN + IP_HEADER_LEN, 0x01, 20); fix_ip_checksum_with_hlen(ip, 40); - /* sizeof(ip_buf) == ETH_HEADER_LEN + ip_hlen, exactly 8 bytes short of - * what wolfIP_send_ttl_exceeded would read. */ + /* Header-only datagram: Time Exceeded quotes the full 40-byte header. */ wolfIP_recv_on(&s, TEST_PRIMARY_IF, ip, (uint32_t)sizeof(ip_buf)); - ck_assert_uint_eq(last_frame_sent_size, 0U); + ck_assert_uint_eq(last_frame_sent_size, + (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + 8 + 40)); + ck_assert_uint_eq(last_frame_sent[ETH_HEADER_LEN + IP_HEADER_LEN], + ICMP_TTL_EXCEEDED); + ck_assert_uint_eq(last_frame_sent[ETH_HEADER_LEN + IP_HEADER_LEN + 8], + 0x4A); /* quoted version/IHL */ } END_TEST #endif diff --git a/src/wolfip.c b/src/wolfip.c index 95e41efa..1d21f802 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -2240,8 +2240,9 @@ static void wolfIP_send_ttl_exceeded(struct wolfIP *s, unsigned int if_idx, /* RFC 1812 4.3.2.7 / RFC 1122 3.2.2: an ICMP error message MUST NOT be * originated in response to another ICMP error. If the packet whose TTL * expired is itself an ICMP error (type 3, 4, 5, 11, 12), drop silently. - * The caller guarantees orig_ihl + 8 bytes are present, so reading the - * embedded ICMP type at offset ETH_HEADER_LEN + orig_ihl is in bounds. */ + * The caller guarantees the frame holds the full IP header, so reading + * the embedded ICMP type at offset ETH_HEADER_LEN + orig_ihl is in + * bounds. */ if (orig->proto == WI_IPPROTO_ICMP) { uint8_t orig_type = *(((uint8_t *)orig) + ETH_HEADER_LEN + orig_ihl); if (orig_type == ICMP_DEST_UNREACH || orig_type == ICMP_FRAG_NEEDED || @@ -2249,7 +2250,16 @@ static void wolfIP_send_ttl_exceeded(struct wolfIP *s, unsigned int if_idx, orig_type == 12 /* Parameter Problem */) return; } - orig_copy = orig_ihl + 8; + /* Quote the original header plus up to 8 payload bytes, or as much of + * the datagram as exists. */ + { + uint32_t orig_total = ee16(orig->len); + if (orig_total < orig_ihl) + orig_total = orig_ihl; + orig_copy = orig_ihl + 8; + if (orig_copy > orig_total) + orig_copy = orig_total; + } if (orig_copy > TTL_EXCEEDED_ORIG_PACKET_SIZE_MAX) orig_copy = TTL_EXCEEDED_ORIG_PACKET_SIZE_MAX; icmp_data_len = 8 + orig_copy; /* ICMP header (type+code+csum+unused) + quoted packet */ @@ -9838,13 +9848,6 @@ static inline void ip_recv(struct wolfIP *s, unsigned int if_idx, int broadcast = 0; if (ip->ttl <= 1) { - /* wolfIP_send_ttl_exceeded copies orig_ihl + 8 bytes from - * offset ETH_HEADER_LEN, so the frame must hold the full - * IP header plus 8 transport bytes; the ip_hlen >= 20 - * floor at line 8313 keeps this >= the historical - * ETH_HEADER_LEN + 28 minimum for IHL=5 frames. */ - if (len < (uint32_t)(ETH_HEADER_LEN + ip_hlen + 8)) - return; wolfIP_send_ttl_exceeded(s, if_idx, ip); return; } From 83bc685eace9c113d250048f58d38b951bb1d38d Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 19 Aug 2026 10:12:34 +0200 Subject: [PATCH 04/16] F-10270: copy the triggering TOS into generated ICMP errors --- src/test/unit/unit.c | 2 ++ src/test/unit/unit_tests_ip_arp_recv.c | 41 ++++++++++++++++++++++++++ src/test/unit/unit_tests_misc_edges.c | 25 ++++++++++++++++ src/wolfip.c | 4 +++ 4 files changed, 72 insertions(+) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 4cc14a3a..88b901b0 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -1452,6 +1452,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_core, test_ip_recv_forward_ttl_normal_decremented); tcase_add_test(tc_core, test_ip_recv_forward_ttl1_short_frame_sends_ttl_exceeded); tcase_add_test(tc_core, test_ip_recv_forward_ttl1_partial_payload_quoted); + tcase_add_test(tc_core, test_forward_ttl_exceeded_copies_orig_tos); tcase_add_test(tc_core, test_ip_recv_dest_matches_secondary_iface_ip_is_local); tcase_add_test(tc_core, test_ip_recv_multicast_dst_not_forwarded); tcase_add_test(tc_core, test_arp_recv_htype_not_ethernet_dropped); @@ -1592,6 +1593,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_core, test_fifo_push_no_hwrap_wraps_to_front_succeeds); tcase_add_test(tc_core, test_fifo_push_exact_end_sets_hwrap); tcase_add_test(tc_core, test_wolfip_send_port_unreachable_large_ihl); + tcase_add_test(tc_core, test_wolfip_send_port_unreachable_copies_orig_tos); #if WOLFIP_RAWSOCKETS tcase_add_test(tc_core, test_wolfip_rawsocket_from_fd_negative_fd); #endif /* WOLFIP_RAWSOCKETS */ diff --git a/src/test/unit/unit_tests_ip_arp_recv.c b/src/test/unit/unit_tests_ip_arp_recv.c index 412f4cb4..90f4d2a7 100644 --- a/src/test/unit/unit_tests_ip_arp_recv.c +++ b/src/test/unit/unit_tests_ip_arp_recv.c @@ -1138,6 +1138,47 @@ START_TEST(test_ip_recv_forward_ttl1_partial_payload_quoted) } END_TEST +/* ========================================================================= + * ip_recv: TTL=1 — the Time Exceeded carries the triggering packet's TOS + * ========================================================================= + * RFC 1812 4.3.2.5: the TOS of a generated ICMP error is the TOS of the + * triggering packet. + */ +START_TEST(test_forward_ttl_exceeded_copies_orig_tos) +{ + struct wolfIP s; + uint8_t frame[ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN]; + struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)frame; + ip4 primary_ip = 0x0A000001U; + ip4 secondary_ip = 0xC0A80101U; + ip4 dest_ip = 0xC0A80155U; + ip4 src_ip = 0x0A000002U; + + setup_stack_with_two_ifaces(&s, primary_ip, secondary_ip); + wolfIP_filter_set_callback(NULL, NULL); + last_frame_sent_size = 0; + + memset(frame, 0, sizeof(frame)); + memcpy(ip->eth.dst, s.ll_dev[TEST_PRIMARY_IF].mac, 6); + memcpy(ip->eth.src, "\x01\x02\x03\x04\x05\x06", 6); + ip->eth.type = ee16(ETH_TYPE_IP); + ip->ver_ihl = 0x45; + ip->tos = 0xB8; + ip->ttl = 1; + ip->proto = WI_IPPROTO_UDP; + ip->len = ee16(IP_HEADER_LEN + UDP_HEADER_LEN); + ip->src = ee32(src_ip); + ip->dst = ee32(dest_ip); + fix_ip_checksum(ip); + + ip_recv(&s, TEST_PRIMARY_IF, ip, (uint32_t)sizeof(frame)); + + ck_assert_uint_eq(last_frame_sent[ETH_HEADER_LEN + IP_HEADER_LEN], + ICMP_TTL_EXCEEDED); + ck_assert_uint_eq(last_frame_sent[ETH_HEADER_LEN + 1], 0xB8); +} +END_TEST + /* ========================================================================= * ip_recv: dest matches own IP on secondary interface → is_local=1, no fwd * ========================================================================= diff --git a/src/test/unit/unit_tests_misc_edges.c b/src/test/unit/unit_tests_misc_edges.c index dc1ddab5..20a6822d 100644 --- a/src/test/unit/unit_tests_misc_edges.c +++ b/src/test/unit/unit_tests_misc_edges.c @@ -1201,6 +1201,31 @@ START_TEST(test_wolfip_send_port_unreachable_large_ihl) ck_assert(1); } END_TEST + +START_TEST(test_wolfip_send_port_unreachable_copies_orig_tos) +{ + struct wolfIP s; + uint8_t framebuf[ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN]; + struct wolfIP_ip_packet *orig = (struct wolfIP_ip_packet *)framebuf; + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0a000001U, 0xffffff00U, 0); + last_frame_sent_size = 0; + memset(framebuf, 0, sizeof(framebuf)); + /* RFC 1812 4.3.2.5: the error carries the triggering packet's TOS. */ + orig->ver_ihl = 0x45; + orig->tos = 0xB8; + orig->proto = WI_IPPROTO_UDP; + orig->len = ee16(IP_HEADER_LEN + UDP_HEADER_LEN); + orig->src = ee32(0x0a0000a1U); + orig->dst = ee32(0x0a000001U); + orig->ttl = 64; + wolfIP_send_port_unreachable(&s, TEST_PRIMARY_IF, orig); + ck_assert_uint_eq(last_frame_sent_size, + (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + 8 + IP_HEADER_LEN + 8)); + ck_assert_uint_eq(last_frame_sent[ETH_HEADER_LEN + 1], 0xB8); +} +END_TEST #endif /* ===================================================================== diff --git a/src/wolfip.c b/src/wolfip.c index 1d21f802..a3ffc384 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -2264,6 +2264,8 @@ static void wolfIP_send_ttl_exceeded(struct wolfIP *s, unsigned int if_idx, orig_copy = TTL_EXCEEDED_ORIG_PACKET_SIZE_MAX; icmp_data_len = 8 + orig_copy; /* ICMP header (type+code+csum+unused) + quoted packet */ icmp.type = ICMP_TTL_EXCEEDED; + /* RFC 1812 4.3.2.5: the error carries the triggering packet's TOS. */ + icmp.ip.tos = orig->tos; memcpy(icmp.orig_packet, ((uint8_t *)orig) + ETH_HEADER_LEN, orig_copy); icmp.csum = ee16(icmp_checksum((struct wolfIP_icmp_packet *)&icmp, icmp_data_len)); @@ -2336,6 +2338,8 @@ static void wolfIP_send_port_unreachable(struct wolfIP *s, unsigned int if_idx, icmp_data_len = 8 + orig_copy; icmp.type = ICMP_DEST_UNREACH; icmp.code = ICMP_PORT_UNREACH; + /* RFC 1812 4.3.2.5: the error carries the triggering packet's TOS. */ + icmp.ip.tos = orig->tos; memcpy(icmp.orig_packet, ((uint8_t *)orig) + ETH_HEADER_LEN, orig_copy); icmp.csum = ee16(icmp_checksum((struct wolfIP_icmp_packet *)&icmp, icmp_data_len)); From 2ef1a48c356b90dde2d0cf8623a9edd44cc7a64d Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 19 Aug 2026 10:26:40 +0200 Subject: [PATCH 05/16] F-10256: send ICMP errors from VLAN sub-interfaces via their parent --- src/test/unit/unit.c | 2 + src/test/unit/unit_tests_vlan.c | 120 ++++++++++++++++++++++++++++++++ src/wolfip.c | 30 +++++++- 3 files changed, 150 insertions(+), 2 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 88b901b0..bebc20ac 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -1630,6 +1630,8 @@ Suite *wolf_suite(void) tcase_add_test(tc_proto, test_vlan_tx_oversize_rejected); tcase_add_test(tc_proto, test_vlan_tx_runt_rejected); tcase_add_test(tc_proto, test_vlan_rx_tagged_match_delivered); + tcase_add_test(tc_proto, test_vlan_udp_closed_port_sends_port_unreachable); + tcase_add_test(tc_proto, test_vlan_ttl1_transit_sends_ttl_exceeded); tcase_add_test(tc_proto, test_vlan_rx_tagged_mismatch_dropped); tcase_add_test(tc_proto, test_vlan_rx_untagged_on_physical_ok); tcase_add_test(tc_proto, test_vlan_rx_runt_tagged_dropped); diff --git a/src/test/unit/unit_tests_vlan.c b/src/test/unit/unit_tests_vlan.c index 5631c7d8..5e492799 100644 --- a/src/test/unit/unit_tests_vlan.c +++ b/src/test/unit/unit_tests_vlan.c @@ -184,6 +184,48 @@ static uint32_t inject_tagged_icmp_echo(struct wolfIP *s, unsigned int parent_id return last_frame_sent_size; } +/* Inject a tagged UDP frame (UDP header only, no payload) from + * vlan_remote_mac/src_ip to dst_ip on VLAN 'vid' of the parent. */ +static uint32_t inject_tagged_udp(struct wolfIP *s, unsigned int parent_idx, + const uint8_t *parent_mac, + ip4 src_ip, ip4 dst_ip, uint8_t ttl, + uint16_t dst_port, uint16_t vid) +{ + uint8_t plain[ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN]; + uint8_t tagged[ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN + 4]; + struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)plain; + uint16_t *udp; + uint32_t plain_len; + uint32_t tagged_len; + + memset(plain, 0, sizeof(plain)); + memcpy(plain, parent_mac, 6); + memcpy(plain + 6, vlan_remote_mac, 6); + plain[12] = 0x08; plain[13] = 0x00; + ip->ver_ihl = 0x45; + ip->len = ee16(IP_HEADER_LEN + UDP_HEADER_LEN); + ip->ttl = ttl; + ip->proto = WI_IPPROTO_UDP; + ip->src = ee32(src_ip); + ip->dst = ee32(dst_ip); + fix_ip_checksum(ip); + udp = (uint16_t *)(plain + ETH_HEADER_LEN + IP_HEADER_LEN); + udp[0] = ee16(47911); + udp[1] = ee16(dst_port); + udp[2] = ee16(UDP_HEADER_LEN); + udp[3] = 0; + + plain_len = (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN); + tagged_len = insert_vlan_tag(tagged, sizeof(tagged), plain, plain_len, + vid, 0, 0); + if (!tagged_len) + return 0; + + last_frame_sent_size = 0; + wolfIP_recv_on(s, parent_idx, tagged, tagged_len); + return last_frame_sent_size; +} + /* Build an untagged Ethernet/IPv4 ARP frame sent by vlan_remote_mac. * opcode is ARP_REQUEST or ARP_REPLY; buf must hold sizeof(struct arp_packet). */ static void build_arp_frame(uint8_t *buf, const uint8_t *eth_dst_mac, @@ -995,6 +1037,84 @@ START_TEST(test_vlan_rx_tagged_mismatch_dropped) } END_TEST +/* ========================================================================= + * ICMP error generation from a VLAN sub-interface + * ========================================================================= + * A live VLAN sub-interface has a NULL send callback and delegates to its + * parent; the ICMP error senders must not treat that as "no interface". + */ +START_TEST(test_vlan_udp_closed_port_sends_port_unreachable) +{ + struct wolfIP s; + struct wolfIP_ll_dev *phys; + unsigned int sub_idx = 0xFFFFFFFFu; + uint32_t sent; + int ret; + ip4 remote_nbo; + + setup_vlan_stack(&s); + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 0, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + wolfIP_ipconfig_set_ex(&s, sub_idx, VLAN_SUB100_IP, 0xFFFFFF00U, 0); + phys = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ck_assert_ptr_nonnull(phys); + phys->send = mock_send; + wolfIP_filter_set_callback(NULL, NULL); + remote_nbo = ee32(VLAN_REMOTE_IP); + + /* UDP to a closed port on the sub-interface. */ + sent = inject_tagged_udp(&s, TEST_PRIMARY_IF, phys->mac, + VLAN_REMOTE_IP, VLAN_SUB100_IP, 64, 53, 100); + + /* Port unreachable must go out tagged on the parent. */ + ck_assert_uint_eq(sent, (uint32_t)(ETH_HEADER_LEN + WOLFIP_VLAN_TAG_LEN + + IP_HEADER_LEN + 8 + IP_HEADER_LEN + 8)); + ck_assert_uint_eq(last_frame_sent[12], 0x81u); + ck_assert_uint_eq(last_frame_sent[13], 0x00u); + ck_assert_uint_eq(last_frame_sent[ETH_HEADER_LEN + WOLFIP_VLAN_TAG_LEN + + IP_HEADER_LEN], ICMP_DEST_UNREACH); + ck_assert_uint_eq(last_frame_sent[ETH_HEADER_LEN + WOLFIP_VLAN_TAG_LEN + + IP_HEADER_LEN + 1], ICMP_PORT_UNREACH); + ck_assert_mem_eq(last_frame_sent + ETH_HEADER_LEN + WOLFIP_VLAN_TAG_LEN + + 16, (const void *)&remote_nbo, 4); +} +END_TEST + +START_TEST(test_vlan_ttl1_transit_sends_ttl_exceeded) +{ + struct wolfIP s; + struct wolfIP_ll_dev *phys; + unsigned int sub_idx = 0xFFFFFFFFu; + uint32_t sent; + int ret; + ip4 remote_nbo; + + setup_vlan_stack(&s); + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 0, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + wolfIP_ipconfig_set_ex(&s, sub_idx, VLAN_SUB100_IP, 0xFFFFFF00U, 0); + phys = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ck_assert_ptr_nonnull(phys); + phys->send = mock_send; + wolfIP_filter_set_callback(NULL, NULL); + remote_nbo = ee32(VLAN_REMOTE_IP); + + /* TTL=1 datagram on the sub-interface destined for the physical's + * subnet: transit with the Time Exceeded originated on the sub-interface. */ + sent = inject_tagged_udp(&s, TEST_PRIMARY_IF, phys->mac, + VLAN_REMOTE_IP, 0x0A0A0A99U, 1, 53, 100); + + ck_assert_uint_eq(sent, (uint32_t)(ETH_HEADER_LEN + WOLFIP_VLAN_TAG_LEN + + IP_HEADER_LEN + 8 + IP_HEADER_LEN + 8)); + ck_assert_uint_eq(last_frame_sent[12], 0x81u); + ck_assert_uint_eq(last_frame_sent[13], 0x00u); + ck_assert_uint_eq(last_frame_sent[ETH_HEADER_LEN + WOLFIP_VLAN_TAG_LEN + + IP_HEADER_LEN], ICMP_TTL_EXCEEDED); + ck_assert_mem_eq(last_frame_sent + ETH_HEADER_LEN + WOLFIP_VLAN_TAG_LEN + + 16, (const void *)&remote_nbo, 4); +} +END_TEST + START_TEST(test_vlan_rx_untagged_on_physical_ok) { struct wolfIP s; diff --git a/src/wolfip.c b/src/wolfip.c index a3ffc384..4c3993f4 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -2233,8 +2233,21 @@ static void wolfIP_send_ttl_exceeded(struct wolfIP *s, unsigned int if_idx, #if !CONFIG_IPFILTER (void)icmp_pkt; #endif - if (!ll || !ll->send) + if (!ll) + return; +#if WOLFIP_VLAN + /* Same interface-validity rule as wolfIP_ll_send_frame: an active VLAN + * sub-iface has a NULL send and delegates to its parent. */ + if (ll->vlan_active) { + if (!ll->vlan_parent) + return; + } else if (!ll->send) { + return; + } +#else + if (!ll->send) return; +#endif if (orig_ihl < IP_HEADER_LEN) orig_ihl = IP_HEADER_LEN; /* RFC 1812 4.3.2.7 / RFC 1122 3.2.2: an ICMP error message MUST NOT be @@ -2328,8 +2341,21 @@ static void wolfIP_send_port_unreachable(struct wolfIP *s, unsigned int if_idx, #if !CONFIG_IPFILTER (void)icmp_pkt; #endif - if (!ll || !ll->send) + if (!ll) + return; +#if WOLFIP_VLAN + /* Same interface-validity rule as wolfIP_ll_send_frame: an active VLAN + * sub-iface has a NULL send and delegates to its parent. */ + if (ll->vlan_active) { + if (!ll->vlan_parent) + return; + } else if (!ll->send) { + return; + } +#else + if (!ll->send) return; +#endif if (orig_ihl < IP_HEADER_LEN) orig_ihl = IP_HEADER_LEN; orig_copy = orig_ihl + 8; From 5e837a45655e14cdc9811e5a91fa8930074d8af9 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 19 Aug 2026 10:37:31 +0200 Subject: [PATCH 06/16] F-10257: encapsulate the echo reply at its declared IP length --- src/test/unit/unit_esp.c | 124 +++++++++++++++++++++++++++++++++++++++ src/wolfip.c | 8 ++- 2 files changed, 130 insertions(+), 2 deletions(-) diff --git a/src/test/unit/unit_esp.c b/src/test/unit/unit_esp.c index 9b00868a..972b0483 100644 --- a/src/test/unit/unit_esp.c +++ b/src/test/unit/unit_esp.c @@ -2063,6 +2063,126 @@ START_TEST(test_forward_packet_esp_wrapped) } END_TEST +/* Same contract as the echo-reply path: a forwarded datagram is + * encapsulated at its declared IP length; trailing L2 bytes beyond the + * datagram must not become ESP payload. */ +START_TEST(test_forward_packet_esp_wraps_ip_length_not_frame_length) +{ + struct wolfIP s; + struct wolfIP_ll_dev *ll; + struct wolfIP_ip_packet *sent_ip; + uint8_t peer_mac[6] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; + uint8_t payload[] = { 'f', 'w', 'd', '!' }; + uint8_t buf[LINK_MTU + 64]; + uint32_t frame_len; + + wolfIP_init(&s); + esp_setup(); + esp_add_cbc_test_sas(); + + wolfIP_ipconfig_set(&s, atoip4("192.168.0.1"), 0xFFFFFF00U, 0); + ll = wolfIP_ll_at(&s, 1); + ck_assert_ptr_nonnull(ll); + memcpy(ll->mac, (uint8_t[]){0x00,0x11,0x22,0x33,0x44,0x66}, 6); + ll->send = esp_test_mock_send; + ll->poll = NULL; + wolfIP_ipconfig_set_ex(&s, 1, atoip4(T_SRC), 0xFFFFFF00U, 0); + + /* UDP packet: declared IP total = 20 + 8 + 4 = 32. Append 10 + * trailing L2 bytes and forward with the padded frame length. */ + frame_len = build_udp_ip_packet(buf, sizeof(buf), + atoip4(T_SRC), atoip4(T_DST), + 1234, 5678, payload, sizeof(payload)); + memset(buf + frame_len, 0x5A, 10); + frame_len += 10; + + esp_test_last_frame_size = 0; + memset(esp_test_last_frame, 0, sizeof(esp_test_last_frame)); + wolfIP_forward_packet(&s, 1, (struct wolfIP_ip_packet *)buf, frame_len, + peer_mac, 0); + + ck_assert_uint_gt(esp_test_last_frame_size, 0); + sent_ip = (struct wolfIP_ip_packet *)esp_test_last_frame; + ck_assert_uint_eq(sent_ip->proto, 0x32); + /* Outer IP total = 32 (declared) + 4 (SPI) + 4 (SEQ) + 16 (IV) + + * 2 (padding) + 1 (pad len) + 1 (next header) + 16 (ICV). */ + ck_assert_uint_eq(ee16(sent_ip->len), 76); +} +END_TEST + +#if defined(WOLFSSL_AESGCM_STREAM) +/* The echo reply must be encapsulated at its declared IP length, not the + * received frame length: trailing L2 bytes are not part of the datagram. */ +START_TEST(test_icmp_echo_reply_esp_wraps_ip_length_not_frame_length) +{ + struct wolfIP s; + struct wolfIP_ll_dev *ll; + struct wolfIP_ip_packet *sent_ip; + struct wolfIP_ip_packet *ip; + struct wolfIP_icmp_packet *icmp; + uint8_t buf[LINK_MTU]; + uint32_t frame_len; + int ret; + + wolfIP_init(&s); + esp_setup(); + + ret = wolfIP_esp_sa_new_gcm(0, (uint8_t *)spi_rt, + atoip4(T_SRC), atoip4(T_DST), + ESP_ENC_GCM_RFC4106, + (uint8_t *)k_aes256_gcm, + sizeof(k_aes256_gcm)); + ck_assert_int_eq(ret, 0); + ret = wolfIP_esp_sa_new_gcm(1, (uint8_t *)spi_rt, + atoip4(T_SRC), atoip4(T_DST), + ESP_ENC_GCM_RFC4106, + (uint8_t *)k_aes256_gcm, + sizeof(k_aes256_gcm)); + ck_assert_int_eq(ret, 0); + + /* Interface 0 is local (T_SRC) and captures the reply. */ + ll = wolfIP_ll_at(&s, 0); + ck_assert_ptr_nonnull(ll); + memcpy(ll->mac, (uint8_t[]){0x00,0x11,0x22,0x33,0x44,0x55}, 6); + ll->send = esp_test_mock_send; + wolfIP_ipconfig_set(&s, atoip4(T_SRC), 0xFFFFFF00U, 0); + + /* Echo request T_DST -> T_SRC: declared IP length is header + ICMP + * header only (28); the frame carries 16 trailing bytes beyond the + * datagram. */ + memset(buf, 0, sizeof(buf)); + ip = (struct wolfIP_ip_packet *)buf; + memcpy(ip->eth.dst, ll->mac, 6); + ip->eth.type = ee16(0x0800U); + ip->ver_ihl = 0x45U; + ip->len = ee16(IP_HEADER_LEN + 8); + ip->ttl = 64U; + ip->proto = WI_IPPROTO_ICMP; + ip->src = ee32(atoip4(T_DST)); + ip->dst = ee32(atoip4(T_SRC)); + iphdr_set_checksum(ip); + icmp = (struct wolfIP_icmp_packet *)buf; + icmp->type = ICMP_ECHO_REQUEST; + icmp->code = 0; + icmp->csum = ee16(icmp_checksum(icmp, 8)); + memset(buf + ETH_HEADER_LEN + IP_HEADER_LEN + 8, 0xA5, 16); + frame_len = (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + 8 + 16); + + esp_test_last_frame_size = 0; + memset(esp_test_last_frame, 0, sizeof(esp_test_last_frame)); + wolfIP_recv_ex(&s, 0, buf, frame_len); + + ck_assert_uint_gt(esp_test_last_frame_size, 0); + sent_ip = (struct wolfIP_ip_packet *)esp_test_last_frame; + ck_assert_uint_eq(sent_ip->proto, 0x32); + /* Outer IP total = 20 (header) + 4 (SPI) + 4 (SEQ) + 8 (IV) + 8 + * (payload) + 2 (alignment) + 1 (pad len) + 1 (next header) + 16 + * (ICV). The 16 trailing frame bytes must not be counted. */ + ck_assert_uint_eq(ee16(sent_ip->len), 64); +} +END_TEST +#endif /* WOLFSSL_AESGCM_STREAM */ + static Suite *esp_suite(void) { Suite *s; @@ -2158,6 +2278,10 @@ static Suite *esp_suite(void) tcase_add_test(tc, test_tcp_zero_wnd_probe_esp_wrapped); tcase_add_test(tc, test_tcp_reset_reply_esp_wrapped); tcase_add_test(tc, test_forward_packet_esp_wrapped); + tcase_add_test(tc, test_forward_packet_esp_wraps_ip_length_not_frame_length); +#if defined(WOLFSSL_AESGCM_STREAM) + tcase_add_test(tc, test_icmp_echo_reply_esp_wraps_ip_length_not_frame_length); +#endif suite_add_tcase(s, tc); return s; diff --git a/src/wolfip.c b/src/wolfip.c index 4c3993f4..0a8d2fda 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -4500,7 +4500,9 @@ static void wolfIP_forward_packet(struct wolfIP *s, unsigned int out_if, #ifdef WOLFIP_ESP if (!wolfIP_ll_is_non_ethernet(s, out_if)) { struct wolfIP_ll_dev *ll_esp = wolfIP_ll_at(s, out_if); - int esp_err = esp_send(ll_esp, ip, (uint16_t)(len - ETH_HEADER_LEN)); + /* Encapsulate the datagram at its declared length; bytes past + * the IP total length are L2 padding, not payload. */ + int esp_err = esp_send(ll_esp, ip, (uint16_t)ee16(ip->len)); if (esp_err == 1) { wolfIP_ll_send_frame(s, out_if, ip, len); } @@ -7933,7 +7935,9 @@ static void icmp_input(struct wolfIP *s, unsigned int if_idx, struct wolfIP_ip_p #ifdef WOLFIP_ESP if (!wolfIP_ll_is_non_ethernet(s, if_idx)) { struct wolfIP_ll_dev *ll = wolfIP_ll_at(s, if_idx); - if (esp_send(ll, ip, len - ETH_HEADER_LEN) == 1) { + /* Encapsulate the datagram at its declared length; bytes past + * the IP total length are L2 padding, not payload. */ + if (esp_send(ll, ip, (uint16_t)ee16(ip->len)) == 1) { /* ipsec not configured on this interface. * send plaintext. */ wolfIP_ll_send_frame(s, if_idx, ip, len); From e1f66ae04b0f9618fd49e8518f7f70bc2b073502 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 19 Aug 2026 10:57:08 +0200 Subject: [PATCH 07/16] F-10258: scope ESP unwrap to the declared IP total length --- src/test/unit/unit_esp.c | 58 ++++++++++++++++++++++++++++++++++++++++ src/wolfesp.c | 21 ++++++++++----- 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/src/test/unit/unit_esp.c b/src/test/unit/unit_esp.c index 972b0483..909875b2 100644 --- a/src/test/unit/unit_esp.c +++ b/src/test/unit/unit_esp.c @@ -956,6 +956,63 @@ END_TEST * 3. wildcard dst ip in both in/out SAs, with misc dst address. * 4. wildcard src ip in both in/out SAs, with misc src address. * */ +/* Unwrap must scope the ESP payload to the declared IP total length, not + * the frame length: trailing L2 bytes after the datagram must not shift + * the ICV window. */ +START_TEST(test_unwrap_trailing_frame_bytes_ignored) +{ + static uint8_t buf[LINK_MTU + 256]; + uint8_t ref[64]; + uint32_t frame_len; + uint16_t ip_len; + struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)buf; + uint32_t wrapped_len; + uint32_t padded_len; + int ret; + uint32_t i; + + for (i = 0U; i < sizeof(ref); i++) { + ref[i] = (uint8_t)(i & 0xFFU); + } + + esp_setup(); + ret = wolfIP_esp_sa_new_cbc_hmac(0, (uint8_t *)spi_rt, + atoip4(T_SRC), atoip4(T_DST), + (uint8_t *)k_aes128, sizeof(k_aes128), + ESP_AUTH_SHA256_RFC4868, + (uint8_t *)k_auth16, sizeof(k_auth16), + ESP_ICVLEN_HMAC_128); + ck_assert_int_eq(ret, 0); + ret = wolfIP_esp_sa_new_cbc_hmac(1, (uint8_t *)spi_rt, + atoip4(T_SRC), atoip4(T_DST), + (uint8_t *)k_aes128, sizeof(k_aes128), + ESP_AUTH_SHA256_RFC4868, + (uint8_t *)k_auth16, sizeof(k_auth16), + ESP_ICVLEN_HMAC_128); + ck_assert_int_eq(ret, 0); + + frame_len = build_ip_packet(buf, sizeof(buf), WI_IPPROTO_UDP, + ref, sizeof(ref)); + ip_len = (uint16_t)(frame_len - ETH_HEADER_LEN); + + ret = esp_transport_wrap(ip, &ip_len); + ck_assert_int_eq(ret, 0); + wrapped_len = (uint32_t)ip_len + ETH_HEADER_LEN; + + /* Append trailing L2 bytes after the datagram and pass the padded + * length, as a driver with FCS/DMA slack would. */ + memset(buf + wrapped_len, 0x5A, 12); + padded_len = wrapped_len + 12; + + ret = esp_transport_unwrap(ip, &padded_len); + ck_assert_int_eq(ret, 0); + ck_assert_uint_eq(ip->proto, WI_IPPROTO_UDP); + ck_assert_mem_eq(ip->data, ref, sizeof(ref)); + /* The dispatched length must exclude the trailing bytes. */ + ck_assert_uint_eq(padded_len, (uint32_t)(ETH_HEADER_LEN + ee16(ip->len))); +} +END_TEST + START_TEST(test_unwrap_ip_filtering) { static uint8_t buf[LINK_MTU + 256]; @@ -2230,6 +2287,7 @@ static Suite *esp_suite(void) tcase_add_test(tc, test_unwrap_below_min_len); tcase_add_test(tc, test_unwrap_pad_too_big); tcase_add_test(tc, test_unwrap_invalid_pad_pattern); + tcase_add_test(tc, test_unwrap_trailing_frame_bytes_ignored); tcase_add_test(tc, test_unwrap_ip_filtering); suite_add_tcase(s, tc); diff --git a/src/wolfesp.c b/src/wolfesp.c index 05397833..5644b4a5 100644 --- a/src/wolfesp.c +++ b/src/wolfesp.c @@ -1352,13 +1352,18 @@ esp_transport_unwrap(struct wolfIP_ip_packet *ip, uint32_t * frame_len) memset(spi, 0, sizeof(spi)); - if (*frame_len <= (ETH_HEADER_LEN + IP_HEADER_LEN)) { - ESP_LOG("error: esp: malformed frame: %d\n", *frame_len); - return -1; + /* Scope the ESP payload to the declared IP total length; bytes past + * the datagram (L2 slack) are not part of the ESP extent. */ + { + uint32_t ip_total = ee16(ip->len); + if (ip_total < IP_HEADER_LEN || + *frame_len < (uint32_t)(ETH_HEADER_LEN + ip_total)) { + ESP_LOG("error: esp: malformed frame: %d\n", *frame_len); + return -1; + } + esp_len = ip_total - IP_HEADER_LEN; } - esp_len = *frame_len - ETH_HEADER_LEN - IP_HEADER_LEN; - /* If not at least SPI and sequence, something wrong. */ if (esp_len < (ESP_SPI_LEN + ESP_SEQ_LEN)) { ESP_LOG("error: esp: malformed packet: %d\n", esp_len); @@ -1552,9 +1557,11 @@ esp_transport_unwrap(struct wolfIP_ip_packet *ip, uint32_t * frame_len) memmove(ip->data, ip->data + ESP_SPI_LEN + ESP_SEQ_LEN + iv_len, esp_len - (ESP_SPI_LEN + ESP_SEQ_LEN + iv_len)); - /* subtract ESP header from frame_len and ip.len. */ - *frame_len = *frame_len - (iv_len + ESP_SPI_LEN + ESP_SEQ_LEN); + /* subtract the ESP header from the IP total length (ip->len is in + * host order here); normalize the frame length to the datagram so + * trailing L2 bytes are excluded from dispatch. */ ip->len = ee16(ip->len) - (iv_len + ESP_SPI_LEN + ESP_SEQ_LEN); + *frame_len = (uint32_t)(ETH_HEADER_LEN + ip->len); /* subtract ESP trailer from frame_len and ip.len. */ *frame_len = *frame_len - (pad_len + ESP_PADDING_LEN + From e4080ed8e6713c277c7d0dbd103c38bd13a28b66 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 19 Aug 2026 11:01:49 +0200 Subject: [PATCH 08/16] F-8571: add ESP state persistence callbacks for reboot survival --- src/test/unit/unit_esp.c | 122 +++++++++++++++++++++++++++++++++++++++ src/wolfesp.c | 58 ++++++++++++++++++- wolfesp.h | 28 +++++++++ 3 files changed, 206 insertions(+), 2 deletions(-) diff --git a/src/test/unit/unit_esp.c b/src/test/unit/unit_esp.c index 909875b2..764ae17b 100644 --- a/src/test/unit/unit_esp.c +++ b/src/test/unit/unit_esp.c @@ -2167,6 +2167,127 @@ START_TEST(test_forward_packet_esp_wraps_ip_length_not_frame_length) } END_TEST +/* ESP state persistence callbacks: the application can restore and flush + * the volatile per-SA sequence/replay state across restarts. */ +static uint8_t state_test_spi[ESP_SPI_LEN] = {0xDE, 0xAD, 0xBE, 0xEF}; +static uint8_t state_fresh_spi[ESP_SPI_LEN] = {0x11, 0x22, 0x33, 0x44}; +static int state_read_calls = 0; +static int state_write_calls = 0; +static uint32_t state_last_oseq = 0; + +static int state_test_read_cb(const uint8_t *spi, uint32_t *oseq, + uint32_t *hi_seq, uint32_t *bitmap) +{ + state_read_calls++; + if (memcmp(spi, state_test_spi, ESP_SPI_LEN) == 0) { + *oseq = 42; + *hi_seq = 43; + *bitmap = 1; + } + return 0; +} + +static int state_test_write_cb(const uint8_t *spi, uint32_t oseq, + uint32_t hi_seq, uint32_t bitmap) +{ + state_write_calls++; + if (memcmp(spi, state_test_spi, ESP_SPI_LEN) == 0) { + state_last_oseq = oseq; + } + (void)hi_seq; + (void)bitmap; + return 0; +} + +static uint32_t +state_test_wire_seq(const struct wolfIP_ip_packet *ip) +{ + uint32_t seq = 0; + memcpy(&seq, ip->data + ESP_SPI_LEN, sizeof(seq)); + return ee32(seq); +} + +START_TEST(test_esp_state_persistence_callbacks) +{ + static uint8_t buf[LINK_MTU + 256]; + struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)buf; + uint16_t ip_len; + uint32_t frame_len; + int ret; + + esp_setup(); + state_read_calls = 0; + state_write_calls = 0; + state_last_oseq = 0; + + ret = wolfIP_esp_state_set_cbs(state_test_write_cb, state_test_read_cb); + ck_assert_int_eq(ret, 0); + + /* Creating an SA with a known SPI must invoke the read callback and + * restore the persisted outbound sequence. */ + ret = wolfIP_esp_sa_new_cbc_hmac(0, state_test_spi, + atoip4(T_SRC), atoip4(T_DST), + (uint8_t *)k_aes128, sizeof(k_aes128), + ESP_AUTH_SHA256_RFC4868, + (uint8_t *)k_auth16, sizeof(k_auth16), + ESP_ICVLEN_HMAC_128); + ck_assert_int_eq(ret, 0); + ck_assert_int_eq(state_read_calls, 1); + + /* The next outbound packet must carry restored oseq + 1, and the + * write callback must see the advanced sequence. */ + frame_len = build_ip_packet(buf, sizeof(buf), WI_IPPROTO_UDP, + (const uint8_t *)"abcd", 4); + ip_len = (uint16_t)(frame_len - ETH_HEADER_LEN); + ret = esp_transport_wrap(ip, &ip_len); + ck_assert_int_eq(ret, 0); + ck_assert_uint_eq(state_test_wire_seq(ip), 43U); + ck_assert_uint_eq(state_last_oseq, 43U); + ck_assert_int_eq(state_write_calls, 1); + + /* Deleting the SA must flush the final state once more. */ + wolfIP_esp_sa_del(0, state_test_spi); + ck_assert_uint_eq(state_last_oseq, 43U); + ck_assert_int_eq(state_write_calls, 2); + + /* An SA with an unknown SPI starts fresh (oseq 0 -> first seq 1). */ + ret = wolfIP_esp_sa_new_cbc_hmac(0, state_fresh_spi, + atoip4(T_SRC), atoip4(T_DST), + (uint8_t *)k_aes128, sizeof(k_aes128), + ESP_AUTH_SHA256_RFC4868, + (uint8_t *)k_auth16, sizeof(k_auth16), + ESP_ICVLEN_HMAC_128); + ck_assert_int_eq(ret, 0); + ck_assert_int_eq(state_read_calls, 2); + frame_len = build_ip_packet(buf, sizeof(buf), WI_IPPROTO_UDP, + (const uint8_t *)"wxyz", 4); + ip_len = (uint16_t)(frame_len - ETH_HEADER_LEN); + ret = esp_transport_wrap(ip, &ip_len); + ck_assert_int_eq(ret, 0); + ck_assert_uint_eq(state_test_wire_seq(ip), 1U); + + /* sa_del_all flushes every live SA before wiping. */ + wolfIP_esp_sa_del_all(); + ck_assert_int_eq(state_write_calls, 4); + + /* No callbacks registered: creation and wrap still work. */ + ret = wolfIP_esp_state_set_cbs(NULL, NULL); + ck_assert_int_eq(ret, 0); + ret = wolfIP_esp_sa_new_cbc_hmac(0, state_test_spi, + atoip4(T_SRC), atoip4(T_DST), + (uint8_t *)k_aes128, sizeof(k_aes128), + ESP_AUTH_SHA256_RFC4868, + (uint8_t *)k_auth16, sizeof(k_auth16), + ESP_ICVLEN_HMAC_128); + ck_assert_int_eq(ret, 0); + frame_len = build_ip_packet(buf, sizeof(buf), WI_IPPROTO_UDP, + (const uint8_t *)"qrst", 4); + ip_len = (uint16_t)(frame_len - ETH_HEADER_LEN); + ret = esp_transport_wrap(ip, &ip_len); + ck_assert_int_eq(ret, 0); +} +END_TEST + #if defined(WOLFSSL_AESGCM_STREAM) /* The echo reply must be encapsulated at its declared IP length, not the * received frame length: trailing L2 bytes are not part of the datagram. */ @@ -2260,6 +2381,7 @@ static Suite *esp_suite(void) tcase_add_test(tc, test_sa_pool_exhaustion); tcase_add_test(tc, test_sa_del_frees_slot); tcase_add_test(tc, test_sa_del_all); + tcase_add_test(tc, test_esp_state_persistence_callbacks); suite_add_tcase(s, tc); /* Replay window */ diff --git a/src/wolfesp.c b/src/wolfesp.c index 5644b4a5..12375887 100644 --- a/src/wolfesp.c +++ b/src/wolfesp.c @@ -30,6 +30,39 @@ static wolfIP_esp_sa out_sa_list[WOLFIP_ESP_NUM_SA]; static uint16_t in_sa_num = WOLFIP_ESP_NUM_SA; static uint16_t out_sa_num = WOLFIP_ESP_NUM_SA; +/* optional state persistence callbacks, set by the application. */ +static wolfIP_esp_state_write_cb esp_state_write_cb = NULL; +static wolfIP_esp_state_read_cb esp_state_read_cb = NULL; + +int wolfIP_esp_state_set_cbs(wolfIP_esp_state_write_cb write, + wolfIP_esp_state_read_cb read) +{ + esp_state_write_cb = write; + esp_state_read_cb = read; + return 0; +} + +/* Hand the SA's volatile state to the persistence callback (if any). */ +static void +esp_state_save(const wolfIP_esp_sa *sa) +{ + if (esp_state_write_cb) { + (void)esp_state_write_cb(sa->spi, sa->replay.oseq, + sa->replay.hi_seq, sa->replay.bitmap); + } +} + +/* Restore persisted state for a fresh SA (if the application provides + * any). A non-zero callback return keeps the fresh state. */ +static void +esp_state_restore(wolfIP_esp_sa *sa) +{ + if (esp_state_read_cb) { + (void)esp_state_read_cb(sa->spi, &sa->replay.oseq, + &sa->replay.hi_seq, &sa->replay.bitmap); + } +} + /* for err and important messages */ #define ESP_LOG(fmt, ...) LOG(fmt, ##__VA_ARGS__) @@ -64,15 +97,25 @@ int wolfIP_esp_init(void) return err; } +static const uint8_t zero_spi[ESP_SPI_LEN] = {0x00, 0x00, 0x00, 0x00}; + void wolfIP_esp_sa_del_all(void) { + size_t i; + /* flush the volatile state of every live SA before wiping. */ + for (i = 0; i < WOLFIP_ESP_NUM_SA; i++) { + if (memcmp(in_sa_list[i].spi, zero_spi, ESP_SPI_LEN) != 0) { + esp_state_save(&in_sa_list[i]); + } + if (memcmp(out_sa_list[i].spi, zero_spi, ESP_SPI_LEN) != 0) { + esp_state_save(&out_sa_list[i]); + } + } wc_ForceZero(in_sa_list, sizeof(in_sa_list)); wc_ForceZero(out_sa_list, sizeof(out_sa_list)); return; } -static const uint8_t zero_spi[ESP_SPI_LEN] = {0x00, 0x00, 0x00, 0x00}; - /* Get an SA by spi. * If spi is null, return the first empty slot (an SA with all zero SPI). * */ @@ -108,6 +151,7 @@ void wolfIP_esp_sa_del(int in, uint8_t * spi) wolfIP_esp_sa * sa = NULL; sa = esp_sa_get(in, spi); if (sa != NULL) { + esp_state_save(sa); wc_ForceZero(sa, sizeof(*sa)); } return; @@ -207,6 +251,8 @@ int wolfIP_esp_sa_new_gcm(int in, uint8_t * spi, ip4 src, ip4 dst, err = -1; } + esp_state_restore(new_sa); + ESP_DEBUG("info: esp_sa_new_gcm: %s\n", in == 1 ? "in" : "out"); return err; } @@ -298,6 +344,8 @@ int wolfIP_esp_sa_new_hmac(int in, uint8_t * spi, ip4 src, ip4 dst, new_sa->auth_key_len = auth_key_len; new_sa->icv_len = icv_len; + esp_state_restore(new_sa); + ESP_DEBUG("info: esp_sa_new_hmac: %s\n", in == 1 ? "in" : "out"); return 0; } @@ -354,6 +402,8 @@ int wolfIP_esp_sa_new_cbc_hmac(int in, uint8_t * spi, ip4 src, ip4 dst, new_sa->auth_key_len = auth_key_len; new_sa->icv_len = icv_len; + esp_state_restore(new_sa); + ESP_DEBUG("info: esp_sa_new_cbc_hmac: %s\n", in == 1 ? "in" : "out"); return 0; } @@ -405,6 +455,8 @@ wolfIP_esp_sa_new_des3_hmac(int in, uint8_t * spi, ip4 src, ip4 dst, new_sa->auth_key_len = auth_key_len; new_sa->icv_len = icv_len; + esp_state_restore(new_sa); + ESP_DEBUG("info: esp_sa_new_des3_hmac: %s\n", in == 1 ? "in" : "out"); return 0; } @@ -1513,6 +1565,7 @@ esp_transport_unwrap(struct wolfIP_ip_packet *ip, uint32_t * frame_len) /* icv verified for hmacs and aeads at this point. now safe to commit the * sequence to the replay window (RFC 4303 s3.4.3). */ esp_replay_commit(&esp_sa->replay, seq); + esp_state_save(esp_sa); /* Payload is now verified and decrypted. We can now parse * the ESP trailer for next header and pad_len. */ @@ -1675,6 +1728,7 @@ esp_transport_wrap(struct wolfIP_ip_packet *ip, uint16_t * ip_len) ESP_LOG("error: oseq overflow\n"); return -1; } + esp_state_save(esp_sa); seq_n = ee32(esp_sa->replay.oseq); memcpy(payload, &seq_n, sizeof(seq_n)); payload += ESP_SEQ_LEN; diff --git a/wolfesp.h b/wolfesp.h index a2101cf1..1d316715 100644 --- a/wolfesp.h +++ b/wolfesp.h @@ -92,6 +92,34 @@ struct wolfIP_esp_sa { }; typedef struct wolfIP_esp_sa wolfIP_esp_sa; +/* ESP state persistence callbacks. + * + * The volatile per-SA state (outbound sequence number and inbound replay + * window) is reset by every wolfIP_esp_init/sa creation. An application + * that must keep ESP traffic alive across a restart registers these + * callbacks and persists the state in non-volatile storage. + * + * The write callback is invoked on every state change: outbound sequence + * advance, inbound replay window commit, SA deletion. It is called once + * per event; the application owns the persistence policy (e.g. batch or + * debounce before committing to flash). A non-zero return value is + * reported but does not alter stack state. + * + * The read callback is invoked when a new SA is created, keyed by SPI. + * It may fill the (fresh) oseq/hi_seq/bitmap values with persisted state. + * Return 0 if state was restored, anything else to start the SA fresh. + * */ +typedef int (*wolfIP_esp_state_write_cb)(const uint8_t *spi, + uint32_t oseq, + uint32_t hi_seq, + uint32_t bitmap); +typedef int (*wolfIP_esp_state_read_cb)(const uint8_t *spi, + uint32_t *oseq, + uint32_t *hi_seq, + uint32_t *bitmap); +int wolfIP_esp_state_set_cbs(wolfIP_esp_state_write_cb write, + wolfIP_esp_state_read_cb read); + int wolfIP_esp_init(void); void wolfIP_esp_sa_del_all(void); void wolfIP_esp_sa_del(int in, uint8_t * spi); From b52ca27d1292d5c21baf1b262b15c42eb695a7fa Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 19 Aug 2026 11:09:12 +0200 Subject: [PATCH 09/16] F-10272: add per-connection IP_TOS setsockopt for outgoing DSCP --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_tcp_flow.c | 57 +++++++++++++++++++++++++++++ src/wolfip.c | 30 ++++++++++++++- wolfip.h | 8 ++++ 4 files changed, 95 insertions(+), 1 deletion(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index bebc20ac..b1a74b86 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -575,6 +575,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_tcp_input_syn_sent_synack_invalid_ack_rejected); tcase_add_test(tc_utils, test_tcp_input_syn_listen_does_not_scale_syn_window); tcase_add_test(tc_utils, test_tcp_input_syn_sent_does_not_scale_synack_window); + tcase_add_test(tc_utils, test_tcp_setsockopt_ip_tos_applied_to_outbound_syn); tcase_add_test(tc_utils, test_tcp_parse_sack_wraparound_block_accepted); tcase_add_test(tc_utils, test_tcp_parse_options_stops_on_truncated_or_invalid_option_length); tcase_add_test(tc_utils, test_tcp_parse_options_returns_when_frame_has_no_option_bytes); diff --git a/src/test/unit/unit_tests_tcp_flow.c b/src/test/unit/unit_tests_tcp_flow.c index dab988fc..06f188f8 100644 --- a/src/test/unit/unit_tests_tcp_flow.c +++ b/src/test/unit/unit_tests_tcp_flow.c @@ -4748,3 +4748,60 @@ START_TEST(test_tcp_input_established_fin_out_of_order_no_transition) ck_assert_uint_eq(ts->events & CB_EVENT_CLOSED, 0); } END_TEST + +/* An application-selected Diffserv value must be carried in the IPv4 TOS + * field of every outgoing segment; validated via setsockopt. */ +START_TEST(test_tcp_setsockopt_ip_tos_applied_to_outbound_syn) +{ + struct wolfIP s; + int tcp_sd; + struct wolfIP_tcp_seg *syn; + struct wolfIP_sockaddr_in sin; + int tos = 0xA0; + int bad = 256; + int neg = -1; + socklen_t len; + static const uint8_t tos_peer_mac[6] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + + tcp_sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_STREAM, WI_IPPROTO_TCP); + ck_assert_int_gt(tcp_sd, 0); + + /* Range and argument validation. */ + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, tcp_sd, WOLFIP_SOL_IP, + WOLFIP_IP_TOS, &bad, sizeof(bad)), -WOLFIP_EINVAL); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, tcp_sd, WOLFIP_SOL_IP, + WOLFIP_IP_TOS, &neg, sizeof(neg)), -WOLFIP_EINVAL); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, tcp_sd, WOLFIP_SOL_IP, + WOLFIP_IP_TOS, NULL, sizeof(tos)), -WOLFIP_EINVAL); + + /* A valid value is accepted and readable via getsockopt. */ + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, tcp_sd, WOLFIP_SOL_IP, + WOLFIP_IP_TOS, &tos, sizeof(tos)), 0); + len = sizeof(tos); + ck_assert_int_eq(wolfIP_sock_getsockopt(&s, tcp_sd, WOLFIP_SOL_IP, + WOLFIP_IP_TOS, &tos, &len), 0); + ck_assert_int_eq(tos, 0xA0); + + /* The connect SYN must carry the selected TOS in its IPv4 header. + * Seed the ARP entry so the SYN is transmitted, not queued. */ + arp_store_neighbor(&s, TEST_PRIMARY_IF, 0x0A000002U, (uint8_t *)tos_peer_mac); + last_frame_sent_size = 0; + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = ee16(5002); + sin.sin_addr.s_addr = ee32(0x0A000002U); + ck_assert_int_eq(wolfIP_sock_connect(&s, tcp_sd, + (struct wolfIP_sockaddr *)&sin, sizeof(sin)), -WOLFIP_EAGAIN); + + /* The SYN is queued in the tx fifo; the poll loop transmits it. */ + wolfIP_poll(&s, 1000); + ck_assert_uint_gt(last_frame_sent_size, 0); + syn = (struct wolfIP_tcp_seg *)last_frame_sent; + ck_assert_uint_eq(syn->flags, TCP_FLAG_SYN); + ck_assert_uint_eq(syn->ip.tos, (uint8_t)0xA0); +} +END_TEST diff --git a/src/wolfip.c b/src/wolfip.c index 0a8d2fda..28ae7d0d 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -1219,6 +1219,7 @@ struct tsocket { uint8_t nexthop_mac[6]; #endif uint8_t if_idx; + uint8_t tos; /* outgoing IPv4 TOS/DS field (setsockopt WOLFIP_IP_TOS) */ uint8_t recv_ttl; uint8_t last_pkt_ttl; uint8_t close_notify_pending; /* slot reserved for a final CB_EVENT_CLOSED */ @@ -4533,7 +4534,7 @@ static int ip_output_add_header(struct tsocket *t, struct wolfIP_ip_packet *ip, ip->src = ee32(t->local_ip); ip->dst = ee32(t->remote_ip); ip->ver_ihl = 0x45; - ip->tos = 0; + ip->tos = t->tos; ip->len = ee16(len); ip->flags_fo = (proto == WI_IPPROTO_TCP) ? ee16(0x4000U) : 0; ip->ttl = 64; @@ -7090,6 +7091,17 @@ int wolfIP_sock_setsockopt(struct wolfIP *s, int sockfd, int level, int optname, ts->recv_ttl = enable ? 1 : 0; return 0; } + if (level == WOLFIP_SOL_IP && optname == WOLFIP_IP_TOS) { + int tos; + if (!optval || optlen < (socklen_t)sizeof(int)) + return -WOLFIP_EINVAL; + memcpy(&tos, optval, sizeof(int)); + if (tos < 0 || tos > 255) { + return -WOLFIP_EINVAL; + } + ts->tos = (uint8_t)tos; + return 0; + } #ifdef IP_MULTICAST if (level == WOLFIP_SOL_IP && IS_SOCKET_UDP(sockfd)) { if (optname == WOLFIP_IP_ADD_MEMBERSHIP || @@ -7261,6 +7273,22 @@ int wolfIP_sock_getsockopt(struct wolfIP *s, int sockfd, int level, int optname, } return 0; } + if (level == WOLFIP_SOL_IP && optname == WOLFIP_IP_TOS) { + int value; + if (!optval || !optlen || *optlen < (socklen_t)sizeof(int)) + return -WOLFIP_EINVAL; +#if WOLFIP_PACKET_SOCKETS + if (ps) + return -WOLFIP_EINVAL; +#endif + if (ts) { + value = ts->tos; + memcpy(optval, &value, sizeof(int)); + *optlen = sizeof(int); + return 0; + } + return -WOLFIP_EINVAL; + } #ifdef IP_MULTICAST if (level == WOLFIP_SOL_IP && IS_SOCKET_UDP(sockfd)) { if (optname == WOLFIP_IP_MULTICAST_TTL || diff --git a/wolfip.h b/wolfip.h index cc29948d..2de6dad9 100644 --- a/wolfip.h +++ b/wolfip.h @@ -64,6 +64,14 @@ typedef unsigned long size_t; #endif #endif +#ifndef WOLFIP_IP_TOS +#ifdef IP_TOS +#define WOLFIP_IP_TOS IP_TOS +#else +#define WOLFIP_IP_TOS 1 +#endif +#endif + #ifndef WOLFIP_SO_DONTROUTE #ifdef SO_DONTROUTE #define WOLFIP_SO_DONTROUTE SO_DONTROUTE From 6fc38d133b428e2190e7773a773aa960b84031d9 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 19 Aug 2026 12:07:53 +0200 Subject: [PATCH 10/16] F-10266: document the no-IPv4-fragmentation forwarding deviation --- docs/advanced_ipv4_howto.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/advanced_ipv4_howto.md b/docs/advanced_ipv4_howto.md index 44340967..e9dcace6 100644 --- a/docs/advanced_ipv4_howto.md +++ b/docs/advanced_ipv4_howto.md @@ -249,6 +249,34 @@ When forwarding is enabled, the optional static-route API is also compiled in route lookup performs longest-prefix matching across connected subnets and static routes together. +### Deviations from RFC 791/1122: no IPv4 fragmentation + +wolfIP is an endpoint stack first; the forwarding path deliberately omits IPv4 +fragmentation and reassembly. These are documented, intended deviations, not +bugs: + +- **No egress fragmentation.** A forwarded datagram is handed to the egress + interface at its declared IP total length, with no comparison against the + egress IP MTU and no fragment generation. If the frame exceeds the link MTU, + `wolfIP_ll_send_frame()` rejects it and the datagram is **dropped silently** — + no ICMP Destination Unreachable (Fragmentation Needed, type 3 code 4) is + sent, regardless of the DF bit. Datagrams that fit the egress MTU are + forwarded normally. +- **No reassembly.** The IP input path drops every fragment (MF set or non-zero + fragment offset); the stack never reassembles fragmented datagrams. +- **Locally generated UDP.** `wolfIP_sock_sendto()` fails with `-1` when the + datagram does not fit the socket's IP MTU (headers excluded) — a clean error + to the caller instead of a silent drop. +- **TCP.** The advertised and accepted MSS is clamped to the MTU, so TCP + segments never require fragmentation. + +The practical consequence for a router build: keep every link's MTU at or +above the largest datagram that traverses it (the usual 1500-byte Ethernet +baseline). A higher-MTU upstream (jumbo frames, 1280-byte tunnels aside) that +injects datagrams larger than a downstream link's IP MTU will see them dropped +at the egress with no diagnostic ICMP. If your topology cannot guarantee that, +IPv4 fragmentation is out of scope for wolfIP and a different stack is needed. + ### Wiring a router `src/test/test_wolfssl_forwarding.c` builds a two-interface router: interface 0 From 696ec4312f3054419b061135078638ac12939fb8 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 19 Aug 2026 12:11:26 +0200 Subject: [PATCH 11/16] F-10275: enforce strict T1 < T2 < lease in DHCP lease timers --- docs/advanced_ipv4_howto.md | 8 ++++---- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_dhcp_edges.c | 27 +++++++++++++++++++++++---- src/wolfip.c | 23 +++++++++++------------ 4 files changed, 39 insertions(+), 20 deletions(-) diff --git a/docs/advanced_ipv4_howto.md b/docs/advanced_ipv4_howto.md index e9dcace6..e7302883 100644 --- a/docs/advanced_ipv4_howto.md +++ b/docs/advanced_ipv4_howto.md @@ -272,10 +272,10 @@ bugs: The practical consequence for a router build: keep every link's MTU at or above the largest datagram that traverses it (the usual 1500-byte Ethernet -baseline). A higher-MTU upstream (jumbo frames, 1280-byte tunnels aside) that -injects datagrams larger than a downstream link's IP MTU will see them dropped -at the egress with no diagnostic ICMP. If your topology cannot guarantee that, -IPv4 fragmentation is out of scope for wolfIP and a different stack is needed. +baseline). A higher-MTU upstream (e.g. jumbo frames) that injects datagrams +larger than a downstream link's IP MTU will see them dropped at the egress +with no diagnostic ICMP. If your topology cannot guarantee that, IPv4 +fragmentation is out of scope for wolfIP and a different stack is needed. ### Wiring a router diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index b1a74b86..c100a0f2 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -1377,6 +1377,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_core, test_dhcp_schedule_lease_timer_rebind_lt_renew_fixed); tcase_add_test(tc_core, test_dhcp_schedule_lease_timer_rebind_gt_lease_clamped); tcase_add_test(tc_core, test_dhcp_schedule_lease_timer_explicit_t1_t2); + tcase_add_test(tc_core, test_dhcp_schedule_lease_timer_t1_t2_equal_lease_resets_defaults); tcase_add_test(tc_core, test_dhcp_msg_type_returns_offer); tcase_add_test(tc_core, test_dhcp_msg_type_returns_nak); tcase_add_test(tc_core, test_dhcp_msg_type_returns_ack); diff --git a/src/test/unit/unit_tests_dhcp_edges.c b/src/test/unit/unit_tests_dhcp_edges.c index bb0d22c6..6ed132b2 100644 --- a/src/test/unit/unit_tests_dhcp_edges.c +++ b/src/test/unit/unit_tests_dhcp_edges.c @@ -123,12 +123,31 @@ START_TEST(test_dhcp_schedule_lease_timer_rebind_lt_renew_fixed) wolfIP_init(&s); s.last_tick = 0U; - /* rebind_s < renew_s → set rebind = renew */ + /* rebind_s < renew_s violates the strict T1 < T2 < lease ordering: + * both are replaced by the client defaults, not coerced to equality. */ dhcp_schedule_lease_timer(&s, 100U, 80U, 20U); - /* rebind_s (20) < renew_s (80), so rebind becomes 80 */ - ck_assert_uint_eq(s.dhcp_renew_at, 80000U); - ck_assert_uint_eq(s.dhcp_rebind_at, 80000U); + /* T1 = 100/2 = 50, T2 = 100*7/8 = 87 */ + ck_assert_uint_eq(s.dhcp_renew_at, 50000U); + ck_assert_uint_eq(s.dhcp_rebind_at, 87000U); + ck_assert_uint_eq(s.dhcp_lease_expires, 100000U); +} +END_TEST + +START_TEST(test_dhcp_schedule_lease_timer_t1_t2_equal_lease_resets_defaults) +{ + struct wolfIP s; + + wolfIP_init(&s); + s.last_tick = 0U; + + /* T1 == lease and T2 == lease violate the strict ordering; the client + * defaults must replace them (T1 = 50%, T2 = 87.5%). */ + dhcp_schedule_lease_timer(&s, 3600U, 3600U, 3600U); + + ck_assert_uint_eq(s.dhcp_renew_at, 1800U * 1000U); + ck_assert_uint_eq(s.dhcp_rebind_at, 3150U * 1000U); + ck_assert_uint_eq(s.dhcp_lease_expires, 3600U * 1000U); } END_TEST diff --git a/src/wolfip.c b/src/wolfip.c index 28ae7d0d..a7d365db 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -8080,20 +8080,19 @@ static void dhcp_schedule_lease_timer(struct wolfIP *s, if (!s || lease_s == 0) return; - if (renew_s == 0 || renew_s > lease_s) { - renew_s = lease_s / 2U; - if (renew_s == 0) - renew_s = 1U; - } - if (rebind_s == 0 || rebind_s > lease_s) { + /* RFC 2131 3.2: the client's timers must satisfy T1 < T2 < lease. + * Server-supplied timer options that violate the strict ordering are + * replaced with the client defaults (T1 = 50% of the lease, T2 = 87.5% + * of the lease) instead of being coerced into T1 == T2 or T2 == lease. */ + if (renew_s == 0U || rebind_s == 0U || + renew_s >= lease_s || rebind_s >= lease_s || rebind_s <= renew_s) { + renew_s = lease_s / 2U; rebind_s = (uint32_t)(((uint64_t)lease_s * 7U) / 8U); - if (rebind_s == 0) - rebind_s = 1U; } - if (rebind_s < renew_s) - rebind_s = renew_s; - if (renew_s > lease_s) - renew_s = lease_s; + if (renew_s == 0U) + renew_s = 1U; + if (rebind_s <= renew_s) + rebind_s = renew_s + 1U; if (rebind_s > lease_s) rebind_s = lease_s; From 9c3bc4b0825dfa403b4677768087d3c733b2ac1b Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 19 Aug 2026 12:46:03 +0200 Subject: [PATCH 12/16] F-9806: validate DHCP chaddr/yiaddr and defer config commit to ACK --- src/test/unit/unit.c | 3 + src/test/unit/unit_tests_dhcp_edges.c | 246 ++++++++++++++++++++++++++ src/test/unit/unit_tests_dns_dhcp.c | 19 +- src/test/unit/unit_tests_proto.c | 2 + src/wolfip.c | 54 +++++- 5 files changed, 314 insertions(+), 10 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index c100a0f2..fc64dd1f 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -431,6 +431,9 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_dhcp_poll_offer_and_ack); tcase_add_test(tc_utils, test_dhcp_poll_renewing_ack_binds_client); tcase_add_test(tc_utils, test_dhcp_poll_rebinding_ack_binds_client); + tcase_add_test(tc_utils, test_dhcp_poll_reply_wrong_chaddr_rejected); + tcase_add_test(tc_utils, test_dhcp_poll_offer_zero_yiaddr_rejected); + tcase_add_test(tc_utils, test_dhcp_poll_offer_defers_commit_until_ack); tcase_add_test(tc_utils, test_regression_dhcp_nak_deconfigures_address_during_renew_and_rebind); tcase_add_test(tc_utils, test_dns_callback_ptr_response); tcase_add_test(tc_utils, test_udp_try_recv_short_frame); diff --git a/src/test/unit/unit_tests_dhcp_edges.c b/src/test/unit/unit_tests_dhcp_edges.c index 6ed132b2..810ccff6 100644 --- a/src/test/unit/unit_tests_dhcp_edges.c +++ b/src/test/unit/unit_tests_dhcp_edges.c @@ -926,6 +926,7 @@ START_TEST(test_dhcp_parse_ack_with_renewal_and_rebind_times) primary->ip = client_ip; build_dhcp_msg_base(&s, &msg, DHCP_ACK); + msg.yiaddr = ee32(client_ip); p = (uint8_t *)msg.options + 3; append_opt4(&p, DHCP_OPTION_SERVER_ID, server_ip); append_opt4(&p, DHCP_OPTION_SUBNET_MASK, 0xFFFFFF00U); @@ -963,6 +964,7 @@ START_TEST(test_dhcp_parse_ack_dns_already_set_skipped) primary->ip = client_ip; build_dhcp_msg_base(&s, &msg, DHCP_ACK); + msg.yiaddr = ee32(client_ip); p = (uint8_t *)msg.options + 3; append_opt4(&p, DHCP_OPTION_SERVER_ID, server_ip); append_opt4(&p, DHCP_OPTION_SUBNET_MASK, 0xFFFFFF00U); @@ -995,6 +997,7 @@ START_TEST(test_dhcp_parse_ack_inner_pad_bytes_skipped) primary->ip = client_ip; build_dhcp_msg_base(&s, &msg, DHCP_ACK); + msg.yiaddr = ee32(client_ip); p = (uint8_t *)msg.options + 3; /* pad byte */ p[0] = 0; p += 1; @@ -1764,3 +1767,246 @@ START_TEST(test_dhcp_decline_wire_format) ck_assert_uint_eq(DHCP_OPT_data_to_u32(opt), client_ip); } END_TEST + +/* ------------------------------------------------------------------------- + * Rogue-server hardening: chaddr validation, yiaddr sanity, and deferring + * the interface reconfiguration from the OFFER to the confirming ACK. + * ------------------------------------------------------------------------- */ + +static struct tsocket * +dhcp_edges_rx_socket(struct wolfIP *s) +{ + s->dhcp_udp_sd = wolfIP_sock_socket(s, AF_INET, IPSTACK_SOCK_DGRAM, + WI_IPPROTO_UDP); + if (s->dhcp_udp_sd <= 0) + return NULL; + return &s->udpsockets[SOCKET_UNMARK(s->dhcp_udp_sd)]; +} + +/* A forged reply that is not addressed to this client's hardware address + * must be dropped: no state change, no configuration. */ +START_TEST(test_dhcp_poll_reply_wrong_chaddr_rejected) +{ + struct wolfIP s; + struct dhcp_msg msg; + struct dhcp_option *opt; + struct tsocket *ts; + uint32_t xid = 0x12345678U; + int ret; + + wolfIP_init(&s); + mock_link_init(&s); + ts = dhcp_edges_rx_socket(&s); + ck_assert_ptr_nonnull(ts); + s.dhcp_xid = xid; + + /* Forged OFFER: matching xid, but chaddr is all zeros. */ + memset(&msg, 0, sizeof(msg)); + msg.op = BOOT_REPLY; + msg.hlen = 6; + msg.magic = ee32(DHCP_MAGIC); + msg.xid = ee32(xid); + msg.yiaddr = ee32(0x0A000064U); + opt = (struct dhcp_option *)msg.options; + opt->code = DHCP_OPTION_MSG_TYPE; + opt->len = 1; + opt->data[0] = DHCP_OFFER; + opt = (struct dhcp_option *)((uint8_t *)opt + 3); + opt->code = DHCP_OPTION_SERVER_ID; + opt->len = 4; + opt->data[0] = 0x0A; + opt->data[1] = 0x00; + opt->data[2] = 0x00; + opt->data[3] = 0x01; + opt = (struct dhcp_option *)((uint8_t *)opt + 6); + opt->code = DHCP_OPTION_END; + + s.dhcp_state = DHCP_DISCOVER_SENT; + enqueue_udp_rx(ts, &msg, sizeof(msg), DHCP_SERVER_PORT); + ret = dhcp_poll(&s); + ck_assert_int_eq(ret, -1); + ck_assert_int_eq(s.dhcp_state, DHCP_DISCOVER_SENT); + ck_assert_uint_eq(s.dhcp_ip, 0U); + ck_assert_uint_eq(s.dhcp_server_ip, 0U); + ck_assert_uint_eq(wolfIP_primary_ipconf(&s)->ip, 0U); +} +END_TEST + +/* An OFFER with yiaddr 0.0.0.0 must be rejected: no broken configuration + * may be committed to the interface. */ +START_TEST(test_dhcp_poll_offer_zero_yiaddr_rejected) +{ + struct wolfIP s; + struct dhcp_msg msg; + struct dhcp_option *opt; + struct tsocket *ts; + uint32_t xid = 0x12345678U; + int ret; + + wolfIP_init(&s); + mock_link_init(&s); + ts = dhcp_edges_rx_socket(&s); + ck_assert_ptr_nonnull(ts); + s.dhcp_xid = xid; + + memset(&msg, 0, sizeof(msg)); + msg.op = BOOT_REPLY; + msg.hlen = 6; + msg.magic = ee32(DHCP_MAGIC); + msg.xid = ee32(xid); + /* yiaddr intentionally left 0.0.0.0 */ + memcpy(msg.chaddr, wolfIP_ll_at(&s, WOLFIP_PRIMARY_IF_IDX)->mac, 6); + opt = (struct dhcp_option *)msg.options; + opt->code = DHCP_OPTION_MSG_TYPE; + opt->len = 1; + opt->data[0] = DHCP_OFFER; + opt = (struct dhcp_option *)((uint8_t *)opt + 3); + opt->code = DHCP_OPTION_SERVER_ID; + opt->len = 4; + opt->data[0] = 0x0A; + opt->data[1] = 0x00; + opt->data[2] = 0x00; + opt->data[3] = 0x01; + opt = (struct dhcp_option *)((uint8_t *)opt + 6); + opt->code = DHCP_OPTION_SUBNET_MASK; + opt->len = 4; + opt->data[0] = 0xFF; + opt->data[1] = 0x00; + opt->data[2] = 0x00; + opt->data[3] = 0x00; + opt = (struct dhcp_option *)((uint8_t *)opt + 6); + opt->code = DHCP_OPTION_END; + + s.dhcp_state = DHCP_DISCOVER_SENT; + enqueue_udp_rx(ts, &msg, sizeof(msg), DHCP_SERVER_PORT); + ret = dhcp_poll(&s); + ck_assert_int_eq(ret, 0); + /* Offer refused: no state advance, no configuration anywhere. */ + ck_assert_int_eq(s.dhcp_state, DHCP_DISCOVER_SENT); + ck_assert_uint_eq(s.dhcp_ip, 0U); + ck_assert_uint_eq(wolfIP_primary_ipconf(&s)->ip, 0U); +} +END_TEST + +/* A valid OFFER stashes the lease without touching the interface; the + * confirming ACK commits ip, mask, gateway, and DNS. */ +START_TEST(test_dhcp_poll_offer_defers_commit_until_ack) +{ + struct wolfIP s; + struct dhcp_msg msg; + struct dhcp_option *opt; + struct tsocket *ts; + struct ipconf *primary; + uint32_t xid = 0x12345678U; + int ret; + + wolfIP_init(&s); + mock_link_init(&s); + ts = dhcp_edges_rx_socket(&s); + ck_assert_ptr_nonnull(ts); + primary = wolfIP_primary_ipconf(&s); + ck_assert_ptr_nonnull(primary); + s.dhcp_xid = xid; + + /* --- Valid OFFER: accepted, but nothing applied yet. --- */ + memset(&msg, 0, sizeof(msg)); + msg.op = BOOT_REPLY; + msg.hlen = 6; + msg.magic = ee32(DHCP_MAGIC); + msg.xid = ee32(xid); + msg.yiaddr = ee32(0x0A000064U); + memcpy(msg.chaddr, wolfIP_ll_at(&s, WOLFIP_PRIMARY_IF_IDX)->mac, 6); + opt = (struct dhcp_option *)msg.options; + opt->code = DHCP_OPTION_MSG_TYPE; + opt->len = 1; + opt->data[0] = DHCP_OFFER; + opt = (struct dhcp_option *)((uint8_t *)opt + 3); + opt->code = DHCP_OPTION_SERVER_ID; + opt->len = 4; + opt->data[0] = 0x0A; + opt->data[1] = 0x00; + opt->data[2] = 0x00; + opt->data[3] = 0x01; + opt = (struct dhcp_option *)((uint8_t *)opt + 6); + opt->code = DHCP_OPTION_SUBNET_MASK; + opt->len = 4; + opt->data[0] = 0xFF; + opt->data[1] = 0xFF; + opt->data[2] = 0xFF; + opt->data[3] = 0x00; + opt = (struct dhcp_option *)((uint8_t *)opt + 6); + opt->code = DHCP_OPTION_END; + + s.dhcp_state = DHCP_DISCOVER_SENT; + enqueue_udp_rx(ts, &msg, sizeof(msg), DHCP_SERVER_PORT); + ret = dhcp_poll(&s); + ck_assert_int_eq(ret, 0); + ck_assert_int_eq(s.dhcp_state, DHCP_REQUEST_SENT); + ck_assert_uint_eq(s.dhcp_ip, 0x0A000064U); + /* The interface is untouched until the ACK. */ + ck_assert_uint_eq(primary->ip, 0U); + ck_assert_uint_eq(primary->mask, 0U); + + /* --- Confirming ACK: commits the full configuration. --- */ + memset(&msg, 0, sizeof(msg)); + msg.op = BOOT_REPLY; + msg.hlen = 6; + msg.magic = ee32(DHCP_MAGIC); + msg.xid = ee32(xid); + msg.yiaddr = ee32(0x0A000064U); + memcpy(msg.chaddr, wolfIP_ll_at(&s, WOLFIP_PRIMARY_IF_IDX)->mac, 6); + opt = (struct dhcp_option *)msg.options; + opt->code = DHCP_OPTION_MSG_TYPE; + opt->len = 1; + opt->data[0] = DHCP_ACK; + opt = (struct dhcp_option *)((uint8_t *)opt + 3); + opt->code = DHCP_OPTION_SERVER_ID; + opt->len = 4; + opt->data[0] = 0x0A; + opt->data[1] = 0x00; + opt->data[2] = 0x00; + opt->data[3] = 0x01; + opt = (struct dhcp_option *)((uint8_t *)opt + 6); + opt->code = DHCP_OPTION_SUBNET_MASK; + opt->len = 4; + opt->data[0] = 0xFF; + opt->data[1] = 0xFF; + opt->data[2] = 0xFF; + opt->data[3] = 0x00; + opt = (struct dhcp_option *)((uint8_t *)opt + 6); + opt->code = DHCP_OPTION_ROUTER; + opt->len = 4; + opt->data[0] = 0x0A; + opt->data[1] = 0x00; + opt->data[2] = 0x00; + opt->data[3] = 0x01; + opt = (struct dhcp_option *)((uint8_t *)opt + 6); + opt->code = DHCP_OPTION_DNS; + opt->len = 4; + opt->data[0] = 0x08; + opt->data[1] = 0x08; + opt->data[2] = 0x08; + opt->data[3] = 0x08; + opt = (struct dhcp_option *)((uint8_t *)opt + 6); + opt->code = DHCP_OPTION_LEASE_TIME; + opt->len = 4; + opt->data[0] = 0x00; + opt->data[1] = 0x00; + opt->data[2] = 0x00; + opt->data[3] = 0x78; /* 120 s */ + opt = (struct dhcp_option *)((uint8_t *)opt + 6); + opt->code = DHCP_OPTION_END; + + enqueue_udp_rx(ts, &msg, sizeof(msg), DHCP_SERVER_PORT); + ret = dhcp_poll(&s); + ck_assert_int_eq(ret, 0); + /* The confirming ACK commits the config and enters RFC 4331 DAD; + * complete the probes to reach BOUND. */ + ck_assert_int_eq(s.dhcp_state, DHCP_DAD); + dhcp_test_complete_dad(&s); + ck_assert_uint_eq(primary->ip, 0x0A000064U); + ck_assert_uint_eq(primary->mask, 0xFFFFFF00U); + ck_assert_uint_eq(primary->gw, 0x0A000001U); + ck_assert_uint_eq(s.dns_server, 0x08080808U); +} +END_TEST diff --git a/src/test/unit/unit_tests_dns_dhcp.c b/src/test/unit/unit_tests_dns_dhcp.c index c62a845a..0e67b59a 100644 --- a/src/test/unit/unit_tests_dns_dhcp.c +++ b/src/test/unit/unit_tests_dns_dhcp.c @@ -37,6 +37,7 @@ static void build_dhcp_ack_msg(struct dhcp_msg *msg, uint32_t server_ip, uint32_ memset(msg, 0, sizeof(*msg)); msg->op = BOOT_REPLY; + msg->hlen = 6; msg->magic = ee32(DHCP_MAGIC); opt = (struct dhcp_option *)msg->options; opt->code = DHCP_OPTION_MSG_TYPE; @@ -162,7 +163,8 @@ START_TEST(test_dhcp_parse_offer_and_ack) ck_assert_int_eq(dhcp_parse_offer(&s, &msg, sizeof(msg)), 0); ck_assert_uint_eq(s.dhcp_ip, offer_ip); ck_assert_uint_eq(s.dhcp_server_ip, server_ip); - ck_assert_uint_eq(primary->mask, mask); + /* The offered netmask is stashed; the interface is committed at the ACK. */ + ck_assert_uint_eq(s.dhcp_offered_mask, mask); ck_assert_int_eq(s.dhcp_state, DHCP_REQUEST_SENT); s.last_tick = 1000U; @@ -301,7 +303,9 @@ START_TEST(test_dhcp_parse_offer_defaults_mask_when_missing) ck_assert_int_eq(dhcp_parse_offer(&s, &msg, sizeof(msg)), 0); ck_assert_uint_eq(s.dhcp_ip, offer_ip); ck_assert_uint_eq(s.dhcp_server_ip, server_ip); - ck_assert_uint_eq(primary->mask, mask); + /* Default 24-bit netmask stashed for the ACK; interface untouched. */ + ck_assert_uint_eq(s.dhcp_offered_mask, mask); + ck_assert_uint_eq(primary->mask, 0U); ck_assert_int_eq(s.dhcp_state, DHCP_REQUEST_SENT); } END_TEST @@ -5444,6 +5448,7 @@ START_TEST(test_dhcp_poll_offer_and_ack) memset(&msg, 0, sizeof(msg)); msg.op = BOOT_REPLY; + msg.hlen = 6; msg.magic = ee32(DHCP_MAGIC); msg.yiaddr = ee32(0x0A000064U); opt = (struct dhcp_option *)msg.options; @@ -5468,6 +5473,7 @@ START_TEST(test_dhcp_poll_offer_and_ack) opt->code = DHCP_OPTION_END; opt->len = 0; + memcpy(msg.chaddr, wolfIP_ll_at(&s, WOLFIP_PRIMARY_IF_IDX)->mac, 6); s.dhcp_state = DHCP_DISCOVER_SENT; enqueue_udp_rx(ts, &msg, sizeof(msg), DHCP_SERVER_PORT); ret = dhcp_poll(&s); @@ -5476,6 +5482,7 @@ START_TEST(test_dhcp_poll_offer_and_ack) memset(&msg, 0, sizeof(msg)); msg.op = BOOT_REPLY; + msg.hlen = 6; msg.magic = ee32(DHCP_MAGIC); opt = (struct dhcp_option *)msg.options; opt->code = DHCP_OPTION_MSG_TYPE; @@ -5520,6 +5527,8 @@ START_TEST(test_dhcp_poll_offer_and_ack) opt->code = DHCP_OPTION_END; opt->len = 0; + msg.yiaddr = ee32(0x0A000064U); + memcpy(msg.chaddr, wolfIP_ll_at(&s, WOLFIP_PRIMARY_IF_IDX)->mac, 6); s.dhcp_state = DHCP_REQUEST_SENT; enqueue_udp_rx(ts, &msg, sizeof(msg), DHCP_SERVER_PORT); ret = dhcp_poll(&s); @@ -5558,6 +5567,8 @@ START_TEST(test_dhcp_poll_renewing_ack_binds_client) primary->ip = client_ip; build_dhcp_ack_msg(&msg, server_ip, mask, router_ip, dns_ip); msg.xid = ee32(s.dhcp_xid); + msg.yiaddr = ee32(client_ip); + memcpy(msg.chaddr, wolfIP_ll_at(&s, WOLFIP_PRIMARY_IF_IDX)->mac, 6); enqueue_udp_rx(ts, &msg, sizeof(msg), DHCP_SERVER_PORT); ret = dhcp_poll(&s); @@ -5602,6 +5613,8 @@ START_TEST(test_dhcp_poll_rebinding_ack_binds_client) primary->ip = client_ip; build_dhcp_ack_msg(&msg, server_ip, mask, router_ip, dns_ip); msg.xid = ee32(s.dhcp_xid); + msg.yiaddr = ee32(client_ip); + memcpy(msg.chaddr, wolfIP_ll_at(&s, WOLFIP_PRIMARY_IF_IDX)->mac, 6); enqueue_udp_rx(ts, &msg, sizeof(msg), DHCP_SERVER_PORT); ret = dhcp_poll(&s); @@ -5637,6 +5650,7 @@ START_TEST(test_regression_dhcp_nak_deconfigures_address_during_renew_and_rebind memset(&msg, 0, sizeof(msg)); msg.op = BOOT_REPLY; + msg.hlen = 6; msg.magic = ee32(DHCP_MAGIC); msg.xid = ee32(s.dhcp_xid); opt = (struct dhcp_option *)msg.options; @@ -5645,6 +5659,7 @@ START_TEST(test_regression_dhcp_nak_deconfigures_address_during_renew_and_rebind opt->data[0] = DHCP_NAK; opt = (struct dhcp_option *)((uint8_t *)opt + 3); opt->code = DHCP_OPTION_END; + memcpy(msg.chaddr, wolfIP_ll_at(&s, WOLFIP_PRIMARY_IF_IDX)->mac, 6); wolfIP_ipconfig_set(&s, 0x0A000064U, 0xFFFFFF00U, 0x0A000001U); s.dhcp_state = DHCP_RENEWING; diff --git a/src/test/unit/unit_tests_proto.c b/src/test/unit/unit_tests_proto.c index 32501bc5..690f2266 100644 --- a/src/test/unit/unit_tests_proto.c +++ b/src/test/unit/unit_tests_proto.c @@ -7037,8 +7037,10 @@ START_TEST(test_regression_dhcp_nak_restarts_configuration) /* Build a minimal DHCPNAK message (type 6) */ memset(&msg, 0, sizeof(msg)); msg.op = 2; /* BOOT_REPLY */ + msg.hlen = 6; msg.magic = ee32(DHCP_MAGIC); msg.xid = ee32(0x12345678U); + memcpy(msg.chaddr, wolfIP_ll_at(&s, WOLFIP_PRIMARY_IF_IDX)->mac, 6); opt = (struct dhcp_option *)msg.options; opt->code = DHCP_OPTION_MSG_TYPE; opt->len = 1; diff --git a/src/wolfip.c b/src/wolfip.c index a7d365db..d6afd4e1 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -1406,6 +1406,7 @@ struct wolfIP { uint8_t dhcp_dad_probes; /* DAD probes sent (0 = DAD inactive) */ ip4 dhcp_server_ip; /* DHCP server IP */ ip4 dhcp_ip; /* IP address assigned by DHCP */ + uint32_t dhcp_offered_mask; /* netmask from the accepted OFFER */ uint64_t dhcp_renew_at; /* Renewal time (T1) */ uint64_t dhcp_rebind_at; /* Rebind time (T2) */ uint64_t dhcp_lease_expires; /* Lease expiration time */ @@ -8427,6 +8428,20 @@ static int dhcp_opt_stream_next(struct dhcp_opt_stream *st, uint8_t *code, } } +/* A lease address must be a usable unicast host address: not 0.0.0.0, + * not the limited broadcast, not multicast, and not the broadcast of + * its own subnet. */ +static int dhcp_lease_ip_sane(uint32_t ip, uint32_t mask) +{ + if (ip == 0U || ip == 0xFFFFFFFFU) + return 0; + if (wolfIP_ip_is_multicast(ip)) + return 0; + if (mask != 0U && ((ip | mask) == 0xFFFFFFFFU)) + return 0; + return 1; +} + static int dhcp_parse_offer(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_len) { struct dhcp_opt_stream st; @@ -8434,7 +8449,6 @@ static int dhcp_parse_offer(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg int saw_server_id = 0; uint32_t ip; uint32_t netmask = DHCP_DEFAULT_24BIT_NETMASK; - struct ipconf *primary = wolfIP_primary_ipconf(s); if (msg_len < DHCP_HEADER_LEN) return -1; if (msg->op != BOOT_REPLY) @@ -8489,11 +8503,12 @@ static int dhcp_parse_offer(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg if (!saw_end || !saw_server_id) return -1; ip = ee32(msg->yiaddr); - if (primary) { - primary->ip = ip; - primary->mask = netmask; - } + if (!dhcp_lease_ip_sane(ip, netmask)) + return -1; + /* Stash the offer; the interface is not reconfigured + * until the server's ACK confirms the lease. */ s->dhcp_ip = ip; + s->dhcp_offered_mask = netmask; dhcp_cancel_timer(s); s->dhcp_state = DHCP_REQUEST_SENT; return 0; @@ -8554,7 +8569,9 @@ static int dhcp_parse_ack(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_l struct dhcp_opt_stream st; int saw_end = 0; int saw_server_id = 0; + int saw_offer_ip = 0; struct ipconf *primary = wolfIP_primary_ipconf(s); + uint32_t lease_ip = 0; uint32_t lease_s = 0; uint32_t renew_s = 0; uint32_t rebind_s = 0; @@ -8606,11 +8623,12 @@ static int dhcp_parse_ack(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_l return -1; s->dhcp_server_ip = val; saw_server_id = 1; - } else if (primary && code == DHCP_OPTION_OFFER_IP) { + } else if (code == DHCP_OPTION_OFFER_IP) { if (len < 4) return -1; val = DHCP_OPT_data_to_u32((struct dhcp_option *)idata); - primary->ip = val; + lease_ip = val; + saw_offer_ip = 1; } else if (primary && code == DHCP_OPTION_SUBNET_MASK) { if (len < 4) return -1; @@ -8642,13 +8660,24 @@ static int dhcp_parse_ack(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_l } if (!saw_end) return -1; + /* The lease address is option 50 (the requested IP) when the + * server echoes it, otherwise the yiaddr it committed; either + * way it must be a usable unicast address before it is applied + * to the interface. The offered netmask applies when the ACK + * carries none. */ + if (!saw_offer_ip) + lease_ip = ee32(msg->yiaddr); + if (primary && primary->mask == 0) + primary->mask = s->dhcp_offered_mask; /* RFC 2131: the IP-address-lease-time option (51) is mandatory * in a DHCPACK. lease_s is only ever set by that option (and a * short option already returns -1 above), so lease_s != 0 means * it was present with a valid nonzero duration. Without it the * lease would be bound with no expiry/renewal timer. */ if (primary && saw_server_id && lease_s != 0 && - (primary->ip != 0) && (primary->mask != 0)) { + (primary->mask != 0) && + dhcp_lease_ip_sane(lease_ip, primary->mask)) { + primary->ip = lease_ip; dhcp_cancel_timer(s); s->dhcp_ip = primary->ip; #ifdef ETHERNET @@ -8698,6 +8727,15 @@ static int dhcp_poll(struct wolfIP *s) (struct wolfIP_sockaddr *)&sin, &sl); if (len < 0) return -1; + /* A reply must be addressed to this client's hardware address. The + * client's MAC never changes mid-transaction, so a reply carrying any + * other chaddr is not addressed to this client and is dropped. */ + { + struct wolfIP_ll_dev *ll = wolfIP_ll_at(s, WOLFIP_PRIMARY_IF_IDX); + if (!ll || msg.hlen != 6U || + memcmp(msg.chaddr, ll->mac, 6) != 0) + return -1; + } if ((s->dhcp_state == DHCP_DISCOVER_SENT) && (dhcp_parse_offer(s, &msg, (uint32_t)len) == 0)) dhcp_send_request(s); else if (s->dhcp_state == DHCP_REQUEST_SENT || From 43b3573027459ef26f4045edd32b577bb07f71fb Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 19 Aug 2026 13:28:13 +0200 Subject: [PATCH 13/16] F-9807: re-arm on re-SYN and fast-fail the pre-accept listener pin --- src/test/unit/unit.c | 6 + src/test/unit/unit_tests_tcp_flow.c | 456 ++++++++++++++++++++++++++++ src/wolfip.c | 156 ++++++++-- 3 files changed, 587 insertions(+), 31 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index fc64dd1f..73ef1e88 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -579,6 +579,12 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_tcp_input_syn_listen_does_not_scale_syn_window); tcase_add_test(tc_utils, test_tcp_input_syn_sent_does_not_scale_synack_window); tcase_add_test(tc_utils, test_tcp_setsockopt_ip_tos_applied_to_outbound_syn); + tcase_add_test(tc_utils, test_tcp_listener_syn_lock_others_rst_resyn_retransmits_rearms); + tcase_add_test(tc_utils, test_tcp_listener_lock_reclaimed_at_255s_via_ctrl_rto); + tcase_add_test(tc_utils, test_tcp_listener_sustained_lock_one_syn_per_window); + tcase_add_test(tc_utils, test_tcp_listener_rst_from_holding_4tuple_releases_lock); + tcase_add_test(tc_utils, test_tcp_listener_preaccept_accept_reverts_port); + tcase_add_test(tc_utils, test_tcp_listener_preaccept_timeout_reverts_port); tcase_add_test(tc_utils, test_tcp_parse_sack_wraparound_block_accepted); tcase_add_test(tc_utils, test_tcp_parse_options_stops_on_truncated_or_invalid_option_length); tcase_add_test(tc_utils, test_tcp_parse_options_returns_when_frame_has_no_option_bytes); diff --git a/src/test/unit/unit_tests_tcp_flow.c b/src/test/unit/unit_tests_tcp_flow.c index 06f188f8..e364fbca 100644 --- a/src/test/unit/unit_tests_tcp_flow.c +++ b/src/test/unit/unit_tests_tcp_flow.c @@ -4805,3 +4805,459 @@ START_TEST(test_tcp_setsockopt_ip_tos_applied_to_outbound_syn) ck_assert_uint_eq(syn->ip.tos, (uint8_t)0xA0); } END_TEST + +/* ------------------------------------------------------------------ + * TCP listener lock (F-9807): a single wire SYN moves the listener + * socket itself into TCP_SYN_RCVD (wolfIP has no SYN backlog); while + * locked, other clients are RST'd. Recovery paths covered here: + * control-RTO expiry, RST from the holding 4-tuple, re-SYN + * retransmission (re-arms the control RTO), and the pre-accept + * fast-fail that reclaims a port whose handshake completed before + * accept() (which would otherwise pin it in ESTABLISHED forever). + * ------------------------------------------------------------------ */ + +#define LLK_LISTEN_PORT 4000U +#define LLK_LOCAL_IP 0x0A000001U +#define LLK_NET_MASK 0xFFFFFF00U +#define LLK_ATT_IP 0x0A000099U +#define LLK_VICTIM_IP 0x0A000050U + +static const struct wolfIP_tcp_seg *llk_last_tcp(void) +{ + const struct wolfIP_tcp_seg *seg; + + if (last_frame_sent_count == 0) + return NULL; + if (last_frame_sent_size < + (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + TCP_HEADER_LEN)) + return NULL; + seg = (const struct wolfIP_tcp_seg *)last_frame_sent; + if (ee16(seg->ip.eth.type) != ETH_TYPE_IP) + return NULL; + if (seg->ip.proto != WI_IPPROTO_TCP) + return NULL; + return seg; +} + +/* tmr_rto stores a timer id (not a heap index): scan by id. */ +static uint32_t llk_timer_expires(struct wolfIP *s, uint32_t id) +{ + uint32_t i; + + if (id == NO_TIMER) + return 0; + for (i = 0; i < s->timers.size; i++) { + if (s->timers.timers[i].id == id) + return (uint32_t)s->timers.timers[i].expires; + } + return 0; +} + +/* Keep the attacker's learned ARP entry fresh past ARP_AGING_TIMEOUT_MS + * (a live network would re-answer the ARP request). */ +static void llk_keep_arp_fresh(struct wolfIP *s, ip4 ip) +{ + uint8_t mac[6] = { 0xAA, 0xBB, 0xCC, 0x00, 0x00, 0x01 }; + + arp_store_neighbor(s, TEST_PRIMARY_IF, ip, mac); +} + +static int llk_open_listener(struct wolfIP *s) +{ + int fd; + struct wolfIP_sockaddr_in sin; + + fd = wolfIP_sock_socket(s, AF_INET, IPSTACK_SOCK_STREAM, WI_IPPROTO_TCP); + ck_assert_int_gt(fd, 0); + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = ee16((uint16_t)LLK_LISTEN_PORT); + sin.sin_addr.s_addr = ee32(LLK_LOCAL_IP); + ck_assert_int_eq(wolfIP_sock_bind(s, fd, (struct wolfIP_sockaddr *)&sin, + sizeof(sin)), 0); + /* The backlog argument is discarded by the stack (no SYN backlog). */ + ck_assert_int_eq(wolfIP_sock_listen(s, fd, 16), 0); + return fd; +} + +static void llk_attacker_syn(struct wolfIP *s, ip4 src_ip, uint16_t src_port, + uint32_t seq, uint64_t now) +{ + inject_tcp_segment(s, TEST_PRIMARY_IF, src_ip, LLK_LOCAL_IP, + src_port, (uint16_t)LLK_LISTEN_PORT, seq, 0, + TCP_FLAG_SYN); + (void)wolfIP_poll(s, now); +} + +/* Complete the handshake from the peer side using the ISN read from the + * wire (internal seq is host-order, so no ee32()). */ +static void llk_complete_handshake(struct wolfIP *s, struct tsocket *lsn, + ip4 src_ip, uint16_t src_port, uint64_t now) +{ + inject_tcp_segment(s, TEST_PRIMARY_IF, src_ip, LLK_LOCAL_IP, + src_port, (uint16_t)LLK_LISTEN_PORT, 2, + lsn->sock.tcp.seq + 1, TCP_FLAG_ACK); + (void)wolfIP_poll(s, now); +} + +/* One SYN puts the listener in SYN_RCVD; other clients are RST'd; a + * re-SYN from the holding 4-tuple retransmits the SYN-ACK and re-arms + * the control RTO from the base value instead of being dropped. */ +START_TEST(test_tcp_listener_syn_lock_others_rst_resyn_retransmits_rearms) +{ + struct wolfIP s; + int fd; + struct tsocket *lsn; + const struct wolfIP_tcp_seg *out; + uint32_t armed_expires; + uint32_t frames_before; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, LLK_LOCAL_IP, LLK_NET_MASK, 0); + fd = llk_open_listener(&s); + lsn = &s.tcpsockets[SOCKET_UNMARK(fd)]; + ck_assert_int_eq(lsn->sock.tcp.state, TCP_LISTEN); + ck_assert_int_eq(lsn->sock.tcp.is_listener, 1); + + /* Pending-only ARP policy: the peers are known neighbors, so the + * SYN-ACK/RST replies actually reach the wire. */ + llk_keep_arp_fresh(&s, LLK_ATT_IP); + llk_keep_arp_fresh(&s, LLK_VICTIM_IP); + + /* Attacker A: one SYN, then silence. */ + llk_attacker_syn(&s, LLK_ATT_IP, 41000, 1, 0); + + /* The listener socket itself is the half-open connection. */ + ck_assert_int_eq(lsn->sock.tcp.state, TCP_SYN_RCVD); + ck_assert_int_eq(lsn->sock.tcp.is_listener, 1); + ck_assert_int_eq(lsn->sock.tcp.ctrl_rto_retries, 0); + ck_assert_uint_eq(lsn->sock.tcp.rto, TCP_RTO_MIN_MS); + ck_assert_uint_ne(lsn->sock.tcp.tmr_rto, NO_TIMER); + out = llk_last_tcp(); + ck_assert_ptr_nonnull(out); + ck_assert(out->flags & (TCP_FLAG_SYN | TCP_FLAG_ACK)); + + /* Victim B: fresh SYN to the same port while the listener is locked. + * No backlog, so B is refused with RST. */ + frames_before = last_frame_sent_count; + inject_tcp_segment(&s, TEST_PRIMARY_IF, LLK_VICTIM_IP, LLK_LOCAL_IP, + 42000, (uint16_t)LLK_LISTEN_PORT, 5, 0, TCP_FLAG_SYN); + (void)wolfIP_poll(&s, 1); + + ck_assert_int_eq(lsn->sock.tcp.state, TCP_SYN_RCVD); + ck_assert_uint_eq(last_frame_sent_count, frames_before + 1); + out = llk_last_tcp(); + ck_assert_ptr_nonnull(out); + ck_assert(out->flags & TCP_FLAG_RST); + ck_assert_uint_eq(ee16(out->src_port), LLK_LISTEN_PORT); + ck_assert_uint_eq(ee16(out->dst_port), 42000); + + /* Victim B sends a confused SYN-ACK: unacceptable ACK in SYN_RCVD + * -> RST again, listener still locked. */ + frames_before = last_frame_sent_count; + inject_tcp_segment(&s, TEST_PRIMARY_IF, LLK_VICTIM_IP, LLK_LOCAL_IP, + 42000, (uint16_t)LLK_LISTEN_PORT, 6, 0xDEADBEEFU, + TCP_FLAG_SYN | TCP_FLAG_ACK); + (void)wolfIP_poll(&s, 2); + + ck_assert_int_eq(lsn->sock.tcp.state, TCP_SYN_RCVD); + ck_assert_uint_eq(last_frame_sent_count, frames_before + 1); + out = llk_last_tcp(); + ck_assert_ptr_nonnull(out); + ck_assert(out->flags & TCP_FLAG_RST); + ck_assert_uint_eq(ee16(out->dst_port), 42000); + + /* Attacker A retransmits the original SYN: the stack retransmits + * the SYN-ACK and re-arms the control RTO from the base value. */ + armed_expires = llk_timer_expires(&s, lsn->sock.tcp.tmr_rto); + frames_before = last_frame_sent_count; + inject_tcp_segment(&s, TEST_PRIMARY_IF, LLK_ATT_IP, LLK_LOCAL_IP, + 41000, (uint16_t)LLK_LISTEN_PORT, 1, 0, TCP_FLAG_SYN); + (void)wolfIP_poll(&s, 3); + + ck_assert_int_eq(lsn->sock.tcp.state, TCP_SYN_RCVD); + ck_assert_int_eq(lsn->sock.tcp.ctrl_rto_retries, 0); + ck_assert_uint_eq(last_frame_sent_count, frames_before + 1); + out = llk_last_tcp(); + ck_assert_ptr_nonnull(out); + ck_assert(out->flags & (TCP_FLAG_SYN | TCP_FLAG_ACK)); + ck_assert_uint_eq(ee16(out->dst_port), 41000); + ck_assert_uint_ne(llk_timer_expires(&s, lsn->sock.tcp.tmr_rto), + armed_expires); +} +END_TEST + +/* Silence: the listener is reclaimed by the control-RTO backoff + * (1,2,4,8,16,32,64,64 s arms = 8 retransmissions, revert at t = 255 s) + * and the port accepts new clients again. */ +START_TEST(test_tcp_listener_lock_reclaimed_at_255s_via_ctrl_rto) +{ + struct wolfIP s; + int fd; + struct tsocket *lsn; + uint64_t t; + uint32_t retrans_at[8]; + int retrans = 0; + uint32_t last_count; + uint64_t reclaimed_at = 0; + const struct wolfIP_tcp_seg *out; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, LLK_LOCAL_IP, LLK_NET_MASK, 0); + fd = llk_open_listener(&s); + lsn = &s.tcpsockets[SOCKET_UNMARK(fd)]; + + /* Pending-only ARP policy: the peers are known neighbors, so the + * initial SYN-ACK and every retransmit reach the wire. */ + llk_keep_arp_fresh(&s, LLK_ATT_IP); + llk_keep_arp_fresh(&s, 0x0A000077U); + + llk_attacker_syn(&s, LLK_ATT_IP, 41000, 1, 0); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_SYN_RCVD); + last_count = last_frame_sent_count; /* initial SYN-ACK already sent */ + + for (t = 1000; t <= 260000; t += 1000) { + (void)wolfIP_poll(&s, t); + if (t % 60000 == 0) + llk_keep_arp_fresh(&s, LLK_ATT_IP); + if (last_frame_sent_count != last_count) { + last_count = last_frame_sent_count; + if (lsn->sock.tcp.state == TCP_SYN_RCVD && retrans < 8) + retrans_at[retrans++] = (uint32_t)t; + } + if (reclaimed_at == 0 && lsn->sock.tcp.state == TCP_LISTEN) + reclaimed_at = t; + } + + ck_assert_int_eq(retrans, 8); + ck_assert_uint_eq(retrans_at[0], 1000); + ck_assert_uint_eq(retrans_at[1], 3000); + ck_assert_uint_eq(retrans_at[2], 7000); + ck_assert_uint_eq(retrans_at[3], 15000); + ck_assert_uint_eq(retrans_at[4], 31000); + ck_assert_uint_eq(retrans_at[5], 63000); + ck_assert_uint_eq(retrans_at[6], 127000); + ck_assert_uint_eq(retrans_at[7], 191000); + + ck_assert_uint_eq(reclaimed_at, 255000); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_LISTEN); + ck_assert_uint_eq(lsn->sock.tcp.tmr_rto, NO_TIMER); + ck_assert_uint_eq(lsn->remote_ip, 0); + ck_assert_uint_eq(lsn->dst_port, 0); + + /* The port is alive again: a new client C gets a SYN-ACK (its ARP + * entry aged out during the 255 s lock: refresh it). */ + llk_keep_arp_fresh(&s, 0x0A000077U); + inject_tcp_segment(&s, TEST_PRIMARY_IF, 0x0A000077U, LLK_LOCAL_IP, + 43000, (uint16_t)LLK_LISTEN_PORT, 9, 0, TCP_FLAG_SYN); + (void)wolfIP_poll(&s, t + 1); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_SYN_RCVD); + out = llk_last_tcp(); + ck_assert_ptr_nonnull(out); + ck_assert(out->flags & (TCP_FLAG_SYN | TCP_FLAG_ACK)); + ck_assert_uint_eq(ee16(out->dst_port), 43000); +} +END_TEST + +/* One SYN per full backoff window keeps the port dead window after + * window: two consecutive 255 s lock windows, victim refused in both. + * (Documents the remaining exposure: a real SYN backlog is the full + * fix; this test pins the current single-listener behavior.) */ +START_TEST(test_tcp_listener_sustained_lock_one_syn_per_window) +{ + struct wolfIP s; + int fd; + struct tsocket *lsn; + uint64_t t; + uint32_t frames_before; + const struct wolfIP_tcp_seg *out; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, LLK_LOCAL_IP, LLK_NET_MASK, 0); + fd = llk_open_listener(&s); + lsn = &s.tcpsockets[SOCKET_UNMARK(fd)]; + + /* Pending-only ARP policy: the peers are known neighbors. */ + llk_keep_arp_fresh(&s, LLK_ATT_IP); + llk_keep_arp_fresh(&s, LLK_VICTIM_IP); + llk_keep_arp_fresh(&s, 0x0A00009AU); + + /* Window 1: lock at t=0, reclaim at t=255 s. */ + llk_attacker_syn(&s, LLK_ATT_IP, 41000, 1, 0); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_SYN_RCVD); + for (t = 1000; t <= 255000; t += 1000) { + (void)wolfIP_poll(&s, t); + if (t % 60000 == 0) + llk_keep_arp_fresh(&s, LLK_ATT_IP); + } + ck_assert_int_eq(lsn->sock.tcp.state, TCP_LISTEN); + + /* Attacker re-locks the port right after reclamation. */ + llk_attacker_syn(&s, 0x0A00009AU, 41001, 2, 256000); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_SYN_RCVD); + + /* Window 2: victim B is refused again. */ + frames_before = last_frame_sent_count; + inject_tcp_segment(&s, TEST_PRIMARY_IF, LLK_VICTIM_IP, LLK_LOCAL_IP, + 42000, (uint16_t)LLK_LISTEN_PORT, 5, 0, TCP_FLAG_SYN); + (void)wolfIP_poll(&s, 256001); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_SYN_RCVD); + ck_assert_uint_eq(last_frame_sent_count, frames_before + 1); + out = llk_last_tcp(); + ck_assert_ptr_nonnull(out); + ck_assert(out->flags & TCP_FLAG_RST); + ck_assert_uint_eq(ee16(out->dst_port), 42000); + + /* Hold through the second full window. */ + for (t = 257000; t <= 511000; t += 1000) { + (void)wolfIP_poll(&s, t); + if (t % 60000 == 0) + llk_keep_arp_fresh(&s, 0x0A00009AU); + } + ck_assert_int_eq(lsn->sock.tcp.state, TCP_LISTEN); +} +END_TEST + +/* A RST from the holding 4-tuple with seq == rcv_nxt reverts the + * listener to LISTEN immediately; a RST with the wrong seq is ignored. */ +START_TEST(test_tcp_listener_rst_from_holding_4tuple_releases_lock) +{ + struct wolfIP s; + int fd; + struct tsocket *lsn; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, LLK_LOCAL_IP, LLK_NET_MASK, 0); + fd = llk_open_listener(&s); + lsn = &s.tcpsockets[SOCKET_UNMARK(fd)]; + + llk_attacker_syn(&s, LLK_ATT_IP, 41000, 1, 0); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_SYN_RCVD); + + /* RST with wrong seq: ignored, lock persists. */ + inject_tcp_segment(&s, TEST_PRIMARY_IF, LLK_ATT_IP, LLK_LOCAL_IP, + 41000, (uint16_t)LLK_LISTEN_PORT, 777, 0, + TCP_FLAG_RST); + (void)wolfIP_poll(&s, 1); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_SYN_RCVD); + + /* RST with seq == rcv_nxt (attacker ISN 1 + 1 = 2): immediate + * revert to LISTEN. */ + inject_tcp_segment(&s, TEST_PRIMARY_IF, LLK_ATT_IP, LLK_LOCAL_IP, + 41000, (uint16_t)LLK_LISTEN_PORT, 2, 0, + TCP_FLAG_RST); + (void)wolfIP_poll(&s, 2); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_LISTEN); + ck_assert_uint_eq(lsn->remote_ip, 0); + ck_assert_uint_eq(lsn->dst_port, 0); +} +END_TEST + +/* The handshake completes before accept(): the listener is ESTABLISHED + * with the pre-accept fast-fail timer armed. accept() can no longer + * clone the connection, but it reverts the port to LISTEN so it is not + * pinned, and new clients are served again. */ +START_TEST(test_tcp_listener_preaccept_accept_reverts_port) +{ + struct wolfIP s; + int fd; + struct tsocket *lsn; + struct wolfIP_sockaddr_in peer; + socklen_t peer_len = sizeof(peer); + const struct wolfIP_tcp_seg *out; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, LLK_LOCAL_IP, LLK_NET_MASK, 0); + fd = llk_open_listener(&s); + lsn = &s.tcpsockets[SOCKET_UNMARK(fd)]; + + /* Pending-only ARP policy: the peers are known neighbors. */ + llk_keep_arp_fresh(&s, LLK_ATT_IP); + llk_keep_arp_fresh(&s, LLK_VICTIM_IP); + + llk_attacker_syn(&s, LLK_ATT_IP, 41000, 1, 0); + llk_complete_handshake(&s, lsn, LLK_ATT_IP, 41000, 1); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_ESTABLISHED); + /* The un-accepted established listener is time-boxed. */ + ck_assert_int_eq(lsn->sock.tcp.preaccept_timeout_active, 1); + ck_assert_uint_ne(lsn->sock.tcp.tmr_rto, NO_TIMER); + + /* accept() cannot clone an ESTABLISHED connection: it fails, but it + * reverts the port to LISTEN instead of leaving it pinned. */ + memset(&peer, 0, sizeof(peer)); + ck_assert_int_eq(wolfIP_sock_accept(&s, fd, + (struct wolfIP_sockaddr *)&peer, &peer_len), -1); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_LISTEN); + ck_assert_int_eq(lsn->sock.tcp.preaccept_timeout_active, 0); + ck_assert_uint_eq(lsn->sock.tcp.tmr_rto, NO_TIMER); + ck_assert_uint_eq(lsn->remote_ip, 0); + ck_assert_uint_eq(lsn->dst_port, 0); + + /* A new client is served again. */ + inject_tcp_segment(&s, TEST_PRIMARY_IF, LLK_VICTIM_IP, LLK_LOCAL_IP, + 42000, (uint16_t)LLK_LISTEN_PORT, 5, 0, TCP_FLAG_SYN); + (void)wolfIP_poll(&s, 2); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_SYN_RCVD); + out = llk_last_tcp(); + ck_assert_ptr_nonnull(out); + ck_assert(out->flags & (TCP_FLAG_SYN | TCP_FLAG_ACK)); + ck_assert_uint_eq(ee16(out->dst_port), 42000); +} +END_TEST + +/* Same pre-accept condition, no accept() call: the fast-fail timer + * reverts the port to LISTEN after TCP_PREACCEPT_TIMEOUT_MS, so the pin + * is bounded even if the application never touches the socket again. */ +START_TEST(test_tcp_listener_preaccept_timeout_reverts_port) +{ + struct wolfIP s; + int fd; + struct tsocket *lsn; + uint64_t t; + uint64_t reverted_at = 0; + const struct wolfIP_tcp_seg *out; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, LLK_LOCAL_IP, LLK_NET_MASK, 0); + fd = llk_open_listener(&s); + lsn = &s.tcpsockets[SOCKET_UNMARK(fd)]; + + /* Pending-only ARP policy: the peers are known neighbors. */ + llk_keep_arp_fresh(&s, LLK_ATT_IP); + llk_keep_arp_fresh(&s, LLK_VICTIM_IP); + + llk_attacker_syn(&s, LLK_ATT_IP, 41000, 1, 0); + llk_complete_handshake(&s, lsn, LLK_ATT_IP, 41000, 1); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_ESTABLISHED); + ck_assert_int_eq(lsn->sock.tcp.preaccept_timeout_active, 1); + + /* No accept() call: the timer alone reclaims the port. */ + for (t = 1000; t <= 10000; t += 1000) { + (void)wolfIP_poll(&s, t); + if (reverted_at == 0 && lsn->sock.tcp.state == TCP_LISTEN) + reverted_at = t; + } + + ck_assert_uint_eq(reverted_at, TCP_PREACCEPT_TIMEOUT_MS); + ck_assert_int_eq(lsn->sock.tcp.preaccept_timeout_active, 0); + ck_assert_uint_eq(lsn->sock.tcp.tmr_rto, NO_TIMER); + ck_assert_uint_eq(lsn->remote_ip, 0); + ck_assert_uint_eq(lsn->dst_port, 0); + + /* A new client is served again. */ + inject_tcp_segment(&s, TEST_PRIMARY_IF, LLK_VICTIM_IP, LLK_LOCAL_IP, + 42000, (uint16_t)LLK_LISTEN_PORT, 5, 0, TCP_FLAG_SYN); + (void)wolfIP_poll(&s, t + 1); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_SYN_RCVD); + out = llk_last_tcp(); + ck_assert_ptr_nonnull(out); + ck_assert(out->flags & (TCP_FLAG_SYN | TCP_FLAG_ACK)); + ck_assert_uint_eq(ee16(out->dst_port), 42000); +} +END_TEST diff --git a/src/wolfip.c b/src/wolfip.c index d6afd4e1..1624e0c7 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -172,6 +172,12 @@ struct wolfIP_icmp_packet; #ifndef TCP_FIN_WAIT_2_TIMEOUT_MS #define TCP_FIN_WAIT_2_TIMEOUT_MS 60000U #endif +/* A listener that completes the handshake without being accepted would be + * pinned in ESTABLISHED forever (accept() only handles SYN_RCVD/LISTEN and + * no other timer is armed): reclaim the port after this grace period. */ +#ifndef TCP_PREACCEPT_TIMEOUT_MS +#define TCP_PREACCEPT_TIMEOUT_MS 5000U +#endif /* Arbitrary upper limit to avoid monopolizing the CPU during poll loops. */ #define WOLFIP_POLL_BUDGET 128 @@ -1168,6 +1174,7 @@ struct tcpsocket { uint8_t ctrl_rto_retries; uint8_t ctrl_rto_active; uint8_t fin_wait_2_timeout_active; + uint8_t preaccept_timeout_active; uint8_t is_listener; uint8_t ack_retry_pending; ip4 local_ip, remote_ip; @@ -1295,6 +1302,9 @@ static void tcp_ctrl_rto_start(struct tsocket *t, uint64_t now); static void tcp_ctrl_rto_stop(struct tsocket *t); static void tcp_fin_wait_2_timeout_start(struct tsocket *t, uint64_t now); static void tcp_fin_wait_2_timeout_stop(struct tsocket *t); +static void tcp_preaccept_timeout_start(struct tsocket *t, uint64_t now); +static void tcp_preaccept_timeout_stop(struct tsocket *t); +static void tcp_listener_revert_to_listen(struct tsocket *t); static int tcp_ctrl_state_needs_rto(const struct tsocket *t); static int tcp_has_pending_unsent_payload(struct tsocket *t); static inline struct wolfIP_ll_dev *wolfIP_ll_at(struct wolfIP *s, unsigned int if_idx); @@ -3831,6 +3841,60 @@ static void tcp_fin_wait_2_timeout_stop(struct tsocket *t) t->sock.tcp.fin_wait_2_timeout_active = 0; } +static void tcp_preaccept_timeout_start(struct tsocket *t, uint64_t now) +{ + struct wolfIP_timer tmr = {0}; + + if (!t || t->proto != WI_IPPROTO_TCP) + return; + if (t->sock.tcp.tmr_rto != NO_TIMER) { + timer_binheap_cancel(&t->S->timers, t->sock.tcp.tmr_rto); + t->sock.tcp.tmr_rto = NO_TIMER; + } + tmr.expires = now + TCP_PREACCEPT_TIMEOUT_MS; + tmr.arg = t; + tmr.cb = tcp_rto_cb; + t->sock.tcp.tmr_rto = timers_binheap_insert(&t->S->timers, tmr); + t->sock.tcp.preaccept_timeout_active = 1; +} + +static void tcp_preaccept_timeout_stop(struct tsocket *t) +{ + if (!t || t->proto != WI_IPPROTO_TCP) + return; + if (t->sock.tcp.tmr_rto != NO_TIMER) { + timer_binheap_cancel(&t->S->timers, t->sock.tcp.tmr_rto); + t->sock.tcp.tmr_rto = NO_TIMER; + } + t->sock.tcp.preaccept_timeout_active = 0; +} + +/* Revert a listening socket stuck in a half-open or established connection + * state back to TCP_LISTEN, clearing the half-open 4-tuple so the port + * accepts new connections again. Used by the control-RTO expiry, the + * pre-accept fast-fail timeout, and the accept() recovery path. */ +static void tcp_listener_revert_to_listen(struct tsocket *t) +{ + if (!t || t->proto != WI_IPPROTO_TCP) + return; + tcp_preaccept_timeout_stop(t); + t->sock.tcp.state = TCP_LISTEN; + t->sock.tcp.seq = wolfIP_getrandom(); + t->sock.tcp.ack = 0; + t->sock.tcp.snd_una = 0; + t->sock.tcp.ctrl_rto_retries = 0; + t->remote_ip = 0; + t->dst_port = 0; + t->events = 0; + if (t->bound_local_ip != IPADDR_ANY) { + int bound_match = 0; + unsigned int bound_if = wolfIP_if_for_local_ip( + t->S, t->bound_local_ip, &bound_match); + t->if_idx = bound_match ? (uint8_t)bound_if : t->if_idx; + t->local_ip = t->bound_local_ip; + } +} + static uint32_t tcp_tx_desc_ip_len(const struct tsocket *t, const struct pkt_desc *desc, const struct wolfIP_tcp_seg *seg) { @@ -4931,12 +4995,16 @@ static void tcp_ack(struct tsocket *t, const struct wolfIP_tcp_seg *tcp) * stop the current RTO timer. If bytes remain in-flight and no new * send happens immediately, we must re-arm RTO here to avoid stalls. */ t->sock.tcp.rto_backoff = 0; + /* fin_wait_2 and the pre-accept fast-fail ride on tmr_rto with their + * own semantics; a forward ACK must not cancel or re-arm their timer. */ if (!t->sock.tcp.fin_wait_2_timeout_active && + !t->sock.tcp.preaccept_timeout_active && t->sock.tcp.tmr_rto != NO_TIMER) { timer_binheap_cancel(&t->S->timers, t->sock.tcp.tmr_rto); t->sock.tcp.tmr_rto = NO_TIMER; } if (!t->sock.tcp.fin_wait_2_timeout_active && + !t->sock.tcp.preaccept_timeout_active && t->sock.tcp.bytes_in_flight > 0) { struct wolfIP_timer new_tmr = { 0 }; new_tmr.cb = tcp_rto_cb; @@ -5384,6 +5452,18 @@ static void tcp_input(struct wolfIP *S, unsigned int if_idx, } /* Check if final ACK to SYN-ACK (may include payload) */ if (t->sock.tcp.state == TCP_SYN_RCVD) { + if ((tcp->flags & TCP_FLAG_SYN) && !(tcp->flags & TCP_FLAG_ACK) && + ee32(tcp->seq) == t->sock.tcp.ack - 1) { + /* Re-SYN from the holding 4-tuple: the peer retransmitted + * its original SYN (our SYN-ACK was lost or it is retrying). + * Retransmit the SYN-ACK and re-arm the control RTO from + * the base value instead of silently dropping the + * retransmission, so the half-open handshake can complete. */ + (void)tcp_send_syn(t, TCP_FLAG_SYN | TCP_FLAG_ACK); + t->sock.tcp.ctrl_rto_retries = 0; + tcp_ctrl_rto_start(t, t->S->last_tick); + continue; + } if (tcp->flags & TCP_FLAG_ACK) { uint32_t expected_ack = tcp_seq_inc(t->sock.tcp.snd_una, 1); uint32_t expected_seq = t->sock.tcp.ack; @@ -5395,6 +5475,8 @@ static void tcp_input(struct wolfIP *S, unsigned int if_idx, } t->sock.tcp.state = TCP_ESTABLISHED; tcp_ctrl_rto_stop(t); + if (t->sock.tcp.is_listener) + tcp_preaccept_timeout_start(t, t->S->last_tick); t->sock.tcp.ack = ee32(tcp->seq); t->sock.tcp.seq = ee32(tcp->ack); t->sock.tcp.snd_una = t->sock.tcp.seq; @@ -5577,6 +5659,21 @@ static void tcp_rto_cb(void *arg) close_socket(ts); return; } + if (ts->sock.tcp.preaccept_timeout_active) { + if (ts->sock.tcp.state != TCP_ESTABLISHED || + !ts->sock.tcp.is_listener) { + /* The socket left the pinned condition (accepted away, reset, + * or closed): disarm quietly. */ + tcp_preaccept_timeout_stop(ts); + return; + } + /* The handshake completed but the application never accepted: an + * un-accepted established listener has no accept() path and no + * other timer, so the port would stay pinned forever. Revert to + * LISTEN; the peer's next segment gets the normal LISTEN RST. */ + tcp_listener_revert_to_listen(ts); + return; + } if (tcp_ctrl_state_needs_rto(ts) || ts->sock.tcp.ctrl_rto_active) { if (!tcp_ctrl_state_needs_rto(ts)) { tcp_ctrl_rto_stop(ts); @@ -5587,27 +5684,10 @@ static void tcp_rto_cb(void *arg) if (ts->sock.tcp.is_listener && ts->sock.tcp.state == TCP_SYN_RCVD) { /* Revert listen socket back to LISTEN instead of - * destroying it, mirrors the accept() recovery path. */ - ts->sock.tcp.state = TCP_LISTEN; - ts->sock.tcp.seq = wolfIP_getrandom(); - ts->sock.tcp.ack = 0; - ts->sock.tcp.snd_una = 0; - ts->sock.tcp.ctrl_rto_retries = 0; - ts->remote_ip = 0; - ts->dst_port = 0; - ts->events = 0; - /* The timed-out SYN-ACK is gone with the connection; drop it - * so the next connection starts with an empty TX FIFO (see - * the accept() revert for why). */ - fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); - if (ts->bound_local_ip != IPADDR_ANY) { - int bound_match = 0; - unsigned int bound_if = wolfIP_if_for_local_ip( - ts->S, ts->bound_local_ip, &bound_match); - ts->if_idx = bound_match ? (uint8_t)bound_if - : ts->if_idx; - ts->local_ip = ts->bound_local_ip; - } + * destroying it, mirrors the accept() recovery path. + * The helper drains the parked SYN-ACK from the TX FIFO + * (see the accept() revert for why). */ + tcp_listener_revert_to_listen(ts); } else { ts->sock.tcp.state = TCP_CLOSED; close_socket(ts); @@ -5779,16 +5859,21 @@ static void tcp_resync_inflight(struct wolfIP *s, struct tsocket *ts, uint64_t n scan = next; } ts->sock.tcp.bytes_in_flight = calc_in_flight; - if (has_sent_payload && ts->sock.tcp.tmr_rto == NO_TIMER) { - struct wolfIP_timer new_tmr = {}; - new_tmr.cb = tcp_rto_cb; - new_tmr.expires = now + tcp_backoff_rto_ms(ts->sock.tcp.rto, - ts->sock.tcp.rto_backoff); - new_tmr.arg = ts; - ts->sock.tcp.tmr_rto = timers_binheap_insert(&s->timers, new_tmr); - } else if (!has_sent_payload && ts->sock.tcp.tmr_rto != NO_TIMER) { - timer_binheap_cancel(&s->timers, ts->sock.tcp.tmr_rto); - ts->sock.tcp.tmr_rto = NO_TIMER; + /* The pre-accept fast-fail rides on tmr_rto with its own semantics and + * must survive a payload-less txbuf (an accepted handshake has nothing + * in flight), so resync must not cancel or re-arm it. */ + if (!ts->sock.tcp.preaccept_timeout_active) { + if (has_sent_payload && ts->sock.tcp.tmr_rto == NO_TIMER) { + struct wolfIP_timer new_tmr = {}; + new_tmr.cb = tcp_rto_cb; + new_tmr.expires = now + tcp_backoff_rto_ms(ts->sock.tcp.rto, + ts->sock.tcp.rto_backoff); + new_tmr.arg = ts; + ts->sock.tcp.tmr_rto = timers_binheap_insert(&s->timers, new_tmr); + } else if (!has_sent_payload && ts->sock.tcp.tmr_rto != NO_TIMER) { + timer_binheap_cancel(&s->timers, ts->sock.tcp.tmr_rto); + ts->sock.tcp.tmr_rto = NO_TIMER; + } } } @@ -6200,6 +6285,15 @@ int wolfIP_sock_accept(struct wolfIP *s, int sockfd, struct wolfIP_sockaddr *add if (SOCKET_UNMARK(sockfd) >= MAX_TCPSOCKETS) return -WOLFIP_EINVAL; ts = &s->tcpsockets[SOCKET_UNMARK(sockfd)]; + if (ts->sock.tcp.state == TCP_ESTABLISHED && + ts->sock.tcp.is_listener) { + /* The handshake completed before accept(): the connection can + * no longer be cloned (accept() only handles SYN_RCVD), and + * without this recovery the port would be pinned in + * ESTABLISHED forever. Revert the port to LISTEN. */ + tcp_listener_revert_to_listen(ts); + return -1; + } if ((ts->sock.tcp.state != TCP_SYN_RCVD) && (ts->sock.tcp.state != TCP_LISTEN)) return -1; From 86fc318d2736d83630c5da81126bbe229a411264 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 19 Aug 2026 14:36:44 +0200 Subject: [PATCH 14/16] skoll: re-SYN cap, listener-revert drain, esp restore, ttl OOB guard --- src/test/unit/unit.c | 4 +- src/test/unit/unit_esp.c | 53 +++++++++++++++++ src/test/unit/unit_tests_ip_arp_recv.c | 44 ++++++++++++++ src/test/unit/unit_tests_tcp_flow.c | 81 ++++++++++++++++++++++++-- src/wolfesp.c | 18 +++++- src/wolfip.c | 40 ++++++++----- 6 files changed, 218 insertions(+), 22 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 73ef1e88..90a0b6c4 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -579,12 +579,13 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_tcp_input_syn_listen_does_not_scale_syn_window); tcase_add_test(tc_utils, test_tcp_input_syn_sent_does_not_scale_synack_window); tcase_add_test(tc_utils, test_tcp_setsockopt_ip_tos_applied_to_outbound_syn); - tcase_add_test(tc_utils, test_tcp_listener_syn_lock_others_rst_resyn_retransmits_rearms); + tcase_add_test(tc_utils, test_tcp_listener_syn_lock_others_rst_resyn_retransmits_synack); tcase_add_test(tc_utils, test_tcp_listener_lock_reclaimed_at_255s_via_ctrl_rto); tcase_add_test(tc_utils, test_tcp_listener_sustained_lock_one_syn_per_window); tcase_add_test(tc_utils, test_tcp_listener_rst_from_holding_4tuple_releases_lock); tcase_add_test(tc_utils, test_tcp_listener_preaccept_accept_reverts_port); tcase_add_test(tc_utils, test_tcp_listener_preaccept_timeout_reverts_port); + tcase_add_test(tc_utils, test_tcp_listener_preaccept_revert_drains_connection_state); tcase_add_test(tc_utils, test_tcp_parse_sack_wraparound_block_accepted); tcase_add_test(tc_utils, test_tcp_parse_options_stops_on_truncated_or_invalid_option_length); tcase_add_test(tc_utils, test_tcp_parse_options_returns_when_frame_has_no_option_bytes); @@ -1462,6 +1463,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_core, test_ip_recv_loopback_src_on_non_loopback_dropped); tcase_add_test(tc_core, test_ip_recv_forward_ttl_normal_decremented); tcase_add_test(tc_core, test_ip_recv_forward_ttl1_short_frame_sends_ttl_exceeded); + tcase_add_test(tc_core, test_ip_recv_forward_ttl1_zero_payload_icmp_not_suppressed); tcase_add_test(tc_core, test_ip_recv_forward_ttl1_partial_payload_quoted); tcase_add_test(tc_core, test_forward_ttl_exceeded_copies_orig_tos); tcase_add_test(tc_core, test_ip_recv_dest_matches_secondary_iface_ip_is_local); diff --git a/src/test/unit/unit_esp.c b/src/test/unit/unit_esp.c index 764ae17b..d36f3395 100644 --- a/src/test/unit/unit_esp.c +++ b/src/test/unit/unit_esp.c @@ -2207,6 +2207,58 @@ state_test_wire_seq(const struct wolfIP_ip_packet *ip) return ee32(seq); } +/* A read callback that fails (non-zero) must leave the SA fresh, even + * when it scribbled the out parameters before failing (corrupt NVM or + * version-mismatch path). */ +static uint8_t state_fail_spi[ESP_SPI_LEN] = {0x99, 0x88, 0x77, 0x66}; + +static int state_fail_read_cb(const uint8_t *spi, uint32_t *oseq, + uint32_t *hi_seq, uint32_t *bitmap) +{ + if (memcmp(spi, state_fail_spi, ESP_SPI_LEN) == 0) { + *oseq = 0xDEADBEEFU; + *hi_seq = 0; + *bitmap = 0xFFFFFFFFU; + return -1; + } + return 0; +} + +START_TEST(test_esp_state_restore_failed_read_keeps_fresh_state) +{ + static uint8_t buf[LINK_MTU + 256]; + struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)buf; + uint16_t ip_len; + uint32_t frame_len; + int ret; + + esp_setup(); + ret = wolfIP_esp_state_set_cbs(NULL, state_fail_read_cb); + ck_assert_int_eq(ret, 0); + + ret = wolfIP_esp_sa_new_cbc_hmac(0, state_fail_spi, + atoip4(T_SRC), atoip4(T_DST), + (uint8_t *)k_aes128, sizeof(k_aes128), + ESP_AUTH_SHA256_RFC4868, + (uint8_t *)k_auth16, sizeof(k_auth16), + ESP_ICVLEN_HMAC_128); + ck_assert_int_eq(ret, 0); + + /* The failed read must not have taken effect: outbound starts from + * the fresh oseq 0, so the first wire seq is 1. */ + frame_len = build_ip_packet(buf, sizeof(buf), WI_IPPROTO_UDP, + (const uint8_t *)"abcd", 4); + ip_len = (uint16_t)(frame_len - ETH_HEADER_LEN); + ret = esp_transport_wrap(ip, &ip_len); + ck_assert_int_eq(ret, 0); + ck_assert_uint_eq(state_test_wire_seq(ip), 1U); + + wolfIP_esp_sa_del_all(); + ret = wolfIP_esp_state_set_cbs(NULL, NULL); + ck_assert_int_eq(ret, 0); +} +END_TEST + START_TEST(test_esp_state_persistence_callbacks) { static uint8_t buf[LINK_MTU + 256]; @@ -2382,6 +2434,7 @@ static Suite *esp_suite(void) tcase_add_test(tc, test_sa_del_frees_slot); tcase_add_test(tc, test_sa_del_all); tcase_add_test(tc, test_esp_state_persistence_callbacks); + tcase_add_test(tc, test_esp_state_restore_failed_read_keeps_fresh_state); suite_add_tcase(s, tc); /* Replay window */ diff --git a/src/test/unit/unit_tests_ip_arp_recv.c b/src/test/unit/unit_tests_ip_arp_recv.c index 90f4d2a7..c023d8ad 100644 --- a/src/test/unit/unit_tests_ip_arp_recv.c +++ b/src/test/unit/unit_tests_ip_arp_recv.c @@ -1138,6 +1138,50 @@ START_TEST(test_ip_recv_forward_ttl1_partial_payload_quoted) } END_TEST +/* ========================================================================= + * ip_recv: TTL=1 on a zero-payload ICMP datagram + * ========================================================================= + * Branch: proto ICMP with declared length == IHL: the ICMP type byte does + * not exist (nothing beyond the IP header), so there is nothing to read + * for error suppression. The Time-Exceeded is generated, quoting only the + * original header. + */ +START_TEST(test_ip_recv_forward_ttl1_zero_payload_icmp_not_suppressed) +{ + struct wolfIP s; + uint8_t frame[ETH_HEADER_LEN + IP_HEADER_LEN]; + struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)frame; + ip4 primary_ip = 0x0A000001U; + ip4 secondary_ip = 0xC0A80101U; + ip4 dest_ip = 0xC0A80155U; + ip4 src_ip = 0x0A000002U; + + setup_stack_with_two_ifaces(&s, primary_ip, secondary_ip); + wolfIP_filter_set_callback(NULL, NULL); + last_frame_sent_size = 0; + + memset(frame, 0, sizeof(frame)); + memcpy(ip->eth.dst, s.ll_dev[TEST_PRIMARY_IF].mac, 6); + memcpy(ip->eth.src, "\x01\x02\x03\x04\x05\x06", 6); + ip->eth.type = ee16(ETH_TYPE_IP); + ip->ver_ihl = 0x45; + ip->ttl = 1; + ip->proto = WI_IPPROTO_ICMP; + ip->len = ee16(IP_HEADER_LEN); /* no ICMP payload */ + ip->src = ee32(src_ip); + ip->dst = ee32(dest_ip); + fix_ip_checksum(ip); + + ip_recv(&s, TEST_PRIMARY_IF, ip, (uint32_t)sizeof(frame)); + + /* Not suppressed: time-exceeded quoting the original header only. */ + ck_assert_uint_eq(last_frame_sent_size, + (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + 8 + IP_HEADER_LEN)); + ck_assert_uint_eq(last_frame_sent[ETH_HEADER_LEN + IP_HEADER_LEN], + ICMP_TTL_EXCEEDED); +} +END_TEST + /* ========================================================================= * ip_recv: TTL=1 — the Time Exceeded carries the triggering packet's TOS * ========================================================================= diff --git a/src/test/unit/unit_tests_tcp_flow.c b/src/test/unit/unit_tests_tcp_flow.c index e364fbca..a56cbdc2 100644 --- a/src/test/unit/unit_tests_tcp_flow.c +++ b/src/test/unit/unit_tests_tcp_flow.c @@ -4901,9 +4901,10 @@ static void llk_complete_handshake(struct wolfIP *s, struct tsocket *lsn, } /* One SYN puts the listener in SYN_RCVD; other clients are RST'd; a - * re-SYN from the holding 4-tuple retransmits the SYN-ACK and re-arms - * the control RTO from the base value instead of being dropped. */ -START_TEST(test_tcp_listener_syn_lock_others_rst_resyn_retransmits_rearms) + * re-SYN from the holding 4-tuple retransmits the SYN-ACK instead of + * being dropped, without touching the control-RTO retry budget (a + * re-SYN must not extend the lock past the retry cap). */ +START_TEST(test_tcp_listener_syn_lock_others_rst_resyn_retransmits_synack) { struct wolfIP s; int fd; @@ -4969,7 +4970,8 @@ START_TEST(test_tcp_listener_syn_lock_others_rst_resyn_retransmits_rearms) ck_assert_uint_eq(ee16(out->dst_port), 42000); /* Attacker A retransmits the original SYN: the stack retransmits - * the SYN-ACK and re-arms the control RTO from the base value. */ + * the SYN-ACK, but the control RTO keeps its own schedule (no + * re-arm), so the retry cap still bounds the lock. */ armed_expires = llk_timer_expires(&s, lsn->sock.tcp.tmr_rto); frames_before = last_frame_sent_count; inject_tcp_segment(&s, TEST_PRIMARY_IF, LLK_ATT_IP, LLK_LOCAL_IP, @@ -4983,7 +4985,7 @@ START_TEST(test_tcp_listener_syn_lock_others_rst_resyn_retransmits_rearms) ck_assert_ptr_nonnull(out); ck_assert(out->flags & (TCP_FLAG_SYN | TCP_FLAG_ACK)); ck_assert_uint_eq(ee16(out->dst_port), 41000); - ck_assert_uint_ne(llk_timer_expires(&s, lsn->sock.tcp.tmr_rto), + ck_assert_uint_eq(llk_timer_expires(&s, lsn->sock.tcp.tmr_rto), armed_expires); } END_TEST @@ -5261,3 +5263,72 @@ START_TEST(test_tcp_listener_preaccept_timeout_reverts_port) ck_assert_uint_eq(ee16(out->dst_port), 42000); } END_TEST + +/* The accept() recovery path must also drain the dead connection's + * transport state: segments parked on the listener socket during the + * pre-accept window must not leak into the next connection (stale + * descriptors would carry dead seqs into the new ACK window). */ +START_TEST(test_tcp_listener_preaccept_revert_drains_connection_state) +{ + struct wolfIP s; + int fd; + struct tsocket *lsn; + struct wolfIP_sockaddr_in peer; + socklen_t peer_len = sizeof(peer); + int i; + const struct wolfIP_tcp_seg *out; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, LLK_LOCAL_IP, LLK_NET_MASK, 0); + fd = llk_open_listener(&s); + lsn = &s.tcpsockets[SOCKET_UNMARK(fd)]; + + llk_attacker_syn(&s, LLK_ATT_IP, 41000, 1, 0); + llk_complete_handshake(&s, lsn, LLK_ATT_IP, 41000, 1); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_ESTABLISHED); + + /* Under the pending-only ARP policy the listener's SYN-ACK is parked + * in the tx fifo (the neighbor never resolved): live state a revert + * must drop, or the next connection inherits a stale segment. */ + ck_assert_int_eq(fifo_is_empty(&lsn->sock.tcp.txbuf), 0); + + /* accept() fails (ESTABLISHED) and reverts the port to LISTEN. */ + memset(&peer, 0, sizeof(peer)); + ck_assert_int_eq(wolfIP_sock_accept(&s, fd, + (struct wolfIP_sockaddr *)&peer, &peer_len), -1); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_LISTEN); + + /* The dead connection's transport state is gone. */ + ck_assert_uint_eq(lsn->sock.tcp.bytes_in_flight, 0); + ck_assert_ptr_null(fifo_peek(&lsn->sock.tcp.txbuf)); + /* An empty ring queue always holds back one slot. */ + ck_assert_uint_eq(queue_space((struct queue *)&lsn->sock.tcp.rxbuf), + RXBUF_SIZE - 1); + for (i = 0; i < TCP_OOO_MAX_SEGS; i++) + ck_assert_int_eq(lsn->sock.tcp.ooo[i].used, 0); + ck_assert_uint_eq(lsn->sock.tcp.tmr_rto, NO_TIMER); + ck_assert_int_eq(lsn->sock.tcp.preaccept_timeout_active, 0); + + /* A new client gets a clean connection and completes the handshake + * (the victim is a known neighbor so its SYN-ACK reaches the wire; + * the attacker's stays unresolved, keeping the parked-SYN-ACK premise + * above intact). */ + llk_keep_arp_fresh(&s, LLK_VICTIM_IP); + inject_tcp_segment(&s, TEST_PRIMARY_IF, LLK_VICTIM_IP, LLK_LOCAL_IP, + 42000, (uint16_t)LLK_LISTEN_PORT, 5, 0, TCP_FLAG_SYN); + (void)wolfIP_poll(&s, 3); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_SYN_RCVD); + out = llk_last_tcp(); + ck_assert_ptr_nonnull(out); + ck_assert(out->flags & (TCP_FLAG_SYN | TCP_FLAG_ACK)); + ck_assert_uint_eq(ee16(out->dst_port), 42000); + + inject_tcp_segment(&s, TEST_PRIMARY_IF, LLK_VICTIM_IP, LLK_LOCAL_IP, + 42000, (uint16_t)LLK_LISTEN_PORT, 6, + lsn->sock.tcp.seq + 1, TCP_FLAG_ACK); + (void)wolfIP_poll(&s, 4); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_ESTABLISHED); + ck_assert_int_eq(lsn->sock.tcp.preaccept_timeout_active, 1); +} +END_TEST diff --git a/src/wolfesp.c b/src/wolfesp.c index 12375887..4cf10550 100644 --- a/src/wolfesp.c +++ b/src/wolfesp.c @@ -53,13 +53,25 @@ esp_state_save(const wolfIP_esp_sa *sa) } /* Restore persisted state for a fresh SA (if the application provides - * any). A non-zero callback return keeps the fresh state. */ + * any). A non-zero callback return keeps the fresh state, even if the + * callback wrote into the out parameters before failing (corrupt NVM, + * version mismatch). */ static void esp_state_restore(wolfIP_esp_sa *sa) { + uint32_t oseq; + uint32_t hi_seq; + uint32_t bitmap; + if (esp_state_read_cb) { - (void)esp_state_read_cb(sa->spi, &sa->replay.oseq, - &sa->replay.hi_seq, &sa->replay.bitmap); + oseq = sa->replay.oseq; + hi_seq = sa->replay.hi_seq; + bitmap = sa->replay.bitmap; + if (esp_state_read_cb(sa->spi, &oseq, &hi_seq, &bitmap) == 0) { + sa->replay.oseq = oseq; + sa->replay.hi_seq = hi_seq; + sa->replay.bitmap = bitmap; + } } } diff --git a/src/wolfip.c b/src/wolfip.c index 1624e0c7..d735bd58 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -2265,10 +2265,10 @@ static void wolfIP_send_ttl_exceeded(struct wolfIP *s, unsigned int if_idx, /* RFC 1812 4.3.2.7 / RFC 1122 3.2.2: an ICMP error message MUST NOT be * originated in response to another ICMP error. If the packet whose TTL * expired is itself an ICMP error (type 3, 4, 5, 11, 12), drop silently. - * The caller guarantees the frame holds the full IP header, so reading - * the embedded ICMP type at offset ETH_HEADER_LEN + orig_ihl is in - * bounds. */ - if (orig->proto == WI_IPPROTO_ICMP) { + * The ICMP type byte only exists when the datagram carries an ICMP + * payload (declared length beyond the IP header); a zero-payload ICMP + * cannot be an error, so it is never suppressed. */ + if (orig->proto == WI_IPPROTO_ICMP && ee16(orig->len) > orig_ihl) { uint8_t orig_type = *(((uint8_t *)orig) + ETH_HEADER_LEN + orig_ihl); if (orig_type == ICMP_DEST_UNREACH || orig_type == ICMP_FRAG_NEEDED || orig_type == 5 /* Redirect */ || orig_type == ICMP_TTL_EXCEEDED || @@ -3872,17 +3872,31 @@ static void tcp_preaccept_timeout_stop(struct tsocket *t) /* Revert a listening socket stuck in a half-open or established connection * state back to TCP_LISTEN, clearing the half-open 4-tuple so the port * accepts new connections again. Used by the control-RTO expiry, the - * pre-accept fast-fail timeout, and the accept() recovery path. */ + * pre-accept fast-fail timeout, and the accept() recovery path. The socket + * is reset to the same baseline as a freshly allocated TCP socket: + * payload descriptors, queued RX data, out-of-order segments and CC state + * of the dead connection must not leak into the next one (retransmitting + * stale descriptors would carry dead seqs into the new ACK window). */ static void tcp_listener_revert_to_listen(struct tsocket *t) { if (!t || t->proto != WI_IPPROTO_TCP) return; + tcp_persist_stop(t); tcp_preaccept_timeout_stop(t); + memset(&t->sock.tcp, 0, sizeof(t->sock.tcp)); + /* A zeroed fifo/queue is not an empty one (size 0, NULL data): re-init + * the buffer bookkeeping against the socket's storage. */ + fifo_init(&t->sock.tcp.txbuf, t->txmem, TXBUF_SIZE); + queue_init(&t->sock.tcp.rxbuf, t->rxmem, RXBUF_SIZE, 0); t->sock.tcp.state = TCP_LISTEN; + t->sock.tcp.is_listener = 1; t->sock.tcp.seq = wolfIP_getrandom(); - t->sock.tcp.ack = 0; - t->sock.tcp.snd_una = 0; - t->sock.tcp.ctrl_rto_retries = 0; + t->sock.tcp.rto = TCP_RTO_MIN_MS; + t->sock.tcp.peer_rwnd = 0xFFFF; + t->sock.tcp.cwnd = tcp_initial_cwnd(t->sock.tcp.peer_rwnd, tcp_cc_mss(t)); + t->sock.tcp.ssthresh = tcp_initial_ssthresh(t->sock.tcp.peer_rwnd); + t->sock.tcp.peer_mss = TCP_DEFAULT_MSS; + t->sock.tcp.sack_offer = 1; t->remote_ip = 0; t->dst_port = 0; t->events = 0; @@ -5456,12 +5470,12 @@ static void tcp_input(struct wolfIP *S, unsigned int if_idx, ee32(tcp->seq) == t->sock.tcp.ack - 1) { /* Re-SYN from the holding 4-tuple: the peer retransmitted * its original SYN (our SYN-ACK was lost or it is retrying). - * Retransmit the SYN-ACK and re-arm the control RTO from - * the base value instead of silently dropping the - * retransmission, so the half-open handshake can complete. */ + * Retransmit the SYN-ACK instead of silently dropping the + * retransmission, so the half-open handshake can complete. + * The control RTO backoff keeps its own schedule: a re-SYN + * must not re-arm the retry budget, or an attacker could + * hold the listener in SYN_RCVD past the retry cap. */ (void)tcp_send_syn(t, TCP_FLAG_SYN | TCP_FLAG_ACK); - t->sock.tcp.ctrl_rto_retries = 0; - tcp_ctrl_rto_start(t, t->S->last_tick); continue; } if (tcp->flags & TCP_FLAG_ACK) { From e4858e19154c5e7fba5f4376b61641f81d1e05bd Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 19 Aug 2026 14:56:04 +0200 Subject: [PATCH 15/16] F-10267: fix OOB writes in truncated-header test --- src/test/unit/unit_tests_tcp_ack.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/test/unit/unit_tests_tcp_ack.c b/src/test/unit/unit_tests_tcp_ack.c index aeceac81..0804538a 100644 --- a/src/test/unit/unit_tests_tcp_ack.c +++ b/src/test/unit/unit_tests_tcp_ack.c @@ -2816,9 +2816,11 @@ END_TEST START_TEST(test_wolfip_forward_ttl_exceeded_truncated_header_no_send) { struct wolfIP s; - /* Frame shorter than the full IP header: dropped in validation, no - * Time Exceeded can be originated without the quoted header. */ - uint8_t ip_buf[ETH_HEADER_LEN + 10]; + /* The on-wire frame is shorter than the full IP header: dropped in + * validation, no Time Exceeded can be originated without the quoted + * header. Build the header in a full-sized buffer (the field writes + * need the whole header), then pass only the truncated length. */ + uint8_t ip_buf[ETH_HEADER_LEN + IP_HEADER_LEN]; struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)ip_buf; ip4 primary_ip = 0x0A000001U; ip4 secondary_ip = 0xC0A80101U; @@ -2839,7 +2841,7 @@ START_TEST(test_wolfip_forward_ttl_exceeded_truncated_header_no_send) ip->dst = ee32(0xC0A80199U); fix_ip_checksum(ip); - wolfIP_recv_on(&s, TEST_PRIMARY_IF, ip, (uint32_t)sizeof(ip_buf)); + wolfIP_recv_on(&s, TEST_PRIMARY_IF, ip, (uint32_t)(ETH_HEADER_LEN + 10)); ck_assert_uint_eq(last_frame_sent_size, 0U); } END_TEST From b5e5d5db9541cc295a12a26c0f1789ac29ad0f92 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 20 Aug 2026 17:58:54 +0200 Subject: [PATCH 16/16] restore fresh-socket option baseline in listener revert --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_tcp_flow.c | 40 +++++++++++++++++++++++++++++ src/wolfip.c | 17 ++++++++++++ 3 files changed, 58 insertions(+) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 90a0b6c4..3b6d5d76 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -586,6 +586,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_tcp_listener_preaccept_accept_reverts_port); tcase_add_test(tc_utils, test_tcp_listener_preaccept_timeout_reverts_port); tcase_add_test(tc_utils, test_tcp_listener_preaccept_revert_drains_connection_state); + tcase_add_test(tc_utils, test_tcp_listener_revert_restores_option_baseline); tcase_add_test(tc_utils, test_tcp_parse_sack_wraparound_block_accepted); tcase_add_test(tc_utils, test_tcp_parse_options_stops_on_truncated_or_invalid_option_length); tcase_add_test(tc_utils, test_tcp_parse_options_returns_when_frame_has_no_option_bytes); diff --git a/src/test/unit/unit_tests_tcp_flow.c b/src/test/unit/unit_tests_tcp_flow.c index a56cbdc2..76b9e4c4 100644 --- a/src/test/unit/unit_tests_tcp_flow.c +++ b/src/test/unit/unit_tests_tcp_flow.c @@ -5332,3 +5332,43 @@ START_TEST(test_tcp_listener_preaccept_revert_drains_connection_state) ck_assert_int_eq(lsn->sock.tcp.preaccept_timeout_active, 1); } END_TEST + +/* The revert must leave the listener on the same option baseline as a + * freshly allocated socket: the receive-window scale we advertise is a + * property of RXBUF_SIZE, not of the dead connection. A revert that left + * it zeroed would make the next connection on the port advertise WS shift + * 0 (receive window capped at 64KB when RXBUF_SIZE > 0xFFFF) and drop the + * WS/TS offers that accept() copies into new connections. */ +START_TEST(test_tcp_listener_revert_restores_option_baseline) +{ + struct wolfIP s; + int fd; + struct tsocket *lsn; + struct tsocket *fresh; + struct wolfIP_sockaddr_in peer; + socklen_t peer_len = sizeof(peer); + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, LLK_LOCAL_IP, LLK_NET_MASK, 0); + fd = llk_open_listener(&s); + lsn = &s.tcpsockets[SOCKET_UNMARK(fd)]; + + llk_attacker_syn(&s, LLK_ATT_IP, 41000, 1, 0); + llk_complete_handshake(&s, lsn, LLK_ATT_IP, 41000, 1); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_ESTABLISHED); + + /* accept() fails (ESTABLISHED) and reverts the port to LISTEN. */ + memset(&peer, 0, sizeof(peer)); + ck_assert_int_eq(wolfIP_sock_accept(&s, fd, + (struct wolfIP_sockaddr *)&peer, &peer_len), -1); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_LISTEN); + + /* The option baseline matches a freshly allocated socket. */ + fresh = tcp_new_socket(&s); + ck_assert_ptr_nonnull(fresh); + ck_assert_uint_eq(lsn->sock.tcp.rcv_wscale, fresh->sock.tcp.rcv_wscale); + ck_assert_uint_eq(lsn->sock.tcp.ws_offer, fresh->sock.tcp.ws_offer); + ck_assert_uint_eq(lsn->sock.tcp.ts_offer, fresh->sock.tcp.ts_offer); +} +END_TEST diff --git a/src/wolfip.c b/src/wolfip.c index d735bd58..9be2492c 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -3897,6 +3897,23 @@ static void tcp_listener_revert_to_listen(struct tsocket *t) t->sock.tcp.ssthresh = tcp_initial_ssthresh(t->sock.tcp.peer_rwnd); t->sock.tcp.peer_mss = TCP_DEFAULT_MSS; t->sock.tcp.sack_offer = 1; + /* The receive-window scale we advertise is a property of the socket + * (RXBUF_SIZE), not of the dead connection: restore it with the offer + * flags so the baseline matches a freshly allocated socket + * (tcp_new_socket). Without this the next connection on the port + * advertises WS shift 0 and caps the receive window at 64KB when + * RXBUF_SIZE > 0xFFFF. */ +#if RXBUF_SIZE > 0xFFFF + { + uint32_t space = RXBUF_SIZE; + uint8_t shift = 0; + while (shift < 14 && (space >> shift) > 0xFFFF) + shift++; + t->sock.tcp.rcv_wscale = shift; + } +#endif + t->sock.tcp.ws_offer = 1; + t->sock.tcp.ts_offer = 1; t->remote_ip = 0; t->dst_port = 0; t->events = 0;