diff --git a/src/supplicant/eap_peap.h b/src/supplicant/eap_peap.h index b4bb02a3..437699c3 100644 --- a/src/supplicant/eap_peap.h +++ b/src/supplicant/eap_peap.h @@ -63,9 +63,9 @@ int eap_peap_build_mschapv2_response(uint8_t *out, size_t out_cap, size_t username_len, size_t *out_len); -/* Build the trivial inner EAP-Response/MSCHAPv2 Success ack: 6 bytes, - * [Code=Resp, id, length=6 BE, type=26, opcode=Success] - * sent in reply to the server's "S=..." Success Request. +/* Build the 2-byte compressed inner MSCHAPv2 Success acknowledgment: + * [type=26, opcode=Success], sent in reply to the server's Success + * Request. eap_id is unused because PEAPv0 omits the inner EAP header. */ int eap_peap_build_mschapv2_ack(uint8_t *out, size_t out_cap, uint8_t eap_id, diff --git a/src/supplicant/mschapv2.h b/src/supplicant/mschapv2.h index 25cf7cb8..58e0a647 100644 --- a/src/supplicant/mschapv2.h +++ b/src/supplicant/mschapv2.h @@ -95,13 +95,13 @@ int mschapv2_verify_authenticator_response( /* Derive the 64-byte EAP-MSCHAPv2 MSK per RFC 3079. * MasterKey = SHA1(PasswordHashHash || NTResponse || MagicConstant1) - * SendKey16 = GetAsymmetricStartKey(MasterKey, 16, server-to-client) - * RecvKey16 = GetAsymmetricStartKey(MasterKey, 16, client-to-server) - * MSK = SendKey16 || RecvKey16 || 32 zero bytes (per RFC 3748) + * SendKey16 = GetAsymmetricStartKey(MasterKey, 16, client-to-server) + * RecvKey16 = GetAsymmetricStartKey(MasterKey, 16, server-to-client) + * MSK = RecvKey16 || SendKey16 || 32 zero bytes (per RFC 3748) * - * Note RFC 3748 sec.7.10 specifies how the EAP MSK is built from - * MSCHAPv2 keys; we follow the "client" perspective: send = MS-MPPE- - * Recv-Key, recv = MS-MPPE-Send-Key, then 32 zero bytes. + * RFC 3748 sec.7.10 builds the EAP MSK from the MSCHAPv2 keys. From the + * client perspective, client send uses the client-to-server key and + * client receive uses the server-to-client key. */ int mschapv2_derive_msk(const char *password, size_t pw_len, const uint8_t nt_response[MSCHAPV2_NT_RESPONSE_LEN], diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index b61f3ce1..c55060f2 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -184,6 +184,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_filter_notify_tcp_metadata); tcase_add_test(tc_utils, test_filter_notify_udp_ihl_options_metadata); tcase_add_test(tc_utils, test_filter_notify_udp_ihl_truncated_no_overread); + tcase_add_test(tc_utils, test_filter_notify_raw_tx_eth_header_built); tcase_add_test(tc_utils, test_filter_dispatch_no_callback); tcase_add_test(tc_utils, test_filter_dispatch_mask_not_set); tcase_add_test(tc_utils, test_filter_fresh_callback_consulted_before_mask_configured); @@ -210,6 +211,9 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_sock_bind_tcp_filter_blocks); tcase_add_test(tc_utils, test_sock_bind_tcp_port_collision_rejected); tcase_add_test(tc_utils, test_sock_bind_udp_src_port_nonzero); + tcase_add_test(tc_utils, test_udp_auto_port_skips_in_use); + tcase_add_test(tc_utils, test_port_alloc_walks_past_long_collision_run); + tcase_add_test(tc_utils, test_port_alloc_returns_zero_when_range_exhausted); tcase_add_test(tc_utils, test_sock_bind_udp_filter_blocks); tcase_add_test(tc_utils, test_sock_bind_icmp_success); tcase_add_test(tc_utils, test_sock_connect_wrong_family); @@ -279,6 +283,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_ip_recv_drops_zero_source); tcase_add_test(tc_utils, test_arp_recv_rejects_broadcast_sender); tcase_add_test(tc_utils, test_arp_recv_rejects_multicast_sender); + tcase_add_test(tc_utils, test_arp_recv_unconfigured_if_does_not_answer); tcase_add_test(tc_utils, test_dhcp_ack_rejects_mismatched_server_id); tcase_add_test(tc_utils, test_udp_no_icmp_unreachable_for_broadcast_src); tcase_add_test(tc_utils, test_udp_no_icmp_unreachable_for_multicast_src); @@ -535,6 +540,7 @@ Suite *wolf_suite(void) 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_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); tcase_add_test(tc_utils, test_dns_resend_query_uses_stored_query_buffer); tcase_add_test(tc_utils, test_dns_resend_query_fails_without_valid_socket); @@ -608,6 +614,8 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_tcp_listener_preaccept_accept_reverts_port); tcase_add_test(tc_utils, test_tcp_listener_preaccept_timeout_reverts_port); tcase_add_test(tc_utils, test_tcp_listener_preaccept_revert_drains_connection_state); + tcase_add_test(tc_utils, test_tcp_listener_preaccept_close_rto_retransmits_finack); + tcase_add_test(tc_utils, test_tcp_fin_in_close_wait_does_not_advance_ack); tcase_add_test(tc_utils, test_tcp_listener_revert_restores_option_baseline); tcase_add_test(tc_utils, test_tcp_parse_sack_wraparound_block_accepted); tcase_add_test(tc_utils, test_tcp_parse_options_stops_on_truncated_or_invalid_option_length); @@ -710,6 +718,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_tcp_process_ts_no_ecr); tcase_add_test(tc_utils, test_tcp_process_ts_future_ecr_rejected); tcase_add_test(tc_utils, test_tcp_process_ts_ooo_segment_keeps_recent); + tcase_add_test(tc_utils, test_tcp_process_ts_recent_compare_host_order); tcase_add_test(tc_utils, test_tcp_input_paws_ooo_does_not_poison_hole_fill); tcase_add_test(tc_utils, test_tcp_process_ts_updates_rtt_when_set); tcase_add_test(tc_utils, test_tcp_send_syn_advertises_sack_permitted); @@ -1471,8 +1480,12 @@ Suite *wolf_suite(void) tcase_add_test(tc_core, test_dhcp_parse_ack_inner_pad_bytes_skipped); tcase_add_test(tc_core, test_dhcp_dad_probe_wire_format); tcase_add_test(tc_core, test_dhcp_dad_conflict_releases_and_rediscover); + tcase_add_test(tc_core, test_dhcp_dad_request_claiming_candidate_conflict); + tcase_add_test(tc_core, test_dhcp_dad_probe_for_candidate_conflict); + tcase_add_test(tc_core, test_dhcp_dad_garp_announcement_conflict); tcase_add_test(tc_core, test_dhcp_dad_own_mac_reply_ignored); tcase_add_test(tc_core, test_dhcp_dad_reply_for_other_ip_ignored); + tcase_add_test(tc_core, test_dhcp_dad_reply_on_unconfigured_secondary_ignored); tcase_add_test(tc_core, test_dhcp_dad_single_dhcp_timer_in_heap); tcase_add_test(tc_core, test_dhcp_dad_probe_count_len_returning_driver); tcase_add_test(tc_core, test_dhcp_decline_wire_format); @@ -1667,6 +1680,8 @@ Suite *wolf_suite(void) tcase_add_test(tc_core, test_wolfip_packetsocket_from_fd_negative_fd); #endif /* WOLFIP_PACKET_SOCKETS */ tcase_add_test(tc_core, test_bind_port_in_use_different_ips_no_collision); + tcase_add_test(tc_core, test_tcp_ctrl_rto_start_no_timer_does_not_set_active); + tcase_add_test(tc_core, test_tcp_ctrl_rto_start_rearm_failure_clears_active); #if WOLFIP_VLAN /* --- unit_tests_vlan.c (30 tests for 802.1Q support) --- */ diff --git a/src/test/unit/unit_tests_api.c b/src/test/unit/unit_tests_api.c index 16513a39..8f6a894f 100644 --- a/src/test/unit/unit_tests_api.c +++ b/src/test/unit/unit_tests_api.c @@ -406,6 +406,53 @@ START_TEST(test_filter_notify_udp_ihl_truncated_no_overread) } END_TEST +START_TEST(test_filter_notify_raw_tx_eth_header_built) +{ + struct wolfIP s; + struct wolfIP_sockaddr_in dst; + uint8_t payload[4] = {0xDE, 0xAD, 0xBE, 0xEF}; + uint8_t bcast_mac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + int raw_sd; + int ret; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + + filter_cb_calls = 0; + memset(&filter_last_event, 0, sizeof(filter_last_event)); + wolfIP_filter_set_callback(test_filter_cb, NULL); + wolfIP_filter_set_mask(WOLFIP_FILT_MASK(WOLFIP_FILT_SENDING)); + + raw_sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_RAW, WI_IPPROTO_UDP); + ck_assert_int_ge(raw_sd, 0); + + /* Limited broadcast: the nexthop MAC is all-ones, so the flush + * path needs no ARP entry. */ + memset(&dst, 0, sizeof(dst)); + dst.sin_family = AF_INET; + dst.sin_addr.s_addr = ee32(0xFFFFFFFFU); + + ret = wolfIP_sock_sendto(&s, raw_sd, payload, sizeof(payload), 0, + (struct wolfIP_sockaddr *)&dst, sizeof(dst)); + ck_assert_int_eq(ret, (int)sizeof(payload)); + + (void)wolfIP_poll(&s, 0); + + wolfIP_filter_set_callback(NULL, NULL); + wolfIP_sock_close(&s, raw_sd); + + /* The eth event must carry the header the frame was actually sent + * with: all-ones destination, the interface MAC as source. */ + ck_assert_int_ge(filter_cb_calls, 1); + ck_assert_uint_eq(filter_last_event.meta.ip_proto, + WOLFIP_FILTER_PROTO_ETH); + ck_assert_int_eq(memcmp(filter_last_event.meta.dst_mac, bcast_mac, 6), 0); + ck_assert_int_eq(memcmp(filter_last_event.meta.src_mac, + wolfIP_ll_at(&s, TEST_PRIMARY_IF)->mac, 6), 0); +} +END_TEST + START_TEST(test_filter_dispatch_no_callback) { @@ -1460,6 +1507,95 @@ START_TEST(test_sock_bind_udp_src_port_nonzero) } END_TEST +/* An auto-assigned UDP source port must not collide with a port already + * bound by another socket: the allocator must skip in-use ports. With the + * RNG pinned to 5000 (the bound port), the fix walks forward to 5001. */ +START_TEST(test_udp_auto_port_skips_in_use) +{ + struct wolfIP s; + int udp_sd1, udp_sd2; + struct tsocket *ts2; + struct wolfIP_sockaddr_in sin; + const char payload[] = "test"; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + + /* Bind the first UDP socket to port 5000. */ + udp_sd1 = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + ck_assert_int_gt(udp_sd1, 0); + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = ee16(5000); + sin.sin_addr.s_addr = ee32(0x0A000001U); + ck_assert_int_eq(wolfIP_sock_bind(&s, udp_sd1, + (struct wolfIP_sockaddr *)&sin, sizeof(sin)), 0); + + /* Pin the RNG to 5000 (the bound port); the auto allocator must walk + * forward to 5001 instead of colliding. */ + test_rand_override_enabled = 1; + test_rand_override_value = 5000U; + + /* Create a second UDP socket and sendto (auto-assigns a source port). */ + udp_sd2 = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + ck_assert_int_gt(udp_sd2, 0); + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = ee16(9999); + sin.sin_addr.s_addr = ee32(0x0A000002U); + ck_assert_int_ge(wolfIP_sock_sendto(&s, udp_sd2, payload, sizeof(payload), 0, + (const struct wolfIP_sockaddr *)&sin, + sizeof(sin)), 0); + test_rand_override_enabled = 0; + + /* The auto port must be 5001 (not the bound 5000). */ + ts2 = &s.udpsockets[SOCKET_UNMARK(udp_sd2)]; + ck_assert_uint_eq(ts2->src_port, 5001U); +} +END_TEST + +/* The allocator walks the whole min_port..65535 range: a collision run + * longer than the old 16-try limit must be skipped, not returned. With the + * RNG pinned to the run start, the old loop stopped after 16 tries and + * returned an in-use port. */ +START_TEST(test_port_alloc_walks_past_long_collision_run) +{ + static struct tsocket arr[18]; + uint16_t port; + int i; + + memset(arr, 0, sizeof(arr)); + /* 17 consecutive ports in use, starting at the pinned RNG start. */ + for (i = 0; i < 17; i++) + arr[i].src_port = (uint16_t)(1024 + i); + test_rand_override_enabled = 1; + test_rand_override_value = 1024U; + port = port_alloc_random(arr, 18, &arr[17], IPADDR_ANY, 1024); + test_rand_override_enabled = 0; + ck_assert_uint_eq(port, 1041U); + ck_assert_int_eq(bind_port_in_use(arr, 18, &arr[17], IPADDR_ANY, port), 0); +} +END_TEST + +/* When the candidate range holds no free port the allocator returns 0 + * instead of a collided value: callers treat 0 as allocation failure. */ +START_TEST(test_port_alloc_returns_zero_when_range_exhausted) +{ + static struct tsocket arr[2]; + uint16_t port; + + memset(arr, 0, sizeof(arr)); + /* The only candidate (min_port == 65535) is already claimed. */ + arr[0].src_port = 65535; + test_rand_override_enabled = 1; + test_rand_override_value = 65535U; + port = port_alloc_random(arr, 2, &arr[1], IPADDR_ANY, 65535); + test_rand_override_enabled = 0; + ck_assert_uint_eq(port, 0U); +} +END_TEST + START_TEST(test_sock_bind_udp_filter_blocks) { struct wolfIP s; @@ -4729,6 +4865,71 @@ START_TEST(test_arp_recv_rejects_multicast_sender) } END_TEST +/* An unconfigured interface (no assigned address) must not answer ARP + * requests: matching the target against a zero conf->ip let a request + * for 0.0.0.0 be answered by advertising 0.0.0.0 as the sender protocol + * address. */ +START_TEST(test_arp_recv_unconfigured_if_does_not_answer) +{ + struct wolfIP s; + struct arp_packet arp; + struct wolfIP_ll_dev *ll; + struct ipconf *conf; + static const uint8_t fake_mac[6] = {0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x03}; + uint32_t frames_before; + + wolfIP_init(&s); + mock_link_init(&s); + mock_link_init_idx(&s, TEST_SECOND_IF, NULL); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + + conf = wolfIP_ipconf_at(&s, TEST_SECOND_IF); + ck_assert_uint_eq(conf->ip, IPADDR_ANY); + + /* A request for 0.0.0.0 on the unconfigured secondary: no reply. */ + ll = wolfIP_getdev_ex(&s, TEST_SECOND_IF); + memset(&arp, 0, sizeof(arp)); + memcpy(arp.eth.dst, ll->mac, 6); + memcpy(arp.eth.src, fake_mac, 6); + arp.eth.type = ee16(ETH_TYPE_ARP); + arp.htype = ee16(1); + arp.ptype = ee16(0x0800); + arp.hlen = 6; + arp.plen = 4; + arp.opcode = ee16(ARP_REQUEST); + memcpy(arp.sma, fake_mac, 6); + arp.sip = ee32(0x0A000002U); + memset(arp.tma, 0, 6); + arp.tip = ee32(IPADDR_ANY); + + frames_before = last_frame_sent_count; + arp_recv(&s, TEST_SECOND_IF, &arp, sizeof(arp)); + ck_assert_uint_eq(last_frame_sent_count, frames_before); + + /* Control: a request for the primary's IP on the configured primary + * is still answered. */ + ll = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + conf = wolfIP_ipconf_at(&s, TEST_PRIMARY_IF); + memset(&arp, 0, sizeof(arp)); + memcpy(arp.eth.dst, ll->mac, 6); + memcpy(arp.eth.src, fake_mac, 6); + arp.eth.type = ee16(ETH_TYPE_ARP); + arp.htype = ee16(1); + arp.ptype = ee16(0x0800); + arp.hlen = 6; + arp.plen = 4; + arp.opcode = ee16(ARP_REQUEST); + memcpy(arp.sma, fake_mac, 6); + arp.sip = ee32(0x0A000002U); + memset(arp.tma, 0, 6); + arp.tip = ee32(conf->ip); + + frames_before = last_frame_sent_count; + arp_recv(&s, TEST_PRIMARY_IF, &arp, sizeof(arp)); + ck_assert_uint_eq(last_frame_sent_count, frames_before + 1); +} +END_TEST + /* Regression: arp_recv must reject ARP packets with incorrect hardware or * protocol type fields (htype != 1, ptype != 0x0800, hlen != 6, plen != 4). * Without validation, non-Ethernet/IPv4 ARP packets pollute the cache. */ diff --git a/src/test/unit/unit_tests_dhcp_edges.c b/src/test/unit/unit_tests_dhcp_edges.c index 2a948b4d..23623d66 100644 --- a/src/test/unit/unit_tests_dhcp_edges.c +++ b/src/test/unit/unit_tests_dhcp_edges.c @@ -1689,6 +1689,168 @@ START_TEST(test_dhcp_dad_conflict_releases_and_rediscover) } END_TEST +/* During DAD, a foreign host that uses the candidate as its sender IP (an + * ARP request, not a reply) must be detected as a conflict: a host that + * owns the candidate can evade DAD by not answering our probe, but betrays + * itself by using the address. */ +START_TEST(test_dhcp_dad_request_claiming_candidate_conflict) +{ + struct wolfIP s; + struct dhcp_msg msg; + struct arp_packet req; + struct wolfIP_ll_dev *ll; + struct ipconf *primary; + uint32_t server_ip = 0x0A000001U; + uint32_t client_ip = 0x0A000064U; + uint8_t other_mac[6] = {0x02, 0xAA, 0xBB, 0xCC, 0xDD, 0x04}; + + wolfIP_init(&s); + mock_link_init(&s); + s.dhcp_xid = 0xDA07U; + s.dhcp_state = DHCP_REQUEST_SENT; + s.last_tick = 1000U; + primary = wolfIP_primary_ipconf(&s); + ck_assert_ptr_nonnull(primary); + primary->ip = client_ip; + + build_full_ack(&s, &msg, server_ip, client_ip, 0xFFFFFF00U, + server_ip, 0x08080808U, 120U); + ck_assert_int_eq(dhcp_parse_ack(&s, &msg, sizeof(msg)), 0); + ck_assert_int_eq(s.dhcp_state, DHCP_DAD); + + /* A foreign host using the candidate as its sender IP (request, not + * reply): the DAD must treat it as a conflict, not answer it. */ + ll = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ck_assert_ptr_nonnull(ll); + memset(&req, 0, sizeof(req)); + memcpy(req.eth.dst, ll->mac, 6); + memcpy(req.eth.src, other_mac, 6); + req.eth.type = ee16(ETH_TYPE_ARP); + req.htype = ee16(1); + req.ptype = ee16(0x0800); + req.hlen = 6; + req.plen = 4; + req.opcode = ee16(ARP_REQUEST); + memcpy(req.sma, other_mac, 6); + req.sip = ee32(client_ip); + req.tip = ee32(0x0A000002U); + + arp_recv(&s, TEST_PRIMARY_IF, &req, sizeof(req)); + + /* Conflict detected: DAD aborted, lease released, back to DISCOVER. */ + ck_assert_int_eq(s.dhcp_state, DHCP_DISCOVER_SENT); + ck_assert_uint_eq(s.dhcp_dad_probes, 0U); + ck_assert_uint_eq(primary->ip, 0U); +} +END_TEST + +/* During DAD, a foreign host probing for the candidate (its own DAD: an ARP + * request with sip = 0.0.0.0, tip = candidate) is a conflict: two hosts + * cannot bind the same address. Our own probe is excluded by the MAC check. */ +START_TEST(test_dhcp_dad_probe_for_candidate_conflict) +{ + struct wolfIP s; + struct dhcp_msg msg; + struct arp_packet req; + struct wolfIP_ll_dev *ll; + struct ipconf *primary; + uint32_t server_ip = 0x0A000001U; + uint32_t client_ip = 0x0A000064U; + uint8_t other_mac[6] = {0x02, 0xAA, 0xBB, 0xCC, 0xDD, 0x05}; + + wolfIP_init(&s); + mock_link_init(&s); + s.dhcp_xid = 0xDA08U; + s.dhcp_state = DHCP_REQUEST_SENT; + s.last_tick = 1000U; + primary = wolfIP_primary_ipconf(&s); + ck_assert_ptr_nonnull(primary); + primary->ip = client_ip; + + build_full_ack(&s, &msg, server_ip, client_ip, 0xFFFFFF00U, + server_ip, 0x08080808U, 120U); + ck_assert_int_eq(dhcp_parse_ack(&s, &msg, sizeof(msg)), 0); + ck_assert_int_eq(s.dhcp_state, DHCP_DAD); + + /* A foreign host probing for the candidate (its own DAD): conflict. */ + ll = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ck_assert_ptr_nonnull(ll); + memset(&req, 0, sizeof(req)); + memcpy(req.eth.dst, ll->mac, 6); + memcpy(req.eth.src, other_mac, 6); + req.eth.type = ee16(ETH_TYPE_ARP); + req.htype = ee16(1); + req.ptype = ee16(0x0800); + req.hlen = 6; + req.plen = 4; + req.opcode = ee16(ARP_REQUEST); + memcpy(req.sma, other_mac, 6); + req.sip = ee32(IPADDR_ANY); + req.tip = ee32(client_ip); + + arp_recv(&s, TEST_PRIMARY_IF, &req, sizeof(req)); + + /* Conflict detected: DAD aborted, lease released, back to DISCOVER. */ + ck_assert_int_eq(s.dhcp_state, DHCP_DISCOVER_SENT); + ck_assert_uint_eq(s.dhcp_dad_probes, 0U); + ck_assert_uint_eq(primary->ip, 0U); +} +END_TEST + +/* During DAD, a foreign host announcing the candidate with a gratuitous + * ARP request (sip==tip==candidate) is a conflict: it is using the address, + * which is exactly what DAD must rule out. */ +START_TEST(test_dhcp_dad_garp_announcement_conflict) +{ + struct wolfIP s; + struct dhcp_msg msg; + struct arp_packet req; + struct wolfIP_ll_dev *ll; + struct ipconf *primary; + uint32_t server_ip = 0x0A000001U; + uint32_t client_ip = 0x0A000064U; + uint8_t other_mac[6] = {0x02, 0xAA, 0xBB, 0xCC, 0xDD, 0x06}; + + wolfIP_init(&s); + mock_link_init(&s); + s.dhcp_xid = 0xDA09U; + s.dhcp_state = DHCP_REQUEST_SENT; + s.last_tick = 1000U; + primary = wolfIP_primary_ipconf(&s); + ck_assert_ptr_nonnull(primary); + primary->ip = client_ip; + + build_full_ack(&s, &msg, server_ip, client_ip, 0xFFFFFF00U, + server_ip, 0x08080808U, 120U); + ck_assert_int_eq(dhcp_parse_ack(&s, &msg, sizeof(msg)), 0); + ck_assert_int_eq(s.dhcp_state, DHCP_DAD); + + /* A foreign host announcing the candidate (gratuitous ARP: the request + * carries sip==tip==candidate): conflict. */ + ll = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ck_assert_ptr_nonnull(ll); + memset(&req, 0, sizeof(req)); + memcpy(req.eth.dst, ll->mac, 6); + memcpy(req.eth.src, other_mac, 6); + req.eth.type = ee16(ETH_TYPE_ARP); + req.htype = ee16(1); + req.ptype = ee16(0x0800); + req.hlen = 6; + req.plen = 4; + req.opcode = ee16(ARP_REQUEST); + memcpy(req.sma, other_mac, 6); + req.sip = ee32(client_ip); + req.tip = ee32(client_ip); + + arp_recv(&s, TEST_PRIMARY_IF, &req, sizeof(req)); + + /* Conflict detected: DAD aborted, lease released, back to DISCOVER. */ + ck_assert_int_eq(s.dhcp_state, DHCP_DISCOVER_SENT); + ck_assert_uint_eq(s.dhcp_dad_probes, 0U); + ck_assert_uint_eq(primary->ip, 0U); +} +END_TEST + /* A reply with our own MAC is our own probe looping back: ignored, DAD * continues. */ START_TEST(test_dhcp_dad_own_mac_reply_ignored) @@ -1823,6 +1985,64 @@ START_TEST(test_dhcp_dad_reply_for_other_ip_ignored) } END_TEST +/* During DAD, a spoofed reply (sip = 0.0.0.0) arriving on an unconfigured + * secondary interface must not force a conflict on the primary lease: the + * DAD hook is bound to the probing interface and the recorded candidate, + * not the receiving interface's (zero) address. */ +START_TEST(test_dhcp_dad_reply_on_unconfigured_secondary_ignored) +{ + struct wolfIP s; + struct dhcp_msg msg; + struct arp_packet reply; + struct wolfIP_ll_dev *ll; + struct ipconf *primary; + uint32_t server_ip = 0x0A000001U; + uint32_t client_ip = 0x0A000064U; + uint8_t other_mac[6] = {0x02, 0xAA, 0xBB, 0xCC, 0xDD, 0x03}; + + /* Primary holds the candidate; the secondary is left unconfigured so + * its conf->ip is IPADDR_ANY (the trap the old check fell into). */ + setup_stack_with_two_ifaces(&s, client_ip, 0x0A010001U); + wolfIP_ipconf_at(&s, TEST_SECOND_IF)->ip = IPADDR_ANY; + s.dhcp_xid = 0xDA06U; + s.dhcp_state = DHCP_REQUEST_SENT; + s.last_tick = 1000U; + primary = wolfIP_primary_ipconf(&s); + ck_assert_ptr_nonnull(primary); + primary->ip = client_ip; + + build_full_ack(&s, &msg, server_ip, client_ip, 0xFFFFFF00U, + server_ip, 0x08080808U, 120U); + ck_assert_int_eq(dhcp_parse_ack(&s, &msg, sizeof(msg)), 0); + ck_assert_int_eq(s.dhcp_state, DHCP_DAD); + + /* A spoofed reply with a zero sender IP on the unconfigured secondary: + * the old check (sip == conf->ip) saw 0 == 0 and forced a conflict on + * the primary lease. The DAD hook must stay on the probing interface. */ + ll = wolfIP_getdev_ex(&s, TEST_SECOND_IF); + ck_assert_ptr_nonnull(ll); + memset(&reply, 0, sizeof(reply)); + memcpy(reply.eth.dst, ll->mac, 6); + memcpy(reply.eth.src, other_mac, 6); + reply.eth.type = ee16(ETH_TYPE_ARP); + reply.htype = ee16(1); + reply.ptype = ee16(0x0800); + reply.hlen = 6; + reply.plen = 4; + reply.opcode = ee16(ARP_REPLY); + memcpy(reply.sma, other_mac, 6); + reply.sip = ee32(IPADDR_ANY); + reply.tip = ee32(client_ip); + + arp_recv(&s, TEST_SECOND_IF, &reply, sizeof(reply)); + + /* No conflict: DAD continues, the primary lease is intact. */ + ck_assert_int_eq(s.dhcp_state, DHCP_DAD); + ck_assert_uint_eq(s.dhcp_dad_probes, 1U); + ck_assert_uint_eq(primary->ip, client_ip); +} +END_TEST + /* Real ll drivers (stm32, lpc, fman, gem, tap) return the frame length on * send success, not 0. The DAD probe counter must treat any non-negative * return as "sent"; otherwise the first probe is miscounted, a fourth probe diff --git a/src/test/unit/unit_tests_dns_dhcp.c b/src/test/unit/unit_tests_dns_dhcp.c index f6bf0bfc..5cb942b7 100644 --- a/src/test/unit/unit_tests_dns_dhcp.c +++ b/src/test/unit/unit_tests_dns_dhcp.c @@ -3169,6 +3169,37 @@ START_TEST(test_dns_send_query_schedules_timeout) } END_TEST +/* When the timer heap is full, dns_send_query must abort the armed query + * (clearing dns_id) and return an error: a failed timeout insert would + * leave the DNS busy guard (dns_id != 0) set with no timer to clear it, + * wedging the resolver for every later lookup. */ +START_TEST(test_dns_send_query_timer_heap_full_aborts) +{ + struct wolfIP s; + struct wolfIP_timer t; + uint16_t id = 0; + int i; + + wolfIP_init(&s); + mock_link_init(&s); + s.dns_server = 0x08080808U; + s.last_tick = 100U; + + /* Fill the timer heap so the DNS timeout insert fails. */ + for (i = 0; i < MAX_TIMERS; i++) { + t.expires = s.last_tick + 1000U + (uint64_t)i; + t.arg = NULL; + t.cb = NULL; + (void)timers_binheap_insert(&s.timers, t); + } + + /* The query must abort (dns_id cleared) rather than wedge the resolver. */ + ck_assert_int_ne(dns_send_query(&s, "example.com", &id, DNS_A), 0); + ck_assert_uint_eq(s.dns_id, 0U); + ck_assert_uint_eq(s.dns_timer, NO_TIMER); +} +END_TEST + START_TEST(test_dns_send_query_send_failure_clears_outstanding_state) { struct wolfIP s; @@ -3572,8 +3603,8 @@ START_TEST(test_dhcp_parse_offer_option_overload) ck_assert_uint_eq(s.dhcp_server_ip, 0x0A000064U); /* --- Scenario 3: overload = both (value 3): the option lists run - * options (terminated by END) -> sname -> file; the mask is in - * sname, the server id in file. */ + * options (terminated by END) -> file -> sname (RFC 2131 + * sec.4.4.1); the mask is in sname, the server id in file. */ memset(&s, 0, sizeof(s)); wolfIP_init(&s); mock_link_init(&s); @@ -3597,7 +3628,9 @@ START_TEST(test_dhcp_parse_offer_option_overload) field_opt->data[0] = 0xFF; field_opt->data[1] = 0xFF; field_opt->data[2] = 0xFF; field_opt->data[3] = 0x00; field_opt = (struct dhcp_option *)((uint8_t *)field_opt + 6); - field_opt->code = 0; /* pad until the file field */ + /* sname is now the last scanned region (RFC 2131 sec.4.4.1), so its + * option list must carry the terminating end option. */ + field_opt->code = DHCP_OPTION_END; field_opt->len = 0; field_opt = (struct dhcp_option *)msg.file; field_opt->code = DHCP_OPTION_SERVER_ID; @@ -3611,7 +3644,58 @@ START_TEST(test_dhcp_parse_offer_option_overload) ret = dhcp_parse_offer(&s, &msg, DHCP_HEADER_LEN + opt_len); ck_assert_int_eq(ret, 0); ck_assert_uint_eq(s.dhcp_server_ip, 0x0A000064U); - ck_assert_uint_eq(s.ipconf[TEST_PRIMARY_IF].mask, 0xFFFFFF00U); + ck_assert_uint_eq(s.dhcp_offered_mask, 0xFFFFFF00U); + + /* --- Scenario 4: overload = both, the same option code present in + * both overloaded fields. RFC 2131 sec.4.4.1 interprets the file + * field before the sname field, and the offer parser is last-wins, + * so the sname value must be the one kept (the old sname-first scan + * let the file value win). --- */ + memset(&s, 0, sizeof(s)); + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + s.dhcp_xid = 0x1234; + memset(&msg, 0, sizeof(msg)); + msg.op = BOOT_REPLY; + msg.magic = ee32(DHCP_MAGIC); + msg.xid = ee32(s.dhcp_xid); + msg.yiaddr = ee32(0x0A00000AU); + opt = (uint8_t *)msg.options; + opt[0] = DHCP_OPTION_MSG_TYPE; opt[1] = 1; opt[2] = DHCP_OFFER; + opt += 3; + opt[0] = 52 /* DHCP_OPTION_OVERLOAD */; opt[1] = 1; opt[2] = 3; + opt += 3; + *opt++ = DHCP_OPTION_END; + opt_len = (uint32_t)(opt - (uint8_t *)msg.options); + field_opt = (struct dhcp_option *)msg.file; + field_opt->code = DHCP_OPTION_SUBNET_MASK; + field_opt->len = 4; + field_opt->data[0] = 0xFF; field_opt->data[1] = 0xFF; + field_opt->data[2] = 0xFF; field_opt->data[3] = 0x00; + field_opt = (struct dhcp_option *)((uint8_t *)field_opt + 6); + field_opt->code = DHCP_OPTION_END; + field_opt->len = 0; + field_opt = (struct dhcp_option *)msg.sname; + field_opt->code = DHCP_OPTION_SERVER_ID; + field_opt->len = 4; + field_opt->data[0] = 0x0A; field_opt->data[1] = 0x00; + field_opt->data[2] = 0x00; field_opt->data[3] = 0x64; + field_opt = (struct dhcp_option *)((uint8_t *)field_opt + 6); + field_opt->code = DHCP_OPTION_SUBNET_MASK; + field_opt->len = 4; + field_opt->data[0] = 0xFF; field_opt->data[1] = 0xFF; + field_opt->data[2] = 0xFF; field_opt->data[3] = 0xF0; + field_opt = (struct dhcp_option *)((uint8_t *)field_opt + 6); + field_opt->code = DHCP_OPTION_END; + field_opt->len = 0; + + ret = dhcp_parse_offer(&s, &msg, DHCP_HEADER_LEN + opt_len); + ck_assert_int_eq(ret, 0); + ck_assert_uint_eq(s.dhcp_server_ip, 0x0A000064U); + /* sname is scanned after file: its mask is the last one seen and + * must be the one kept. */ + ck_assert_uint_eq(s.dhcp_offered_mask, 0xFFFFFFF0U); } END_TEST diff --git a/src/test/unit/unit_tests_tcp_ack.c b/src/test/unit/unit_tests_tcp_ack.c index c4b529da..3c9dd682 100644 --- a/src/test/unit/unit_tests_tcp_ack.c +++ b/src/test/unit/unit_tests_tcp_ack.c @@ -4461,6 +4461,53 @@ START_TEST(test_tcp_process_ts_ooo_segment_keeps_recent) } END_TEST +/* The TS.Recent freshness test must compare both operands in + * the same byte-order domain. po.ts_val is host order while last_ts is + * stored in wire order (it is emitted verbatim as ECR); the old + * comparison swapped only the incoming value, so it ordered byte-swapped + * numbers. With TS.Recent = 1, TSval 256 is newer in host order (and + * tcp_paws_check accepts it), but 0x00010000 < 0x01000000 in the + * swapped domain, so the stale TS.Recent was kept; and with TS.Recent + * = 256 the swapped comparison even rolled it back to 1. */ +START_TEST(test_tcp_process_ts_recent_compare_host_order) +{ + struct wolfIP s; + struct tsocket *ts; + uint8_t buf[sizeof(struct wolfIP_tcp_seg) + TCP_OPTIONS_LEN]; + struct wolfIP_tcp_seg *tcp = (struct wolfIP_tcp_seg *)buf; + struct tcp_opt_ts *opt = (struct tcp_opt_ts *)tcp->data; + + wolfIP_init(&s); + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->sock.tcp.ack = 1000; /* RCV.NXT == Last.ACK.sent (host order) */ + ts->sock.tcp.last_ts = ee32(1); + ts->sock.tcp.ts_recent_valid = 1; + + memset(buf, 0, sizeof(buf)); + tcp->hlen = (TCP_HEADER_LEN + TCP_OPTIONS_LEN) << 2; + opt->opt = TCP_OPTION_TS; + opt->len = TCP_OPTION_TS_LEN; + opt->pad = TCP_OPTION_NOP; + opt->eoo = TCP_OPTION_EOO; + + /* In-order segment with TSval 256 > TS.Recent 1: advance. */ + tcp->seq = ee32(1000); + opt->val = ee32(256); + tcp_process_ts(ts, tcp, sizeof(buf)); + ck_assert_uint_eq(ts->sock.tcp.last_ts, ee32(256)); + + /* Mirror image: TS.Recent 256, incoming TSval 1 is older in host + * order and must not roll TS.Recent back. */ + tcp->seq = ee32(1000); + opt->val = ee32(1); + tcp_process_ts(ts, tcp, sizeof(buf)); + ck_assert_uint_eq(ts->sock.tcp.last_ts, ee32(256)); +} +END_TEST + START_TEST(test_tcp_input_paws_ooo_does_not_poison_hole_fill) { struct wolfIP s; diff --git a/src/test/unit/unit_tests_tcp_flow.c b/src/test/unit/unit_tests_tcp_flow.c index f95f7d68..1a45c03e 100644 --- a/src/test/unit/unit_tests_tcp_flow.c +++ b/src/test/unit/unit_tests_tcp_flow.c @@ -5766,3 +5766,190 @@ START_TEST(test_tcp_listener_revert_restores_option_baseline) ck_assert_uint_eq(lsn->sock.tcp.ts_offer, fresh->sock.tcp.ts_offer); } END_TEST + +/* A handshake that completes before accept() leaves the listener + * ESTABLISHED with the pre-accept fast-fail timer armed. If the + * application closes the socket instead of accepting, the control RTO + * takes over the shared timer slot and must keep retransmitting the + * FIN-ACK on every expiry. The pre-accept flag has to go with the timer + * it armed: left behind, tcp_rto_cb's pre-accept branch sees a socket + * that left the pinned condition, disarms quietly, and the FIN_WAIT_1 + * close stalls with no retransmit and no retry budget. */ +START_TEST(test_tcp_listener_preaccept_close_rto_retransmits_finack) +{ + struct wolfIP s; + int fd; + struct tsocket *lsn; + uint64_t t; + uint32_t frames_before; + const struct wolfIP_tcp_seg *out; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, LLK_LOCAL_IP, LLK_NET_MASK, 0); + fd = llk_open_listener(&s); + lsn = &s.tcpsockets[SOCKET_UNMARK(fd)]; + + /* Pending-only ARP policy: the peer is a known neighbor, so the + * FIN-ACK retransmits reach the wire. */ + llk_keep_arp_fresh(&s, LLK_ATT_IP); + + llk_attacker_syn(&s, LLK_ATT_IP, 41000, 1, 0); + llk_complete_handshake(&s, lsn, LLK_ATT_IP, 41000, 1); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_ESTABLISHED); + ck_assert_int_eq(lsn->sock.tcp.preaccept_timeout_active, 1); + + /* Close the established-but-unaccepted connection: FIN_WAIT_1, the + * control RTO owns the timer slot, and the pre-accept flag must be + * gone with the timer it armed. */ + ck_assert_int_eq(wolfIP_sock_close(&s, fd), -WOLFIP_EAGAIN); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_FIN_WAIT_1); + ck_assert_int_eq(lsn->sock.tcp.preaccept_timeout_active, 0); + ck_assert_int_eq(lsn->sock.tcp.ctrl_rto_active, 1); + ck_assert_uint_ne(lsn->sock.tcp.tmr_rto, NO_TIMER); + + /* First poll drains the queued FIN-ACK. */ + (void)wolfIP_poll(&s, 3); + out = llk_last_tcp(); + ck_assert_ptr_nonnull(out); + ck_assert(out->flags & (TCP_FLAG_FIN | TCP_FLAG_ACK)); + + /* The control RTO fires: the FIN-ACK is retransmitted and the + * backoff re-armed, not silently disarmed. */ + frames_before = last_frame_sent_count; + for (t = 4; t <= 2500; t += 100) { + (void)wolfIP_poll(&s, t); + } + ck_assert_uint_eq(last_frame_sent_count, frames_before + 1); + out = llk_last_tcp(); + ck_assert_ptr_nonnull(out); + ck_assert(out->flags & (TCP_FLAG_FIN | TCP_FLAG_ACK)); + ck_assert_uint_eq(ee16(out->src_port), LLK_LISTEN_PORT); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_FIN_WAIT_1); + ck_assert_int_eq(lsn->sock.tcp.ctrl_rto_active, 1); + ck_assert_uint_ne(lsn->sock.tcp.tmr_rto, NO_TIMER); +} +END_TEST + +/* An acceptable FIN that arrives in CLOSE_WAIT or CLOSING does not move + * the state machine: the peer's FIN was already consumed on the way in. + * It must not advance the receive ACK either, or a peer that keeps + * sending FINs at RCV.NXT would march the ACK forward with no state + * change. */ +START_TEST(test_tcp_fin_in_close_wait_does_not_advance_ack) +{ + struct wolfIP s; + int fd; + struct tsocket *lsn; + uint8_t seg_buf[sizeof(struct wolfIP_tcp_seg)]; + struct wolfIP_tcp_seg *fin = (struct wolfIP_tcp_seg *)seg_buf; + uint32_t rcv_nxt; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, LLK_LOCAL_IP, LLK_NET_MASK, 0); + fd = llk_open_listener(&s); + lsn = &s.tcpsockets[SOCKET_UNMARK(fd)]; + + llk_keep_arp_fresh(&s, LLK_ATT_IP); + + llk_attacker_syn(&s, LLK_ATT_IP, 41000, 1, 0); + llk_complete_handshake(&s, lsn, LLK_ATT_IP, 41000, 1); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_ESTABLISHED); + rcv_nxt = lsn->sock.tcp.ack; + + /* The peer's FIN at RCV.NXT: ESTABLISHED -> CLOSE_WAIT, ACK advances. */ + memset(seg_buf, 0, sizeof(seg_buf)); + fin->ip.ver_ihl = 0x45; + fin->ip.proto = WI_IPPROTO_TCP; + fin->ip.ttl = 64; + fin->ip.len = ee16(IP_HEADER_LEN + TCP_HEADER_LEN); + fin->ip.src = ee32(lsn->remote_ip); + fin->ip.dst = ee32(lsn->local_ip); + fin->dst_port = ee16(lsn->src_port); + fin->src_port = ee16(lsn->dst_port); + fin->seq = ee32(rcv_nxt); + fin->ack = ee32(tcp_seq_inc(lsn->sock.tcp.snd_una, 1)); + fin->hlen = TCP_HEADER_LEN << 2; + fin->flags = TCP_FLAG_FIN | TCP_FLAG_ACK; + fix_tcp_checksums(fin); + tcp_input(&s, TEST_PRIMARY_IF, fin, + (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + TCP_HEADER_LEN)); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_CLOSE_WAIT); + ck_assert_uint_eq(lsn->sock.tcp.ack, rcv_nxt + 1); + + /* A second FIN at the new RCV.NXT: no state change in CLOSE_WAIT, + * and the receive ACK must not advance. */ + fin->seq = ee32(rcv_nxt + 1); + fix_tcp_checksums(fin); + tcp_input(&s, TEST_PRIMARY_IF, fin, + (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + TCP_HEADER_LEN)); + ck_assert_int_eq(lsn->sock.tcp.state, TCP_CLOSE_WAIT); + ck_assert_uint_eq(lsn->sock.tcp.ack, rcv_nxt + 1); +} +END_TEST + +/* When the timer heap is full, tcp_ctrl_rto_start must not mark the + * control RTO active: an active flag with no timer behind it would never + * fire and would suppress every other timeout. */ +START_TEST(test_tcp_ctrl_rto_start_no_timer_does_not_set_active) +{ + struct wolfIP s; + struct tsocket *ts; + struct wolfIP_timer t = {0}; + int i; + + wolfIP_init(&s); + t.cb = tcp_rto_cb; + for (i = 0; i < MAX_TIMERS; i++) { + t.arg = (void *)(intptr_t)i; + t.expires = s.last_tick + 1000 + i; + timers_binheap_insert(&s.timers, t); + } + ts = &s.tcpsockets[0]; + ts->S = &s; + ts->proto = WI_IPPROTO_TCP; + ts->sock.tcp.rto = 1000; + ts->sock.tcp.ctrl_rto_retries = 0; + ts->sock.tcp.tmr_rto = NO_TIMER; + ts->sock.tcp.ctrl_rto_active = 0; + tcp_ctrl_rto_start(ts, s.last_tick); + /* Heap full: insert failed, so the control RTO must not be active. */ + ck_assert_int_eq(ts->sock.tcp.tmr_rto, NO_TIMER); + ck_assert_int_eq(ts->sock.tcp.ctrl_rto_active, 0); +} +END_TEST + +/* A re-arm (the normal retransmit path) enters with ctrl_rto_active set by + * the prior arm: if the re-insert into a full heap fails, the flag must not + * survive - an active flag with no timer behind it wedges the socket the + * same way a failed first arm does. */ +START_TEST(test_tcp_ctrl_rto_start_rearm_failure_clears_active) +{ + struct wolfIP s; + struct tsocket *ts; + struct wolfIP_timer t = {0}; + int i; + + wolfIP_init(&s); + t.cb = tcp_rto_cb; + for (i = 0; i < MAX_TIMERS; i++) { + t.arg = (void *)(intptr_t)i; + t.expires = s.last_tick + 1000 + i; + timers_binheap_insert(&s.timers, t); + } + ts = &s.tcpsockets[0]; + ts->S = &s; + ts->proto = WI_IPPROTO_TCP; + ts->sock.tcp.rto = 1000; + ts->sock.tcp.ctrl_rto_retries = 0; + ts->sock.tcp.tmr_rto = NO_TIMER; + /* State at the start of a retransmit: the prior arm succeeded and its + * timer has since fired, so the flag is set and the slot is empty. */ + ts->sock.tcp.ctrl_rto_active = 1; + tcp_ctrl_rto_start(ts, s.last_tick); + /* Heap full: the re-insert failed, so the flag must be cleared. */ + ck_assert_int_eq(ts->sock.tcp.tmr_rto, NO_TIMER); + ck_assert_int_eq(ts->sock.tcp.ctrl_rto_active, 0); +} +END_TEST diff --git a/src/wolfip.c b/src/wolfip.c index 4408bc8c..5e2b23c0 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -1348,7 +1348,7 @@ static void tcp_persist_start(struct tsocket *t, uint64_t now); static void tcp_persist_stop(struct tsocket *t); static void tcp_rto_update_from_sample(struct tsocket *t, uint32_t sample_ms); static void tcp_rto_cb(void *arg); -static void tcp_ctrl_rto_start(struct tsocket *t, uint64_t now); +static int tcp_ctrl_rto_start(struct tsocket *t, uint64_t now); static void tcp_ctrl_rto_stop(struct tsocket *t); static void tcp_fin_wait_2_timeout_start(struct tsocket *t, uint64_t now); static void tcp_fin_wait_2_timeout_stop(struct tsocket *t); @@ -1464,6 +1464,7 @@ struct wolfIP { uint32_t dhcp_timer; /* Timer for DHCP */ uint32_t dhcp_timeout_count; /* DHCP timeout counter */ uint8_t dhcp_dad_probes; /* DAD probes sent (0 = DAD inactive) */ + uint8_t dhcp_dad_if; /* Interface DAD probes on (valid in DHCP_DAD) */ ip4 dhcp_server_ip; /* DHCP server IP */ ip4 dhcp_ip; /* IP address assigned by DHCP */ uint32_t dhcp_offered_mask; /* netmask from the accepted OFFER */ @@ -4005,13 +4006,20 @@ static uint32_t tcp_backoff_rto_ms(uint32_t rto_ms, uint32_t retries) /* Arm/re-arm control-RTO timer using exponential backoff over the current base RTO. * This path is dedicated to SYN/SYN-ACK/FIN reliability (not data-loss recovery). */ -static void tcp_ctrl_rto_start(struct tsocket *t, uint64_t now) +static int tcp_ctrl_rto_start(struct tsocket *t, uint64_t now) { struct wolfIP_timer tmr = {0}; uint64_t shift_rto; if (!t || t->proto != WI_IPPROTO_TCP) - return; + return 0; + /* The control RTO takes over the shared timer slot: tcp_rto_cb + * dispatches on the timeout flags from that same slot, so every flag + * it replaces must be cleared with the timer it armed. */ t->sock.tcp.fin_wait_2_timeout_active = 0; + t->sock.tcp.preaccept_timeout_active = 0; + /* Re-arms enter with the flag set by the prior arm: clear it up front + * so a failed insert cannot leave it set with no timer behind it. */ + t->sock.tcp.ctrl_rto_active = 0; if (t->sock.tcp.tmr_rto != NO_TIMER) { timer_binheap_cancel(&t->S->timers, t->sock.tcp.tmr_rto); t->sock.tcp.tmr_rto = NO_TIMER; @@ -4021,7 +4029,14 @@ static void tcp_ctrl_rto_start(struct tsocket *t, uint64_t now) tmr.arg = t; tmr.cb = tcp_rto_cb; t->sock.tcp.tmr_rto = timers_binheap_insert(&t->S->timers, tmr); + /* Only mark the control RTO active when the timer actually took a slot: + * an active flag with no timer behind it would never fire and would + * suppress every other timeout until a later event cleared it. */ + if (t->sock.tcp.tmr_rto == NO_TIMER) { + return -1; + } t->sock.tcp.ctrl_rto_active = 1; + return 0; } static void tcp_fin_wait_2_timeout_start(struct tsocket *t, uint64_t now) @@ -4922,9 +4937,12 @@ static int tcp_process_ts(struct tsocket *t, const struct wolfIP_tcp_seg *tcp, * until then there is no reference to compare against. A zeroed last_ts * is not a timestamp - treating it as one makes every segment whose * TSval sits in the upper half of the 32-bit space look "older" and - * tcp_paws_check drops the whole data flow after the handshake. */ + * tcp_paws_check drops the whole data flow after the handshake. + * last_ts is stored in wire order (it is emitted verbatim as ECR), + * so compare in host order as tcp_paws_check does: byte-swapped + * values are not ordered. */ if (!t->sock.tcp.ts_recent_valid || - (!tcp_seq_lt(ee32(po.ts_val), t->sock.tcp.last_ts) && + (!tcp_seq_lt(po.ts_val, ee32(t->sock.tcp.last_ts)) && tcp_seq_leq(ee32(tcp->seq), t->sock.tcp.ack))) { t->sock.tcp.last_ts = ee32(po.ts_val); t->sock.tcp.ts_recent_valid = 1; @@ -5896,12 +5914,10 @@ static void tcp_input(struct wolfIP *S, unsigned int if_idx, if (!(tcp->flags & TCP_FLAG_ACK)) continue; - if (tcp->flags & TCP_FLAG_ACK) { - tcp_ack(t, tcp); - if (t->sock.tcp.state == TCP_CLOSED) - continue; - tcp_process_ts(t, tcp, frame_len); - } + tcp_ack(t, tcp); + if (t->sock.tcp.state == TCP_CLOSED) + continue; + tcp_process_ts(t, tcp, frame_len); if (tcplen > 0) { if ((t->sock.tcp.state == TCP_LAST_ACK) || (t->sock.tcp.state == TCP_CLOSING) || (t->sock.tcp.state == TCP_CLOSED)) @@ -5912,6 +5928,7 @@ static void tcp_input(struct wolfIP *S, unsigned int if_idx, uint32_t seq = ee32(tcp->seq); uint32_t fin_seq_end = tcp_seq_inc(seq, tcplen); int accept_fin = 1; + int transitioned = 0; if ((tcplen == 0 && t->sock.tcp.ack != seq) || (tcplen > 0 && t->sock.tcp.ack != fin_seq_end)) { @@ -5924,18 +5941,27 @@ static void tcp_input(struct wolfIP *S, unsigned int if_idx, (void)wolfIP_filter_notify_socket_event( WOLFIP_FILT_CLOSE_WAIT, S, t, t->local_ip, t->src_port, t->remote_ip, t->dst_port); + transitioned = 1; } else if (t->sock.tcp.state == TCP_FIN_WAIT_1) { t->sock.tcp.state = TCP_CLOSING; + transitioned = 1; } else if (t->sock.tcp.state == TCP_FIN_WAIT_2) { tcp_fin_wait_2_timeout_stop(t); t->sock.tcp.state = TCP_TIME_WAIT; + transitioned = 1; } - if (tcplen > 0) { - t->sock.tcp.ack = tcp_seq_inc(fin_seq_end, 1); - } else { - t->sock.tcp.ack = tcp_seq_inc(seq, 1); + /* Only a state transition consumes the FIN's + * sequence number: a FIN that re-arrives in + * CLOSE_WAIT, CLOSING or TIME_WAIT is re-ACKed + * without advancing the receive ACK. */ + if (transitioned) { + if (tcplen > 0) { + t->sock.tcp.ack = tcp_seq_inc(fin_seq_end, 1); + } else { + t->sock.tcp.ack = tcp_seq_inc(seq, 1); + } + t->events |= CB_EVENT_CLOSED | CB_EVENT_READABLE; } - t->events |= CB_EVENT_CLOSED | CB_EVENT_READABLE; tcp_send_ack(t); } else { tcp_send_ack(t); @@ -6393,6 +6419,41 @@ int wolfIP_sock_socket(struct wolfIP *s, int domain, int type, int protocol) return -1; } +/* Forward declaration: defined below, used by port_alloc_random. */ +static int bind_port_in_use(const struct tsocket *arr, int n, + const struct tsocket *self, + ip4 new_local_ip, uint16_t new_port); + +/* Pick a source port (or ICMP id) that no other socket in arr claims. + * Start from a random value >= min_port, then walk forward on a + * collision (wrapping to min_port) until every candidate in + * min_port..65535 has been tried. local_ip may be IPADDR_ANY when the + * route is not resolved yet; the check then compares ports only. + * Returns 0 when the range holds no free port: a collided value would + * break the "no other socket claims it" contract. */ +static uint16_t port_alloc_random(const struct tsocket *arr, int n, + const struct tsocket *self, + ip4 local_ip, uint16_t min_port) +{ + uint16_t port; + uint16_t scanned; + uint16_t range; + range = (uint16_t)(0x10000 - min_port); + port = (uint16_t)(wolfIP_getrandom() & 0xFFFF); + if (port < min_port) + port = (uint16_t)(min_port + (port % range)); + scanned = 0; + do { + if (!bind_port_in_use(arr, n, self, local_ip, port)) + return port; + port++; + if (port < min_port) + port = min_port; + scanned++; + } while (scanned < range); + return 0; +} + int wolfIP_sock_connect(struct wolfIP *s, int sockfd, const struct wolfIP_sockaddr *addr, socklen_t addrlen) { @@ -6564,8 +6625,14 @@ int wolfIP_sock_connect(struct wolfIP *s, int sockfd, const struct wolfIP_sockad ts->remote_ip = new_remote_ip; ts->if_idx = new_if_idx; ts->local_ip = new_local_ip; - if (!ts->src_port) - ts->src_port = (uint16_t)(wolfIP_getrandom() & 0xFFFF); + if (!ts->src_port) { + ts->src_port = port_alloc_random(s->tcpsockets, MAX_TCPSOCKETS, + ts, ts->local_ip, 1024); + if (ts->src_port == 0) { + ts->sock.tcp.state = TCP_CLOSED; + return -WOLFIP_EAGAIN; + } + } if (ts->src_port < 1024) ts->src_port += 1024; ts->dst_port = ee16(sin->sin_port); @@ -6825,9 +6892,10 @@ int wolfIP_sock_sendto(struct wolfIP *s, int sockfd, const void *buf, size_t len if ((ts->dst_port==0) || (ts->remote_ip==0)) return -1; if (ts->src_port == 0) { - ts->src_port = (uint16_t)(wolfIP_getrandom() & 0xFFFF); - if (ts->src_port < 1024) - ts->src_port += 1024; + ts->src_port = port_alloc_random(s->udpsockets, MAX_UDPSOCKETS, + ts, IPADDR_ANY, 1024); + if (ts->src_port == 0) + return -WOLFIP_EAGAIN; } if_idx = wolfIP_route_for_ip(s, ts->remote_ip); #ifdef IP_MULTICAST @@ -6885,9 +6953,10 @@ int wolfIP_sock_sendto(struct wolfIP *s, int sockfd, const void *buf, size_t len if (ts->remote_ip == 0) return -1; if (ts->src_port == 0) { - ts->src_port = (uint16_t)(wolfIP_getrandom() & 0xFFFF); + ts->src_port = port_alloc_random(s->icmpsockets, MAX_ICMPSOCKETS, + ts, IPADDR_ANY, 1); if (ts->src_port == 0) - ts->src_port = 1; + return -WOLFIP_EAGAIN; } if (ts->bound_local_ip != IPADDR_ANY) { int bound_match = 0; @@ -8709,7 +8778,8 @@ static void dhcp_deconfigure_lease(struct wolfIP *s) * RFC 2132 ยง9.3: option 52 (Overload) tells the client that the reply's * sname (value bit 2) and/or file (value bit 1) fields carry additional * options, interpreted after the standard options field is exhausted. - * The stream is therefore scanned options -> sname -> file (wire order), + * The stream is therefore scanned options -> file -> sname (RFC 2131 + * sec.4.4.1: the file field is interpreted next, followed by sname), * each region with its own bounds, and ends at option 255 or when the * last active region runs out. Non-overloaded sname/file fields hold * plain strings (TFTP server, bootfile) and are never scanned. */ @@ -8748,24 +8818,24 @@ static void dhcp_opt_stream_init(struct dhcp_opt_stream *st, static int dhcp_opt_stream_next_region(struct dhcp_opt_stream *st) { if (st->region == 0) { - if (st->overload & 2) { - st->region = 1; - st->ptr = st->region_sname; - st->end = st->region_sname_end; - return 1; - } if (st->overload & 1) { st->region = 2; st->ptr = st->region_file; st->end = st->region_file_end; return 1; } + if (st->overload & 2) { + st->region = 1; + st->ptr = st->region_sname; + st->end = st->region_sname_end; + return 1; + } } - else if (st->region == 1) { - if (st->overload & 1) { - st->region = 2; - st->ptr = st->region_file; - st->end = st->region_file_end; + else if (st->region == 2) { + if (st->overload & 2) { + st->region = 1; + st->ptr = st->region_sname; + st->end = st->region_sname_end; return 1; } } @@ -9122,6 +9192,7 @@ static int dhcp_parse_ack(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_l * arp_recv (dhcp_dad_conflict). */ s->dhcp_state = DHCP_DAD; s->dhcp_dad_probes = 0; + s->dhcp_dad_if = WOLFIP_PRIMARY_IF_IDX; /* Arm the lease absolutes, then swap the renew timer for * the DAD timer: handle_timers() fires every expired * entry, so leaving both in the heap would double-fire @@ -9808,7 +9879,26 @@ static void arp_recv(struct wolfIP *s, unsigned int if_idx, void *buf, int len) if (arp->sma[0] & 0x01) return; - if (arp->opcode == ee16(ARP_REQUEST) && arp->tip == ee32(conf->ip)) { + /* RFC 4331/5227 DAD: on the probing interface, a request from a + * foreign MAC that claims the candidate (sender IP, including a + * gratuitous announcement with sip==tip) or probes for it is a + * conflict - a host that owns the candidate may not answer our + * probe (DAD evasion), but betrays itself by using/probing the IP. */ + if (arp->opcode == ee16(ARP_REQUEST) && s->dhcp_state == DHCP_DAD && + if_idx == s->dhcp_dad_if && memcmp(arp->sma, ll->mac, 6) != 0) { + ip4 sip = ee32(arp->sip); + ip4 tip = ee32(arp->tip); + if (sip == s->dhcp_ip || (sip == IPADDR_ANY && tip == s->dhcp_ip)) { + dhcp_dad_conflict(s); + return; + } + } + + /* An unconfigured interface (no assigned address) must not answer + * ARP requests: matching tip against a zero conf->ip would let a + * request for 0.0.0.0 be answered by advertising 0.0.0.0. */ + if (arp->opcode == ee16(ARP_REQUEST) && conf->ip != IPADDR_ANY && + arp->tip == ee32(conf->ip)) { uint32_t sender_ip = arp->sip; uint8_t sender_mac[6]; memcpy(sender_mac, arp->sma, 6); @@ -9839,10 +9929,12 @@ static void arp_recv(struct wolfIP *s, unsigned int if_idx, void *buf, int len) else if (arp->opcode == ee16(ARP_REPLY)) { ip4 sip = ee32(arp->sip); int pending; - /* RFC 4331 DAD: a reply claiming the address being probed means - * it is in use, unless it came from our own MAC (looped probe). - * This is the one case where a reply for our own IP is acted on. */ - if (s->dhcp_state == DHCP_DAD && sip == conf->ip) { + /* RFC 4331 DAD: a reply on the probing interface claiming the + * candidate is a conflict, unless it is our own MAC (looped probe). + * Bound to the DAD interface + recorded candidate so a reply on + * another (e.g. unconfigured) interface cannot force one. */ + if (s->dhcp_state == DHCP_DAD && if_idx == s->dhcp_dad_if && + sip == s->dhcp_ip) { if (memcmp(arp->sma, ll->mac, 6) != 0) dhcp_dad_conflict(s); return; @@ -10955,14 +11047,14 @@ static void dns_cancel_timer(struct wolfIP *s) } } -static void dns_schedule_timer(struct wolfIP *s) +static int dns_schedule_timer(struct wolfIP *s) { struct wolfIP_timer tmr = { }; uint64_t interval = DNS_QUERY_TIMEOUT; uint8_t shift; if (!s) - return; + return -1; if (s->dns_retry_count == 0) { /* RFC 1035 recommends a 2s initial retransmission interval. On embedded * targets, add a small 0..390 ms random offset to 1800 ms so many @@ -10980,6 +11072,9 @@ static void dns_schedule_timer(struct wolfIP *s) tmr.arg = s; tmr.cb = dns_timeout_cb; s->dns_timer = timers_binheap_insert(&s->timers, tmr); + if (s->dns_timer == NO_TIMER) + return -1; + return 0; } static int dns_resend_query(struct wolfIP *s) @@ -11031,7 +11126,10 @@ static void dns_timeout_cb(void *arg) return; } s->dns_retry_count++; - dns_schedule_timer(s); + if (dns_schedule_timer(s) != 0) { + dns_abort_query(s); + return; + } } else { dns_abort_query(s); } @@ -11243,7 +11341,14 @@ static int dns_send_query(struct wolfIP *s, const char *dname, uint16_t *id, *id = DNS_ID_NONE; return ret; } - dns_schedule_timer(s); + if (dns_schedule_timer(s) != 0) { + /* Timer heap full: abort the armed query, or the busy guard + * (dns_id != 0) would block every later lookup with no timer to + * clear it. */ + dns_abort_query(s); + *id = DNS_ID_NONE; + return -1; + } return 0; } @@ -11757,15 +11862,26 @@ static void flush_raw_tx(struct wolfIP *s) ip->csum = 0; iphdr_set_checksum(ip); } - if (wolfIP_filter_notify_ip(WOLFIP_FILT_SENDING, s, tx_if, ip, desc->len) != 0) +#ifdef ETHERNET + /* Build the ethernet header before the filter callbacks run: + * they may inspect the destination address the frame will + * actually carry. */ + if (!wolfIP_ll_is_non_ethernet(s, tx_if)) { + if (eth_output_add_header(s, tx_if, r->nexthop_mac, &ip->eth, + ETH_TYPE_IP) != 0) { + break; + } + } +#endif + if (wolfIP_filter_notify_ip(WOLFIP_FILT_SENDING, s, tx_if, ip, desc->len) != 0) { break; + } #ifdef ETHERNET if (!wolfIP_ll_is_non_ethernet(s, tx_if)) { if (wolfIP_filter_notify_eth(WOLFIP_FILT_SENDING, s, tx_if, - &ip->eth, desc->len) != 0) + &ip->eth, desc->len) != 0) { break; - eth_output_add_header(s, tx_if, r->nexthop_mac, &ip->eth, - ETH_TYPE_IP); + } } #endif /* Mirror flush_datagram_tx: on driver backpressure/hard error