diff --git a/Makefile b/Makefile index 915e8d89..6a42f664 100644 --- a/Makefile +++ b/Makefile @@ -978,7 +978,8 @@ UNIT_TEST_SRCS:=src/test/unit/unit.c \ src/test/unit/unit_tests_arp_regression.c \ src/test/unit/unit_tests_dns_edges.c \ src/test/unit/unit_tests_misc_edges.c \ - src/test/unit/unit_tests_vlan.c + src/test/unit/unit_tests_vlan.c \ + src/test/unit/unit_tests_forwarding.c unit: build/test/unit diff --git a/src/port/amd/common/gem.h b/src/port/amd/common/gem.h index 56b72c35..a445315b 100644 --- a/src/port/amd/common/gem.h +++ b/src/port/amd/common/gem.h @@ -17,7 +17,8 @@ /* Initialize the GEM, its clock + reset, the PHY, and populate the wolfIP * link-layer device. Returns: - * < 0 on error (negated TRM code) + * < 0 on error (driver-local sentinel, not a TRM code: + * -10 = no PHY responded on MDIO, -11 = gem_phy_init failed) * bits [7:0] PHY MDIO address used * bit [8] link_up flag (1 = link is up at end of init) */ diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index d7207394..68fd3318 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -38,6 +38,7 @@ #include "unit_tests_dns_edges.c" #include "unit_tests_misc_edges.c" #include "unit_tests_vlan.c" +#include "unit_tests_forwarding.c" Suite *wolf_suite(void) { @@ -163,6 +164,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_wolfip_send_port_unreachable_ignores_missing_link_sender); tcase_add_test(tc_utils, test_wolfip_send_port_unreachable_non_ethernet_skips_eth_filter); tcase_add_test(tc_utils, test_wolfip_send_port_unreachable_sets_df); + tcase_add_test(tc_utils, test_wolfip_send_port_unreachable_quotes_no_more_than_datagram); tcase_add_test(tc_utils, test_tcp_adv_win_clamps_and_applies_window_scale); tcase_add_test(tc_utils, test_tcp_segment_acceptable_zero_window_and_overlap_cases); tcase_add_test(tc_utils, test_tcp_segment_acceptable_counts_syn_in_segment_length); @@ -263,6 +265,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_sock_accept_negative_fd); tcase_add_test(tc_utils, test_sock_accept_invalid_tcp_fd); tcase_add_test(tc_utils, test_sock_accept_success_sets_addr); + tcase_add_test(tc_utils, test_tcp_listen_before_ipconfig_accepts_after); tcase_add_test(tc_utils, test_sock_accept_listener_resets_paws_state); tcase_add_test(tc_utils, test_syn_rcvd_rst_listener_resets_paws_state); tcase_add_test(tc_utils, test_sock_accept_no_available_socket); @@ -419,6 +422,8 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_poll_tcp_residual_window_allows_exact_fit); tcase_add_test(tc_utils, test_poll_tcp_zero_window_arms_persist); tcase_add_test(tc_utils, test_tcp_persist_start_stops_when_window_reopens_or_no_unsent_payload); + tcase_add_test(tc_utils, test_tcp_persist_start_no_active_flag_when_timer_heap_full); + tcase_add_test(tc_utils, test_tcp_ctrl_rto_stop_resets_base_rto_after_control_timeout); tcase_add_test(tc_utils, test_tcp_persist_helpers_ignore_non_tcp_and_null_inputs); tcase_add_test(tc_utils, test_tcp_has_pending_unsent_payload_ignores_zero_ip_len_ack_only_desc); tcase_add_test(tc_utils, test_tcp_initial_cwnd_caps_to_iw10_and_half_rwnd); @@ -547,7 +552,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_sock_opts_and_names); tcase_add_test(tc_utils, test_dns_send_query_errors); tcase_add_test(tc_utils, test_dns_schedule_timer_initial_jitter_and_cancel); - tcase_add_test(tc_utils, test_dns_schedule_timer_caps_large_retry_shift); + tcase_add_test(tc_utils, test_dns_schedule_timer_caps_retry_shift); tcase_add_test(tc_utils, test_dns_send_query_schedules_timeout); tcase_add_test(tc_utils, test_dns_send_query_timer_heap_full_aborts); tcase_add_test(tc_utils, test_dns_send_query_send_failure_clears_outstanding_state); @@ -751,6 +756,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_tcp_consume_ooo_wrap_drop_fully_acked); tcase_add_test(tc_utils, test_tcp_store_ooo_overlap_does_not_exhaust_cache); tcase_add_test(tc_utils, test_tcp_ack_sack_early_retransmit_before_three_dupack); + tcase_add_test(tc_utils, test_tcp_ack_reclaims_data_acked_behind_pure_ack); tcase_add_test(tc_utils, test_tcp_ack_forward_ack_after_retransmit_not_duplicate); tcase_add_test(tc_utils, test_tcp_input_listen_syn_without_sack_disables_sack); tcase_add_test(tc_utils, test_tcp_input_listen_syn_arms_control_rto); @@ -768,6 +774,8 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_tcp_mark_unsacked_retransmits_partially_acked_segment); tcase_add_test(tc_utils, test_tcp_mark_unsacked_rescans_after_clearing_stale_sack); tcase_add_test(tc_utils, test_tcp_mark_unsacked_ignores_zero_ip_len_unsent_ack_only_desc); + tcase_add_test(tc_utils, test_flush_tcp_tx_pure_ack_keeps_unacked_data_desc); + tcase_add_test(tc_utils, test_tcp_ack_parked_zero_desc_keeps_rtt_sample); tcase_add_test(tc_utils, test_tcp_ack_sack_blocks_clamped_and_dropped); tcase_add_test(tc_utils, test_tcp_recv_ooo_capacity_limit); tcase_add_test(tc_utils, test_tcp_recv_overlapping_ooo_segments_coalesce_on_consume); @@ -945,6 +953,8 @@ Suite *wolf_suite(void) tcase_add_test(tc_proto, test_ip_recv_forward_arp_queue_and_flush); tcase_add_test(tc_proto, test_arp_flush_pending_ttl_expired); tcase_add_test(tc_proto, test_wolfip_forwarding_basic); + tcase_add_test(tc_proto, test_ip_recv_forwarding_relays_transit_fragments); + tcase_add_test(tc_proto, test_ip_recv_forwarding_drops_local_fragment); tcase_add_test(tc_proto, test_wolfip_forwarding_ttl_expired); tcase_add_test(tc_proto, test_regression_forwarding_no_ttl_exceeded_for_icmp_error); tcase_add_test(tc_proto, test_forward_packet_ip_filter_drop); @@ -990,6 +1000,9 @@ Suite *wolf_suite(void) tcase_add_test(tc_proto, test_udp_sendto_and_recvfrom); tcase_add_test(tc_proto, test_udp_sendto_unbound_socket_receives_reply); tcase_add_test(tc_proto, test_udp_wildcard_bind_receives_all_local_addrs); + tcase_add_test(tc_proto, test_udp_wildcard_bind_before_ipconfig_receives_after); + tcase_add_test(tc_proto, test_udp_wildcard_bind_drops_third_party_dst); + tcase_add_test(tc_proto, test_udp_dhcp_exchange_only_reaches_dhcp_socket); tcase_add_test(tc_proto, test_udp_sendto_respects_mtu_api); tcase_add_test(tc_proto, test_udp_recvfrom_sets_remote_ip); tcase_add_test(tc_proto, test_udp_recvfrom_null_src_addr_len); @@ -1439,6 +1452,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_core, test_poll_tx_tcp_filter_tcp_blocks_send); tcase_add_test(tc_core, test_poll_tx_tcp_send_eagain_breaks_loop); tcase_add_test(tc_core, test_poll_tx_tcp_zero_window_starts_persist); + tcase_add_test(tc_core, test_poll_tx_tcp_zero_window_probe_fires_under_fast_poll); tcase_add_test(tc_core, test_poll_tx_tcp_retransmit_replay); tcase_add_test(tc_core, test_poll_tx_tcp_loopback_path); tcase_add_test(tc_core, test_poll_tx_udp_sends_on_arp_hit); @@ -1791,6 +1805,17 @@ Suite *wolf_suite(void) #endif #endif /* WOLFIP_VLAN */ +#if WOLFIP_ENABLE_FORWARDING + /* --- unit_tests_forwarding.c (router build) --- */ + tcase_add_test(tc_proto, test_fwd_nonfirst_frag_ttl1_silent_drop); + tcase_add_test(tc_proto, test_fwd_first_frag_ttl1_sends_ttl_exceeded); + tcase_add_test(tc_proto, test_fwd_nonfirst_frag_df_oversize_silent_drop); + tcase_add_test(tc_proto, test_fwd_first_frag_df_oversize_sends_frag_needed); + tcase_add_test(tc_proto, test_fwd_nonfirst_frag_bad_option_silent_drop); + tcase_add_test(tc_proto, test_fwd_multicast_dest_bad_option_silent_drop); + tcase_add_test(tc_proto, test_fwd_nonfirst_frag_l4_filter_not_notified); +#endif /* WOLFIP_ENABLE_FORWARDING */ + suite_add_tcase(s, tc_core); suite_add_tcase(s, tc_utils); suite_add_tcase(s, tc_proto); diff --git a/src/test/unit/unit_tests_api.c b/src/test/unit/unit_tests_api.c index b0674e91..cb555c5e 100644 --- a/src/test/unit/unit_tests_api.c +++ b/src/test/unit/unit_tests_api.c @@ -1013,6 +1013,202 @@ START_TEST(test_udp_wildcard_bind_receives_all_local_addrs) } END_TEST +/* Embedded start-up order: the socket is bound before the interface has + * any address. The bind snapshot latches local_ip == 0; the stack must + * still deliver once the address arrives, not answer port-unreachable + * for the life of the socket. */ +START_TEST(test_udp_wildcard_bind_before_ipconfig_receives_after) +{ + struct wolfIP s; + int sd; + struct wolfIP_sockaddr_in sin; + struct wolfIP_sockaddr_in from; + socklen_t from_len = sizeof(from); + uint8_t payload[4] = {1, 2, 3, 4}; + uint8_t rxbuf[8]; + ip4 local_ip = 0x0A000001U; + int ret; + + wolfIP_init(&s); + mock_link_init(&s); + + sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + ck_assert_int_gt(sd, 0); + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = ee16(5353); + sin.sin_addr.s_addr = 0U; /* INADDR_ANY */ + ck_assert_int_eq(wolfIP_sock_bind(&s, sd, (struct wolfIP_sockaddr *)&sin, + sizeof(sin)), 0); + /* No interface address existed at bind time: the snapshot is 0. */ + ck_assert_uint_eq(s.udpsockets[SOCKET_UNMARK(sd)].local_ip, 0); + + wolfIP_ipconfig_set(&s, local_ip, 0xFFFFFF00U, 0); + + inject_udp_datagram(&s, TEST_PRIMARY_IF, 0x0A000064U, local_ip, + 60000, 5353, payload, sizeof(payload)); + + memset(&from, 0, sizeof(from)); + ret = wolfIP_sock_recvfrom(&s, sd, rxbuf, sizeof(rxbuf), 0, + (struct wolfIP_sockaddr *)&from, &from_len); + ck_assert_int_eq(ret, (int)sizeof(payload)); + ck_assert_mem_eq(rxbuf, payload, sizeof(payload)); + /* Delivered, not bounced with ICMP Port Unreachable. */ + ck_assert_uint_eq(last_frame_sent_count, 0); +} +END_TEST + +/* Same start-up order on the TCP side: bind + listen before the address + * is configured. A SYN addressed to the just-configured address must be + * taken by the wildcard listener and accepted. */ +START_TEST(test_tcp_listen_before_ipconfig_accepts_after) +{ + struct wolfIP s; + int listen_sd; + int client_sd; + struct tsocket *listener; + struct wolfIP_sockaddr_in sin; + socklen_t alen = sizeof(sin); + ip4 local_ip = 0x0A000001U; + + wolfIP_init(&s); + mock_link_init(&s); + + listen_sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_STREAM, + WI_IPPROTO_TCP); + ck_assert_int_gt(listen_sd, 0); + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = ee16(8080); + sin.sin_addr.s_addr = 0U; /* INADDR_ANY */ + ck_assert_int_eq(wolfIP_sock_bind(&s, listen_sd, + (struct wolfIP_sockaddr *)&sin, sizeof(sin)), 0); + ck_assert_int_eq(wolfIP_sock_listen(&s, listen_sd, 1), 0); + ck_assert_uint_eq(s.tcpsockets[SOCKET_UNMARK(listen_sd)].local_ip, 0); + + wolfIP_ipconfig_set(&s, local_ip, 0xFFFFFF00U, 0); + + inject_tcp_syn(&s, TEST_PRIMARY_IF, local_ip, 8080); + listener = &s.tcpsockets[SOCKET_UNMARK(listen_sd)]; + ck_assert_int_eq(listener->sock.tcp.state, TCP_SYN_RCVD); + + client_sd = wolfIP_sock_accept(&s, listen_sd, (struct wolfIP_sockaddr *)&sin, + &alen); + ck_assert_int_gt(client_sd, 0); +} +END_TEST + +START_TEST(test_udp_wildcard_bind_drops_third_party_dst) +{ + struct wolfIP s; + int sd; + struct wolfIP_sockaddr_in sin; + uint8_t payload[4] = {1, 2, 3, 4}; + uint8_t rxbuf[LINK_MTU]; + int ret; + + setup_stack_with_two_ifaces(&s, 0x0A000001U, 0x0A010001U); + + sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + ck_assert_int_gt(sd, 0); + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = ee16(5353); + sin.sin_addr.s_addr = 0U; /* INADDR_ANY */ + ck_assert_int_eq(wolfIP_sock_bind(&s, sd, (struct wolfIP_sockaddr *)&sin, + sizeof(sin)), 0); + + /* A datagram addressed to a third-party IP is not for this host: RFC + * 1122 requires silent discard. Pre-fix the wildcard bind matched any + * destination, so an L2-adjacent attacker could inject application + * traffic addressed to someone else. */ + inject_udp_datagram(&s, TEST_PRIMARY_IF, 0x0A000064U, 0xC0A80164U, + 60000, 5353, payload, sizeof(payload)); + ret = wolfIP_sock_recvfrom(&s, sd, rxbuf, sizeof(rxbuf), 0, + (struct wolfIP_sockaddr *)NULL, NULL); + ck_assert_int_eq(ret, -WOLFIP_EAGAIN); + + /* The gate must not over-reject: a limited broadcast is still + * delivered to the wildcard bind. */ + inject_udp_datagram(&s, TEST_PRIMARY_IF, 0x0A000064U, 0xFFFFFFFFU, + 60002, 5353, payload, sizeof(payload)); + ret = wolfIP_sock_recvfrom(&s, sd, rxbuf, sizeof(rxbuf), 0, + (struct wolfIP_sockaddr *)NULL, NULL); + ck_assert_int_eq(ret, (int)sizeof(payload)); + ck_assert_mem_eq(rxbuf, payload, sizeof(payload)); +} +END_TEST + +/* Regression for the DHCP exception in the udp_try_recv() gate: a + * third-party-addressed 67->68 datagram (the pre-address RFC 2131 + * OFFER/ACK window) must reach only the DHCP client's own socket - never + * a plain wildcard bind on port 68 - and never at all when DHCP is off. + * The phases reuse the single wildcard :68 bind (the stack rejects a + * second wildcard bind on the same port). */ +START_TEST(test_udp_dhcp_exchange_only_reaches_dhcp_socket) +{ + struct wolfIP s; + int sd; + struct wolfIP_sockaddr_in sin; + uint8_t payload[4] = {1, 2, 3, 4}; + uint8_t rxbuf[LINK_MTU]; + int ret; + ip4 third_party = 0xC0A80164U; /* 192.168.1.100, not ours */ + + setup_stack_with_two_ifaces(&s, 0x0A000001U, 0x0A010001U); + + sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, + WI_IPPROTO_UDP); + ck_assert_int_gt(sd, 0); + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = ee16(DHCP_CLIENT_PORT); + sin.sin_addr.s_addr = 0U; + ck_assert_int_eq(wolfIP_sock_bind(&s, sd, (struct wolfIP_sockaddr *)&sin, + sizeof(sin)), 0); + + /* Phase 1: a plain application socket, wildcard bound on port 68, + * DHCP exchanging: the third-party addressed datagram must NOT be + * delivered to it. */ + s.dhcp_state = DHCP_DISCOVER_SENT; + s.dhcp_udp_sd = 0; /* no DHCP socket */ + inject_udp_datagram(&s, TEST_PRIMARY_IF, 0x0A000064U, third_party, + DHCP_SERVER_PORT, DHCP_CLIENT_PORT, payload, sizeof(payload)); + ret = wolfIP_sock_recvfrom(&s, sd, rxbuf, sizeof(rxbuf), 0, + (struct wolfIP_sockaddr *)NULL, NULL); + ck_assert_int_eq(ret, -WOLFIP_EAGAIN); + + wolfIP_sock_close(&s, sd); + + /* Phase 2: the same socket registered as the DHCP client socket: + * the legitimate pre-address exchange is delivered. */ + sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, + WI_IPPROTO_UDP); + ck_assert_int_gt(sd, 0); + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = ee16(DHCP_CLIENT_PORT); + sin.sin_addr.s_addr = 0U; + ck_assert_int_eq(wolfIP_sock_bind(&s, sd, (struct wolfIP_sockaddr *)&sin, + sizeof(sin)), 0); + s.dhcp_udp_sd = sd; + inject_udp_datagram(&s, TEST_PRIMARY_IF, 0x0A000064U, third_party, + DHCP_SERVER_PORT, DHCP_CLIENT_PORT, payload, sizeof(payload)); + ret = wolfIP_sock_recvfrom(&s, sd, rxbuf, sizeof(rxbuf), 0, + (struct wolfIP_sockaddr *)NULL, NULL); + ck_assert_int_eq(ret, (int)sizeof(payload)); + ck_assert_mem_eq(rxbuf, payload, sizeof(payload)); + + /* Phase 3: DHCP off: the same datagram is dropped at the gate. */ + s.dhcp_state = DHCP_OFF; + inject_udp_datagram(&s, TEST_PRIMARY_IF, 0x0A000064U, third_party, + DHCP_SERVER_PORT, DHCP_CLIENT_PORT, payload, sizeof(payload)); + ret = wolfIP_sock_recvfrom(&s, sd, rxbuf, sizeof(rxbuf), 0, + (struct wolfIP_sockaddr *)NULL, NULL); + ck_assert_int_eq(ret, -WOLFIP_EAGAIN); +} +END_TEST + START_TEST(test_udp_sendto_respects_mtu_api) { struct wolfIP s; diff --git a/src/test/unit/unit_tests_dns_dhcp.c b/src/test/unit/unit_tests_dns_dhcp.c index 62d707ba..e42f0dbc 100644 --- a/src/test/unit/unit_tests_dns_dhcp.c +++ b/src/test/unit/unit_tests_dns_dhcp.c @@ -3367,17 +3367,20 @@ START_TEST(test_dns_schedule_timer_initial_jitter_and_cancel) } END_TEST -START_TEST(test_dns_schedule_timer_caps_large_retry_shift) +START_TEST(test_dns_schedule_timer_caps_retry_shift) { struct wolfIP s; wolfIP_init(&s); s.last_tick = 100U; - s.dns_retry_count = 64U; + /* The largest shift reachable: the single increment site caps the + * count at DNS_QUERY_RETRIES. */ + s.dns_retry_count = DNS_QUERY_RETRIES; dns_schedule_timer(&s); ck_assert_int_ne(s.dns_timer, NO_TIMER); - ck_assert_uint_eq(find_timer_expiry(&s, s.dns_timer), UINT64_MAX); + ck_assert_uint_eq(find_timer_expiry(&s, s.dns_timer), + 100U + (DNS_QUERY_TIMEOUT << DNS_QUERY_RETRIES)); } END_TEST @@ -6641,7 +6644,11 @@ START_TEST(test_udp_try_recv_conf_null) udp->dst_port = ee16(1234); udp->len = ee16(UDP_HEADER_LEN + 4); udp_try_recv(&s, TEST_PRIMARY_IF, udp, (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN + 4)); - ck_assert_ptr_nonnull(fifo_peek(&ts->sock.udp.rxbuf)); + /* With no configured interfaces no destination is local, so the + * datagram is dropped even though the socket's manually set local_ip + * matches the destination (RFC 1122: a host consumes only traffic + * addressed to its own addresses). */ + ck_assert_ptr_eq(fifo_peek(&ts->sock.udp.rxbuf), NULL); } END_TEST diff --git a/src/test/unit/unit_tests_forwarding.c b/src/test/unit/unit_tests_forwarding.c new file mode 100644 index 00000000..42d9e673 --- /dev/null +++ b/src/test/unit/unit_tests_forwarding.c @@ -0,0 +1,440 @@ +/* unit_tests_forwarding.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfIP TCP/IP stack. + * + * wolfIP is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfIP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* Forwarding-path tests (router build). This module forces + * WOLFIP_ENABLE_FORWARDING=1 so it is self-contained: every test here is + * dead code in a non-forwarding build, and the module must not silently + * lose coverage if the shared harness default changes. + * + * Covers the branches the general ip_recv tests leave open: + * - RFC 1812 4.3.2.7: no ICMP error for a non-first fragment (Time + * Exceeded, Fragmentation Needed, Parameter Problem all suppressed), + * while the first fragment of the same datagram still gets its reply. + * - RFC 1812 4.3.2.4: Parameter Problem suppressed for multicast + * destinations. + * - RFC 1858: the SENDING L4 filter hooks must not be notified for a + * non-first fragment, whose bytes at the IHL offset are payload, not a + * transport header (port-based policy matching on garbage). + */ +#undef WOLFIP_ENABLE_FORWARDING +#define WOLFIP_ENABLE_FORWARDING 1 + +/* Test-local filter counters for the L4-notify suppression test. */ +static int fwd_filter_notify_count; +static uint16_t fwd_filter_last_dport; + +static int fwd_filter_count_cb(void *arg, + const struct wolfIP_filter_event *event) +{ + (void)arg; + if (event->reason == WOLFIP_FILT_SENDING && + event->meta.ip_proto == WOLFIP_FILTER_PROTO_TCP) { + fwd_filter_notify_count++; + fwd_filter_last_dport = event->meta.l4.tcp.dst_port; + } + return 0; +} + +static void fwd_arp_store(struct wolfIP *s, unsigned int if_idx, ip4 ip, + const uint8_t *mac) +{ + s->arp.neighbors[0].ip = ip; + s->arp.neighbors[0].if_idx = if_idx; + memcpy(s->arp.neighbors[0].mac, mac, 6); +} + +/* ========================================================================= + * RFC 1812 4.3.2.7: non-first fragment, TTL=1 - silent drop + * ========================================================================= + * A router must not generate an ICMP error for a non-first fragment: it + * cannot validate what the fragment does not carry. The datagram is not + * relayed (TTL expired) and no Time Exceeded is sent. + */ +START_TEST(test_fwd_nonfirst_frag_ttl1_silent_drop) +{ + struct wolfIP s; + uint8_t frame[ETH_HEADER_LEN + IP_HEADER_LEN + 8]; + 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; + static const uint8_t dest_mac[6] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; + + setup_stack_with_two_ifaces(&s, primary_ip, secondary_ip); + wolfIP_filter_set_callback(NULL, NULL); + fwd_arp_store(&s, TEST_SECOND_IF, dest_ip, dest_mac); + last_frame_sent_count = 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->flags_fo = ee16(0x0001U); /* MF=0, offset=1 (8 bytes) */ + ip->ttl = 1; + ip->proto = WI_IPPROTO_UDP; + ip->len = ee16(IP_HEADER_LEN + 8); + 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)); + + /* Silent: no Time Exceeded, no relay. */ + ck_assert_uint_eq(last_frame_sent_count, 0); +} +END_TEST + +/* ========================================================================= + * RFC 1812 4.3.2.7 selectivity: first fragment, TTL=1 - Time Exceeded + * ========================================================================= + * The same datagram's first fragment (offset 0) carries a valid transport + * header, so the Time Exceeded reply is generated. Pairs with the + * non-first-fragment silent drop above to prove the guard is selective. + */ +START_TEST(test_fwd_first_frag_ttl1_sends_ttl_exceeded) +{ + struct wolfIP s; + uint8_t frame[ETH_HEADER_LEN + IP_HEADER_LEN + 8]; + 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; + static const uint8_t dest_mac[6] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; + + setup_stack_with_two_ifaces(&s, primary_ip, secondary_ip); + wolfIP_filter_set_callback(NULL, NULL); + fwd_arp_store(&s, TEST_SECOND_IF, dest_ip, dest_mac); + last_frame_sent_count = 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->flags_fo = ee16(0x2000U); /* MF=1, offset=0 */ + ip->ttl = 1; + ip->proto = WI_IPPROTO_UDP; + ip->len = ee16(IP_HEADER_LEN + 8); + 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 8 payload bytes present. */ + ck_assert_uint_eq(last_frame_sent_count, 1); + 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 + IP_HEADER_LEN], + ICMP_TTL_EXCEEDED); +} +END_TEST + +/* ========================================================================= + * RFC 1812 4.3.2.7: non-first fragment, DF set, larger than egress MTU + * - silent drop + * ========================================================================= + * The Fragmentation Needed reply is suppressed for a non-first fragment; + * the datagram is dropped (it does not fit the egress and cannot be + * fragmented further). + */ +START_TEST(test_fwd_nonfirst_frag_df_oversize_silent_drop) +{ + struct wolfIP s; + uint8_t frame[ETH_HEADER_LEN + IP_HEADER_LEN + 580]; + 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; + static const uint8_t dest_mac[6] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; + + setup_stack_with_two_ifaces(&s, primary_ip, secondary_ip); + wolfIP_filter_set_callback(NULL, NULL); + /* Egress frame budget 590 bytes: IP MTU 576 (the IPv4 minimum). */ + s.ll_dev[TEST_SECOND_IF].mtu = 590; + fwd_arp_store(&s, TEST_SECOND_IF, dest_ip, dest_mac); + last_frame_sent_count = 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->flags_fo = ee16(0x4001U); /* DF=1, offset=1 (8 bytes) */ + ip->ttl = 64; + ip->proto = WI_IPPROTO_UDP; + ip->len = ee16(IP_HEADER_LEN + 580); /* 600 > egress MTU 576 */ + 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)); + + /* Silent: no Fragmentation Needed, no relay. */ + ck_assert_uint_eq(last_frame_sent_count, 0); +} +END_TEST + +/* ========================================================================= + * RFC 1812 4.3.2.7 selectivity: first fragment, DF set, larger than + * egress MTU - Fragmentation Needed + * ========================================================================= + * Pairs with the non-first-fragment silent drop above: the first fragment + * carries a valid transport header, so the Fragmentation Needed reply is + * generated. + */ +START_TEST(test_fwd_first_frag_df_oversize_sends_frag_needed) +{ + struct wolfIP s; + uint8_t frame[ETH_HEADER_LEN + IP_HEADER_LEN + 580]; + 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; + static const uint8_t dest_mac[6] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; + + setup_stack_with_two_ifaces(&s, primary_ip, secondary_ip); + wolfIP_filter_set_callback(NULL, NULL); + s.ll_dev[TEST_SECOND_IF].mtu = 590; + fwd_arp_store(&s, TEST_SECOND_IF, dest_ip, dest_mac); + last_frame_sent_count = 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->flags_fo = ee16(0x4000U); /* DF=1, offset=0 */ + ip->ttl = 64; + ip->proto = WI_IPPROTO_UDP; + ip->len = ee16(IP_HEADER_LEN + 580); /* 600 > egress MTU 576 */ + 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)); + + /* Fragmentation Needed with the egress next-hop MTU (576 = 0x0240). */ + ck_assert_uint_eq(last_frame_sent_count, 1); + ck_assert_uint_eq(last_frame_sent[ETH_HEADER_LEN + IP_HEADER_LEN], + ICMP_DEST_UNREACH); + ck_assert_uint_eq(last_frame_sent[ETH_HEADER_LEN + IP_HEADER_LEN + 1], + ICMP_FRAG_NEEDED); + ck_assert_uint_eq(last_frame_sent[ETH_HEADER_LEN + IP_HEADER_LEN + 6], + 0x02); + ck_assert_uint_eq(last_frame_sent[ETH_HEADER_LEN + IP_HEADER_LEN + 7], + 0x40); +} +END_TEST + +/* ========================================================================= + * RFC 1812 4.3.2.7: non-first fragment with a malformed IP option - + * silent drop + * ========================================================================= + * The Parameter Problem reply is suppressed for a non-first fragment; the + * datagram is dropped either way. + */ +START_TEST(test_fwd_nonfirst_frag_bad_option_silent_drop) +{ + struct wolfIP s; + uint8_t frame[ETH_HEADER_LEN + 24 + 8]; + 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; + static const uint8_t dest_mac[6] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; + uint8_t *opt; + + setup_stack_with_two_ifaces(&s, primary_ip, secondary_ip); + wolfIP_filter_set_callback(NULL, NULL); + fwd_arp_store(&s, TEST_SECOND_IF, dest_ip, dest_mac); + last_frame_sent_count = 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 = 0x46; /* IHL 6: 20-byte header + 4 option bytes */ + ip->flags_fo = ee16(0x0001U); /* MF=0, offset=1 (8 bytes) */ + ip->ttl = 64; + ip->proto = WI_IPPROTO_UDP; + ip->len = ee16(24 + 8); + ip->src = ee32(src_ip); + ip->dst = ee32(dest_ip); + /* Record Route option at offset 20 with length 100: runs past the end + * of the 4-byte option area. */ + opt = frame + ETH_HEADER_LEN + IP_HEADER_LEN; + opt[0] = 0x44; + opt[1] = 100; + fix_ip_checksum(ip); + + ip_recv(&s, TEST_PRIMARY_IF, ip, (uint32_t)sizeof(frame)); + + /* Silent: no Parameter Problem, no relay. */ + ck_assert_uint_eq(last_frame_sent_count, 0); +} +END_TEST + +/* ========================================================================= + * RFC 1812 4.3.2.4: multicast destination with a malformed IP option - + * silent drop + * ========================================================================= + * Multicast destinations are exempt from the Parameter Problem reply + * (error storms on group traffic); the datagram is dropped. + */ +START_TEST(test_fwd_multicast_dest_bad_option_silent_drop) +{ + struct wolfIP s; + uint8_t frame[ETH_HEADER_LEN + 24 + 8]; + struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)frame; + ip4 primary_ip = 0x0A000001U; + ip4 secondary_ip = 0xC0A80101U; + ip4 dest_ip = 0xE0000001U; /* 224.0.0.1, all-hosts */ + ip4 src_ip = 0x0A000002U; + ip4 gw_ip = 0xC0A801FEU; + static const uint8_t gw_mac[6] = {0x20, 0x21, 0x22, 0x23, 0x24, 0x25}; + uint8_t *opt; + + setup_stack_with_two_ifaces(&s, primary_ip, secondary_ip); + wolfIP_filter_set_callback(NULL, NULL); + /* The multicast destination is not on any connected subnet: a default + * route gives the forwarding path an egress so the option check runs. */ + ck_assert_int_eq(wolfIP_route_add(&s, TEST_SECOND_IF, 0x00000000U, 0, + gw_ip), 0); + fwd_arp_store(&s, TEST_SECOND_IF, gw_ip, gw_mac); + last_frame_sent_count = 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 = 0x46; /* IHL 6: 20-byte header + 4 option bytes */ + ip->flags_fo = 0; + ip->ttl = 64; + ip->proto = WI_IPPROTO_UDP; + ip->len = ee16(24 + 8); + ip->src = ee32(src_ip); + ip->dst = ee32(dest_ip); + opt = frame + ETH_HEADER_LEN + IP_HEADER_LEN; + opt[0] = 0x44; + opt[1] = 100; + fix_ip_checksum(ip); + + ip_recv(&s, TEST_PRIMARY_IF, ip, (uint32_t)sizeof(frame)); + + /* Silent: multicast destination is exempt, no relay. */ + ck_assert_uint_eq(last_frame_sent_count, 0); +} +END_TEST + +/* ========================================================================= + * RFC 1858: SENDING L4 filter hooks are not notified for a non-first + * fragment + * ========================================================================= + * The bytes at the IHL offset of a non-first fragment are payload, not a + * transport header. Notifying the TCP/UDP hooks would let port-based + * policy match on garbage. The non-first fragment is relayed without a + * TCP notification; the first fragment of a TCP datagram is notified with + * the real header's destination port. + */ +START_TEST(test_fwd_nonfirst_frag_l4_filter_not_notified) +{ + struct wolfIP s; + uint8_t frame[ETH_HEADER_LEN + IP_HEADER_LEN + TCP_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; + static const uint8_t dest_mac[6] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; + uint8_t *payload; + + setup_stack_with_two_ifaces(&s, primary_ip, secondary_ip); + wolfIP_filter_set_callback(fwd_filter_count_cb, NULL); + wolfIP_filter_set_tcp_mask(WOLFIP_FILT_MASK(WOLFIP_FILT_SENDING)); + wolfIP_filter_set_udp_mask(0); + wolfIP_filter_set_icmp_mask(0); + wolfIP_filter_set_ip_mask(0); + fwd_arp_store(&s, TEST_SECOND_IF, dest_ip, dest_mac); + fwd_filter_notify_count = 0; + fwd_filter_last_dport = 0; + last_frame_sent_count = 0; + + /* Non-first fragment: the 20 payload bytes are crafted to look like a + * TCP header (src port 80, dst port 443). If the notify fired on them, + * the counter would move. */ + 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->flags_fo = ee16(0x0001U); /* MF=0, offset=1: non-first fragment */ + ip->ttl = 64; + ip->proto = WI_IPPROTO_TCP; + ip->len = ee16(IP_HEADER_LEN + TCP_HEADER_LEN); + ip->src = ee32(src_ip); + ip->dst = ee32(dest_ip); + payload = frame + ETH_HEADER_LEN + IP_HEADER_LEN; + payload[0] = 0x00; payload[1] = 0x50; /* src port 80 */ + payload[2] = 0x01; payload[3] = 0xBB; /* dst port 443 */ + fix_ip_checksum(ip); + + ip_recv(&s, TEST_PRIMARY_IF, ip, (uint32_t)sizeof(frame)); + + /* Relayed, and no TCP notification was generated from the payload. */ + ck_assert_uint_eq(last_frame_sent_count, 1); + ck_assert_uint_eq(fwd_filter_notify_count, 0); + + /* First fragment of a TCP datagram: the notify fires with the real + * header's destination port. */ + 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->flags_fo = ee16(0x2000U); /* MF=1, offset=0 */ + ip->ttl = 64; + ip->proto = WI_IPPROTO_TCP; + ip->len = ee16(IP_HEADER_LEN + TCP_HEADER_LEN); + ip->src = ee32(src_ip); + ip->dst = ee32(dest_ip); + payload = frame + ETH_HEADER_LEN + IP_HEADER_LEN; + payload[0] = 0x00; payload[1] = 0x50; /* src port 80 */ + payload[2] = 0x01; payload[3] = 0xBB; /* dst port 443 */ + fix_ip_checksum(ip); + + last_frame_sent_count = 0; + ip_recv(&s, TEST_PRIMARY_IF, ip, (uint32_t)sizeof(frame)); + + ck_assert_uint_eq(last_frame_sent_count, 1); + ck_assert_uint_eq(fwd_filter_notify_count, 1); + ck_assert_uint_eq(fwd_filter_last_dport, ee16(443)); + + wolfIP_filter_set_callback(NULL, NULL); + wolfIP_filter_set_tcp_mask(0); +} +END_TEST diff --git a/src/test/unit/unit_tests_ip_arp_recv.c b/src/test/unit/unit_tests_ip_arp_recv.c index 5bf29cbb..6cbe11ec 100644 --- a/src/test/unit/unit_tests_ip_arp_recv.c +++ b/src/test/unit/unit_tests_ip_arp_recv.c @@ -799,6 +799,10 @@ START_TEST(test_ip_recv_l2_group_dhcp_still_reaches_local_udp) ip4 dest_ip = 0x0A000063U; /* 10.0.0.99 — not yet ours */ setup_stack_with_two_ifaces(&s, primary_ip, secondary_ip); + /* The client is mid-exchange: OFFER/ACK only arrive in this window, and + * the udp_try_recv() gate only admits third-party addressed 67->68 + * datagrams while DHCP is running. */ + s.dhcp_state = DHCP_DISCOVER_SENT; f11438_install_rx_observer(); memset(frame, 0, sizeof(frame)); @@ -1120,6 +1124,9 @@ END_TEST START_TEST(test_ip_recv_loopback_dst_on_non_loopback_dropped) { 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; + uint8_t *udp_hdr = frame + ETH_HEADER_LEN + IP_HEADER_LEN; struct tsocket *ts; ip4 local_ip = 0x0A000001U; ip4 remote_ip = 0x0A000002U; @@ -1129,16 +1136,33 @@ START_TEST(test_ip_recv_loopback_dst_on_non_loopback_dropped) mock_link_init(&s); wolfIP_ipconfig_set(&s, local_ip, 0xFFFFFF00U, 0); + /* A wildcard socket on the destination port: the drop must happen in + * ip_recv, before any demux can see the datagram. */ ts = udp_new_socket(&s); ck_assert_ptr_nonnull(ts); ts->src_port = 1234; ts->local_ip = IPADDR_ANY; /* Inject from non-loopback interface to loopback destination */ - inject_udp_datagram(&s, TEST_PRIMARY_IF, remote_ip, loop_dst, - 9999, 1234, NULL, 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 = 64; + ip->proto = WI_IPPROTO_UDP; + ip->len = ee16(IP_HEADER_LEN + UDP_HEADER_LEN); + ip->src = ee32(remote_ip); + ip->dst = ee32(loop_dst); + fix_ip_checksum(ip); + udp_hdr[0] = 0x27; udp_hdr[1] = 0x0F; /* src port 9999 */ + udp_hdr[2] = 0x04; udp_hdr[3] = 0xD2; /* dst port 1234 */ + udp_hdr[4] = 0x00; udp_hdr[5] = UDP_HEADER_LEN; + /* csum left 0: validation is skipped, the drop is upstream of it. */ + + ip_recv(&s, TEST_PRIMARY_IF, ip, (uint32_t)sizeof(frame)); - /* Must be dropped — loopback addresses must not arrive on wire */ + /* Must be dropped - loopback addresses must not arrive on wire */ ck_assert_ptr_eq(fifo_peek(&ts->sock.udp.rxbuf), NULL); ck_assert_uint_eq(ts->events & CB_EVENT_READABLE, 0); } @@ -1152,6 +1176,9 @@ END_TEST START_TEST(test_ip_recv_loopback_src_on_non_loopback_dropped) { 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; + uint8_t *udp_hdr = frame + ETH_HEADER_LEN + IP_HEADER_LEN; struct tsocket *ts; ip4 local_ip = 0x0A000001U; ip4 loop_src = 0x7F000002U; /* 127.0.0.2 as source */ @@ -1165,8 +1192,24 @@ START_TEST(test_ip_recv_loopback_src_on_non_loopback_dropped) ts->src_port = 1234; ts->local_ip = IPADDR_ANY; - inject_udp_datagram(&s, TEST_PRIMARY_IF, loop_src, local_ip, - 9999, 1234, NULL, 0); + /* Loopback source, valid local destination: the symmetric source + * check in ip_recv must drop it. */ + 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 = 64; + ip->proto = WI_IPPROTO_UDP; + ip->len = ee16(IP_HEADER_LEN + UDP_HEADER_LEN); + ip->src = ee32(loop_src); + ip->dst = ee32(local_ip); + fix_ip_checksum(ip); + udp_hdr[0] = 0x27; udp_hdr[1] = 0x0F; /* src port 9999 */ + udp_hdr[2] = 0x04; udp_hdr[3] = 0xD2; /* dst port 1234 */ + udp_hdr[4] = 0x00; udp_hdr[5] = UDP_HEADER_LEN; + + ip_recv(&s, TEST_PRIMARY_IF, ip, (uint32_t)sizeof(frame)); ck_assert_ptr_eq(fifo_peek(&ts->sock.udp.rxbuf), NULL); } diff --git a/src/test/unit/unit_tests_poll_dispatcher.c b/src/test/unit/unit_tests_poll_dispatcher.c index 8099656d..e088cc6a 100644 --- a/src/test/unit/unit_tests_poll_dispatcher.c +++ b/src/test/unit/unit_tests_poll_dispatcher.c @@ -792,6 +792,62 @@ START_TEST(test_poll_tx_tcp_zero_window_starts_persist) } END_TEST +/* Regression: tcp_persist_start() used to cancel and re-insert the persist + * timer on every call, and flush_tcp_tx() calls it on every poll while the + * peer window is zero. With a poll cadence shorter than TCP_PERSIST_MIN_MS + * (the normal embedded main-loop pattern) the deadline was pushed forward + * before it could expire, so tcp_persist_cb() never ran and no zero-window + * probe was ever transmitted (RFC 9293 3.8.6.1: the probe is the only + * recovery when the peer's window-reopening ACK is lost). */ +START_TEST(test_poll_tx_tcp_zero_window_probe_fires_under_fast_poll) +{ + struct wolfIP s; + struct tsocket *ts; + struct wolfIP_tcp_seg *probe; + ip4 local_ip = 0x0A000001U; + ip4 remote_ip = 0x0A000002U; + uint8_t peer_mac[6] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x04}; + uint64_t now; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, local_ip, 0xFFFFFF00U, 0); + wolfIP_filter_set_callback(NULL, NULL); + + s.arp.neighbors[0].ip = remote_ip; + s.arp.neighbors[0].if_idx = TEST_PRIMARY_IF; + memcpy(s.arp.neighbors[0].mac, peer_mac, 6); + + ts = &s.tcpsockets[0]; + setup_tcp_socket(&s, ts, local_ip, remote_ip, TEST_PRIMARY_IF); + /* Force zero peer window */ + ts->sock.tcp.peer_rwnd = 0; + ts->sock.tcp.cwnd = TCP_MSS; + + ck_assert_int_eq(enqueue_tcp_tx(ts, 4, TCP_FLAG_ACK | TCP_FLAG_PSH), 0); + + mock_link_capture_reset(); + + /* Poll every 100 ms (10x faster than TCP_PERSIST_MIN_MS) up to and + * including the first persist deadline (200 + 1000 ms). */ + for (now = 200; now <= 1200; now += 100) { + (void)wolfIP_poll(&s, now); + if (now < 1200) + ck_assert_uint_eq(last_frame_sent_count, 0U); + } + + /* The probe fired: exactly one frame, the 1-byte zero-window probe + * retransmitting from snd_una. Pre-fix nothing was ever transmitted. */ + ck_assert_uint_eq(last_frame_sent_count, 1U); + ck_assert_int_eq(ts->sock.tcp.persist_backoff, 1); + probe = (struct wolfIP_tcp_seg *)(last_frame_sent + ETH_HEADER_LEN + + IP_HEADER_LEN); + ck_assert_uint_eq(ee32(probe->seq), ts->sock.tcp.snd_una); + /* Probe re-armed with backoff: next deadline 1200 + 1000 ms. */ + ck_assert_int_eq(ts->sock.tcp.persist_active, 1); +} +END_TEST + START_TEST(test_poll_tx_tcp_retransmit_replay) { struct wolfIP s; diff --git a/src/test/unit/unit_tests_proto.c b/src/test/unit/unit_tests_proto.c index 78584294..9b8ce409 100644 --- a/src/test/unit/unit_tests_proto.c +++ b/src/test/unit/unit_tests_proto.c @@ -1584,6 +1584,71 @@ START_TEST(test_tcp_persist_start_stops_when_window_reopens_or_no_unsent_payload } END_TEST +/* A persist arm that cannot take a timer slot must not leave the active + * flag set: with no timer behind it the probe would never fire and the + * sender would stall on a zero-window peer (F-14171). */ +START_TEST(test_tcp_persist_start_no_active_flag_when_timer_heap_full) +{ + struct wolfIP s; + struct tsocket *ts; + struct wolfIP_timer dummy; + uint32_t i; + + wolfIP_init(&s); + mock_link_init(&s); + + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->sock.tcp.state = TCP_ESTABLISHED; + ts->sock.tcp.peer_rwnd = 0; + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + ck_assert_int_eq(enqueue_tcp_tx(ts, 8, (TCP_FLAG_ACK | TCP_FLAG_PSH)), 0); + + /* Fill the timer heap so the persist arm cannot take a slot. */ + memset(&dummy, 0, sizeof(dummy)); + for (i = 0; i < MAX_TIMERS; i++) { + dummy.expires = 1000000U + i; + ck_assert_int_ne(timers_binheap_insert(&s.timers, dummy), NO_TIMER); + } + + tcp_persist_start(ts, 1000); + ck_assert_uint_eq(ts->sock.tcp.persist_active, 0); + ck_assert_int_eq(ts->sock.tcp.tmr_persist, NO_TIMER); +} +END_TEST + +/* RFC 6298 5.7: if a control timeout (SYN retransmit) occurred while the + * base RTO was below 3 s, the base must be reinitialized to 3 s when the + * control sequence completes (F-8566). */ +START_TEST(test_tcp_ctrl_rto_stop_resets_base_rto_after_control_timeout) +{ + struct wolfIP s; + struct tsocket *ts; + + wolfIP_init(&s); + mock_link_init(&s); + + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->sock.tcp.tmr_rto = NO_TIMER; + + ts->sock.tcp.rto = 1000; + ts->sock.tcp.ctrl_rto_retries = 2; + tcp_ctrl_rto_stop(ts); + ck_assert_uint_eq(ts->sock.tcp.rto, 3000U); + ck_assert_uint_eq(ts->sock.tcp.ctrl_rto_retries, 0); + + /* Without a control timeout the base RTO is left untouched. */ + ts->sock.tcp.rto = 1000; + tcp_ctrl_rto_stop(ts); + ck_assert_uint_eq(ts->sock.tcp.rto, 1000U); +} +END_TEST + START_TEST(test_tcp_persist_helpers_ignore_non_tcp_and_null_inputs) { struct wolfIP s; @@ -3472,6 +3537,8 @@ START_TEST(test_wolfip_send_port_unreachable_non_ethernet_skips_eth_filter) last_frame_sent_size = 0; memset(orig_buf, 0, sizeof(orig_buf)); + orig->ver_ihl = 0x45; + orig->len = ee16(TTL_EXCEEDED_ORIG_PACKET_SIZE_DEFAULT); orig->src = ee32(0x0A000002U); wolfIP_send_port_unreachable(&s, TEST_PRIMARY_IF, orig); @@ -3507,6 +3574,31 @@ START_TEST(test_wolfip_send_port_unreachable_sets_df) } END_TEST +START_TEST(test_wolfip_send_port_unreachable_quotes_no_more_than_datagram) +{ + struct wolfIP s; + uint8_t orig_buf[ETH_HEADER_LEN + 24]; + struct wolfIP_ip_packet *orig = (struct wolfIP_ip_packet *)orig_buf; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + last_frame_sent_size = 0; + + memset(orig_buf, 0, sizeof(orig_buf)); + orig->ver_ihl = 0x45; + orig->len = ee16(24); /* 20-byte IP header + 4-byte UDP header */ + orig->src = ee32(0x0A000002U); + orig->dst = ee32(0x0A000001U); + + wolfIP_send_port_unreachable(&s, TEST_PRIMARY_IF, orig); + /* The quoted part is the 24-byte datagram, not the usual ihl + 8: + * the frame is ETH + 20 + 8 + 24. */ + ck_assert_uint_eq(last_frame_sent_size, + (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + 8 + 24)); +} +END_TEST + START_TEST(test_tcp_adv_win_clamps_and_applies_window_scale) { struct tsocket ts; @@ -3864,6 +3956,129 @@ START_TEST(test_wolfip_forwarding_basic) } END_TEST +/* Regression: a router must relay transit IP fragments without reassembly + * (RFC 1812 5.2.6); ip_recv used to drop every fragment ahead of the + * forwarding decision, so a router build relayed none of them. */ +START_TEST(test_ip_recv_forwarding_relays_transit_fragments) +{ + struct wolfIP s; + uint8_t frame_buf[64]; + struct wolfIP_ip_packet *frame = (struct wolfIP_ip_packet *)frame_buf; + struct wolfIP_ip_packet *fwd; + uint8_t src_mac[6] = {0x52, 0x54, 0x00, 0x12, 0x34, 0x56}; + uint8_t iface1_mac[6] = {0x02, 0x00, 0x00, 0x00, 0x00, 0x02}; + uint8_t next_hop_mac[6] = {0x02, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE}; + uint32_t dest_ip = 0xC0A80164; /* 192.168.1.100 */ + uint8_t initial_ttl = 64; + + wolfIP_init(&s); + mock_link_init(&s); + mock_link_init_idx(&s, TEST_SECOND_IF, iface1_mac); + wolfIP_ipconfig_set(&s, 0xC0A80001, 0xFFFFFF00, 0); + wolfIP_ipconfig_set_ex(&s, TEST_SECOND_IF, 0xC0A80101, 0xFFFFFF00, 0); + s.arp.neighbors[0].ip = dest_ip; + s.arp.neighbors[0].if_idx = TEST_SECOND_IF; + memcpy(s.arp.neighbors[0].mac, next_hop_mac, 6); + + /* First fragment: MF set, offset zero, 8 bytes of payload. */ + memset(frame_buf, 0, sizeof(frame_buf)); + memcpy(frame->eth.dst, s.ll_dev[TEST_PRIMARY_IF].mac, 6); + memcpy(frame->eth.src, src_mac, 6); + frame->eth.type = ee16(ETH_TYPE_IP); + frame->ver_ihl = 0x45; + frame->ttl = initial_ttl; + frame->proto = WI_IPPROTO_UDP; + frame->len = ee16(IP_HEADER_LEN + 8); + frame->flags_fo = ee16(0x2000U); /* MF=1, offset=0 */ + frame->src = ee32(0xC0A800AA); + frame->dst = ee32(dest_ip); + frame->csum = 0; + iphdr_set_checksum(frame); + + memset(last_frame_sent, 0, sizeof(last_frame_sent)); + last_frame_sent_size = 0; + + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, + ETH_HEADER_LEN + IP_HEADER_LEN + 8); + + /* Relayed unchanged, TTL decremented, fragment field intact. */ + ck_assert_uint_eq(last_frame_sent_size, + (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + 8)); + fwd = (struct wolfIP_ip_packet *)last_frame_sent; + ck_assert_mem_eq(fwd->eth.dst, next_hop_mac, 6); + ck_assert_uint_eq(fwd->ttl, (uint8_t)(initial_ttl - 1)); + ck_assert_uint_eq(ee16(fwd->flags_fo), 0x2000U); + + /* Non-first fragment: MF clear, offset non-zero. Same path. */ + memset(frame_buf, 0, sizeof(frame_buf)); + memcpy(frame->eth.dst, s.ll_dev[TEST_PRIMARY_IF].mac, 6); + memcpy(frame->eth.src, src_mac, 6); + frame->eth.type = ee16(ETH_TYPE_IP); + frame->ver_ihl = 0x45; + frame->ttl = initial_ttl; + frame->proto = WI_IPPROTO_UDP; + frame->len = ee16(IP_HEADER_LEN + 8); + frame->flags_fo = ee16(0x0001U); /* MF=0, offset=1 (8 bytes) */ + frame->src = ee32(0xC0A800AA); + frame->dst = ee32(dest_ip); + frame->csum = 0; + iphdr_set_checksum(frame); + + memset(last_frame_sent, 0, sizeof(last_frame_sent)); + last_frame_sent_size = 0; + + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, + ETH_HEADER_LEN + IP_HEADER_LEN + 8); + + ck_assert_uint_eq(last_frame_sent_size, + (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + 8)); + fwd = (struct wolfIP_ip_packet *)last_frame_sent; + ck_assert_uint_eq(fwd->ttl, (uint8_t)(initial_ttl - 1)); + ck_assert_uint_eq(ee16(fwd->flags_fo), 0x0001U); +} +END_TEST + +/* Locally addressed fragments are still dropped: reassembly is not + * implemented (F-1326), and the fix must not start delivering partial + * datagrams to local sockets. */ +START_TEST(test_ip_recv_forwarding_drops_local_fragment) +{ + struct wolfIP s; + uint8_t frame_buf[64]; + struct wolfIP_ip_packet *frame = (struct wolfIP_ip_packet *)frame_buf; + uint8_t src_mac[6] = {0x52, 0x54, 0x00, 0x12, 0x34, 0x56}; + + wolfIP_init(&s); + mock_link_init(&s); + mock_link_init_idx(&s, TEST_SECOND_IF, NULL); + wolfIP_ipconfig_set(&s, 0xC0A80001, 0xFFFFFF00, 0); + wolfIP_ipconfig_set_ex(&s, TEST_SECOND_IF, 0xC0A80101, 0xFFFFFF00, 0); + + memset(frame_buf, 0, sizeof(frame_buf)); + memcpy(frame->eth.dst, s.ll_dev[TEST_PRIMARY_IF].mac, 6); + memcpy(frame->eth.src, src_mac, 6); + frame->eth.type = ee16(ETH_TYPE_IP); + frame->ver_ihl = 0x45; + frame->ttl = 64; + frame->proto = WI_IPPROTO_UDP; + frame->len = ee16(IP_HEADER_LEN + 8); + frame->flags_fo = ee16(0x2000U); /* MF=1, offset=0 */ + frame->src = ee32(0xC0A800AA); + frame->dst = ee32(0xC0A80001); /* our own interface 0 address */ + frame->csum = 0; + iphdr_set_checksum(frame); + + memset(last_frame_sent, 0, sizeof(last_frame_sent)); + last_frame_sent_size = 0; + + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, + ETH_HEADER_LEN + IP_HEADER_LEN + 8); + + /* Dropped: neither forwarded nor delivered. */ + ck_assert_uint_eq(last_frame_sent_size, 0); +} +END_TEST + START_TEST(test_wolfip_forwarding_ttl_expired) { struct wolfIP s; diff --git a/src/test/unit/unit_tests_tcp_ack.c b/src/test/unit/unit_tests_tcp_ack.c index a5794d5e..39da78ad 100644 --- a/src/test/unit/unit_tests_tcp_ack.c +++ b/src/test/unit/unit_tests_tcp_ack.c @@ -5065,6 +5065,90 @@ START_TEST(test_tcp_ack_sack_early_retransmit_before_three_dupack) } END_TEST +/* A zero-length (pure-ACK) descriptor parked between two data descriptors + * must not strand the second: an ACK covering both marks both data + * descriptors ACKED, and the cleanup must reclaim all three, not stop at + * the parked zero-length one (which would block the marking scan, which + * only walks SENT descriptors, for every later segment). */ +START_TEST(test_tcp_ack_reclaims_data_acked_behind_pure_ack) +{ + struct wolfIP s; + struct tsocket *ts; + struct tcp_seg_buf segbuf1; + struct tcp_seg_buf segbuf2; + struct tcp_seg_buf segbuf3; + struct wolfIP_tcp_seg *seg1; + struct wolfIP_tcp_seg *seg2; + struct wolfIP_tcp_seg *seg3; + uint8_t ackbuf[sizeof(struct wolfIP_tcp_seg)]; + struct wolfIP_tcp_seg *ackseg = (struct wolfIP_tcp_seg *)ackbuf; + struct pkt_desc *desc; + + wolfIP_init(&s); + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->sock.tcp.state = TCP_ESTABLISHED; + ts->sock.tcp.seq = 102; + ts->sock.tcp.snd_una = 100; + ts->sock.tcp.bytes_in_flight = 2; + ts->sock.tcp.cwnd = TCP_MSS * 4; + ts->sock.tcp.peer_rwnd = TCP_MSS * 4; + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + /* data1: 1 byte at seq 100. */ + memset(&segbuf1, 0, sizeof(segbuf1)); + seg1 = &segbuf1.seg; + seg1->ip.len = ee16(IP_HEADER_LEN + TCP_HEADER_LEN + 1); + seg1->hlen = TCP_HEADER_LEN << 2; + seg1->seq = ee32(100); + ck_assert_int_eq(fifo_push(&ts->sock.tcp.txbuf, &segbuf1, + sizeof(segbuf1)), 0); + desc = fifo_peek(&ts->sock.tcp.txbuf); + ck_assert_ptr_nonnull(desc); + desc->flags |= PKT_FLAG_SENT; + + /* pure ACK: zero length at seq 101. */ + memset(&segbuf2, 0, sizeof(segbuf2)); + seg2 = &segbuf2.seg; + seg2->ip.len = ee16(IP_HEADER_LEN + TCP_HEADER_LEN); + seg2->hlen = TCP_HEADER_LEN << 2; + seg2->seq = ee32(101); + ck_assert_int_eq(fifo_push(&ts->sock.tcp.txbuf, &segbuf2, + sizeof(segbuf2)), 0); + desc = fifo_next(&ts->sock.tcp.txbuf, desc); + ck_assert_ptr_nonnull(desc); + desc->flags |= PKT_FLAG_SENT; + + /* data2: 1 byte at seq 101. */ + memset(&segbuf3, 0, sizeof(segbuf3)); + seg3 = &segbuf3.seg; + seg3->ip.len = ee16(IP_HEADER_LEN + TCP_HEADER_LEN + 1); + seg3->hlen = TCP_HEADER_LEN << 2; + seg3->seq = ee32(101); + ck_assert_int_eq(fifo_push(&ts->sock.tcp.txbuf, &segbuf3, + sizeof(segbuf3)), 0); + desc = fifo_next(&ts->sock.tcp.txbuf, desc); + ck_assert_ptr_nonnull(desc); + desc->flags |= PKT_FLAG_SENT; + + /* One ACK covering everything: ack = 102. */ + memset(ackbuf, 0, sizeof(ackbuf)); + ackseg->ack = ee32(102); + ackseg->hlen = TCP_HEADER_LEN << 2; + ackseg->flags = TCP_FLAG_ACK; + + tcp_ack(ts, ackseg); + + /* All three descriptors reclaimed: the ACKED data2 behind the parked + * pure ACK included. */ + ck_assert_ptr_eq(fifo_peek(&ts->sock.tcp.txbuf), NULL); + ck_assert_uint_eq(ts->sock.tcp.bytes_in_flight, 0); + ck_assert_uint_eq(ts->sock.tcp.snd_una, 102); +} +END_TEST + /* F-13765: a forward ACK that fully acknowledges a segment whose * PKT_FLAG_SENT was cleared by the retransmit marker advances * snd_una, but the marking loop counts zero descriptors, so the diff --git a/src/test/unit/unit_tests_tcp_flow.c b/src/test/unit/unit_tests_tcp_flow.c index 9c5f6210..f84b3000 100644 --- a/src/test/unit/unit_tests_tcp_flow.c +++ b/src/test/unit/unit_tests_tcp_flow.c @@ -6212,3 +6212,155 @@ START_TEST(test_tcp_ctrl_rto_start_rearm_failure_clears_active) ck_assert_int_eq(ts->sock.tcp.ctrl_rto_active, 0); } END_TEST + +/* Regression: flush_tcp_tx() must not pop an unacked data descriptor when it + * retires a payload-less segment that is not at the FIFO tail. fifo_pop() only + * removes the tail; once the cursor has advanced past the tail, popping there + * discards the data descriptor and the payload can never be retransmitted. */ +START_TEST(test_flush_tcp_tx_pure_ack_keeps_unacked_data_desc) +{ + struct wolfIP s; + struct tsocket *ts; + struct pkt_desc *desc; + struct pkt_desc *data_desc; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + s.arp.neighbors[0].ip = 0x0A000002U; + s.arp.neighbors[0].if_idx = TEST_PRIMARY_IF; + memcpy(s.arp.neighbors[0].mac, + (uint8_t[]){0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}, 6); + + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->if_idx = TEST_PRIMARY_IF; + ts->sock.tcp.state = TCP_ESTABLISHED; + ts->sock.tcp.ack = 100; + ts->sock.tcp.seq = 1000; + ts->sock.tcp.snd_una = 1000; + ts->sock.tcp.rto = 200; + ts->sock.tcp.cwnd = TXBUF_SIZE; + ts->sock.tcp.peer_rwnd = TXBUF_SIZE; + ts->src_port = 1234; + ts->dst_port = 4321; + ts->local_ip = 0x0A000001U; + ts->remote_ip = 0x0A000002U; + queue_init(&ts->sock.tcp.rxbuf, ts->rxmem, RXBUF_SIZE, ts->sock.tcp.ack); + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + /* Data descriptor at the tail, pure ACK queued behind it. */ + ck_assert_int_eq(enqueue_tcp_tx(ts, 8, (TCP_FLAG_ACK | TCP_FLAG_PSH)), 0); + data_desc = fifo_peek(&ts->sock.tcp.txbuf); + ck_assert_ptr_nonnull(data_desc); + ck_assert_int_eq(enqueue_tcp_tx(ts, 0, TCP_FLAG_ACK), 0); + + /* Flush: sends the data (marks SENT, advances past the tail), then sends + * the pure ACK. Pre-fix the ACK's fifo_pop() discards data_desc. */ + (void)wolfIP_poll(&s, 200); + + /* The unacked data descriptor must survive the flush. */ + desc = fifo_peek(&ts->sock.tcp.txbuf); + ck_assert_ptr_nonnull(desc); + ck_assert_ptr_eq(desc, data_desc); + ck_assert_int_ne(desc->flags & PKT_FLAG_SENT, 0); + + /* Exactly two descriptors remain queued: data at the tail, the pure + * ACK after it, and nothing beyond (fifo_next() stops at the head). */ + desc = fifo_next(&ts->sock.tcp.txbuf, desc); + ck_assert_ptr_nonnull(desc); + ck_assert_ptr_ne(desc, data_desc); + ck_assert_int_ne(desc->flags & PKT_FLAG_SENT, 0); + desc = fifo_next(&ts->sock.tcp.txbuf, desc); + ck_assert_ptr_null(desc); +} +END_TEST + +/* Regression: the tcp_ack() zero-length drain pops the oldest descriptor, + * so a zero-length descriptor parked behind a just-acked data descriptor + * must not be popped there (that would discard the data descriptor and lose + * the RTT sample for the ACK). The parked descriptor stays until it becomes + * the oldest. */ +START_TEST(test_tcp_ack_parked_zero_desc_keeps_rtt_sample) +{ + struct wolfIP s; + struct tsocket *ts; + struct wolfIP_tcp_seg ackseg; + struct pkt_desc *desc; + struct pkt_desc *data_desc; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + s.arp.neighbors[0].ip = 0x0A000002U; + s.arp.neighbors[0].if_idx = TEST_PRIMARY_IF; + memcpy(s.arp.neighbors[0].mac, + (uint8_t[]){0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}, 6); + + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->if_idx = TEST_PRIMARY_IF; + ts->sock.tcp.state = TCP_ESTABLISHED; + ts->sock.tcp.ack = 100; + ts->sock.tcp.seq = 1000; + ts->sock.tcp.snd_una = 1000; + ts->sock.tcp.rto = 200; + ts->sock.tcp.cwnd = TXBUF_SIZE; + ts->sock.tcp.peer_rwnd = TXBUF_SIZE; + ts->src_port = 1234; + ts->dst_port = 4321; + ts->local_ip = 0x0A000001U; + ts->remote_ip = 0x0A000002U; + queue_init(&ts->sock.tcp.rxbuf, ts->rxmem, RXBUF_SIZE, ts->sock.tcp.ack); + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + /* Data at the tail, pure ACK parked behind it; flush sends both. */ + ck_assert_int_eq(enqueue_tcp_tx(ts, 8, (TCP_FLAG_ACK | TCP_FLAG_PSH)), 0); + data_desc = fifo_peek(&ts->sock.tcp.txbuf); + ck_assert_ptr_nonnull(data_desc); + ck_assert_int_eq(enqueue_tcp_tx(ts, 0, TCP_FLAG_ACK), 0); + /* The real send path advances seq as it enqueues; the test helper does + * not, so mirror it here. */ + ts->sock.tcp.seq = 1008; + (void)wolfIP_poll(&s, 200); + + desc = fifo_peek(&ts->sock.tcp.txbuf); + ck_assert_ptr_eq(desc, data_desc); + ck_assert_int_ne(desc->flags & PKT_FLAG_SENT, 0); + desc = fifo_next(&ts->sock.tcp.txbuf, desc); + ck_assert_ptr_nonnull(desc); + ck_assert_int_ne(desc->flags & PKT_FLAG_SENT, 0); + + /* Distinct send timestamps: the RTT sample must come from the data + * descriptor (100 - 90 = 10), not the parked pure ACK behind it + * (which would give 100 - 95 = 5). */ + data_desc->time_sent = 90; + desc->time_sent = 95; + s.last_tick = 100; + + /* ACK the data. Pre-fix the drain's fifo_pop() discards the just-acked + * data descriptor, fresh_desc ends up NULL and no RTT sample is taken; + * post-fix the sample lands and the parked zero-length descriptor is + * reclaimed with it. */ + memset(&ackseg, 0, sizeof(ackseg)); + ackseg.ip.len = ee16(IP_HEADER_LEN + TCP_HEADER_LEN); + ackseg.hlen = TCP_HEADER_LEN << 2; + ackseg.flags = TCP_FLAG_ACK; + ackseg.ack = ee32(1008); + tcp_ack(ts, &ackseg); + + ck_assert_uint_eq(ts->sock.tcp.snd_una, 1008); + ck_assert_uint_eq(ts->sock.tcp.bytes_in_flight, 0); + ck_assert_int_eq(ts->sock.tcp.rto_initialized, 1); + ck_assert_uint_eq(ts->sock.tcp.rtt, 10); + ck_assert_uint_ne(ts->sock.tcp.rto, 200); + /* The parked zero-length descriptor is reclaimed along with the ACKed + * data: it carries no in-flight bytes, and keeping it around would + * block the marking scan if ACKed data ever sat behind it. */ + ck_assert_ptr_eq(fifo_peek(&ts->sock.tcp.txbuf), NULL); +} +END_TEST diff --git a/src/wolfip.c b/src/wolfip.c index 7093c0ff..03692fa7 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -169,6 +169,8 @@ struct wolfIP_icmp_packet; #define TCP_RTO_G_MS 1U /* RFC 6298 §5.5: maximum timer value G; caps the backed-off RTO. */ #define TCP_RTO_BACKOFF_MAX_MS 64000U +/* RFC 6298 §5.7: base RTO to reinitialize after a SYN timeout. */ +#define TCP_RTO_SYN_INIT_MS 3000U #define TCP_PERSIST_MIN_MS 1000U #define TCP_PERSIST_MAX_MS 60000U #ifndef TCP_FIN_WAIT_2_TIMEOUT_MS @@ -2675,6 +2677,7 @@ static void wolfIP_send_port_unreachable(struct wolfIP *s, unsigned int if_idx, struct wolfIP_icmp_dest_unreachable_packet icmp = {0}; struct wolfIP_icmp_packet *icmp_pkt = (struct wolfIP_icmp_packet *)&icmp; uint32_t orig_ihl = (orig->ver_ihl & 0x0F) * 4; + uint32_t orig_total; uint32_t orig_copy; uint32_t icmp_data_len; #if !CONFIG_IPFILTER @@ -2697,7 +2700,15 @@ static void wolfIP_send_port_unreachable(struct wolfIP *s, unsigned int if_idx, #endif if (orig_ihl < IP_HEADER_LEN) orig_ihl = IP_HEADER_LEN; + /* Quote the original header plus up to 8 payload bytes, or as much of + * the datagram as exists (same clamp as the other ICMP error senders). + */ + 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; @@ -2998,6 +3009,9 @@ static void udp_try_recv(struct wolfIP *s, unsigned int if_idx, { int i; int matched = 0; + int dst_local = 0; + int dst_local_ok; + int dhcp_exchange; ip4 dst_ip; ip4 src_ip; @@ -3041,6 +3055,26 @@ static void udp_try_recv(struct wolfIP *s, unsigned int if_idx, dst_ip = ee32(udp->ip.dst); src_ip = ee32(udp->ip.src); + /* A host consumes only datagrams addressed to itself: one of its own + * interface addresses, a broadcast, a multicast, or the pre-address + * DHCP exchange (RFC 2131: OFFER/ACK may carry a unicast ip.dst the + * client does not own yet). RFC 1122 requires silently dropping + * everything else. Without this gate a wildcard (INADDR_ANY) bind + * delivers third-party traffic in non-forwarding builds, where + * ip_recv() compiles out its is_local check. */ + (void)wolfIP_if_for_local_ip(s, dst_ip, &dst_local); + dst_local_ok = dst_local || dst_ip == IPADDR_ANY || + wolfIP_ip_is_broadcast(s, dst_ip) || + wolfIP_ip_is_multicast(dst_ip); + /* The DHCP exception is only valid while the client is actively + * exchanging (DHCP_IS_RUNNING excludes OFF and BOUND); with DHCP off a + * 67->68 datagram to a third-party IP is dropped like any other. */ + dhcp_exchange = !dst_local_ok && + ee16(udp->src_port) == DHCP_SERVER_PORT && + ee16(udp->dst_port) == DHCP_CLIENT_PORT; + if (!dst_local_ok && !(dhcp_exchange && DHCP_IS_RUNNING(s))) + return; + if (wolfIP_filter_notify_udp(WOLFIP_FILT_RECEIVING, s, if_idx, udp, frame_len, IP_HEADER_LEN) != 0) return; @@ -3066,16 +3100,25 @@ static void udp_try_recv(struct wolfIP *s, unsigned int if_idx, * selected into local_ip at bind time: a wildcard (INADDR_ANY) * bind must receive datagrams addressed to any local address * (POSIX), the same rule the TCP LISTEN match applies via - * bound_local_ip. local_ip/if_idx stay egress-only. The - * t->local_ip != 0 guard keeps an unbound socket (local_ip == 0) - * out of the match: only the DHCP relaxation above may deliver - * to one. */ - int bound_match = (t->local_ip != 0) && + * bound_local_ip. local_ip/if_idx stay egress-only. Liveness is + * src_port != 0 (a bound slot), not local_ip != 0: a socket bound + * before any interface had an address snapshots local_ip == 0 and + * must still receive once the address arrives. */ + int bound_match = (t->src_port != 0) && ((t->bound_local_ip == IPADDR_ANY) || (t->bound_local_ip == dst_ip)); - int addr_match = - (((t->local_ip == 0) && DHCP_IS_RUNNING(s) && is_dhcp) || - (bound_match && peer_match)); + int addr_match; + if (dhcp_exchange) { + /* A third-party-addressed 67->68 datagram may only reach the + * DHCP client's own socket; a wildcard bind on port 68 must + * not receive it. (The gate above guarantees DHCP is running + * when dhcp_exchange is set.) */ + addr_match = is_dhcp; + } else { + addr_match = + (((t->local_ip == 0) && DHCP_IS_RUNNING(s) && is_dhcp) || + (bound_match && peer_match)); + } #ifdef IP_MULTICAST if (wolfIP_ip_is_multicast(dst_ip)) { addr_match = udp_socket_has_mcast(t, if_idx, dst_ip) && @@ -4265,6 +4308,14 @@ static void tcp_ctrl_rto_stop(struct tsocket *t) timer_binheap_cancel(&t->S->timers, t->sock.tcp.tmr_rto); t->sock.tcp.tmr_rto = NO_TIMER; } + /* RFC 6298 §5.7: a control timeout (e.g. SYN retransmit) while the + * base RTO was below 3 s means the first RTT estimate is stale; reset + * the base to 3 s now that the control sequence is done and data may + * flow. RTT sampling re-derives the RTO from there. */ + if (t->sock.tcp.ctrl_rto_retries > 0 && + t->sock.tcp.rto < TCP_RTO_SYN_INIT_MS) { + t->sock.tcp.rto = TCP_RTO_SYN_INIT_MS; + } t->sock.tcp.ctrl_rto_active = 0; t->sock.tcp.ctrl_rto_retries = 0; } @@ -4539,14 +4590,25 @@ static void tcp_persist_start(struct tsocket *t, uint64_t now) return; } if (t->sock.tcp.tmr_persist != NO_TIMER) { - timer_binheap_cancel(&t->S->timers, t->sock.tcp.tmr_persist); - t->sock.tcp.tmr_persist = NO_TIMER; + /* Already armed: keep the existing deadline. flush_tcp_tx() calls + * this on every poll while the peer window is zero; re-arming here + * would push the deadline past every poll cadence shorter than + * TCP_PERSIST_MIN_MS so the probe would never fire. Only + * tcp_persist_cb() re-arms, after a probe has been sent. */ + return; } interval = tcp_persist_interval_ms(t); tmr.expires = now + interval; tmr.arg = t; tmr.cb = tcp_persist_cb; t->sock.tcp.tmr_persist = timers_binheap_insert(&t->S->timers, tmr); + /* Only mark persist active when the timer actually took a slot: + * an active flag with no timer behind it would never fire and would + * stall the sender on a zero-window peer until a later event cleared + * it (same guard as the control RTO arm). */ + if (t->sock.tcp.tmr_persist == NO_TIMER) { + return; + } t->sock.tcp.persist_active = 1; } @@ -4679,6 +4741,9 @@ static void tcp_persist_cb(void *arg) (void)tcp_send_zero_wnd_probe(t); if (t->sock.tcp.persist_backoff < 10) t->sock.tcp.persist_backoff++; + /* The timer that fired is out of the heap; drop the stale handle so + * tcp_persist_start() re-arms instead of seeing it as armed. */ + t->sock.tcp.tmr_persist = NO_TIMER; tcp_persist_start(t, t->S->last_tick); } @@ -5148,6 +5213,7 @@ static void wolfIP_forward_packet(struct wolfIP *s, unsigned int out_if, * so the filter notify below must read the transport header at the * actual IHL, not a fixed 20-byte offset. */ uint32_t ip_hlen = (uint32_t)(ip->ver_ihl & 0x0fU) << 2; + int nonfirst_frag = (ee16(ip->flags_fo) & 0x1FFFU) != 0U; if (ip_hlen < IP_HEADER_LEN) ip_hlen = IP_HEADER_LEN; @@ -5157,20 +5223,27 @@ static void wolfIP_forward_packet(struct wolfIP *s, unsigned int out_if, else eth_output_add_header(s, out_if, mac, &ip->eth, ETH_TYPE_IP); } - if (ip->proto == WI_IPPROTO_TCP) - drop = wolfIP_filter_notify_tcp(WOLFIP_FILT_SENDING, s, out_if, - (struct wolfIP_tcp_seg *)ip, len, - ip_hlen); - else if (ip->proto == WI_IPPROTO_UDP) - drop = wolfIP_filter_notify_udp(WOLFIP_FILT_SENDING, s, out_if, - (struct wolfIP_udp_datagram *)ip, len, - ip_hlen); - else if (ip->proto == WI_IPPROTO_ICMP) - drop = wolfIP_filter_notify_icmp(WOLFIP_FILT_SENDING, s, out_if, - (struct wolfIP_icmp_packet *)ip, len, - ip_hlen); - if (drop != 0) - return; + /* A non-first fragment carries no L4 header: the bytes at the ip_hlen + * offset are payload, so the L4 filter hooks would match on garbage + * (RFC 1858 policy evasion). Only the IP-level policy applies to such + * fragments; the first fragment (offset 0) still carries a valid L4 + * header and is notified as usual. */ + if (!nonfirst_frag) { + if (ip->proto == WI_IPPROTO_TCP) + drop = wolfIP_filter_notify_tcp(WOLFIP_FILT_SENDING, s, out_if, + (struct wolfIP_tcp_seg *)ip, len, + ip_hlen); + else if (ip->proto == WI_IPPROTO_UDP) + drop = wolfIP_filter_notify_udp(WOLFIP_FILT_SENDING, s, out_if, + (struct wolfIP_udp_datagram *)ip, + len, ip_hlen); + else if (ip->proto == WI_IPPROTO_ICMP) + drop = wolfIP_filter_notify_icmp(WOLFIP_FILT_SENDING, s, out_if, + (struct wolfIP_icmp_packet *)ip, + len, ip_hlen); + if (drop != 0) + return; + } if (wolfIP_filter_notify_ip(WOLFIP_FILT_SENDING, s, out_if, ip, len) != 0) return; if (!wolfIP_ll_is_non_ethernet(s, out_if)) { @@ -5631,10 +5704,19 @@ static void tcp_ack(struct tsocket *t, const struct wolfIP_tcp_seg *tcp) struct wolfIP_tcp_seg *seg = (struct wolfIP_tcp_seg *)(t->txmem + desc->pos + sizeof(*desc)); uint32_t seg_len = ee16(seg->ip.len) - (IP_HEADER_LEN + (seg->hlen >> 2)); if (seg_len == 0) { - /* Advance the tail and discard */ - desc = fifo_pop(&t->sock.tcp.txbuf); - (void)desc; - desc = fifo_peek(&t->sock.tcp.txbuf); + if (desc == fifo_peek(&t->sock.tcp.txbuf)) { + /* fifo_pop() removes the oldest descriptor, which is the + * cursor: discard it and resume from the new head. */ + desc = fifo_pop(&t->sock.tcp.txbuf); + (void)desc; + desc = fifo_peek(&t->sock.tcp.txbuf); + } else { + /* A zero-length descriptor parked ahead of a newer one: + * leave it in place (popping would remove the newest + * descriptor, not this one) and advance the cursor. The + * cleanup drain below reclaims it. */ + desc = fifo_next(&t->sock.tcp.txbuf, desc); + } continue; } if (tcp_seq_leq(ee32(seg->seq) + seg_len, ack)) { @@ -5714,16 +5796,36 @@ static void tcp_ack(struct tsocket *t, const struct wolfIP_tcp_seg *tcp) } ack_advanced = 1; } - if (ack_count > 0) { + { struct pkt_desc *fresh_desc = NULL; uint32_t ack_ip_len = ee16(tcp->ip.len); uint32_t ack_hdr_len = IP_HEADER_LEN + tcp_data_offset_bytes(tcp->hlen); uint32_t ack_frame_len = 0; - /* This ACK ackwnowledged some data. */ + /* Reclaim descriptors the peer has already accounted for: ACKED + * data and zero-length (pure-ACK) descriptors, which carry no + * in-flight bytes. Runs on every ACK, not only when this segment + * marked new descriptors: the marking scan above only walks SENT + * descriptors, so an ACKED descriptor parked behind a zero-length + * one would otherwise sit at the FIFO head forever, blocking the + * scan (and retransmission) for every later segment. */ desc = fifo_peek(&t->sock.tcp.txbuf); - while (desc && (desc->flags & PKT_FLAG_ACKED)) { - fresh_desc = fifo_pop(&t->sock.tcp.txbuf); - desc = fifo_peek(&t->sock.tcp.txbuf); + while (desc) { + struct wolfIP_tcp_seg *seg = + (struct wolfIP_tcp_seg *)(t->txmem + desc->pos + sizeof(*desc)); + uint32_t seg_len = + ee16(seg->ip.len) - (IP_HEADER_LEN + (seg->hlen >> 2)); + if ((desc->flags & PKT_FLAG_ACKED) || + ((desc->flags & PKT_FLAG_SENT) && (seg_len == 0))) { + struct pkt_desc *popped = fifo_pop(&t->sock.tcp.txbuf); + /* RTT sample source: only an ACKED (data) descriptor. + * A zero-length descriptor popped after it carries a + * later time_sent and would underestimate the RTT. */ + if (popped->flags & PKT_FLAG_ACKED) + fresh_desc = popped; + desc = fifo_peek(&t->sock.tcp.txbuf); + } else { + break; + } } if (fresh_desc) { /* Karn rule: ignore RTT samples for retransmitted segments. */ @@ -5765,7 +5867,8 @@ static void tcp_ack(struct tsocket *t, const struct wolfIP_tcp_seg *tcp) if (tx_has_writable_space(t)) t->events |= CB_EVENT_WRITABLE; } - } else { + } + if (ack_count == 0) { /* Duplicate ack (no advance in snd_una). RFC 5681: only a segment * that carries no data and repeats the previously advertised * receive window counts as a duplicate ACK, so data-bearing @@ -11006,9 +11109,6 @@ static inline void ip_recv(struct wolfIP *s, unsigned int if_idx, /* validate IP header checksum per RFC 1122 */ if (iphdr_verify_checksum(ip) != 0) return; - /* Fragment reassembly is not implemented; drop all fragments. */ - if ((ee16(ip->flags_fo) & 0x3FFFU) != 0U) - return; /* RFC 1122 §3.2.1.3: discard packets with non-unicast source addresses. */ { ip4 src = ee32(ip->src); @@ -11206,19 +11306,24 @@ static inline void ip_recv(struct wolfIP *s, unsigned int if_idx, if (out_if >= 0) { uint8_t mac[6]; int broadcast = 0; + /* RFC 1812 4.3.2.7: no ICMP error may be generated for a + * non-first fragment (the router cannot validate what the + * fragment does not carry); such drops are silent. */ + int nonfirst_frag = (ee16(ip->flags_fo) & 0x1FFFU) != 0U; if (bad_opt_off != 0) { /* RFC 1122 3.2.2.4: a transit datagram with a malformed * IP option gets a Parameter Problem pointing at the * offending option byte, not a silent drop. Multicast * destinations are exempt (RFC 1812 4.3.2.4). */ - if (!wolfIP_ip_is_multicast(dest)) + if (!wolfIP_ip_is_multicast(dest) && !nonfirst_frag) wolfIP_send_param_problem(s, if_idx, ip, (uint8_t)bad_opt_off); return; } if (ip->ttl <= 1) { - wolfIP_send_ttl_exceeded(s, if_idx, ip); + if (!nonfirst_frag) + wolfIP_send_ttl_exceeded(s, if_idx, ip); return; } /* A datagram larger than the egress IP MTU cannot be relayed. @@ -11229,7 +11334,8 @@ static inline void ip_recv(struct wolfIP *s, unsigned int if_idx, if (ee16(ip->len) > (wolfIP_frame_mtu(s, (unsigned int)out_if) - ETH_HEADER_LEN) && (ee16(ip->flags_fo) & 0x4000U) != 0U) { - wolfIP_send_frag_needed(s, if_idx, (unsigned int)out_if, ip); + if (!nonfirst_frag) + wolfIP_send_frag_needed(s, if_idx, (unsigned int)out_if, ip); return; } if (!wolfIP_forward_prepare(s, out_if, next_hop, mac, @@ -11286,6 +11392,13 @@ static inline void ip_recv(struct wolfIP *s, unsigned int if_idx, } } #endif /* WOLFIP_ENABLE_FORWARDING */ + /* Fragment reassembly is not implemented: only a locally addressed + * fragment can reach this point, since the forwarding path above relays + * transit fragments without reassembly (RFC 1812 5.2.6). Drop it before + * L4 dispatch; raw sockets and the IP-level filter observe it as an IP + * datagram, which is the correct granularity for them. */ + if ((ee16(ip->flags_fo) & 0x3FFFU) != 0U) + return; if (bad_opt_off != 0) return; /* malformed IP options: never deliver locally */ #ifdef DEBUG_IP @@ -11796,7 +11909,6 @@ static int dns_schedule_timer(struct wolfIP *s) { struct wolfIP_timer tmr = { }; uint64_t interval = DNS_QUERY_TIMEOUT; - uint8_t shift; if (!s) return -1; @@ -11807,11 +11919,9 @@ static int dns_schedule_timer(struct wolfIP *s) interval = DNS_QUERY_TIMEOUT_INITIAL + (wolfIP_getrandom() % DNS_QUERY_TIMEOUT_INITIAL_JITTER); } else { - shift = s->dns_retry_count; - if (shift >= 64U || interval > (UINT64_MAX >> shift)) - interval = UINT64_MAX - s->last_tick; - else - interval <<= shift; + /* dns_retry_count is capped at DNS_QUERY_RETRIES by its single + * increment site, so the shift cannot overflow. */ + interval <<= s->dns_retry_count; } tmr.expires = s->last_tick + interval; tmr.arg = s; @@ -12400,7 +12510,22 @@ static void flush_tcp_tx(struct wolfIP *s, uint64_t now) desc->flags |= PKT_FLAG_WAS_RETRANS; desc->time_sent = now; if (size == IP_HEADER_LEN + (uint32_t)(tcp->hlen >> 2)) { - desc = fifo_pop(&ts->sock.tcp.txbuf); + if (desc == fifo_peek(&ts->sock.tcp.txbuf)) { + /* Cursor at the tail: fifo_pop() removes exactly + * this descriptor. */ + desc = fifo_pop(&ts->sock.tcp.txbuf); + } else { + /* fifo_pop() only removes the tail, so popping + * here would discard the unacked data descriptor + * at the tail. Leave the payload-less descriptor + * in place with PKT_FLAG_SENT set; tcp_ack() + * reclaims zero-length sent descriptors from the + * tail once the data ahead of them is acked. */ + next_desc = fifo_next(&ts->sock.tcp.txbuf, desc); + if (next_desc == desc) + break; + desc = next_desc; + } } else { uint32_t payload_len = size - (IP_HEADER_LEN + (tcp->hlen >> 2)); if (ts->sock.tcp.tmr_rto != NO_TIMER) {