Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/rfc9293-scope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# wolfIP protocol scope decisions

Features the protocol specifications require but wolfIP deliberately does not
implement. Each entry records the decision, the rationale, and the date, so a
standards review (or a scanner) sees a documented scope-out, not an oversight.

## TCP urgent data (RFC 9293 §3.8.5) - not supported by design

**Decision:** 2026-10-01 (Daniele Lacamera). wolfIP's TCP receiver does not
process the URG flag or the urgent pointer of incoming segments, and the
socket layer provides no out-of-band notification. The `urg` field is carried
in `struct wolfIP_tcp_seg` for wire completeness and is sent as 0.

**Rationale:** RFC 9293 §3.8.5 tells new applications not to employ the
mechanism (SHLD-13), and the supporting MUSTs (MUST-30 urgent pointer,
MUST-31 arbitrary-length sequences, MUST-32 asynchronous notification,
MUST-33 remaining-urgent-data query, MUST-62 pointer semantics, MUST-66
processing at a zero window) would require OOB notification API and
pointer-tracking state the stack does not carry. wolfIP's socket model is
callback-based with an in-band data stream; urgent data would be delivered
inline, unmarked, which is what a peer gets from a receiver that ignores the
pointer. Segments carrying URG are otherwise processed normally: the data is
in-band per RFC 9293 §3.8.5 ("the urgent pointer ... points into the
transmission stream"), so ignoring the pointer loses no data.
25 changes: 25 additions & 0 deletions src/test/unit/unit.c
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,9 @@ Suite *wolf_suite(void)
tcase_add_test(tc_utils, test_sock_connect_udp_bound_local_ip_no_match);
tcase_add_test(tc_utils, test_sock_connect_udp_bound_local_ip_match);
tcase_add_test(tc_utils, test_sock_connect_icmp_sets_local_ip_from_conf);
tcase_add_test(tc_utils, test_sendto_wildcard_bound_uses_egress_if_source);
tcase_add_test(tc_utils, test_sock_connect_icmp_bound_local_ip_match);
tcase_add_test(tc_utils, test_sock_connect_icmp_bound_local_ip_no_match_keeps_state);
tcase_add_test(tc_utils, test_sock_connect_icmp_wrong_family);
tcase_add_test(tc_utils, test_sock_connect_icmp_local_ip_pre_set);
tcase_add_test(tc_utils, test_sock_connect_icmp_conf_null);
Expand Down Expand Up @@ -430,6 +432,15 @@ Suite *wolf_suite(void)
tcase_add_test(tc_utils, test_tcp_initial_cwnd_caps_to_iw10_and_half_rwnd);
tcase_add_test(tc_utils, test_tcp_persist_cb_sends_one_byte_probe);
tcase_add_test(tc_utils, test_tcp_zero_wnd_probe_includes_timestamp_when_enabled);
tcase_add_test(tc_utils, test_tcp_persist_cb_fin_wait_1_sends_probe);
tcase_add_test(tc_utils, test_tcp_persist_cb_last_ack_subsegment_window_sends_probe);
tcase_add_test(tc_utils, test_tcp_persist_cb_closing_sends_probe);
tcase_add_test(tc_utils, test_tcp_persist_cb_closing_gives_up_after_maxrtx);
tcase_add_test(tc_utils, test_tcp_persist_close_resets_retry_budget);
tcase_add_test(tc_utils, test_tcp_persist_survives_poll_with_subsegment_window);
tcase_add_test(tc_utils, test_tcp_persist_start_armed_for_subsegment_window);
tcase_add_test(tc_utils, test_tcp_persist_cb_fin_wait_1_gives_up_after_maxrtx);
tcase_add_test(tc_utils, test_tcp_ack_forward_progress_resets_persist_retries);
tcase_add_test(tc_utils, test_tcp_zero_wnd_probe_rejects_invalid_inputs_and_empty_payload);
tcase_add_test(tc_utils, test_tcp_zero_wnd_probe_skips_ack_only_segment);
tcase_add_test(tc_utils, test_tcp_zero_wnd_probe_selects_middle_byte_at_snd_una);
Expand Down Expand Up @@ -467,6 +478,9 @@ Suite *wolf_suite(void)
tcase_add_test(tc_utils, test_dhcp_poll_offer_and_ack);
tcase_add_test(tc_utils, test_dhcp_poll_renewing_ack_binds_client);
tcase_add_test(tc_utils, test_dhcp_poll_rebinding_ack_binds_client);
tcase_add_test(tc_utils, test_dhcp_poll_rebinding_ack_foreign_server_binds_client);
tcase_add_test(tc_utils, test_dhcp_poll_rebinding_nak_foreign_server_restarts_discovery);
tcase_add_test(tc_utils, test_dhcp_poll_rebinding_nak_without_server_id_ignored);
tcase_add_test(tc_utils, test_dhcp_poll_reply_wrong_chaddr_rejected);
tcase_add_test(tc_utils, test_dhcp_poll_offer_zero_yiaddr_rejected);
tcase_add_test(tc_utils, test_dhcp_poll_offer_defers_commit_until_ack);
Expand Down Expand Up @@ -602,6 +616,10 @@ Suite *wolf_suite(void)
tcase_add_test(tc_utils, test_tcp_rto_cb_fin_wait_1_no_data_full_txbuf_keeps_retry_budget);
tcase_add_test(tc_utils, test_tcp_ack_fin_wait_1_ack_of_fin_moves_to_fin_wait_2_and_arms_timeout);
tcase_add_test(tc_utils, test_tcp_ack_closing_ack_of_fin_moves_to_time_wait_and_stops_timer);
tcase_add_test(tc_utils, test_tcp_rto_cb_closing_no_data_requeues_finack);
tcase_add_test(tc_utils, test_tcp_rto_cb_closing_ctrl_maxretries_closes_socket);
tcase_add_test(tc_utils, test_tcp_rto_cb_closing_with_data_retransmits_data);
tcase_add_test(tc_utils, test_tcp_ack_closing_data_drained_rearms_ctrl_rto);
tcase_add_test(tc_utils, test_tcp_rto_cb_control_retry_cap_closes_socket);
tcase_add_test(tc_utils, test_tcp_rto_cb_cancels_existing_timer);
tcase_add_test(tc_utils, test_tcp_rto_cb_clears_sack_and_marks_lowest_only);
Expand Down Expand Up @@ -800,6 +818,7 @@ Suite *wolf_suite(void)
tcase_add_test(tc_utils, test_tcp_mark_unsacked_rescans_after_clearing_stale_sack);
tcase_add_test(tc_utils, test_tcp_mark_unsacked_ignores_zero_ip_len_unsent_ack_only_desc);
tcase_add_test(tc_utils, test_flush_tcp_tx_pure_ack_keeps_unacked_data_desc);
tcase_add_test(tc_utils, test_flush_tcp_tx_popped_tail_wrap_gap_no_phantom_frames);
tcase_add_test(tc_utils, test_tcp_ack_parked_zero_desc_keeps_rtt_sample);
tcase_add_test(tc_utils, test_flush_tcp_tx_sends_pure_ack_behind_window_blocked_data);
tcase_add_test(tc_utils, test_flush_tcp_tx_fin_ack_stays_behind_window_blocked_data);
Expand Down Expand Up @@ -1004,6 +1023,7 @@ Suite *wolf_suite(void)
tcase_add_test(tc_proto, test_icmp_socket_send_recv);
tcase_add_test(tc_proto, test_icmp_input_echo_reply_queues);
tcase_add_test(tc_proto, test_icmp_input_echo_reply_wrong_dst_dropped);
tcase_add_test(tc_proto, test_icmp_input_echo_reply_after_second_if_sendto_delivered);
tcase_add_test(tc_proto, test_icmp_input_echo_request_reply_sent);
tcase_add_test(tc_proto, test_icmp_input_echo_reply_sets_df);
tcase_add_test(tc_proto, test_icmp_echo_reply_code_zeroed);
Expand Down Expand Up @@ -1608,6 +1628,9 @@ Suite *wolf_suite(void)
tcase_add_test(tc_core, test_dhcp_timer_cb_default_state_noop);
tcase_add_test(tc_core, test_dhcp_timer_cb_null_arg_noop);
tcase_add_test(tc_core, test_dhcp_renew_rerandomizes_xid_rejecting_stale_ack);
tcase_add_test(tc_core, test_dhcp_renew_ack_same_ip_skips_dad_and_bounds);
tcase_add_test(tc_core, test_dhcp_rebind_ack_same_ip_skips_dad_and_bounds);
tcase_add_test(tc_core, test_dhcp_renew_ack_new_ip_still_runs_dad);
tcase_add_test(tc_core, test_dhcp_parse_ack_without_lease_time_rejected);
tcase_add_test(tc_core, test_dhcp_lease_expiry_relearns_dns_server);
tcase_add_test(tc_core, test_dhcp_nak_relearns_dns_server);
Expand Down Expand Up @@ -1800,6 +1823,8 @@ Suite *wolf_suite(void)
tcase_add_test(tc_core, test_wolfip_packetsocket_from_fd_negative_fd);
#endif /* WOLFIP_PACKET_SOCKETS */
tcase_add_test(tc_core, test_bind_port_in_use_different_ips_no_collision);
tcase_add_test(tc_core, test_bind_wildcard_and_specific_same_port_collide);
tcase_add_test(tc_core, test_tcp_connect_wildcard_zero_avoids_specific_bound_port);
tcase_add_test(tc_core, test_bind_tcp_rejected_preserves_if_idx);
tcase_add_test(tc_core, test_bind_udp_rejected_preserves_if_idx);
tcase_add_test(tc_core, test_bind_icmp_rejected_preserves_if_idx);
Expand Down
71 changes: 71 additions & 0 deletions src/test/unit/unit_tests_api.c
Original file line number Diff line number Diff line change
Expand Up @@ -2615,6 +2615,77 @@ START_TEST(test_sock_connect_icmp_primary_ip_fallback)
}
END_TEST

