diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index c347ab1a..a83298c4 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -177,6 +177,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_filter_notify_tcp_metadata); 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); tcase_add_test(tc_utils, test_filter_dispatch_lock_blocks); tcase_add_test(tc_utils, test_filter_dispatch_meta_null_initializes); tcase_add_test(tc_utils, test_filter_socket_event_unknown_proto); @@ -521,6 +522,13 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_dns_abort_query_clears_timer_and_query_state); tcase_add_test(tc_utils, test_dns_timeout_retries_then_aborts_and_allows_new_query); tcase_add_test(tc_utils, test_dns_send_query_invalid_name); + tcase_add_test(tc_utils, test_dns_send_query_rejects_empty_labels); + tcase_add_test(tc_utils, test_dns_callback_rejects_a_record_with_wrong_rdlength); + tcase_add_test(tc_utils, test_dhcp_parse_offer_option_overload); + tcase_add_test(tc_utils, + test_dhcp_parse_offer_option_split_across_region_boundary); + tcase_add_test(tc_utils, test_dhcp_discover_first_retry_delay_rfc2131); + tcase_add_test(tc_utils, test_dhcp_discover_retry_delay_small_base_no_underflow); tcase_add_test(tc_utils, test_dns_wrapper_apis); tcase_add_test(tc_utils, test_wolfip_static_instance_apis); tcase_add_test(tc_utils, test_wolfip_ip_is_multicast_variants); @@ -534,6 +542,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_tcp_rto_cb_non_tcp_noop); tcase_add_test(tc_utils, test_tcp_rto_cb_non_established_noop); tcase_add_test(tc_utils, test_tcp_rto_cb_syn_sent_requeues_syn_and_arms_timer); + tcase_add_test(tc_utils, test_tcp_syn_retransmit_duration_meets_rfc9293_r2); tcase_add_test(tc_utils, test_tcp_input_synack_cancels_control_rto); tcase_add_test(tc_utils, test_tcp_rto_cb_last_ack_requeues_finack_and_arms_timer); tcase_add_test(tc_utils, test_tcp_rto_cb_last_ack_full_txbuf_keeps_retry_budget); @@ -634,10 +643,14 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_tcp_ack_duplicate_ssthresh_min); tcase_add_test(tc_utils, test_tcp_ack_progress_resets_rto_recovery_state); tcase_add_test(tc_utils, test_tcp_ack_cwnd_grows_when_payload_acked_is_mss_minus_options); + tcase_add_test(tc_utils, test_tcp_ack_data_segments_not_counted_as_dup_acks); + tcase_add_test(tc_utils, test_tcp_rto_backoff_capped_at_64s); tcase_add_test(tc_utils, test_tcp_ack_inflight_deflate_sets_writable_without_acked_desc); tcase_add_test(tc_utils, test_tcp_input_peer_rwnd_growth_sets_writable); tcase_add_test(tc_utils, test_tcp_input_synack_negotiates_peer_mss); tcase_add_test(tc_utils, test_tcp_connect_syn_advertises_interface_mss); + tcase_add_test(tc_utils, test_tcp_connect_rejects_broadcast_multicast_dest); + tcase_add_test(tc_utils, test_tcp_parse_options_keeps_sub_default_advertised_mss); tcase_add_test(tc_utils, test_tcp_connect_syn_limits_options_to_small_mtu); tcase_add_test(tc_utils, test_sock_sendto_tcp_respects_negotiated_peer_mss); tcase_add_test(tc_utils, test_sock_sendto_tcp_defaults_to_rfc_mss_when_unset_by_peer); @@ -940,6 +953,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_proto, test_regression_paws_rejects_stale_timestamp); tcase_add_test(tc_proto, test_regression_paws_accepts_wrapped_newer_timestamp); tcase_add_test(tc_proto, test_regression_paws_drops_segment_without_timestamp_option); + tcase_add_test(tc_proto, test_regression_paws_preempts_acceptability_for_replayed_segment); tcase_add_test(tc_proto, test_regression_paws_drops_last_ack_segment_without_timestamp_option); tcase_add_test(tc_proto, test_regression_paws_drops_time_wait_segment_without_timestamp_option); tcase_add_test(tc_proto, test_regression_dhcp_nak_restarts_configuration); @@ -1057,6 +1071,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_core, test_route_for_ip_gateway_fallback); tcase_add_test(tc_core, test_route_for_ip_first_non_loop_fallback); tcase_add_test(tc_core, test_sendto_udp_txbuf_full_eagain); + tcase_add_test(tc_core, test_udp_sendto_queued_datagram_keeps_enqueue_dst); tcase_add_test(tc_core, test_sendto_icmp_no_remote_after_addr_zero); tcase_add_test(tc_core, test_ip_recv_with_ip_options_strips_and_dispatches); tcase_add_test(tc_core, test_ip_recv_wrong_version_dropped); @@ -1152,6 +1167,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_core, test_sock_sendto_raw_null_dest_uses_stored_remote_ip); tcase_add_test(tc_core, test_sock_sendto_raw_null_dest_no_remote_ip); tcase_add_test(tc_core, test_sock_sendto_raw_hdrincl_dst_from_buf); + tcase_add_test(tc_core, test_raw_hdrincl_dst_override_recomputes_ip_checksum); tcase_add_test(tc_core, test_sock_sendto_raw_invalid_fd); tcase_add_test(tc_core, test_sock_sendto_raw_fifo_full_returns_eagain); tcase_add_test(tc_core, test_sock_setsockopt_raw_hdrincl); @@ -1218,7 +1234,7 @@ 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_mss_below_default_kept_verbatim); 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); @@ -1234,6 +1250,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_core, test_tcp_input_syn_rcvd_ack_with_fin_enters_close_wait); tcase_add_test(tc_core, test_tcp_input_window_grows_from_zero_stops_persist); tcase_add_test(tc_core, test_tcp_rto_cb_fin_wait_2_timeout_closes_socket); + tcase_add_test(tc_core, test_accept_synack_retransmit_repeats_isn); tcase_add_test(tc_core, test_tcp_rto_cb_fin_wait_2_wrong_state_stops_timer); tcase_add_test(tc_core, test_tcp_rto_cb_ctrl_not_needed_stops); tcase_add_test(tc_core, test_tcp_rto_cb_ctrl_maxretries_nonlistener_closes); @@ -1398,6 +1415,10 @@ Suite *wolf_suite(void) tcase_add_test(tc_core, test_ip_recv_directed_broadcast_dst_is_local); tcase_add_test(tc_core, test_ip_recv_ipaddr_any_dst_is_local); tcase_add_test(tc_core, test_ip_recv_forward_arp_hit_sends_immediately); + tcase_add_test(tc_core, test_ip_recv_forward_static_route_uses_gateway); + tcase_add_test(tc_core, test_ip_recv_forward_static_route_hairpin_dropped); + tcase_add_test(tc_core, test_ip_recv_forward_static_route_default); + tcase_add_test(tc_core, test_ip_recv_forward_declared_length_checked); tcase_add_test(tc_core, test_ip_recv_forward_unconfigured_iface_skipped); tcase_add_test(tc_core, test_ip_recv_forward_link_local_src_rpf_drop); tcase_add_test(tc_core, test_ip_recv_forward_self_ip_src_dropped); diff --git a/src/test/unit/unit_shared.c b/src/test/unit/unit_shared.c index a19421c0..fdab7be1 100644 --- a/src/test/unit/unit_shared.c +++ b/src/test/unit/unit_shared.c @@ -81,15 +81,44 @@ static uint8_t last_frame_sent[LINK_MTU]; static uint32_t last_frame_sent_size = 0; static uint32_t last_frame_sent_count = 0; +/* When armed, the next send() returns -WOLFIP_EAGAIN without transmitting, + * leaving any in-flight descriptor queued in the socket txbuf. */ +static int mock_send_eagain_armed = 0; + +/* Ring of the most recent frames actually transmitted (up to 4). */ +#define MOCK_SENT_FRAMES_MAX 4 +static uint8_t mock_sent_frames[MOCK_SENT_FRAMES_MAX][LINK_MTU]; +static uint32_t mock_sent_frames_size[MOCK_SENT_FRAMES_MAX]; +static uint32_t mock_sent_frames_count = 0; + static int mock_send(struct wolfIP_ll_dev *dev, void *frame, uint32_t len) { (void)dev; + if (mock_send_eagain_armed) { + mock_send_eagain_armed = 0; + return -WOLFIP_EAGAIN; + } memcpy(last_frame_sent, frame, len); last_frame_sent_size = len; last_frame_sent_count++; + if (mock_sent_frames_count < MOCK_SENT_FRAMES_MAX) { + memcpy(mock_sent_frames[mock_sent_frames_count], frame, len); + mock_sent_frames_size[mock_sent_frames_count] = len; + mock_sent_frames_count++; + } return 0; } +void mock_link_capture_reset(void) +{ + last_frame_sent_size = 0; + last_frame_sent_count = 0; + mock_sent_frames_count = 0; + /* Disarm so a test that fails before consuming the flag cannot + * poison the next transmit in a later test. */ + mock_send_eagain_armed = 0; +} + static int mock_poll(struct wolfIP_ll_dev *dev, void *frame, uint32_t len) { (void)dev; diff --git a/src/test/unit/unit_tests_api.c b/src/test/unit/unit_tests_api.c index 8d621ec7..91e53461 100644 --- a/src/test/unit/unit_tests_api.c +++ b/src/test/unit/unit_tests_api.c @@ -233,6 +233,62 @@ START_TEST(test_filter_dispatch_mask_not_set) } END_TEST +/* A freshly installed callback must not fail open: with no explicit mask + * configuration since the last uninstall, dispatch consults the callback + * for every reason; any explicit mask configuration (even a zero mask, + * even one made before installing the callback) switches to the + * configured reasons only. */ +START_TEST(test_filter_fresh_callback_consulted_before_mask_configured) +{ + struct wolfIP s; + struct wolfIP_filter_metadata meta; + + memset(&s, 0, sizeof(s)); + /* Clear stale masks from other tests, then return to the initial + * state: uninstall resets the all-reasons default. */ + wolfIP_filter_set_mask(0); + wolfIP_filter_set_eth_mask(0); + wolfIP_filter_set_ip_mask(0); + wolfIP_filter_set_tcp_mask(0); + wolfIP_filter_set_udp_mask(0); + wolfIP_filter_set_icmp_mask(0); + wolfIP_filter_set_callback(NULL, NULL); + + wolfIP_filter_init_metadata(&meta); + meta.ip_proto = WOLFIP_FILTER_PROTO_TCP; + + /* Fresh install, no mask configuration: consulted for all reasons. */ + filter_cb_calls = 0; + wolfIP_filter_set_callback(test_filter_cb, NULL); + wolfIP_filter_dispatch(WOLFIP_FILT_RECEIVING, &s, 0, NULL, 0, &meta); + ck_assert_int_eq(filter_cb_calls, 1); + + /* The first explicit mask configuration selects the reasons. */ + wolfIP_filter_set_mask(WOLFIP_FILT_MASK(WOLFIP_FILT_CONNECTING)); + filter_cb_calls = 0; + wolfIP_filter_dispatch(WOLFIP_FILT_RECEIVING, &s, 0, NULL, 0, &meta); + ck_assert_int_eq(filter_cb_calls, 0); + + /* An explicit empty mask silences the filter. */ + wolfIP_filter_set_mask(0); + filter_cb_calls = 0; + wolfIP_filter_dispatch(WOLFIP_FILT_RECEIVING, &s, 0, NULL, 0, &meta); + ck_assert_int_eq(filter_cb_calls, 0); + + /* Explicit zero configured *before* installing the callback must be + * honored, not overridden by the all-reasons default. */ + wolfIP_filter_set_callback(NULL, NULL); + wolfIP_filter_set_mask(0); + filter_cb_calls = 0; + wolfIP_filter_set_callback(test_filter_cb, NULL); + wolfIP_filter_dispatch(WOLFIP_FILT_RECEIVING, &s, 0, NULL, 0, &meta); + ck_assert_int_eq(filter_cb_calls, 0); + + wolfIP_filter_set_callback(NULL, NULL); + wolfIP_filter_set_mask(0); +} +END_TEST + START_TEST(test_filter_dispatch_lock_blocks) { struct wolfIP s; @@ -2425,7 +2481,9 @@ START_TEST(test_sock_accept_initializes_snd_una) ck_assert_int_gt(client_sd, 0); accepted = &s.tcpsockets[SOCKET_UNMARK(client_sd)]; - ck_assert_uint_eq(accepted->sock.tcp.seq, (uint32_t)(0x80000000U + 1U)); + /* While in SYN_RCVD the socket's seq stays at the ISN; the final ACK + * handler advances it to ISN+1 on establishment. */ + ck_assert_uint_eq(accepted->sock.tcp.seq, 0x80000000U); ck_assert_uint_eq(accepted->sock.tcp.snd_una, 0x80000000U); ck_assert_int_eq(tcp_seq_leq(accepted->sock.tcp.snd_una, accepted->sock.tcp.seq), 1); } @@ -2739,7 +2797,8 @@ START_TEST(test_sock_accept_ack_transitions_to_established) ack.src_port = ee16(40000); /* Remote port from inject_tcp_syn */ ack.dst_port = ee16(1234); ack.seq = ee32(accepted->sock.tcp.ack); - ack.ack = ee32(accepted->sock.tcp.seq); + /* The client's final ACK is at the server's snd.nxt: ISN+1. */ + ack.ack = ee32(tcp_seq_inc(accepted->sock.tcp.snd_una, 1)); ack.hlen = TCP_HEADER_LEN << 2; ack.flags = TCP_FLAG_ACK; ack.win = ee16(65535); diff --git a/src/test/unit/unit_tests_branches.c b/src/test/unit/unit_tests_branches.c index 897b628d..89530fc4 100644 --- a/src/test/unit/unit_tests_branches.c +++ b/src/test/unit/unit_tests_branches.c @@ -1778,6 +1778,72 @@ START_TEST(test_sendto_udp_txbuf_full_eagain) } END_TEST +/* Queued datagram keeps its enqueue-time destination: a descriptor left in + * the txbuf after a failed flush must not be re-targeted to the destination + * of a later sendto() on the same socket. */ +START_TEST(test_udp_sendto_queued_datagram_keeps_enqueue_dst) +{ + struct wolfIP s; + int udp_sd; + struct wolfIP_sockaddr_in sin; + static const uint8_t mac_a[6] = {0x02, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE}; + static const uint8_t mac_b[6] = {0x03, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE}; + uint8_t payload_a[8]; + uint8_t payload_b[8]; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + udp_sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + ck_assert_int_gt(udp_sd, 0); + + /* Resolve both destinations up front so the flush does not stall on ARP. */ + arp_store_neighbor(&s, TEST_PRIMARY_IF, 0x0A000002U, mac_a); + arp_store_neighbor(&s, TEST_PRIMARY_IF, 0x0A000003U, mac_b); + + memset(payload_a, 0xA5, sizeof(payload_a)); + memset(payload_b, 0x5A, sizeof(payload_b)); + mock_link_capture_reset(); + mock_send_eagain_armed = 1; + + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = ee16(9000); + sin.sin_addr.s_addr = ee32(0x0A000002U); + ck_assert_int_eq(wolfIP_sock_sendto(&s, udp_sd, payload_a, sizeof(payload_a), 0, + (struct wolfIP_sockaddr *)&sin, sizeof(sin)), (int)sizeof(payload_a)); + + /* The flush fails with -EAGAIN; the first descriptor stays queued. */ + wolfIP_poll(&s, 0); + ck_assert_uint_eq(mock_sent_frames_count, 0U); + + /* Second datagram on the same socket, different destination. */ + sin.sin_port = ee16(9001); + sin.sin_addr.s_addr = ee32(0x0A000003U); + ck_assert_int_eq(wolfIP_sock_sendto(&s, udp_sd, payload_b, sizeof(payload_b), 0, + (struct wolfIP_sockaddr *)&sin, sizeof(sin)), (int)sizeof(payload_b)); + + wolfIP_poll(&s, 0); + ck_assert_uint_eq(mock_sent_frames_count, 2U); + + { + const struct wolfIP_ip_packet *ip0 = + (const struct wolfIP_ip_packet *)mock_sent_frames[0]; + const struct wolfIP_udp_datagram *udp0 = + (const struct wolfIP_udp_datagram *)mock_sent_frames[0]; + const struct wolfIP_ip_packet *ip1 = + (const struct wolfIP_ip_packet *)mock_sent_frames[1]; + /* First transmitted frame is the first-enqueued datagram, carrying + * the destination and payload it had when it was queued. */ + ck_assert_uint_eq(ip0->dst, ee32(0x0A000002U)); + ck_assert_uint_eq(udp0->dst_port, ee16(9000)); + ck_assert_mem_eq(udp0->data, payload_a, sizeof(payload_a)); + /* Second transmitted frame is the second datagram. */ + ck_assert_uint_eq(ip1->dst, ee32(0x0A000003U)); + } +} +END_TEST + /* ---- wolfIP_sock_sendto: bound_local_ip mismatch on ICMP ---- */ START_TEST(test_sendto_icmp_no_remote_after_addr_zero) diff --git a/src/test/unit/unit_tests_dns_dhcp.c b/src/test/unit/unit_tests_dns_dhcp.c index 1f93cce5..0af55c95 100644 --- a/src/test/unit/unit_tests_dns_dhcp.c +++ b/src/test/unit/unit_tests_dns_dhcp.c @@ -1136,6 +1136,52 @@ START_TEST(test_dhcp_messages_set_secs_from_process_start) } END_TEST +START_TEST(test_dhcp_discover_first_retry_delay_rfc2131) +{ + struct wolfIP s; + uint64_t delay; + + wolfIP_init(&s); + mock_link_init(&s); + s.dhcp_xid = 1U; + s.last_tick = 1000U; + 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); + + ck_assert_int_eq(dhcp_send_discover(&s), 0); + ck_assert_int_ne(s.dhcp_timer, NO_TIMER); + delay = find_timer_expiry(&s, s.dhcp_timer) - s.last_tick; + /* RFC 2131 §4.1 (10 Mb/s Ethernet example): first retransmission + * at 4 s, randomized uniformly by plus or minus 1 s. */ + ck_assert_uint_ge(delay, 3000U); + ck_assert_uint_le(delay, 5000U); +} +END_TEST + +/* A configured discover base smaller than the jitter half-window must not + * underflow the centered jitter window (which would schedule the retry + * far in the future). */ +START_TEST(test_dhcp_discover_retry_delay_small_base_no_underflow) +{ + struct wolfIP s; + uint64_t delay; + + wolfIP_init(&s); + mock_link_init(&s); + + /* Default base: 4 s ± 1 s window. */ + delay = dhcp_discover_retry_delay(&s, 4000U); + ck_assert_uint_ge(delay, 3000U); + ck_assert_uint_le(delay, 5000U); + + /* Base below the jitter half-window stays bounded. */ + delay = dhcp_discover_retry_delay(&s, 200U); + ck_assert_uint_le(delay, 2000U); +} +END_TEST + + START_TEST(test_sock_connect_tcp_src_port_low) { struct wolfIP s; @@ -3052,8 +3098,10 @@ START_TEST(test_dns_send_query_invalid_name) memcpy(name + 65, "com", 3); name[68] = 0; ck_assert_int_eq(dns_send_query(&s, name, &id, DNS_A), -22); + /* The failed encode must not leave the resolver armed. */ + ck_assert_uint_eq(s.dns_id, 0); + ck_assert_uint_eq(id, DNS_ID_NONE); - s.dns_id = 0; memset(name, 'a', sizeof(name)); pos = 0; memset(name + pos, 'a', 63); @@ -3069,6 +3117,336 @@ START_TEST(test_dns_send_query_invalid_name) pos += 63; name[pos] = 0; ck_assert_int_eq(dns_send_query(&s, name, &id, DNS_A), -22); + ck_assert_uint_eq(s.dns_id, 0); + ck_assert_uint_eq(id, DNS_ID_NONE); + + /* A subsequent lookup must not be blocked by the failed ones. */ + ck_assert_int_eq(dns_send_query(&s, "example.com", &id, DNS_A), 0); + ck_assert_uint_ne(s.dns_id, 0); + dns_abort_query(&s); + ck_assert_uint_eq(s.dns_id, 0); +} +END_TEST + +/* A zero-length label is the wire-format root terminator (RFC 1035 + * §3.1); it is only legal at the end of the name. Leading or interior + * dots must be rejected, while the trailing-dot FQDN presentation form + * stays valid. */ +START_TEST(test_dns_send_query_rejects_empty_labels) +{ + struct wolfIP s; + uint16_t id = 0; + + wolfIP_init(&s); + mock_link_init(&s); + s.dns_server = 0x08080808U; + + /* Leading dot. */ + ck_assert_int_eq(dns_send_query(&s, ".com", &id, DNS_A), -22); + ck_assert_uint_eq(s.dns_id, 0); + ck_assert_uint_eq(id, DNS_ID_NONE); + + /* Interior dot. */ + ck_assert_int_eq(dns_send_query(&s, "a..com", &id, DNS_A), -22); + ck_assert_uint_eq(s.dns_id, 0); + ck_assert_uint_eq(id, DNS_ID_NONE); + + /* Trailing root dot: valid FQDN presentation form. */ + ck_assert_int_eq(dns_send_query(&s, "example.com.", &id, DNS_A), 0); + ck_assert_uint_ne(s.dns_id, 0); + dns_abort_query(&s); + ck_assert_uint_eq(s.dns_id, 0); +} +END_TEST + +/* RFC 1035 §3.4.1: A RDATA is exactly a 32-bit address. An A RR whose + * RDLENGTH is not 4 is malformed and must not be accepted as the answer; + * the query stays outstanding for the retry/timeout path. */ +START_TEST(test_dns_callback_rejects_a_record_with_wrong_rdlength) +{ + struct wolfIP s; + uint8_t response[192]; + int pos; + struct dns_header *hdr = (struct dns_header *)response; + struct dns_question *q; + struct dns_rr *rr; + struct tsocket *ts; + + wolfIP_init(&s); + mock_link_init(&s); + s.dns_server = 0x0A000001U; + arm_dns_query(&s, 0x1234, dns_qname_a_com, (int)sizeof(dns_qname_a_com), + DNS_A); + s.dns_lookup_cb = test_dns_lookup_cb; + dns_lookup_ip = 0; + s.dns_udp_sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, + WI_IPPROTO_UDP); + ck_assert_int_gt(s.dns_udp_sd, 0); + ts = &s.udpsockets[SOCKET_UNMARK(s.dns_udp_sd)]; + + memset(response, 0, sizeof(response)); + hdr->id = ee16(s.dns_id); + hdr->flags = ee16(0x8100); + hdr->qdcount = ee16(1); + hdr->ancount = ee16(1); + pos = (int)sizeof(struct dns_header); + response[pos++] = 1; response[pos++] = 'a'; + response[pos++] = 3; memcpy(&response[pos], "com", 3); pos += 3; + response[pos++] = 0; + q = (struct dns_question *)(response + pos); + q->qtype = ee16(DNS_A); + q->qclass = ee16(1); + pos += (int)sizeof(struct dns_question); + response[pos++] = 0xC0; + response[pos++] = (uint8_t)sizeof(struct dns_header); + rr = (struct dns_rr *)(response + pos); + rr->type = ee16(DNS_A); + rr->class = ee16(1); + rr->ttl = ee32(60); + rr->rdlength = ee16(5); /* malformed: A RDATA is exactly 4 bytes */ + pos += (int)sizeof(struct dns_rr); + response[pos++] = 0x0A; + response[pos++] = 0x00; + response[pos++] = 0x00; + response[pos++] = 0x02; + response[pos++] = 0x55; /* trailing junk beyond the 4-byte address */ + + enqueue_udp_rx(ts, response, (uint16_t)pos, DNS_PORT); + dns_callback(s.dns_udp_sd, CB_EVENT_READABLE, &s); + + /* Not delivered as an answer, and the query not retired. */ + ck_assert_uint_eq(dns_lookup_ip, 0); + ck_assert_int_ne(s.dns_query_type, DNS_QUERY_TYPE_NONE); + ck_assert_uint_ne(s.dns_id, 0); + dns_abort_query(&s); +} +END_TEST + +/* RFC 2132 §9.3: option 52 (Overload) marks the reply's sname (value + * bit 2) and/or file (value bit 1) fields as carrying additional + * options. The option stream continues into those fields after the + * standard options field is exhausted, each with its own bounds, so an + * OFFER whose server identifier is overloaded must still be accepted. */ +START_TEST(test_dhcp_parse_offer_option_overload) +{ + struct wolfIP s; + struct dhcp_msg msg; + struct dhcp_option *field_opt; + uint8_t *opt; + uint32_t opt_len; + int ret; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + s.dhcp_xid = 0x1234; + + /* --- Scenario 1: overload = sname (value 2) --- + * Options field: OFFER, mask, overload; no END — the stream + * continues into the sname field, which holds the server id. */ + 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] = DHCP_OPTION_SUBNET_MASK; opt[1] = 4; + opt[2] = 0xFF; opt[3] = 0xFF; opt[4] = 0xFF; opt[5] = 0x00; + opt += 6; + opt[0] = 52 /* DHCP_OPTION_OVERLOAD */; opt[1] = 1; opt[2] = 2; + opt += 3; + opt_len = (uint32_t)(opt - (uint8_t *)msg.options); + 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_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); + ck_assert_uint_eq(s.dhcp_ip, 0x0A00000AU); + ck_assert_int_eq(s.dhcp_state, DHCP_REQUEST_SENT); + ck_assert_uint_eq(s.ipconf[TEST_PRIMARY_IF].mask, 0xFFFFFF00U); + + /* --- Scenario 2: overload = file (value 1) --- */ + 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] = 1; + opt += 3; + opt_len = (uint32_t)(opt - (uint8_t *)msg.options); + field_opt = (struct dhcp_option *)msg.file; + 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_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); + + /* --- Scenario 3: overload = both (value 3): the stream runs + * options -> sname -> file; the mask is in sname, the server id in + * file. */ + 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_len = (uint32_t)(opt - (uint8_t *)msg.options); + field_opt = (struct dhcp_option *)msg.sname; + 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 = 0; /* pad until the file field */ + field_opt->len = 0; + field_opt = (struct dhcp_option *)msg.file; + 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_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); + ck_assert_uint_eq(s.ipconf[TEST_PRIMARY_IF].mask, 0xFFFFFF00U); +} +END_TEST + +/* An option split across a region boundary is a continuation of the + * option stream (RFC 2132 §9.3), not the start of a new option list: + * the server id must be parsed across the split in each boundary + * shape. */ +START_TEST(test_dhcp_parse_offer_option_split_across_region_boundary) +{ + struct wolfIP s; + struct dhcp_msg msg; + uint8_t *opt; + uint8_t *sn; + uint8_t *fl; + uint32_t opt_len; + int ret; + + /* --- Scenario 1: the server id's code byte is the last byte of + * the options field; its length, data and END continue in sname. --- */ + 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] = 2; + opt += 3; + *opt++ = DHCP_OPTION_SERVER_ID; /* last byte of the options field */ + opt_len = (uint32_t)(opt - (uint8_t *)msg.options); + sn = (uint8_t *)msg.sname; + sn[0] = 4; /* length continues in sname */ + sn[1] = 0x0A; sn[2] = 0x00; sn[3] = 0x00; sn[4] = 0x64; + sn[5] = DHCP_OPTION_END; + + 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.dhcp_ip, 0x0A00000AU); + ck_assert_int_eq(s.dhcp_state, DHCP_REQUEST_SENT); + + /* --- Scenario 2: code + length are the last two bytes of the + * options field; the four data bytes split 2 + 2 into sname. --- */ + 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] = 2; + opt += 3; + *opt++ = DHCP_OPTION_SERVER_ID; + *opt++ = 4; /* last two bytes of the options field */ + opt_len = (uint32_t)(opt - (uint8_t *)msg.options); + sn = (uint8_t *)msg.sname; + sn[0] = 0x0A; sn[1] = 0x00; /* first half of the data */ + sn[2] = 0x00; sn[3] = 0x64; /* second half */ + sn[4] = DHCP_OPTION_END; + + 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); + + /* --- Scenario 3: overload = both; the server id starts at the + * end of sname (pads precede it) and its data runs into file. --- */ + 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_len = (uint32_t)(opt - (uint8_t *)msg.options); + sn = (uint8_t *)msg.sname; + fl = (uint8_t *)msg.file; + sn[60] = DHCP_OPTION_SERVER_ID; sn[61] = 4; + sn[62] = 0x0A; sn[63] = 0x00; /* data starts in sname */ + fl[0] = 0x00; fl[1] = 0x64; /* ...and ends in file */ + fl[2] = DHCP_OPTION_END; + + 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); } END_TEST START_TEST(test_fifo_push_and_pop) { @@ -3190,7 +3568,8 @@ START_TEST(test_sock_accept_ack_with_payload_completes_handshake) seg->dst_port = ee16(local_port); base_seq = new_ts->sock.tcp.ack; seg->seq = ee32(base_seq); - seg->ack = ee32(new_ts->sock.tcp.seq); + /* The client's ACK is at the server's snd.nxt: ISN+1. */ + seg->ack = ee32(tcp_seq_inc(new_ts->sock.tcp.snd_una, 1)); seg->hlen = TCP_HEADER_LEN << 2; seg->flags = TCP_FLAG_ACK; memcpy(seg->data, payload, sizeof(payload)); @@ -3250,7 +3629,8 @@ START_TEST(test_sock_accept_ack_at_snd_nxt_completes_handshake) ackseg.src_port = ee16(remote_port); ackseg.dst_port = ee16(local_port); ackseg.seq = ee32(new_ts->sock.tcp.ack); - ackseg.ack = ee32(new_ts->sock.tcp.seq); + /* The client's final ACK is at the server's snd.nxt: ISN+1. */ + ackseg.ack = ee32(tcp_seq_inc(new_ts->sock.tcp.snd_una, 1)); ackseg.hlen = TCP_HEADER_LEN << 2; ackseg.flags = TCP_FLAG_ACK; fix_tcp_checksums(&ackseg); @@ -3311,7 +3691,8 @@ START_TEST(test_sock_accept_ack_psh_with_payload_completes_handshake) seg->dst_port = ee16(local_port); base_seq = new_ts->sock.tcp.ack; seg->seq = ee32(base_seq); - seg->ack = ee32(new_ts->sock.tcp.seq); + /* The client's ACK is at the server's snd.nxt: ISN+1. */ + seg->ack = ee32(tcp_seq_inc(new_ts->sock.tcp.snd_una, 1)); seg->hlen = TCP_HEADER_LEN << 2; seg->flags = (TCP_FLAG_ACK | TCP_FLAG_PSH); memcpy(seg->data, payload, sizeof(payload)); @@ -3681,6 +4062,59 @@ START_TEST(test_tcp_rto_cb_syn_sent_requeues_syn_and_arms_timer) } END_TEST +/* RFC 9293 §3.5: the R2 retransmission timeout for SYN segments defaults + * to 3 minutes, so an unanswered active-open SYN must not be abandoned + * before 180 s have elapsed. */ +START_TEST(test_tcp_syn_retransmit_duration_meets_rfc9293_r2) +{ + struct wolfIP s; + struct tsocket *ts; + uint64_t expiry = 0; + int found = 0; + int iterations = 0; + int i; + + wolfIP_init(&s); + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->sock.tcp.state = TCP_SYN_SENT; + ts->sock.tcp.rto = TCP_RTO_MIN_MS; + ts->sock.tcp.ctrl_rto_active = 1; + ts->src_port = 12345; + ts->dst_port = 5001; + ts->local_ip = 0x0A000001U; + ts->remote_ip = 0x0A000002U; + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + /* The first SYN went out at t=0 with the control RTO armed. */ + s.last_tick = 0; + tcp_ctrl_rto_start(ts, 0); + + /* No answer ever arrives: drive the retransmission timer until the + * stack gives up. */ + while (ts->sock.tcp.state == TCP_SYN_SENT && iterations < 64) { + expiry = 0; + found = 0; + for (i = 0; i < (int)s.timers.size; i++) { + if (s.timers.timers[i].id == ts->sock.tcp.tmr_rto) { + expiry = s.timers.timers[i].expires; + found = 1; + break; + } + } + ck_assert_int_eq(found, 1); + s.last_tick = expiry; + tcp_rto_cb(ts); + iterations++; + } + + ck_assert_int_ne(ts->sock.tcp.state, TCP_SYN_SENT); + ck_assert_uint_ge(s.last_tick, 180000); +} +END_TEST + START_TEST(test_tcp_input_synack_cancels_control_rto) { struct wolfIP s; diff --git a/src/test/unit/unit_tests_ip_arp_recv.c b/src/test/unit/unit_tests_ip_arp_recv.c index e2705a87..14f1b46c 100644 --- a/src/test/unit/unit_tests_ip_arp_recv.c +++ b/src/test/unit/unit_tests_ip_arp_recv.c @@ -237,6 +237,227 @@ START_TEST(test_ip_recv_forward_arp_hit_sends_immediately) } END_TEST +/* A transit packet whose destination is reachable only through a static + * route (not a connected subnet) must be forwarded out the route's + * interface, resolved to the route's gateway for ARP, with the IP + * destination untouched. */ +START_TEST(test_ip_recv_forward_static_route_uses_gateway) +{ + struct wolfIP s; + uint8_t frame[ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN]; + struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)frame; + struct wolfIP_ip_packet *sent; + ip4 primary_ip = 0x0A000001U; /* 10.0.0.1 on if1 */ + ip4 secondary_ip = 0xC0A80101U; /* 192.168.1.1 on if2 */ + ip4 dest_ip = 0x0A020005U; /* 10.2.0.5 — static-route only */ + ip4 src_ip = 0x0A000002U; + ip4 gw_ip = 0xC0A801FEU; /* 192.168.1.254, route gateway */ + static const uint8_t gw_mac[6] = {0x10, 0x11, 0x12, 0x13, 0x14, 0x15}; + + setup_stack_with_two_ifaces(&s, primary_ip, secondary_ip); + wolfIP_filter_set_callback(NULL, NULL); + ck_assert_int_eq(wolfIP_route_add(&s, TEST_SECOND_IF, 0x0A020000U, 24, + gw_ip), 0); + + /* ARP is known for the gateway; the final destination is not on-link + * and has no cache entry. */ + arp_store_neighbor(&s, TEST_SECOND_IF, gw_ip, gw_mac); + + last_frame_sent_size = 0; + + memset(frame, 0, sizeof(frame)); + memcpy(ip->eth.dst, s.ll_dev[TEST_PRIMARY_IF].mac, 6); + memcpy(ip->eth.src, "\x01\x02\x03\x04\x05\x06", 6); + ip->eth.type = ee16(ETH_TYPE_IP); + ip->ver_ihl = 0x45; + ip->ttl = 64; + ip->proto = WI_IPPROTO_UDP; + ip->len = ee16(IP_HEADER_LEN + UDP_HEADER_LEN); + ip->src = ee32(src_ip); + ip->dst = ee32(dest_ip); + fix_ip_checksum(ip); + { + uint16_t *udp = (uint16_t *)(frame + ETH_HEADER_LEN + IP_HEADER_LEN); + udp[0] = ee16(9999); udp[1] = ee16(53); + udp[2] = ee16(UDP_HEADER_LEN); udp[3] = 0; + } + + ip_recv(&s, TEST_PRIMARY_IF, ip, (uint32_t)sizeof(frame)); + + /* Forwarded out if2, addressed to the gateway, IP dst preserved and + * TTL decremented. */ + ck_assert_uint_gt(last_frame_sent_size, 0); + ck_assert_mem_eq(last_frame_sent + 0, gw_mac, 6); + sent = (struct wolfIP_ip_packet *)last_frame_sent; + ck_assert_uint_eq(ee32(sent->dst), dest_ip); + ck_assert_uint_eq(sent->ttl, 63); +} +END_TEST + +/* A transit packet whose only matching static route points back at the + * ingress interface must be dropped (hairpin), not forwarded back onto + * the interface it arrived on. */ +START_TEST(test_ip_recv_forward_static_route_hairpin_dropped) +{ + struct wolfIP s; + uint8_t frame[ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN]; + struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)frame; + uint16_t *udp; + ip4 primary_ip = 0x0A000001U; /* 10.0.0.1 on if1 (ingress) */ + ip4 secondary_ip = 0xC0A80101U; /* 192.168.1.1 on if2 */ + ip4 dest_ip = 0x0A020005U; /* 10.2.0.5 — route on if1 only */ + ip4 src_ip = 0x0A000002U; + + setup_stack_with_two_ifaces(&s, primary_ip, secondary_ip); + wolfIP_filter_set_callback(NULL, NULL); + ck_assert_int_eq(wolfIP_route_add(&s, TEST_PRIMARY_IF, 0x0A020000U, 24, + IPADDR_ANY), 0); + + last_frame_sent_size = 0; + + memset(frame, 0, sizeof(frame)); + memcpy(ip->eth.dst, s.ll_dev[TEST_PRIMARY_IF].mac, 6); + memcpy(ip->eth.src, "\x01\x02\x03\x04\x05\x06", 6); + ip->eth.type = ee16(ETH_TYPE_IP); + ip->ver_ihl = 0x45; + ip->ttl = 64; + ip->proto = WI_IPPROTO_UDP; + ip->len = ee16(IP_HEADER_LEN + UDP_HEADER_LEN); + ip->src = ee32(src_ip); + ip->dst = ee32(dest_ip); + fix_ip_checksum(ip); + udp = (uint16_t *)(frame + ETH_HEADER_LEN + IP_HEADER_LEN); + udp[0] = ee16(9999); udp[1] = ee16(53); + udp[2] = ee16(UDP_HEADER_LEN); udp[3] = 0; + + ip_recv(&s, TEST_PRIMARY_IF, ip, (uint32_t)sizeof(frame)); + + /* The route matches but points at the ingress interface: drop. */ + ck_assert_uint_eq(last_frame_sent_size, 0); +} +END_TEST + +/* A 0.0.0.0/0 default route must catch transit destinations that no + * connected subnet covers and forward them via the route's gateway. */ +START_TEST(test_ip_recv_forward_static_route_default) +{ + struct wolfIP s; + uint8_t frame[ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN]; + struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)frame; + struct wolfIP_ip_packet *sent; + uint16_t *udp; + ip4 primary_ip = 0x0A000001U; /* 10.0.0.1 on if1 */ + ip4 secondary_ip = 0xC0A80101U; /* 192.168.1.1 on if2 */ + ip4 dest_ip = 0x08080808U; /* 8.8.8.8 — default route only */ + ip4 src_ip = 0x0A000002U; + ip4 gw_ip = 0xC0A801FEU; /* 192.168.1.254, route gateway */ + static const uint8_t gw_mac[6] = {0x20, 0x21, 0x22, 0x23, 0x24, 0x25}; + + setup_stack_with_two_ifaces(&s, primary_ip, secondary_ip); + wolfIP_filter_set_callback(NULL, NULL); + ck_assert_int_eq(wolfIP_route_add(&s, TEST_SECOND_IF, 0x00000000U, 0, + gw_ip), 0); + + /* ARP is known for the gateway. */ + arp_store_neighbor(&s, TEST_SECOND_IF, gw_ip, gw_mac); + + last_frame_sent_size = 0; + + memset(frame, 0, sizeof(frame)); + memcpy(ip->eth.dst, s.ll_dev[TEST_PRIMARY_IF].mac, 6); + memcpy(ip->eth.src, "\x01\x02\x03\x04\x05\x06", 6); + ip->eth.type = ee16(ETH_TYPE_IP); + ip->ver_ihl = 0x45; + ip->ttl = 64; + ip->proto = WI_IPPROTO_UDP; + ip->len = ee16(IP_HEADER_LEN + UDP_HEADER_LEN); + ip->src = ee32(src_ip); + ip->dst = ee32(dest_ip); + fix_ip_checksum(ip); + udp = (uint16_t *)(frame + ETH_HEADER_LEN + IP_HEADER_LEN); + udp[0] = ee16(9999); udp[1] = ee16(53); + udp[2] = ee16(UDP_HEADER_LEN); udp[3] = 0; + + ip_recv(&s, TEST_PRIMARY_IF, ip, (uint32_t)sizeof(frame)); + + /* Forwarded out if2 to the gateway, IP dst preserved, TTL 63. */ + ck_assert_uint_gt(last_frame_sent_size, 0); + ck_assert_mem_eq(last_frame_sent + 0, gw_mac, 6); + sent = (struct wolfIP_ip_packet *)last_frame_sent; + ck_assert_uint_eq(ee32(sent->dst), dest_ip); + ck_assert_uint_eq(sent->ttl, 63); +} +END_TEST + +/* The declared IPv4 total length must match the bytes actually received: + * a frame whose header claims more payload than it carries is dropped + * (it must not be relayed with an inconsistent length), and a frame with + * trailing link-layer padding is forwarded at the declared length, not + * the padded frame length. */ +START_TEST(test_ip_recv_forward_declared_length_checked) +{ + struct wolfIP s; + uint8_t frame[ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN + 4]; + struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)frame; + ip4 primary_ip = 0x0A000001U; + ip4 secondary_ip = 0xC0A80101U; + ip4 dest_ip = 0xC0A80155U; /* 192.168.1.85 — local to if2 */ + ip4 src_ip = 0x0A000002U; + static const uint8_t dest_mac[6] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; + uint32_t ip_len = IP_HEADER_LEN + UDP_HEADER_LEN; + + setup_stack_with_two_ifaces(&s, primary_ip, secondary_ip); + wolfIP_filter_set_callback(NULL, NULL); + arp_store_neighbor(&s, TEST_SECOND_IF, dest_ip, dest_mac); + + /* Scenario 1: header declares 16 more payload bytes than the frame + * carries; the checksum is valid for the (inconsistent) header. */ + last_frame_sent_size = 0; + memset(frame, 0, sizeof(frame)); + memcpy(ip->eth.dst, s.ll_dev[TEST_PRIMARY_IF].mac, 6); + memcpy(ip->eth.src, "\x01\x02\x03\x04\x05\x06", 6); + ip->eth.type = ee16(ETH_TYPE_IP); + ip->ver_ihl = 0x45; + ip->ttl = 64; + ip->proto = WI_IPPROTO_UDP; + ip->len = ee16(ip_len + 16); + ip->src = ee32(src_ip); + ip->dst = ee32(dest_ip); + fix_ip_checksum(ip); + { + uint16_t *udp = (uint16_t *)(frame + ETH_HEADER_LEN + IP_HEADER_LEN); + udp[0] = ee16(9999); udp[1] = ee16(53); + udp[2] = ee16(UDP_HEADER_LEN); udp[3] = 0; + } + + ip_recv(&s, TEST_PRIMARY_IF, ip, ETH_HEADER_LEN + ip_len); + ck_assert_uint_eq(last_frame_sent_size, 0); + + /* Scenario 2: well-formed datagram plus 4 bytes of link-layer + * padding; the forwarded frame must be exactly the declared length. */ + last_frame_sent_size = 0; + memset(frame, 0, sizeof(frame)); + memcpy(ip->eth.dst, s.ll_dev[TEST_PRIMARY_IF].mac, 6); + memcpy(ip->eth.src, "\x01\x02\x03\x04\x05\x06", 6); + ip->eth.type = ee16(ETH_TYPE_IP); + ip->ver_ihl = 0x45; + ip->ttl = 64; + ip->proto = WI_IPPROTO_UDP; + ip->len = ee16(ip_len); + ip->src = ee32(src_ip); + ip->dst = ee32(dest_ip); + fix_ip_checksum(ip); + { + uint16_t *udp = (uint16_t *)(frame + ETH_HEADER_LEN + IP_HEADER_LEN); + udp[0] = ee16(9999); udp[1] = ee16(53); + udp[2] = ee16(UDP_HEADER_LEN); udp[3] = 0; + } + + ip_recv(&s, TEST_PRIMARY_IF, ip, ETH_HEADER_LEN + ip_len + 4); + ck_assert_uint_eq(last_frame_sent_size, ETH_HEADER_LEN + ip_len); +} +END_TEST + /* ========================================================================= * ip_recv: forward interface with no configured IP is skipped * ========================================================================= diff --git a/src/test/unit/unit_tests_proto.c b/src/test/unit/unit_tests_proto.c index 01a279ca..f13dbfc4 100644 --- a/src/test/unit/unit_tests_proto.c +++ b/src/test/unit/unit_tests_proto.c @@ -6278,6 +6278,9 @@ START_TEST(test_regression_fast_recovery_cwnd_ssthresh_rfc5681) ts->sock.tcp.ssthresh = 20 * smss; ts->sock.tcp.peer_rwnd = 20 * smss; ts->sock.tcp.bytes_in_flight = flight_size; + /* The peer has advertised this window since the handshake, so the + * phase-1 ACKs repeat it rather than establish it. */ + ts->sock.tcp.last_peer_win = 65535; queue_init(&ts->sock.tcp.rxbuf, ts->rxmem, RXBUF_SIZE, ts->sock.tcp.ack); fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); @@ -6686,6 +6689,83 @@ START_TEST(test_regression_paws_drops_segment_without_timestamp_option) } END_TEST +/* RFC 7323 §5.3: PAWS verification takes precedence over the regular + * segment acceptability test. A replayed segment that fails the sequence + * test and omits the TSopt on a timestamp-synchronized connection is + * dropped silently, without the ACK an unacceptable segment would + * otherwise elicit. */ +START_TEST(test_regression_paws_preempts_acceptability_for_replayed_segment) +{ + struct wolfIP s; + struct tsocket *ts; + uint8_t buf[sizeof(struct wolfIP_tcp_seg) + 4]; + struct wolfIP_tcp_seg *seg = (struct wolfIP_tcp_seg *)buf; + uint8_t payload[4] = {0xDE, 0xAD, 0xBE, 0xEF}; + uint32_t tcp_hlen = TCP_HEADER_LEN; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + last_frame_sent_size = 0; + last_frame_sent_count = 0; + + s.arp.neighbors[0].ip = 0x0A000002U; + s.arp.neighbors[0].if_idx = TEST_PRIMARY_IF; + memcpy(s.arp.neighbors[0].mac, + (uint8_t[]){0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}, 6); + + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->if_idx = TEST_PRIMARY_IF; + ts->sock.tcp.state = TCP_ESTABLISHED; + ts->sock.tcp.ack = 100; + ts->sock.tcp.seq = 1000; + ts->sock.tcp.snd_una = 1000; + ts->sock.tcp.cwnd = TCP_MSS; + ts->sock.tcp.peer_rwnd = TCP_MSS; + ts->sock.tcp.ts_enabled = 1; + ts->sock.tcp.last_ts = ee32(5000); + ts->src_port = 1234; + ts->dst_port = 4321; + ts->local_ip = 0x0A000001U; + ts->remote_ip = 0x0A000002U; + queue_init(&ts->sock.tcp.rxbuf, ts->rxmem, RXBUF_SIZE, ts->sock.tcp.ack); + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + /* Replayed data behind rcv_nxt (fails the acceptability test) with no + * TSopt (PAWS violation). */ + memset(buf, 0, sizeof(buf)); + seg->ip.ver_ihl = 0x45; + seg->ip.ttl = 64; + seg->ip.proto = WI_IPPROTO_TCP; + seg->ip.len = ee16(IP_HEADER_LEN + tcp_hlen + sizeof(payload)); + seg->ip.src = ee32(ts->remote_ip); + seg->ip.dst = ee32(ts->local_ip); + seg->dst_port = ee16(ts->src_port); + seg->src_port = ee16(ts->dst_port); + seg->hlen = (uint8_t)(tcp_hlen << 2); + seg->flags = TCP_FLAG_ACK; + seg->seq = ee32(50); + seg->ack = ee32(ts->sock.tcp.seq); + seg->win = ee16(65535); + + memcpy(seg->data, payload, sizeof(payload)); + fix_tcp_checksums(seg); + + tcp_input(&s, TEST_PRIMARY_IF, seg, + (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + tcp_hlen + sizeof(payload))); + wolfIP_poll(&s, 1000); + + /* Dropped silently before the acceptability test runs: no reply frame, + * no payload delivery. */ + ck_assert_uint_eq(ts->sock.tcp.ack, 100); + ck_assert_uint_eq(queue_len(&ts->sock.tcp.rxbuf), 0); + ck_assert_uint_eq(last_frame_sent_count, 0); +} +END_TEST + /* RFC 7323 §3.2: LAST_ACK is a synchronized state, so a final ACK that * arrives without a TSopt is silently dropped instead of closing the * connection. */ diff --git a/src/test/unit/unit_tests_socket_api_arms.c b/src/test/unit/unit_tests_socket_api_arms.c index b203cc12..ab4763cf 100644 --- a/src/test/unit/unit_tests_socket_api_arms.c +++ b/src/test/unit/unit_tests_socket_api_arms.c @@ -930,6 +930,66 @@ START_TEST(test_sock_sendto_raw_hdrincl_dst_from_buf) } END_TEST +/* IP_HDRINCL: when the sendto() socket address overrides the destination + * of the caller-supplied header, the IPv4 header checksum must be + * recomputed; flush_raw_tx trusts the caller's header as-is. */ +START_TEST(test_raw_hdrincl_dst_override_recomputes_ip_checksum) +{ + struct wolfIP s; + int sd; + int one = 1; + uint8_t ip_buf[ETH_HEADER_LEN + IP_HEADER_LEN + 4]; + struct wolfIP_ip_packet *ip; + struct wolfIP_sockaddr_in sin; + struct wolfIP_ip_packet *sent; + uint8_t nh_mac[6] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; + int ret; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + + s.arp.neighbors[0].ip = 0x0A000003U; + s.arp.neighbors[0].if_idx = TEST_PRIMARY_IF; + memcpy(s.arp.neighbors[0].mac, nh_mac, 6); + + sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_RAW, WI_IPPROTO_UDP); + ck_assert_int_ge(sd, 0); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_HDRINCL, &one, sizeof(one)), 0); + + /* Header checksummed for destination A. */ + memset(ip_buf, 0, sizeof(ip_buf)); + ip = (struct wolfIP_ip_packet *)ip_buf; + ip->ver_ihl = 0x45; + ip->ttl = 64; + ip->proto = WI_IPPROTO_UDP; + ip->len = ee16(IP_HEADER_LEN + 4); + ip->src = ee32(0x0A000001U); + ip->dst = ee32(0x0A000002U); + iphdr_set_checksum(ip); + + /* sendto addresses the packet at B: the override must reach the wire + * with a checksum valid for B. */ + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_addr.s_addr = ee32(0x0A000003U); + last_frame_sent_size = 0; + ret = wolfIP_sock_sendto(&s, sd, + (uint8_t *)ip + ETH_HEADER_LEN, + IP_HEADER_LEN + 4, + 0, (const struct wolfIP_sockaddr *)&sin, + sizeof(sin)); + ck_assert_int_eq(ret, IP_HEADER_LEN + 4); + wolfIP_poll(&s, 10); + + ck_assert_uint_gt(last_frame_sent_size, 0); + sent = (struct wolfIP_ip_packet *)last_frame_sent; + ck_assert_uint_eq(ee32(sent->dst), 0x0A000003U); + ck_assert_int_eq(iphdr_verify_checksum(sent), 0); +} +END_TEST + START_TEST(test_sock_sendto_raw_invalid_fd) { struct wolfIP s; diff --git a/src/test/unit/unit_tests_tcp_flow.c b/src/test/unit/unit_tests_tcp_flow.c index 8fecb8ed..dab988fc 100644 --- a/src/test/unit/unit_tests_tcp_flow.c +++ b/src/test/unit/unit_tests_tcp_flow.c @@ -1005,6 +1005,31 @@ START_TEST(test_tcp_parse_options_parses_mss_sack_permitted_timestamp_and_two_sa } END_TEST +/* An explicitly advertised MSS is the peer's commitment about what it will + * receive; the 536 default applies only when no MSS option is present. */ +START_TEST(test_tcp_parse_options_keeps_sub_default_advertised_mss) +{ + uint8_t seg_buf[sizeof(struct wolfIP_tcp_seg) + 4]; + struct wolfIP_tcp_seg *seg = (struct wolfIP_tcp_seg *)seg_buf; + struct tcp_parsed_opts po; + uint32_t frame_len; + + memset(seg_buf, 0, sizeof(seg_buf)); + seg->hlen = (uint8_t)((TCP_HEADER_LEN + 4) << 2); + seg->data[0] = TCP_OPTION_MSS; + seg->data[1] = TCP_OPTION_MSS_LEN; + seg->data[2] = 0x01; + seg->data[3] = 0x2C; /* 300 */ + frame_len = ETH_HEADER_LEN + IP_HEADER_LEN + TCP_HEADER_LEN + 4; + + memset(&po, 0, sizeof(po)); + tcp_parse_options(seg, frame_len, &po); + + ck_assert_int_eq(po.mss_found, 1); + ck_assert_uint_eq(po.mss, 300U); +} +END_TEST + START_TEST(test_tcp_parse_options_ignores_unknown_option_kinds) { uint8_t seg_buf[sizeof(struct wolfIP_tcp_seg) + 8]; @@ -2501,6 +2526,134 @@ START_TEST(test_tcp_ack_cwnd_grows_when_payload_acked_is_mss_minus_options) } END_TEST +/* RFC 5681: a duplicate ACK carries no data and repeats the previously + * advertised receive window. Peer data segments that do not advance our + * snd_una (normal in bidirectional transfer) must not inflate the dup + * count into a spurious fast retransmit. */ +START_TEST(test_tcp_ack_data_segments_not_counted_as_dup_acks) +{ + struct wolfIP s; + struct tsocket *ts; + struct tcp_seg_buf segbuf; + struct wolfIP_tcp_seg *seg; + struct pkt_desc *desc; + uint8_t pbuf[sizeof(struct wolfIP_tcp_seg) + 8]; + struct wolfIP_tcp_seg *pseg = (struct wolfIP_tcp_seg *)pbuf; + uint32_t seq = 1000; + int i; + + wolfIP_init(&s); + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->sock.tcp.state = TCP_ESTABLISHED; + ts->sock.tcp.cwnd = TCP_MSS * 4; + ts->sock.tcp.ssthresh = TCP_MSS * 8; + ts->sock.tcp.snd_una = seq; + ts->sock.tcp.seq = seq + TCP_MSS; + ts->sock.tcp.bytes_in_flight = TCP_MSS; + ts->sock.tcp.peer_rwnd = TCP_MSS * 8; + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + /* One in-flight segment so the dup-ACK branch is reachable. */ + memset(&segbuf, 0, sizeof(segbuf)); + seg = &segbuf.seg; + seg->ip.len = ee16(IP_HEADER_LEN + TCP_HEADER_LEN + TCP_MSS); + seg->hlen = TCP_HEADER_LEN << 2; + seg->seq = ee32(seq); + 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; + + /* Three peer data segments, none acknowledging the in-flight one. */ + memset(pbuf, 0, sizeof(pbuf)); + pseg->ip.len = ee16(IP_HEADER_LEN + TCP_HEADER_LEN + 8); + pseg->hlen = TCP_HEADER_LEN << 2; + pseg->flags = TCP_FLAG_ACK; + pseg->win = ee16(65535); + pseg->ack = ee32(seq); + memset(pseg->data, 0x5A, 8); + for (i = 0; i < 3; i++) { + pseg->seq = ee32(500 + i * 8); + tcp_ack(ts, pseg); + } + + ck_assert_uint_eq(ts->sock.tcp.dup_acks, 0); + ck_assert_int_eq(ts->sock.tcp.fast_recovery, 0); + ck_assert_uint_eq(ts->sock.tcp.snd_una, seq); + ck_assert_uint_eq(ts->sock.tcp.bytes_in_flight, TCP_MSS); +} +END_TEST + +/* RFC 6298 §5.5: on a retransmission timeout the new RTO is min(2*RTO, G) + * with G the maximum timer value, 64 s. With a 2 s base RTO and six + * backoff doublings the uncapped interval would be 128 s; the re-armed + * timer must land at 64 s. */ +START_TEST(test_tcp_rto_backoff_capped_at_64s) +{ + struct wolfIP s; + struct tsocket *ts; + struct tcp_seg_buf segbuf; + struct pkt_desc *desc; + uint64_t now = 100000; + uint32_t seq = 1000; + + 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->if_idx = TEST_PRIMARY_IF; + ts->src_port = 1234; + ts->dst_port = 4321; + ts->local_ip = 0x0A000001U; + ts->remote_ip = 0x0A000002U; + ts->sock.tcp.state = TCP_ESTABLISHED; + ts->sock.tcp.snd_una = seq; + ts->sock.tcp.seq = seq + TCP_MSS; + ts->sock.tcp.bytes_in_flight = TCP_MSS; + ts->sock.tcp.rto = 2000; + ts->sock.tcp.rto_backoff = 6; + ts->sock.tcp.tmr_rto = NO_TIMER; + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + /* In-flight segment covering snd_una so the timeout retransmits and + * re-arms the RTO timer. */ + memset(&segbuf, 0, sizeof(segbuf)); + segbuf.seg.ip.len = ee16(IP_HEADER_LEN + TCP_HEADER_LEN + TCP_MSS); + segbuf.seg.hlen = TCP_HEADER_LEN << 2; + segbuf.seg.seq = ee32(seq); + 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; + + s.last_tick = now; + tcp_rto_cb(ts); + + ck_assert_int_ne(ts->sock.tcp.tmr_rto, NO_TIMER); + { + uint32_t armed_expires = 0; + int found = 0; + int i; + for (i = 0; i < (int)s.timers.size; i++) { + if (s.timers.timers[i].id == ts->sock.tcp.tmr_rto) { + armed_expires = s.timers.timers[i].expires; + found = 1; + break; + } + } + ck_assert_int_eq(found, 1); + ck_assert_uint_eq(armed_expires, now + TCP_RTO_BACKOFF_MAX_MS); + } +} +END_TEST + START_TEST(test_tcp_ack_inflight_deflate_sets_writable_without_acked_desc) { struct wolfIP s; @@ -2673,6 +2826,51 @@ START_TEST(test_tcp_connect_syn_advertises_interface_mss) } END_TEST +/* A TCP active OPEN has no defined destination semantics for broadcast or + * multicast addresses (multiple hosts would answer one SYN; there is no + * single peer for a group). connect() must reject them before mutating the + * socket, the same way the inbound SYN path rejects such sources. */ +START_TEST(test_tcp_connect_rejects_broadcast_multicast_dest) +{ + struct wolfIP s; + int tcp_sd; + struct tsocket *ts; + struct wolfIP_sockaddr_in sin; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + + tcp_sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_STREAM, WI_IPPROTO_TCP); + ck_assert_int_gt(tcp_sd, 0); + ts = &s.tcpsockets[SOCKET_UNMARK(tcp_sd)]; + + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = ee16(5004); + + /* Limited broadcast. */ + sin.sin_addr.s_addr = ee32(0xFFFFFFFFU); + ck_assert_int_eq(wolfIP_sock_connect(&s, tcp_sd, + (struct wolfIP_sockaddr *)&sin, sizeof(sin)), -WOLFIP_EINVAL); + ck_assert_int_eq(ts->sock.tcp.state, TCP_CLOSED); + ck_assert_uint_eq(ts->remote_ip, 0U); + + /* All-hosts multicast group. */ + sin.sin_addr.s_addr = ee32(0xE0000001U); + ck_assert_int_eq(wolfIP_sock_connect(&s, tcp_sd, + (struct wolfIP_sockaddr *)&sin, sizeof(sin)), -WOLFIP_EINVAL); + ck_assert_int_eq(ts->sock.tcp.state, TCP_CLOSED); + ck_assert_uint_eq(ts->remote_ip, 0U); + + /* The socket is reusable: a unicast destination still proceeds. */ + sin.sin_addr.s_addr = ee32(0x0A000002U); + ck_assert_int_eq(wolfIP_sock_connect(&s, tcp_sd, + (struct wolfIP_sockaddr *)&sin, sizeof(sin)), -WOLFIP_EAGAIN); + ck_assert_int_eq(ts->sock.tcp.state, TCP_SYN_SENT); +} +END_TEST + START_TEST(test_tcp_connect_syn_limits_options_to_small_mtu) { struct wolfIP s; diff --git a/src/test/unit/unit_tests_tcp_state.c b/src/test/unit/unit_tests_tcp_state.c index 7e05663e..0844c195 100644 --- a/src/test/unit/unit_tests_tcp_state.c +++ b/src/test/unit/unit_tests_tcp_state.c @@ -466,13 +466,13 @@ 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) +/* An explicitly advertised MSS is kept verbatim, even below the 536 IPv4 + * default: it is the peer's commitment about what it will receive, and the + * effective send MSS must not exceed it (RFC 9293 §3.7.1). */ +START_TEST(test_tcp_parse_options_mss_below_default_kept_verbatim) { uint8_t opts[] = { - TCP_OPTION_MSS, 4, 0x00, 0x01, /* MSS=1: below the 536 floor */ + TCP_OPTION_MSS, 4, 0x00, 0x01, /* MSS=1: below the 536 default */ TCP_OPTION_EOO }; struct wolfIP s; @@ -494,8 +494,8 @@ START_TEST(test_tcp_parse_options_mss_below_floor_clamped) 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); + /* The advertised value is recorded as-is. */ + ck_assert_uint_eq(ts->sock.tcp.peer_mss, 1U); } END_TEST @@ -2525,3 +2525,80 @@ START_TEST(test_last_ack_final_ack_delivers_close_event) ck_assert_uint_ne(socket_cb_last_events & CB_EVENT_CLOSED, 0); } END_TEST + +/* A SYN-ACK retransmitted by the control RTO on an accepted socket must + * repeat the ISN advertised by the original SYN-ACK; the peer's final ACK + * is validated against ISN+1, so a changed sequence number turns loss + * recovery into a RST. */ +START_TEST(test_accept_synack_retransmit_repeats_isn) +{ + struct wolfIP s; + int listen_sd; + struct tsocket *syn_rcvd = NULL; + struct tsocket *accepted; + struct wolfIP_sockaddr_in sin; + int syn_rcvd_sd; + int acc_sd; + int i; + uint32_t isn; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + listen_sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_STREAM, WI_IPPROTO_TCP); + ck_assert_int_gt(listen_sd, 0); + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = ee16(1234); + sin.sin_addr.s_addr = ee32(0x0A000001U); + ck_assert_int_eq(wolfIP_sock_bind(&s, listen_sd, + (struct wolfIP_sockaddr *)&sin, sizeof(sin)), 0); + ck_assert_int_eq(wolfIP_sock_listen(&s, listen_sd, 1), 0); + + /* Peer SYN triggers passive open; the SYN-ACK carries the clone's ISN. */ + inject_tcp_syn(&s, TEST_PRIMARY_IF, 0x0A000001U, 1234); + wolfIP_poll(&s, 1000); + for (i = 0; i < MAX_TCPSOCKETS; i++) { + if (s.tcpsockets[i].sock.tcp.state == TCP_SYN_RCVD) { + syn_rcvd = &s.tcpsockets[i]; + break; + } + } + ck_assert_ptr_nonnull(syn_rcvd); + isn = syn_rcvd->sock.tcp.seq; + ck_assert_uint_gt(last_frame_sent_size, 0U); + { + const struct wolfIP_tcp_seg *synack = + (const struct wolfIP_tcp_seg *)last_frame_sent; + ck_assert_uint_eq(ee32(synack->seq), isn); + } + + /* accept() sends a duplicate SYN-ACK from the new socket, same ISN. */ + syn_rcvd_sd = (int)(syn_rcvd - s.tcpsockets) | MARK_TCP_SOCKET; + acc_sd = wolfIP_sock_accept(&s, syn_rcvd_sd, NULL, 0); + ck_assert_int_gt(acc_sd, 0); + accepted = &s.tcpsockets[SOCKET_UNMARK(acc_sd)]; + ck_assert_int_eq(accepted->sock.tcp.state, TCP_SYN_RCVD); + /* Poll before the accept-armed control RTO (last_tick 1000 + 1000ms + * default) expires, so this flushes accept's own SYN-ACK. */ + wolfIP_poll(&s, 1500); + ck_assert_uint_gt(last_frame_sent_size, 0U); + { + const struct wolfIP_tcp_seg *synack2 = + (const struct wolfIP_tcp_seg *)last_frame_sent; + ck_assert_uint_eq(ee32(synack2->seq), isn); + } + + /* Control RTO retransmit: same flags, same ISN. */ + tcp_rto_cb(accepted); + wolfIP_poll(&s, 3000); + ck_assert_uint_gt(last_frame_sent_size, 0U); + { + const struct wolfIP_tcp_seg *retrans = + (const struct wolfIP_tcp_seg *)last_frame_sent; + ck_assert_int_eq(retrans->flags & (TCP_FLAG_SYN | TCP_FLAG_ACK), + TCP_FLAG_SYN | TCP_FLAG_ACK); + ck_assert_uint_eq(ee32(retrans->seq), isn); + } +} +END_TEST diff --git a/src/wolfip.c b/src/wolfip.c index e45bb504..d2cbcf7e 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -148,7 +148,12 @@ struct wolfIP_icmp_packet; /* Compatibility alias for legacy fixed-size uses. */ #define TCP_MSS TCP_MSS_MAX #define TCP_DEFAULT_MSS 536U -#define TCP_CTRL_RTO_MAXRTX 6U +/* Control-segment (SYN/SYN-ACK/FIN) retry budget. RFC 9293 §3.5 gives the + * R2 retransmission timeout for SYN and FIN segments a 3-minute default, + * so the retry schedule must accumulate at least 180 s: with the 1 s base + * RTO and the 64 s backoff cap the arms are 1,2,4,8,16,32,64,64,64 = 255 s + * before the 8th timeout gives up. */ +#define TCP_CTRL_RTO_MAXRTX 8U #define TCP_RTO_MAX_BACKOFF 15U /* Max retries before closing; also clamps shift */ #ifdef IP_MULTICAST @@ -160,6 +165,8 @@ struct wolfIP_icmp_packet; #define TCP_RTO_MIN_MS 1000U #define TCP_RTO_MAX_MS 60000U #define TCP_RTO_G_MS 1U +/* RFC 6298 §5.5: maximum timer value G; caps the backed-off RTO. */ +#define TCP_RTO_BACKOFF_MAX_MS 64000U #define TCP_PERSIST_MIN_MS 1000U #define TCP_PERSIST_MAX_MS 60000U #ifndef TCP_FIN_WAIT_2_TIMEOUT_MS @@ -831,9 +838,16 @@ static uint32_t wolfip_filter_mask_tcp; static uint32_t wolfip_filter_mask_udp; static uint32_t wolfip_filter_mask_icmp; static int wolfip_filter_lock; +/* Set by the first explicit mask configuration (any value, including 0) + * after the last uninstall; reset by uninstalling the callback. While + * unset, dispatch consults the callback for every reason so a freshly + * installed filter cannot fail open. */ +static int wolfip_filter_mask_touched; void wolfIP_filter_set_callback(wolfIP_filter_cb cb, void *arg) { + if (cb == NULL) + wolfip_filter_mask_touched = 0; wolfip_filter_cb = cb; wolfip_filter_arg = arg; } @@ -841,31 +855,37 @@ void wolfIP_filter_set_callback(wolfIP_filter_cb cb, void *arg) void wolfIP_filter_set_mask(uint32_t mask) { wolfip_filter_mask = mask; + wolfip_filter_mask_touched = 1; } void wolfIP_filter_set_eth_mask(uint32_t mask) { wolfip_filter_mask_eth = mask; + wolfip_filter_mask_touched = 1; } void wolfIP_filter_set_ip_mask(uint32_t mask) { wolfip_filter_mask_ip = mask; + wolfip_filter_mask_touched = 1; } void wolfIP_filter_set_tcp_mask(uint32_t mask) { wolfip_filter_mask_tcp = mask; + wolfip_filter_mask_touched = 1; } void wolfIP_filter_set_udp_mask(uint32_t mask) { wolfip_filter_mask_udp = mask; + wolfip_filter_mask_touched = 1; } void wolfIP_filter_set_icmp_mask(uint32_t mask) { wolfip_filter_mask_icmp = mask; + wolfip_filter_mask_touched = 1; } uint32_t wolfIP_filter_get_mask(void) @@ -911,6 +931,8 @@ static int wolfIP_filter_dispatch(enum wolfIP_filter_reason reason, mask = wolfip_filter_mask; else mask = wolfIP_filter_mask_for_proto(meta->ip_proto); + if (!wolfip_filter_mask_touched) + mask = ~0U; if ((mask & (1U << reason)) == 0) return 0; if (wolfip_filter_lock) @@ -1054,6 +1076,7 @@ static int wolfIP_filter_notify_icmp(enum wolfIP_filter_reason reason, #define DHCP_SERVER_PORT 67 #define DHCP_CLIENT_PORT 68 #define DHCP_OPTION_MSG_TYPE 53 +#define DHCP_OPTION_OVERLOAD 52 #define DHCP_OPTION_SUBNET_MASK 1 #define DHCP_OPTION_ROUTER 3 #define DHCP_OPTION_DNS 6 @@ -1064,7 +1087,10 @@ static int wolfIP_filter_notify_icmp(enum wolfIP_filter_reason reason, #define DHCP_OPTION_REBIND_TIME 59 #define DHCP_OPTION_OFFER_IP 50 #define DHCP_OPTION_END 0xFF -#define DHCP_DISCOVER_TIMEOUT 2000 +/* RFC 2131 §4.1 (10 Mb/s Ethernet example): the first DHCPDISCOVER + * retransmission is due 4 s after the initial send. */ +#define DHCP_DISCOVER_TIMEOUT 4000 +#define DHCP_DISCOVER_JITTER_MS 1000U /* uniform ±1 s */ #ifndef DHCP_DISCOVER_RETRIES #define DHCP_DISCOVER_RETRIES 3 #endif @@ -1141,6 +1167,9 @@ struct tcpsocket { ip4 local_ip, remote_ip; uint32_t peer_rwnd; uint16_t peer_mss; + /* Raw receive window advertised by the previously processed ACK; dup + * ACK detection compares against it (RFC 5681 condition e). */ + uint16_t last_peer_win; uint8_t snd_wscale, rcv_wscale, ws_enabled, ws_offer; uint8_t ts_enabled, ts_offer; uint8_t sack_offer, sack_permitted; @@ -3052,13 +3081,11 @@ 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; + /* Keep the advertised value as-is: it is the peer's + * commitment about what it will receive (RFC 9293 §3.7.1), + * and the effective send MSS must not exceed it. The 536 + * default is applied by the caller only when no MSS option + * is present. */ po->mss = mss; po->mss_found = 1; } @@ -3395,6 +3422,10 @@ static int tcp_send_empty_immediate(struct tsocket *t, struct wolfIP_tcp_seg *tc tcp->win = ee16(tcp_adv_win(t, 1)); ip_output_add_header(t, (struct wolfIP_ip_packet *)tcp, WI_IPPROTO_TCP, (uint16_t)(frame_len - ETH_HEADER_LEN)); +#ifdef ETHERNET + if (!wolfIP_ll_is_non_ethernet(t->S, tx_if)) + eth_output_add_header(t->S, tx_if, t->nexthop_mac, &tcp->ip.eth, ETH_TYPE_IP); +#endif if (wolfIP_filter_notify_tcp(WOLFIP_FILT_SENDING, t->S, tx_if, tcp, frame_len) != 0) return -1; @@ -3689,6 +3720,16 @@ static void tcp_ctrl_rto_stop(struct tsocket *t) t->sock.tcp.ctrl_rto_retries = 0; } +/* RFC 6298 §5.5: the RTO is doubled per retransmission timeout but MUST be + * capped at the maximum timer value G (64 s). */ +static uint32_t tcp_backoff_rto_ms(uint32_t rto_ms, uint32_t retries) +{ + uint64_t rto = (uint64_t)rto_ms << retries; + if (rto > TCP_RTO_BACKOFF_MAX_MS) + rto = TCP_RTO_BACKOFF_MAX_MS; + return (uint32_t)rto; +} + /* 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) @@ -3702,7 +3743,7 @@ static void tcp_ctrl_rto_start(struct tsocket *t, uint64_t now) timer_binheap_cancel(&t->S->timers, t->sock.tcp.tmr_rto); t->sock.tcp.tmr_rto = NO_TIMER; } - shift_rto = (uint64_t)t->sock.tcp.rto << t->sock.tcp.ctrl_rto_retries; + shift_rto = tcp_backoff_rto_ms(t->sock.tcp.rto, t->sock.tcp.ctrl_rto_retries); tmr.expires = now + shift_rto; tmr.arg = t; tmr.cb = tcp_rto_cb; @@ -3914,6 +3955,10 @@ static int tcp_send_zero_wnd_probe(struct tsocket *t) #endif ip_output_add_header(t, (struct wolfIP_ip_packet *)probe, WI_IPPROTO_TCP, (uint16_t)(IP_HEADER_LEN + TCP_HEADER_LEN + opt_len + 1)); +#ifdef ETHERNET + if (!wolfIP_ll_is_non_ethernet(t->S, tx_if)) + eth_output_add_header(t->S, tx_if, t->nexthop_mac, &probe->ip.eth, ETH_TYPE_IP); +#endif if (wolfIP_filter_notify_tcp(WOLFIP_FILT_SENDING, t->S, tx_if, probe, frame_len) != 0) return -1; @@ -4431,7 +4476,6 @@ static int ip_output_add_header(struct tsocket *t, struct wolfIP_ip_packet *ip, uint8_t proto, uint16_t len) { union transport_pseudo_header ph; - unsigned int if_idx; memset(&ph, 0, sizeof(ph)); memset(ip, 0, sizeof(struct wolfIP_ip_packet)); ip->src = ee32(t->local_ip); @@ -4473,15 +4517,10 @@ static int ip_output_add_header(struct tsocket *t, struct wolfIP_ip_packet *ip, icmp->csum = 0; icmp->csum = ee16(icmp_checksum(icmp, ee16(ph.ph.len))); } -#ifdef ETHERNET - if_idx = wolfIP_socket_if_idx(t); - if (!wolfIP_ll_is_non_ethernet(t->S, if_idx)) { - eth_output_add_header(t->S, if_idx, t->nexthop_mac, (struct wolfIP_eth_frame *)ip, - ETH_TYPE_IP); - } -#else - (void)if_idx; -#endif + /* The link-layer header is added by the caller once the egress interface + * and next-hop MAC are known. Datagram callers fill this in at enqueue + * time so a queued frame keeps the destination it was sent to; TCP fills + * it in per segment. */ return 0; } @@ -4760,6 +4799,12 @@ static void tcp_ack(struct tsocket *t, const struct wolfIP_tcp_seg *tcp) int recovery_exit_ack = 0; uint32_t inflight_pre = t->sock.tcp.bytes_in_flight; + /* Every caller presents an ACK-flagged segment, so each one is the new + * "previously received ACK" for dup detection. */ + uint16_t adv_win = ee16(tcp->win); + int win_changed = (adv_win != t->sock.tcp.last_peer_win); + t->sock.tcp.last_peer_win = adv_win; + if (t->sock.tcp.state == TCP_LAST_ACK && tcp_seq_leq(fin_acked, ack)) { tcp_ctrl_rto_stop(t); t->sock.tcp.state = TCP_CLOSED; @@ -4926,11 +4971,21 @@ static void tcp_ack(struct tsocket *t, const struct wolfIP_tcp_seg *tcp) t->events |= CB_EVENT_WRITABLE; } } else { - /* Duplicate ack (no advance in snd_una). */ + /* Duplicate ack (no advance in snd_una). RFC 5681: only a segment + * that carries no data and repeats the previously advertised + * receive window counts as a duplicate ACK, so data-bearing + * segments and window updates must not inflate the count or + * trigger fast retransmit. */ + uint32_t ip_len = ee16(tcp->ip.len); + uint32_t hdr_len = IP_HEADER_LEN + tcp_data_offset_bytes(tcp->hlen); if (ack != t->sock.tcp.snd_una) return; if (inflight_pre == 0) return; + if (ip_len > hdr_len) + return; + if (win_changed) + return; if (t->sock.tcp.dup_acks < 255) t->sock.tcp.dup_acks++; if (t->sock.tcp.peer_sack_count > 0 && @@ -5321,12 +5376,8 @@ static void tcp_input(struct wolfIP *S, unsigned int if_idx, tcp_send_ack(t); continue; } else if (t->sock.tcp.state == TCP_LAST_ACK) { - /* RFC 9293 s3.10.7.2: segment acceptability applies - * to all synchronized states including LAST_ACK. */ - if (!tcp_segment_acceptable(t, tcp, tcplen)) { - tcp_send_ack(t); - continue; - } + /* RFC 7323 §5.3: PAWS takes precedence over the regular + * acceptability test on synchronized connections. */ { int paws = tcp_paws_check(t, tcp, frame_len); if (paws == TCP_PAWS_ACK_DROP) @@ -5334,6 +5385,12 @@ static void tcp_input(struct wolfIP *S, unsigned int if_idx, if (paws != TCP_PAWS_OK) continue; } + /* RFC 9293 s3.10.7.2: segment acceptability applies + * to all synchronized states including LAST_ACK. */ + if (!tcp_segment_acceptable(t, tcp, tcplen)) { + tcp_send_ack(t); + continue; + } /* RFC 9293 §3.10.7.4: if the SYN bit is set on a * synchronized connection, send a challenge ACK and * drop the segment (RFC 5961). */ @@ -5354,11 +5411,8 @@ static void tcp_input(struct wolfIP *S, unsigned int if_idx, (t->sock.tcp.state == TCP_FIN_WAIT_1) || (t->sock.tcp.state == TCP_FIN_WAIT_2) || (t->sock.tcp.state == TCP_CLOSING)) { - if (!tcp_segment_acceptable(t, tcp, tcplen)) { - tcp_send_ack(t); - continue; - } - + /* RFC 7323 §5.3: PAWS takes precedence over the regular + * acceptability test on synchronized connections. */ { int paws = tcp_paws_check(t, tcp, frame_len); if (paws == TCP_PAWS_ACK_DROP) @@ -5367,6 +5421,11 @@ static void tcp_input(struct wolfIP *S, unsigned int if_idx, continue; } + if (!tcp_segment_acceptable(t, tcp, tcplen)) { + tcp_send_ack(t); + continue; + } + /* RFC 9293 §3.10.7.4: if the SYN bit is set on a * synchronized connection, send a challenge ACK and * drop the segment (RFC 5961). */ @@ -5623,7 +5682,8 @@ static void tcp_rto_cb(void *arg) ts->sock.tcp.recovery_point = ts->sock.tcp.snd_una; ptmr = &tmr; - ptmr->expires = ts->S->last_tick + (ts->sock.tcp.rto << ts->sock.tcp.rto_backoff); + ptmr->expires = ts->S->last_tick + + tcp_backoff_rto_ms(ts->sock.tcp.rto, ts->sock.tcp.rto_backoff); ptmr->arg = ts; ptmr->cb = tcp_rto_cb; ts->sock.tcp.tmr_rto = timers_binheap_insert(&ts->S->timers, *ptmr); @@ -5667,7 +5727,8 @@ static void tcp_resync_inflight(struct wolfIP *s, struct tsocket *ts, uint64_t n if (has_sent_payload && ts->sock.tcp.tmr_rto == NO_TIMER) { struct wolfIP_timer new_tmr = {}; new_tmr.cb = tcp_rto_cb; - new_tmr.expires = now + (ts->sock.tcp.rto << ts->sock.tcp.rto_backoff); + new_tmr.expires = now + tcp_backoff_rto_ms(ts->sock.tcp.rto, + ts->sock.tcp.rto_backoff); new_tmr.arg = ts; ts->sock.tcp.tmr_rto = timers_binheap_insert(&s->timers, new_tmr); } else if (!has_sent_payload && ts->sock.tcp.tmr_rto != NO_TIMER) { @@ -6001,6 +6062,14 @@ int wolfIP_sock_connect(struct wolfIP *s, int sockfd, const struct wolfIP_sockad uint8_t new_if_idx; ip4 new_local_ip; + /* A TCP connection is a single peer: broadcast destinations get + * answers from every host on the segment and multicast groups have + * no single peer, so an active OPEN to either is invalid. Reject + * before mutating the socket, mirroring the inbound SYN filter. */ + if (wolfIP_ip_is_broadcast(s, new_remote_ip) || + wolfIP_ip_is_multicast(new_remote_ip)) + return -WOLFIP_EINVAL; + /* Resolve and validate the local binding into locals before mutating * the socket. A failed validation here must not leave the socket in * TCP_SYN_SENT (no SYN queued, no RTO timer), which would make every @@ -6122,7 +6191,10 @@ int wolfIP_sock_accept(struct wolfIP *s, int sockfd, struct wolfIP_sockaddr *add return -WOLFIP_EAGAIN; } ts->events &= ~CB_EVENT_READABLE; - newts->sock.tcp.seq++; + /* Keep seq at the ISN while in SYN_RCVD: control RTO + * retransmits rebuild the SYN-ACK from seq, and a retransmitted + * SYN-ACK must repeat the original ISN. The final ACK handler + * advances seq to ISN+1 when the connection is established. */ newts->sock.tcp.ctrl_rto_retries = 0; tcp_ctrl_rto_start(newts, s->last_tick); if (sin) { @@ -6311,6 +6383,11 @@ int wolfIP_sock_sendto(struct wolfIP *s, int sockfd, const void *buf, size_t len udp->len = ee16(len + UDP_HEADER_LEN); udp->csum = 0; memcpy(udp->data, buf, len); + /* Pin the IP header to this datagram's destination/source while the + * socket's routing state still matches it; the flush only adds the + * link-layer header. */ + ip_output_add_header(ts, &udp->ip, WI_IPPROTO_UDP, + (uint16_t)(frame_len - ETH_HEADER_LEN)); if (fifo_push(&ts->sock.udp.txbuf, udp, frame_len) < 0) return -WOLFIP_EAGAIN; return len; @@ -6371,8 +6448,11 @@ int wolfIP_sock_sendto(struct wolfIP *s, int sockfd, const void *buf, size_t len memcpy(frame + sizeof(struct wolfIP_ip_packet), buf, payload_len); if (icmp->type == ICMP_ECHO_REQUEST) icmp_set_echo_id(icmp, ts->src_port); - icmp->csum = 0; - icmp->csum = ee16(icmp_checksum(icmp, (uint16_t)payload_len)); + /* Pin the IP header and ICMP checksum to this datagram's + * destination/source at enqueue time; the flush only adds the + * link-layer header. */ + ip_output_add_header(ts, &icmp->ip, WI_IPPROTO_ICMP, + (uint16_t)(frame_len - ETH_HEADER_LEN)); if (fifo_push(&ts->sock.udp.txbuf, icmp, frame_len) < 0) return -WOLFIP_EAGAIN; return (int)payload_len; @@ -6427,8 +6507,14 @@ int wolfIP_sock_sendto(struct wolfIP *s, int sockfd, const void *buf, size_t len total_len = (uint32_t)len + ETH_HEADER_LEN; if (dst_ip == 0) dst_ip = ee32(rip->dst); - else + else { + /* The override changes a field covered by the IPv4 header + * checksum, and the flush path trusts the caller's header + * as-is, so recompute it for the transmitted destination. */ rip->dst = ee32(dst_ip); + rip->csum = 0; + iphdr_set_checksum(rip); + } if (rs->remote_ip == 0 && dst_ip != 0) rs->remote_ip = dst_ip; rs->local_ip = ee32(rip->src); @@ -7825,9 +7911,10 @@ static void dhcp_schedule_timer_at(struct wolfIP *s, uint64_t when) } /* 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. */ + * prior attempt (dhcp_timeout_count), saturating at DHCP_BACKOFF_MAX_MS. + * Callers add their own 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; @@ -7838,7 +7925,22 @@ static uint64_t dhcp_backoff_delay(const struct wolfIP *s, uint32_t base_ms) delay = (uint64_t)base_ms << count; if (delay > DHCP_BACKOFF_MAX_MS) delay = DHCP_BACKOFF_MAX_MS; - return delay + (wolfIP_getrandom() % 200U); + return delay; +} + +/* DHCPDISCOVER retry delay: the backoff base with the RFC's uniform + * ±1 s jitter centered on it. When the configured base is smaller than + * the jitter half-window the window is anchored at zero instead, so a + * small base cannot underflow into a far-future retry. */ +static uint64_t dhcp_discover_retry_delay(const struct wolfIP *s, + uint32_t base_ms) +{ + uint64_t base = dhcp_backoff_delay(s, base_ms); + uint64_t jitter = wolfIP_getrandom() % (2U * DHCP_DISCOVER_JITTER_MS + 1U); + + base = (base > DHCP_DISCOVER_JITTER_MS) ? + base - DHCP_DISCOVER_JITTER_MS : 0U; + return base + jitter; } static void dhcp_schedule_retry_timer(struct wolfIP *s, uint64_t deadline) @@ -7847,7 +7949,8 @@ static void dhcp_schedule_retry_timer(struct wolfIP *s, uint64_t deadline) if (!s) return; - next = s->last_tick + dhcp_backoff_delay(s, DHCP_REQUEST_TIMEOUT); + next = s->last_tick + dhcp_backoff_delay(s, DHCP_REQUEST_TIMEOUT) + + (wolfIP_getrandom() % 200U); if (deadline != 0 && next > deadline) next = deadline; dhcp_schedule_timer_at(s, next); @@ -8025,10 +8128,185 @@ static void dhcp_deconfigure_lease(struct wolfIP *s) */ #define DHCP_DEFAULT_24BIT_NETMASK (0xFFFFFF00u) +/* Iterator over a DHCP reply's option stream. + * + * 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), + * 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. */ +struct dhcp_opt_stream { + uint8_t *ptr; + uint8_t *end; + uint8_t *region_sname; + uint8_t *region_sname_end; + uint8_t *region_file; + uint8_t *region_file_end; + uint8_t region; /* 0 options, 1 sname, 2 file */ + uint8_t overload; + int strict; /* -1 on malformed, or treat as end of stream */ + /* Option in progress across a region boundary, materialized here as + * it is read: the sname and file fields live in separate parts of + * the message, so an option whose bytes span a boundary cannot be + * handed out as a contiguous [code, len, data...] block in place. + * fill is NULL when no option is in progress; the buffer holds one + * full option at most (1 + 1 + 255 bytes). */ + uint8_t *fill; + uint8_t part_in_region0; + uint8_t part_buf[257]; +}; + +static void dhcp_opt_stream_init(struct dhcp_opt_stream *st, + struct dhcp_msg *msg, uint32_t msg_len, + int strict) +{ + uint32_t opt_space = msg_len - DHCP_HEADER_LEN; + if (opt_space > sizeof(msg->options)) + opt_space = sizeof(msg->options); + st->ptr = (uint8_t *)msg->options; + st->end = st->ptr + opt_space; + st->region_sname = (uint8_t *)msg->sname; + st->region_sname_end = st->region_sname + sizeof(msg->sname); + st->region_file = (uint8_t *)msg->file; + st->region_file_end = st->region_file + sizeof(msg->file); + st->region = 0; + st->overload = 0; + st->strict = strict; + st->fill = NULL; + st->part_in_region0 = 0; +} + +/* Advance the stream to the next region selected by option 52. Return 1 + * when a region was entered, 0 when no further region exists. */ +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; + } + } + else if (st->region == 1) { + if (st->overload & 1) { + st->region = 2; + st->ptr = st->region_file; + st->end = st->region_file_end; + return 1; + } + } + return 0; +} + +/* Return 1 with the out-params set to the next option (option 255 is + * delivered as code 255, len 0), 0 at end of stream (option 255 consumed + * or every active region exhausted without END), or -1 on malformed + * options in strict mode (non-strict callers get 0 instead). */ +static int dhcp_opt_stream_next(struct dhcp_opt_stream *st, uint8_t *code, + uint8_t *len, uint8_t **data) +{ + while (1) { + uint8_t c; + uint8_t l; + if (st->fill != NULL) { + /* Option in progress across region boundaries: pull bytes + * from the current region into part_buf until the option is + * complete. Its total size (2 + part_buf[1]) is known once + * the header bytes have been collected. */ + while (st->ptr < st->end && + (st->fill < st->part_buf + 2 || + st->fill < st->part_buf + 2 + st->part_buf[1])) + *st->fill++ = *st->ptr++; + if (st->fill >= st->part_buf + 2 && + st->fill >= st->part_buf + 2 + st->part_buf[1]) { + if (st->part_in_region0 && + st->part_buf[0] == DHCP_OPTION_OVERLOAD) { + if (st->part_buf[1] != 1 || st->part_buf[2] < 1 || + st->part_buf[2] > 3) { + st->fill = NULL; + if (st->strict) + return -1; + return 0; + } + st->overload = st->part_buf[2]; + } + *code = st->part_buf[0]; + *len = st->part_buf[1]; + *data = st->part_buf; + st->fill = NULL; + return 1; + } + /* Region exhausted mid-option: continue into the next + * overloaded region, or end the stream. */ + if (!dhcp_opt_stream_next_region(st)) { + st->fill = NULL; + if (st->strict) + return -1; + return 0; + } + continue; + } + if (st->ptr + 1 > st->end) + goto region_end; + c = st->ptr[0]; + if (c == DHCP_OPTION_END) { + *code = c; + *len = 0; + *data = st->ptr; + st->ptr = st->end; /* stream ends here */ + return 1; + } + if (c == 0) { /* Pad */ + st->ptr++; + continue; + } + if (st->ptr + 2 > st->end || + st->ptr + 2 + st->ptr[1] > st->end) { + /* Option header or data runs past the region end. RFC 2132 + * §9.3 makes the sname/file fields a continuation of the + * option stream, so materialize the option across the + * boundary instead of dropping it. */ + st->fill = st->part_buf; + st->part_in_region0 = (st->region == 0); + continue; + } + l = st->ptr[1]; + if (st->region == 0 && c == DHCP_OPTION_OVERLOAD) { + /* RFC 2132 §9.3: length 1, values 1 (file), 2 (sname), 3. */ + if (l != 1 || st->ptr[2] < 1 || st->ptr[2] > 3) + goto region_end; + st->overload = st->ptr[2]; + } + *code = c; + *len = l; + /* Points at the option's code byte, so callers may cast it to + * struct dhcp_option * (whose data[] member starts 2 bytes in). */ + *data = st->ptr; + st->ptr += 2 + l; + return 1; +region_end: + /* Region exhausted without END: continue into the overloaded + * fields (sname, then file) if option 52 selected them. */ + if (!dhcp_opt_stream_next_region(st)) { + if (st->strict) + return -1; + return 0; + } + } +} + static int dhcp_parse_offer(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_len) { - uint8_t *opt = (uint8_t *)msg->options; - uint8_t *opt_end; + struct dhcp_opt_stream st; int saw_end = 0; int saw_server_id = 0; uint32_t ip; @@ -8042,66 +8320,48 @@ static int dhcp_parse_offer(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg return -1; if (ee32(msg->xid) != s->dhcp_xid) return -1; - if (msg_len - DHCP_HEADER_LEN > sizeof(msg->options)) - opt_end = (uint8_t *)msg->options + sizeof(msg->options); - else - opt_end = (uint8_t *)msg->options + (msg_len - DHCP_HEADER_LEN); - while (opt < opt_end) { + dhcp_opt_stream_init(&st, msg, msg_len, 1); + while (1) { uint8_t code; uint8_t len; - if (opt + 1 > opt_end) + uint8_t *data; + int r = dhcp_opt_stream_next(&st, &code, &len, &data); + if (r < 0) + return -1; + if (r == 0) break; - code = opt[0]; if (code == DHCP_OPTION_END) { saw_end = 1; break; } - if (code == 0) { /* Pad */ - opt++; - continue; - } - if (opt + 2 > opt_end) - return -1; - len = opt[1]; - if (opt + 2 + len > opt_end) - return -1; if (code == DHCP_OPTION_MSG_TYPE) { if (len != 1) return -1; - if (opt[2] == DHCP_OFFER) { - opt += 2 + len; - saw_end = 0; - while (opt < opt_end) { - struct dhcp_option *inner; - if (opt + 1 > opt_end) + if (data[2] == DHCP_OFFER) { + while (1) { + uint8_t *idata; + r = dhcp_opt_stream_next(&st, &code, &len, &idata); + if (r < 0) + return -1; + if (r == 0) break; - code = opt[0]; if (code == DHCP_OPTION_END) { saw_end = 1; break; } - if (code == 0) { - opt++; - continue; - } - if (opt + 2 > opt_end) - return -1; - len = opt[1]; - if (opt + 2 + len > opt_end) - return -1; - inner = (struct dhcp_option *)opt; if (code == DHCP_OPTION_SERVER_ID) { if (len < 4) return -1; - s->dhcp_server_ip = DHCP_OPT_data_to_u32(inner); + s->dhcp_server_ip = + DHCP_OPT_data_to_u32((struct dhcp_option *)idata); saw_server_id = 1; } if (code == DHCP_OPTION_SUBNET_MASK) { if (len < 4) return -1; - netmask = DHCP_OPT_data_to_u32(inner); + netmask = + DHCP_OPT_data_to_u32((struct dhcp_option *)idata); } - opt += 2 + len; } if (!saw_end || !saw_server_id) return -1; @@ -8116,7 +8376,6 @@ static int dhcp_parse_offer(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg return 0; } } - opt += 2 + len; } if (!saw_end) return -1; @@ -8131,8 +8390,10 @@ static int dhcp_parse_offer(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg /* Return the DHCP message type from a validated message, or -1 on error. */ static int dhcp_msg_type(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_len) { - uint8_t *opt = (uint8_t *)msg->options; - uint8_t *opt_end; + struct dhcp_opt_stream st; + uint8_t code; + uint8_t len; + uint8_t *data; int msg_type = -1; int saw_server_id = 0; uint32_t server_id = 0; @@ -8144,31 +8405,18 @@ static int dhcp_msg_type(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_le return -1; if (msg->op != BOOT_REPLY) return -1; - if (msg_len - DHCP_HEADER_LEN > sizeof(msg->options)) - opt_end = (uint8_t *)msg->options + sizeof(msg->options); - else - opt_end = (uint8_t *)msg->options + (msg_len - DHCP_HEADER_LEN); - while (opt < opt_end) { - uint8_t code = opt[0]; - uint8_t len; + /* Non-strict: a truncated option stream ends the scan instead of + * failing the message. */ + dhcp_opt_stream_init(&st, msg, msg_len, 0); + while (dhcp_opt_stream_next(&st, &code, &len, &data) == 1) { if (code == DHCP_OPTION_END) break; - if (code == 0) { - opt++; - continue; - } - if (opt + 2 > opt_end) - break; - len = opt[1]; - if (opt + 2 + len > opt_end) - break; if (code == DHCP_OPTION_MSG_TYPE && len == 1) { - msg_type = opt[2]; + msg_type = data[2]; } else if (code == DHCP_OPTION_SERVER_ID && len >= 4) { - server_id = DHCP_OPT_data_to_u32((struct dhcp_option *)opt); + server_id = DHCP_OPT_data_to_u32((struct dhcp_option *)data); saw_server_id = 1; } - opt += 2 + len; } /* Reject a reply that does not carry the server identifier of the * server we committed to during the OFFER phase. */ @@ -8180,8 +8428,7 @@ static int dhcp_msg_type(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_le static int dhcp_parse_ack(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_len) { - uint8_t *opt = (uint8_t *)msg->options; - uint8_t *opt_end; + struct dhcp_opt_stream st; int saw_end = 0; int saw_server_id = 0; struct ipconf *primary = wolfIP_primary_ipconf(s); @@ -8196,99 +8443,79 @@ static int dhcp_parse_ack(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_l return -1; if (ee32(msg->xid) != s->dhcp_xid) return -1; - if (msg_len - DHCP_HEADER_LEN > sizeof(msg->options)) - opt_end = (uint8_t *)msg->options + sizeof(msg->options); - else - opt_end = (uint8_t *)msg->options + (msg_len - DHCP_HEADER_LEN); - while (opt < opt_end) { + dhcp_opt_stream_init(&st, msg, msg_len, 1); + while (1) { uint8_t code; uint8_t len; - if (opt + 1 > opt_end) + uint8_t *data; + int r = dhcp_opt_stream_next(&st, &code, &len, &data); + if (r < 0) + return -1; + if (r == 0) break; - code = opt[0]; if (code == DHCP_OPTION_END) { saw_end = 1; break; } - if (code == 0) { /* Pad */ - opt++; - continue; - } - if (opt + 2 > opt_end) - return -1; - len = opt[1]; - if (opt + 2 + len > opt_end) - return -1; if (code == DHCP_OPTION_MSG_TYPE) { if (len != 1) return -1; - if (opt[2] == DHCP_ACK) { - opt += 2 + len; - saw_end = 0; - while (opt < opt_end) { - struct dhcp_option *inner; - uint32_t data; - if (opt + 1 > opt_end) + if (data[2] == DHCP_ACK) { + while (1) { + uint8_t *idata; + uint32_t val; + r = dhcp_opt_stream_next(&st, &code, &len, &idata); + if (r < 0) + return -1; + if (r == 0) break; - code = opt[0]; if (code == DHCP_OPTION_END) { saw_end = 1; break; } - if (code == 0) { - opt++; - continue; - } - if (opt + 2 > opt_end) - return -1; - len = opt[1]; - if (opt + 2 + len > opt_end) - return -1; - inner = (struct dhcp_option *)opt; if (code == DHCP_OPTION_SERVER_ID) { if (len < 4) return -1; - data = DHCP_OPT_data_to_u32(inner); + val = DHCP_OPT_data_to_u32((struct dhcp_option *)idata); /* Reject ACK from a server other than the one * we committed to during the OFFER phase. */ - if (s->dhcp_server_ip != 0 && data != s->dhcp_server_ip) + if (s->dhcp_server_ip != 0 && val != s->dhcp_server_ip) return -1; - s->dhcp_server_ip = data; + s->dhcp_server_ip = val; saw_server_id = 1; } else if (primary && code == DHCP_OPTION_OFFER_IP) { if (len < 4) return -1; - data = DHCP_OPT_data_to_u32(inner); - primary->ip = data; + val = DHCP_OPT_data_to_u32((struct dhcp_option *)idata); + primary->ip = val; } else if (primary && code == DHCP_OPTION_SUBNET_MASK) { if (len < 4) return -1; - data = DHCP_OPT_data_to_u32(inner); - primary->mask = data; + val = DHCP_OPT_data_to_u32((struct dhcp_option *)idata); + primary->mask = val; } else if (primary && code == DHCP_OPTION_ROUTER) { if (len < 4) return -1; - data = DHCP_OPT_data_to_u32(inner); - primary->gw = data; + val = DHCP_OPT_data_to_u32((struct dhcp_option *)idata); + primary->gw = val; } else if ((code == DHCP_OPTION_DNS) && (s->dns_server == 0)) { if (len < 4) return -1; - data = DHCP_OPT_data_to_u32(inner); - s->dns_server = data; + val = DHCP_OPT_data_to_u32((struct dhcp_option *)idata); + s->dns_server = val; } else if (code == DHCP_OPTION_LEASE_TIME) { if (len < 4) return -1; - lease_s = DHCP_OPT_data_to_u32(inner); + lease_s = DHCP_OPT_data_to_u32((struct dhcp_option *)idata); } else if (code == DHCP_OPTION_RENEWAL_TIME) { if (len < 4) return -1; - renew_s = DHCP_OPT_data_to_u32(inner); + renew_s = DHCP_OPT_data_to_u32((struct dhcp_option *)idata); } else if (code == DHCP_OPTION_REBIND_TIME) { if (len < 4) return -1; - rebind_s = DHCP_OPT_data_to_u32(inner); + rebind_s = DHCP_OPT_data_to_u32((struct dhcp_option *)idata); } - opt += 2 + len; } if (!saw_end) return -1; @@ -8305,9 +8532,8 @@ static int dhcp_parse_ack(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_l return 0; } } - break; + break; /* the message type was seen; nothing else to scan */ } - opt += 2 + len; } return -1; } @@ -8521,7 +8747,11 @@ 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_backoff_delay(s, DHCP_DISCOVER_TIMEOUT)); + /* RFC 2131 §4.1 (10 Mb/s Ethernet example): the first retransmission + * is 4 s, randomized uniformly by plus or minus 1 s; the base doubles + * per attempt. */ + dhcp_schedule_timer_at(s, s->last_tick + + dhcp_discover_retry_delay(s, DHCP_DISCOVER_TIMEOUT)); s->dhcp_state = DHCP_DISCOVER_SENT; return 0; } @@ -9208,6 +9438,11 @@ static inline void ip_recv(struct wolfIP *s, unsigned int if_idx, return; if (ee16(ip->len) < ip_hlen) return; + /* The declared total length cannot exceed the bytes actually received, + * or the datagram would be relayed or delivered with a header claiming + * more payload than exists on the wire. */ + if (ee16(ip->len) > len - ETH_HEADER_LEN) + return; /* validate IP header checksum per RFC 1122 */ if (iphdr_verify_checksum(ip) != 0) return; @@ -9353,6 +9588,39 @@ static inline void ip_recv(struct wolfIP *s, unsigned int if_idx, if (!l2_group) { int out_if = wolfIP_forward_interface(s, if_idx, dest); + ip4 next_hop = dest; + /* Forward the datagram at its declared length so trailing + * link-layer padding is not relayed as IP payload. */ + uint32_t fwd_len = ETH_HEADER_LEN + ee16(ip->len); + if (out_if < 0) { + /* No connected egress: resolve the destination in the + * static route table (longest prefix, then order, then + * interface) and forward to the route's gateway. */ + unsigned int ri; + uint8_t best_plen = 0; + uint32_t best_order = UINT32_MAX; + for (ri = 0; ri < WOLFIP_MAX_ROUTES; ri++) { + const struct wolfIP_route_entry *route = &s->routes[ri]; + if (!route->used) + continue; + if (!wolfIP_route_match_prefix(dest, route->prefix, + route->prefix_len)) + continue; + if (out_if < 0 || route->prefix_len > best_plen || + (route->prefix_len == best_plen && + (route->order < best_order || + (route->order == best_order && + route->if_idx < (unsigned int)out_if)))) { + out_if = (int)route->if_idx; + best_plen = route->prefix_len; + best_order = route->order; + next_hop = route->gateway != IPADDR_ANY ? + route->gateway : dest; + } + } + if (out_if >= 0 && (unsigned int)out_if == if_idx) + out_if = -1; + } if (out_if >= 0) { uint8_t mac[6]; int broadcast = 0; @@ -9368,14 +9636,16 @@ static inline void ip_recv(struct wolfIP *s, unsigned int if_idx, wolfIP_send_ttl_exceeded(s, if_idx, ip); return; } - if (!wolfIP_forward_prepare(s, out_if, dest, mac, &broadcast)) { - arp_queue_packet(s, out_if, dest, ip, len); + if (!wolfIP_forward_prepare(s, out_if, next_hop, mac, + &broadcast)) { + arp_queue_packet(s, out_if, next_hop, ip, fwd_len); return; } ip->ttl--; ip->csum = 0; iphdr_set_checksum(ip); - wolfIP_forward_packet(s, out_if, ip, len, broadcast ? NULL : mac, broadcast); + wolfIP_forward_packet(s, out_if, ip, fwd_len, broadcast ? NULL : mac, + broadcast); return; } } @@ -10015,7 +10285,7 @@ void dns_callback(int dns_sd, uint16_t ev, void *arg) if (s->dns_query_type == DNS_QUERY_TYPE_A && ee16(rr->type) == DNS_A && ee16(rr->class) == DNS_CLASS_IN && - rdlen >= DNS_IPV4_RDATA_LEN) { + rdlen == DNS_IPV4_RDATA_LEN) { uint32_t ip; ip = get_be32((const uint8_t *)buf + pos); if (s->dns_lookup_cb) @@ -10080,8 +10350,29 @@ static int dns_send_query(struct wolfIP *s, const char *dname, uint16_t *id, tok_end++; } label_len = (uint32_t)(tok_end - tok_start); - if (label_len > MAX_DNS_LABEL_LEN) return -22; - if (tok_len + label_len + 1 > MAX_DNS_NAME_LEN) return -22; + if (label_len == 0) { + /* A zero-length label is the wire-format root terminator + * (RFC 1035 §3.1), only legal at the end of the name. The + * trailing-dot presentation form ends the loop before an + * empty token is encoded, so any zero-length label here is + * a leading or interior dot: invalid. */ + dns_abort_query(s); + *id = DNS_ID_NONE; + return -22; + } + if (label_len > MAX_DNS_LABEL_LEN) { + /* Unencodable name: roll back the armed query state, or every + * later lookup would fail the busy guard with no timer to + * clear it. */ + dns_abort_query(s); + *id = DNS_ID_NONE; + return -22; + } + if (tok_len + label_len + 1 > MAX_DNS_NAME_LEN) { + dns_abort_query(s); + *id = DNS_ID_NONE; + return -22; + } *q_name = (char)label_len; q_name++; memcpy(q_name, tok_start, label_len); @@ -10360,6 +10651,10 @@ static void flush_tcp_tx(struct wolfIP *s, uint64_t now) tcp->ack = ee32(ts->sock.tcp.ack); tcp->win = ee16(tcp_adv_win(ts, 1)); ip_output_add_header(ts, (struct wolfIP_ip_packet *)tcp, WI_IPPROTO_TCP, size); +#ifdef ETHERNET + if (!wolfIP_ll_is_non_ethernet(ts->S, tx_if)) + eth_output_add_header(ts->S, tx_if, ts->nexthop_mac, &tcp->ip.eth, ETH_TYPE_IP); +#endif if (wolfIP_filter_notify_tcp(WOLFIP_FILT_SENDING, ts->S, tx_if, tcp, desc->len) != 0) { break; } @@ -10411,7 +10706,8 @@ static void flush_tcp_tx(struct wolfIP *s, uint64_t now) ts->sock.tcp.tmr_rto = NO_TIMER; } new_tmr.cb = tcp_rto_cb; - new_tmr.expires = now + (ts->sock.tcp.rto << ts->sock.tcp.rto_backoff); + new_tmr.expires = now + tcp_backoff_rto_ms(ts->sock.tcp.rto, + ts->sock.tcp.rto_backoff); new_tmr.arg = ts; ts->sock.tcp.tmr_rto = timers_binheap_insert(&s->timers, new_tmr); if (!is_retrans) { @@ -10453,22 +10749,36 @@ static void flush_datagram_tx(struct wolfIP *s, struct tsocket *socks, struct tsocket *t = &socks[i]; struct pkt_desc *desc = fifo_peek(&t->sock.udp.txbuf); int tx_drained = 0; - int len; while (desc) { struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)(t->txmem + desc->pos + sizeof(*desc)); unsigned int tx_if = wolfIP_socket_if_idx(t); int send_ret = 0; + /* The IP header was filled at enqueue time and carries this + * descriptor's destination; route and resolve for that address, + * not the socket's current one. */ + ip4 desc_dst = ee32(ip->dst); #ifdef ETHERNET - ip4 nexthop = wolfIP_select_nexthop_ex(s, &tx_if, t->remote_ip); + ip4 nexthop; +#ifdef IP_MULTICAST + if (is_udp && wolfIP_ip_is_multicast(desc_dst) && + t->sock.udp.mcast_if_set) { + /* IP_MULTICAST_IF pins this socket's multicast egress; the + * route lookup's no-route fallback would move the frame to + * another interface. */ + tx_if = t->sock.udp.mcast_if_idx; + nexthop = desc_dst; + } else +#endif + nexthop = wolfIP_select_nexthop_ex(s, &tx_if, desc_dst); if (wolfIP_is_loopback_if(tx_if)) { struct wolfIP_ll_dev *loop = wolfIP_ll_at(s, tx_if); if (loop) memcpy(t->nexthop_mac, loop->mac, 6); } else if (!wolfIP_ll_is_non_ethernet(s, tx_if)) { #ifdef IP_MULTICAST - if (is_udp && wolfIP_ip_is_multicast(t->remote_ip)) { - mcast_ip_to_eth(t->remote_ip, t->nexthop_mac); + if (is_udp && wolfIP_ip_is_multicast(desc_dst)) { + mcast_ip_to_eth(desc_dst, t->nexthop_mac); } else #endif if (!wolfIP_ip_is_broadcast(s, nexthop) && @@ -10481,8 +10791,10 @@ static void flush_datagram_tx(struct wolfIP *s, struct tsocket *socks, memset(t->nexthop_mac, 0xFF, 6); } #endif - len = desc->len - ETH_HEADER_LEN; - ip_output_add_header(t, ip, proto, len); +#ifdef ETHERNET + if (!wolfIP_ll_is_non_ethernet(s, tx_if)) + eth_output_add_header(s, tx_if, t->nexthop_mac, &ip->eth, ETH_TYPE_IP); +#endif if (is_udp) { if (wolfIP_filter_notify_udp(WOLFIP_FILT_SENDING, s, tx_if, @@ -10508,7 +10820,7 @@ static void flush_datagram_tx(struct wolfIP *s, struct tsocket *socks, /* IPsec not configured on this interface. * Send plaintext instead. * */ - if (esp_send(ll, ip, len) == 1) + if (esp_send(ll, ip, (uint16_t)(desc->len - ETH_HEADER_LEN)) == 1) send_ret = wolfIP_ll_send_frame(s, tx_if, ip, desc->len); } else { send_ret = wolfIP_ll_send_frame(s, tx_if, ip, desc->len); @@ -10524,7 +10836,7 @@ static void flush_datagram_tx(struct wolfIP *s, struct tsocket *socks, * when a SENDING filter blocked the frame or the driver returned * -EAGAIN: the descriptor stays in the txbuf and every subsequent * wolfIP_poll() re-enters the loop and re-loops the datagram. */ - if (is_udp && wolfIP_ip_is_multicast(t->remote_ip) && t->sock.udp.mcast_loop) + if (is_udp && wolfIP_ip_is_multicast(desc_dst) && t->sock.udp.mcast_loop) udp_try_recv(s, tx_if, (struct wolfIP_udp_datagram *)ip, desc->len); #endif fifo_pop(&t->sock.udp.txbuf); diff --git a/wolfip-filter.h b/wolfip-filter.h index 64d0a515..79ce525e 100644 --- a/wolfip-filter.h +++ b/wolfip-filter.h @@ -66,7 +66,28 @@ struct wolfIP_filter_event { typedef int (*wolfIP_filter_cb)(void *arg, const struct wolfIP_filter_event *event); +/* + * Install the filter callback. The callback is only consulted for the + * reasons selected in the reason masks; a reason bit left clear in a + * configured mask means the stack silently allows that traffic (no + * callback, no error). To keep a freshly installed filter from failing + * open, a callback installed without any explicit mask configuration + * since the last uninstall is consulted for all reasons until the first + * explicit mask configuration (wolfIP_filter_set_mask() or any per-proto + * setter, including an explicit zero); afterwards only the configured + * reasons are consulted. Passing a NULL callback uninstalls it and resets + * this all-reasons default (masks keep their values). + */ void wolfIP_filter_set_callback(wolfIP_filter_cb cb, void *arg); + +/* + * Select the reasons the callback is consulted for (all protocols). Any + * call — including an explicit zero — counts as an explicit mask + * configuration and ends the all-reasons default of a freshly installed + * callback. Once configured, an all-zero mask disables all notifications + * and silently allows every packet and socket event. Per-proto masks + * (eth/ip/tcp/udp/icmp) take precedence over this one when non-zero. + */ void wolfIP_filter_set_mask(uint32_t mask); void wolfIP_filter_set_eth_mask(uint32_t mask); void wolfIP_filter_set_ip_mask(uint32_t mask);