diff --git a/.github/workflows/stm32h563-m33mu-freertos.yml b/.github/workflows/stm32h563-m33mu-freertos.yml index d99c84b7..5f0f2277 100644 --- a/.github/workflows/stm32h563-m33mu-freertos.yml +++ b/.github/workflows/stm32h563-m33mu-freertos.yml @@ -17,3 +17,14 @@ jobs: - name: Run m33mu + DHCP + TCP echo FreeRTOS test run: /bin/bash tools/scripts/run-m33mu-ci-in-container.sh stm32h563-m33mu-freertos stm32h563_m33mu_echo_freertos + + - name: Upload tap0 capture + board logs (DEBUG) + if: always() + uses: actions/upload-artifact@v4 + with: + name: m33mu-echo-debug + path: | + /tmp/echo.pcap + /tmp/m33mu.log + /tmp/echo.log + if-no-files-found: warn diff --git a/src/test/test_wolfguard_loopback.c b/src/test/test_wolfguard_loopback.c index dd052de5..ba9eb031 100644 --- a/src/test/test_wolfguard_loopback.c +++ b/src/test/test_wolfguard_loopback.c @@ -540,7 +540,7 @@ START_TEST(test_dos_cookie_mechanism) memset(&init_msg, 0xAA, sizeof(init_msg)); mac_off = offsetof(struct wg_msg_initiation, macs); - ret = wg_cookie_add_macs(&peer, &init_msg, sizeof(init_msg), mac_off); + ret = wg_cookie_add_macs(&peer, &init_msg, sizeof(init_msg), mac_off, dev.now); ck_assert_int_eq(ret, 0); /* Verify mac2 is zero (no cookie available) */ @@ -560,13 +560,13 @@ START_TEST(test_dos_cookie_mechanism) ck_assert_int_eq(ret, 0); /* Step 4: Peer consumes cookie reply */ - ret = wg_cookie_consume_reply(&peer, &cookie_reply); + ret = wg_cookie_consume_reply(&peer, &cookie_reply, dev.now); ck_assert_int_eq(ret, 0); ck_assert_int_eq(peer.cookie.is_valid, 1); /* Step 5: Re-create initiation with mac1 + mac2 (using cookie) */ memset(&init_msg, 0xBB, sizeof(init_msg)); - ret = wg_cookie_add_macs(&peer, &init_msg, sizeof(init_msg), mac_off); + ret = wg_cookie_add_macs(&peer, &init_msg, sizeof(init_msg), mac_off, dev.now); ck_assert_int_eq(ret, 0); /* Verify mac2 is NOT zero anymore */ diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 3abcf1ce..1b6a294b 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -279,6 +279,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_multicast_udp_receive_requires_join); tcase_add_test(tc_utils, test_multicast_udp_send_mac_ttl_loop_and_options); tcase_add_test(tc_utils, test_multicast_igmp_query_refreshes_report); + tcase_add_test(tc_utils, test_multicast_igmp_query_flood_coalesced); tcase_add_test(tc_utils, test_multicast_igmp_query_bad_checksum_dropped); tcase_add_test(tc_utils, test_multicast_igmp_query_spoofed_dropped); tcase_add_test(tc_utils, test_multicast_join_requires_configured_ip); @@ -410,6 +411,8 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_poll_icmp_send_on_arp_hit); tcase_add_test(tc_utils, test_poll_icmp_send_on_arp_miss_requests_arp_and_retains_queue); tcase_add_test(tc_utils, test_dhcp_timer_cb_paths); + tcase_add_test(tc_utils, test_dhcp_discover_retransmit_backoff); + tcase_add_test(tc_utils, test_dhcp_request_retransmit_backoff); tcase_add_test(tc_utils, test_regression_dhcp_lease_expiry_deconfigures_address); tcase_add_test(tc_utils, test_dhcp_request_retry_exhaustion_deconfigures_lease); tcase_add_test(tc_utils, test_dhcp_timer_cb_send_failure_does_not_consume_retry_budget); @@ -849,6 +852,7 @@ Suite *wolf_suite(void) 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_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); tcase_add_test(tc_proto, test_forward_packet_eth_filter_drop); tcase_add_test(tc_proto, test_loopback_dest_not_forwarded); @@ -1190,7 +1194,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_core, test_notify_loopback_null_stack_no_crash); /* === Branch-coverage tests from fleet ===*/ - /* --- unit_tests_tcp_state.c (62 tests) --- */ + /* --- unit_tests_tcp_state.c (65 tests) --- */ tcase_add_test(tc_core, test_tcp_send_reset_reply_ignores_rst_input); tcase_add_test(tc_core, test_tcp_send_reset_reply_ack_in_uses_ack_seq); tcase_add_test(tc_core, test_tcp_send_reset_reply_syn_no_ack_sets_rst_ack); @@ -1201,10 +1205,12 @@ Suite *wolf_suite(void) tcase_add_test(tc_core, test_tcp_parse_options_timestamp_parsed); tcase_add_test(tc_core, test_tcp_parse_options_timestamp_overlong_ignored); tcase_add_test(tc_core, test_tcp_parse_options_mss_zero_ignored); + tcase_add_test(tc_core, test_tcp_parse_options_mss_below_floor_clamped); tcase_add_test(tc_core, test_tcp_parse_options_sack_permitted_parsed); tcase_add_test(tc_core, test_tcp_input_syn_rcvd_rst_bad_seq_ignored); tcase_add_test(tc_core, test_tcp_input_syn_rcvd_rst_good_seq_reverts_to_listen); tcase_add_test(tc_core, test_tcp_input_syn_rcvd_rst_good_seq_nonlistener_closes); + tcase_add_test(tc_core, test_tcp_input_syn_rcvd_rst_nullcb_recv_reports_eof); tcase_add_test(tc_core, test_tcp_input_time_wait_sends_ack_on_any_segment); tcase_add_test(tc_core, test_tcp_input_last_ack_unacceptable_sends_ack); tcase_add_test(tc_core, test_tcp_input_last_ack_syn_sends_challenge_ack); @@ -1258,6 +1264,10 @@ Suite *wolf_suite(void) tcase_add_test(tc_core, test_tcp_input_fin_wait_1_fin_enters_closing); tcase_add_test(tc_core, test_tcp_input_fin_wait_2_fin_enters_time_wait); tcase_add_test(tc_core, test_tcp_input_rst_in_window_not_exact_sends_ack); + tcase_add_test(tc_core, test_sock_close_established_disarms_callback); + tcase_add_test(tc_core, test_sock_close_close_wait_disarms_callback); + tcase_add_test(tc_core, test_rst_in_fin_wait_1_delivers_close_event); + tcase_add_test(tc_core, test_last_ack_final_ack_delivers_close_event); /* --- unit_tests_poll_dispatcher.c (47 tests) --- */ tcase_add_test(tc_core, test_poll_device_poll_returns_zero_exits_loop); tcase_add_test(tc_core, test_poll_device_poll_returns_negative_exits_loop); diff --git a/src/test/unit/unit_shared.c b/src/test/unit/unit_shared.c index 690d8742..615e875e 100644 --- a/src/test/unit/unit_shared.c +++ b/src/test/unit/unit_shared.c @@ -79,12 +79,14 @@ static uint32_t memsz = 8 * 1024; static const uint8_t ifmac[] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55}; static uint8_t last_frame_sent[LINK_MTU]; static uint32_t last_frame_sent_size = 0; +static uint32_t last_frame_sent_count = 0; static int mock_send(struct wolfIP_ll_dev *dev, void *frame, uint32_t len) { (void)dev; memcpy(last_frame_sent, frame, len); last_frame_sent_size = len; + last_frame_sent_count++; return 0; } diff --git a/src/test/unit/unit_tests_dns_dhcp.c b/src/test/unit/unit_tests_dns_dhcp.c index 75abea98..59462449 100644 --- a/src/test/unit/unit_tests_dns_dhcp.c +++ b/src/test/unit/unit_tests_dns_dhcp.c @@ -4310,6 +4310,91 @@ START_TEST(test_dhcp_timer_cb_paths) } END_TEST +/* RFC 2131 §4.1: DHCPDISCOVER retransmissions must back off exponentially + * (doubling each attempt) instead of repeating on a fixed ~2s cadence. + * dhcp_timer_cb() reschedules using the current dhcp_timeout_count, so drive it + * at successive counts and capture the scheduled delay; with the fixed-interval + * scheme all three are equal, which the growth assertions below reject. */ +START_TEST(test_dhcp_discover_retransmit_backoff) +{ + struct wolfIP s; + uint64_t delay0, delay1, delay2; + const uint64_t now = 100000U; + + wolfIP_init(&s); + mock_link_init(&s); + s.dhcp_udp_sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + ck_assert_int_gt(s.dhcp_udp_sd, 0); + s.dhcp_xid = 1; + + s.dhcp_state = DHCP_DISCOVER_SENT; + s.last_tick = now; + s.dhcp_timeout_count = 0; + dhcp_timer_cb(&s); + ck_assert_uint_eq(s.dhcp_timeout_count, 1U); /* retransmit queued */ + delay0 = find_timer_expiry(&s, s.dhcp_timer) - now; + + s.dhcp_state = DHCP_DISCOVER_SENT; + s.last_tick = now; + s.dhcp_timeout_count = 1; + dhcp_timer_cb(&s); + delay1 = find_timer_expiry(&s, s.dhcp_timer) - now; + + s.dhcp_state = DHCP_DISCOVER_SENT; + s.last_tick = now; + s.dhcp_timeout_count = 2; + dhcp_timer_cb(&s); + delay2 = find_timer_expiry(&s, s.dhcp_timer) - now; + + /* Fixed-cadence (current) scheme makes all three equal. */ + ck_assert_uint_gt(delay1, delay0); + ck_assert_uint_gt(delay2, delay1); + /* The test RNG is deterministic, so the jitter is identical on every call + * and cancels in the deltas: exponential backoff means each step is twice + * the previous one. */ + ck_assert_uint_eq(delay2 - delay1, 2U * (delay1 - delay0)); +} +END_TEST + +/* Same exponential-backoff requirement for DHCPREQUEST retransmissions, which + * are scheduled through dhcp_schedule_retry_timer() rather than inline. */ +START_TEST(test_dhcp_request_retransmit_backoff) +{ + struct wolfIP s; + uint64_t delay0, delay1, delay2; + const uint64_t now = 100000U; + + wolfIP_init(&s); + mock_link_init(&s); + s.dhcp_udp_sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + ck_assert_int_gt(s.dhcp_udp_sd, 0); + s.dhcp_xid = 1; + + s.dhcp_state = DHCP_REQUEST_SENT; + s.last_tick = now; + s.dhcp_timeout_count = 0; + dhcp_timer_cb(&s); + ck_assert_uint_eq(s.dhcp_timeout_count, 1U); /* retransmit queued */ + delay0 = find_timer_expiry(&s, s.dhcp_timer) - now; + + s.dhcp_state = DHCP_REQUEST_SENT; + s.last_tick = now; + s.dhcp_timeout_count = 1; + dhcp_timer_cb(&s); + delay1 = find_timer_expiry(&s, s.dhcp_timer) - now; + + s.dhcp_state = DHCP_REQUEST_SENT; + s.last_tick = now; + s.dhcp_timeout_count = 2; + dhcp_timer_cb(&s); + delay2 = find_timer_expiry(&s, s.dhcp_timer) - now; + + ck_assert_uint_gt(delay1, delay0); + ck_assert_uint_gt(delay2, delay1); + ck_assert_uint_eq(delay2 - delay1, 2U * (delay1 - delay0)); +} +END_TEST + START_TEST(test_regression_dhcp_lease_expiry_deconfigures_address) { struct wolfIP s; diff --git a/src/test/unit/unit_tests_multicast.c b/src/test/unit/unit_tests_multicast.c index 72886c3c..d06beed5 100644 --- a/src/test/unit/unit_tests_multicast.c +++ b/src/test/unit/unit_tests_multicast.c @@ -266,8 +266,74 @@ START_TEST(test_multicast_igmp_query_refreshes_report) put_be16(igmp + 2, ip_checksum_buf(igmp, IGMPV3_QUERY_MIN_LEN)); fix_ip_checksum(ip); + /* RFC 3376 §5.2: the report is deferred to a timer, not emitted from the + * receive path. Max Resp Code 0 floors the delay to 1 ms, so it fires on + * the next poll. */ last_frame_sent_size = 0; wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, sizeof(frame)); + ck_assert_uint_eq(last_frame_sent_size, 0); + wolfIP_poll(&s, 2); + ck_assert_uint_gt(last_frame_sent_size, 0); + ck_assert_uint_eq(last_igmp_payload()[8], IGMPV3_REC_MODE_IS_EXCLUDE); +} +END_TEST + +/* A Membership Query must not draw an immediate, undelayed report. RFC 3376 + * §5.2 requires a host to defer its Current-State Report by a delay chosen + * uniformly in (0, Max Resp Time], and §5.2 rule 1 says a query that arrives + * while a response is already pending must not schedule another. Together this + * coalesces a query flood into a single deferred report: an on-link attacker + * spraying General Queries (TTL 1 -> 224.0.0.1) can otherwise force one report + * per query, draining a constrained host. Max Resp Code 100 -> 10 s window. */ +START_TEST(test_multicast_igmp_query_flood_coalesced) +{ + struct wolfIP s; + int sd; + struct wolfIP_ip_mreq mreq; + uint8_t frame[ETH_HEADER_LEN + IP_HEADER_LEN + IGMPV3_QUERY_MIN_LEN]; + struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)frame; + uint8_t *igmp = frame + ETH_HEADER_LEN + IP_HEADER_LEN; + ip4 group = 0xE9010207U; + unsigned int q; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000002U, 0xFFFFFF00U, 0); + sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + ck_assert_int_gt(sd, 0); + multicast_mreq(&mreq, group, IPADDR_ANY); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)), 0); + + /* General query, Max Resp Code 100 (= 10 s). */ + memset(frame, 0, sizeof(frame)); + memcpy(ip->eth.dst, "\x01\x00\x5e\x00\x00\x01", 6); + memcpy(ip->eth.src, "\x02\x00\x00\x00\x00\x01", 6); + ip->eth.type = ee16(ETH_TYPE_IP); + ip->ver_ihl = 0x45; + ip->ttl = 1; + ip->proto = WI_IPPROTO_IGMP; + ip->len = ee16(IP_HEADER_LEN + IGMPV3_QUERY_MIN_LEN); + ip->src = ee32(0x0A000001U); + ip->dst = ee32(IGMP_ALL_HOSTS); + igmp[0] = IGMP_TYPE_MEMBERSHIP_QUERY; + igmp[1] = 100; + put_be32(igmp + 4, group); + put_be16(igmp + 2, ip_checksum_buf(igmp, IGMPV3_QUERY_MIN_LEN)); + fix_ip_checksum(ip); + + /* Feed a burst of identical queries without polling. None may be answered + * synchronously from the receive path; the report is deferred to a timer. */ + last_frame_sent_count = 0; + for (q = 0; q < 5; q++) + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, sizeof(frame)); + ck_assert_uint_eq(last_frame_sent_count, 0); + + /* Poll past the 10 s window: the whole burst collapses to exactly one + * deferred Current-State Report (mode IS_EXCLUDE). */ + last_frame_sent_size = 0; + wolfIP_poll(&s, 10001); + ck_assert_uint_eq(last_frame_sent_count, 1); ck_assert_uint_gt(last_frame_sent_size, 0); ck_assert_uint_eq(last_igmp_payload()[8], IGMPV3_REC_MODE_IS_EXCLUDE); } @@ -380,8 +446,9 @@ START_TEST(test_multicast_igmp_query_spoofed_dropped) wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, sizeof(frame)); ck_assert_uint_eq(last_frame_sent_size, 0); - /* Sanity: a compliant query (TTL 1, all-hosts dst) still solicits a report, - * so the guards did not over-block. */ + /* Sanity: a compliant query (TTL 1, all-hosts dst) still solicits a report + * (deferred per RFC 3376 §5.2, then emitted on poll), so the guards did not + * over-block. */ memset(frame, 0, sizeof(frame)); memcpy(ip->eth.dst, "\x01\x00\x5e\x00\x00\x01", 6); memcpy(ip->eth.src, "\x02\x00\x00\x00\x00\x01", 6); @@ -398,6 +465,8 @@ START_TEST(test_multicast_igmp_query_spoofed_dropped) fix_ip_checksum(ip); last_frame_sent_size = 0; wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, sizeof(frame)); + ck_assert_uint_eq(last_frame_sent_size, 0); + wolfIP_poll(&s, 2); ck_assert_uint_gt(last_frame_sent_size, 0); } END_TEST diff --git a/src/test/unit/unit_tests_proto.c b/src/test/unit/unit_tests_proto.c index 4cc2ee76..333b88ce 100644 --- a/src/test/unit/unit_tests_proto.c +++ b/src/test/unit/unit_tests_proto.c @@ -3865,6 +3865,93 @@ START_TEST(test_wolfip_forwarding_ttl_expired) } END_TEST +/* Regression: an ICMP error message MUST NOT trigger another ICMP error + * (RFC 1812 sec 4.3.2.7, RFC 1122 sec 3.2.2). When forwarding an ICMP error + * datagram (type 3, 4, 5, 11, 12) whose TTL has reached 1, wolfIP must + * silently drop it instead of emitting a Time Exceeded reply to the source. */ +START_TEST(test_regression_forwarding_no_ttl_exceeded_for_icmp_error) +{ + 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, 0xAA, 0xBB, 0xCC}; + uint8_t iface1_mac[6] = {0x02, 0x00, 0x00, 0x00, 0x00, 0x03}; + uint32_t dest_ip = 0xC0A80110; + /* The ICMP error types that must not provoke a Time Exceeded reply. */ + uint8_t icmp_error_types[5] = {ICMP_DEST_UNREACH, 4 /* Source Quench */, + 5 /* Redirect */, ICMP_TTL_EXCEEDED, 12 /* Parameter Problem */}; + unsigned int t; + + for (t = 0; t < 5; t++) { + 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); + + 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 = 1; + frame->proto = WI_IPPROTO_ICMP; + frame->len = ee16(IP_HEADER_LEN + 8); + frame->src = ee32(0xC0A800AA); + frame->dst = ee32(dest_ip); + /* Embedded ICMP message: type is the first byte of the IP payload. */ + frame->data[0] = icmp_error_types[t]; + 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); + + /* No Time Exceeded (or any other frame) may be emitted. */ + ck_assert_uint_eq(last_frame_sent_size, 0); + } + + /* Positive control: a non-error ICMP message (Echo Request, type 8) is a + * query, not an error, so a TTL-expired forward of it must STILL produce a + * Time Exceeded. This guards against an over-broad fix that suppresses the + * reply for every ICMP packet rather than only ICMP error messages. */ + 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); + + 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 = 1; + frame->proto = WI_IPPROTO_ICMP; + frame->len = ee16(IP_HEADER_LEN + 8); + frame->src = ee32(0xC0A800AA); + frame->dst = ee32(dest_ip); + frame->data[0] = ICMP_ECHO_REQUEST; + 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 + ICMP_TTL_EXCEEDED_SIZE)); + ck_assert_uint_eq( + ((struct wolfIP_icmp_ttl_exceeded_packet *)last_frame_sent)->type, + ICMP_TTL_EXCEEDED); +} +END_TEST + START_TEST(test_loopback_dest_not_forwarded) { struct wolfIP s; diff --git a/src/test/unit/unit_tests_socket_api_arms.c b/src/test/unit/unit_tests_socket_api_arms.c index b845822d..b203cc12 100644 --- a/src/test/unit/unit_tests_socket_api_arms.c +++ b/src/test/unit/unit_tests_socket_api_arms.c @@ -1204,7 +1204,11 @@ START_TEST(test_sock_recvfrom_tcp_not_established) sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_STREAM, 0); ck_assert_int_ge(sd, 0); ts = &s.tcpsockets[SOCKET_UNMARK(sd)]; - ts->sock.tcp.state = TCP_CLOSED; + /* A genuinely not-established stream (mid-connect) reports an error. + * TCP_CLOSED is intentionally NOT used here: a torn-down/closed stream now + * reports EOF (0), covered by + * test_tcp_input_syn_rcvd_rst_nullcb_recv_reports_eof. */ + ts->sock.tcp.state = TCP_SYN_SENT; ck_assert_int_eq(wolfIP_sock_recvfrom(&s, sd, buf, sizeof(buf), 0, NULL, NULL), -1); diff --git a/src/test/unit/unit_tests_tcp_flow.c b/src/test/unit/unit_tests_tcp_flow.c index 565283b1..8fecb8ed 100644 --- a/src/test/unit/unit_tests_tcp_flow.c +++ b/src/test/unit/unit_tests_tcp_flow.c @@ -2620,7 +2620,8 @@ START_TEST(test_tcp_input_synack_negotiates_peer_mss) synack.seg.win = ee16(65535); synack.mss_opt[0] = TCP_OPTION_MSS; synack.mss_opt[1] = TCP_OPTION_MSS_LEN; - mss_be = ee16(512); + /* Above the RFC 9293 §3.7.1 floor (536), so it is recorded verbatim. */ + mss_be = ee16(1000); memcpy(&synack.mss_opt[2], &mss_be, sizeof(mss_be)); fix_tcp_checksums(&synack.seg); @@ -2628,7 +2629,7 @@ START_TEST(test_tcp_input_synack_negotiates_peer_mss) (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + TCP_HEADER_LEN + 4)); ck_assert_int_eq(ts->sock.tcp.state, TCP_ESTABLISHED); - ck_assert_uint_eq(ts->sock.tcp.peer_mss, 512U); + ck_assert_uint_eq(ts->sock.tcp.peer_mss, 1000U); } END_TEST @@ -2763,7 +2764,10 @@ START_TEST(test_sock_sendto_tcp_respects_negotiated_peer_mss) synack.seg.win = ee16(65535); synack.mss_opt[0] = TCP_OPTION_MSS; synack.mss_opt[1] = TCP_OPTION_MSS_LEN; - mss_be = ee16(512); + /* Above the RFC 9293 §3.7.1 floor (536) so it is recorded verbatim, yet + * below our own interface MSS so it still binds tcp_tx_payload_cap(); a + * 1200-byte payload then splits into >=3 segments (ceil(1200/560)=3). */ + mss_be = ee16(560); memcpy(&synack.mss_opt[2], &mss_be, sizeof(mss_be)); fix_tcp_checksums(&synack.seg); @@ -2791,7 +2795,7 @@ START_TEST(test_sock_sendto_tcp_respects_negotiated_peer_mss) base_len = (uint32_t)(sizeof(struct wolfIP_tcp_seg) + opt_len); ck_assert_uint_ge(desc->len, base_len); seg_payload = desc->len - base_len; - ck_assert_uint_le(seg_payload, 512U); + ck_assert_uint_le(seg_payload, 560U); seg_count++; desc = fifo_next(&ts->sock.tcp.txbuf, desc); diff --git a/src/test/unit/unit_tests_tcp_state.c b/src/test/unit/unit_tests_tcp_state.c index 4ab5cc1b..7e05663e 100644 --- a/src/test/unit/unit_tests_tcp_state.c +++ b/src/test/unit/unit_tests_tcp_state.c @@ -466,6 +466,39 @@ START_TEST(test_tcp_parse_options_mss_zero_ignored) } END_TEST +/* A peer-advertised MSS below the RFC 9293 floor (536) must be clamped up to + * TCP_DEFAULT_MSS, so a malicious tiny MSS cannot coerce us into emitting 1-byte + * segments (small-MSS DoS amplification). Symmetric with the ICMP PTB floor. */ +START_TEST(test_tcp_parse_options_mss_below_floor_clamped) +{ + uint8_t opts[] = { + TCP_OPTION_MSS, 4, 0x00, 0x01, /* MSS=1: below the 536 floor */ + TCP_OPTION_EOO + }; + struct wolfIP s; + struct tsocket *ts; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->sock.tcp.state = TCP_LISTEN; + ts->src_port = 8080; + + inject_tcp_segment_with_opts(&s, TEST_PRIMARY_IF, + 0x0A0000A1U, 0x0A000001U, 40000, 8080, + 1, 0, TCP_FLAG_SYN, + opts, (uint8_t)sizeof(opts), NULL, 0); + + /* Sub-floor MSS must not drag peer_mss below TCP_DEFAULT_MSS. */ + ck_assert_uint_ge(ts->sock.tcp.peer_mss, TCP_DEFAULT_MSS); +} +END_TEST + /* SACK-permitted option is parsed */ START_TEST(test_tcp_parse_options_sack_permitted_parsed) { @@ -624,6 +657,58 @@ START_TEST(test_tcp_input_syn_rcvd_rst_good_seq_nonlistener_closes) } END_TEST +START_TEST(test_tcp_input_syn_rcvd_rst_nullcb_recv_reports_eof) +{ + struct wolfIP s; + struct tsocket *ts; + char buf[16]; + int fd; + int can_read_ret; + int recv_ret; + ip4 local_ip = 0x0A000001U; + ip4 remote_ip = 0x0A0000A1U; + uint16_t lport = 8080, rport = 40000; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, local_ip, 0xFFFFFF00U, 0); + + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->sock.tcp.state = TCP_SYN_RCVD; + ts->sock.tcp.is_listener = 0; /* accepted clone, not the listener */ + ts->sock.tcp.ack = 2; /* rcv_nxt = 2 */ + ts->local_ip = local_ip; + ts->remote_ip = remote_ip; + ts->src_port = lport; + ts->dst_port = rport; + ts->sock.tcp.tmr_rto = NO_TIMER; + ts->callback = NULL; /* accept() never blocked: no callback armed */ + ts->callback_arg = NULL; + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + fd = (int)(MARK_TCP_SOCKET | 0); + + /* Peer RST (seq == rcv_nxt) tears the half-open clone down. With no + * callback the teardown is immediate (close_socket from the RX path). */ + inject_tcp_segment(&s, TEST_PRIMARY_IF, remote_ip, local_ip, + rport, lport, 2, 0, TCP_FLAG_RST); + + ck_assert_int_eq(ts->sock.tcp.state, TCP_CLOSED); + + /* can_read() reports the closed stream as readable... */ + can_read_ret = wolfIP_sock_can_read(&s, fd); + ck_assert_int_eq(can_read_ret, 1); + + /* ...so recv() must report EOF (0), not a bare -1. The -1 is what the + * FreeRTOS shim turns into "recv failed ret=-1 sock_err=1". */ + recv_ret = wolfIP_sock_recv(&s, fd, buf, sizeof(buf), 0); + ck_assert_int_eq(recv_ret, 0); +} +END_TEST + /* Time-wait state re-ACKs any incoming segment */ START_TEST(test_tcp_input_time_wait_sends_ack_on_any_segment) { @@ -2208,3 +2293,235 @@ START_TEST(test_tcp_input_rst_in_window_not_exact_sends_ack) ck_assert_int_ne(ts->proto, 0); } END_TEST + +/* + * wolfIP_sock_close() must disarm the callback on EAGAIN teardown paths. + * this is the transition ESTABLISHED -> FIN_WAIT_1. + * */ +START_TEST(test_sock_close_established_disarms_callback) +{ + struct wolfIP s; + struct tsocket *ts; + ip4 local_ip = 0x0A000001U; + ip4 remote_ip = 0x0A0000A1U; + uint16_t lport = 9100, rport = 41000; + int sd = MARK_TCP_SOCKET; /* tcpsockets[0] */ + int callback_arg = 0; /* stand-in for the app's heap context */ + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, local_ip, 0xFFFFFF00U, 0); + + ts = setup_established_socket(&s, local_ip, remote_ip, lport, rport); + wolfIP_register_callback(&s, sd, test_socket_cb, &callback_arg); + + /* Active close: FIN sent, FIN_WAIT_1, EAGAIN, callback disarmed. */ + ck_assert_int_eq(wolfIP_sock_close(&s, sd), -WOLFIP_EAGAIN); + ck_assert_int_eq(ts->sock.tcp.state, TCP_FIN_WAIT_1); + ck_assert_ptr_null(ts->callback); + ck_assert_ptr_null(ts->callback_arg); + + /* The app is now free to release callback_arg. Drive the remote FIN the + * report relies on: seq == rcv_nxt, ack == snd_una (does not ack our FIN) + * -> FIN_WAIT_1 transitions to CLOSING and raises the close events. */ + socket_cb_calls = 0; + inject_tcp_segment(&s, TEST_PRIMARY_IF, remote_ip, local_ip, + rport, lport, 100, 200, TCP_FLAG_ACK | TCP_FLAG_FIN); + ck_assert_int_eq(ts->sock.tcp.state, TCP_CLOSING); + ck_assert_uint_ne(ts->events & (CB_EVENT_CLOSED | CB_EVENT_READABLE), 0); + + /* wolfIP_poll() Step 3 must not dispatch the disarmed callback. */ + (void)wolfIP_poll(&s, 1); + ck_assert_int_eq(socket_cb_calls, 0); +} +END_TEST + +/* + * wolfIP_sock_close() must disarm the callback on EAGAIN teardown paths. + * this is the transition CLOSE_WAIT -> LAST_ACK. + * */ +START_TEST(test_sock_close_close_wait_disarms_callback) +{ + struct wolfIP s; + struct tsocket *ts; + struct tcp_seg_buf segbuf; + struct wolfIP_tcp_seg *seg; + struct pkt_desc *desc; + ip4 local_ip = 0x0A000001U; + ip4 remote_ip = 0x0A0000A1U; + uint16_t lport = 9101, rport = 41001; + int sd = MARK_TCP_SOCKET; /* tcpsockets[0] */ + int callback_arg = 0; /* stand-in for the app's heap context */ + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, local_ip, 0xFFFFFF00U, 0); + + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->sock.tcp.state = TCP_CLOSE_WAIT; /* remote FIN already received */ + ts->sock.tcp.ack = 100; /* rcv_nxt */ + ts->sock.tcp.snd_una = 100; + ts->sock.tcp.seq = 120; /* 20 bytes sent, unacked */ + ts->sock.tcp.last = 120; + ts->sock.tcp.bytes_in_flight = 20; + ts->sock.tcp.cwnd = TCP_MSS * 4; + ts->sock.tcp.peer_rwnd = TCP_MSS * 4; + ts->sock.tcp.ssthresh = TCP_MSS * 8; + ts->local_ip = local_ip; + ts->remote_ip = remote_ip; + ts->src_port = lport; + ts->dst_port = rport; + ts->if_idx = TEST_PRIMARY_IF; + ts->sock.tcp.tmr_rto = NO_TIMER; + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + queue_init(&ts->sock.tcp.rxbuf, ts->rxmem, RXBUF_SIZE, 0); + arp_store_neighbor(&s, TEST_PRIMARY_IF, remote_ip, (uint8_t *)setup_peer_mac); + + /* Stage the 20 bytes of sent-but-unacked data (seq 100..119). */ + memset(&segbuf, 0, sizeof(segbuf)); + seg = &segbuf.seg; + seg->ip.len = ee16(IP_HEADER_LEN + TCP_HEADER_LEN + 20); + seg->hlen = TCP_HEADER_LEN << 2; + seg->seq = ee32(100); + ck_assert_int_eq(fifo_push(&ts->sock.tcp.txbuf, &segbuf, sizeof(segbuf)), 0); + desc = fifo_peek(&ts->sock.tcp.txbuf); + ck_assert_ptr_nonnull(desc); + desc->flags |= PKT_FLAG_SENT; + + wolfIP_register_callback(&s, sd, test_socket_cb, &callback_arg); + + /* Active close from CLOSE_WAIT: FIN sent (seq 120), LAST_ACK, EAGAIN, + * callback disarmed. */ + ck_assert_int_eq(wolfIP_sock_close(&s, sd), -WOLFIP_EAGAIN); + ck_assert_int_eq(ts->sock.tcp.state, TCP_LAST_ACK); + ck_assert_ptr_null(ts->callback); + ck_assert_ptr_null(ts->callback_arg); + + /* Remote ACKs the data (ack=120) but not our FIN (would need 121): the + * socket stays in LAST_ACK and tcp_ack raises CB_EVENT_WRITABLE. */ + socket_cb_calls = 0; + inject_tcp_segment(&s, TEST_PRIMARY_IF, remote_ip, local_ip, + rport, lport, 100, 120, TCP_FLAG_ACK); + ck_assert_int_eq(ts->sock.tcp.state, TCP_LAST_ACK); + ck_assert_uint_ne(ts->events & CB_EVENT_WRITABLE, 0); + + /* wolfIP_poll() Step 3 must not dispatch the disarmed callback. */ + (void)wolfIP_poll(&s, 1); + ck_assert_int_eq(socket_cb_calls, 0); +} +END_TEST + +/* + * A RST that closes a FIN_WAIT_1 socket must deliver CB_EVENT_CLOSED to a + * re-armed waiter. + */ +START_TEST(test_rst_in_fin_wait_1_delivers_close_event) +{ + struct wolfIP s; + struct tsocket *ts; + ip4 local_ip = 0x0A000001U; + ip4 remote_ip = 0x0A0000A1U; + uint16_t lport = 9102, rport = 41002; + int sd = MARK_TCP_SOCKET; /* tcpsockets[0] */ + int callback_arg = 0; /* stand-in for the app's heap context */ + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, local_ip, 0xFFFFFF00U, 0); + + ts = setup_established_socket(&s, local_ip, remote_ip, lport, rport); + wolfIP_register_callback(&s, sd, test_socket_cb, &callback_arg); + + /* Active close: FIN sent, FIN_WAIT_1, EAGAIN, native callback disarmed. */ + ck_assert_int_eq(wolfIP_sock_close(&s, sd), -WOLFIP_EAGAIN); + ck_assert_int_eq(ts->sock.tcp.state, TCP_FIN_WAIT_1); + + /* The wrapper re-arms its own callback to wait for CB_EVENT_CLOSED. */ + wolfIP_register_callback(&s, sd, test_socket_cb, &callback_arg); + socket_cb_calls = 0; + socket_cb_last_events = 0; + + /* Forged/legit RST with SEG.SEQ == RCV.NXT (rcv_nxt == ts->sock.tcp.ack + * == 100): the generic RST handler accepts it and calls close_socket(). */ + inject_tcp_segment(&s, TEST_PRIMARY_IF, remote_ip, local_ip, + rport, lport, 100, 0, TCP_FLAG_RST); + + /* The connection is correctly torn down... */ + ck_assert_int_eq(ts->sock.tcp.state, TCP_CLOSED); + + /* ...but the re-armed close waiter must still be notified exactly once + * with CB_EVENT_CLOSED. On current code the memset wiped the callback + * before Step 3 ran, so these assertions fail (the bug). */ + (void)wolfIP_poll(&s, 1); + ck_assert_int_ge(socket_cb_calls, 1); + ck_assert_uint_ne(socket_cb_last_events & CB_EVENT_CLOSED, 0); +} +END_TEST + +/* + * The peer's final ACK that closes a LAST_ACK socket must deliver + * CB_EVENT_CLOSED to a re-armed waiter. + */ +START_TEST(test_last_ack_final_ack_delivers_close_event) +{ + struct wolfIP s; + struct tsocket *ts; + ip4 local_ip = 0x0A000001U; + ip4 remote_ip = 0x0A0000A1U; + uint16_t lport = 9103, rport = 41003; + int sd = MARK_TCP_SOCKET; /* tcpsockets[0] */ + int callback_arg = 0; /* stand-in for the app's heap context */ + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, local_ip, 0xFFFFFF00U, 0); + + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->sock.tcp.state = TCP_CLOSE_WAIT; /* remote FIN already received */ + ts->sock.tcp.ack = 100; /* rcv_nxt */ + ts->sock.tcp.snd_una = 120; + ts->sock.tcp.seq = 120; /* nothing outstanding */ + ts->sock.tcp.last = 120; + ts->sock.tcp.cwnd = TCP_MSS * 4; + ts->sock.tcp.peer_rwnd = TCP_MSS * 4; + ts->sock.tcp.ssthresh = TCP_MSS * 8; + ts->local_ip = local_ip; + ts->remote_ip = remote_ip; + ts->src_port = lport; + ts->dst_port = rport; + ts->if_idx = TEST_PRIMARY_IF; + ts->sock.tcp.tmr_rto = NO_TIMER; + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + queue_init(&ts->sock.tcp.rxbuf, ts->rxmem, RXBUF_SIZE, 0); + arp_store_neighbor(&s, TEST_PRIMARY_IF, remote_ip, (uint8_t *)setup_peer_mac); + + wolfIP_register_callback(&s, sd, test_socket_cb, &callback_arg); + + /* Active close from CLOSE_WAIT: FIN sent (seq 120), LAST_ACK, EAGAIN, + * native callback disarmed. */ + ck_assert_int_eq(wolfIP_sock_close(&s, sd), -WOLFIP_EAGAIN); + ck_assert_int_eq(ts->sock.tcp.state, TCP_LAST_ACK); + + /* The wrapper re-arms its own callback to wait for CB_EVENT_CLOSED. */ + wolfIP_register_callback(&s, sd, test_socket_cb, &callback_arg); + socket_cb_calls = 0; + socket_cb_last_events = 0; + + /* Peer's final ACK acks our FIN (ack == 121): tcp_ack() LAST_ACK branch + * transitions to TCP_CLOSED and calls close_socket(). */ + inject_tcp_segment(&s, TEST_PRIMARY_IF, remote_ip, local_ip, + rport, lport, 100, 121, TCP_FLAG_ACK); + ck_assert_int_eq(ts->sock.tcp.state, TCP_CLOSED); + + /* The re-armed close waiter must be notified with CB_EVENT_CLOSED. */ + (void)wolfIP_poll(&s, 1); + ck_assert_int_ge(socket_cb_calls, 1); + ck_assert_uint_ne(socket_cb_last_events & CB_EVENT_CLOSED, 0); +} +END_TEST diff --git a/src/test/unit/unit_wolfguard.c b/src/test/unit/unit_wolfguard.c index 176b7ce1..ad2de930 100644 --- a/src/test/unit/unit_wolfguard.c +++ b/src/test/unit/unit_wolfguard.c @@ -658,7 +658,7 @@ START_TEST(test_cookie_mac1_valid) memset(&msg, 0xAA, sizeof(msg)); mac_off = offsetof(struct wg_msg_initiation, macs); - ret = wg_cookie_add_macs(&peer, &msg, sizeof(msg), mac_off); + ret = wg_cookie_add_macs(&peer, &msg, sizeof(msg), mac_off, dev.now); ck_assert_int_eq(ret, 0); /* Validate */ @@ -688,7 +688,7 @@ START_TEST(test_cookie_mac1_invalid) memset(&msg, 0xBB, sizeof(msg)); mac_off = offsetof(struct wg_msg_initiation, macs); - wg_cookie_add_macs(&peer, &msg, sizeof(msg), mac_off); + wg_cookie_add_macs(&peer, &msg, sizeof(msg), mac_off, dev.now); /* Tamper with mac1 */ msg.macs.mac1[0] ^= 0xFF; @@ -724,7 +724,7 @@ START_TEST(test_cookie_reply) /* Create trigger message with MACs */ memset(&trigger, 0xCC, sizeof(trigger)); mac_off = offsetof(struct wg_msg_initiation, macs); - wg_cookie_add_macs(&peer, &trigger, sizeof(trigger), mac_off); + wg_cookie_add_macs(&peer, &trigger, sizeof(trigger), mac_off, dev.now); /* Create cookie reply */ ret = wg_cookie_create_reply(&dev, &cookie_reply, &trigger, @@ -734,7 +734,7 @@ START_TEST(test_cookie_reply) ck_assert_int_eq(ret, 0); /* Consume cookie reply */ - ret = wg_cookie_consume_reply(&peer, &cookie_reply); + ret = wg_cookie_consume_reply(&peer, &cookie_reply, dev.now); ck_assert_int_eq(ret, 0); ck_assert_int_eq(peer.cookie.is_valid, 1); @@ -742,11 +742,70 @@ START_TEST(test_cookie_reply) ck_assert_int_eq(peer.cookie.have_sent_mac1, 0); /* Replaying the same cookie reply should be rejected */ - ret = wg_cookie_consume_reply(&peer, &cookie_reply); + ret = wg_cookie_consume_reply(&peer, &cookie_reply, dev.now); ck_assert_int_ne(ret, 0); } END_TEST +/* + * wg_cookie_consume_reply must record peer->cookie.birthdate, and + * wg_cookie_add_macs must stop attaching mac2 once the cookie is older than + * the responder's secret-rotation window. Without this, a stale (or on-path + * forged) cookie is used to compute mac2 indefinitely. + */ +START_TEST(test_cookie_expiry) +{ + struct wg_device dev; + struct wg_peer peer; + struct wg_msg_initiation trigger; + struct wg_msg_cookie cookie_reply; + size_t mac_off; + uint8_t zero_mac[WG_COOKIE_LEN]; + int ret; + + init_test_rng(); + + memset(&dev, 0, sizeof(dev)); + memset(&peer, 0, sizeof(peer)); + memset(zero_mac, 0, sizeof(zero_mac)); + + wg_dh_generate(dev.static_private, dev.static_public, &test_rng); + memcpy(&dev.rng, &test_rng, sizeof(WC_RNG)); + dev.now = 5000; + + wg_cookie_checker_init(&dev.cookie_checker, dev.static_public); + wg_cookie_init(&peer.cookie, dev.static_public); + + /* Establish a cookie at t = dev.now */ + memset(&trigger, 0xCC, sizeof(trigger)); + mac_off = offsetof(struct wg_msg_initiation, macs); + wg_cookie_add_macs(&peer, &trigger, sizeof(trigger), mac_off, dev.now); + + ret = wg_cookie_create_reply(&dev, &cookie_reply, &trigger, mac_off, + trigger.sender_index, 0x0A0A0A01, 12345); + ck_assert_int_eq(ret, 0); + + ret = wg_cookie_consume_reply(&peer, &cookie_reply, dev.now); + ck_assert_int_eq(ret, 0); + ck_assert_int_eq(peer.cookie.is_valid, 1); + + /* consume_reply must record the cookie birthdate */ + ck_assert_uint_eq(peer.cookie.birthdate, dev.now); + + /* At exactly the max age the cookie is still usable: mac2 attached */ + memset(&trigger, 0xBB, sizeof(trigger)); + wg_cookie_add_macs(&peer, &trigger, sizeof(trigger), mac_off, + dev.now + (uint64_t)WG_COOKIE_SECRET_MAX_AGE * 1000ULL); + ck_assert_int_ne(memcmp(trigger.macs.mac2, zero_mac, WG_COOKIE_LEN), 0); + + /* One ms past the max age the cookie is expired: mac2 must be zero */ + memset(&trigger, 0xBB, sizeof(trigger)); + wg_cookie_add_macs(&peer, &trigger, sizeof(trigger), mac_off, + dev.now + (uint64_t)WG_COOKIE_SECRET_MAX_AGE * 1000ULL + 1); + ck_assert_int_eq(memcmp(trigger.macs.mac2, zero_mac, WG_COOKIE_LEN), 0); +} +END_TEST + /* * a default-initialized cookie secret must never validate a mac2. */ @@ -780,7 +839,7 @@ START_TEST(test_cookie_zero_secret_no_mac2_bypass) memset(&msg, 0xAA, sizeof(msg)); mac_off = offsetof(struct wg_msg_initiation, macs); - ck_assert_int_eq(wg_cookie_add_macs(&peer, &msg, sizeof(msg), mac_off), 0); + ck_assert_int_eq(wg_cookie_add_macs(&peer, &msg, sizeof(msg), mac_off, dev.now), 0); /* Forge mac2 from the known all-zero secret, exactly as validate() would * derive the cookie for this source */ @@ -1021,14 +1080,14 @@ static void setup_paired_devices(struct wg_device *dev_a, /* Perform handshake */ ck_assert_int_eq(wg_noise_create_initiation(dev_a, peer_a, &init_msg), 0); mac_off = offsetof(struct wg_msg_initiation, macs); - wg_cookie_add_macs(peer_a, &init_msg, sizeof(init_msg), mac_off); + wg_cookie_add_macs(peer_a, &init_msg, sizeof(init_msg), mac_off, dev_a->now); found = wg_noise_consume_initiation(dev_b, &init_msg); ck_assert_ptr_nonnull(found); ck_assert_int_eq(wg_noise_create_response(dev_b, found, &resp_msg), 0); mac_off = offsetof(struct wg_msg_response, macs); - wg_cookie_add_macs(found, &resp_msg, sizeof(resp_msg), mac_off); + wg_cookie_add_macs(found, &resp_msg, sizeof(resp_msg), mac_off, dev_b->now); ck_assert_int_eq(wg_noise_consume_response(dev_a, peer_a, &resp_msg), 0); @@ -1344,14 +1403,14 @@ static void setup_tick_devices(struct wg_device *dev_a, ck_assert_int_eq(wg_noise_create_initiation(dev_a, &dev_a->peers[0], &init_msg), 0); mac_off = offsetof(struct wg_msg_initiation, macs); - wg_cookie_add_macs(&dev_a->peers[0], &init_msg, sizeof(init_msg), mac_off); + wg_cookie_add_macs(&dev_a->peers[0], &init_msg, sizeof(init_msg), mac_off, dev_a->now); found = wg_noise_consume_initiation(dev_b, &init_msg); ck_assert_ptr_nonnull(found); ck_assert_int_eq(wg_noise_create_response(dev_b, found, &resp_msg), 0); mac_off = offsetof(struct wg_msg_response, macs); - wg_cookie_add_macs(found, &resp_msg, sizeof(resp_msg), mac_off); + wg_cookie_add_macs(found, &resp_msg, sizeof(resp_msg), mac_off, dev_b->now); ck_assert_int_eq(wg_noise_consume_response(dev_a, &dev_a->peers[0], &resp_msg), 0); @@ -1686,14 +1745,14 @@ START_TEST(test_endpoint_unchanged_on_bad_response) dev_b.static_public, NULL, &dev_a.rng); ck_assert_int_eq(wg_noise_create_initiation(&dev_a, &peer_a, &init_msg), 0); mac_off = offsetof(struct wg_msg_initiation, macs); - wg_cookie_add_macs(&peer_a, &init_msg, sizeof(init_msg), mac_off); + wg_cookie_add_macs(&peer_a, &init_msg, sizeof(init_msg), mac_off, dev_a.now); /* B consumes and creates response */ found = wg_noise_consume_initiation(&dev_b, &init_msg); ck_assert_ptr_nonnull(found); ck_assert_int_eq(wg_noise_create_response(&dev_b, found, &resp_msg), 0); mac_off = offsetof(struct wg_msg_response, macs); - wg_cookie_add_macs(found, &resp_msg, sizeof(resp_msg), mac_off); + wg_cookie_add_macs(found, &resp_msg, sizeof(resp_msg), mac_off, dev_b.now); /* Tamper with the response to make auth fail */ resp_msg.encrypted_nothing[0] ^= 0xFF; @@ -1731,7 +1790,7 @@ START_TEST(test_cookie_enforcement_under_load) dev_a.now = 30000; ck_assert_int_eq(wg_noise_create_initiation(&dev_a, &peer_a, &init_msg), 0); mac_off = offsetof(struct wg_msg_initiation, macs); - wg_cookie_add_macs(&peer_a, &init_msg, sizeof(init_msg), mac_off); + wg_cookie_add_macs(&peer_a, &init_msg, sizeof(init_msg), mac_off, dev_a.now); /* Verify mac2 is zero (no cookie yet) */ memset(zero_mac, 0, sizeof(zero_mac)); @@ -1770,7 +1829,7 @@ START_TEST(test_response_under_load_threshold) dev_b.static_public, NULL, &dev_a.rng); ck_assert_int_eq(wg_noise_create_initiation(&dev_a, &peer_a, &init_msg), 0); mac_off = offsetof(struct wg_msg_initiation, macs); - wg_cookie_add_macs(&peer_a, &init_msg, sizeof(init_msg), mac_off); + wg_cookie_add_macs(&peer_a, &init_msg, sizeof(init_msg), mac_off, dev_a.now); found = wg_noise_consume_initiation(&dev_b, &init_msg); ck_assert_ptr_nonnull(found); @@ -1778,7 +1837,7 @@ START_TEST(test_response_under_load_threshold) resp_msg.receiver_index = 0xDEADBEEFu; mac_off = offsetof(struct wg_msg_response, macs); - wg_cookie_add_macs(found, &resp_msg, sizeof(resp_msg), mac_off); + wg_cookie_add_macs(found, &resp_msg, sizeof(resp_msg), mac_off, dev_b.now); wolfguard_poll(&dev_a, 30000); ck_assert_int_eq(dev_a.under_load, 0); @@ -1891,6 +1950,97 @@ START_TEST(test_psk_survives_rekey) } END_TEST +/* A forged response that fails AEAD auth must not corrupt the initiator's + * handshake state. wg_noise_consume_response runs the Noise KDF/hash chain + * over hs->chaining_key and hs->hash, then authenticates via AEAD. If a + * forged response (valid framing, attacker-chosen ephemeral, garbage + * encrypted_nothing) is allowed to mutate chaining_key/hash before the + * failed AEAD check, the half-advanced state poisons the next consume: + * the genuine responder reply then derives the wrong key and is rejected, + * a persistent handshake DoS an on-path attacker can drive each cycle. + * The function must leave chaining_key/hash/state untouched on failure so + * the genuine response still completes. */ +START_TEST(test_consume_response_forgery_preserves_state) +{ + struct wg_device dev_a, dev_b; + struct wg_peer peer_a; + struct wg_msg_initiation init_msg; + struct wg_msg_response resp_msg, forged_msg; + struct wg_peer *found; + uint8_t chaining_key_before[WG_HASH_LEN]; + uint8_t hash_before[WG_HASH_LEN]; + int ret; + + init_test_rng(); + + memset(&dev_a, 0, sizeof(dev_a)); + memset(&dev_b, 0, sizeof(dev_b)); + memset(&peer_a, 0, sizeof(peer_a)); + + wg_dh_generate(dev_a.static_private, dev_a.static_public, &test_rng); + wg_dh_generate(dev_b.static_private, dev_b.static_public, &test_rng); + memcpy(&dev_a.rng, &test_rng, sizeof(WC_RNG)); + memcpy(&dev_b.rng, &test_rng, sizeof(WC_RNG)); + + memcpy(peer_a.public_key, dev_b.static_public, WG_PUBLIC_KEY_LEN); + peer_a.is_active = 1; + wg_noise_handshake_init(&peer_a.handshake, dev_a.static_private, + dev_b.static_public, NULL, &test_rng); + + memcpy(dev_b.peers[0].public_key, dev_a.static_public, WG_PUBLIC_KEY_LEN); + dev_b.peers[0].is_active = 1; + wg_noise_handshake_init(&dev_b.peers[0].handshake, dev_b.static_private, + dev_a.static_public, NULL, &test_rng); + + /* A creates an initiation; B consumes it and builds the genuine + * response. Keep resp_msg intact for the recovery step below. */ + dev_a.now = 5000; + dev_b.now = 5000; + ck_assert_int_eq(wg_noise_create_initiation(&dev_a, &peer_a, &init_msg), 0); + found = wg_noise_consume_initiation(&dev_b, &init_msg); + ck_assert_ptr_nonnull(found); + ck_assert_int_eq(wg_noise_create_response(&dev_b, found, &resp_msg), 0); + + /* A is now waiting for the response. Snapshot the handshake state. */ + ck_assert_int_eq(peer_a.handshake.state, WG_HANDSHAKE_CREATED_INITIATION); + memcpy(chaining_key_before, peer_a.handshake.chaining_key, WG_HASH_LEN); + memcpy(hash_before, peer_a.handshake.hash, WG_HASH_LEN); + + /* Forge a response: copy a real frame, then replace the ephemeral and + * encrypted_nothing with attacker-chosen bytes. Without the derived key + * the attacker cannot produce a valid tag, so AEAD must fail. */ + memcpy(&forged_msg, &resp_msg, sizeof(forged_msg)); + wc_RNG_GenerateBlock(&test_rng, forged_msg.ephemeral, WG_PUBLIC_KEY_LEN); + wc_RNG_GenerateBlock(&test_rng, forged_msg.encrypted_nothing, + WG_AUTHTAG_LEN); + + /* The forged response must be rejected... */ + ret = wg_noise_consume_response(&dev_a, &peer_a, &forged_msg); + ck_assert_int_eq(ret, -1); + + /* ...and must leave the handshake exactly as it was. */ + ck_assert_int_eq(memcmp(peer_a.handshake.chaining_key, + chaining_key_before, WG_HASH_LEN), 0); + ck_assert_int_eq(memcmp(peer_a.handshake.hash, hash_before, WG_HASH_LEN), + 0); + ck_assert_int_eq(peer_a.handshake.state, WG_HANDSHAKE_CREATED_INITIATION); + + /* The genuine responder reply must still complete the handshake. */ + ret = wg_noise_consume_response(&dev_a, &peer_a, &resp_msg); + ck_assert_int_eq(ret, 0); + ck_assert_int_eq(peer_a.handshake.state, WG_HANDSHAKE_CONSUMED_RESPONSE); + + /* And the derived session keys must agree with the responder. */ + dev_a.now = 5001; + dev_b.now = 5001; + ck_assert_int_eq(wg_noise_begin_session(&dev_a, &peer_a), 0); + ck_assert_int_eq(wg_noise_begin_session(&dev_b, found), 0); + ck_assert_int_eq(memcmp(peer_a.keypairs.current->sending.key, + found->keypairs.next->receiving.key, + WG_SYMMETRIC_KEY_LEN), 0); +} +END_TEST + /* Staged packet buffers zeroed after send */ START_TEST(test_staged_packets_zeroed_after_send) { @@ -1921,6 +2071,40 @@ START_TEST(test_staged_packets_zeroed_after_send) } END_TEST +/* Staged packets must survive a drain that cannot transmit (no current + * keypair, e.g. a responder whose session is still in 'next'). */ +START_TEST(test_staged_packets_preserved_when_no_keypair) +{ + struct wg_device dev_a, dev_b; + struct wg_peer peer_a, peer_b; + uint8_t test_pkt[64]; + int i; + + setup_paired_devices(&dev_a, &dev_b, &peer_a, &peer_b); + + /* Force the "cannot send yet" condition: no current keypair. Set a + * non-zeroed handshake state so wg_packet_send only re-stages without + * kicking off a fresh initiation. */ + peer_b.keypairs.current = NULL; + peer_b.handshake.state = WG_HANDSHAKE_CREATED_INITIATION; + + for (i = 0; i < 64; i++) + test_pkt[i] = (uint8_t)(i + 1); + + memcpy(peer_b.staged_packets[0], test_pkt, 64); + peer_b.staged_packet_lens[0] = 64; + peer_b.staged_count = 1; + + wg_packet_send_staged(&dev_b, &peer_b); + + /* The packet could not be transmitted, so it must remain staged for a + * later drain — not be silently zeroed/dropped. */ + ck_assert_uint_eq(peer_b.staged_count, 1); + ck_assert_uint_eq(peer_b.staged_packet_lens[0], 64); + ck_assert_int_eq(memcmp(peer_b.staged_packets[0], test_pkt, 64), 0); +} +END_TEST + /* Rate-limiting: rapid initiations from same peer rejected */ START_TEST(test_initiation_rate_limit) { @@ -2122,6 +2306,80 @@ START_TEST(test_allowed_ip_source_rejected) } END_TEST +/* + * Rotating the device static private key (wolfguard_set_private_key) must + * invalidate every peer's existing session keypairs. The symmetric session + * key is derived during the handshake and is independent of the static + * identity, so a captured pre-rotation keypair stays valid (is_valid==1, + * within REJECT_AFTER_TIME) unless explicitly cleared. If it survives, + * wg_handle_data resolves the stale receiver_index, decrypts the frame with + * the unchanged key and injects the plaintext into wg0 — letting a holder of + * the old session keep tunnelling traffic across the rotation. + */ +START_TEST(test_keypairs_cleared_on_private_key_rotation) +{ + struct wg_device dev_a, dev_b; + struct wg_peer peer_a, peer_b; + struct wg_keypair *kp; + uint8_t inner_pkt[32]; + uint8_t msg_buf[sizeof(struct wg_msg_data) + 32 + WG_AUTHTAG_LEN]; + struct wg_msg_data *data_msg = (struct wg_msg_data *)msg_buf; + uint8_t new_priv[WG_PRIVATE_KEY_LEN]; + uint64_t ctr, rx_before; + + setup_paired_devices(&dev_a, &dev_b, &peer_a, &peer_b); + + /* Steady-state established session: promote B's responder keypair to + * current, the state a long-lived peer sits in after exchanging data. */ + if (dev_b.peers[0].keypairs.next) { + dev_b.peers[0].keypairs.current = dev_b.peers[0].keypairs.next; + dev_b.peers[0].keypairs.next = NULL; + } + + /* B permits peer A to source from 10.0.0.0/24, so a decrypted frame from + * 10.0.0.1 would pass cryptokey routing and be injected into wg0. */ + wg_allowedips_insert(&dev_b, ee32(0x0A000000), 24, 0); + + /* A builds a legitimate data frame (src 10.0.0.1 -> dst 10.0.0.2). */ + kp = peer_a.keypairs.current; + ck_assert_ptr_nonnull(kp); + + memset(inner_pkt, 0, sizeof(inner_pkt)); + inner_pkt[0] = 0x45; /* IPv4, IHL=5 */ + inner_pkt[2] = 0x00; inner_pkt[3] = 32; /* total length = 32 */ + inner_pkt[12] = 10; inner_pkt[13] = 0; /* src 10.0.0.1 */ + inner_pkt[14] = 0; inner_pkt[15] = 1; + inner_pkt[16] = 10; inner_pkt[17] = 0; /* dst 10.0.0.2 */ + inner_pkt[18] = 0; inner_pkt[19] = 2; + + ctr = kp->sending_counter++; + data_msg->header.type = wg_le32_encode(WG_MSG_DATA); + data_msg->receiver_index = wg_le32_encode(kp->remote_index); + data_msg->counter = wg_le64_encode(ctr); + ck_assert_int_eq( + wg_aead_encrypt(data_msg->encrypted_data, kp->sending.key, + ctr, inner_pkt, sizeof(inner_pkt), NULL, 0), 0); + + /* Rotate B's static private key. */ + wc_RNG_GenerateBlock(&test_rng, new_priv, WG_PRIVATE_KEY_LEN); + ck_assert_int_eq(wolfguard_set_private_key(&dev_b, new_priv), 0); + + /* The pre-rotation session keypairs must be gone. */ + ck_assert_ptr_null(dev_b.peers[0].keypairs.current); + ck_assert_ptr_null(dev_b.peers[0].keypairs.previous); + ck_assert_ptr_null(dev_b.peers[0].keypairs.next); + + /* Replaying the captured frame must be dropped: rx_bytes only advances + * once decrypt succeeds, so it must stay flat after the rotation. */ + rx_before = dev_b.peers[0].rx_bytes; + dev_b.now = 10001; + wg_packet_receive(&dev_b, msg_buf, + sizeof(struct wg_msg_data) + sizeof(inner_pkt) + WG_AUTHTAG_LEN, + ee32(0xC0A80101), ee16(51821)); + ck_assert_uint_eq(dev_b.peers[0].rx_bytes, rx_before); +} +END_TEST + /* * wolfguard_output (the wg0 TX dispatch) must reject frames whose IP version * nibble is not 4. wolfguard is IPv4-only (wg_allowedips_lookup keys on a @@ -2211,6 +2469,7 @@ static Suite *wolfguard_suite(void) tcase_add_test(tc, test_noise_handshake_with_psk); tcase_add_test(tc, test_noise_replay_protection); tcase_add_test(tc, test_noise_replay_after_session); + tcase_add_test(tc, test_consume_response_forgery_preserves_state); tcase_add_test(tc, test_psk_survives_rekey); tcase_add_test(tc, test_initiation_rate_limit); suite_add_tcase(s, tc); @@ -2221,6 +2480,7 @@ static Suite *wolfguard_suite(void) tcase_add_test(tc, test_cookie_mac1_valid); tcase_add_test(tc, test_cookie_mac1_invalid); tcase_add_test(tc, test_cookie_reply); + tcase_add_test(tc, test_cookie_expiry); tcase_add_test(tc, test_cookie_zero_secret_no_mac2_bypass); tcase_add_test(tc, test_cookie_enforcement_under_load); tcase_add_test(tc, test_response_under_load_threshold); @@ -2252,8 +2512,10 @@ static Suite *wolfguard_suite(void) tcase_add_test(tc, test_packet_key_agreement); tcase_add_test(tc, test_endpoint_unchanged_on_bad_response); tcase_add_test(tc, test_staged_packets_zeroed_after_send); + tcase_add_test(tc, test_staged_packets_preserved_when_no_keypair); tcase_add_test(tc, test_keepalive_rejected_expired_key); tcase_add_test(tc, test_allowed_ip_source_rejected); + tcase_add_test(tc, test_keypairs_cleared_on_private_key_rotation); tcase_add_test(tc, test_output_rejects_non_ipv4); suite_add_tcase(s, tc); diff --git a/src/wolfguard/wg_cookie.c b/src/wolfguard/wg_cookie.c index d07c6362..43d581d0 100644 --- a/src/wolfguard/wg_cookie.c +++ b/src/wolfguard/wg_cookie.c @@ -66,7 +66,7 @@ void wg_cookie_init(struct wg_cookie *cookie, * */ int wg_cookie_add_macs(struct wg_peer *peer, void *msg, size_t msg_len, - size_t mac_offset) + size_t mac_offset, uint64_t now) { uint8_t *msg_bytes = (uint8_t *)msg; struct wg_msg_macs *macs; @@ -87,8 +87,12 @@ int wg_cookie_add_macs(struct wg_peer *peer, void *msg, size_t msg_len, memcpy(peer->cookie.last_mac1_sent, macs->mac1, WG_COOKIE_LEN); peer->cookie.have_sent_mac1 = 1; - /* mac2: only if we have a valid cookie */ - if (peer->cookie.is_valid) { + /* mac2: only if we have a valid, non-expired cookie. A cookie is only + * usable for WG_COOKIE_SECRET_MAX_AGE (matching the responder's secret + * rotation); past that the responder's cookie no longer validates, so + * stop attaching mac2 (WireGuard spec section 5.4.4 / COOKIE_TIMEOUT). */ + if (peer->cookie.is_valid && + now - peer->cookie.birthdate <= WG_COOKIE_SECRET_MAX_AGE * 1000ULL) { ret = wg_mac(macs->mac2, peer->cookie.cookie, WG_COOKIE_LEN, msg_bytes, mac_offset + WG_COOKIE_LEN); if (ret != 0) @@ -232,7 +236,8 @@ int wg_cookie_create_reply(struct wg_device *dev, struct wg_msg_cookie *reply, * Consume cookie reply message * */ -int wg_cookie_consume_reply(struct wg_peer *peer, struct wg_msg_cookie *msg) +int wg_cookie_consume_reply(struct wg_peer *peer, struct wg_msg_cookie *msg, + uint64_t now) { uint8_t cookie[WG_COOKIE_LEN]; int ret; @@ -252,6 +257,7 @@ int wg_cookie_consume_reply(struct wg_peer *peer, struct wg_msg_cookie *msg) memcpy(peer->cookie.cookie, cookie, WG_COOKIE_LEN); peer->cookie.is_valid = 1; + peer->cookie.birthdate = now; peer->cookie.have_sent_mac1 = 0; wg_memzero(cookie, sizeof(cookie)); diff --git a/src/wolfguard/wg_noise.c b/src/wolfguard/wg_noise.c index 675bec71..a0e3aa37 100644 --- a/src/wolfguard/wg_noise.c +++ b/src/wolfguard/wg_noise.c @@ -489,30 +489,34 @@ int wg_noise_consume_response(struct wg_device *dev, struct wg_peer *peer, uint8_t tau[WG_HASH_LEN]; uint8_t dh_result[WG_SYMMETRIC_KEY_LEN]; uint8_t ephemeral_public[WG_PUBLIC_KEY_LEN]; + uint8_t chaining_key[WG_HASH_LEN]; + uint8_t hash[WG_HASH_LEN]; int ret; if (hs->state != WG_HANDSHAKE_CREATED_INITIATION) return -1; memcpy(ephemeral_public, msg->ephemeral, WG_PUBLIC_KEY_LEN); + memcpy(chaining_key, hs->chaining_key, WG_HASH_LEN); + memcpy(hash, hs->hash, WG_HASH_LEN); /* C = KDF1(C, E_r_pub) */ - ret = wg_kdf1(hs->chaining_key, hs->chaining_key, + ret = wg_kdf1(chaining_key, chaining_key, ephemeral_public, WG_PUBLIC_KEY_LEN); if (ret != 0) - return -1; + goto fail; /* H = Hash(H || msg.ephemeral) */ - ret = mix_hash(hs->hash, ephemeral_public, WG_PUBLIC_KEY_LEN); + ret = mix_hash(hash, ephemeral_public, WG_PUBLIC_KEY_LEN); if (ret != 0) - return -1; + goto fail; /* C = KDF1(C, DH(E_i_priv, E_r_pub)) */ ret = wg_dh(dh_result, hs->ephemeral_private, ephemeral_public, &dev->rng); if (ret != 0) goto fail; - ret = wg_kdf1(hs->chaining_key, hs->chaining_key, + ret = wg_kdf1(chaining_key, chaining_key, dh_result, WG_SYMMETRIC_KEY_LEN); if (ret != 0) goto fail; @@ -522,19 +526,19 @@ int wg_noise_consume_response(struct wg_device *dev, struct wg_peer *peer, if (ret != 0) goto fail; - ret = wg_kdf1(hs->chaining_key, hs->chaining_key, + ret = wg_kdf1(chaining_key, chaining_key, dh_result, WG_SYMMETRIC_KEY_LEN); if (ret != 0) goto fail; /* (C, tau, k) = KDF3(C, psk) */ - ret = wg_kdf3(hs->chaining_key, tau, key, hs->chaining_key, + ret = wg_kdf3(chaining_key, tau, key, chaining_key, hs->preshared_key, WG_SYMMETRIC_KEY_LEN); if (ret != 0) goto fail; /* H = Hash(H || tau) */ - ret = mix_hash(hs->hash, tau, WG_HASH_LEN); + ret = mix_hash(hash, tau, WG_HASH_LEN); if (ret != 0) goto fail; @@ -543,28 +547,35 @@ int wg_noise_consume_response(struct wg_device *dev, struct wg_peer *peer, uint8_t nothing[1]; /* Dummy buffer for zero-length decrypt */ ret = wg_aead_decrypt(nothing, key, 0, msg->encrypted_nothing, WG_AUTHTAG_LEN, - hs->hash, WG_HASH_LEN); + hash, WG_HASH_LEN); } if (ret != 0) goto fail; /* H = Hash(H || msg.empty) */ - ret = mix_hash(hs->hash, msg->encrypted_nothing, WG_AUTHTAG_LEN); + ret = mix_hash(hash, msg->encrypted_nothing, WG_AUTHTAG_LEN); if (ret != 0) goto fail; + /* Authenticated: commit the derived state to the handshake. */ + memcpy(hs->chaining_key, chaining_key, WG_HASH_LEN); + memcpy(hs->hash, hash, WG_HASH_LEN); hs->remote_index = le32_decode(msg->sender_index); hs->state = WG_HANDSHAKE_CONSUMED_RESPONSE; wg_memzero(key, sizeof(key)); wg_memzero(tau, sizeof(tau)); wg_memzero(dh_result, sizeof(dh_result)); + wg_memzero(chaining_key, sizeof(chaining_key)); + wg_memzero(hash, sizeof(hash)); return 0; fail: wg_memzero(key, sizeof(key)); wg_memzero(tau, sizeof(tau)); wg_memzero(dh_result, sizeof(dh_result)); + wg_memzero(chaining_key, sizeof(chaining_key)); + wg_memzero(hash, sizeof(hash)); return -1; } diff --git a/src/wolfguard/wg_packet.c b/src/wolfguard/wg_packet.c index 37962d7f..08b6cf9d 100644 --- a/src/wolfguard/wg_packet.c +++ b/src/wolfguard/wg_packet.c @@ -190,7 +190,7 @@ int wg_packet_send(struct wg_device *dev, struct wg_peer *peer, if (ret == 0) { size_t mac_off = offsetof(struct wg_msg_initiation, macs); wg_cookie_add_macs(peer, &init_msg, sizeof(init_msg), - mac_off); + mac_off, dev->now); memset(&dst, 0, sizeof(dst)); dst.sin_family = AF_INET; dst.sin_addr.s_addr = peer->endpoint_ip; @@ -263,7 +263,7 @@ int wg_packet_send(struct wg_device *dev, struct wg_peer *peer, if (wg_noise_create_initiation(dev, peer, &init_msg) == 0) { size_t mac_off = offsetof(struct wg_msg_initiation, macs); wg_cookie_add_macs(peer, &init_msg, sizeof(init_msg), - mac_off); + mac_off, dev->now); memset(&dst, 0, sizeof(dst)); dst.sin_family = AF_INET; dst.sin_addr.s_addr = peer->endpoint_ip; @@ -288,17 +288,28 @@ void wg_packet_send_staged(struct wg_device *dev, struct wg_peer *peer) { int i; uint8_t count = peer->staged_count; + uint8_t local[LINK_MTU]; peer->staged_count = 0; for (i = 0; i < count; i++) { - wg_packet_send(dev, peer, - peer->staged_packets[i], - peer->staged_packet_lens[i]); - wg_memzero(peer->staged_packets[i], - peer->staged_packet_lens[i]); + size_t len = peer->staged_packet_lens[i]; + + /* Copy out and clear the source slot before sending: if + * wg_packet_send cannot transmit (no current keypair, e.g. a + * responder whose session is still in 'next', or an expired + * keypair) it re-stages the payload into staged_packets[], which + * after the reset above aliases the slots we are draining. Zeroing + * the source up front means a re-staged packet is preserved instead + * of being clobbered. */ + memcpy(local, peer->staged_packets[i], len); + wg_memzero(peer->staged_packets[i], len); peer->staged_packet_lens[i] = 0; + + wg_packet_send(dev, peer, local, len); } + + wg_memzero(local, sizeof(local)); } /* @@ -430,13 +441,17 @@ static void wg_handle_data(struct wg_device *dev, const uint8_t *data, if (plaintext_len == 0) goto out; - /* Validate inner source IP against allowed IPs */ - if (plaintext_len >= 20) { - memcpy(&inner_src_ip, plaintext + 12, 4); /* IPv4 src addr offset */ - peer_idx = wg_allowedips_lookup(dev, inner_src_ip); - if (peer_idx < 0 || &dev->peers[peer_idx] != peer) - goto out; /* Source IP not allowed for this peer */ - } + /* Validate the decrypted inner packet before injecting it into wg0. + * wolfguard is IPv4-only: require a full IPv4 header, a total-length that + * fits what we decrypted, and a source within this peer's AllowedIPs. */ + if (plaintext_len < 20 || (plaintext[0] >> 4) != 4) + goto out; + if ((uint16_t)((plaintext[2] << 8) | plaintext[3]) > plaintext_len) + goto out; + memcpy(&inner_src_ip, plaintext + 12, 4); /* IPv4 src addr offset */ + peer_idx = wg_allowedips_lookup(dev, inner_src_ip); + if (peer_idx < 0 || &dev->peers[peer_idx] != peer) + goto out; /* Source IP not allowed for this peer */ /* Inject decrypted packet into the wg0 interface */ wolfIP_recv_ex(dev->stack, dev->wg_if_idx, (void *)plaintext, @@ -514,7 +529,7 @@ static void wg_handle_initiation(struct wg_device *dev, const uint8_t *data, /* Add MACs to response */ mac_off = offsetof(struct wg_msg_response, macs); - wg_cookie_add_macs(peer, &resp, sizeof(resp), mac_off); + wg_cookie_add_macs(peer, &resp, sizeof(resp), mac_off, dev->now); /* Send response */ memset(&dst, 0, sizeof(dst)); @@ -653,7 +668,7 @@ static void wg_handle_cookie(struct wg_device *dev, const uint8_t *data, if (peer == NULL) return; - wg_cookie_consume_reply(peer, msg); + wg_cookie_consume_reply(peer, msg, dev->now); } /* diff --git a/src/wolfguard/wg_timers.c b/src/wolfguard/wg_timers.c index 9ab0d643..a2926bc0 100644 --- a/src/wolfguard/wg_timers.c +++ b/src/wolfguard/wg_timers.c @@ -129,7 +129,7 @@ void wg_timers_tick(struct wg_device *dev, uint64_t now_ms) if (wg_noise_create_initiation(dev, peer, &msg) == 0) { size_t mac_off = offsetof(struct wg_msg_initiation, macs); - wg_cookie_add_macs(peer, &msg, sizeof(msg), mac_off); + wg_cookie_add_macs(peer, &msg, sizeof(msg), mac_off, now_ms); memset(&dst, 0, sizeof(dst)); dst.sin_family = AF_INET; @@ -180,7 +180,7 @@ void wg_timers_tick(struct wg_device *dev, uint64_t now_ms) if (wg_noise_create_initiation(dev, peer, &msg) == 0) { size_t mac_off = offsetof(struct wg_msg_initiation, macs); - wg_cookie_add_macs(peer, &msg, sizeof(msg), mac_off); + wg_cookie_add_macs(peer, &msg, sizeof(msg), mac_off, now_ms); memset(&dst, 0, sizeof(dst)); dst.sin_family = AF_INET; @@ -222,7 +222,7 @@ void wg_timers_tick(struct wg_device *dev, uint64_t now_ms) if (wg_noise_create_initiation(dev, peer, &msg) == 0) { size_t mac_off = offsetof(struct wg_msg_initiation, macs); - wg_cookie_add_macs(peer, &msg, sizeof(msg), mac_off); + wg_cookie_add_macs(peer, &msg, sizeof(msg), mac_off, now_ms); memset(&dst, 0, sizeof(dst)); dst.sin_family = AF_INET; diff --git a/src/wolfguard/wolfguard.c b/src/wolfguard/wolfguard.c index 7390e54b..d68f526a 100644 --- a/src/wolfguard/wolfguard.c +++ b/src/wolfguard/wolfguard.c @@ -164,6 +164,7 @@ int wolfguard_set_private_key(struct wg_device *dev, const uint8_t *private_key) { int ret; + int i; memcpy(dev->static_private, private_key, WG_PRIVATE_KEY_LEN); @@ -176,6 +177,36 @@ int wolfguard_set_private_key(struct wg_device *dev, /* Re-initialize cookie checker with new public key */ wg_cookie_checker_init(&dev->cookie_checker, dev->static_public); + for (i = 0; i < WOLFGUARD_MAX_PEERS; i++) { + struct wg_peer *peer = &dev->peers[i]; + + if (!peer->is_active) + continue; + + /* Drop the live session keypairs. */ + wg_memzero(&peer->keypairs.keypair_slots, + sizeof(peer->keypairs.keypair_slots)); + peer->keypairs.current = NULL; + peer->keypairs.previous = NULL; + peer->keypairs.next = NULL; + + /* Drop plaintext queued under the old identity. */ + wg_memzero(peer->staged_packets, sizeof(peer->staged_packets)); + memset(peer->staged_packet_lens, 0, sizeof(peer->staged_packet_lens)); + peer->staged_count = 0; + + /* Re-init the handshake with the new static key, preserving the PSK. + * This also recomputes precomputed_static_static so future handshakes + * use the rotated identity. */ + { + uint8_t psk[WG_SYMMETRIC_KEY_LEN]; + memcpy(psk, peer->handshake.preshared_key, WG_SYMMETRIC_KEY_LEN); + wg_noise_handshake_init(&peer->handshake, dev->static_private, + peer->public_key, psk, &dev->rng); + wg_memzero(psk, sizeof(psk)); + } + } + return 0; } diff --git a/src/wolfguard/wolfguard.h b/src/wolfguard/wolfguard.h index 8ae48d9f..ed6df503 100644 --- a/src/wolfguard/wolfguard.h +++ b/src/wolfguard/wolfguard.h @@ -412,7 +412,7 @@ void wg_cookie_init(struct wg_cookie *cookie, const uint8_t *peer_public_key); int wg_cookie_add_macs(struct wg_peer *peer, void *msg, size_t msg_len, - size_t mac_offset); + size_t mac_offset, uint64_t now); enum wg_cookie_mac_state wg_cookie_validate( struct wg_cookie_checker *checker, void *msg, size_t msg_len, @@ -423,7 +423,8 @@ int wg_cookie_create_reply(struct wg_device *dev, struct wg_msg_cookie *reply, uint32_t sender_index, uint32_t src_ip, uint16_t src_port); -int wg_cookie_consume_reply(struct wg_peer *peer, struct wg_msg_cookie *msg); +int wg_cookie_consume_reply(struct wg_peer *peer, struct wg_msg_cookie *msg, + uint64_t now); /* * Allowed IPs, implemented in wg_allowedips.c diff --git a/src/wolfip.c b/src/wolfip.c index 153fe375..cd4bd124 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -798,6 +798,12 @@ struct wolfIP_mcast_membership { ip4 group; uint8_t if_idx; uint8_t refs; + /* RFC 3376 §5.2: a query response is deferred by a random delay rather + * than sent synchronously. tmr_report is the pending report timer (or + * NO_TIMER when none is scheduled); S is the owning stack, needed because + * the timer callback only receives this membership as its argument. */ + uint32_t tmr_report; + struct wolfIP *S; }; #endif @@ -1066,6 +1072,8 @@ static int wolfIP_filter_notify_icmp(enum wolfIP_filter_reason reason, #ifndef DHCP_REQUEST_RETRIES #define DHCP_REQUEST_RETRIES 3 #endif +/* RFC 2131 §4.1: retransmission delay doubles each attempt up to a 64s ceiling. */ +#define DHCP_BACKOFF_MAX_MS 64000U enum dhcp_state { DHCP_OFF = 0, @@ -1178,6 +1186,7 @@ struct tsocket { uint8_t if_idx; uint8_t recv_ttl; uint8_t last_pkt_ttl; + uint8_t close_notify_pending; /* slot reserved for a final CB_EVENT_CLOSED */ uint8_t rxmem[RXBUF_SIZE]; uint8_t txmem[TXBUF_SIZE]; tsocket_cb callback; @@ -1313,6 +1322,9 @@ struct arp_pending_entry { static int arp_lookup(struct wolfIP *s, unsigned int if_idx, ip4 ip, uint8_t *mac); static void arp_request(struct wolfIP *s, unsigned int if_idx, ip4 tip); +static void arp_store_neighbor(struct wolfIP *s, unsigned int if_idx, ip4 ip, + const uint8_t *mac); +static int arp_neighbor_index(struct wolfIP *s, unsigned int if_idx, ip4 ip); #if WOLFIP_ENABLE_FORWARDING static void wolfIP_forward_packet(struct wolfIP *s, unsigned int out_if, struct wolfIP_ip_packet *ip, uint32_t len, @@ -2180,6 +2192,18 @@ static void wolfIP_send_ttl_exceeded(struct wolfIP *s, unsigned int if_idx, return; 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 + * 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. */ + 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 || + orig_type == 5 /* Redirect */ || orig_type == ICMP_TTL_EXCEEDED || + orig_type == 12 /* Parameter Problem */) + return; + } orig_copy = orig_ihl + 8; if (orig_copy > TTL_EXCEEDED_ORIG_PACKET_SIZE_MAX) orig_copy = TTL_EXCEEDED_ORIG_PACKET_SIZE_MAX; @@ -3019,6 +3043,13 @@ static void tcp_parse_options(const struct wolfIP_tcp_seg *tcp, uint32_t frame_l memcpy(&mss, opt + 2, sizeof(mss)); mss = ee16(mss); if (mss > 0) { + /* RFC 9293 §3.7.1: IPv4 default MSS is 536. Floor the + * advertised value so a peer cannot drag peer_mss below it + * and coerce us into tiny segments (small-MSS DoS + * amplification). Symmetric with the ICMP PTB floor in + * icmp_try_deliver_tcp_error(). */ + if (mss < TCP_DEFAULT_MSS) + mss = TCP_DEFAULT_MSS; po->mss = mss; po->mss_found = 1; } @@ -4179,6 +4210,42 @@ static int igmp_send_report(struct wolfIP *s, unsigned int if_idx, ip4 group, return wolfIP_ll_send_frame(s, if_idx, frame, sizeof(frame)); } +/* RFC 3376 §4.1.1: decode the Max Resp Code byte of a query into a Maximum + * Response Time in milliseconds. Values < 128 are a direct count of tenths of + * a second (this also covers an IGMPv2 query, whose byte is the Max Resp Time + * directly, and an IGMPv1 query, whose byte is 0). Values >= 128 carry a + * floating-point mantissa/exponent: mant|0x10 shifted left by exp+3 tenths. */ +static uint32_t igmp_max_resp_ms(uint8_t code) +{ + uint32_t tenths; + + if (code < 128) { + tenths = code; + } else { + uint32_t mant = code & 0x0FU; + uint32_t exp = (code >> 4) & 0x07U; + tenths = (mant | 0x10U) << (exp + 3); + } + return tenths * 100U; +} + +/* Timer callback: the random response delay for a membership has elapsed, so + * emit the deferred Current-State Report. arg is the membership; it carries a + * back-pointer to the owning stack because the timer API passes only one arg. + * A membership released during the delay window (refs == 0) is skipped; its + * timer is cancelled at drop time, so this is belt-and-suspenders. */ +static void igmp_report_timer_cb(void *arg) +{ + struct wolfIP_mcast_membership *m = (struct wolfIP_mcast_membership *)arg; + + if (!m) + return; + m->tmr_report = NO_TIMER; + if (!m->S || m->refs == 0) + return; + (void)igmp_send_report(m->S, m->if_idx, m->group, IGMPV3_REC_MODE_IS_EXCLUDE); +} + static void igmp_input(struct wolfIP *s, unsigned int if_idx, struct wolfIP_ip_packet *ip, uint32_t frame_len) { @@ -4222,13 +4289,35 @@ static void igmp_input(struct wolfIP *s, unsigned int if_idx, return; } - for (i = 0; i < WOLFIP_MCAST_MEMBERSHIPS; i++) { - if (s->mcast[i].refs == 0 || s->mcast[i].if_idx != if_idx) - continue; - if (group != IPADDR_ANY && group != s->mcast[i].group) - continue; - (void)igmp_send_report(s, if_idx, s->mcast[i].group, - IGMPV3_REC_MODE_IS_EXCLUDE); + /* RFC 3376 §5.2: do not answer synchronously. Schedule a Current-State + * Report after a delay drawn uniformly from (0, Max Resp Time], so many + * hosts answering one query do not implode, and so an attacker spraying + * queries cannot force one report per query out of a constrained host. */ + { + uint32_t max_ms = igmp_max_resp_ms(igmp[1]); + + for (i = 0; i < WOLFIP_MCAST_MEMBERSHIPS; i++) { + struct wolfIP_timer tmr = {0}; + uint32_t delay; + + if (s->mcast[i].refs == 0 || s->mcast[i].if_idx != if_idx) + continue; + if (group != IPADDR_ANY && group != s->mcast[i].group) + continue; + /* §5.2 rule 1: a query arriving while a response is already pending + * for this membership schedules nothing further. This coalesces a + * query flood into a single deferred report per group. */ + if (s->mcast[i].tmr_report != NO_TIMER) + continue; + /* Floor at 1 ms: a zero window (IGMPv1 query) still fires on the + * next poll, and expires must stay non-zero because the timer heap + * treats expires == 0 as a cancelled slot. */ + delay = max_ms ? (wolfIP_getrandom() % max_ms) + 1U : 1U; + tmr.expires = s->last_tick + delay; + tmr.arg = &s->mcast[i]; + tmr.cb = igmp_report_timer_cb; + s->mcast[i].tmr_report = timers_binheap_insert(&s->timers, tmr); + } } } #endif @@ -5117,6 +5206,13 @@ static void tcp_input(struct wolfIP *S, unsigned int if_idx, t->sock.tcp.snd_una = t->sock.tcp.seq; t->dst_port = ee16(tcp->src_port); t->remote_ip = ee32(tcp->ip.src); + { + unsigned int nh_if = if_idx; + ip4 nh = wolfIP_select_nexthop_ex(S, &nh_if, t->remote_ip); + if (!wolfIP_ll_is_non_ethernet(S, nh_if) && + arp_neighbor_index(S, nh_if, nh) < 0) + arp_store_neighbor(S, nh_if, nh, tcp->ip.eth.src); + } t->events |= CB_EVENT_READABLE; /* Keep flag until application calls accept */ tcp_process_ts(t, tcp, frame_len); tcp_send_syn(t, TCP_FLAG_SYN | TCP_FLAG_ACK); @@ -5569,6 +5665,10 @@ static struct pkt_desc *tcp_find_pending_retrans(struct tsocket *ts, struct pkt_ static void close_socket(struct tsocket *ts) { + tsocket_cb cb; + void *cb_arg; + struct wolfIP *S; + int notify; if (!ts) return; if (ts->proto == WI_IPPROTO_TCP) { @@ -5582,7 +5682,25 @@ static void close_socket(struct tsocket *ts) if (ts->proto == WI_IPPROTO_UDP) udp_mcast_drop_all(ts); #endif + /* An armed callback on an involuntarily torn-down TCP socket (RST, + * FIN_WAIT_2 / control RTO timeout, final ACK in LAST_ACK) is a waiter + * blocked on CB_EVENT_CLOSED. Callbacks may only run from wolfIP_poll() + * Step 3, so reserve the slot and defer one final CB_EVENT_CLOSED for the + * dispatcher to deliver and reap. App-initiated closes disarm the callback + * first, so they take the plain teardown path. */ + notify = (ts->proto == WI_IPPROTO_TCP) && (ts->callback != NULL); + cb = ts->callback; + cb_arg = ts->callback_arg; + S = ts->S; memset(ts, 0, sizeof(struct tsocket)); + if (notify) { + ts->S = S; + ts->proto = WI_IPPROTO_TCP; + ts->callback = cb; + ts->callback_arg = cb_arg; + ts->events = CB_EVENT_CLOSED; + ts->close_notify_pending = 1; + } } #if WOLFIP_RAWSOCKETS @@ -6437,7 +6555,17 @@ int wolfIP_sock_recvfrom(struct wolfIP *s, int sockfd, void *buf, size_t len, in tcp_send_ack(ts); } return ret; - } else { /* Not established */ + } else if (ts->sock.tcp.state == TCP_CLOSED) { + /* Torn-down stream (peer RST, abortive close, or reaped after + * teardown): drain any bytes still queued, then report EOF (0) + * instead of a bare -1. can_read() already advertises a CLOSED + * socket as readable, and the FreeRTOS BSD recv() wrapper only + * blocks while can_read()==0 -- so a -1 here surfaces as a + * spurious "recv failed sock_err=1" rather than end-of-stream. */ + if (queue_len(&ts->sock.tcp.rxbuf) == 0) + return 0; + return queue_pop(&ts->sock.tcp.rxbuf, buf, len); + } else { /* Not established (LISTEN / SYN_SENT / SYN_RCVD / closing) */ return -1; } } else if (IS_SOCKET_UDP(sockfd)) { @@ -6662,6 +6790,8 @@ static int udp_mcast_join(struct wolfIP *s, struct tsocket *ts, ip4 group, m = &s->mcast[j]; m->group = group; m->if_idx = (uint8_t)if_idx; + m->tmr_report = NO_TIMER; + m->S = s; break; } } @@ -6700,6 +6830,10 @@ static int udp_mcast_drop(struct wolfIP *s, struct tsocket *ts, ip4 group, if (m && m->refs > 0) { m->refs--; if (m->refs == 0) { + /* Cancel any deferred query response before the slot is zeroed, + * else its timer would fire into a freed membership. */ + if (m->tmr_report != NO_TIMER) + timer_binheap_cancel(&s->timers, m->tmr_report); (void)igmp_send_report(s, if_idx, group, IGMPV3_REC_CHANGE_TO_INCLUDE); memset(m, 0, sizeof(*m)); @@ -7001,12 +7135,16 @@ int wolfIP_sock_close(struct wolfIP *s, int sockfd) ts->sock.tcp.state = TCP_FIN_WAIT_1; ts->sock.tcp.ctrl_rto_retries = 0; tcp_ctrl_rto_start(ts, s->last_tick); + ts->callback = NULL; + ts->callback_arg = NULL; return -WOLFIP_EAGAIN; } else if (ts->sock.tcp.state == TCP_LISTEN) { ts->sock.tcp.state = TCP_CLOSED; (void)wolfIP_filter_notify_socket_event( WOLFIP_FILT_STOP_LISTENING, s, ts, ts->local_ip, ts->src_port, IPADDR_ANY, 0); + ts->callback = NULL; + ts->callback_arg = NULL; close_socket(ts); return 0; } else if (ts->sock.tcp.state == TCP_CLOSE_WAIT) { @@ -7015,6 +7153,8 @@ int wolfIP_sock_close(struct wolfIP *s, int sockfd) ts->sock.tcp.state = TCP_LAST_ACK; ts->sock.tcp.ctrl_rto_retries = 0; tcp_ctrl_rto_start(ts, s->last_tick); + ts->callback = NULL; + ts->callback_arg = NULL; return -WOLFIP_EAGAIN; } else if (ts->sock.tcp.state == TCP_CLOSING) { return -WOLFIP_EAGAIN; @@ -7026,6 +7166,8 @@ int wolfIP_sock_close(struct wolfIP *s, int sockfd) (void)wolfIP_filter_notify_socket_event( WOLFIP_FILT_CLOSED, s, ts, ts->local_ip, ts->src_port, ts->remote_ip, ts->dst_port); + ts->callback = NULL; + ts->callback_arg = NULL; close_socket(ts); return 0; } else return -1; @@ -7625,13 +7767,30 @@ static void dhcp_schedule_timer_at(struct wolfIP *s, uint64_t when) s->dhcp_timer = timers_binheap_insert(&s->timers, tmr); } +/* Exponential-backoff retransmission delay: double the base timeout for each + * prior attempt (dhcp_timeout_count), saturating at DHCP_BACKOFF_MAX_MS, plus + * the existing small jitter. The shift is clamped first because renew/rebind do + * not cap dhcp_timeout_count, so it can grow past the point of UB. */ +static uint64_t dhcp_backoff_delay(const struct wolfIP *s, uint32_t base_ms) +{ + uint32_t count = s ? s->dhcp_timeout_count : 0; + uint64_t delay; + + if (count > 16) + count = 16; + delay = (uint64_t)base_ms << count; + if (delay > DHCP_BACKOFF_MAX_MS) + delay = DHCP_BACKOFF_MAX_MS; + return delay + (wolfIP_getrandom() % 200U); +} + static void dhcp_schedule_retry_timer(struct wolfIP *s, uint64_t deadline) { uint64_t next; if (!s) return; - next = s->last_tick + DHCP_REQUEST_TIMEOUT + (wolfIP_getrandom() % 200U); + next = s->last_tick + dhcp_backoff_delay(s, DHCP_REQUEST_TIMEOUT); if (deadline != 0 && next > deadline) next = deadline; dhcp_schedule_timer_at(s, next); @@ -8303,7 +8462,7 @@ static int dhcp_send_discover(struct wolfIP *s) dhcp_schedule_timer_at(s, retry_at); return ret; } - dhcp_schedule_timer_at(s, s->last_tick + DHCP_DISCOVER_TIMEOUT + (wolfIP_getrandom() % 200U)); + dhcp_schedule_timer_at(s, s->last_tick + dhcp_backoff_delay(s, DHCP_DISCOVER_TIMEOUT)); s->dhcp_state = DHCP_DISCOVER_SENT; return 0; } @@ -9895,11 +10054,27 @@ int wolfIP_poll(struct wolfIP *s, uint64_t now) /* Step 3: handle DHCP and application callbacks */ for (i = 0; i < MAX_TCPSOCKETS; i++) { struct tsocket *ts = &s->tcpsockets[i]; + + /* close_socket() deferred a final CB_EVENT_CLOSED (involuntary teardown + * that ran close_socket() directly from the RX path, e.g. a RST). Reap + * the slot first so the callback may safely re-enter the stack, then + * deliver the saved event exactly once. */ + if (ts->close_notify_pending) { + tsocket_cb cb = ts->callback; + void *cb_arg = ts->callback_arg; + uint16_t events = ts->events; + memset(ts, 0, sizeof(struct tsocket)); + if (cb) + cb(i | MARK_TCP_SOCKET, events, cb_arg); + continue; + } + if ((ts->callback == NULL) || (ts->events == 0)) continue; + /* A socket the RX path moved to TCP_CLOSED is dispatched here only when - * it deferred a CB_EVENT_CLOSED for delivery on this shallow stack - * (LAST_ACK final ACK, or RST on a half-open accepted socket); the + * it deferred a CB_EVENT_CLOSED inline for delivery on this shallow + * stack (LAST_ACK final ACK, or RST on a half-open accepted socket); the * teardown was deferred too so the callback would not run deep in * packet processing. Any other TCP_CLOSED socket is left alone. */ if ((ts->sock.tcp.state == TCP_CLOSED) && !(ts->events & CB_EVENT_CLOSED)) @@ -9910,10 +10085,15 @@ int wolfIP_poll(struct wolfIP *s, uint64_t now) ts->callback(i | MARK_TCP_SOCKET, events, ts->callback_arg); } /* Now that CB_EVENT_CLOSED has been delivered, reap the deferred-close - * socket. A socket closed elsewhere is already memset (callback NULL) - * and never reaches this branch. */ - if (ts->sock.tcp.state == TCP_CLOSED) + * socket. Disarm the callback first so close_socket() takes the plain + * teardown path instead of re-deferring (it re-arms close_notify_pending + * whenever a TCP socket still has a callback). A socket closed elsewhere + * is already memset (callback NULL) and never reaches this branch. */ + if (ts->sock.tcp.state == TCP_CLOSED) { + ts->callback = NULL; + ts->callback_arg = NULL; close_socket(ts); + } } for (i = 0; i < MAX_UDPSOCKETS; i++) { struct tsocket *ts = &s->udpsockets[i]; diff --git a/tools/scripts/run-m33mu-ci-in-container.sh b/tools/scripts/run-m33mu-ci-in-container.sh index 68824603..465115c2 100755 --- a/tools/scripts/run-m33mu-ci-in-container.sh +++ b/tools/scripts/run-m33mu-ci-in-container.sh @@ -410,6 +410,8 @@ job_echo_freertos() { echo "==> Running stm32h563_m33mu_echo_freertos" trap cleanup_runtime EXIT setup_tap_and_dnsmasq + tcpdump -i tap0 -nn -s0 -U -w /tmp/echo.pcap > /tmp/tcpdump.log 2>&1 & + printf '%s\n' "$!" > /tmp/tcpdump.pid start_m33mu 180 --quit-on-faults local ip ip="$(wait_for_lease 90)"