START_TEST(test_sendto_wildcard_bound_uses_egress_if_source)
{
struct wolfIP s;
int udp_sd;
struct wolfIP_sockaddr_in sin;
uint8_t peer_mac[6] = {0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
const uint8_t payload[] = "hi";

setup_stack_with_two_ifaces(&s, 0x0a000001U, 0x0a000101U);
/* Pre-seed the ARP neighbor so the datagram is not stuck behind
* unresolved resolution. */
s.arp.neighbors[0].ip = 0x0a000102U;
s.arp.neighbors[0].if_idx = TEST_SECOND_IF;
memcpy(s.arp.neighbors[0].mac, peer_mac, sizeof(peer_mac));
udp_sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP);
ck_assert_int_gt(udp_sd, 0);

memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = ee16(5000);
sin.sin_addr.s_addr = ee32(IPADDR_ANY);
ck_assert_int_eq(wolfIP_sock_bind(&s, udp_sd, (struct wolfIP_sockaddr *)&sin,
sizeof(sin)), 0);

/* Destination in the secondary interface's subnet: the datagram must
* be sourced from that interface's address, not the primary one
* snapshotted into local_ip at bind time. */
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = ee16(53);
sin.sin_addr.s_addr = ee32(0x0a000102U);
ck_assert_int_ge(wolfIP_sock_sendto(&s, udp_sd, payload, sizeof(payload), 0,
(const struct wolfIP_sockaddr *)&sin,
sizeof(sin)), 0);
(void)wolfIP_poll(&s, 100);

ck_assert_uint_eq(last_frame_sent_count, 1);
/* IP src = frame[14+12 .. 14+15] */
ck_assert_uint_eq(last_frame_sent[26], 0x0a);
ck_assert_uint_eq(last_frame_sent[27], 0x00);
ck_assert_uint_eq(last_frame_sent[28], 0x01);
ck_assert_uint_eq(last_frame_sent[29], 0x01);
}
END_TEST

