From a42f1490c3ee83f3d52c15331aec1fc6ba76c4f6 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 1 Oct 2026 07:49:51 +0200 Subject: [PATCH 01/15] F-14518: retransmit the FIN and time out in TCP_CLOSING --- src/test/unit/unit.c | 4 + src/test/unit/unit_tests_dns_dhcp.c | 144 ++++++++++++++++++++++++++++ src/wolfip.c | 18 +++- 3 files changed, 161 insertions(+), 5 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 20e3cd7c..1a2d52e8 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -602,6 +602,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); diff --git a/src/test/unit/unit_tests_dns_dhcp.c b/src/test/unit/unit_tests_dns_dhcp.c index b4bf8d3d..6b2c1cc4 100644 --- a/src/test/unit/unit_tests_dns_dhcp.c +++ b/src/test/unit/unit_tests_dns_dhcp.c @@ -5311,6 +5311,150 @@ START_TEST(test_tcp_ack_closing_ack_of_fin_moves_to_time_wait_and_stops_timer) } END_TEST +/* CLOSING with the data fully drained: the control RTO must keep + * retransmitting the FIN, same handling as FIN_WAIT_1. */ +START_TEST(test_tcp_rto_cb_closing_no_data_requeues_finack) +{ + struct wolfIP s; + struct tsocket *ts; + struct pkt_desc *desc; + struct wolfIP_tcp_seg *seg; + + 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_CLOSING; + ts->sock.tcp.rto = 100; + ts->sock.tcp.ctrl_rto_active = 1; + ts->src_port = 12345; + ts->dst_port = 5001; + ts->local_ip = 0x0A000001U; + ts->remote_ip = 0x0A000002U; + ts->sock.tcp.bytes_in_flight = 0; + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + s.last_tick = 1000; + tcp_rto_cb(ts); + + desc = fifo_peek(&ts->sock.tcp.txbuf); + ck_assert_ptr_nonnull(desc); + seg = (struct wolfIP_tcp_seg *)(ts->txmem + desc->pos + sizeof(*desc)); + ck_assert_uint_eq(seg->flags, (TCP_FLAG_FIN | TCP_FLAG_ACK)); + ck_assert_uint_eq(ts->sock.tcp.ctrl_rto_retries, 1); + ck_assert_int_ne(ts->sock.tcp.tmr_rto, NO_TIMER); +} +END_TEST + +/* CLOSING with the control retry budget exhausted: give up and reclaim + * the socket slot instead of pinning it forever. */ +START_TEST(test_tcp_rto_cb_closing_ctrl_maxretries_closes_socket) +{ + struct wolfIP s; + struct tsocket *ts; + + 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_CLOSING; + ts->sock.tcp.rto = 100; + ts->sock.tcp.ctrl_rto_active = 1; + ts->sock.tcp.ctrl_rto_retries = TCP_CTRL_RTO_MAXRTX; + ts->sock.tcp.bytes_in_flight = 0; + + tcp_rto_cb(ts); + ck_assert_int_eq(ts->proto, 0); +} +END_TEST + +/* CLOSING with payload still in flight: the control RTO must yield to the + * data path, which retransmits the outstanding data. */ +START_TEST(test_tcp_rto_cb_closing_with_data_retransmits_data) +{ + struct wolfIP s; + struct tsocket *ts; + struct pkt_desc *desc; + + wolfIP_init(&s); + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->sock.tcp.state = TCP_CLOSING; + ts->sock.tcp.rto = 100; + ts->sock.tcp.ctrl_rto_active = 1; + ts->sock.tcp.snd_una = 101; + ts->sock.tcp.seq = 101; + ts->sock.tcp.bytes_in_flight = 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); + + ck_assert_int_eq(enqueue_tcp_tx(ts, 1, TCP_FLAG_PSH), 0); + desc = fifo_peek(&ts->sock.tcp.txbuf); + ck_assert_ptr_nonnull(desc); + desc->flags |= PKT_FLAG_SENT; + + s.last_tick = 1000; + tcp_rto_cb(ts); + + ck_assert_int_ne(desc->flags & PKT_FLAG_RETRANS, 0); + ck_assert_int_eq(desc->flags & PKT_FLAG_SENT, 0); + ck_assert_int_ne(ts->sock.tcp.tmr_rto, NO_TIMER); +} +END_TEST + +/* CLOSING: once the peer's ACK drains the last outstanding data byte, the + * control RTO takes over again so the unacked FIN keeps being retransmitted. + */ +START_TEST(test_tcp_ack_closing_data_drained_rearms_ctrl_rto) +{ + struct wolfIP s; + struct tsocket *ts; + struct pkt_desc *desc; + struct wolfIP_tcp_seg ackseg; + + wolfIP_init(&s); + s.last_tick = 1000U; + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->sock.tcp.state = TCP_CLOSING; + /* FIN at seq 100 (last=100); one data byte at seq 99 still unacked. */ + ts->sock.tcp.last = 100; + ts->sock.tcp.snd_una = 99; + ts->sock.tcp.seq = 99; + ts->sock.tcp.rto = 100; + ts->sock.tcp.bytes_in_flight = 1; + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + ck_assert_int_eq(enqueue_tcp_tx(ts, 1, TCP_FLAG_PSH), 0); + desc = fifo_peek(&ts->sock.tcp.txbuf); + ck_assert_ptr_nonnull(desc); + desc->flags |= PKT_FLAG_SENT; + ts->sock.tcp.seq = 101; + + memset(&ackseg, 0, sizeof(ackseg)); + ackseg.hlen = TCP_HEADER_LEN << 2; + ackseg.flags = TCP_FLAG_ACK; + ackseg.ack = ee32(100); /* acks the data byte, not the FIN at 101 */ + ackseg.ip.len = ee16(IP_HEADER_LEN + TCP_HEADER_LEN); + + tcp_ack(ts, &ackseg); + + ck_assert_int_eq(ts->sock.tcp.state, TCP_CLOSING); + ck_assert_uint_eq(ts->sock.tcp.bytes_in_flight, 0); + ck_assert_uint_eq(ts->sock.tcp.ctrl_rto_active, 1); + ck_assert_int_ne(ts->sock.tcp.tmr_rto, NO_TIMER); +} +END_TEST + START_TEST(test_tcp_rto_cb_control_retry_cap_closes_socket) { struct wolfIP s; diff --git a/src/wolfip.c b/src/wolfip.c index 713b4433..12a03757 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -4485,10 +4485,11 @@ static int tcp_ctrl_state_needs_rto(const struct tsocket *t) if ((t->sock.tcp.state == TCP_SYN_SENT) || (t->sock.tcp.state == TCP_SYN_RCVD)) return 1; - /* In FIN_WAIT_1 and LAST_ACK keep data-RTO active while payload is still - * outstanding. Switch to control-RTO only after data is fully drained and - * only the FIN/ACK teardown control traffic remains. */ + /* In FIN_WAIT_1, CLOSING and LAST_ACK keep data-RTO active while payload + * is still outstanding. Switch to control-RTO only after data is fully + * drained and only the FIN/ACK teardown control traffic remains. */ if (((t->sock.tcp.state == TCP_FIN_WAIT_1) || + (t->sock.tcp.state == TCP_CLOSING) || (t->sock.tcp.state == TCP_LAST_ACK)) && (t->sock.tcp.bytes_in_flight == 0) && !tcp_has_pending_unsent_payload((struct tsocket *)t)) @@ -4789,7 +4790,10 @@ static int tcp_has_pending_unsent_payload(struct tsocket *t) uint32_t seg_len; seg = (struct wolfIP_tcp_seg *)(t->txmem + desc->pos + sizeof(*desc)); seg_len = tcp_tx_desc_payload_len(t, desc, seg); - if (seg_len > 0 && !(desc->flags & PKT_FLAG_SENT)) + /* An ACKED descriptor is done (its cleanup pop runs later in the + * same tcp_ack() pass); it is not pending payload. */ + if (seg_len > 0 && !(desc->flags & PKT_FLAG_SENT) && + !(desc->flags & PKT_FLAG_ACKED)) return 1; desc = fifo_next(&t->sock.tcp.txbuf, desc); } @@ -6033,6 +6037,7 @@ static void tcp_ack(struct tsocket *t, const struct wolfIP_tcp_seg *tcp) t->sock.tcp.bytes_in_flight == 0 && !tcp_has_pending_unsent_payload(t) && (t->sock.tcp.state == TCP_FIN_WAIT_1 || + t->sock.tcp.state == TCP_CLOSING || t->sock.tcp.state == TCP_LAST_ACK) && t->sock.tcp.tmr_rto == NO_TIMER) { /* Data fully drained but the FIN is still in flight: hand the RTO @@ -6844,7 +6849,9 @@ static void tcp_rto_cb(void *arg) queued = (tcp_send_syn(ts, TCP_FLAG_SYN) == 0); } else if (ts->sock.tcp.state == TCP_SYN_RCVD) { queued = (tcp_send_syn(ts, TCP_FLAG_SYN | TCP_FLAG_ACK) == 0); - } else if (ts->sock.tcp.state == TCP_FIN_WAIT_1 || ts->sock.tcp.state == TCP_LAST_ACK) { + } else if (ts->sock.tcp.state == TCP_FIN_WAIT_1 || + ts->sock.tcp.state == TCP_CLOSING || + ts->sock.tcp.state == TCP_LAST_ACK) { queued = (tcp_send_finack(ts) == 0); if (queued) ts->sock.tcp.ctrl_rto_retries++; @@ -6862,6 +6869,7 @@ static void tcp_rto_cb(void *arg) } if (ts->sock.tcp.state != TCP_ESTABLISHED && ts->sock.tcp.state != TCP_FIN_WAIT_1 && + ts->sock.tcp.state != TCP_CLOSING && ts->sock.tcp.state != TCP_CLOSE_WAIT && ts->sock.tcp.state != TCP_LAST_ACK) { /* The fired timer's id is stale once the heap popped it: clear it so a From de9ac85eb7ae855b7804d8b6d046a52a86a083b0 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 1 Oct 2026 07:49:51 +0200 Subject: [PATCH 02/15] F-14519: probe and time out window-blocked teardown sockets --- src/test/unit/unit.c | 5 + src/test/unit/unit_tests_proto.c | 218 +++++++++++++++++++++++++++++++ src/wolfip.c | 47 +++++-- 3 files changed, 262 insertions(+), 8 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 1a2d52e8..f0cafbd9 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -430,6 +430,11 @@ 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_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); diff --git a/src/test/unit/unit_tests_proto.c b/src/test/unit/unit_tests_proto.c index cc532d7a..8c5a7390 100644 --- a/src/test/unit/unit_tests_proto.c +++ b/src/test/unit/unit_tests_proto.c @@ -1888,6 +1888,224 @@ START_TEST(test_tcp_zero_wnd_probe_includes_timestamp_when_enabled) } END_TEST +/* FIN_WAIT_1 with window-blocked queued data: the persist probe must fire, + * as it does in ESTABLISHED, so a silent peer cannot pin the socket. */ +START_TEST(test_tcp_persist_cb_fin_wait_1_sends_probe) +{ + struct wolfIP s; + struct tsocket *ts; + ip4 local_ip = 0x0A000001U; + ip4 remote_ip = 0x0A000002U; + uint8_t peer_mac[6] = {0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xf2}; + struct wolfIP_tcp_seg *tcp; + uint8_t payload[4] = {0x15, 0x16, 0x17, 0x18}; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, local_ip, 0xFFFFFF00U, 0); + wolfIP_filter_set_callback(NULL, NULL); + last_frame_sent_size = 0; + + s.arp.neighbors[0].ip = remote_ip; + s.arp.neighbors[0].if_idx = TEST_PRIMARY_IF; + memcpy(s.arp.neighbors[0].mac, peer_mac, sizeof(peer_mac)); + + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->sock.tcp.state = TCP_FIN_WAIT_1; + ts->local_ip = local_ip; + ts->remote_ip = remote_ip; + ts->if_idx = TEST_PRIMARY_IF; + ts->src_port = 1111; + ts->dst_port = 2222; + ts->sock.tcp.seq = 10; + ts->sock.tcp.ack = 20; + ts->sock.tcp.snd_una = 10; + ts->sock.tcp.rto = 100; + ts->sock.tcp.cwnd = TCP_MSS * 4; + ts->sock.tcp.peer_rwnd = 0; + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + ck_assert_int_eq(enqueue_tcp_tx_with_payload(ts, payload, sizeof(payload), + (TCP_FLAG_ACK | TCP_FLAG_PSH)), 0); + s.last_tick = 500; + tcp_persist_cb(ts); + + ck_assert_uint_gt(last_frame_sent_size, 0); + tcp = (struct wolfIP_tcp_seg *)last_frame_sent; + ck_assert_uint_eq(ee32(tcp->seq), ts->sock.tcp.snd_una); + ck_assert_uint_eq(tcp->data[0], 0x15U); + ck_assert_uint_eq(ts->sock.tcp.persist_active, 1); + ck_assert_int_ne(ts->sock.tcp.tmr_persist, NO_TIMER); +} +END_TEST + +/* LAST_ACK with a sub-segment peer window: the head segment does not fit, + * so the probe must fire even though the window is non-zero. */ +START_TEST(test_tcp_persist_cb_last_ack_subsegment_window_sends_probe) +{ + struct wolfIP s; + struct tsocket *ts; + ip4 local_ip = 0x0A000001U; + ip4 remote_ip = 0x0A000002U; + uint8_t peer_mac[6] = {0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xf2}; + struct wolfIP_tcp_seg *tcp; + uint8_t payload[8] = {0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c}; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, local_ip, 0xFFFFFF00U, 0); + wolfIP_filter_set_callback(NULL, NULL); + last_frame_sent_size = 0; + + s.arp.neighbors[0].ip = remote_ip; + s.arp.neighbors[0].if_idx = TEST_PRIMARY_IF; + memcpy(s.arp.neighbors[0].mac, peer_mac, sizeof(peer_mac)); + + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->sock.tcp.state = TCP_LAST_ACK; + ts->local_ip = local_ip; + ts->remote_ip = remote_ip; + ts->if_idx = TEST_PRIMARY_IF; + ts->src_port = 1111; + ts->dst_port = 2222; + ts->sock.tcp.seq = 10; + ts->sock.tcp.ack = 20; + ts->sock.tcp.snd_una = 10; + ts->sock.tcp.rto = 100; + ts->sock.tcp.cwnd = TCP_MSS * 4; + ts->sock.tcp.peer_rwnd = 2; + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + ck_assert_int_eq(enqueue_tcp_tx_with_payload(ts, payload, sizeof(payload), + (TCP_FLAG_ACK | TCP_FLAG_PSH)), 0); + s.last_tick = 500; + tcp_persist_cb(ts); + + ck_assert_uint_gt(last_frame_sent_size, 0); + tcp = (struct wolfIP_tcp_seg *)last_frame_sent; + ck_assert_uint_eq(ee32(tcp->seq), ts->sock.tcp.snd_una); + ck_assert_uint_eq(ts->sock.tcp.persist_active, 1); +} +END_TEST + +/* A non-zero peer window smaller than the head segment must still arm the + * persist timer (the old peer_rwnd == 0 gate missed this case). */ +START_TEST(test_tcp_persist_start_armed_for_subsegment_window) +{ + struct wolfIP s; + struct tsocket *ts; + uint8_t payload[8] = {0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c}; + + 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_FIN_WAIT_1; + ts->sock.tcp.seq = 10; + ts->sock.tcp.snd_una = 10; + ts->sock.tcp.rto = 100; + ts->sock.tcp.peer_rwnd = 2; + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + ck_assert_int_eq(enqueue_tcp_tx_with_payload(ts, payload, sizeof(payload), + (TCP_FLAG_ACK | TCP_FLAG_PSH)), 0); + + s.last_tick = 500; + tcp_persist_start(ts, s.last_tick); + + ck_assert_uint_eq(ts->sock.tcp.persist_active, 1); + ck_assert_int_ne(ts->sock.tcp.tmr_persist, NO_TIMER); +} +END_TEST + +/* Teardown states must not be pinnable by a silent peer: after the probe + * budget runs out without any acknowledgment, release the socket. */ +START_TEST(test_tcp_persist_cb_fin_wait_1_gives_up_after_maxrtx) +{ + struct wolfIP s; + struct tsocket *ts; + uint8_t payload[4] = {0x15, 0x16, 0x17, 0x18}; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + wolfIP_filter_set_callback(NULL, NULL); + + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->sock.tcp.state = TCP_FIN_WAIT_1; + ts->local_ip = 0x0A000001U; + ts->remote_ip = 0x0A000002U; + ts->if_idx = TEST_PRIMARY_IF; + ts->src_port = 1111; + ts->dst_port = 2222; + ts->sock.tcp.seq = 10; + ts->sock.tcp.snd_una = 10; + ts->sock.tcp.rto = 100; + ts->sock.tcp.peer_rwnd = 0; + ts->sock.tcp.persist_retries = TCP_PERSIST_MAXRTX - 1; + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + ck_assert_int_eq(enqueue_tcp_tx_with_payload(ts, payload, sizeof(payload), + (TCP_FLAG_ACK | TCP_FLAG_PSH)), 0); + + s.last_tick = 500; + tcp_persist_cb(ts); + + ck_assert_int_eq(ts->proto, 0); + ck_assert_uint_eq(ts->sock.tcp.persist_active, 0); +} +END_TEST + +/* Any forward acknowledgment resets the unanswered-probe counter so a slow + * but live peer is not given up on. */ +START_TEST(test_tcp_ack_forward_progress_resets_persist_retries) +{ + struct wolfIP s; + struct tsocket *ts; + struct wolfIP_tcp_seg ackseg; + struct pkt_desc *desc; + + wolfIP_init(&s); + s.last_tick = 1000U; + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->sock.tcp.state = TCP_ESTABLISHED; + ts->sock.tcp.seq = 1000; + ts->sock.tcp.snd_una = 999; + ts->sock.tcp.rto = 100; + ts->sock.tcp.bytes_in_flight = 1; + ts->sock.tcp.persist_retries = 5; + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + ck_assert_int_eq(enqueue_tcp_tx(ts, 1, TCP_FLAG_PSH), 0); + desc = fifo_peek(&ts->sock.tcp.txbuf); + ck_assert_ptr_nonnull(desc); + desc->flags |= PKT_FLAG_SENT; + + memset(&ackseg, 0, sizeof(ackseg)); + ackseg.hlen = TCP_HEADER_LEN << 2; + ackseg.flags = TCP_FLAG_ACK; + ackseg.ack = ee32(1000); + ackseg.ip.len = ee16(IP_HEADER_LEN + TCP_HEADER_LEN); + + tcp_ack(ts, &ackseg); + + ck_assert_uint_eq(ts->sock.tcp.persist_retries, 0); +} +END_TEST + START_TEST(test_tcp_zero_wnd_probe_selects_middle_byte_at_snd_una) { struct wolfIP s; diff --git a/src/wolfip.c b/src/wolfip.c index 12a03757..8277cb0d 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -164,6 +164,11 @@ struct wolfIP_icmp_packet; * 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 +/* Unanswered zero-window probe budget for teardown states (FIN_WAIT_1, + * LAST_ACK): with the 1 s base interval and 60 s backoff cap the arms are + * 1,2,4,8,16,32,60,60 = 183 s of peer silence before the socket is + * released, in the spirit of the RFC 9293 R2 (3 min) teardown timeout. */ +#define TCP_PERSIST_MAXRTX 8U #define TCP_RTO_MAX_BACKOFF 15U /* Max retries before closing; also clamps shift */ #ifdef IP_MULTICAST @@ -1252,6 +1257,7 @@ struct tcpsocket { uint8_t early_rexmit_done; uint8_t persist_backoff; uint8_t persist_active; + uint8_t persist_retries; uint8_t ctrl_rto_retries; uint8_t ctrl_rto_active; uint8_t fin_wait_2_timeout_active; @@ -4775,7 +4781,10 @@ static uint32_t tcp_snd_nxt(struct tsocket *t) return snd_nxt; } -static int tcp_has_pending_unsent_payload(struct tsocket *t) +/* Payload length of the oldest unsent data segment, 0 when none is queued. + * ACKED descriptors are done (their cleanup pop runs later in the same + * tcp_ack() pass), so they are not pending payload. */ +static uint32_t tcp_head_unsent_seg_len(struct tsocket *t) { struct pkt_desc *desc; uint32_t guard = 0; @@ -4790,16 +4799,19 @@ static int tcp_has_pending_unsent_payload(struct tsocket *t) uint32_t seg_len; seg = (struct wolfIP_tcp_seg *)(t->txmem + desc->pos + sizeof(*desc)); seg_len = tcp_tx_desc_payload_len(t, desc, seg); - /* An ACKED descriptor is done (its cleanup pop runs later in the - * same tcp_ack() pass); it is not pending payload. */ if (seg_len > 0 && !(desc->flags & PKT_FLAG_SENT) && !(desc->flags & PKT_FLAG_ACKED)) - return 1; + return seg_len; desc = fifo_next(&t->sock.tcp.txbuf, desc); } return 0; } +static int tcp_has_pending_unsent_payload(struct tsocket *t) +{ + return tcp_head_unsent_seg_len(t) > 0; +} + static uint32_t tcp_persist_interval_ms(const struct tsocket *t) { uint64_t interval = (uint64_t)t->sock.tcp.rto << t->sock.tcp.persist_backoff; @@ -4820,16 +4832,21 @@ static void tcp_persist_stop(struct tsocket *t) } t->sock.tcp.persist_backoff = 0; t->sock.tcp.persist_active = 0; + t->sock.tcp.persist_retries = 0; } static void tcp_persist_start(struct tsocket *t, uint64_t now) { struct wolfIP_timer tmr = {0}; uint32_t interval; + uint32_t head_len; if (!t || t->proto != WI_IPPROTO_TCP) return; - if (t->sock.tcp.peer_rwnd > 0 || !tcp_has_pending_unsent_payload(t)) { + head_len = tcp_head_unsent_seg_len(t); + /* Persist only probes a window-blocked queue: no pending payload, or a + * peer window that already fits the head segment, means data flows. */ + if (head_len == 0 || head_len <= t->sock.tcp.peer_rwnd) { tcp_persist_stop(t); return; } @@ -4981,15 +4998,27 @@ static void tcp_persist_cb(void *arg) struct tsocket *t = (struct tsocket *)arg; if (!t || t->proto != WI_IPPROTO_TCP) return; - if (t->sock.tcp.state != TCP_ESTABLISHED && t->sock.tcp.state != TCP_CLOSE_WAIT) { + if (t->sock.tcp.state != TCP_ESTABLISHED && + t->sock.tcp.state != TCP_CLOSE_WAIT && + t->sock.tcp.state != TCP_FIN_WAIT_1 && + t->sock.tcp.state != TCP_LAST_ACK) { tcp_persist_stop(t); return; } - if (t->sock.tcp.peer_rwnd > 0 || !tcp_has_pending_unsent_payload(t)) { + if (t->sock.tcp.peer_rwnd >= tcp_head_unsent_seg_len(t)) { tcp_persist_stop(t); return; } (void)tcp_send_zero_wnd_probe(t); + t->sock.tcp.persist_retries++; + if ((t->sock.tcp.state == TCP_FIN_WAIT_1 || + t->sock.tcp.state == TCP_LAST_ACK) && + t->sock.tcp.persist_retries >= TCP_PERSIST_MAXRTX) { + /* Teardown must not be pinnable by a silent peer: release the + * socket once the unanswered-probe budget runs out. */ + tcp_ctrl_rto_give_up(t); + return; + } if (t->sock.tcp.persist_backoff < 10) t->sock.tcp.persist_backoff++; /* The timer that fired is out of the heap; drop the stale handle so @@ -6011,6 +6040,7 @@ static void tcp_ack(struct tsocket *t, const struct wolfIP_tcp_seg *tcp) t->sock.tcp.dup_acks = 0; t->sock.tcp.early_rexmit_done = 0; t->sock.tcp.last_early_rexmit_ack = ack; + t->sock.tcp.persist_retries = 0; /* Any forward ACK exits RTO recovery: clear exponential backoff and * stop the current RTO timer. If bytes remain in-flight and no new * send happens immediately, we must re-arm RTO here to avoid stalls. */ @@ -13084,7 +13114,8 @@ static void flush_tcp_tx(struct wolfIP *s, uint64_t now) } else { struct pkt_desc *rexmit_desc = NULL; struct pkt_desc *ack_desc = NULL; - if (seg_payload_len > 0 && ts->sock.tcp.peer_rwnd == 0) + if (seg_payload_len > 0 && in_flight == 0 && + seg_payload_len > ts->sock.tcp.peer_rwnd) tcp_persist_start(ts, now); if (!is_retrans) { rexmit_desc = tcp_find_pending_retrans(ts, desc); From bd6f4a2eff6b50b179d4ef904af3f384b2c4119f Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 1 Oct 2026 07:49:51 +0200 Subject: [PATCH 03/15] F-14489: skip DAD when a renew/rebind ACK keeps the in-use IP --- src/test/unit/unit.c | 3 + src/test/unit/unit_tests_dhcp_edges.c | 116 ++++++++++++++++++++++++++ src/wolfip.c | 57 ++++++++----- 3 files changed, 153 insertions(+), 23 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index f0cafbd9..f59eb175 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -1617,6 +1617,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); diff --git a/src/test/unit/unit_tests_dhcp_edges.c b/src/test/unit/unit_tests_dhcp_edges.c index ca3e0e94..7e23facc 100644 --- a/src/test/unit/unit_tests_dhcp_edges.c +++ b/src/test/unit/unit_tests_dhcp_edges.c @@ -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 diff --git a/src/wolfip.c b/src/wolfip.c index 8277cb0d..8f4f1f51 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -10407,6 +10407,12 @@ static int dhcp_parse_ack(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_l if (primary && saw_server_id && lease_s != 0 && (lease_mask != 0) && dhcp_lease_ip_sane(lease_ip, lease_mask)) { + /* Renewal/rebind that re-confirms the address already in use: + * RFC 4331 DAD guards a NEW address; the in-use one already + * proved itself, so go straight to BOUND without re-probing. */ + int skip_dad = (lease_ip == primary->ip) && + ((s->dhcp_state == DHCP_RENEWING) || + (s->dhcp_state == DHCP_REBINDING)); /* Commit the validated configuration atomically. */ s->dhcp_server_ip = cand_server_ip; primary->ip = lease_ip; @@ -10418,29 +10424,34 @@ static int dhcp_parse_ack(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_l dhcp_cancel_timer(s); s->dhcp_ip = primary->ip; #ifdef ETHERNET - /* RFC 4331: probe the address before using it. The - * lease timers are armed now so they are in place when - * the probes complete; the short DAD timer overrides - * them until then. A conflicting answer is detected in - * arp_recv (dhcp_dad_conflict). */ - s->dhcp_state = DHCP_DAD; - s->dhcp_dad_probes = 0; - s->dhcp_dad_if = WOLFIP_PRIMARY_IF_IDX; - /* Arm the lease absolutes, then swap the renew timer for - * the DAD timer: handle_timers() fires every expired - * entry, so leaving both in the heap would double-fire - * the renew. The absolutes survive; the DAD completion - * re-arms the renew timer. */ - dhcp_schedule_lease_timer(s, lease_s, renew_s, rebind_s); - timer_binheap_cancel(&s->timers, s->dhcp_timer); - s->dhcp_timer = NO_TIMER; - /* ll drivers return the frame length (>= 0) on - * success, not 0; only count the probe when it - * actually went out. */ - if (dhcp_send_dad_probe(s) >= 0) - s->dhcp_dad_probes = 1; - dhcp_schedule_timer_at(s, - s->last_tick + DHCP_DAD_INTERVAL_MS); + if (skip_dad) { + s->dhcp_state = DHCP_BOUND; + dhcp_schedule_lease_timer(s, lease_s, renew_s, rebind_s); + } else { + /* RFC 4331: probe the address before using it. The + * lease timers are armed now so they are in place when + * the probes complete; the short DAD timer overrides + * them until then. A conflicting answer is detected in + * arp_recv (dhcp_dad_conflict). */ + s->dhcp_state = DHCP_DAD; + s->dhcp_dad_probes = 0; + s->dhcp_dad_if = WOLFIP_PRIMARY_IF_IDX; + /* Arm the lease absolutes, then swap the renew timer for + * the DAD timer: handle_timers() fires every expired + * entry, so leaving both in the heap would double-fire + * the renew. The absolutes survive; the DAD completion + * re-arms the renew timer. */ + dhcp_schedule_lease_timer(s, lease_s, renew_s, rebind_s); + timer_binheap_cancel(&s->timers, s->dhcp_timer); + s->dhcp_timer = NO_TIMER; + /* ll drivers return the frame length (>= 0) on + * success, not 0; only count the probe when it + * actually went out. */ + if (dhcp_send_dad_probe(s) >= 0) + s->dhcp_dad_probes = 1; + dhcp_schedule_timer_at(s, + s->last_tick + DHCP_DAD_INTERVAL_MS); + } #else s->dhcp_state = DHCP_BOUND; dhcp_schedule_lease_timer(s, lease_s, renew_s, rebind_s); From edfcccf6b7fa611b9319f1910b2f30803b49d34c Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 1 Oct 2026 07:49:51 +0200 Subject: [PATCH 04/15] F-14490: clear is_listener when the filter vetoes listen() --- src/test/unit/unit_tests_dns_dhcp.c | 3 +++ src/wolfip.c | 1 + 2 files changed, 4 insertions(+) diff --git a/src/test/unit/unit_tests_dns_dhcp.c b/src/test/unit/unit_tests_dns_dhcp.c index 6b2c1cc4..d5346806 100644 --- a/src/test/unit/unit_tests_dns_dhcp.c +++ b/src/test/unit/unit_tests_dns_dhcp.c @@ -805,6 +805,9 @@ START_TEST(test_sock_listen_errors) wolfIP_filter_set_mask(WOLFIP_FILT_MASK(WOLFIP_FILT_LISTENING)); ck_assert_int_eq(wolfIP_sock_listen(&s, tcp_sd, 1), -1); ck_assert_int_eq(ts->sock.tcp.state, TCP_CLOSED); + /* A rejected listen() must not leave the listener role behind: the + * descriptor would otherwise refuse send/recv after a later connect(). */ + ck_assert_uint_eq(ts->sock.tcp.is_listener, 0); wolfIP_filter_set_callback(NULL, NULL); wolfIP_filter_set_mask(0); diff --git a/src/wolfip.c b/src/wolfip.c index 8f4f1f51..dd455d63 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -9457,6 +9457,7 @@ int wolfIP_sock_listen(struct wolfIP *s, int sockfd, int backlog) WOLFIP_FILT_LISTENING, s, ts, ts->local_ip, ts->src_port, IPADDR_ANY, 0) != 0) { ts->sock.tcp.state = TCP_CLOSED; + ts->sock.tcp.is_listener = 0; return -1; } return 0; From 29ce82ff204864e5487f4e0d986bd75dc09169e0 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 1 Oct 2026 07:49:51 +0200 Subject: [PATCH 05/15] F-14491: stop flush_tcp_tx from walking the stale cursor after a pop --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_tcp_flow.c | 69 +++++++++++++++++++++++++++++ src/wolfip.c | 6 ++- 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index f59eb175..67641972 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -809,6 +809,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); diff --git a/src/test/unit/unit_tests_tcp_flow.c b/src/test/unit/unit_tests_tcp_flow.c index cf0ec7e1..a6119d27 100644 --- a/src/test/unit/unit_tests_tcp_flow.c +++ b/src/test/unit/unit_tests_tcp_flow.c @@ -6975,6 +6975,75 @@ START_TEST(test_flush_tcp_tx_pure_ack_keeps_unacked_data_desc) } END_TEST +/* A pure ACK at the FIFO tail whose raw end lands on the wrap boundary: + * the pop clears h_wrap, so the walk must resume from the new peek, not + * the just-popped (stale) descriptor - fifo_next() from it would wrap to + * the zeroed head region and send phantom frames. */ +START_TEST(test_flush_tcp_tx_popped_tail_wrap_gap_no_phantom_frames) +{ + struct wolfIP s; + struct tsocket *ts; + struct pkt_desc *desc; + uint8_t *tx; + uint32_t t_pos; + uint32_t desc_total; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + s.arp.neighbors[0].ip = 0x0A000002U; + s.arp.neighbors[0].if_idx = TEST_PRIMARY_IF; + memcpy(s.arp.neighbors[0].mac, + (uint8_t[]){0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}, 6); + + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->if_idx = TEST_PRIMARY_IF; + ts->sock.tcp.state = TCP_ESTABLISHED; + ts->sock.tcp.ack = 100; + ts->sock.tcp.seq = 1000; + ts->sock.tcp.snd_una = 1000; + ts->sock.tcp.rto = 200; + ts->sock.tcp.cwnd = TXBUF_SIZE; + ts->sock.tcp.peer_rwnd = TXBUF_SIZE; + ts->src_port = 1234; + ts->dst_port = 4321; + ts->local_ip = 0x0A000001U; + ts->remote_ip = 0x0A000002U; + queue_init(&ts->sock.tcp.rxbuf, ts->rxmem, RXBUF_SIZE, ts->sock.tcp.ack); + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + /* One pure ACK, then rotate it (4-aligned) so its raw end lands 3 bytes + * short of the buffer end: the push-wrap bookkeeping leaves h_wrap at + * the raw end and head at 0, so the pop clears h_wrap. The stale + * cursor's fifo_next() then lands in the wrap gap past the aligned + * end, where it reads a descriptor that is not there. */ + ck_assert_int_eq(enqueue_tcp_tx(ts, 0, TCP_FLAG_ACK), 0); + desc = fifo_peek(&ts->sock.tcp.txbuf); + ck_assert_ptr_nonnull(desc); + desc_total = sizeof(struct pkt_desc) + desc->len; + ck_assert_uint_eq(desc_total % 4, 2); /* Ethernet geometry: 2-byte gap */ + t_pos = TXBUF_SIZE - desc_total - 6; /* 4-aligned, gap of 4 bytes after */ + tx = ts->txmem; + memmove(tx + t_pos, tx, desc_total); + memset(tx, 0, t_pos); + ((struct pkt_desc *)(tx + t_pos))->pos = t_pos; + ts->sock.tcp.txbuf.tail = t_pos; + ts->sock.tcp.txbuf.h_wrap = t_pos + desc_total; + ts->sock.tcp.txbuf.head = 0; + + last_frame_sent_count = 0; + (void)wolfIP_poll(&s, 200); + + /* The ACK goes out exactly once; a stale-cursor walk into the zeroed + * head region would emit phantom frames. */ + ck_assert_uint_eq(last_frame_sent_count, 1); + ck_assert_int_eq(fifo_is_empty(&ts->sock.tcp.txbuf), 1); +} +END_TEST + /* Regression: the tcp_ack() zero-length drain pops the oldest descriptor, * so a zero-length descriptor parked behind a just-acked data descriptor * must not be popped there (that would discard the data descriptor and lose diff --git a/src/wolfip.c b/src/wolfip.c index dd455d63..455f43b4 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -13087,8 +13087,12 @@ static void flush_tcp_tx(struct wolfIP *s, uint64_t now) if (size == IP_HEADER_LEN + (uint32_t)(tcp->hlen >> 2)) { if (desc == fifo_peek(&ts->sock.tcp.txbuf)) { /* Cursor at the tail: fifo_pop() removes exactly - * this descriptor. */ + * this descriptor. Resume from the new peek: the + * popped pointer is stale, and when the pop + * clears h_wrap, fifo_next() from it lands in the + * unused wrap gap. */ desc = fifo_pop(&ts->sock.tcp.txbuf); + desc = fifo_peek(&ts->sock.tcp.txbuf); } else { /* fifo_pop() only removes the tail, so popping * here would discard the unacked data descriptor From ba4f5ce88f005d213898f758e13690cb2a7b705e Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 1 Oct 2026 07:49:51 +0200 Subject: [PATCH 06/15] F-14496: drop the dead second closed-socket guard in tcp_process_ts --- src/wolfip.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/wolfip.c b/src/wolfip.c index 455f43b4..3d70cbcf 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -5636,8 +5636,6 @@ static int tcp_process_ts(struct tsocket *t, const struct wolfIP_tcp_seg *tcp, tcp_parse_options(tcp, frame_len, &po); if (!po.ts_found) return -1; - if (!t->S) - return -1; /* Socket was closed; ignore. */ /* RFC 7323 section 4.3 rule (2): TS.Recent is replaced only when the * segment's TSval is not older than the stored one and the segment's * sequence is at or below the ACK field of the last segment we sent From ff284470750ddf2ff77e9c7aac15be44af78acd9 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 1 Oct 2026 07:49:51 +0200 Subject: [PATCH 07/15] docs: record the TCP urgent-data scope decision --- docs/rfc9293-scope.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 docs/rfc9293-scope.md diff --git a/docs/rfc9293-scope.md b/docs/rfc9293-scope.md new file mode 100644 index 00000000..c42ff00c --- /dev/null +++ b/docs/rfc9293-scope.md @@ -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. From 9d9bd9b9312e0a3c1003a3312d75c3cbc0a521f4 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 1 Oct 2026 09:25:36 +0200 Subject: [PATCH 08/15] F-14497: log wc_AesGcmEncrypt, not wc_AesGcmDecrypt, on encrypt failure --- src/wolfesp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wolfesp.c b/src/wolfesp.c index 788c6739..8ebb71ad 100644 --- a/src/wolfesp.c +++ b/src/wolfesp.c @@ -1149,7 +1149,7 @@ esp_aes_rfc4106_enc(const wolfIP_esp_sa * esp_sa, uint8_t * esp_data, err = wc_AesGcmEncrypt(&gcm_enc, enc_payload, enc_payload, enc_len, nonce, sizeof(nonce), icv, icv_len, aad, aad_len); if (err != 0) { - ESP_LOG("error: wc_AesGcmDecrypt: %d\n", err); + ESP_LOG("error: wc_AesGcmEncrypt: %d\n", err); goto rfc4106_enc_out; } From 974a47f9e9fd752d846f944578b8aab6a46a77db Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 1 Oct 2026 09:29:57 +0200 Subject: [PATCH 09/15] F-14493: wildcard-bound sockets source from the egress interface --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_api.c | 45 ++++++++++++++++++++++++++++++++++ src/wolfip.c | 25 +++++++++++-------- 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 67641972..31439f45 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -246,6 +246,7 @@ 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_wrong_family); tcase_add_test(tc_utils, test_sock_connect_icmp_local_ip_pre_set); diff --git a/src/test/unit/unit_tests_api.c b/src/test/unit/unit_tests_api.c index 9df17ebd..f2f0810e 100644 --- a/src/test/unit/unit_tests_api.c +++ b/src/test/unit/unit_tests_api.c @@ -2585,6 +2585,51 @@ START_TEST(test_sock_connect_icmp_primary_ip_any) } 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_primary_ip_fallback) { struct wolfIP s; diff --git a/src/wolfip.c b/src/wolfip.c index 3d70cbcf..36f41629 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -7860,7 +7860,7 @@ int wolfIP_sock_sendto(struct wolfIP *s, int sockfd, const void *buf, size_t len struct ipconf *conf; uint16_t dst_port; ip4 remote_ip; - ip4 src_ip; + ip4 src_ip = 0; uint32_t ip_mtu; uint32_t frame_len; if (SOCKET_UNMARK(sockfd) >= MAX_UDPSOCKETS) @@ -7896,7 +7896,12 @@ int wolfIP_sock_sendto(struct wolfIP *s, int sockfd, const void *buf, size_t len if_idx = ts->sock.udp.mcast_if_idx; #endif conf = wolfIP_ipconf_at(s, if_idx); - src_ip = ts->local_ip; + /* A socket bound to a specific address pins its source; a + * wildcard (or unbound) socket sources each datagram from the + * egress interface's address. local_ip stays egress-only; + * ingress matching uses bound_local_ip. */ + if (ts->bound && ts->bound_local_ip != IPADDR_ANY) + src_ip = ts->local_ip; if (src_ip == 0) { if (conf && conf->ip != IPADDR_ANY) src_ip = conf->ip; @@ -7904,6 +7909,8 @@ int wolfIP_sock_sendto(struct wolfIP *s, int sockfd, const void *buf, size_t len struct ipconf *primary = wolfIP_primary_ipconf(s); if (primary && primary->ip != IPADDR_ANY) src_ip = primary->ip; + else + src_ip = IPADDR_ANY; } } /* Bind the socket's egress state once for any sendto (connected or @@ -7983,14 +7990,12 @@ int wolfIP_sock_sendto(struct wolfIP *s, int sockfd, const void *buf, size_t len if_idx = wolfIP_route_for_ip(s, remote_ip); conf = wolfIP_ipconf_at(s, if_idx); ts->if_idx = (uint8_t)if_idx; - if (ts->local_ip == 0) { - if (conf && conf->ip != IPADDR_ANY) - ts->local_ip = conf->ip; - else { - struct ipconf *primary = wolfIP_primary_ipconf(s); - if (primary && primary->ip != IPADDR_ANY) - ts->local_ip = primary->ip; - } + if (conf && conf->ip != IPADDR_ANY) + ts->local_ip = conf->ip; + else { + struct ipconf *primary = wolfIP_primary_ipconf(s); + if (primary && primary->ip != IPADDR_ANY) + ts->local_ip = primary->ip; } } ip_mtu = wolfIP_socket_ip_mtu(ts); From b8f5ba1147292724e6ed4e21ea70cba039b1f1ac Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 1 Oct 2026 09:30:59 +0200 Subject: [PATCH 10/15] F-14494: resolve the ICMP connect target before the bound-address check --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_api.c | 53 ++++++++++++++++++++++++++++++++++ src/wolfip.c | 7 ++++- 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 31439f45..91a76f91 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -247,6 +247,7 @@ Suite *wolf_suite(void) 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_no_match_keeps_state); tcase_add_test(tc_utils, test_sock_connect_icmp_bound_local_ip_match); tcase_add_test(tc_utils, test_sock_connect_icmp_wrong_family); tcase_add_test(tc_utils, test_sock_connect_icmp_local_ip_pre_set); diff --git a/src/test/unit/unit_tests_api.c b/src/test/unit/unit_tests_api.c index f2f0810e..1237b91c 100644 --- a/src/test/unit/unit_tests_api.c +++ b/src/test/unit/unit_tests_api.c @@ -2630,6 +2630,59 @@ START_TEST(test_sendto_wildcard_bound_uses_egress_if_source) } 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; + const ip4 primary_ip = 0xC0A80009U; + const ip4 secondary_ip = 0xC0A80109U; + const ip4 remote_secondary = 0xC0A801A1U; + int icmp_sd; + struct tsocket *ts; + struct wolfIP_sockaddr_in sin; + + setup_stack_with_two_ifaces(&s, primary_ip, secondary_ip); + + 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 = primary_ip; + + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_addr.s_addr = ee32(remote_secondary); + + ck_assert_int_eq(wolfIP_sock_connect(&s, icmp_sd, (struct wolfIP_sockaddr *)&sin, sizeof(sin)), 0); + ck_assert_uint_eq(ts->local_ip, primary_ip); + ck_assert_uint_eq(ts->if_idx, TEST_PRIMARY_IF); +} +END_TEST + START_TEST(test_sock_connect_icmp_primary_ip_fallback) { struct wolfIP s; diff --git a/src/wolfip.c b/src/wolfip.c index 36f41629..df60b926 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -7414,21 +7414,26 @@ int wolfIP_sock_connect(struct wolfIP *s, int sockfd, const struct wolfIP_sockad return 0; } else if (IS_SOCKET_ICMP(sockfd)) { struct ipconf *conf; + ip4 new_remote_ip; if (SOCKET_UNMARK(sockfd) >= MAX_ICMPSOCKETS) return -WOLFIP_EINVAL; ts = &s->icmpsockets[SOCKET_UNMARK(sockfd)]; if ((sin->sin_family != AF_INET) || (addrlen < sizeof(struct wolfIP_sockaddr_in))) return -WOLFIP_EINVAL; - ts->remote_ip = ee32(sin->sin_addr.s_addr); + /* Resolve into a local first, as in the UDP branch above: a failed + * bound-address check must not narrow the receive filter. */ + new_remote_ip = ee32(sin->sin_addr.s_addr); if (ts->bound_local_ip != IPADDR_ANY) { int bound_match = 0; unsigned int bound_if = wolfIP_if_for_local_ip(s, ts->bound_local_ip, &bound_match); if (!bound_match) return -WOLFIP_EINVAL; + ts->remote_ip = new_remote_ip; ts->if_idx = (uint8_t)bound_if; ts->local_ip = ts->bound_local_ip; } else { + ts->remote_ip = new_remote_ip; if_idx = wolfIP_route_for_ip(s, ts->remote_ip); conf = wolfIP_ipconf_at(s, if_idx); ts->if_idx = (uint8_t)if_idx; From fe18fb41c7a9e9def65d2fa9bd7befaadf9df304 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 1 Oct 2026 09:31:05 +0200 Subject: [PATCH 11/15] F-14492: compare bind claims, not resolved egress IPs, in the port-conflict check --- src/test/unit/unit.c | 3 +- src/test/unit/unit_tests_api.c | 87 +++++++++------------------ src/test/unit/unit_tests_misc_edges.c | 41 +++++++++++++ src/wolfip.c | 18 ++++-- 4 files changed, 86 insertions(+), 63 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 91a76f91..6b975574 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -247,8 +247,8 @@ Suite *wolf_suite(void) 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_no_match_keeps_state); 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); @@ -1815,6 +1815,7 @@ 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_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); diff --git a/src/test/unit/unit_tests_api.c b/src/test/unit/unit_tests_api.c index 1237b91c..1d6eec25 100644 --- a/src/test/unit/unit_tests_api.c +++ b/src/test/unit/unit_tests_api.c @@ -2585,6 +2585,36 @@ START_TEST(test_sock_connect_icmp_primary_ip_any) } END_TEST +START_TEST(test_sock_connect_icmp_primary_ip_fallback) +{ + struct wolfIP s; + int icmp_sd; + struct tsocket *ts; + struct wolfIP_sockaddr_in sin; + ip4 primary_ip = 0x0A000001U; + + wolfIP_init(&s); + mock_link_init(&s); + s.if_count = TEST_SECOND_IF + 1; + s.ipconf[TEST_PRIMARY_IF].ip = primary_ip; + s.ipconf[TEST_PRIMARY_IF].mask = 0xFFFFFF00U; + s.ipconf[TEST_SECOND_IF].ip = IPADDR_ANY; + s.ipconf[TEST_SECOND_IF].gw = 0xC0A801FEU; + + 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->local_ip = 0; + + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_addr.s_addr = ee32(0x0B000001U); + + ck_assert_int_eq(wolfIP_sock_connect(&s, icmp_sd, (struct wolfIP_sockaddr *)&sin, sizeof(sin)), 0); + ck_assert_uint_eq(ts->local_ip, primary_ip); +} +END_TEST + START_TEST(test_sendto_wildcard_bound_uses_egress_if_source) { struct wolfIP s; @@ -2683,63 +2713,6 @@ START_TEST(test_sock_connect_icmp_bound_local_ip_match) } END_TEST -START_TEST(test_sock_connect_icmp_primary_ip_fallback) -{ - struct wolfIP s; - int icmp_sd; - struct tsocket *ts; - struct wolfIP_sockaddr_in sin; - ip4 primary_ip = 0x0A000001U; - - wolfIP_init(&s); - mock_link_init(&s); - s.if_count = TEST_SECOND_IF + 1; - s.ipconf[TEST_PRIMARY_IF].ip = primary_ip; - s.ipconf[TEST_PRIMARY_IF].mask = 0xFFFFFF00U; - s.ipconf[TEST_SECOND_IF].ip = IPADDR_ANY; - s.ipconf[TEST_SECOND_IF].gw = 0xC0A801FEU; - - 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->local_ip = 0; - - memset(&sin, 0, sizeof(sin)); - sin.sin_family = AF_INET; - sin.sin_addr.s_addr = ee32(0x0B000001U); - - ck_assert_int_eq(wolfIP_sock_connect(&s, icmp_sd, (struct wolfIP_sockaddr *)&sin, sizeof(sin)), 0); - ck_assert_uint_eq(ts->local_ip, primary_ip); -} -END_TEST - -START_TEST(test_sock_connect_icmp_bound_local_ip_match) -{ - struct wolfIP s; - const ip4 primary_ip = 0xC0A80009U; - const ip4 secondary_ip = 0xC0A80109U; - const ip4 remote_secondary = 0xC0A801A1U; - int icmp_sd; - struct tsocket *ts; - struct wolfIP_sockaddr_in sin; - - setup_stack_with_two_ifaces(&s, primary_ip, secondary_ip); - - 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 = primary_ip; - - memset(&sin, 0, sizeof(sin)); - sin.sin_family = AF_INET; - sin.sin_addr.s_addr = ee32(remote_secondary); - - ck_assert_int_eq(wolfIP_sock_connect(&s, icmp_sd, (struct wolfIP_sockaddr *)&sin, sizeof(sin)), 0); - ck_assert_uint_eq(ts->local_ip, primary_ip); - ck_assert_uint_eq(ts->if_idx, TEST_PRIMARY_IF); -} -END_TEST - START_TEST(test_sock_connect_tcp_established_returns_zero) { struct wolfIP s; diff --git a/src/test/unit/unit_tests_misc_edges.c b/src/test/unit/unit_tests_misc_edges.c index bf5e5712..71b772e2 100644 --- a/src/test/unit/unit_tests_misc_edges.c +++ b/src/test/unit/unit_tests_misc_edges.c @@ -1290,6 +1290,47 @@ START_TEST(test_bind_port_in_use_different_ips_no_collision) } END_TEST +/* ===================================================================== + * bind_port_in_use -- wildcard and specific binds on the same port collide + * ===================================================================== */ +START_TEST(test_bind_wildcard_and_specific_same_port_collide) +{ + struct wolfIP s; + struct wolfIP_sockaddr_in sin; + int fd1; + int fd2; + + setup_stack_with_two_ifaces(&s, 0x0a000001U, 0x0a000101U); + fd1 = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + fd2 = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + ck_assert_int_ge(fd1, 0); + ck_assert_int_ge(fd2, 0); + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = ee16(5000); + + /* Specific first, then wildcard: the wildcard claims the whole port. */ + sin.sin_addr.s_addr = ee32(0x0a000101U); + ck_assert_int_eq(wolfIP_sock_bind(&s, fd1, (struct wolfIP_sockaddr *)&sin, + sizeof(sin)), 0); + sin.sin_addr.s_addr = ee32(IPADDR_ANY); + ck_assert_int_eq(wolfIP_sock_bind(&s, fd2, (struct wolfIP_sockaddr *)&sin, + sizeof(sin)), -1); + + /* Wildcard first, then a specific address on the same port. */ + wolfIP_sock_close(&s, fd1); + wolfIP_sock_close(&s, fd2); + fd1 = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + fd2 = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + sin.sin_addr.s_addr = ee32(IPADDR_ANY); + ck_assert_int_eq(wolfIP_sock_bind(&s, fd1, (struct wolfIP_sockaddr *)&sin, + sizeof(sin)), 0); + sin.sin_addr.s_addr = ee32(0x0a000101U); + ck_assert_int_eq(wolfIP_sock_bind(&s, fd2, (struct wolfIP_sockaddr *)&sin, + sizeof(sin)), -1); +} +END_TEST + /* ===================================================================== * wolfIP_sock_bind -- a rejected TCP bind leaves if_idx unchanged * ===================================================================== */ diff --git a/src/wolfip.c b/src/wolfip.c index df60b926..f7fbc1f0 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -1310,6 +1310,7 @@ struct tsocket { uint16_t proto, events; ip4 local_ip, remote_ip; ip4 bound_local_ip; + uint8_t bound; /* bound_local_ip is a bind claim, not just 0 */ uint16_t src_port, dst_port; struct wolfIP *S; #ifdef ETHERNET @@ -9211,12 +9212,16 @@ static int bind_port_in_use(const struct tsocket *arr, int n, return 0; for (i = 0; i < n; i++) { const struct tsocket *tk = &arr[i]; + /* Compare the bind claim, not the resolved egress address: a + * wildcard bind owns the port on every address; a never-bound + * socket claims its resolved local_ip. */ + ip4 claim = tk->bound ? tk->bound_local_ip : tk->local_ip; if (tk == self) continue; if (tk->src_port != new_port) continue; - if (tk->local_ip != IPADDR_ANY && new_local_ip != IPADDR_ANY && - tk->local_ip != new_local_ip) + if (claim != IPADDR_ANY && new_local_ip != IPADDR_ANY && + claim != new_local_ip) continue; return 1; } @@ -9305,7 +9310,7 @@ int wolfIP_sock_bind(struct wolfIP *s, int sockfd, const struct wolfIP_sockaddr ts->local_ip = IPADDR_ANY; } if (bind_port_in_use(s->tcpsockets, MAX_TCPSOCKETS, ts, - ts->local_ip, new_port)) { + bind_ip, new_port)) { ts->local_ip = prev_ip; ts->if_idx = prev_if_idx; return -1; @@ -9321,6 +9326,7 @@ int wolfIP_sock_bind(struct wolfIP *s, int sockfd, const struct wolfIP_sockaddr ts->src_port = new_port; } ts->bound_local_ip = bind_ip; + ts->bound = 1; return 0; } else if (IS_SOCKET_UDP(sockfd)) { if (SOCKET_UNMARK(sockfd) >= MAX_UDPSOCKETS) @@ -9348,7 +9354,7 @@ int wolfIP_sock_bind(struct wolfIP *s, int sockfd, const struct wolfIP_sockaddr ts->local_ip = IPADDR_ANY; } if (bind_port_in_use(s->udpsockets, MAX_UDPSOCKETS, ts, - ts->local_ip, new_port)) { + bind_ip, new_port)) { ts->local_ip = prev_ip; ts->if_idx = prev_if_idx; return -1; @@ -9369,6 +9375,7 @@ int wolfIP_sock_bind(struct wolfIP *s, int sockfd, const struct wolfIP_sockaddr ts->src_port = new_port; } ts->bound_local_ip = bind_ip; + ts->bound = 1; return 0; } else if (IS_SOCKET_ICMP(sockfd)) { if (SOCKET_UNMARK(sockfd) >= MAX_ICMPSOCKETS) @@ -9396,7 +9403,7 @@ int wolfIP_sock_bind(struct wolfIP *s, int sockfd, const struct wolfIP_sockaddr ts->local_ip = IPADDR_ANY; } if (bind_port_in_use(s->icmpsockets, MAX_ICMPSOCKETS, ts, - ts->local_ip, new_id)) { + bind_ip, new_id)) { ts->local_ip = prev_ip; ts->if_idx = prev_if_idx; return -1; @@ -9414,6 +9421,7 @@ int wolfIP_sock_bind(struct wolfIP *s, int sockfd, const struct wolfIP_sockaddr ts->src_port = new_id; } ts->bound_local_ip = bind_ip; + ts->bound = 1; return 0; #if WOLFIP_RAWSOCKETS } else if (IS_SOCKET_RAW(sockfd)) { From df110482496df07f3a4c576df735365cb880f6ea Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 1 Oct 2026 13:13:11 +0200 Subject: [PATCH 12/15] PR 183 Copilot fixes: persist accepts CLOSING teardown, stops only when the window fits the head segment, and a rebind ACK may come from a foreign server --- src/test/unit/unit.c | 4 + src/test/unit/unit_tests_dns_dhcp.c | 48 ++++++++++ src/test/unit/unit_tests_proto.c | 141 ++++++++++++++++++++++++++++ src/wolfip.c | 25 +++-- 4 files changed, 211 insertions(+), 7 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 6b975574..50ea5e5e 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -434,6 +434,9 @@ Suite *wolf_suite(void) 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_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); @@ -474,6 +477,7 @@ 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_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); diff --git a/src/test/unit/unit_tests_dns_dhcp.c b/src/test/unit/unit_tests_dns_dhcp.c index d5346806..c68d04c2 100644 --- a/src/test/unit/unit_tests_dns_dhcp.c +++ b/src/test/unit/unit_tests_dns_dhcp.c @@ -6730,6 +6730,54 @@ START_TEST(test_dhcp_poll_rebinding_ack_binds_client) } END_TEST +/* A rebind request is a broadcast: a server other than the one committed + * during the OFFER phase may legitimately reconfirm the in-use address. + * The server-ID identity check must not reject that ACK, or the skip-DAD + * rebind path is unreachable for exactly the peers rebind exists for. */ +START_TEST(test_dhcp_poll_rebinding_ack_foreign_server_binds_client) +{ + struct wolfIP s; + struct dhcp_msg msg; + struct tsocket *ts; + struct ipconf *primary; + uint32_t old_server_ip = 0x0A000001U; + uint32_t new_server_ip = 0x0A000002U; + uint32_t client_ip = 0x0A000064U; + uint32_t router_ip = 0x0A000002U; + uint32_t dns_ip = 0x08080808U; + uint32_t mask = 0xFFFFFF00U; + int ret; + + wolfIP_init(&s); + mock_link_init(&s); + primary = wolfIP_primary_ipconf(&s); + ck_assert_ptr_nonnull(primary); + 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); + ts = &s.udpsockets[SOCKET_UNMARK(s.dhcp_udp_sd)]; + + s.last_tick = 1000U; + s.dhcp_state = DHCP_REBINDING; + s.dhcp_xid = 0x12345678U; + s.dhcp_server_ip = old_server_ip; + primary->ip = client_ip; + build_dhcp_ack_msg(&msg, new_server_ip, mask, router_ip, dns_ip); + msg.xid = ee32(s.dhcp_xid); + msg.yiaddr = ee32(client_ip); + memcpy(msg.chaddr, wolfIP_ll_at(&s, WOLFIP_PRIMARY_IF_IDX)->mac, 6); + + enqueue_udp_rx(ts, &msg, sizeof(msg), DHCP_SERVER_PORT); + ret = dhcp_poll(&s); + + /* Same-address reconfirmation from the new server: skip DAD and bind, + * committing the responding server. */ + ck_assert_int_eq(ret, 0); + ck_assert_uint_eq(s.dhcp_state, DHCP_BOUND); + ck_assert_uint_eq(primary->ip, client_ip); + ck_assert_uint_eq(s.dhcp_server_ip, new_server_ip); +} +END_TEST + START_TEST(test_regression_dhcp_nak_deconfigures_address_during_renew_and_rebind) { struct wolfIP s; diff --git a/src/test/unit/unit_tests_proto.c b/src/test/unit/unit_tests_proto.c index 8c5a7390..60aa807c 100644 --- a/src/test/unit/unit_tests_proto.c +++ b/src/test/unit/unit_tests_proto.c @@ -1994,6 +1994,100 @@ START_TEST(test_tcp_persist_cb_last_ack_subsegment_window_sends_probe) } END_TEST +/* CLOSING (FIN_WAIT_1 that received the peer's FIN) keeps window-blocked + * payload pinned: the data RTO is idle with nothing in flight and the + * control RTO yields while payload is queued, so persist is the only + * recovery path and must accept the state. */ +START_TEST(test_tcp_persist_cb_closing_sends_probe) +{ + struct wolfIP s; + struct tsocket *ts; + uint8_t peer_mac[6] = {0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xf2}; + struct wolfIP_tcp_seg *tcp; + uint8_t payload[8] = {0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c}; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + wolfIP_filter_set_callback(NULL, NULL); + last_frame_sent_size = 0; + + s.arp.neighbors[0].ip = 0x0A000002U; + s.arp.neighbors[0].if_idx = TEST_PRIMARY_IF; + memcpy(s.arp.neighbors[0].mac, peer_mac, sizeof(peer_mac)); + + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->sock.tcp.state = TCP_CLOSING; + ts->local_ip = 0x0A000001U; + ts->remote_ip = 0x0A000002U; + ts->if_idx = TEST_PRIMARY_IF; + ts->src_port = 1111; + ts->dst_port = 2222; + ts->sock.tcp.seq = 10; + ts->sock.tcp.ack = 20; + ts->sock.tcp.snd_una = 10; + ts->sock.tcp.rto = 100; + ts->sock.tcp.cwnd = TCP_MSS * 4; + ts->sock.tcp.peer_rwnd = 0; + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + ck_assert_int_eq(enqueue_tcp_tx_with_payload(ts, payload, sizeof(payload), + (TCP_FLAG_ACK | TCP_FLAG_PSH)), 0); + s.last_tick = 500; + tcp_persist_cb(ts); + + ck_assert_uint_gt(last_frame_sent_size, 0); + tcp = (struct wolfIP_tcp_seg *)last_frame_sent; + ck_assert_uint_eq(ee32(tcp->seq), ts->sock.tcp.snd_una); + ck_assert_uint_eq(ts->sock.tcp.persist_active, 1); +} +END_TEST + +/* The teardown retry budget must cover CLOSING as well: a silent peer + * must not be able to pin a closing socket past the unanswered-probe + * budget. */ +START_TEST(test_tcp_persist_cb_closing_gives_up_after_maxrtx) +{ + struct wolfIP s; + struct tsocket *ts; + uint8_t payload[4] = {0x25, 0x26, 0x27, 0x28}; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + wolfIP_filter_set_callback(NULL, NULL); + + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->sock.tcp.state = TCP_CLOSING; + ts->local_ip = 0x0A000001U; + ts->remote_ip = 0x0A000002U; + ts->if_idx = TEST_PRIMARY_IF; + ts->src_port = 1111; + ts->dst_port = 2222; + ts->sock.tcp.seq = 10; + ts->sock.tcp.snd_una = 10; + ts->sock.tcp.rto = 100; + ts->sock.tcp.peer_rwnd = 0; + ts->sock.tcp.persist_retries = TCP_PERSIST_MAXRTX - 1; + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + ck_assert_int_eq(enqueue_tcp_tx_with_payload(ts, payload, sizeof(payload), + (TCP_FLAG_ACK | TCP_FLAG_PSH)), 0); + + s.last_tick = 500; + tcp_persist_cb(ts); + + ck_assert_int_eq(ts->proto, 0); + ck_assert_uint_eq(ts->sock.tcp.persist_active, 0); +} +END_TEST + /* A non-zero peer window smaller than the head segment must still arm the * persist timer (the old peer_rwnd == 0 gate missed this case). */ START_TEST(test_tcp_persist_start_armed_for_subsegment_window) @@ -2025,6 +2119,53 @@ START_TEST(test_tcp_persist_start_armed_for_subsegment_window) } END_TEST +/* A sub-segment peer window (non-zero, smaller than the head segment) must + * not cancel an armed persist timer on the next ordinary poll: the queue + * is still window-blocked, so the probe deadline must survive until it + * fires. */ +START_TEST(test_tcp_persist_survives_poll_with_subsegment_window) +{ + struct wolfIP s; + struct tsocket *ts; + uint8_t payload[8] = {0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c}; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + wolfIP_filter_set_callback(NULL, NULL); + + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->sock.tcp.state = TCP_ESTABLISHED; + ts->local_ip = 0x0A000001U; + ts->remote_ip = 0x0A000002U; + ts->if_idx = TEST_PRIMARY_IF; + ts->src_port = 1111; + ts->dst_port = 2222; + ts->sock.tcp.seq = 10; + ts->sock.tcp.ack = 20; + ts->sock.tcp.snd_una = 10; + ts->sock.tcp.rto = 100; + ts->sock.tcp.cwnd = TCP_MSS * 4; + ts->sock.tcp.peer_rwnd = 2; + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + ck_assert_int_eq(enqueue_tcp_tx_with_payload(ts, payload, sizeof(payload), + (TCP_FLAG_ACK | TCP_FLAG_PSH)), 0); + + s.last_tick = 500; + tcp_persist_start(ts, s.last_tick); + ck_assert_uint_eq(ts->sock.tcp.persist_active, 1); + + (void)wolfIP_poll(&s, 100); + + ck_assert_uint_eq(ts->sock.tcp.persist_active, 1); + ck_assert_int_ne(ts->sock.tcp.tmr_persist, NO_TIMER); +} +END_TEST + /* Teardown states must not be pinnable by a silent peer: after the probe * budget runs out without any acknowledgment, release the socket. */ START_TEST(test_tcp_persist_cb_fin_wait_1_gives_up_after_maxrtx) diff --git a/src/wolfip.c b/src/wolfip.c index f7fbc1f0..2dc9c2d4 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -5002,7 +5002,8 @@ static void tcp_persist_cb(void *arg) if (t->sock.tcp.state != TCP_ESTABLISHED && t->sock.tcp.state != TCP_CLOSE_WAIT && t->sock.tcp.state != TCP_FIN_WAIT_1 && - t->sock.tcp.state != TCP_LAST_ACK) { + t->sock.tcp.state != TCP_LAST_ACK && + t->sock.tcp.state != TCP_CLOSING) { tcp_persist_stop(t); return; } @@ -5013,6 +5014,7 @@ static void tcp_persist_cb(void *arg) (void)tcp_send_zero_wnd_probe(t); t->sock.tcp.persist_retries++; if ((t->sock.tcp.state == TCP_FIN_WAIT_1 || + t->sock.tcp.state == TCP_CLOSING || t->sock.tcp.state == TCP_LAST_ACK) && t->sock.tcp.persist_retries >= TCP_PERSIST_MAXRTX) { /* Teardown must not be pinnable by a silent peer: release the @@ -6404,7 +6406,11 @@ static void tcp_input(struct wolfIP *S, unsigned int if_idx, t->sock.tcp.snd_wscale : 0; t->sock.tcp.peer_rwnd = (uint32_t)raw_win << ws_shift; if (t->sock.tcp.peer_rwnd > prev_peer_rwnd) { - if (t->sock.tcp.persist_active) + /* Stop persist only when the window now fits the head + * segment: a sub-segment window leaves the queue + * window-blocked and the probe must keep running. */ + if (t->sock.tcp.persist_active && + tcp_head_unsent_seg_len(t) <= t->sock.tcp.peer_rwnd) tcp_persist_stop(t); t->events |= CB_EVENT_WRITABLE; } @@ -10355,8 +10361,12 @@ static int dhcp_parse_ack(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_l return -1; val = DHCP_OPT_data_to_u32((struct dhcp_option *)data); /* Reject ACK from a server other than the one we committed to - * during the OFFER phase, wherever the option appears. */ - if (s->dhcp_server_ip != 0 && val != s->dhcp_server_ip) + * during the OFFER phase, wherever the option appears. While + * REBINDING the request is a broadcast and any server may + * reconfirm the in-use address (RFC 2131 4.3.5): accept a + * foreign server ID there, the commit below records it. */ + if (s->dhcp_server_ip != 0 && val != s->dhcp_server_ip && + s->dhcp_state != DHCP_REBINDING) return -1; cand_server_ip = val; saw_server_id = 1; @@ -12978,8 +12988,8 @@ static void flush_tcp_tx(struct wolfIP *s, uint64_t now) ts->sock.tcp.ack_retry_pending = 0; } in_flight = ts->sock.tcp.bytes_in_flight; - if (ts->sock.tcp.persist_active && (ts->sock.tcp.peer_rwnd > 0 || - !tcp_has_pending_unsent_payload(ts))) + if (ts->sock.tcp.persist_active && + tcp_head_unsent_seg_len(ts) <= ts->sock.tcp.peer_rwnd) tcp_persist_stop(ts); desc = fifo_peek(&ts->sock.tcp.txbuf); while (desc && send_guard++ < send_budget) { @@ -13140,7 +13150,8 @@ static void flush_tcp_tx(struct wolfIP *s, uint64_t now) if (next_desc == desc) break; desc = next_desc; - if (ts->sock.tcp.persist_active && ts->sock.tcp.peer_rwnd > 0) + if (ts->sock.tcp.persist_active && + tcp_head_unsent_seg_len(ts) <= ts->sock.tcp.peer_rwnd) tcp_persist_stop(ts); } } else { From e80f71c4aed74ad9f15eec9c267029025ef81057 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 1 Oct 2026 13:53:14 +0200 Subject: [PATCH 13/15] Review fixes: fresh persist budget at close, ICMP ingress matches the bind claim, rebind accepts foreign-server NAKs --- src/test/unit/unit.c | 3 + src/test/unit/unit_tests_dns_dhcp.c | 118 ++++++++++++++++++++++++++++ src/test/unit/unit_tests_proto.c | 58 ++++++++++++++ src/test/unit/unit_tests_tcp_ack.c | 8 ++ src/wolfip.c | 32 +++++--- 5 files changed, 210 insertions(+), 9 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 50ea5e5e..0b3132dd 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -436,6 +436,7 @@ Suite *wolf_suite(void) 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); @@ -478,6 +479,7 @@ Suite *wolf_suite(void) 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_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); @@ -1020,6 +1022,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); diff --git a/src/test/unit/unit_tests_dns_dhcp.c b/src/test/unit/unit_tests_dns_dhcp.c index c68d04c2..ef5d250c 100644 --- a/src/test/unit/unit_tests_dns_dhcp.c +++ b/src/test/unit/unit_tests_dns_dhcp.c @@ -2374,6 +2374,66 @@ START_TEST(test_icmp_input_echo_reply_wrong_dst_dropped) } END_TEST +/* sendto() rewrites local_ip to the egress interface's address on every + * send; ingress matching must use the bound address (wildcard here), + * otherwise a reply to an earlier ping on the other interface is dropped. */ +START_TEST(test_icmp_input_echo_reply_after_second_if_sendto_delivered) +{ + struct wolfIP s; + int icmp_sd; + struct tsocket *ts; + struct wolfIP_sockaddr_in sin; + struct wolfIP_icmp_packet icmp; + uint8_t peer_mac[6] = {0x02, 0x03, 0x04, 0x05, 0x06, 0x07}; + uint32_t frame_len; + uint8_t payload[ICMP_HEADER_LEN] = {0, 0, 0, 0, 0, 0, 0, 1}; + + setup_stack_with_two_ifaces(&s, 0x0a000001U, 0x0a000101U); + s.arp.neighbors[0].ip = 0x0a000002U; + s.arp.neighbors[0].if_idx = TEST_PRIMARY_IF; + memcpy(s.arp.neighbors[0].mac, peer_mac, sizeof(peer_mac)); + s.arp.neighbors[1].ip = 0x0a000102U; + s.arp.neighbors[1].if_idx = TEST_SECOND_IF; + memcpy(s.arp.neighbors[1].mac, peer_mac, sizeof(peer_mac)); + + 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)]; + + /* Ping through the primary, then through the secondary: the second + * send rewrites local_ip to the secondary's address. */ + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_addr.s_addr = ee32(0x0a000002U); + ck_assert_int_ge(wolfIP_sock_sendto(&s, icmp_sd, payload, sizeof(payload), 0, + (const struct wolfIP_sockaddr *)&sin, + sizeof(sin)), 0); + ck_assert_uint_eq(ts->local_ip, 0x0a000001U); + ck_assert_uint_ne(ts->src_port, 0); + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_addr.s_addr = ee32(0x0a000102U); + ck_assert_int_ge(wolfIP_sock_sendto(&s, icmp_sd, payload, sizeof(payload), 0, + (const struct wolfIP_sockaddr *)&sin, + sizeof(sin)), 0); + ck_assert_uint_eq(ts->local_ip, 0x0a000101U); + + /* The reply to the first ping is addressed to the primary's address: + * a wildcard-bound socket must receive it. */ + frame_len = (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + ICMP_HEADER_LEN); + memset(&icmp, 0, sizeof(icmp)); + icmp.ip.src = ee32(0x0a000002U); + icmp.ip.dst = ee32(0x0a000001U); + icmp.ip.ttl = 55; + icmp.ip.len = ee16(IP_HEADER_LEN + ICMP_HEADER_LEN); + icmp.type = ICMP_ECHO_REPLY; + icmp_set_echo_id(&icmp, ts->src_port); + icmp.csum = ee16(icmp_checksum(&icmp, ICMP_HEADER_LEN)); + icmp_input(&s, TEST_PRIMARY_IF, (struct wolfIP_ip_packet *)&icmp, frame_len); + ck_assert_ptr_nonnull(fifo_peek(&ts->sock.udp.rxbuf)); +} +END_TEST + START_TEST(test_icmp_input_echo_request_reply_sent) { struct wolfIP s; @@ -6778,6 +6838,64 @@ START_TEST(test_dhcp_poll_rebinding_ack_foreign_server_binds_client) } END_TEST +/* The mirror of the foreign-server ACK: while REBINDING a NAK from any + * server must deconfigure and restart discovery, not be silently ignored + * (RFC 2131 4.4.5). */ +START_TEST(test_dhcp_poll_rebinding_nak_foreign_server_restarts_discovery) +{ + struct wolfIP s; + struct dhcp_msg msg; + struct dhcp_option *opt; + struct tsocket *ts; + struct ipconf *primary; + uint32_t old_server_ip = 0x0A000001U; + uint32_t new_server_ip = 0x0A000002U; + int ret; + + wolfIP_init(&s); + mock_link_init(&s); + primary = wolfIP_primary_ipconf(&s); + ck_assert_ptr_nonnull(primary); + 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); + ts = &s.udpsockets[SOCKET_UNMARK(s.dhcp_udp_sd)]; + + s.last_tick = 1000U; + s.dhcp_state = DHCP_REBINDING; + s.dhcp_xid = 0x12345678U; + s.dhcp_server_ip = old_server_ip; + primary->ip = 0x0A000064U; + + memset(&msg, 0, sizeof(msg)); + msg.op = BOOT_REPLY; + msg.hlen = 6; + msg.magic = ee32(DHCP_MAGIC); + msg.xid = ee32(s.dhcp_xid); + opt = (struct dhcp_option *)msg.options; + opt->code = DHCP_OPTION_MSG_TYPE; + opt->len = 1; + opt->data[0] = DHCP_NAK; + opt = (struct dhcp_option *)((uint8_t *)opt + 3); + opt->code = DHCP_OPTION_SERVER_ID; + opt->len = 4; + opt->data[0] = (new_server_ip >> 24) & 0xFF; + opt->data[1] = (new_server_ip >> 16) & 0xFF; + opt->data[2] = (new_server_ip >> 8) & 0xFF; + opt->data[3] = (new_server_ip >> 0) & 0xFF; + opt = (struct dhcp_option *)((uint8_t *)opt + 6); + opt->code = DHCP_OPTION_END; + memcpy(msg.chaddr, wolfIP_ll_at(&s, WOLFIP_PRIMARY_IF_IDX)->mac, 6); + + enqueue_udp_rx(ts, &msg, sizeof(msg), DHCP_SERVER_PORT); + ret = dhcp_poll(&s); + + ck_assert_int_eq(ret, 0); + ck_assert_int_eq(s.dhcp_state, DHCP_DISCOVER_SENT); + ck_assert_uint_eq(primary->ip, 0U); + ck_assert_uint_eq(primary->mask, 0U); +} +END_TEST + START_TEST(test_regression_dhcp_nak_deconfigures_address_during_renew_and_rebind) { struct wolfIP s; diff --git a/src/test/unit/unit_tests_proto.c b/src/test/unit/unit_tests_proto.c index 60aa807c..2b916a2e 100644 --- a/src/test/unit/unit_tests_proto.c +++ b/src/test/unit/unit_tests_proto.c @@ -2088,6 +2088,64 @@ START_TEST(test_tcp_persist_cb_closing_gives_up_after_maxrtx) } END_TEST +/* Probes sent while ESTABLISHED must not consume the teardown budget: + * close() starts a fresh give-up window, otherwise a socket that probed + * near the limit before close() is released after one teardown probe and + * its queued data is discarded. */ +START_TEST(test_tcp_persist_close_resets_retry_budget) +{ + struct wolfIP s; + int tcp_sd; + struct tsocket *ts; + uint8_t peer_mac[6] = {0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xf2}; + uint8_t payload[8] = {0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c}; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + wolfIP_filter_set_callback(NULL, NULL); + + s.arp.neighbors[0].ip = 0x0A000002U; + s.arp.neighbors[0].if_idx = TEST_PRIMARY_IF; + memcpy(s.arp.neighbors[0].mac, peer_mac, sizeof(peer_mac)); + + 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)]; + ts->S = &s; + ts->sock.tcp.state = TCP_ESTABLISHED; + ts->local_ip = 0x0A000001U; + ts->remote_ip = 0x0A000002U; + ts->if_idx = TEST_PRIMARY_IF; + ts->src_port = 1111; + ts->dst_port = 2222; + ts->sock.tcp.seq = 10; + ts->sock.tcp.ack = 20; + ts->sock.tcp.snd_una = 10; + ts->sock.tcp.rto = 100; + ts->sock.tcp.cwnd = TCP_MSS * 4; + ts->sock.tcp.peer_rwnd = 0; + ts->sock.tcp.persist_retries = TCP_PERSIST_MAXRTX - 2; + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + ck_assert_int_eq(enqueue_tcp_tx_with_payload(ts, payload, sizeof(payload), + (TCP_FLAG_ACK | TCP_FLAG_PSH)), 0); + + s.last_tick = 500; + ck_assert_int_eq(wolfIP_sock_close(&s, tcp_sd), -WOLFIP_EAGAIN); + ck_assert_uint_eq(ts->sock.tcp.state, TCP_FIN_WAIT_1); + + /* Two unanswered teardown probes must not exhaust a fresh budget. */ + s.last_tick = 600; + tcp_persist_cb(ts); + s.last_tick = 700; + tcp_persist_cb(ts); + + ck_assert_int_ne(ts->proto, 0); + ck_assert_uint_eq(ts->sock.tcp.state, TCP_FIN_WAIT_1); +} +END_TEST + /* A non-zero peer window smaller than the head segment must still arm the * persist timer (the old peer_rwnd == 0 gate missed this case). */ START_TEST(test_tcp_persist_start_armed_for_subsegment_window) diff --git a/src/test/unit/unit_tests_tcp_ack.c b/src/test/unit/unit_tests_tcp_ack.c index 39da78ad..ea2845d6 100644 --- a/src/test/unit/unit_tests_tcp_ack.c +++ b/src/test/unit/unit_tests_tcp_ack.c @@ -3143,7 +3143,11 @@ START_TEST(test_icmp_try_recv_mismatch_paths) 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)]; + /* Bound to a specific address: the address filter applies to the bind + * claim, not the per-send egress address in local_ip. */ ts->local_ip = 0x0A000001U; + ts->bound_local_ip = 0x0A000001U; + ts->bound = 1; ts->remote_ip = 0x0A000002U; ts->src_port = ee16(0x1234); @@ -3184,7 +3188,11 @@ START_TEST(test_icmp_try_recv_mismatch_local_ip) ts = icmp_new_socket(&s); ck_assert_ptr_nonnull(ts); + /* Bound to a specific address: the address filter now applies to the + * bind claim, not the per-send egress address in local_ip. */ ts->local_ip = 0x0A000001U; + ts->bound_local_ip = 0x0A000001U; + ts->bound = 1; memset(&icmp, 0, sizeof(icmp)); icmp.ip.len = ee16(IP_HEADER_LEN + ICMP_HEADER_LEN); diff --git a/src/wolfip.c b/src/wolfip.c index 2dc9c2d4..ea52a8e6 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -164,10 +164,12 @@ struct wolfIP_icmp_packet; * 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 -/* Unanswered zero-window probe budget for teardown states (FIN_WAIT_1, +/* Zero-window probe budget for teardown states (FIN_WAIT_1, CLOSING, * LAST_ACK): with the 1 s base interval and 60 s backoff cap the arms are - * 1,2,4,8,16,32,60,60 = 183 s of peer silence before the socket is - * released, in the spirit of the RFC 9293 R2 (3 min) teardown timeout. */ + * 1,2,4,8,16,32,60,60 = 183 s before the socket is released, in the spirit + * of the RFC 9293 R2 (3 min) teardown timeout. A forward ACK resets the + * counter; a peer that keeps answering with a zero window still consumes + * the budget - a deliberately finite patience, not infinite. */ #define TCP_PERSIST_MAXRTX 8U #define TCP_RTO_MAX_BACKOFF 15U /* Max retries before closing; also clamps shift */ @@ -3389,7 +3391,10 @@ static void icmp_try_recv(struct wolfIP *s, unsigned int if_idx, struct tsocket *t = &s->icmpsockets[i]; if (t->proto != WI_IPPROTO_ICMP) continue; - if (t->local_ip != 0 && t->local_ip != dst_ip) + /* Ingress matches the bound address, not the per-send egress + * address in local_ip: a wildcard (or unbound) socket receives + * replies to pings sent through any interface. */ + if (t->bound_local_ip != IPADDR_ANY && t->bound_local_ip != dst_ip) continue; if (t->src_port != 0 && t->src_port != echo_id) continue; @@ -8905,6 +8910,9 @@ int wolfIP_sock_close(struct wolfIP *s, int sockfd) return -WOLFIP_EAGAIN; ts->sock.tcp.state = TCP_FIN_WAIT_1; ts->sock.tcp.ctrl_rto_retries = 0; + /* Fresh teardown: probes sent while ESTABLISHED must not + * consume the teardown give-up budget. */ + ts->sock.tcp.persist_retries = 0; ts->callback = NULL; ts->callback_arg = NULL; if (tcp_ctrl_rto_start(ts, s->last_tick) < 0) { @@ -8928,6 +8936,9 @@ int wolfIP_sock_close(struct wolfIP *s, int sockfd) return -WOLFIP_EAGAIN; ts->sock.tcp.state = TCP_LAST_ACK; ts->sock.tcp.ctrl_rto_retries = 0; + /* Fresh teardown: probes sent before close() must not consume + * the teardown give-up budget. */ + ts->sock.tcp.persist_retries = 0; ts->callback = NULL; ts->callback_arg = NULL; if (tcp_ctrl_rto_start(ts, s->last_tick) < 0) { @@ -10294,8 +10305,10 @@ static int dhcp_msg_type(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_le } } /* Reject a reply that does not carry the server identifier of the - * server we committed to during the OFFER phase. */ - if (s->dhcp_server_ip != 0 && + * server we committed to during the OFFER phase. While REBINDING the + * request is a broadcast and any server may answer (RFC 2131 4.3.5), + * so the identity check yields there - including for NAKs. */ + if (s->dhcp_server_ip != 0 && s->dhcp_state != DHCP_REBINDING && (!saw_server_id || server_id != s->dhcp_server_ip)) return -1; return msg_type; @@ -10435,7 +10448,7 @@ static int dhcp_parse_ack(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_l (lease_mask != 0) && dhcp_lease_ip_sane(lease_ip, lease_mask)) { /* Renewal/rebind that re-confirms the address already in use: - * RFC 4331 DAD guards a NEW address; the in-use one already + * RFC 5227 DAD guards a NEW address; the in-use one already * proved itself, so go straight to BOUND without re-probing. */ int skip_dad = (lease_ip == primary->ip) && ((s->dhcp_state == DHCP_RENEWING) || @@ -10455,7 +10468,7 @@ static int dhcp_parse_ack(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_l s->dhcp_state = DHCP_BOUND; dhcp_schedule_lease_timer(s, lease_s, renew_s, rebind_s); } else { - /* RFC 4331: probe the address before using it. The + /* RFC 5227: probe the address before using it. The * lease timers are armed now so they are in place when * the probes complete; the short DAD timer overrides * them until then. A conflicting answer is detected in @@ -13151,7 +13164,8 @@ static void flush_tcp_tx(struct wolfIP *s, uint64_t now) break; desc = next_desc; if (ts->sock.tcp.persist_active && - tcp_head_unsent_seg_len(ts) <= ts->sock.tcp.peer_rwnd) + tcp_head_unsent_seg_len(ts) <= + ts->sock.tcp.peer_rwnd) tcp_persist_stop(ts); } } else { From aab490323107e7cf4256fb86f9e631693a48aa39 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 1 Oct 2026 14:43:13 +0200 Subject: [PATCH 14/15] Copilot re-review: allocate TCP ephemerals against the bind claim; keep the server-ID presence check unconditional in REBINDING --- src/test/unit/unit.c | 2 + src/test/unit/unit_tests_dns_dhcp.c | 52 ++++++++++++++++++++++++++ src/test/unit/unit_tests_misc_edges.c | 54 +++++++++++++++++++++++++++ src/wolfip.c | 18 ++++++--- 4 files changed, 120 insertions(+), 6 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 0b3132dd..887909fc 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -480,6 +480,7 @@ Suite *wolf_suite(void) 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); @@ -1823,6 +1824,7 @@ Suite *wolf_suite(void) #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); diff --git a/src/test/unit/unit_tests_dns_dhcp.c b/src/test/unit/unit_tests_dns_dhcp.c index ef5d250c..8ba16055 100644 --- a/src/test/unit/unit_tests_dns_dhcp.c +++ b/src/test/unit/unit_tests_dns_dhcp.c @@ -6896,6 +6896,58 @@ START_TEST(test_dhcp_poll_rebinding_nak_foreign_server_restarts_discovery) } END_TEST +/* A DHCPNAK without a server identifier is not a rebind answer, in any + * state: the presence check stays unconditional, REBINDING only waives + * the equality comparison. */ +START_TEST(test_dhcp_poll_rebinding_nak_without_server_id_ignored) +{ + struct wolfIP s; + struct dhcp_msg msg; + struct dhcp_option *opt; + struct tsocket *ts; + struct ipconf *primary; + int ret; + + wolfIP_init(&s); + mock_link_init(&s); + primary = wolfIP_primary_ipconf(&s); + ck_assert_ptr_nonnull(primary); + 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); + ts = &s.udpsockets[SOCKET_UNMARK(s.dhcp_udp_sd)]; + + s.last_tick = 1000U; + s.dhcp_state = DHCP_REBINDING; + s.dhcp_xid = 0x12345678U; + s.dhcp_server_ip = 0x0A000001U; + primary->ip = 0x0A000064U; + primary->mask = 0xFFFFFF00U; + + /* NAK with no option 54 at all. */ + memset(&msg, 0, sizeof(msg)); + msg.op = BOOT_REPLY; + msg.hlen = 6; + msg.magic = ee32(DHCP_MAGIC); + msg.xid = ee32(s.dhcp_xid); + opt = (struct dhcp_option *)msg.options; + opt->code = DHCP_OPTION_MSG_TYPE; + opt->len = 1; + opt->data[0] = DHCP_NAK; + opt = (struct dhcp_option *)((uint8_t *)opt + 3); + opt->code = DHCP_OPTION_END; + memcpy(msg.chaddr, wolfIP_ll_at(&s, WOLFIP_PRIMARY_IF_IDX)->mac, 6); + + enqueue_udp_rx(ts, &msg, sizeof(msg), DHCP_SERVER_PORT); + ret = dhcp_poll(&s); + + /* The lease survives: the NAK was not from a server. */ + ck_assert_int_eq(ret, 0); + ck_assert_int_eq(s.dhcp_state, DHCP_REBINDING); + ck_assert_uint_eq(primary->ip, 0x0A000064U); + ck_assert_uint_eq(primary->mask, 0xFFFFFF00U); +} +END_TEST + START_TEST(test_regression_dhcp_nak_deconfigures_address_during_renew_and_rebind) { struct wolfIP s; diff --git a/src/test/unit/unit_tests_misc_edges.c b/src/test/unit/unit_tests_misc_edges.c index 71b772e2..5e7586f3 100644 --- a/src/test/unit/unit_tests_misc_edges.c +++ b/src/test/unit/unit_tests_misc_edges.c @@ -1331,6 +1331,60 @@ START_TEST(test_bind_wildcard_and_specific_same_port_collide) } END_TEST +/* A TCP socket bound to INADDR_ANY:0 keeps a wildcard claim: its + * ephemeral allocation must not take a port that is bound on a specific + * interface, even when the egress address resolved at connect time + * differs from that interface's. */ +START_TEST(test_tcp_connect_wildcard_zero_avoids_specific_bound_port) +{ + struct wolfIP s; + struct wolfIP_sockaddr_in sin; + struct tsocket *tsb; + int fd_a; + int fd_b; + + setup_stack_with_two_ifaces(&s, 0x0a000001U, 0x0a000101U); + + fd_a = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_STREAM, WI_IPPROTO_TCP); + fd_b = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_STREAM, WI_IPPROTO_TCP); + ck_assert_int_ge(fd_a, 0); + ck_assert_int_ge(fd_b, 0); + + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = ee16(8080); + sin.sin_addr.s_addr = ee32(0x0a000101U); + ck_assert_int_eq(wolfIP_sock_bind(&s, fd_a, (struct wolfIP_sockaddr *)&sin, + sizeof(sin)), 0); + + /* Wildcard :0: no port yet, the claim is the whole port space. */ + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = 0; + sin.sin_addr.s_addr = ee32(IPADDR_ANY); + ck_assert_int_eq(wolfIP_sock_bind(&s, fd_b, (struct wolfIP_sockaddr *)&sin, + sizeof(sin)), 0); + tsb = &s.tcpsockets[SOCKET_UNMARK(fd_b)]; + + /* Force the allocator's first candidate to 8080. */ + test_rand_override_enabled = 1; + test_rand_override_value = 8080; + + /* Connect through the primary interface: the egress address is + * 10.0.0.1, but the wildcard claim still covers 192.168.1.1:8080. */ + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = ee16(9); + sin.sin_addr.s_addr = ee32(0x0a000002U); + ck_assert_int_eq(wolfIP_sock_connect(&s, fd_b, (struct wolfIP_sockaddr *)&sin, + sizeof(sin)), -WOLFIP_EAGAIN); + test_rand_override_enabled = 0; + + ck_assert_uint_ne(tsb->src_port, 8080); + ck_assert_uint_eq(tsb->src_port, 8081); +} +END_TEST + /* ===================================================================== * wolfIP_sock_bind -- a rejected TCP bind leaves if_idx unchanged * ===================================================================== */ diff --git a/src/wolfip.c b/src/wolfip.c index ea52a8e6..ffb74d9a 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -7543,8 +7543,12 @@ int wolfIP_sock_connect(struct wolfIP *s, int sockfd, const struct wolfIP_sockad ts->if_idx = new_if_idx; ts->local_ip = new_local_ip; if (!ts->src_port) { + /* Check the collision against the bind claim, not the egress + * address resolved above: a wildcard-bound socket owns the + * port on every address. */ + ip4 claim = ts->bound ? ts->bound_local_ip : ts->local_ip; ts->src_port = port_alloc_random(s->tcpsockets, MAX_TCPSOCKETS, - ts, ts->local_ip, 1024); + ts, claim, 1024); if (ts->src_port == 0) { ts->sock.tcp.state = TCP_CLOSED; return -WOLFIP_EAGAIN; @@ -10304,12 +10308,14 @@ static int dhcp_msg_type(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_le saw_server_id = 1; } } - /* Reject a reply that does not carry the server identifier of the - * server we committed to during the OFFER phase. While REBINDING the - * request is a broadcast and any server may answer (RFC 2131 4.3.5), - * so the identity check yields there - including for NAKs. */ + /* A reply without a server identifier is not from a server in any + * state (a DHCPNAK in particular must carry one, RFC 2131). While + * REBINDING the request is a broadcast and any server may answer + * (RFC 2131 4.3.5), so only the equality comparison is waived there. */ + if (s->dhcp_server_ip != 0 && !saw_server_id) + return -1; if (s->dhcp_server_ip != 0 && s->dhcp_state != DHCP_REBINDING && - (!saw_server_id || server_id != s->dhcp_server_ip)) + server_id != s->dhcp_server_ip) return -1; return msg_type; } From feeb01b2dd75cacd1a6d97a4aaedf29a16f431fa Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 1 Oct 2026 15:00:35 +0200 Subject: [PATCH 15/15] Fix the endpoint in the wildcard-:0 connect test comment (10.0.1.1, not 192.168.1.1) --- src/test/unit/unit_tests_misc_edges.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/unit/unit_tests_misc_edges.c b/src/test/unit/unit_tests_misc_edges.c index 5e7586f3..41c25e6a 100644 --- a/src/test/unit/unit_tests_misc_edges.c +++ b/src/test/unit/unit_tests_misc_edges.c @@ -1371,7 +1371,7 @@ START_TEST(test_tcp_connect_wildcard_zero_avoids_specific_bound_port) test_rand_override_value = 8080; /* Connect through the primary interface: the egress address is - * 10.0.0.1, but the wildcard claim still covers 192.168.1.1:8080. */ + * 10.0.0.1, but the wildcard claim still covers 10.0.1.1:8080. */ memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_port = ee16(9);