START_TEST(test_sock_connect_icmp_bound_local_ip_no_match_keeps_state)
{
struct wolfIP s;
int icmp_sd;
struct tsocket *ts;
struct wolfIP_sockaddr_in sin;

wolfIP_init(&s);
mock_link_init(&s);
wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0);

icmp_sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_ICMP);
ck_assert_int_gt(icmp_sd, 0);
ts = &s.icmpsockets[SOCKET_UNMARK(icmp_sd)];
ts->bound_local_ip = 0x0B000001U;
ts->remote_ip = 0x0A000009U; /* must survive a failed connect */

memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = ee32(0x0A000002U);

ck_assert_int_eq(wolfIP_sock_connect(&s, icmp_sd, (struct wolfIP_sockaddr *)&sin, sizeof(sin)), -WOLFIP_EINVAL);
ck_assert_uint_eq(ts->remote_ip, 0x0A000009U);
}
END_TEST

START_TEST(test_sock_connect_icmp_bound_local_ip_match)
{
struct wolfIP s;
Expand Down
116 changes: 116 additions & 0 deletions src/test/unit/unit_tests_dhcp_edges.c
Original file line number Diff line number Diff line change
Expand Up @@ -1435,6 +1435,122 @@ START_TEST(test_dhcp_renew_rerandomizes_xid_rejecting_stale_ack)
}
END_TEST

/* A renewal ACK re-confirming the address already in use must skip the
* RFC 4331 DAD phase: the address already proved itself, and probing a
* live address drops the client out of BOUND for ~3 s on every renewal
* while the DAD conflict hooks sit armed on the working address. */
START_TEST(test_dhcp_renew_ack_same_ip_skips_dad_and_bounds)
{
struct wolfIP s;
struct dhcp_msg msg;
struct ipconf *primary;
const uint32_t server_ip = 0x0A000001U;
const uint32_t lease_ip = 0x0A000064U;

wolfIP_init(&s);
mock_link_init(&s);
primary = wolfIP_primary_ipconf(&s);
ck_assert_ptr_nonnull(primary);
primary->ip = lease_ip;
primary->mask = 0xFFFFFF00U;
primary->gw = server_ip;
s.dhcp_server_ip = server_ip;
s.dhcp_ip = lease_ip;
s.dhcp_xid = 0x12345678U;
s.dhcp_state = DHCP_RENEWING;
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);
last_frame_sent_count = 0;

build_full_ack(&s, &msg, server_ip, lease_ip, primary->mask,
server_ip, 0x08080808U, 120U);
ck_assert_int_eq(dhcp_parse_ack(&s, &msg, sizeof(msg)), 0);

/* Straight to BOUND: no DAD state, no ARP probe, lease timer armed. */
ck_assert_int_eq(s.dhcp_state, DHCP_BOUND);
ck_assert_uint_eq(s.dhcp_dad_probes, 0);
ck_assert_uint_eq(last_frame_sent_count, 0);
ck_assert_int_ne(s.dhcp_timer, NO_TIMER);
}
END_TEST

/* Same for REBINDING: a rebind ACK on the in-use address must not re-probe. */
START_TEST(test_dhcp_rebind_ack_same_ip_skips_dad_and_bounds)
{
struct wolfIP s;
struct dhcp_msg msg;
struct ipconf *primary;
const uint32_t server_ip = 0x0A000001U;
const uint32_t lease_ip = 0x0A000064U;

wolfIP_init(&s);
mock_link_init(&s);
primary = wolfIP_primary_ipconf(&s);
ck_assert_ptr_nonnull(primary);
primary->ip = lease_ip;
primary->mask = 0xFFFFFF00U;
primary->gw = server_ip;
s.dhcp_server_ip = server_ip;
s.dhcp_ip = lease_ip;
s.dhcp_xid = 0x12345678U;
s.dhcp_state = DHCP_REBINDING;
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);
last_frame_sent_count = 0;

build_full_ack(&s, &msg, server_ip, lease_ip, primary->mask,
server_ip, 0x08080808U, 120U);
ck_assert_int_eq(dhcp_parse_ack(&s, &msg, sizeof(msg)), 0);

ck_assert_int_eq(s.dhcp_state, DHCP_BOUND);
ck_assert_uint_eq(s.dhcp_dad_probes, 0);
ck_assert_uint_eq(last_frame_sent_count, 0);
ck_assert_int_ne(s.dhcp_timer, NO_TIMER);
}
END_TEST

/* Guard: a renewal ACK that hands out a NEW address must still run DAD,
* the probe is what protects the interface from adopting a busy address. */
START_TEST(test_dhcp_renew_ack_new_ip_still_runs_dad)
{
struct wolfIP s;
struct dhcp_msg msg;
struct ipconf *primary;
const uint32_t server_ip = 0x0A000001U;
const uint32_t old_ip = 0x0A000064U;
const uint32_t new_ip = 0x0A000065U;

wolfIP_init(&s);
mock_link_init(&s);
primary = wolfIP_primary_ipconf(&s);
ck_assert_ptr_nonnull(primary);
primary->ip = old_ip;
primary->mask = 0xFFFFFF00U;
primary->gw = server_ip;
s.dhcp_server_ip = server_ip;
s.dhcp_ip = old_ip;
s.dhcp_xid = 0x12345678U;
s.dhcp_state = DHCP_RENEWING;
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);
last_frame_sent_count = 0;

build_full_ack(&s, &msg, server_ip, new_ip, primary->mask,
server_ip, 0x08080808U, 120U);
ck_assert_int_eq(dhcp_parse_ack(&s, &msg, sizeof(msg)), 0);

ck_assert_int_eq(s.dhcp_state, DHCP_DAD);
ck_assert_uint_gt(s.dhcp_dad_probes, 0);
ck_assert_uint_gt(last_frame_sent_count, 0);
}
END_TEST

/* F-5482: a DHCPACK is only valid with the mandatory IP-address-lease-time
* option (51, RFC 2131). An ACK missing it - or carrying a zero duration -
* must be rejected, never bound, otherwise the lease has no expiry/renewal
Expand Down
Loading
Loading