From bf0c0c90049c857c5a0f4ddadf5a5061aa2a8278 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 17 Aug 2026 11:01:23 +0200 Subject: [PATCH 01/21] F-8513: pin datagram IP header to enqueue-time destination wolfIP_sock_sendto() queued UDP/ICMP frames carried only the transport header and payload in the txbuf descriptor; the IP header (dst/src, id, ttl, checksums) was filled from the socket-wide t->remote_ip / t->local_ip at flush time in flush_datagram_tx(). A descriptor left in the queue after a failed flush (LL -EAGAIN, pending ARP, filter break) was therefore re-targeted to whatever destination the next sendto() on the same socket used, so the first datagram went out with the second one's destination IP, route and next-hop MAC. Fill the IP header and transport checksums at enqueue time, while the socket's routing state still matches the datagram being queued. The flush now only adds the link-layer header and derives the egress interface and next hop from the destination stored in the frame. The Ethernet header move is factored out of ip_output_add_header() and re-added at the three TCP send sites, which are unaffected by the bug (one connection, one destination). --- src/test/unit/unit.c | 1 + src/test/unit/unit_shared.c | 26 ++++++++++++ src/test/unit/unit_tests_branches.c | 66 +++++++++++++++++++++++++++++ src/wolfip.c | 59 +++++++++++++++++--------- 4 files changed, 132 insertions(+), 20 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index c347ab1a..719fa11e 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -1057,6 +1057,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_core, test_route_for_ip_gateway_fallback); tcase_add_test(tc_core, test_route_for_ip_first_non_loop_fallback); tcase_add_test(tc_core, test_sendto_udp_txbuf_full_eagain); + tcase_add_test(tc_core, test_udp_sendto_queued_datagram_keeps_enqueue_dst); tcase_add_test(tc_core, test_sendto_icmp_no_remote_after_addr_zero); tcase_add_test(tc_core, test_ip_recv_with_ip_options_strips_and_dispatches); tcase_add_test(tc_core, test_ip_recv_wrong_version_dropped); diff --git a/src/test/unit/unit_shared.c b/src/test/unit/unit_shared.c index a19421c0..3d822e84 100644 --- a/src/test/unit/unit_shared.c +++ b/src/test/unit/unit_shared.c @@ -81,15 +81,41 @@ static uint8_t last_frame_sent[LINK_MTU]; static uint32_t last_frame_sent_size = 0; static uint32_t last_frame_sent_count = 0; +/* When armed, the next send() returns -WOLFIP_EAGAIN without transmitting, + * leaving any in-flight descriptor queued in the socket txbuf. */ +static int mock_send_eagain_armed = 0; + +/* Ring of the most recent frames actually transmitted (up to 4). */ +#define MOCK_SENT_FRAMES_MAX 4 +static uint8_t mock_sent_frames[MOCK_SENT_FRAMES_MAX][LINK_MTU]; +static uint32_t mock_sent_frames_size[MOCK_SENT_FRAMES_MAX]; +static uint32_t mock_sent_frames_count = 0; + static int mock_send(struct wolfIP_ll_dev *dev, void *frame, uint32_t len) { (void)dev; + if (mock_send_eagain_armed) { + mock_send_eagain_armed = 0; + return -WOLFIP_EAGAIN; + } memcpy(last_frame_sent, frame, len); last_frame_sent_size = len; last_frame_sent_count++; + if (mock_sent_frames_count < MOCK_SENT_FRAMES_MAX) { + memcpy(mock_sent_frames[mock_sent_frames_count], frame, len); + mock_sent_frames_size[mock_sent_frames_count] = len; + mock_sent_frames_count++; + } return 0; } +void mock_link_capture_reset(void) +{ + last_frame_sent_size = 0; + last_frame_sent_count = 0; + mock_sent_frames_count = 0; +} + static int mock_poll(struct wolfIP_ll_dev *dev, void *frame, uint32_t len) { (void)dev; diff --git a/src/test/unit/unit_tests_branches.c b/src/test/unit/unit_tests_branches.c index 897b628d..89530fc4 100644 --- a/src/test/unit/unit_tests_branches.c +++ b/src/test/unit/unit_tests_branches.c @@ -1778,6 +1778,72 @@ START_TEST(test_sendto_udp_txbuf_full_eagain) } END_TEST +/* Queued datagram keeps its enqueue-time destination: a descriptor left in + * the txbuf after a failed flush must not be re-targeted to the destination + * of a later sendto() on the same socket. */ +START_TEST(test_udp_sendto_queued_datagram_keeps_enqueue_dst) +{ + struct wolfIP s; + int udp_sd; + struct wolfIP_sockaddr_in sin; + static const uint8_t mac_a[6] = {0x02, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE}; + static const uint8_t mac_b[6] = {0x03, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE}; + uint8_t payload_a[8]; + uint8_t payload_b[8]; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + udp_sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + ck_assert_int_gt(udp_sd, 0); + + /* Resolve both destinations up front so the flush does not stall on ARP. */ + arp_store_neighbor(&s, TEST_PRIMARY_IF, 0x0A000002U, mac_a); + arp_store_neighbor(&s, TEST_PRIMARY_IF, 0x0A000003U, mac_b); + + memset(payload_a, 0xA5, sizeof(payload_a)); + memset(payload_b, 0x5A, sizeof(payload_b)); + mock_link_capture_reset(); + mock_send_eagain_armed = 1; + + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = ee16(9000); + sin.sin_addr.s_addr = ee32(0x0A000002U); + ck_assert_int_eq(wolfIP_sock_sendto(&s, udp_sd, payload_a, sizeof(payload_a), 0, + (struct wolfIP_sockaddr *)&sin, sizeof(sin)), (int)sizeof(payload_a)); + + /* The flush fails with -EAGAIN; the first descriptor stays queued. */ + wolfIP_poll(&s, 0); + ck_assert_uint_eq(mock_sent_frames_count, 0U); + + /* Second datagram on the same socket, different destination. */ + sin.sin_port = ee16(9001); + sin.sin_addr.s_addr = ee32(0x0A000003U); + ck_assert_int_eq(wolfIP_sock_sendto(&s, udp_sd, payload_b, sizeof(payload_b), 0, + (struct wolfIP_sockaddr *)&sin, sizeof(sin)), (int)sizeof(payload_b)); + + wolfIP_poll(&s, 0); + ck_assert_uint_eq(mock_sent_frames_count, 2U); + + { + const struct wolfIP_ip_packet *ip0 = + (const struct wolfIP_ip_packet *)mock_sent_frames[0]; + const struct wolfIP_udp_datagram *udp0 = + (const struct wolfIP_udp_datagram *)mock_sent_frames[0]; + const struct wolfIP_ip_packet *ip1 = + (const struct wolfIP_ip_packet *)mock_sent_frames[1]; + /* First transmitted frame is the first-enqueued datagram, carrying + * the destination and payload it had when it was queued. */ + ck_assert_uint_eq(ip0->dst, ee32(0x0A000002U)); + ck_assert_uint_eq(udp0->dst_port, ee16(9000)); + ck_assert_mem_eq(udp0->data, payload_a, sizeof(payload_a)); + /* Second transmitted frame is the second datagram. */ + ck_assert_uint_eq(ip1->dst, ee32(0x0A000003U)); + } +} +END_TEST + /* ---- wolfIP_sock_sendto: bound_local_ip mismatch on ICMP ---- */ START_TEST(test_sendto_icmp_no_remote_after_addr_zero) diff --git a/src/wolfip.c b/src/wolfip.c index e45bb504..3416c94c 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -3395,6 +3395,10 @@ static int tcp_send_empty_immediate(struct tsocket *t, struct wolfIP_tcp_seg *tc tcp->win = ee16(tcp_adv_win(t, 1)); ip_output_add_header(t, (struct wolfIP_ip_packet *)tcp, WI_IPPROTO_TCP, (uint16_t)(frame_len - ETH_HEADER_LEN)); +#ifdef ETHERNET + if (!wolfIP_ll_is_non_ethernet(t->S, tx_if)) + eth_output_add_header(t->S, tx_if, t->nexthop_mac, &tcp->ip.eth, ETH_TYPE_IP); +#endif if (wolfIP_filter_notify_tcp(WOLFIP_FILT_SENDING, t->S, tx_if, tcp, frame_len) != 0) return -1; @@ -3914,6 +3918,10 @@ static int tcp_send_zero_wnd_probe(struct tsocket *t) #endif ip_output_add_header(t, (struct wolfIP_ip_packet *)probe, WI_IPPROTO_TCP, (uint16_t)(IP_HEADER_LEN + TCP_HEADER_LEN + opt_len + 1)); +#ifdef ETHERNET + if (!wolfIP_ll_is_non_ethernet(t->S, tx_if)) + eth_output_add_header(t->S, tx_if, t->nexthop_mac, &probe->ip.eth, ETH_TYPE_IP); +#endif if (wolfIP_filter_notify_tcp(WOLFIP_FILT_SENDING, t->S, tx_if, probe, frame_len) != 0) return -1; @@ -4431,7 +4439,6 @@ static int ip_output_add_header(struct tsocket *t, struct wolfIP_ip_packet *ip, uint8_t proto, uint16_t len) { union transport_pseudo_header ph; - unsigned int if_idx; memset(&ph, 0, sizeof(ph)); memset(ip, 0, sizeof(struct wolfIP_ip_packet)); ip->src = ee32(t->local_ip); @@ -4473,15 +4480,10 @@ static int ip_output_add_header(struct tsocket *t, struct wolfIP_ip_packet *ip, icmp->csum = 0; icmp->csum = ee16(icmp_checksum(icmp, ee16(ph.ph.len))); } -#ifdef ETHERNET - if_idx = wolfIP_socket_if_idx(t); - if (!wolfIP_ll_is_non_ethernet(t->S, if_idx)) { - eth_output_add_header(t->S, if_idx, t->nexthop_mac, (struct wolfIP_eth_frame *)ip, - ETH_TYPE_IP); - } -#else - (void)if_idx; -#endif + /* The link-layer header is added by the caller once the egress interface + * and next-hop MAC are known. Datagram callers fill this in at enqueue + * time so a queued frame keeps the destination it was sent to; TCP fills + * it in per segment. */ return 0; } @@ -6311,6 +6313,11 @@ int wolfIP_sock_sendto(struct wolfIP *s, int sockfd, const void *buf, size_t len udp->len = ee16(len + UDP_HEADER_LEN); udp->csum = 0; memcpy(udp->data, buf, len); + /* Pin the IP header to this datagram's destination/source while the + * socket's routing state still matches it; the flush only adds the + * link-layer header. */ + ip_output_add_header(ts, &udp->ip, WI_IPPROTO_UDP, + (uint16_t)(frame_len - ETH_HEADER_LEN)); if (fifo_push(&ts->sock.udp.txbuf, udp, frame_len) < 0) return -WOLFIP_EAGAIN; return len; @@ -6371,8 +6378,11 @@ int wolfIP_sock_sendto(struct wolfIP *s, int sockfd, const void *buf, size_t len memcpy(frame + sizeof(struct wolfIP_ip_packet), buf, payload_len); if (icmp->type == ICMP_ECHO_REQUEST) icmp_set_echo_id(icmp, ts->src_port); - icmp->csum = 0; - icmp->csum = ee16(icmp_checksum(icmp, (uint16_t)payload_len)); + /* Pin the IP header and ICMP checksum to this datagram's + * destination/source at enqueue time; the flush only adds the + * link-layer header. */ + ip_output_add_header(ts, &icmp->ip, WI_IPPROTO_ICMP, + (uint16_t)(frame_len - ETH_HEADER_LEN)); if (fifo_push(&ts->sock.udp.txbuf, icmp, frame_len) < 0) return -WOLFIP_EAGAIN; return (int)payload_len; @@ -10360,6 +10370,10 @@ static void flush_tcp_tx(struct wolfIP *s, uint64_t now) tcp->ack = ee32(ts->sock.tcp.ack); tcp->win = ee16(tcp_adv_win(ts, 1)); ip_output_add_header(ts, (struct wolfIP_ip_packet *)tcp, WI_IPPROTO_TCP, size); +#ifdef ETHERNET + if (!wolfIP_ll_is_non_ethernet(ts->S, tx_if)) + eth_output_add_header(ts->S, tx_if, ts->nexthop_mac, &tcp->ip.eth, ETH_TYPE_IP); +#endif if (wolfIP_filter_notify_tcp(WOLFIP_FILT_SENDING, ts->S, tx_if, tcp, desc->len) != 0) { break; } @@ -10453,22 +10467,25 @@ static void flush_datagram_tx(struct wolfIP *s, struct tsocket *socks, struct tsocket *t = &socks[i]; struct pkt_desc *desc = fifo_peek(&t->sock.udp.txbuf); int tx_drained = 0; - int len; while (desc) { struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)(t->txmem + desc->pos + sizeof(*desc)); unsigned int tx_if = wolfIP_socket_if_idx(t); int send_ret = 0; + /* The IP header was filled at enqueue time and carries this + * descriptor's destination; route and resolve for that address, + * not the socket's current one. */ + ip4 desc_dst = ee32(ip->dst); #ifdef ETHERNET - ip4 nexthop = wolfIP_select_nexthop_ex(s, &tx_if, t->remote_ip); + ip4 nexthop = wolfIP_select_nexthop_ex(s, &tx_if, desc_dst); if (wolfIP_is_loopback_if(tx_if)) { struct wolfIP_ll_dev *loop = wolfIP_ll_at(s, tx_if); if (loop) memcpy(t->nexthop_mac, loop->mac, 6); } else if (!wolfIP_ll_is_non_ethernet(s, tx_if)) { #ifdef IP_MULTICAST - if (is_udp && wolfIP_ip_is_multicast(t->remote_ip)) { - mcast_ip_to_eth(t->remote_ip, t->nexthop_mac); + if (is_udp && wolfIP_ip_is_multicast(desc_dst)) { + mcast_ip_to_eth(desc_dst, t->nexthop_mac); } else #endif if (!wolfIP_ip_is_broadcast(s, nexthop) && @@ -10481,8 +10498,10 @@ static void flush_datagram_tx(struct wolfIP *s, struct tsocket *socks, memset(t->nexthop_mac, 0xFF, 6); } #endif - len = desc->len - ETH_HEADER_LEN; - ip_output_add_header(t, ip, proto, len); +#ifdef ETHERNET + if (!wolfIP_ll_is_non_ethernet(s, tx_if)) + eth_output_add_header(s, tx_if, t->nexthop_mac, &ip->eth, ETH_TYPE_IP); +#endif if (is_udp) { if (wolfIP_filter_notify_udp(WOLFIP_FILT_SENDING, s, tx_if, @@ -10508,7 +10527,7 @@ static void flush_datagram_tx(struct wolfIP *s, struct tsocket *socks, /* IPsec not configured on this interface. * Send plaintext instead. * */ - if (esp_send(ll, ip, len) == 1) + if (esp_send(ll, ip, (uint16_t)(desc->len - ETH_HEADER_LEN)) == 1) send_ret = wolfIP_ll_send_frame(s, tx_if, ip, desc->len); } else { send_ret = wolfIP_ll_send_frame(s, tx_if, ip, desc->len); @@ -10524,7 +10543,7 @@ static void flush_datagram_tx(struct wolfIP *s, struct tsocket *socks, * when a SENDING filter blocked the frame or the driver returned * -EAGAIN: the descriptor stays in the txbuf and every subsequent * wolfIP_poll() re-enters the loop and re-loops the datagram. */ - if (is_udp && wolfIP_ip_is_multicast(t->remote_ip) && t->sock.udp.mcast_loop) + if (is_udp && wolfIP_ip_is_multicast(desc_dst) && t->sock.udp.mcast_loop) udp_try_recv(s, tx_if, (struct wolfIP_udp_datagram *)ip, desc->len); #endif fifo_pop(&t->sock.udp.txbuf); From ffa9c1cf8264d4f6375fbfb66e2a333279097393 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 17 Aug 2026 11:14:14 +0200 Subject: [PATCH 02/21] F-8514: keep accept socket seq at the ISN while in SYN_RCVD wolfIP_sock_accept() sent the SYN-ACK from the cloned socket with seq = ISN, then incremented the socket's seq. The control RTO retransmit rebuilds the SYN-ACK from that field, so the retransmit carried ISN+1. A peer processing it would ack ISN+2, which the SYN_RCVD final-ack check (expected ack = snd_una+1 = ISN+1) rejects with a RST, turning a lost SYN-ACK or final ACK into a dropped connection instead of a recovered handshake. Drop the increment: while in SYN_RCVD no data is sent, seq stays at the ISN, and the final-ack handler advances it to ISN+1 on establishment. The passive-open and active-open paths never had the increment. Three existing tests built the client's final ACK from the accepted socket's seq field, which only equalled snd.nxt (ISN+1) because of the increment; they now use tcp_seq_inc(snd_una, 1) explicitly, which is the same wire value. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_api.c | 7 ++- src/test/unit/unit_tests_dns_dhcp.c | 9 ++-- src/test/unit/unit_tests_tcp_state.c | 77 ++++++++++++++++++++++++++++ src/wolfip.c | 5 +- 5 files changed, 93 insertions(+), 6 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 719fa11e..baa78fe5 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -1235,6 +1235,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_core, test_tcp_input_syn_rcvd_ack_with_fin_enters_close_wait); tcase_add_test(tc_core, test_tcp_input_window_grows_from_zero_stops_persist); tcase_add_test(tc_core, test_tcp_rto_cb_fin_wait_2_timeout_closes_socket); + tcase_add_test(tc_core, test_accept_synack_retransmit_repeats_isn); tcase_add_test(tc_core, test_tcp_rto_cb_fin_wait_2_wrong_state_stops_timer); tcase_add_test(tc_core, test_tcp_rto_cb_ctrl_not_needed_stops); tcase_add_test(tc_core, test_tcp_rto_cb_ctrl_maxretries_nonlistener_closes); diff --git a/src/test/unit/unit_tests_api.c b/src/test/unit/unit_tests_api.c index 8d621ec7..0c1edc31 100644 --- a/src/test/unit/unit_tests_api.c +++ b/src/test/unit/unit_tests_api.c @@ -2425,7 +2425,9 @@ START_TEST(test_sock_accept_initializes_snd_una) ck_assert_int_gt(client_sd, 0); accepted = &s.tcpsockets[SOCKET_UNMARK(client_sd)]; - ck_assert_uint_eq(accepted->sock.tcp.seq, (uint32_t)(0x80000000U + 1U)); + /* While in SYN_RCVD the socket's seq stays at the ISN; the final ACK + * handler advances it to ISN+1 on establishment. */ + ck_assert_uint_eq(accepted->sock.tcp.seq, 0x80000000U); ck_assert_uint_eq(accepted->sock.tcp.snd_una, 0x80000000U); ck_assert_int_eq(tcp_seq_leq(accepted->sock.tcp.snd_una, accepted->sock.tcp.seq), 1); } @@ -2739,7 +2741,8 @@ START_TEST(test_sock_accept_ack_transitions_to_established) ack.src_port = ee16(40000); /* Remote port from inject_tcp_syn */ ack.dst_port = ee16(1234); ack.seq = ee32(accepted->sock.tcp.ack); - ack.ack = ee32(accepted->sock.tcp.seq); + /* The client's final ACK is at the server's snd.nxt: ISN+1. */ + ack.ack = ee32(tcp_seq_inc(accepted->sock.tcp.snd_una, 1)); ack.hlen = TCP_HEADER_LEN << 2; ack.flags = TCP_FLAG_ACK; ack.win = ee16(65535); diff --git a/src/test/unit/unit_tests_dns_dhcp.c b/src/test/unit/unit_tests_dns_dhcp.c index 1f93cce5..37112b52 100644 --- a/src/test/unit/unit_tests_dns_dhcp.c +++ b/src/test/unit/unit_tests_dns_dhcp.c @@ -3190,7 +3190,8 @@ START_TEST(test_sock_accept_ack_with_payload_completes_handshake) seg->dst_port = ee16(local_port); base_seq = new_ts->sock.tcp.ack; seg->seq = ee32(base_seq); - seg->ack = ee32(new_ts->sock.tcp.seq); + /* The client's ACK is at the server's snd.nxt: ISN+1. */ + seg->ack = ee32(tcp_seq_inc(new_ts->sock.tcp.snd_una, 1)); seg->hlen = TCP_HEADER_LEN << 2; seg->flags = TCP_FLAG_ACK; memcpy(seg->data, payload, sizeof(payload)); @@ -3250,7 +3251,8 @@ START_TEST(test_sock_accept_ack_at_snd_nxt_completes_handshake) ackseg.src_port = ee16(remote_port); ackseg.dst_port = ee16(local_port); ackseg.seq = ee32(new_ts->sock.tcp.ack); - ackseg.ack = ee32(new_ts->sock.tcp.seq); + /* The client's final ACK is at the server's snd.nxt: ISN+1. */ + ackseg.ack = ee32(tcp_seq_inc(new_ts->sock.tcp.snd_una, 1)); ackseg.hlen = TCP_HEADER_LEN << 2; ackseg.flags = TCP_FLAG_ACK; fix_tcp_checksums(&ackseg); @@ -3311,7 +3313,8 @@ START_TEST(test_sock_accept_ack_psh_with_payload_completes_handshake) seg->dst_port = ee16(local_port); base_seq = new_ts->sock.tcp.ack; seg->seq = ee32(base_seq); - seg->ack = ee32(new_ts->sock.tcp.seq); + /* The client's ACK is at the server's snd.nxt: ISN+1. */ + seg->ack = ee32(tcp_seq_inc(new_ts->sock.tcp.snd_una, 1)); seg->hlen = TCP_HEADER_LEN << 2; seg->flags = (TCP_FLAG_ACK | TCP_FLAG_PSH); memcpy(seg->data, payload, sizeof(payload)); diff --git a/src/test/unit/unit_tests_tcp_state.c b/src/test/unit/unit_tests_tcp_state.c index 7e05663e..0b06d430 100644 --- a/src/test/unit/unit_tests_tcp_state.c +++ b/src/test/unit/unit_tests_tcp_state.c @@ -2525,3 +2525,80 @@ START_TEST(test_last_ack_final_ack_delivers_close_event) ck_assert_uint_ne(socket_cb_last_events & CB_EVENT_CLOSED, 0); } END_TEST + +/* A SYN-ACK retransmitted by the control RTO on an accepted socket must + * repeat the ISN advertised by the original SYN-ACK; the peer's final ACK + * is validated against ISN+1, so a changed sequence number turns loss + * recovery into a RST. */ +START_TEST(test_accept_synack_retransmit_repeats_isn) +{ + struct wolfIP s; + int listen_sd; + struct tsocket *syn_rcvd = NULL; + struct tsocket *accepted; + struct wolfIP_sockaddr_in sin; + int syn_rcvd_sd; + int acc_sd; + int i; + uint32_t isn; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + listen_sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_STREAM, WI_IPPROTO_TCP); + ck_assert_int_gt(listen_sd, 0); + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = ee16(1234); + sin.sin_addr.s_addr = ee32(0x0A000001U); + ck_assert_int_eq(wolfIP_sock_bind(&s, listen_sd, + (struct wolfIP_sockaddr *)&sin, sizeof(sin)), 0); + ck_assert_int_eq(wolfIP_sock_listen(&s, listen_sd, 1), 0); + + /* Peer SYN triggers passive open; the SYN-ACK carries the clone's ISN. */ + inject_tcp_syn(&s, TEST_PRIMARY_IF, 0x0A000001U, 1234); + wolfIP_poll(&s, 1000); + for (i = 0; i < MAX_TCPSOCKETS; i++) { + if (s.tcpsockets[i].sock.tcp.state == TCP_SYN_RCVD) { + syn_rcvd = &s.tcpsockets[i]; + break; + } + } + ck_assert_ptr_nonnull(syn_rcvd); + isn = syn_rcvd->sock.tcp.seq; + ck_assert_uint_gt(last_frame_sent_size, 0U); + { + const struct wolfIP_tcp_seg *synack = + (const struct wolfIP_tcp_seg *)last_frame_sent; + ck_assert_uint_eq(ee32(synack->seq), isn); + } + + /* accept() sends a duplicate SYN-ACK from the new socket, same ISN. */ + syn_rcvd_sd = (int)(syn_rcvd - s.tcpsockets) | MARK_TCP_SOCKET; + acc_sd = wolfIP_sock_accept(&s, syn_rcvd_sd, NULL, 0); + ck_assert_int_gt(acc_sd, 0); + accepted = &s.tcpsockets[SOCKET_UNMARK(acc_sd)]; + ck_assert_int_eq(accepted->sock.tcp.state, TCP_SYN_RCVD); + /* Poll before the accept-armed control RTO (last_tick 1000 + 1000ms + * default) expires, so this flushes accept's own SYN-ACK. */ + wolfIP_poll(&s, 1500); + ck_assert_uint_gt(last_frame_sent_size, 0U); + { + const struct wolfIP_tcp_seg *synack2 = + (const struct wolfIP_tcp_seg *)last_frame_sent; + ck_assert_uint_eq(ee32(synack2->seq), isn); + } + + /* Control RTO retransmit: same flags, same ISN. */ + tcp_rto_cb(accepted); + wolfIP_poll(&s, 3000); + ck_assert_uint_gt(last_frame_sent_size, 0U); + { + const struct wolfIP_tcp_seg *retrans = + (const struct wolfIP_tcp_seg *)last_frame_sent; + ck_assert_int_eq(retrans->flags & (TCP_FLAG_SYN | TCP_FLAG_ACK), + TCP_FLAG_SYN | TCP_FLAG_ACK); + ck_assert_uint_eq(ee32(retrans->seq), isn); + } +} +END_TEST diff --git a/src/wolfip.c b/src/wolfip.c index 3416c94c..2d9905c8 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -6124,7 +6124,10 @@ int wolfIP_sock_accept(struct wolfIP *s, int sockfd, struct wolfIP_sockaddr *add return -WOLFIP_EAGAIN; } ts->events &= ~CB_EVENT_READABLE; - newts->sock.tcp.seq++; + /* Keep seq at the ISN while in SYN_RCVD: control RTO + * retransmits rebuild the SYN-ACK from seq, and a retransmitted + * SYN-ACK must repeat the original ISN. The final ACK handler + * advances seq to ISN+1 when the connection is established. */ newts->sock.tcp.ctrl_rto_retries = 0; tcp_ctrl_rto_start(newts, s->last_tick); if (sin) { From 64879f1d3dc4e5021b49149bb931c79fd760b1a1 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 17 Aug 2026 11:16:54 +0200 Subject: [PATCH 03/21] F-8557: reject TCP connect to broadcast and multicast destinations An active TCP OPEN to a broadcast address gets SYN-ACK candidates from every host on the segment, and a multicast group has no single peer, so neither can complete a connection. wolfIP_sock_connect() mutated the socket to TCP_SYN_SENT, assigned an ISN and queued the SYN for such destinations anyway, while the inbound SYN path already drops SYNs from broadcast and multicast sources. Validate the destination with the existing predicates before mutating the socket and return -WOLFIP_EINVAL, leaving the socket reusable. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_tcp_flow.c | 45 +++++++++++++++++++++++++++++ src/wolfip.c | 8 +++++ 3 files changed, 54 insertions(+) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index baa78fe5..b637ba2d 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -638,6 +638,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_tcp_input_peer_rwnd_growth_sets_writable); tcase_add_test(tc_utils, test_tcp_input_synack_negotiates_peer_mss); tcase_add_test(tc_utils, test_tcp_connect_syn_advertises_interface_mss); + tcase_add_test(tc_utils, test_tcp_connect_rejects_broadcast_multicast_dest); tcase_add_test(tc_utils, test_tcp_connect_syn_limits_options_to_small_mtu); tcase_add_test(tc_utils, test_sock_sendto_tcp_respects_negotiated_peer_mss); tcase_add_test(tc_utils, test_sock_sendto_tcp_defaults_to_rfc_mss_when_unset_by_peer); diff --git a/src/test/unit/unit_tests_tcp_flow.c b/src/test/unit/unit_tests_tcp_flow.c index 8fecb8ed..b8929bd2 100644 --- a/src/test/unit/unit_tests_tcp_flow.c +++ b/src/test/unit/unit_tests_tcp_flow.c @@ -2673,6 +2673,51 @@ START_TEST(test_tcp_connect_syn_advertises_interface_mss) } END_TEST +/* A TCP active OPEN has no defined destination semantics for broadcast or + * multicast addresses (multiple hosts would answer one SYN; there is no + * single peer for a group). connect() must reject them before mutating the + * socket, the same way the inbound SYN path rejects such sources. */ +START_TEST(test_tcp_connect_rejects_broadcast_multicast_dest) +{ + struct wolfIP s; + int tcp_sd; + struct tsocket *ts; + struct wolfIP_sockaddr_in sin; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + + tcp_sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_STREAM, WI_IPPROTO_TCP); + ck_assert_int_gt(tcp_sd, 0); + ts = &s.tcpsockets[SOCKET_UNMARK(tcp_sd)]; + + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = ee16(5004); + + /* Limited broadcast. */ + sin.sin_addr.s_addr = ee32(0xFFFFFFFFU); + ck_assert_int_eq(wolfIP_sock_connect(&s, tcp_sd, + (struct wolfIP_sockaddr *)&sin, sizeof(sin)), -WOLFIP_EINVAL); + ck_assert_int_eq(ts->sock.tcp.state, TCP_CLOSED); + ck_assert_uint_eq(ts->remote_ip, 0U); + + /* All-hosts multicast group. */ + sin.sin_addr.s_addr = ee32(0xE0000001U); + ck_assert_int_eq(wolfIP_sock_connect(&s, tcp_sd, + (struct wolfIP_sockaddr *)&sin, sizeof(sin)), -WOLFIP_EINVAL); + ck_assert_int_eq(ts->sock.tcp.state, TCP_CLOSED); + ck_assert_uint_eq(ts->remote_ip, 0U); + + /* The socket is reusable: a unicast destination still proceeds. */ + sin.sin_addr.s_addr = ee32(0x0A000002U); + ck_assert_int_eq(wolfIP_sock_connect(&s, tcp_sd, + (struct wolfIP_sockaddr *)&sin, sizeof(sin)), -WOLFIP_EAGAIN); + ck_assert_int_eq(ts->sock.tcp.state, TCP_SYN_SENT); +} +END_TEST + START_TEST(test_tcp_connect_syn_limits_options_to_small_mtu) { struct wolfIP s; diff --git a/src/wolfip.c b/src/wolfip.c index 2d9905c8..5b1815fa 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -6003,6 +6003,14 @@ int wolfIP_sock_connect(struct wolfIP *s, int sockfd, const struct wolfIP_sockad uint8_t new_if_idx; ip4 new_local_ip; + /* A TCP connection is a single peer: broadcast destinations get + * answers from every host on the segment and multicast groups have + * no single peer, so an active OPEN to either is invalid. Reject + * before mutating the socket, mirroring the inbound SYN filter. */ + if (wolfIP_ip_is_broadcast(s, new_remote_ip) || + wolfIP_ip_is_multicast(new_remote_ip)) + return -WOLFIP_EINVAL; + /* Resolve and validate the local binding into locals before mutating * the socket. A failed validation here must not leave the socket in * TCP_SYN_SENT (no SYN queued, no RTO timer), which would make every From a528fdf645d85b3660cdfa7bc0c409ab1682b7d9 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 17 Aug 2026 11:22:31 +0200 Subject: [PATCH 04/21] F-8563: keep peer-advertised TCP MSS verbatim An explicitly advertised, nonzero MSS is the peer's commitment about what it will receive; the effective send MSS must not exceed it (RFC 9293 section 3.7.1). tcp_parse_options() raised any value below the 536 IPv4 default up to 536, which (a) violated the constraint and (b) blackholed legitimate low-MTU peers: this stack sends TCP with DF set, and the ICMP PTM path ignores next-hop MTUs below 576, so segments sized at the floored 536 are dropped forever on a path that genuinely carries less. The 536 default still applies when no MSS option is present. The ICMP PTM path keeps its separate rejection of sub-576 MTUs: that input is unauthenticated (spoofable), unlike the MSS option of a peer completing the handshake. One existing test asserted the clamping as intended behavior; it now asserts the advertised value is recorded as-is. --- src/test/unit/unit.c | 3 ++- src/test/unit/unit_tests_tcp_flow.c | 25 +++++++++++++++++++++++++ src/test/unit/unit_tests_tcp_state.c | 14 +++++++------- src/wolfip.c | 12 +++++------- 4 files changed, 39 insertions(+), 15 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index b637ba2d..64cd1b96 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -639,6 +639,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_tcp_input_synack_negotiates_peer_mss); tcase_add_test(tc_utils, test_tcp_connect_syn_advertises_interface_mss); tcase_add_test(tc_utils, test_tcp_connect_rejects_broadcast_multicast_dest); + tcase_add_test(tc_utils, test_tcp_parse_options_keeps_sub_default_advertised_mss); tcase_add_test(tc_utils, test_tcp_connect_syn_limits_options_to_small_mtu); tcase_add_test(tc_utils, test_sock_sendto_tcp_respects_negotiated_peer_mss); tcase_add_test(tc_utils, test_sock_sendto_tcp_defaults_to_rfc_mss_when_unset_by_peer); @@ -1220,7 +1221,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_core, test_tcp_parse_options_timestamp_parsed); tcase_add_test(tc_core, test_tcp_parse_options_timestamp_overlong_ignored); tcase_add_test(tc_core, test_tcp_parse_options_mss_zero_ignored); - tcase_add_test(tc_core, test_tcp_parse_options_mss_below_floor_clamped); + tcase_add_test(tc_core, test_tcp_parse_options_mss_below_default_kept_verbatim); tcase_add_test(tc_core, test_tcp_parse_options_sack_permitted_parsed); tcase_add_test(tc_core, test_tcp_input_syn_rcvd_rst_bad_seq_ignored); tcase_add_test(tc_core, test_tcp_input_syn_rcvd_rst_good_seq_reverts_to_listen); diff --git a/src/test/unit/unit_tests_tcp_flow.c b/src/test/unit/unit_tests_tcp_flow.c index b8929bd2..fec70e15 100644 --- a/src/test/unit/unit_tests_tcp_flow.c +++ b/src/test/unit/unit_tests_tcp_flow.c @@ -1005,6 +1005,31 @@ START_TEST(test_tcp_parse_options_parses_mss_sack_permitted_timestamp_and_two_sa } END_TEST +/* An explicitly advertised MSS is the peer's commitment about what it will + * receive; the 536 default applies only when no MSS option is present. */ +START_TEST(test_tcp_parse_options_keeps_sub_default_advertised_mss) +{ + uint8_t seg_buf[sizeof(struct wolfIP_tcp_seg) + 4]; + struct wolfIP_tcp_seg *seg = (struct wolfIP_tcp_seg *)seg_buf; + struct tcp_parsed_opts po; + uint32_t frame_len; + + memset(seg_buf, 0, sizeof(seg_buf)); + seg->hlen = (uint8_t)((TCP_HEADER_LEN + 4) << 2); + seg->data[0] = TCP_OPTION_MSS; + seg->data[1] = TCP_OPTION_MSS_LEN; + seg->data[2] = 0x01; + seg->data[3] = 0x2C; /* 300 */ + frame_len = ETH_HEADER_LEN + IP_HEADER_LEN + TCP_HEADER_LEN + 4; + + memset(&po, 0, sizeof(po)); + tcp_parse_options(seg, frame_len, &po); + + ck_assert_int_eq(po.mss_found, 1); + ck_assert_uint_eq(po.mss, 300U); +} +END_TEST + START_TEST(test_tcp_parse_options_ignores_unknown_option_kinds) { uint8_t seg_buf[sizeof(struct wolfIP_tcp_seg) + 8]; diff --git a/src/test/unit/unit_tests_tcp_state.c b/src/test/unit/unit_tests_tcp_state.c index 0b06d430..0844c195 100644 --- a/src/test/unit/unit_tests_tcp_state.c +++ b/src/test/unit/unit_tests_tcp_state.c @@ -466,13 +466,13 @@ START_TEST(test_tcp_parse_options_mss_zero_ignored) } END_TEST -/* A peer-advertised MSS below the RFC 9293 floor (536) must be clamped up to - * TCP_DEFAULT_MSS, so a malicious tiny MSS cannot coerce us into emitting 1-byte - * segments (small-MSS DoS amplification). Symmetric with the ICMP PTB floor. */ -START_TEST(test_tcp_parse_options_mss_below_floor_clamped) +/* An explicitly advertised MSS is kept verbatim, even below the 536 IPv4 + * default: it is the peer's commitment about what it will receive, and the + * effective send MSS must not exceed it (RFC 9293 §3.7.1). */ +START_TEST(test_tcp_parse_options_mss_below_default_kept_verbatim) { uint8_t opts[] = { - TCP_OPTION_MSS, 4, 0x00, 0x01, /* MSS=1: below the 536 floor */ + TCP_OPTION_MSS, 4, 0x00, 0x01, /* MSS=1: below the 536 default */ TCP_OPTION_EOO }; struct wolfIP s; @@ -494,8 +494,8 @@ START_TEST(test_tcp_parse_options_mss_below_floor_clamped) 1, 0, TCP_FLAG_SYN, opts, (uint8_t)sizeof(opts), NULL, 0); - /* Sub-floor MSS must not drag peer_mss below TCP_DEFAULT_MSS. */ - ck_assert_uint_ge(ts->sock.tcp.peer_mss, TCP_DEFAULT_MSS); + /* The advertised value is recorded as-is. */ + ck_assert_uint_eq(ts->sock.tcp.peer_mss, 1U); } END_TEST diff --git a/src/wolfip.c b/src/wolfip.c index 5b1815fa..8eca6629 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -3052,13 +3052,11 @@ static void tcp_parse_options(const struct wolfIP_tcp_seg *tcp, uint32_t frame_l memcpy(&mss, opt + 2, sizeof(mss)); mss = ee16(mss); if (mss > 0) { - /* RFC 9293 §3.7.1: IPv4 default MSS is 536. Floor the - * advertised value so a peer cannot drag peer_mss below it - * and coerce us into tiny segments (small-MSS DoS - * amplification). Symmetric with the ICMP PTB floor in - * icmp_try_deliver_tcp_error(). */ - if (mss < TCP_DEFAULT_MSS) - mss = TCP_DEFAULT_MSS; + /* Keep the advertised value as-is: it is the peer's + * commitment about what it will receive (RFC 9293 §3.7.1), + * and the effective send MSS must not exceed it. The 536 + * default is applied by the caller only when no MSS option + * is present. */ po->mss = mss; po->mss_found = 1; } From 72cd51850f1ead1340335dff5c4f57636daddb27 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 17 Aug 2026 11:30:32 +0200 Subject: [PATCH 05/21] F-8565: run PAWS before the segment acceptability test RFC 7323 section 5.3 requires PAWS verification to take precedence over the regular TCP acceptability test on timestamp-synchronized connections. In LAST_ACK and in the ESTABLISHED/CLOSE_WAIT/FIN_WAIT_1/ FIN_WAIT_2/CLOSING branch, tcp_input() ran tcp_segment_acceptable() first; a segment failing the sequence test got its challenge ACK before PAWS was ever consulted, so a replayed TSopt-less segment drew an ACK reply instead of the RFC-mandated silent drop. Move the existing PAWS check ahead of the acceptability test in both branches. TIME_WAIT already ran PAWS first; the RST exemption is inside tcp_paws_check and is unchanged. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_proto.c | 77 ++++++++++++++++++++++++++++++++ src/wolfip.c | 26 ++++++----- 3 files changed, 93 insertions(+), 11 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 64cd1b96..533251ca 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -942,6 +942,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_proto, test_regression_paws_rejects_stale_timestamp); tcase_add_test(tc_proto, test_regression_paws_accepts_wrapped_newer_timestamp); tcase_add_test(tc_proto, test_regression_paws_drops_segment_without_timestamp_option); + tcase_add_test(tc_proto, test_regression_paws_preempts_acceptability_for_replayed_segment); tcase_add_test(tc_proto, test_regression_paws_drops_last_ack_segment_without_timestamp_option); tcase_add_test(tc_proto, test_regression_paws_drops_time_wait_segment_without_timestamp_option); tcase_add_test(tc_proto, test_regression_dhcp_nak_restarts_configuration); diff --git a/src/test/unit/unit_tests_proto.c b/src/test/unit/unit_tests_proto.c index 01a279ca..ec786cf0 100644 --- a/src/test/unit/unit_tests_proto.c +++ b/src/test/unit/unit_tests_proto.c @@ -6686,6 +6686,83 @@ START_TEST(test_regression_paws_drops_segment_without_timestamp_option) } END_TEST +/* RFC 7323 §5.3: PAWS verification takes precedence over the regular + * segment acceptability test. A replayed segment that fails the sequence + * test and omits the TSopt on a timestamp-synchronized connection is + * dropped silently, without the ACK an unacceptable segment would + * otherwise elicit. */ +START_TEST(test_regression_paws_preempts_acceptability_for_replayed_segment) +{ + struct wolfIP s; + struct tsocket *ts; + uint8_t buf[sizeof(struct wolfIP_tcp_seg) + 4]; + struct wolfIP_tcp_seg *seg = (struct wolfIP_tcp_seg *)buf; + uint8_t payload[4] = {0xDE, 0xAD, 0xBE, 0xEF}; + uint32_t tcp_hlen = TCP_HEADER_LEN; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + last_frame_sent_size = 0; + last_frame_sent_count = 0; + + s.arp.neighbors[0].ip = 0x0A000002U; + s.arp.neighbors[0].if_idx = TEST_PRIMARY_IF; + memcpy(s.arp.neighbors[0].mac, + (uint8_t[]){0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}, 6); + + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->if_idx = TEST_PRIMARY_IF; + ts->sock.tcp.state = TCP_ESTABLISHED; + ts->sock.tcp.ack = 100; + ts->sock.tcp.seq = 1000; + ts->sock.tcp.snd_una = 1000; + ts->sock.tcp.cwnd = TCP_MSS; + ts->sock.tcp.peer_rwnd = TCP_MSS; + ts->sock.tcp.ts_enabled = 1; + ts->sock.tcp.last_ts = ee32(5000); + ts->src_port = 1234; + ts->dst_port = 4321; + ts->local_ip = 0x0A000001U; + ts->remote_ip = 0x0A000002U; + queue_init(&ts->sock.tcp.rxbuf, ts->rxmem, RXBUF_SIZE, ts->sock.tcp.ack); + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + /* Replayed data behind rcv_nxt (fails the acceptability test) with no + * TSopt (PAWS violation). */ + memset(buf, 0, sizeof(buf)); + seg->ip.ver_ihl = 0x45; + seg->ip.ttl = 64; + seg->ip.proto = WI_IPPROTO_TCP; + seg->ip.len = ee16(IP_HEADER_LEN + tcp_hlen + sizeof(payload)); + seg->ip.src = ee32(ts->remote_ip); + seg->ip.dst = ee32(ts->local_ip); + seg->dst_port = ee16(ts->src_port); + seg->src_port = ee16(ts->dst_port); + seg->hlen = (uint8_t)(tcp_hlen << 2); + seg->flags = TCP_FLAG_ACK; + seg->seq = ee32(50); + seg->ack = ee32(ts->sock.tcp.seq); + seg->win = ee16(65535); + + memcpy(seg->data, payload, sizeof(payload)); + fix_tcp_checksums(seg); + + tcp_input(&s, TEST_PRIMARY_IF, seg, + (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + tcp_hlen + sizeof(payload))); + wolfIP_poll(&s, 1000); + + /* Dropped silently before the acceptability test runs: no reply frame, + * no payload delivery. */ + ck_assert_uint_eq(ts->sock.tcp.ack, 100); + ck_assert_uint_eq(queue_len(&ts->sock.tcp.rxbuf), 0); + ck_assert_uint_eq(last_frame_sent_count, 0); +} +END_TEST + /* RFC 7323 §3.2: LAST_ACK is a synchronized state, so a final ACK that * arrives without a TSopt is silently dropped instead of closing the * connection. */ diff --git a/src/wolfip.c b/src/wolfip.c index 8eca6629..0090d899 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -5321,12 +5321,8 @@ static void tcp_input(struct wolfIP *S, unsigned int if_idx, tcp_send_ack(t); continue; } else if (t->sock.tcp.state == TCP_LAST_ACK) { - /* RFC 9293 s3.10.7.2: segment acceptability applies - * to all synchronized states including LAST_ACK. */ - if (!tcp_segment_acceptable(t, tcp, tcplen)) { - tcp_send_ack(t); - continue; - } + /* RFC 7323 §5.3: PAWS takes precedence over the regular + * acceptability test on synchronized connections. */ { int paws = tcp_paws_check(t, tcp, frame_len); if (paws == TCP_PAWS_ACK_DROP) @@ -5334,6 +5330,12 @@ static void tcp_input(struct wolfIP *S, unsigned int if_idx, if (paws != TCP_PAWS_OK) continue; } + /* RFC 9293 s3.10.7.2: segment acceptability applies + * to all synchronized states including LAST_ACK. */ + if (!tcp_segment_acceptable(t, tcp, tcplen)) { + tcp_send_ack(t); + continue; + } /* RFC 9293 §3.10.7.4: if the SYN bit is set on a * synchronized connection, send a challenge ACK and * drop the segment (RFC 5961). */ @@ -5354,11 +5356,8 @@ static void tcp_input(struct wolfIP *S, unsigned int if_idx, (t->sock.tcp.state == TCP_FIN_WAIT_1) || (t->sock.tcp.state == TCP_FIN_WAIT_2) || (t->sock.tcp.state == TCP_CLOSING)) { - if (!tcp_segment_acceptable(t, tcp, tcplen)) { - tcp_send_ack(t); - continue; - } - + /* RFC 7323 §5.3: PAWS takes precedence over the regular + * acceptability test on synchronized connections. */ { int paws = tcp_paws_check(t, tcp, frame_len); if (paws == TCP_PAWS_ACK_DROP) @@ -5367,6 +5366,11 @@ static void tcp_input(struct wolfIP *S, unsigned int if_idx, continue; } + if (!tcp_segment_acceptable(t, tcp, tcplen)) { + tcp_send_ack(t); + continue; + } + /* RFC 9293 §3.10.7.4: if the SYN bit is set on a * synchronized connection, send a challenge ACK and * drop the segment (RFC 5961). */ From dc521b4a1ac8627d72e2ed39369efe39ab8480d5 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 17 Aug 2026 11:44:45 +0200 Subject: [PATCH 06/21] F-6472: count only pure ACKs with unchanged window as duplicate ACKs RFC 5681 defines a duplicate ACK with five conditions, including that the segment carries no data and that its advertised receive window equals the previously received ACK's window. tcp_ack() counted any ACK-flagged segment that repeated snd_una while data was in flight, so a peer sending three or more data segments without acknowledging new data from us (normal bidirectional transfer) miscounted as three duplicate ACKs and entered fast recovery: ssthresh halved, cwnd inflated, and a retransmit issued with no loss having occurred. Add the two missing conditions to the dup-ACK branch: skip segments whose IP length exceeds the header length, and skip segments whose advertised window differs from the previously processed ACK's. Track the previous raw window in a new last_peer_win field updated for every ACK-flagged segment entering tcp_ack. One existing test primed its window reference explicitly; its three phase-1 ACKs now repeat rather than establish the window. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_proto.c | 3 ++ src/test/unit/unit_tests_tcp_flow.c | 61 +++++++++++++++++++++++++++++ src/wolfip.c | 21 +++++++++- 4 files changed, 85 insertions(+), 1 deletion(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 533251ca..d2f8f1be 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -634,6 +634,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_tcp_ack_duplicate_ssthresh_min); tcase_add_test(tc_utils, test_tcp_ack_progress_resets_rto_recovery_state); tcase_add_test(tc_utils, test_tcp_ack_cwnd_grows_when_payload_acked_is_mss_minus_options); + tcase_add_test(tc_utils, test_tcp_ack_data_segments_not_counted_as_dup_acks); tcase_add_test(tc_utils, test_tcp_ack_inflight_deflate_sets_writable_without_acked_desc); tcase_add_test(tc_utils, test_tcp_input_peer_rwnd_growth_sets_writable); tcase_add_test(tc_utils, test_tcp_input_synack_negotiates_peer_mss); diff --git a/src/test/unit/unit_tests_proto.c b/src/test/unit/unit_tests_proto.c index ec786cf0..f13dbfc4 100644 --- a/src/test/unit/unit_tests_proto.c +++ b/src/test/unit/unit_tests_proto.c @@ -6278,6 +6278,9 @@ START_TEST(test_regression_fast_recovery_cwnd_ssthresh_rfc5681) ts->sock.tcp.ssthresh = 20 * smss; ts->sock.tcp.peer_rwnd = 20 * smss; ts->sock.tcp.bytes_in_flight = flight_size; + /* The peer has advertised this window since the handshake, so the + * phase-1 ACKs repeat it rather than establish it. */ + ts->sock.tcp.last_peer_win = 65535; queue_init(&ts->sock.tcp.rxbuf, ts->rxmem, RXBUF_SIZE, ts->sock.tcp.ack); fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); diff --git a/src/test/unit/unit_tests_tcp_flow.c b/src/test/unit/unit_tests_tcp_flow.c index fec70e15..31320516 100644 --- a/src/test/unit/unit_tests_tcp_flow.c +++ b/src/test/unit/unit_tests_tcp_flow.c @@ -2526,6 +2526,67 @@ START_TEST(test_tcp_ack_cwnd_grows_when_payload_acked_is_mss_minus_options) } END_TEST +/* RFC 5681: a duplicate ACK carries no data and repeats the previously + * advertised receive window. Peer data segments that do not advance our + * snd_una (normal in bidirectional transfer) must not inflate the dup + * count into a spurious fast retransmit. */ +START_TEST(test_tcp_ack_data_segments_not_counted_as_dup_acks) +{ + struct wolfIP s; + struct tsocket *ts; + struct tcp_seg_buf segbuf; + struct wolfIP_tcp_seg *seg; + struct pkt_desc *desc; + uint8_t pbuf[sizeof(struct wolfIP_tcp_seg) + 8]; + struct wolfIP_tcp_seg *pseg = (struct wolfIP_tcp_seg *)pbuf; + uint32_t seq = 1000; + int i; + + wolfIP_init(&s); + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->sock.tcp.state = TCP_ESTABLISHED; + ts->sock.tcp.cwnd = TCP_MSS * 4; + ts->sock.tcp.ssthresh = TCP_MSS * 8; + ts->sock.tcp.snd_una = seq; + ts->sock.tcp.seq = seq + TCP_MSS; + ts->sock.tcp.bytes_in_flight = TCP_MSS; + ts->sock.tcp.peer_rwnd = TCP_MSS * 8; + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + /* One in-flight segment so the dup-ACK branch is reachable. */ + memset(&segbuf, 0, sizeof(segbuf)); + seg = &segbuf.seg; + seg->ip.len = ee16(IP_HEADER_LEN + TCP_HEADER_LEN + TCP_MSS); + seg->hlen = TCP_HEADER_LEN << 2; + seg->seq = ee32(seq); + ck_assert_int_eq(fifo_push(&ts->sock.tcp.txbuf, &segbuf, sizeof(segbuf)), 0); + desc = fifo_peek(&ts->sock.tcp.txbuf); + ck_assert_ptr_nonnull(desc); + desc->flags |= PKT_FLAG_SENT; + + /* Three peer data segments, none acknowledging the in-flight one. */ + memset(pbuf, 0, sizeof(pbuf)); + pseg->ip.len = ee16(IP_HEADER_LEN + TCP_HEADER_LEN + 8); + pseg->hlen = TCP_HEADER_LEN << 2; + pseg->flags = TCP_FLAG_ACK; + pseg->win = ee16(65535); + pseg->ack = ee32(seq); + memset(pseg->data, 0x5A, 8); + for (i = 0; i < 3; i++) { + pseg->seq = ee32(500 + i * 8); + tcp_ack(ts, pseg); + } + + ck_assert_uint_eq(ts->sock.tcp.dup_acks, 0); + ck_assert_int_eq(ts->sock.tcp.fast_recovery, 0); + ck_assert_uint_eq(ts->sock.tcp.snd_una, seq); + ck_assert_uint_eq(ts->sock.tcp.bytes_in_flight, TCP_MSS); +} +END_TEST + START_TEST(test_tcp_ack_inflight_deflate_sets_writable_without_acked_desc) { struct wolfIP s; diff --git a/src/wolfip.c b/src/wolfip.c index 0090d899..36c28a44 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -1141,6 +1141,9 @@ struct tcpsocket { ip4 local_ip, remote_ip; uint32_t peer_rwnd; uint16_t peer_mss; + /* Raw receive window advertised by the previously processed ACK; dup + * ACK detection compares against it (RFC 5681 condition e). */ + uint16_t last_peer_win; uint8_t snd_wscale, rcv_wscale, ws_enabled, ws_offer; uint8_t ts_enabled, ts_offer; uint8_t sack_offer, sack_permitted; @@ -4760,6 +4763,12 @@ static void tcp_ack(struct tsocket *t, const struct wolfIP_tcp_seg *tcp) int recovery_exit_ack = 0; uint32_t inflight_pre = t->sock.tcp.bytes_in_flight; + /* Every caller presents an ACK-flagged segment, so each one is the new + * "previously received ACK" for dup detection. */ + uint16_t adv_win = ee16(tcp->win); + int win_changed = (adv_win != t->sock.tcp.last_peer_win); + t->sock.tcp.last_peer_win = adv_win; + if (t->sock.tcp.state == TCP_LAST_ACK && tcp_seq_leq(fin_acked, ack)) { tcp_ctrl_rto_stop(t); t->sock.tcp.state = TCP_CLOSED; @@ -4926,11 +4935,21 @@ static void tcp_ack(struct tsocket *t, const struct wolfIP_tcp_seg *tcp) t->events |= CB_EVENT_WRITABLE; } } else { - /* Duplicate ack (no advance in snd_una). */ + /* Duplicate ack (no advance in snd_una). RFC 5681: only a segment + * that carries no data and repeats the previously advertised + * receive window counts as a duplicate ACK, so data-bearing + * segments and window updates must not inflate the count or + * trigger fast retransmit. */ + uint32_t ip_len = ee16(tcp->ip.len); + uint32_t hdr_len = IP_HEADER_LEN + tcp_data_offset_bytes(tcp->hlen); if (ack != t->sock.tcp.snd_una) return; if (inflight_pre == 0) return; + if (ip_len > hdr_len) + return; + if (win_changed) + return; if (t->sock.tcp.dup_acks < 255) t->sock.tcp.dup_acks++; if (t->sock.tcp.peer_sack_count > 0 && From 07fcef063ed3034eb15e90938bcb61ad43933707 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 17 Aug 2026 11:48:56 +0200 Subject: [PATCH 07/21] F-6938: cap the backed-off TCP RTO at 64 seconds RFC 6298 section 5.5 requires the new RTO after a retransmission timeout to be min(2*RTO, G) where G is the maximum timer value, 64 seconds. The re-arm paths computed rto << backoff with no upper bound: tcp_rto_cb() (timeout re-arm and the bookkeeping-resync re-arm), the TCP flush re-arm, and tcp_ctrl_rto_start() for control segments. With a 2 s base RTO and a handful of timeouts the effective interval ran into the minutes, delaying loss recovery and control-segment retries far beyond the RFC limit. Add tcp_backoff_rto_ms() which doubles the base RTO per retry and caps the result at TCP_RTO_BACKOFF_MAX_MS (64 s), and route all four re-arm sites through it. The retry counters feeding the shift were already bounded (TCP_RTO_MAX_BACKOFF, TCP_CTRL_RTO_MAXRTX), so the shift itself stays defined; only the missing cap is added. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_tcp_flow.c | 67 +++++++++++++++++++++++++++++ src/wolfip.c | 23 ++++++++-- 3 files changed, 87 insertions(+), 4 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index d2f8f1be..e5638591 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -635,6 +635,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_tcp_ack_progress_resets_rto_recovery_state); tcase_add_test(tc_utils, test_tcp_ack_cwnd_grows_when_payload_acked_is_mss_minus_options); tcase_add_test(tc_utils, test_tcp_ack_data_segments_not_counted_as_dup_acks); + tcase_add_test(tc_utils, test_tcp_rto_backoff_capped_at_64s); tcase_add_test(tc_utils, test_tcp_ack_inflight_deflate_sets_writable_without_acked_desc); tcase_add_test(tc_utils, test_tcp_input_peer_rwnd_growth_sets_writable); tcase_add_test(tc_utils, test_tcp_input_synack_negotiates_peer_mss); diff --git a/src/test/unit/unit_tests_tcp_flow.c b/src/test/unit/unit_tests_tcp_flow.c index 31320516..dab988fc 100644 --- a/src/test/unit/unit_tests_tcp_flow.c +++ b/src/test/unit/unit_tests_tcp_flow.c @@ -2587,6 +2587,73 @@ START_TEST(test_tcp_ack_data_segments_not_counted_as_dup_acks) } END_TEST +/* RFC 6298 §5.5: on a retransmission timeout the new RTO is min(2*RTO, G) + * with G the maximum timer value, 64 s. With a 2 s base RTO and six + * backoff doublings the uncapped interval would be 128 s; the re-armed + * timer must land at 64 s. */ +START_TEST(test_tcp_rto_backoff_capped_at_64s) +{ + struct wolfIP s; + struct tsocket *ts; + struct tcp_seg_buf segbuf; + struct pkt_desc *desc; + uint64_t now = 100000; + uint32_t seq = 1000; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->if_idx = TEST_PRIMARY_IF; + ts->src_port = 1234; + ts->dst_port = 4321; + ts->local_ip = 0x0A000001U; + ts->remote_ip = 0x0A000002U; + ts->sock.tcp.state = TCP_ESTABLISHED; + ts->sock.tcp.snd_una = seq; + ts->sock.tcp.seq = seq + TCP_MSS; + ts->sock.tcp.bytes_in_flight = TCP_MSS; + ts->sock.tcp.rto = 2000; + ts->sock.tcp.rto_backoff = 6; + ts->sock.tcp.tmr_rto = NO_TIMER; + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + /* In-flight segment covering snd_una so the timeout retransmits and + * re-arms the RTO timer. */ + memset(&segbuf, 0, sizeof(segbuf)); + segbuf.seg.ip.len = ee16(IP_HEADER_LEN + TCP_HEADER_LEN + TCP_MSS); + segbuf.seg.hlen = TCP_HEADER_LEN << 2; + segbuf.seg.seq = ee32(seq); + ck_assert_int_eq(fifo_push(&ts->sock.tcp.txbuf, &segbuf, sizeof(segbuf)), 0); + desc = fifo_peek(&ts->sock.tcp.txbuf); + ck_assert_ptr_nonnull(desc); + desc->flags |= PKT_FLAG_SENT; + + s.last_tick = now; + tcp_rto_cb(ts); + + ck_assert_int_ne(ts->sock.tcp.tmr_rto, NO_TIMER); + { + uint32_t armed_expires = 0; + int found = 0; + int i; + for (i = 0; i < (int)s.timers.size; i++) { + if (s.timers.timers[i].id == ts->sock.tcp.tmr_rto) { + armed_expires = s.timers.timers[i].expires; + found = 1; + break; + } + } + ck_assert_int_eq(found, 1); + ck_assert_uint_eq(armed_expires, now + TCP_RTO_BACKOFF_MAX_MS); + } +} +END_TEST + START_TEST(test_tcp_ack_inflight_deflate_sets_writable_without_acked_desc) { struct wolfIP s; diff --git a/src/wolfip.c b/src/wolfip.c index 36c28a44..2aa8087b 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -160,6 +160,8 @@ struct wolfIP_icmp_packet; #define TCP_RTO_MIN_MS 1000U #define TCP_RTO_MAX_MS 60000U #define TCP_RTO_G_MS 1U +/* RFC 6298 §5.5: maximum timer value G; caps the backed-off RTO. */ +#define TCP_RTO_BACKOFF_MAX_MS 64000U #define TCP_PERSIST_MIN_MS 1000U #define TCP_PERSIST_MAX_MS 60000U #ifndef TCP_FIN_WAIT_2_TIMEOUT_MS @@ -3694,6 +3696,16 @@ static void tcp_ctrl_rto_stop(struct tsocket *t) t->sock.tcp.ctrl_rto_retries = 0; } +/* RFC 6298 §5.5: the RTO is doubled per retransmission timeout but MUST be + * capped at the maximum timer value G (64 s). */ +static uint32_t tcp_backoff_rto_ms(uint32_t rto_ms, uint32_t retries) +{ + uint64_t rto = (uint64_t)rto_ms << retries; + if (rto > TCP_RTO_BACKOFF_MAX_MS) + rto = TCP_RTO_BACKOFF_MAX_MS; + return (uint32_t)rto; +} + /* Arm/re-arm control-RTO timer using exponential backoff over the current base RTO. * This path is dedicated to SYN/SYN-ACK/FIN reliability (not data-loss recovery). */ static void tcp_ctrl_rto_start(struct tsocket *t, uint64_t now) @@ -3707,7 +3719,7 @@ static void tcp_ctrl_rto_start(struct tsocket *t, uint64_t now) timer_binheap_cancel(&t->S->timers, t->sock.tcp.tmr_rto); t->sock.tcp.tmr_rto = NO_TIMER; } - shift_rto = (uint64_t)t->sock.tcp.rto << t->sock.tcp.ctrl_rto_retries; + shift_rto = tcp_backoff_rto_ms(t->sock.tcp.rto, t->sock.tcp.ctrl_rto_retries); tmr.expires = now + shift_rto; tmr.arg = t; tmr.cb = tcp_rto_cb; @@ -5646,7 +5658,8 @@ static void tcp_rto_cb(void *arg) ts->sock.tcp.recovery_point = ts->sock.tcp.snd_una; ptmr = &tmr; - ptmr->expires = ts->S->last_tick + (ts->sock.tcp.rto << ts->sock.tcp.rto_backoff); + ptmr->expires = ts->S->last_tick + + tcp_backoff_rto_ms(ts->sock.tcp.rto, ts->sock.tcp.rto_backoff); ptmr->arg = ts; ptmr->cb = tcp_rto_cb; ts->sock.tcp.tmr_rto = timers_binheap_insert(&ts->S->timers, *ptmr); @@ -5690,7 +5703,8 @@ static void tcp_resync_inflight(struct wolfIP *s, struct tsocket *ts, uint64_t n if (has_sent_payload && ts->sock.tcp.tmr_rto == NO_TIMER) { struct wolfIP_timer new_tmr = {}; new_tmr.cb = tcp_rto_cb; - new_tmr.expires = now + (ts->sock.tcp.rto << ts->sock.tcp.rto_backoff); + new_tmr.expires = now + tcp_backoff_rto_ms(ts->sock.tcp.rto, + ts->sock.tcp.rto_backoff); new_tmr.arg = ts; ts->sock.tcp.tmr_rto = timers_binheap_insert(&s->timers, new_tmr); } else if (!has_sent_payload && ts->sock.tcp.tmr_rto != NO_TIMER) { @@ -10457,7 +10471,8 @@ static void flush_tcp_tx(struct wolfIP *s, uint64_t now) ts->sock.tcp.tmr_rto = NO_TIMER; } new_tmr.cb = tcp_rto_cb; - new_tmr.expires = now + (ts->sock.tcp.rto << ts->sock.tcp.rto_backoff); + new_tmr.expires = now + tcp_backoff_rto_ms(ts->sock.tcp.rto, + ts->sock.tcp.rto_backoff); new_tmr.arg = ts; ts->sock.tcp.tmr_rto = timers_binheap_insert(&s->timers, new_tmr); if (!is_retrans) { From 2ae0fa2bb5bdc0cf3db7284b9b0bdf768fbf78c9 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 17 Aug 2026 11:54:03 +0200 Subject: [PATCH 08/21] F-8564: extend control-segment retry budget to meet the 3-minute R2 floor RFC 9293 section 3.5 gives the R2 retransmission timeout for SYN and FIN segments a 3-minute default. The control-RTO budget of 6 retries over the 1 s base RTO accumulated 1+2+4+8+16+32+64 = 127 s, so an unanswered active-open SYN was abandoned well short of the required 180 s (and FIN retransmits with it). Raise TCP_CTRL_RTO_MAXRTX from 6 to 8. With the 64 s backoff cap in place the arms are 1,2,4,8,16,32,64,64,64 s, giving up at 255 s: at or above the R2 floor with margin, and bounded. The budget is shared by SYN, SYN-ACK and FIN retransmission, all of which the R2 guidance covers. A test drives the retransmission timer to exhaustion and asserts the give-up is not before 180 s. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_dns_dhcp.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 e5638591..cdd43b26 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -534,6 +534,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_tcp_rto_cb_non_tcp_noop); tcase_add_test(tc_utils, test_tcp_rto_cb_non_established_noop); tcase_add_test(tc_utils, test_tcp_rto_cb_syn_sent_requeues_syn_and_arms_timer); + tcase_add_test(tc_utils, test_tcp_syn_retransmit_duration_meets_rfc9293_r2); tcase_add_test(tc_utils, test_tcp_input_synack_cancels_control_rto); tcase_add_test(tc_utils, test_tcp_rto_cb_last_ack_requeues_finack_and_arms_timer); tcase_add_test(tc_utils, test_tcp_rto_cb_last_ack_full_txbuf_keeps_retry_budget); diff --git a/src/test/unit/unit_tests_dns_dhcp.c b/src/test/unit/unit_tests_dns_dhcp.c index 37112b52..084147b9 100644 --- a/src/test/unit/unit_tests_dns_dhcp.c +++ b/src/test/unit/unit_tests_dns_dhcp.c @@ -3684,6 +3684,59 @@ START_TEST(test_tcp_rto_cb_syn_sent_requeues_syn_and_arms_timer) } END_TEST +/* RFC 9293 §3.5: the R2 retransmission timeout for SYN segments defaults + * to 3 minutes, so an unanswered active-open SYN must not be abandoned + * before 180 s have elapsed. */ +START_TEST(test_tcp_syn_retransmit_duration_meets_rfc9293_r2) +{ + struct wolfIP s; + struct tsocket *ts; + uint64_t expiry = 0; + int found = 0; + int iterations = 0; + int i; + + wolfIP_init(&s); + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->sock.tcp.state = TCP_SYN_SENT; + ts->sock.tcp.rto = TCP_RTO_MIN_MS; + ts->sock.tcp.ctrl_rto_active = 1; + ts->src_port = 12345; + ts->dst_port = 5001; + ts->local_ip = 0x0A000001U; + ts->remote_ip = 0x0A000002U; + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + /* The first SYN went out at t=0 with the control RTO armed. */ + s.last_tick = 0; + tcp_ctrl_rto_start(ts, 0); + + /* No answer ever arrives: drive the retransmission timer until the + * stack gives up. */ + while (ts->sock.tcp.state == TCP_SYN_SENT && iterations < 64) { + expiry = 0; + found = 0; + for (i = 0; i < (int)s.timers.size; i++) { + if (s.timers.timers[i].id == ts->sock.tcp.tmr_rto) { + expiry = s.timers.timers[i].expires; + found = 1; + break; + } + } + ck_assert_int_eq(found, 1); + s.last_tick = expiry; + tcp_rto_cb(ts); + iterations++; + } + + ck_assert_int_ne(ts->sock.tcp.state, TCP_SYN_SENT); + ck_assert_uint_ge(s.last_tick, 180000); +} +END_TEST + START_TEST(test_tcp_input_synack_cancels_control_rto) { struct wolfIP s; diff --git a/src/wolfip.c b/src/wolfip.c index 2aa8087b..bc8963f2 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -148,7 +148,12 @@ struct wolfIP_icmp_packet; /* Compatibility alias for legacy fixed-size uses. */ #define TCP_MSS TCP_MSS_MAX #define TCP_DEFAULT_MSS 536U -#define TCP_CTRL_RTO_MAXRTX 6U +/* Control-segment (SYN/SYN-ACK/FIN) retry budget. RFC 9293 §3.5 gives the + * R2 retransmission timeout for SYN and FIN segments a 3-minute default, + * so the retry schedule must accumulate at least 180 s: with the 1 s base + * RTO and the 64 s backoff cap the arms are 1,2,4,8,16,32,64,64,64 = 255 s + * before the 8th timeout gives up. */ +#define TCP_CTRL_RTO_MAXRTX 8U #define TCP_RTO_MAX_BACKOFF 15U /* Max retries before closing; also clamps shift */ #ifdef IP_MULTICAST From 8714895c1862b549956736701ae6f40af2ea029b Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 17 Aug 2026 12:01:14 +0200 Subject: [PATCH 09/21] F-8516: forward transit packets using the static route table wolfIP_forward_interface() only matched directly connected subnets, so a transit packet whose destination was reachable only through a configured static route (wolfIP_route_add) found no egress and was dropped in local-delivery processing, while locally originated traffic honored the same routes. When no connected egress exists, resolve the destination in the static route table with the same tie-breaks as the local lookup (longest prefix, then order, then interface) and forward through the route's interface. ARP resolution and the pending-ARP queue now use the route's gateway as next hop instead of the final destination, and a route pointing back at the ingress interface is not honored. Connected subnet forwarding, the no-route drop behavior, and TTL handling are unchanged. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_ip_arp_recv.c | 57 ++++++++++++++++++++++++++ src/wolfip.c | 35 +++++++++++++++- 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index cdd43b26..c70a22e6 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -1406,6 +1406,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_core, test_ip_recv_directed_broadcast_dst_is_local); tcase_add_test(tc_core, test_ip_recv_ipaddr_any_dst_is_local); tcase_add_test(tc_core, test_ip_recv_forward_arp_hit_sends_immediately); + tcase_add_test(tc_core, test_ip_recv_forward_static_route_uses_gateway); tcase_add_test(tc_core, test_ip_recv_forward_unconfigured_iface_skipped); tcase_add_test(tc_core, test_ip_recv_forward_link_local_src_rpf_drop); tcase_add_test(tc_core, test_ip_recv_forward_self_ip_src_dropped); diff --git a/src/test/unit/unit_tests_ip_arp_recv.c b/src/test/unit/unit_tests_ip_arp_recv.c index e2705a87..38ee4bea 100644 --- a/src/test/unit/unit_tests_ip_arp_recv.c +++ b/src/test/unit/unit_tests_ip_arp_recv.c @@ -237,6 +237,63 @@ START_TEST(test_ip_recv_forward_arp_hit_sends_immediately) } END_TEST +/* A transit packet whose destination is reachable only through a static + * route (not a connected subnet) must be forwarded out the route's + * interface, resolved to the route's gateway for ARP, with the IP + * destination untouched. */ +START_TEST(test_ip_recv_forward_static_route_uses_gateway) +{ + struct wolfIP s; + uint8_t frame[ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN]; + struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)frame; + struct wolfIP_ip_packet *sent; + ip4 primary_ip = 0x0A000001U; /* 10.0.0.1 on if1 */ + ip4 secondary_ip = 0xC0A80101U; /* 192.168.1.1 on if2 */ + ip4 dest_ip = 0x0A020005U; /* 10.2.0.5 — static-route only */ + ip4 src_ip = 0x0A000002U; + ip4 gw_ip = 0xC0A801FEU; /* 192.168.1.254, route gateway */ + static const uint8_t gw_mac[6] = {0x10, 0x11, 0x12, 0x13, 0x14, 0x15}; + + setup_stack_with_two_ifaces(&s, primary_ip, secondary_ip); + wolfIP_filter_set_callback(NULL, NULL); + ck_assert_int_eq(wolfIP_route_add(&s, TEST_SECOND_IF, 0x0A020000U, 24, + gw_ip), 0); + + /* ARP is known for the gateway; the final destination is not on-link + * and has no cache entry. */ + arp_store_neighbor(&s, TEST_SECOND_IF, gw_ip, gw_mac); + + last_frame_sent_size = 0; + + memset(frame, 0, sizeof(frame)); + memcpy(ip->eth.dst, s.ll_dev[TEST_PRIMARY_IF].mac, 6); + memcpy(ip->eth.src, "\x01\x02\x03\x04\x05\x06", 6); + ip->eth.type = ee16(ETH_TYPE_IP); + ip->ver_ihl = 0x45; + ip->ttl = 64; + ip->proto = WI_IPPROTO_UDP; + ip->len = ee16(IP_HEADER_LEN + UDP_HEADER_LEN); + ip->src = ee32(src_ip); + ip->dst = ee32(dest_ip); + fix_ip_checksum(ip); + { + uint16_t *udp = (uint16_t *)(frame + ETH_HEADER_LEN + IP_HEADER_LEN); + udp[0] = ee16(9999); udp[1] = ee16(53); + udp[2] = ee16(UDP_HEADER_LEN); udp[3] = 0; + } + + ip_recv(&s, TEST_PRIMARY_IF, ip, (uint32_t)sizeof(frame)); + + /* Forwarded out if2, addressed to the gateway, IP dst preserved and + * TTL decremented. */ + ck_assert_uint_gt(last_frame_sent_size, 0); + ck_assert_mem_eq(last_frame_sent + 0, gw_mac, 6); + sent = (struct wolfIP_ip_packet *)last_frame_sent; + ck_assert_uint_eq(ee32(sent->dst), dest_ip); + ck_assert_uint_eq(sent->ttl, 63); +} +END_TEST + /* ========================================================================= * ip_recv: forward interface with no configured IP is skipped * ========================================================================= diff --git a/src/wolfip.c b/src/wolfip.c index bc8963f2..113b526a 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -9414,6 +9414,36 @@ static inline void ip_recv(struct wolfIP *s, unsigned int if_idx, if (!l2_group) { int out_if = wolfIP_forward_interface(s, if_idx, dest); + ip4 next_hop = dest; + if (out_if < 0) { + /* No connected egress: resolve the destination in the + * static route table (longest prefix, then order, then + * interface) and forward to the route's gateway. */ + unsigned int ri; + uint8_t best_plen = 0; + uint32_t best_order = UINT32_MAX; + for (ri = 0; ri < WOLFIP_MAX_ROUTES; ri++) { + const struct wolfIP_route_entry *route = &s->routes[ri]; + if (!route->used) + continue; + if (!wolfIP_route_match_prefix(dest, route->prefix, + route->prefix_len)) + continue; + if (out_if < 0 || route->prefix_len > best_plen || + (route->prefix_len == best_plen && + (route->order < best_order || + (route->order == best_order && + route->if_idx < (unsigned int)out_if)))) { + out_if = (int)route->if_idx; + best_plen = route->prefix_len; + best_order = route->order; + next_hop = route->gateway != IPADDR_ANY ? + route->gateway : dest; + } + } + if (out_if >= 0 && (unsigned int)out_if == if_idx) + out_if = -1; + } if (out_if >= 0) { uint8_t mac[6]; int broadcast = 0; @@ -9429,8 +9459,9 @@ static inline void ip_recv(struct wolfIP *s, unsigned int if_idx, wolfIP_send_ttl_exceeded(s, if_idx, ip); return; } - if (!wolfIP_forward_prepare(s, out_if, dest, mac, &broadcast)) { - arp_queue_packet(s, out_if, dest, ip, len); + if (!wolfIP_forward_prepare(s, out_if, next_hop, mac, + &broadcast)) { + arp_queue_packet(s, out_if, next_hop, ip, len); return; } ip->ttl--; From 69f0baef90aebf392592f8c143498652987fdf77 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 17 Aug 2026 12:05:06 +0200 Subject: [PATCH 10/21] F-8558: drop and clamp forwarding on the declared IP total length ip_recv validated the minimum frame length, IHL, the total-length floor, and the header checksum, but never compared the declared IPv4 total length against the bytes actually received. A frame whose header claimed more payload than it carried (with a valid header checksum) was relayed downstream with an internally inconsistent length, and a frame carrying trailing link-layer padding was forwarded at the padded frame length, relaying padding as IP payload. Reject any datagram whose declared total length exceeds the received bytes before filtering or forwarding, and forward at ETH_HEADER_LEN + declared length so padding is stripped. The TTL-exceeded guard keeps using the received length: the Time Exceeded reply copies bytes from the frame as received, and datagrams with a declared length shorter than the frame are legitimate. Also resolves F-8518 (same root cause, wolfip-bugs target). --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_ip_arp_recv.c | 69 ++++++++++++++++++++++++++ src/wolfip.c | 13 ++++- 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index c70a22e6..4bb16882 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -1407,6 +1407,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_core, test_ip_recv_ipaddr_any_dst_is_local); tcase_add_test(tc_core, test_ip_recv_forward_arp_hit_sends_immediately); tcase_add_test(tc_core, test_ip_recv_forward_static_route_uses_gateway); + tcase_add_test(tc_core, test_ip_recv_forward_declared_length_checked); tcase_add_test(tc_core, test_ip_recv_forward_unconfigured_iface_skipped); tcase_add_test(tc_core, test_ip_recv_forward_link_local_src_rpf_drop); tcase_add_test(tc_core, test_ip_recv_forward_self_ip_src_dropped); diff --git a/src/test/unit/unit_tests_ip_arp_recv.c b/src/test/unit/unit_tests_ip_arp_recv.c index 38ee4bea..3b9b7343 100644 --- a/src/test/unit/unit_tests_ip_arp_recv.c +++ b/src/test/unit/unit_tests_ip_arp_recv.c @@ -294,6 +294,75 @@ START_TEST(test_ip_recv_forward_static_route_uses_gateway) } END_TEST +/* The declared IPv4 total length must match the bytes actually received: + * a frame whose header claims more payload than it carries is dropped + * (it must not be relayed with an inconsistent length), and a frame with + * trailing link-layer padding is forwarded at the declared length, not + * the padded frame length. */ +START_TEST(test_ip_recv_forward_declared_length_checked) +{ + struct wolfIP s; + uint8_t frame[ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN + 4]; + struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)frame; + ip4 primary_ip = 0x0A000001U; + ip4 secondary_ip = 0xC0A80101U; + ip4 dest_ip = 0xC0A80155U; /* 192.168.1.85 — local to if2 */ + ip4 src_ip = 0x0A000002U; + static const uint8_t dest_mac[6] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; + uint32_t ip_len = IP_HEADER_LEN + UDP_HEADER_LEN; + + setup_stack_with_two_ifaces(&s, primary_ip, secondary_ip); + wolfIP_filter_set_callback(NULL, NULL); + arp_store_neighbor(&s, TEST_SECOND_IF, dest_ip, dest_mac); + + /* Scenario 1: header declares 16 more payload bytes than the frame + * carries; the checksum is valid for the (inconsistent) header. */ + last_frame_sent_size = 0; + memset(frame, 0, sizeof(frame)); + memcpy(ip->eth.dst, s.ll_dev[TEST_PRIMARY_IF].mac, 6); + memcpy(ip->eth.src, "\x01\x02\x03\x04\x05\x06", 6); + ip->eth.type = ee16(ETH_TYPE_IP); + ip->ver_ihl = 0x45; + ip->ttl = 64; + ip->proto = WI_IPPROTO_UDP; + ip->len = ee16(ip_len + 16); + ip->src = ee32(src_ip); + ip->dst = ee32(dest_ip); + fix_ip_checksum(ip); + { + uint16_t *udp = (uint16_t *)(frame + ETH_HEADER_LEN + IP_HEADER_LEN); + udp[0] = ee16(9999); udp[1] = ee16(53); + udp[2] = ee16(UDP_HEADER_LEN); udp[3] = 0; + } + + ip_recv(&s, TEST_PRIMARY_IF, ip, ETH_HEADER_LEN + ip_len); + ck_assert_uint_eq(last_frame_sent_size, 0); + + /* Scenario 2: well-formed datagram plus 4 bytes of link-layer + * padding; the forwarded frame must be exactly the declared length. */ + last_frame_sent_size = 0; + memset(frame, 0, sizeof(frame)); + memcpy(ip->eth.dst, s.ll_dev[TEST_PRIMARY_IF].mac, 6); + memcpy(ip->eth.src, "\x01\x02\x03\x04\x05\x06", 6); + ip->eth.type = ee16(ETH_TYPE_IP); + ip->ver_ihl = 0x45; + ip->ttl = 64; + ip->proto = WI_IPPROTO_UDP; + ip->len = ee16(ip_len); + ip->src = ee32(src_ip); + ip->dst = ee32(dest_ip); + fix_ip_checksum(ip); + { + uint16_t *udp = (uint16_t *)(frame + ETH_HEADER_LEN + IP_HEADER_LEN); + udp[0] = ee16(9999); udp[1] = ee16(53); + udp[2] = ee16(UDP_HEADER_LEN); udp[3] = 0; + } + + ip_recv(&s, TEST_PRIMARY_IF, ip, ETH_HEADER_LEN + ip_len + 4); + ck_assert_uint_eq(last_frame_sent_size, ETH_HEADER_LEN + ip_len); +} +END_TEST + /* ========================================================================= * ip_recv: forward interface with no configured IP is skipped * ========================================================================= diff --git a/src/wolfip.c b/src/wolfip.c index 113b526a..01e5880c 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -9269,6 +9269,11 @@ static inline void ip_recv(struct wolfIP *s, unsigned int if_idx, return; if (ee16(ip->len) < ip_hlen) return; + /* The declared total length cannot exceed the bytes actually received, + * or the datagram would be relayed or delivered with a header claiming + * more payload than exists on the wire. */ + if (ee16(ip->len) > len - ETH_HEADER_LEN) + return; /* validate IP header checksum per RFC 1122 */ if (iphdr_verify_checksum(ip) != 0) return; @@ -9415,6 +9420,9 @@ static inline void ip_recv(struct wolfIP *s, unsigned int if_idx, if (!l2_group) { int out_if = wolfIP_forward_interface(s, if_idx, dest); ip4 next_hop = dest; + /* Forward the datagram at its declared length so trailing + * link-layer padding is not relayed as IP payload. */ + uint32_t fwd_len = ETH_HEADER_LEN + ee16(ip->len); if (out_if < 0) { /* No connected egress: resolve the destination in the * static route table (longest prefix, then order, then @@ -9461,13 +9469,14 @@ static inline void ip_recv(struct wolfIP *s, unsigned int if_idx, } if (!wolfIP_forward_prepare(s, out_if, next_hop, mac, &broadcast)) { - arp_queue_packet(s, out_if, next_hop, ip, len); + arp_queue_packet(s, out_if, next_hop, ip, fwd_len); return; } ip->ttl--; ip->csum = 0; iphdr_set_checksum(ip); - wolfIP_forward_packet(s, out_if, ip, len, broadcast ? NULL : mac, broadcast); + wolfIP_forward_packet(s, out_if, ip, fwd_len, broadcast ? NULL : mac, + broadcast); return; } } From 632eeaaed28728ddca92f6aaafa8e4e5646e5c09 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 17 Aug 2026 12:08:07 +0200 Subject: [PATCH 11/21] F-8517: recompute the IP header checksum after the IP_HDRINCL destination override With ipheader_include set, flush_raw_tx skips checksum generation on the assumption the caller supplied a complete, correct header. But wolfIP_sock_sendto overrides that header's destination with the sendto() socket address when one is given, and destination is covered by the IPv4 header checksum. The packet then reached the wire addressed to the socket destination carrying the caller's checksum for the header destination, failing verification in the middle of the net. Recompute the header checksum after the override. The override itself (socket address wins over the header) is unchanged. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_socket_api_arms.c | 60 ++++++++++++++++++++++ src/wolfip.c | 8 ++- 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 4bb16882..18967888 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -1159,6 +1159,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_core, test_sock_sendto_raw_null_dest_uses_stored_remote_ip); tcase_add_test(tc_core, test_sock_sendto_raw_null_dest_no_remote_ip); tcase_add_test(tc_core, test_sock_sendto_raw_hdrincl_dst_from_buf); + tcase_add_test(tc_core, test_raw_hdrincl_dst_override_recomputes_ip_checksum); tcase_add_test(tc_core, test_sock_sendto_raw_invalid_fd); tcase_add_test(tc_core, test_sock_sendto_raw_fifo_full_returns_eagain); tcase_add_test(tc_core, test_sock_setsockopt_raw_hdrincl); diff --git a/src/test/unit/unit_tests_socket_api_arms.c b/src/test/unit/unit_tests_socket_api_arms.c index b203cc12..ab4763cf 100644 --- a/src/test/unit/unit_tests_socket_api_arms.c +++ b/src/test/unit/unit_tests_socket_api_arms.c @@ -930,6 +930,66 @@ START_TEST(test_sock_sendto_raw_hdrincl_dst_from_buf) } END_TEST +/* IP_HDRINCL: when the sendto() socket address overrides the destination + * of the caller-supplied header, the IPv4 header checksum must be + * recomputed; flush_raw_tx trusts the caller's header as-is. */ +START_TEST(test_raw_hdrincl_dst_override_recomputes_ip_checksum) +{ + struct wolfIP s; + int sd; + int one = 1; + uint8_t ip_buf[ETH_HEADER_LEN + IP_HEADER_LEN + 4]; + struct wolfIP_ip_packet *ip; + struct wolfIP_sockaddr_in sin; + struct wolfIP_ip_packet *sent; + uint8_t nh_mac[6] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; + int ret; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + + s.arp.neighbors[0].ip = 0x0A000003U; + s.arp.neighbors[0].if_idx = TEST_PRIMARY_IF; + memcpy(s.arp.neighbors[0].mac, nh_mac, 6); + + sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_RAW, WI_IPPROTO_UDP); + ck_assert_int_ge(sd, 0); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_HDRINCL, &one, sizeof(one)), 0); + + /* Header checksummed for destination A. */ + memset(ip_buf, 0, sizeof(ip_buf)); + ip = (struct wolfIP_ip_packet *)ip_buf; + ip->ver_ihl = 0x45; + ip->ttl = 64; + ip->proto = WI_IPPROTO_UDP; + ip->len = ee16(IP_HEADER_LEN + 4); + ip->src = ee32(0x0A000001U); + ip->dst = ee32(0x0A000002U); + iphdr_set_checksum(ip); + + /* sendto addresses the packet at B: the override must reach the wire + * with a checksum valid for B. */ + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_addr.s_addr = ee32(0x0A000003U); + last_frame_sent_size = 0; + ret = wolfIP_sock_sendto(&s, sd, + (uint8_t *)ip + ETH_HEADER_LEN, + IP_HEADER_LEN + 4, + 0, (const struct wolfIP_sockaddr *)&sin, + sizeof(sin)); + ck_assert_int_eq(ret, IP_HEADER_LEN + 4); + wolfIP_poll(&s, 10); + + ck_assert_uint_gt(last_frame_sent_size, 0); + sent = (struct wolfIP_ip_packet *)last_frame_sent; + ck_assert_uint_eq(ee32(sent->dst), 0x0A000003U); + ck_assert_int_eq(iphdr_verify_checksum(sent), 0); +} +END_TEST + START_TEST(test_sock_sendto_raw_invalid_fd) { struct wolfIP s; diff --git a/src/wolfip.c b/src/wolfip.c index 01e5880c..f5c5345f 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -6488,8 +6488,14 @@ int wolfIP_sock_sendto(struct wolfIP *s, int sockfd, const void *buf, size_t len total_len = (uint32_t)len + ETH_HEADER_LEN; if (dst_ip == 0) dst_ip = ee32(rip->dst); - else + else { + /* The override changes a field covered by the IPv4 header + * checksum, and the flush path trusts the caller's header + * as-is, so recompute it for the transmitted destination. */ rip->dst = ee32(dst_ip); + rip->csum = 0; + iphdr_set_checksum(rip); + } if (rs->remote_ip == 0 && dst_ip != 0) rs->remote_ip = dst_ip; rs->local_ip = ee32(rip->src); From f97f172f3a024b696a025652751cb6ca7ca63af1 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 17 Aug 2026 12:10:04 +0200 Subject: [PATCH 12/21] F-8515: roll back the armed DNS query when name encoding fails dns_send_query() assigned s->dns_id before validating the encoded name. The two invalid-label exits (label over MAX_DNS_LABEL_LEN, or name over MAX_DNS_NAME_LEN) returned -22 without cleanup, unlike every other failure path in the function. No timer was scheduled on those exits, so dns_timeout_cb() never cleared the state: s->dns_id stayed nonzero and both nslookup() and dns_send_query() gate on it, making every subsequent lookup on the stack fail with -16 until reinitialization. Call dns_abort_query() and reset *id on both exits, matching the connect/sendto failure paths. The existing invalid-name test now asserts the resolver is left un-armed and that a subsequent lookup succeeds, dropping the manual s.dns_id reset the defect had forced on it. --- src/test/unit/unit_tests_dns_dhcp.c | 12 +++++++++++- src/wolfip.c | 15 +++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/test/unit/unit_tests_dns_dhcp.c b/src/test/unit/unit_tests_dns_dhcp.c index 084147b9..913fb9ec 100644 --- a/src/test/unit/unit_tests_dns_dhcp.c +++ b/src/test/unit/unit_tests_dns_dhcp.c @@ -3052,8 +3052,10 @@ START_TEST(test_dns_send_query_invalid_name) memcpy(name + 65, "com", 3); name[68] = 0; ck_assert_int_eq(dns_send_query(&s, name, &id, DNS_A), -22); + /* The failed encode must not leave the resolver armed. */ + ck_assert_uint_eq(s.dns_id, 0); + ck_assert_uint_eq(id, DNS_ID_NONE); - s.dns_id = 0; memset(name, 'a', sizeof(name)); pos = 0; memset(name + pos, 'a', 63); @@ -3069,6 +3071,14 @@ START_TEST(test_dns_send_query_invalid_name) pos += 63; name[pos] = 0; ck_assert_int_eq(dns_send_query(&s, name, &id, DNS_A), -22); + ck_assert_uint_eq(s.dns_id, 0); + ck_assert_uint_eq(id, DNS_ID_NONE); + + /* A subsequent lookup must not be blocked by the failed ones. */ + ck_assert_int_eq(dns_send_query(&s, "example.com", &id, DNS_A), 0); + ck_assert_uint_ne(s.dns_id, 0); + dns_abort_query(&s); + ck_assert_uint_eq(s.dns_id, 0); } END_TEST START_TEST(test_fifo_push_and_pop) { diff --git a/src/wolfip.c b/src/wolfip.c index f5c5345f..882b13e1 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -10187,8 +10187,19 @@ static int dns_send_query(struct wolfIP *s, const char *dname, uint16_t *id, tok_end++; } label_len = (uint32_t)(tok_end - tok_start); - if (label_len > MAX_DNS_LABEL_LEN) return -22; - if (tok_len + label_len + 1 > MAX_DNS_NAME_LEN) return -22; + if (label_len > MAX_DNS_LABEL_LEN) { + /* Unencodable name: roll back the armed query state, or every + * later lookup would fail the busy guard with no timer to + * clear it. */ + dns_abort_query(s); + *id = DNS_ID_NONE; + return -22; + } + if (tok_len + label_len + 1 > MAX_DNS_NAME_LEN) { + dns_abort_query(s); + *id = DNS_ID_NONE; + return -22; + } *q_name = (char)label_len; q_name++; memcpy(q_name, tok_start, label_len); From e2322a1c5eb49b0887b88446209bd87fc02a00ae Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 17 Aug 2026 12:12:03 +0200 Subject: [PATCH 13/21] F-8569: reject zero-length DNS labels in the query encoder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The presentation-name encoder split the name on '.' without rejecting empty labels. A leading or interior dot encoded a zero-length label — the wire-format root terminator per RFC 1035 section 3.1 — and then kept appending the remaining labels after it, producing a malformed QNAME with labels beyond the terminator. Reject a zero-length label wherever the tokenizer encounters one. The trailing-dot FQDN presentation form ("example.com.") ends the loop before an empty token is encoded and remains valid. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_dns_dhcp.c | 31 +++++++++++++++++++++++++++++ src/wolfip.c | 10 ++++++++++ 3 files changed, 42 insertions(+) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 18967888..ebef4f5f 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -521,6 +521,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_dns_abort_query_clears_timer_and_query_state); tcase_add_test(tc_utils, test_dns_timeout_retries_then_aborts_and_allows_new_query); tcase_add_test(tc_utils, test_dns_send_query_invalid_name); + tcase_add_test(tc_utils, test_dns_send_query_rejects_empty_labels); tcase_add_test(tc_utils, test_dns_wrapper_apis); tcase_add_test(tc_utils, test_wolfip_static_instance_apis); tcase_add_test(tc_utils, test_wolfip_ip_is_multicast_variants); diff --git a/src/test/unit/unit_tests_dns_dhcp.c b/src/test/unit/unit_tests_dns_dhcp.c index 913fb9ec..4375ac97 100644 --- a/src/test/unit/unit_tests_dns_dhcp.c +++ b/src/test/unit/unit_tests_dns_dhcp.c @@ -3081,6 +3081,37 @@ START_TEST(test_dns_send_query_invalid_name) ck_assert_uint_eq(s.dns_id, 0); } END_TEST + +/* A zero-length label is the wire-format root terminator (RFC 1035 + * §3.1); it is only legal at the end of the name. Leading or interior + * dots must be rejected, while the trailing-dot FQDN presentation form + * stays valid. */ +START_TEST(test_dns_send_query_rejects_empty_labels) +{ + struct wolfIP s; + uint16_t id = 0; + + wolfIP_init(&s); + mock_link_init(&s); + s.dns_server = 0x08080808U; + + /* Leading dot. */ + ck_assert_int_eq(dns_send_query(&s, ".com", &id, DNS_A), -22); + ck_assert_uint_eq(s.dns_id, 0); + ck_assert_uint_eq(id, DNS_ID_NONE); + + /* Interior dot. */ + ck_assert_int_eq(dns_send_query(&s, "a..com", &id, DNS_A), -22); + ck_assert_uint_eq(s.dns_id, 0); + ck_assert_uint_eq(id, DNS_ID_NONE); + + /* Trailing root dot: valid FQDN presentation form. */ + ck_assert_int_eq(dns_send_query(&s, "example.com.", &id, DNS_A), 0); + ck_assert_uint_ne(s.dns_id, 0); + dns_abort_query(&s); + ck_assert_uint_eq(s.dns_id, 0); +} +END_TEST START_TEST(test_fifo_push_and_pop) { struct fifo f; struct pkt_desc *desc, *desc2; diff --git a/src/wolfip.c b/src/wolfip.c index 882b13e1..03b9dc53 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -10187,6 +10187,16 @@ static int dns_send_query(struct wolfIP *s, const char *dname, uint16_t *id, tok_end++; } label_len = (uint32_t)(tok_end - tok_start); + if (label_len == 0) { + /* A zero-length label is the wire-format root terminator + * (RFC 1035 §3.1), only legal at the end of the name. The + * trailing-dot presentation form ends the loop before an + * empty token is encoded, so any zero-length label here is + * a leading or interior dot: invalid. */ + dns_abort_query(s); + *id = DNS_ID_NONE; + return -22; + } if (label_len > MAX_DNS_LABEL_LEN) { /* Unencodable name: roll back the armed query state, or every * later lookup would fail the busy guard with no timer to From 792c766fcf82ba843ccc5c3443076ac7c2fd907f Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 17 Aug 2026 12:14:23 +0200 Subject: [PATCH 14/21] F-8570: require an exact 4-byte RDATA for DNS A records The A-record answer handler accepted any RDLENGTH >= 4 and read the first 4 bytes as the address. RFC 1035 section 3.4.1 defines A RDATA as exactly a 32-bit address, so an RR with a different RDLENGTH is malformed and must not be delivered as the answer. Require rdlen == DNS_IPV4_RDATA_LEN in the A branch. A nonconforming RR is now skipped like any other non-matching record and the query stays outstanding for the retry/timeout path. The PTR branch is untouched: PTR RDATA is a name and legitimately varies in length. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_dns_dhcp.c | 63 +++++++++++++++++++++++++++++ src/wolfip.c | 2 +- 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index ebef4f5f..53767109 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -522,6 +522,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_dns_timeout_retries_then_aborts_and_allows_new_query); tcase_add_test(tc_utils, test_dns_send_query_invalid_name); tcase_add_test(tc_utils, test_dns_send_query_rejects_empty_labels); + tcase_add_test(tc_utils, test_dns_callback_rejects_a_record_with_wrong_rdlength); tcase_add_test(tc_utils, test_dns_wrapper_apis); tcase_add_test(tc_utils, test_wolfip_static_instance_apis); tcase_add_test(tc_utils, test_wolfip_ip_is_multicast_variants); diff --git a/src/test/unit/unit_tests_dns_dhcp.c b/src/test/unit/unit_tests_dns_dhcp.c index 4375ac97..5780d009 100644 --- a/src/test/unit/unit_tests_dns_dhcp.c +++ b/src/test/unit/unit_tests_dns_dhcp.c @@ -3112,6 +3112,69 @@ START_TEST(test_dns_send_query_rejects_empty_labels) ck_assert_uint_eq(s.dns_id, 0); } END_TEST + +/* RFC 1035 §3.4.1: A RDATA is exactly a 32-bit address. An A RR whose + * RDLENGTH is not 4 is malformed and must not be accepted as the answer; + * the query stays outstanding for the retry/timeout path. */ +START_TEST(test_dns_callback_rejects_a_record_with_wrong_rdlength) +{ + struct wolfIP s; + uint8_t response[192]; + int pos; + struct dns_header *hdr = (struct dns_header *)response; + struct dns_question *q; + struct dns_rr *rr; + struct tsocket *ts; + + wolfIP_init(&s); + mock_link_init(&s); + s.dns_server = 0x0A000001U; + arm_dns_query(&s, 0x1234, dns_qname_a_com, (int)sizeof(dns_qname_a_com), + DNS_A); + s.dns_lookup_cb = test_dns_lookup_cb; + dns_lookup_ip = 0; + s.dns_udp_sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, + WI_IPPROTO_UDP); + ck_assert_int_gt(s.dns_udp_sd, 0); + ts = &s.udpsockets[SOCKET_UNMARK(s.dns_udp_sd)]; + + memset(response, 0, sizeof(response)); + hdr->id = ee16(s.dns_id); + hdr->flags = ee16(0x8100); + hdr->qdcount = ee16(1); + hdr->ancount = ee16(1); + pos = (int)sizeof(struct dns_header); + response[pos++] = 1; response[pos++] = 'a'; + response[pos++] = 3; memcpy(&response[pos], "com", 3); pos += 3; + response[pos++] = 0; + q = (struct dns_question *)(response + pos); + q->qtype = ee16(DNS_A); + q->qclass = ee16(1); + pos += (int)sizeof(struct dns_question); + response[pos++] = 0xC0; + response[pos++] = (uint8_t)sizeof(struct dns_header); + rr = (struct dns_rr *)(response + pos); + rr->type = ee16(DNS_A); + rr->class = ee16(1); + rr->ttl = ee32(60); + rr->rdlength = ee16(5); /* malformed: A RDATA is exactly 4 bytes */ + pos += (int)sizeof(struct dns_rr); + response[pos++] = 0x0A; + response[pos++] = 0x00; + response[pos++] = 0x00; + response[pos++] = 0x02; + response[pos++] = 0x55; /* trailing junk beyond the 4-byte address */ + + enqueue_udp_rx(ts, response, (uint16_t)pos, DNS_PORT); + dns_callback(s.dns_udp_sd, CB_EVENT_READABLE, &s); + + /* Not delivered as an answer, and the query not retired. */ + ck_assert_uint_eq(dns_lookup_ip, 0); + ck_assert_int_ne(s.dns_query_type, DNS_QUERY_TYPE_NONE); + ck_assert_uint_ne(s.dns_id, 0); + dns_abort_query(&s); +} +END_TEST START_TEST(test_fifo_push_and_pop) { struct fifo f; struct pkt_desc *desc, *desc2; diff --git a/src/wolfip.c b/src/wolfip.c index 03b9dc53..b9d1855b 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -10122,7 +10122,7 @@ void dns_callback(int dns_sd, uint16_t ev, void *arg) if (s->dns_query_type == DNS_QUERY_TYPE_A && ee16(rr->type) == DNS_A && ee16(rr->class) == DNS_CLASS_IN && - rdlen >= DNS_IPV4_RDATA_LEN) { + rdlen == DNS_IPV4_RDATA_LEN) { uint32_t ip; ip = get_be32((const uint8_t *)buf + pos); if (s->dns_lookup_cb) From 664cb34115a6f38b53a74c57f18d30c46775df6b Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 17 Aug 2026 12:33:32 +0200 Subject: [PATCH 15/21] =?UTF-8?q?F-8567:=20honor=20the=20DHCP=20option=20o?= =?UTF-8?q?verload=20option=20(RFC=202132=20=C2=A79.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The DHCP client bounded its option scans to the standard options field and never recognized option 52 (Overload). A server that legitimately overloads parameters into the reply's sname (value bit 2) and/or file (value bit 1) fields had them silently ignored; in particular dhcp_parse_offer() rejected an OFFER whose server identifier lived in an overloaded field, dead-ending the DHCP transaction. Add a small option-stream iterator that walks the reply's options in RFC order — standard options field, then sname, then file — with independent bounds per region, continuing into an overloaded field only when the options field is exhausted without a terminating option and option 52 selected it. Option 52 values outside 1-3 are malformed. dhcp_parse_offer(), dhcp_msg_type() (non-strict) and dhcp_parse_ack() (strict) now all run their existing per-option logic over the full stream through the iterator. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_dns_dhcp.c | 125 ++++++++++++ src/wolfip.c | 285 +++++++++++++++++----------- 3 files changed, 298 insertions(+), 113 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 53767109..c35d29be 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -523,6 +523,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_dns_send_query_invalid_name); tcase_add_test(tc_utils, test_dns_send_query_rejects_empty_labels); tcase_add_test(tc_utils, test_dns_callback_rejects_a_record_with_wrong_rdlength); + tcase_add_test(tc_utils, test_dhcp_parse_offer_option_overload); tcase_add_test(tc_utils, test_dns_wrapper_apis); tcase_add_test(tc_utils, test_wolfip_static_instance_apis); tcase_add_test(tc_utils, test_wolfip_ip_is_multicast_variants); diff --git a/src/test/unit/unit_tests_dns_dhcp.c b/src/test/unit/unit_tests_dns_dhcp.c index 5780d009..e8f5aa1d 100644 --- a/src/test/unit/unit_tests_dns_dhcp.c +++ b/src/test/unit/unit_tests_dns_dhcp.c @@ -3175,6 +3175,131 @@ START_TEST(test_dns_callback_rejects_a_record_with_wrong_rdlength) dns_abort_query(&s); } END_TEST + +/* RFC 2132 §9.3: option 52 (Overload) marks the reply's sname (value + * bit 2) and/or file (value bit 1) fields as carrying additional + * options. The option stream continues into those fields after the + * standard options field is exhausted, each with its own bounds, so an + * OFFER whose server identifier is overloaded must still be accepted. */ +START_TEST(test_dhcp_parse_offer_option_overload) +{ + struct wolfIP s; + struct dhcp_msg msg; + struct dhcp_option *field_opt; + uint8_t *opt; + uint32_t opt_len; + int ret; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + s.dhcp_xid = 0x1234; + + /* --- Scenario 1: overload = sname (value 2) --- + * Options field: OFFER, mask, overload; no END — the stream + * continues into the sname field, which holds the server id. */ + memset(&msg, 0, sizeof(msg)); + msg.op = BOOT_REPLY; + msg.magic = ee32(DHCP_MAGIC); + msg.xid = ee32(s.dhcp_xid); + msg.yiaddr = ee32(0x0A00000AU); + opt = (uint8_t *)msg.options; + opt[0] = DHCP_OPTION_MSG_TYPE; opt[1] = 1; opt[2] = DHCP_OFFER; + opt += 3; + opt[0] = DHCP_OPTION_SUBNET_MASK; opt[1] = 4; + opt[2] = 0xFF; opt[3] = 0xFF; opt[4] = 0xFF; opt[5] = 0x00; + opt += 6; + opt[0] = 52 /* DHCP_OPTION_OVERLOAD */; opt[1] = 1; opt[2] = 2; + opt += 3; + opt_len = (uint32_t)(opt - (uint8_t *)msg.options); + field_opt = (struct dhcp_option *)msg.sname; + field_opt->code = DHCP_OPTION_SERVER_ID; + field_opt->len = 4; + field_opt->data[0] = 0x0A; field_opt->data[1] = 0x00; + field_opt->data[2] = 0x00; field_opt->data[3] = 0x64; + field_opt = (struct dhcp_option *)((uint8_t *)field_opt + 6); + field_opt->code = DHCP_OPTION_END; + field_opt->len = 0; + + ret = dhcp_parse_offer(&s, &msg, DHCP_HEADER_LEN + opt_len); + ck_assert_int_eq(ret, 0); + ck_assert_uint_eq(s.dhcp_server_ip, 0x0A000064U); + ck_assert_uint_eq(s.dhcp_ip, 0x0A00000AU); + ck_assert_int_eq(s.dhcp_state, DHCP_REQUEST_SENT); + ck_assert_uint_eq(s.ipconf[TEST_PRIMARY_IF].mask, 0xFFFFFF00U); + + /* --- Scenario 2: overload = file (value 1) --- */ + memset(&s, 0, sizeof(s)); + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + s.dhcp_xid = 0x1234; + memset(&msg, 0, sizeof(msg)); + msg.op = BOOT_REPLY; + msg.magic = ee32(DHCP_MAGIC); + msg.xid = ee32(s.dhcp_xid); + msg.yiaddr = ee32(0x0A00000AU); + opt = (uint8_t *)msg.options; + opt[0] = DHCP_OPTION_MSG_TYPE; opt[1] = 1; opt[2] = DHCP_OFFER; + opt += 3; + opt[0] = 52 /* DHCP_OPTION_OVERLOAD */; opt[1] = 1; opt[2] = 1; + opt += 3; + opt_len = (uint32_t)(opt - (uint8_t *)msg.options); + field_opt = (struct dhcp_option *)msg.file; + field_opt->code = DHCP_OPTION_SERVER_ID; + field_opt->len = 4; + field_opt->data[0] = 0x0A; field_opt->data[1] = 0x00; + field_opt->data[2] = 0x00; field_opt->data[3] = 0x64; + field_opt = (struct dhcp_option *)((uint8_t *)field_opt + 6); + field_opt->code = DHCP_OPTION_END; + field_opt->len = 0; + + ret = dhcp_parse_offer(&s, &msg, DHCP_HEADER_LEN + opt_len); + ck_assert_int_eq(ret, 0); + ck_assert_uint_eq(s.dhcp_server_ip, 0x0A000064U); + + /* --- Scenario 3: overload = both (value 3): the stream runs + * options -> sname -> file; the mask is in sname, the server id in + * file. */ + memset(&s, 0, sizeof(s)); + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + s.dhcp_xid = 0x1234; + memset(&msg, 0, sizeof(msg)); + msg.op = BOOT_REPLY; + msg.magic = ee32(DHCP_MAGIC); + msg.xid = ee32(s.dhcp_xid); + msg.yiaddr = ee32(0x0A00000AU); + opt = (uint8_t *)msg.options; + opt[0] = DHCP_OPTION_MSG_TYPE; opt[1] = 1; opt[2] = DHCP_OFFER; + opt += 3; + opt[0] = 52 /* DHCP_OPTION_OVERLOAD */; opt[1] = 1; opt[2] = 3; + opt += 3; + opt_len = (uint32_t)(opt - (uint8_t *)msg.options); + field_opt = (struct dhcp_option *)msg.sname; + field_opt->code = DHCP_OPTION_SUBNET_MASK; + field_opt->len = 4; + field_opt->data[0] = 0xFF; field_opt->data[1] = 0xFF; + field_opt->data[2] = 0xFF; field_opt->data[3] = 0x00; + field_opt = (struct dhcp_option *)((uint8_t *)field_opt + 6); + field_opt->code = 0; /* pad until the file field */ + field_opt->len = 0; + field_opt = (struct dhcp_option *)msg.file; + field_opt->code = DHCP_OPTION_SERVER_ID; + field_opt->len = 4; + field_opt->data[0] = 0x0A; field_opt->data[1] = 0x00; + field_opt->data[2] = 0x00; field_opt->data[3] = 0x64; + field_opt = (struct dhcp_option *)((uint8_t *)field_opt + 6); + field_opt->code = DHCP_OPTION_END; + field_opt->len = 0; + + ret = dhcp_parse_offer(&s, &msg, DHCP_HEADER_LEN + opt_len); + ck_assert_int_eq(ret, 0); + ck_assert_uint_eq(s.dhcp_server_ip, 0x0A000064U); + ck_assert_uint_eq(s.ipconf[TEST_PRIMARY_IF].mask, 0xFFFFFF00U); +} +END_TEST START_TEST(test_fifo_push_and_pop) { struct fifo f; struct pkt_desc *desc, *desc2; diff --git a/src/wolfip.c b/src/wolfip.c index b9d1855b..c9b553ef 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -1061,6 +1061,7 @@ static int wolfIP_filter_notify_icmp(enum wolfIP_filter_reason reason, #define DHCP_SERVER_PORT 67 #define DHCP_CLIENT_PORT 68 #define DHCP_OPTION_MSG_TYPE 53 +#define DHCP_OPTION_OVERLOAD 52 #define DHCP_OPTION_SUBNET_MASK 1 #define DHCP_OPTION_ROUTER 3 #define DHCP_OPTION_DNS 6 @@ -8092,10 +8093,120 @@ static void dhcp_deconfigure_lease(struct wolfIP *s) */ #define DHCP_DEFAULT_24BIT_NETMASK (0xFFFFFF00u) +/* Iterator over a DHCP reply's option stream. + * + * RFC 2132 §9.3: option 52 (Overload) tells the client that the reply's + * sname (value bit 2) and/or file (value bit 1) fields carry additional + * options, interpreted after the standard options field is exhausted. + * The stream is therefore scanned options -> sname -> file (wire order), + * each region with its own bounds, and ends at option 255 or when the + * last active region runs out. Non-overloaded sname/file fields hold + * plain strings (TFTP server, bootfile) and are never scanned. */ +struct dhcp_opt_stream { + uint8_t *ptr; + uint8_t *end; + uint8_t *region_sname; + uint8_t *region_sname_end; + uint8_t *region_file; + uint8_t *region_file_end; + uint8_t region; /* 0 options, 1 sname, 2 file */ + uint8_t overload; + int strict; /* -1 on malformed, or treat as end of stream */ +}; + +static void dhcp_opt_stream_init(struct dhcp_opt_stream *st, + struct dhcp_msg *msg, uint32_t msg_len, + int strict) +{ + uint32_t opt_space = msg_len - DHCP_HEADER_LEN; + if (opt_space > sizeof(msg->options)) + opt_space = sizeof(msg->options); + st->ptr = (uint8_t *)msg->options; + st->end = st->ptr + opt_space; + st->region_sname = (uint8_t *)msg->sname; + st->region_sname_end = st->region_sname + sizeof(msg->sname); + st->region_file = (uint8_t *)msg->file; + st->region_file_end = st->region_file + sizeof(msg->file); + st->region = 0; + st->overload = 0; + st->strict = strict; +} + +/* Return 1 with the out-params set to the next option (option 255 is + * delivered as code 255, len 0), 0 at end of stream (option 255 consumed + * or every active region exhausted without END), or -1 on malformed + * options in strict mode (non-strict callers get 0 instead). */ +static int dhcp_opt_stream_next(struct dhcp_opt_stream *st, uint8_t *code, + uint8_t *len, uint8_t **data) +{ + while (1) { + uint8_t c; + uint8_t l; + if (st->ptr + 1 > st->end) + goto region_end; + c = st->ptr[0]; + if (c == DHCP_OPTION_END) { + *code = c; + *len = 0; + *data = st->ptr; + st->ptr = st->end; /* stream ends here */ + return 1; + } + if (c == 0) { /* Pad */ + st->ptr++; + continue; + } + if (st->ptr + 2 > st->end) + goto region_end; + l = st->ptr[1]; + if (st->ptr + 2 + l > st->end) + goto region_end; + if (st->region == 0 && c == DHCP_OPTION_OVERLOAD) { + /* RFC 2132 §9.3: length 1, values 1 (file), 2 (sname), 3. */ + if (l != 1 || st->ptr[2] < 1 || st->ptr[2] > 3) + goto region_end; + st->overload = st->ptr[2]; + } + *code = c; + *len = l; + /* Points at the option's code byte, so callers may cast it to + * struct dhcp_option * (whose data[] member starts 2 bytes in). */ + *data = st->ptr; + st->ptr += 2 + l; + return 1; +region_end: + /* Region exhausted without END: continue into the overloaded + * fields (sname, then file) if option 52 selected them. */ + if (st->region == 0) { + if (st->overload & 2) { + st->region = 1; + st->ptr = st->region_sname; + st->end = st->region_sname_end; + continue; + } + if (st->overload & 1) { + st->region = 2; + st->ptr = st->region_file; + st->end = st->region_file_end; + continue; + } + } else if (st->region == 1) { + if (st->overload & 1) { + st->region = 2; + st->ptr = st->region_file; + st->end = st->region_file_end; + continue; + } + } + if (st->strict) + return -1; + return 0; + } +} + static int dhcp_parse_offer(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_len) { - uint8_t *opt = (uint8_t *)msg->options; - uint8_t *opt_end; + struct dhcp_opt_stream st; int saw_end = 0; int saw_server_id = 0; uint32_t ip; @@ -8109,66 +8220,48 @@ static int dhcp_parse_offer(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg return -1; if (ee32(msg->xid) != s->dhcp_xid) return -1; - if (msg_len - DHCP_HEADER_LEN > sizeof(msg->options)) - opt_end = (uint8_t *)msg->options + sizeof(msg->options); - else - opt_end = (uint8_t *)msg->options + (msg_len - DHCP_HEADER_LEN); - while (opt < opt_end) { + dhcp_opt_stream_init(&st, msg, msg_len, 1); + while (1) { uint8_t code; uint8_t len; - if (opt + 1 > opt_end) + uint8_t *data; + int r = dhcp_opt_stream_next(&st, &code, &len, &data); + if (r < 0) + return -1; + if (r == 0) break; - code = opt[0]; if (code == DHCP_OPTION_END) { saw_end = 1; break; } - if (code == 0) { /* Pad */ - opt++; - continue; - } - if (opt + 2 > opt_end) - return -1; - len = opt[1]; - if (opt + 2 + len > opt_end) - return -1; if (code == DHCP_OPTION_MSG_TYPE) { if (len != 1) return -1; - if (opt[2] == DHCP_OFFER) { - opt += 2 + len; - saw_end = 0; - while (opt < opt_end) { - struct dhcp_option *inner; - if (opt + 1 > opt_end) + if (data[2] == DHCP_OFFER) { + while (1) { + uint8_t *idata; + r = dhcp_opt_stream_next(&st, &code, &len, &idata); + if (r < 0) + return -1; + if (r == 0) break; - code = opt[0]; if (code == DHCP_OPTION_END) { saw_end = 1; break; } - if (code == 0) { - opt++; - continue; - } - if (opt + 2 > opt_end) - return -1; - len = opt[1]; - if (opt + 2 + len > opt_end) - return -1; - inner = (struct dhcp_option *)opt; if (code == DHCP_OPTION_SERVER_ID) { if (len < 4) return -1; - s->dhcp_server_ip = DHCP_OPT_data_to_u32(inner); + s->dhcp_server_ip = + DHCP_OPT_data_to_u32((struct dhcp_option *)idata); saw_server_id = 1; } if (code == DHCP_OPTION_SUBNET_MASK) { if (len < 4) return -1; - netmask = DHCP_OPT_data_to_u32(inner); + netmask = + DHCP_OPT_data_to_u32((struct dhcp_option *)idata); } - opt += 2 + len; } if (!saw_end || !saw_server_id) return -1; @@ -8183,7 +8276,6 @@ static int dhcp_parse_offer(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg return 0; } } - opt += 2 + len; } if (!saw_end) return -1; @@ -8198,8 +8290,10 @@ static int dhcp_parse_offer(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg /* Return the DHCP message type from a validated message, or -1 on error. */ static int dhcp_msg_type(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_len) { - uint8_t *opt = (uint8_t *)msg->options; - uint8_t *opt_end; + struct dhcp_opt_stream st; + uint8_t code; + uint8_t len; + uint8_t *data; int msg_type = -1; int saw_server_id = 0; uint32_t server_id = 0; @@ -8211,31 +8305,18 @@ static int dhcp_msg_type(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_le return -1; if (msg->op != BOOT_REPLY) return -1; - if (msg_len - DHCP_HEADER_LEN > sizeof(msg->options)) - opt_end = (uint8_t *)msg->options + sizeof(msg->options); - else - opt_end = (uint8_t *)msg->options + (msg_len - DHCP_HEADER_LEN); - while (opt < opt_end) { - uint8_t code = opt[0]; - uint8_t len; + /* Non-strict: a truncated option stream ends the scan instead of + * failing the message. */ + dhcp_opt_stream_init(&st, msg, msg_len, 0); + while (dhcp_opt_stream_next(&st, &code, &len, &data) == 1) { if (code == DHCP_OPTION_END) break; - if (code == 0) { - opt++; - continue; - } - if (opt + 2 > opt_end) - break; - len = opt[1]; - if (opt + 2 + len > opt_end) - break; if (code == DHCP_OPTION_MSG_TYPE && len == 1) { - msg_type = opt[2]; + msg_type = data[2]; } else if (code == DHCP_OPTION_SERVER_ID && len >= 4) { - server_id = DHCP_OPT_data_to_u32((struct dhcp_option *)opt); + server_id = DHCP_OPT_data_to_u32((struct dhcp_option *)data); saw_server_id = 1; } - opt += 2 + len; } /* Reject a reply that does not carry the server identifier of the * server we committed to during the OFFER phase. */ @@ -8247,8 +8328,7 @@ static int dhcp_msg_type(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_le static int dhcp_parse_ack(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_len) { - uint8_t *opt = (uint8_t *)msg->options; - uint8_t *opt_end; + struct dhcp_opt_stream st; int saw_end = 0; int saw_server_id = 0; struct ipconf *primary = wolfIP_primary_ipconf(s); @@ -8263,99 +8343,79 @@ static int dhcp_parse_ack(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_l return -1; if (ee32(msg->xid) != s->dhcp_xid) return -1; - if (msg_len - DHCP_HEADER_LEN > sizeof(msg->options)) - opt_end = (uint8_t *)msg->options + sizeof(msg->options); - else - opt_end = (uint8_t *)msg->options + (msg_len - DHCP_HEADER_LEN); - while (opt < opt_end) { + dhcp_opt_stream_init(&st, msg, msg_len, 1); + while (1) { uint8_t code; uint8_t len; - if (opt + 1 > opt_end) + uint8_t *data; + int r = dhcp_opt_stream_next(&st, &code, &len, &data); + if (r < 0) + return -1; + if (r == 0) break; - code = opt[0]; if (code == DHCP_OPTION_END) { saw_end = 1; break; } - if (code == 0) { /* Pad */ - opt++; - continue; - } - if (opt + 2 > opt_end) - return -1; - len = opt[1]; - if (opt + 2 + len > opt_end) - return -1; if (code == DHCP_OPTION_MSG_TYPE) { if (len != 1) return -1; - if (opt[2] == DHCP_ACK) { - opt += 2 + len; - saw_end = 0; - while (opt < opt_end) { - struct dhcp_option *inner; - uint32_t data; - if (opt + 1 > opt_end) + if (data[2] == DHCP_ACK) { + while (1) { + uint8_t *idata; + uint32_t val; + r = dhcp_opt_stream_next(&st, &code, &len, &idata); + if (r < 0) + return -1; + if (r == 0) break; - code = opt[0]; if (code == DHCP_OPTION_END) { saw_end = 1; break; } - if (code == 0) { - opt++; - continue; - } - if (opt + 2 > opt_end) - return -1; - len = opt[1]; - if (opt + 2 + len > opt_end) - return -1; - inner = (struct dhcp_option *)opt; if (code == DHCP_OPTION_SERVER_ID) { if (len < 4) return -1; - data = DHCP_OPT_data_to_u32(inner); + val = DHCP_OPT_data_to_u32((struct dhcp_option *)idata); /* Reject ACK from a server other than the one * we committed to during the OFFER phase. */ - if (s->dhcp_server_ip != 0 && data != s->dhcp_server_ip) + if (s->dhcp_server_ip != 0 && val != s->dhcp_server_ip) return -1; - s->dhcp_server_ip = data; + s->dhcp_server_ip = val; saw_server_id = 1; } else if (primary && code == DHCP_OPTION_OFFER_IP) { if (len < 4) return -1; - data = DHCP_OPT_data_to_u32(inner); - primary->ip = data; + val = DHCP_OPT_data_to_u32((struct dhcp_option *)idata); + primary->ip = val; } else if (primary && code == DHCP_OPTION_SUBNET_MASK) { if (len < 4) return -1; - data = DHCP_OPT_data_to_u32(inner); - primary->mask = data; + val = DHCP_OPT_data_to_u32((struct dhcp_option *)idata); + primary->mask = val; } else if (primary && code == DHCP_OPTION_ROUTER) { if (len < 4) return -1; - data = DHCP_OPT_data_to_u32(inner); - primary->gw = data; + val = DHCP_OPT_data_to_u32((struct dhcp_option *)idata); + primary->gw = val; } else if ((code == DHCP_OPTION_DNS) && (s->dns_server == 0)) { if (len < 4) return -1; - data = DHCP_OPT_data_to_u32(inner); - s->dns_server = data; + val = DHCP_OPT_data_to_u32((struct dhcp_option *)idata); + s->dns_server = val; } else if (code == DHCP_OPTION_LEASE_TIME) { if (len < 4) return -1; - lease_s = DHCP_OPT_data_to_u32(inner); + lease_s = DHCP_OPT_data_to_u32((struct dhcp_option *)idata); } else if (code == DHCP_OPTION_RENEWAL_TIME) { if (len < 4) return -1; - renew_s = DHCP_OPT_data_to_u32(inner); + renew_s = DHCP_OPT_data_to_u32((struct dhcp_option *)idata); } else if (code == DHCP_OPTION_REBIND_TIME) { if (len < 4) return -1; - rebind_s = DHCP_OPT_data_to_u32(inner); + rebind_s = DHCP_OPT_data_to_u32((struct dhcp_option *)idata); } - opt += 2 + len; } if (!saw_end) return -1; @@ -8372,9 +8432,8 @@ static int dhcp_parse_ack(struct wolfIP *s, struct dhcp_msg *msg, uint32_t msg_l return 0; } } - break; + break; /* the message type was seen; nothing else to scan */ } - opt += 2 + len; } return -1; } From c8bcb31c8868bf751ab488d139f0e84bbb166567 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 17 Aug 2026 12:40:39 +0200 Subject: [PATCH 16/21] =?UTF-8?q?F-8568:=20schedule=20the=20first=20DHCPDI?= =?UTF-8?q?SCOVER=20retry=20at=204=20s=20=C2=B1=201=20s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RFC 2131 section 4.1's 10 Mb/s Ethernet example puts the first DHCPDISCOVER retransmission at 4 seconds, randomized uniformly by plus or minus 1 second. The client scheduled it at 2 s plus 0-199 ms of jitter, re-probing twice as fast as the guidance intends. Move the discover base to 4000 ms and jitter it uniformly over -1000/+1000 ms around the (per-attempt doubled) backoff. The jitter now lives at the call sites: the discover path uses the RFC 4 s ± 1 s window, and the request/renew/rebind retry scheduler keeps its existing 0-199 ms jitter exactly as before. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_dns_dhcp.c | 24 ++++++++++++++++++++++++ src/wolfip.c | 25 ++++++++++++++++++------- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index c35d29be..f0f8a3a1 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -524,6 +524,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_dns_send_query_rejects_empty_labels); tcase_add_test(tc_utils, test_dns_callback_rejects_a_record_with_wrong_rdlength); tcase_add_test(tc_utils, test_dhcp_parse_offer_option_overload); + tcase_add_test(tc_utils, test_dhcp_discover_first_retry_delay_rfc2131); tcase_add_test(tc_utils, test_dns_wrapper_apis); tcase_add_test(tc_utils, test_wolfip_static_instance_apis); tcase_add_test(tc_utils, test_wolfip_ip_is_multicast_variants); diff --git a/src/test/unit/unit_tests_dns_dhcp.c b/src/test/unit/unit_tests_dns_dhcp.c index e8f5aa1d..ab254f09 100644 --- a/src/test/unit/unit_tests_dns_dhcp.c +++ b/src/test/unit/unit_tests_dns_dhcp.c @@ -1136,6 +1136,30 @@ START_TEST(test_dhcp_messages_set_secs_from_process_start) } END_TEST +START_TEST(test_dhcp_discover_first_retry_delay_rfc2131) +{ + struct wolfIP s; + uint64_t delay; + + wolfIP_init(&s); + mock_link_init(&s); + s.dhcp_xid = 1U; + s.last_tick = 1000U; + s.dhcp_udp_sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, + WI_IPPROTO_UDP); + ck_assert_int_gt(s.dhcp_udp_sd, 0); + + ck_assert_int_eq(dhcp_send_discover(&s), 0); + ck_assert_int_ne(s.dhcp_timer, NO_TIMER); + delay = find_timer_expiry(&s, s.dhcp_timer) - s.last_tick; + /* RFC 2131 §4.1 (10 Mb/s Ethernet example): first retransmission + * at 4 s, randomized uniformly by plus or minus 1 s. */ + ck_assert_uint_ge(delay, 3000U); + ck_assert_uint_le(delay, 5000U); +} +END_TEST + + START_TEST(test_sock_connect_tcp_src_port_low) { struct wolfIP s; diff --git a/src/wolfip.c b/src/wolfip.c index c9b553ef..3e7f46d5 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -1072,7 +1072,10 @@ static int wolfIP_filter_notify_icmp(enum wolfIP_filter_reason reason, #define DHCP_OPTION_REBIND_TIME 59 #define DHCP_OPTION_OFFER_IP 50 #define DHCP_OPTION_END 0xFF -#define DHCP_DISCOVER_TIMEOUT 2000 +/* RFC 2131 §4.1 (10 Mb/s Ethernet example): the first DHCPDISCOVER + * retransmission is due 4 s after the initial send. */ +#define DHCP_DISCOVER_TIMEOUT 4000 +#define DHCP_DISCOVER_JITTER_MS 1000U /* uniform ±1 s */ #ifndef DHCP_DISCOVER_RETRIES #define DHCP_DISCOVER_RETRIES 3 #endif @@ -7893,9 +7896,10 @@ static void dhcp_schedule_timer_at(struct wolfIP *s, uint64_t when) } /* Exponential-backoff retransmission delay: double the base timeout for each - * prior attempt (dhcp_timeout_count), saturating at DHCP_BACKOFF_MAX_MS, plus - * the existing small jitter. The shift is clamped first because renew/rebind do - * not cap dhcp_timeout_count, so it can grow past the point of UB. */ + * prior attempt (dhcp_timeout_count), saturating at DHCP_BACKOFF_MAX_MS. + * Callers add their own jitter. The shift is clamped first because + * renew/rebind do not cap dhcp_timeout_count, so it can grow past the point + * of UB. */ static uint64_t dhcp_backoff_delay(const struct wolfIP *s, uint32_t base_ms) { uint32_t count = s ? s->dhcp_timeout_count : 0; @@ -7906,7 +7910,7 @@ static uint64_t dhcp_backoff_delay(const struct wolfIP *s, uint32_t base_ms) delay = (uint64_t)base_ms << count; if (delay > DHCP_BACKOFF_MAX_MS) delay = DHCP_BACKOFF_MAX_MS; - return delay + (wolfIP_getrandom() % 200U); + return delay; } static void dhcp_schedule_retry_timer(struct wolfIP *s, uint64_t deadline) @@ -7915,7 +7919,8 @@ static void dhcp_schedule_retry_timer(struct wolfIP *s, uint64_t deadline) if (!s) return; - next = s->last_tick + dhcp_backoff_delay(s, DHCP_REQUEST_TIMEOUT); + next = s->last_tick + dhcp_backoff_delay(s, DHCP_REQUEST_TIMEOUT) + + (wolfIP_getrandom() % 200U); if (deadline != 0 && next > deadline) next = deadline; dhcp_schedule_timer_at(s, next); @@ -8647,7 +8652,13 @@ static int dhcp_send_discover(struct wolfIP *s) dhcp_schedule_timer_at(s, retry_at); return ret; } - dhcp_schedule_timer_at(s, s->last_tick + dhcp_backoff_delay(s, DHCP_DISCOVER_TIMEOUT)); + /* RFC 2131 §4.1 (10 Mb/s Ethernet example): the first retransmission + * is 4 s, randomized uniformly by plus or minus 1 s; the base doubles + * per attempt. */ + dhcp_schedule_timer_at(s, s->last_tick + + dhcp_backoff_delay(s, DHCP_DISCOVER_TIMEOUT) - + DHCP_DISCOVER_JITTER_MS + + (wolfIP_getrandom() % (2U * DHCP_DISCOVER_JITTER_MS + 1U))); s->dhcp_state = DHCP_DISCOVER_SENT; return 0; } From 11ff6131a8adc7578f5f0b306b92d2f4b68b2e73 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 17 Aug 2026 12:57:40 +0200 Subject: [PATCH 17/21] F-6941: consult a freshly registered filter callback for all reasons wolfIP_filter_set_callback() only stored the callback; with the reason masks at their zero default, wolfIP_filter_dispatch() returned allow for every packet and socket event, so a filter installed without a separate mask call was silently never invoked. Track whether a reason mask has been explicitly configured since the callback was registered. While every mask is still at its zero default, dispatch uses the full reason set, so a freshly installed filter is consulted for everything; the first explicit mask configuration (including an explicit zero) switches to the configured reasons only. Document the two-step model and the fail-open default in wolfip-filter.h. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_api.c | 43 ++++++++++++++++++++++++++++++++++ src/wolfip.c | 17 ++++++++++++++ wolfip-filter.h | 20 ++++++++++++++++ 4 files changed, 81 insertions(+) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index f0f8a3a1..1c06359b 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -177,6 +177,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_filter_notify_tcp_metadata); tcase_add_test(tc_utils, test_filter_dispatch_no_callback); tcase_add_test(tc_utils, test_filter_dispatch_mask_not_set); + tcase_add_test(tc_utils, test_filter_fresh_callback_consulted_before_mask_configured); tcase_add_test(tc_utils, test_filter_dispatch_lock_blocks); tcase_add_test(tc_utils, test_filter_dispatch_meta_null_initializes); tcase_add_test(tc_utils, test_filter_socket_event_unknown_proto); diff --git a/src/test/unit/unit_tests_api.c b/src/test/unit/unit_tests_api.c index 0c1edc31..f69b84fc 100644 --- a/src/test/unit/unit_tests_api.c +++ b/src/test/unit/unit_tests_api.c @@ -233,6 +233,49 @@ START_TEST(test_filter_dispatch_mask_not_set) } END_TEST +/* A freshly registered callback must not fail open: while every reason + * mask is still at its zero default, dispatch consults the callback for + * every reason; the first explicit mask configuration (even a zero mask) + * switches to the configured reasons only. */ +START_TEST(test_filter_fresh_callback_consulted_before_mask_configured) +{ + struct wolfIP s; + struct wolfIP_filter_metadata meta; + + memset(&s, 0, sizeof(s)); + wolfIP_filter_set_callback(NULL, NULL); + wolfIP_filter_set_mask(0); + wolfIP_filter_set_eth_mask(0); + wolfIP_filter_set_ip_mask(0); + wolfIP_filter_set_tcp_mask(0); + wolfIP_filter_set_udp_mask(0); + wolfIP_filter_set_icmp_mask(0); + + filter_cb_calls = 0; + wolfIP_filter_set_callback(test_filter_cb, NULL); + + wolfIP_filter_init_metadata(&meta); + meta.ip_proto = WOLFIP_FILTER_PROTO_TCP; + wolfIP_filter_dispatch(WOLFIP_FILT_RECEIVING, &s, 0, NULL, 0, &meta); + ck_assert_int_eq(filter_cb_calls, 1); + + /* The first explicit mask configuration selects the reasons. */ + wolfIP_filter_set_mask(WOLFIP_FILT_MASK(WOLFIP_FILT_CONNECTING)); + filter_cb_calls = 0; + wolfIP_filter_dispatch(WOLFIP_FILT_RECEIVING, &s, 0, NULL, 0, &meta); + ck_assert_int_eq(filter_cb_calls, 0); + + /* An explicit empty mask silences the filter. */ + wolfIP_filter_set_mask(0); + filter_cb_calls = 0; + wolfIP_filter_dispatch(WOLFIP_FILT_RECEIVING, &s, 0, NULL, 0, &meta); + ck_assert_int_eq(filter_cb_calls, 0); + + wolfIP_filter_set_callback(NULL, NULL); + wolfIP_filter_set_mask(0); +} +END_TEST + START_TEST(test_filter_dispatch_lock_blocks) { struct wolfIP s; diff --git a/src/wolfip.c b/src/wolfip.c index 3e7f46d5..8328c6f3 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -838,41 +838,56 @@ static uint32_t wolfip_filter_mask_tcp; static uint32_t wolfip_filter_mask_udp; static uint32_t wolfip_filter_mask_icmp; static int wolfip_filter_lock; +/* Set while a callback is registered and every reason mask is still at its + * zero default; cleared by the first explicit mask configuration. Until + * cleared, dispatch consults the callback for every reason so a freshly + * installed filter cannot fail open. */ +static int wolfip_filter_mask_touched; void wolfIP_filter_set_callback(wolfIP_filter_cb cb, void *arg) { wolfip_filter_cb = cb; wolfip_filter_arg = arg; + wolfip_filter_mask_touched = (cb == NULL) || (wolfip_filter_mask != 0) || + (wolfip_filter_mask_eth != 0) || (wolfip_filter_mask_ip != 0) || + (wolfip_filter_mask_tcp != 0) || (wolfip_filter_mask_udp != 0) || + (wolfip_filter_mask_icmp != 0); } void wolfIP_filter_set_mask(uint32_t mask) { wolfip_filter_mask = mask; + wolfip_filter_mask_touched = 1; } void wolfIP_filter_set_eth_mask(uint32_t mask) { wolfip_filter_mask_eth = mask; + wolfip_filter_mask_touched = 1; } void wolfIP_filter_set_ip_mask(uint32_t mask) { wolfip_filter_mask_ip = mask; + wolfip_filter_mask_touched = 1; } void wolfIP_filter_set_tcp_mask(uint32_t mask) { wolfip_filter_mask_tcp = mask; + wolfip_filter_mask_touched = 1; } void wolfIP_filter_set_udp_mask(uint32_t mask) { wolfip_filter_mask_udp = mask; + wolfip_filter_mask_touched = 1; } void wolfIP_filter_set_icmp_mask(uint32_t mask) { wolfip_filter_mask_icmp = mask; + wolfip_filter_mask_touched = 1; } uint32_t wolfIP_filter_get_mask(void) @@ -918,6 +933,8 @@ static int wolfIP_filter_dispatch(enum wolfIP_filter_reason reason, mask = wolfip_filter_mask; else mask = wolfIP_filter_mask_for_proto(meta->ip_proto); + if (!wolfip_filter_mask_touched) + mask = ~0U; if ((mask & (1U << reason)) == 0) return 0; if (wolfip_filter_lock) diff --git a/wolfip-filter.h b/wolfip-filter.h index 64d0a515..a1a956d8 100644 --- a/wolfip-filter.h +++ b/wolfip-filter.h @@ -66,7 +66,27 @@ struct wolfIP_filter_event { typedef int (*wolfIP_filter_cb)(void *arg, const struct wolfIP_filter_event *event); +/* + * Install the filter callback. The callback is only consulted for the + * reasons selected in the reason masks; a reason bit left clear in a + * configured mask means the stack silently allows that traffic (no + * callback, no error). To keep a freshly installed filter from failing + * open, a callback registered while every reason mask is still zero is + * consulted for all reasons until the first explicit mask configuration + * (wolfIP_filter_set_mask() or any per-proto setter); afterwards only + * the configured reasons are consulted. Passing a NULL callback + * uninstalls it (masks are left as configured). + */ void wolfIP_filter_set_callback(wolfIP_filter_cb cb, void *arg); + +/* + * Select the reasons the callback is consulted for (all protocols). + * This is the first explicit mask configuration: it ends the + * all-reasons default of a freshly registered callback. Once configured, + * an all-zero mask disables all notifications and silently allows every + * packet and socket event. Per-proto masks (eth/ip/tcp/udp/icmp) take + * precedence over this one when non-zero. + */ void wolfIP_filter_set_mask(uint32_t mask); void wolfIP_filter_set_eth_mask(uint32_t mask); void wolfIP_filter_set_ip_mask(uint32_t mask); From 16f2eb2503cad68956784cffdd5150ec323800cc Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 17 Aug 2026 14:45:07 +0200 Subject: [PATCH 18/21] F-8513: keep IP_MULTICAST_IF pinned egress at flush time filling the IP header at enqueue time moved the egress interface derivation fully to flush_datagram_tx(), where the route lookup for a multicast group (no matching route) falls back to the first non-loopback interface and silently overrode the per-socket IP_MULTICAST_IF pin. The frame then egressed, and carried its source MAC, from the wrong interface (regression in test_multicast_if_pins_egress_interface). When a UDP multicast descriptor belongs to a socket with a pinned multicast interface, use that interface for the egress and take the group address as the next hop (the MAC is derived from the group address); otherwise route as before. --- src/wolfip.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/wolfip.c b/src/wolfip.c index 8328c6f3..7c8d9b6f 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -10683,7 +10683,18 @@ static void flush_datagram_tx(struct wolfIP *s, struct tsocket *socks, * not the socket's current one. */ ip4 desc_dst = ee32(ip->dst); #ifdef ETHERNET - ip4 nexthop = wolfIP_select_nexthop_ex(s, &tx_if, desc_dst); + ip4 nexthop; +#ifdef IP_MULTICAST + if (is_udp && wolfIP_ip_is_multicast(desc_dst) && + t->sock.udp.mcast_if_set) { + /* IP_MULTICAST_IF pins this socket's multicast egress; the + * route lookup's no-route fallback would move the frame to + * another interface. */ + tx_if = t->sock.udp.mcast_if_idx; + nexthop = desc_dst; + } else +#endif + nexthop = wolfIP_select_nexthop_ex(s, &tx_if, desc_dst); if (wolfIP_is_loopback_if(tx_if)) { struct wolfIP_ll_dev *loop = wolfIP_ll_at(s, tx_if); if (loop) From 8c23611b64e25868916915f4c4fbbfc4abecb60d Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 17 Aug 2026 14:55:58 +0200 Subject: [PATCH 19/21] F-6941: honor explicit mask configuration made before installing the callback wolfIP_filter_set_callback() recomputed the all-reasons default flag from the current mask values, so an explicit configuration to zero made before installing the callback (all values zero) indistinguishable from no configuration at all: the flag reset and the callback was consulted for every reason, ignoring the explicit masks. Track explicit configuration as an event instead of inferring it from values: any mask setter (including an explicit zero) sets the flag, and only uninstalling the callback clears it. Install no longer recomputes anything. Update the header docs to state the event-based semantics. --- src/test/unit/unit_tests_api.c | 29 +++++++++++++++++++++-------- src/wolfip.c | 12 +++++------- wolfip-filter.h | 23 ++++++++++++----------- 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/src/test/unit/unit_tests_api.c b/src/test/unit/unit_tests_api.c index f69b84fc..91e53461 100644 --- a/src/test/unit/unit_tests_api.c +++ b/src/test/unit/unit_tests_api.c @@ -233,29 +233,33 @@ START_TEST(test_filter_dispatch_mask_not_set) } END_TEST -/* A freshly registered callback must not fail open: while every reason - * mask is still at its zero default, dispatch consults the callback for - * every reason; the first explicit mask configuration (even a zero mask) - * switches to the configured reasons only. */ +/* A freshly installed callback must not fail open: with no explicit mask + * configuration since the last uninstall, dispatch consults the callback + * for every reason; any explicit mask configuration (even a zero mask, + * even one made before installing the callback) switches to the + * configured reasons only. */ START_TEST(test_filter_fresh_callback_consulted_before_mask_configured) { struct wolfIP s; struct wolfIP_filter_metadata meta; memset(&s, 0, sizeof(s)); - wolfIP_filter_set_callback(NULL, NULL); + /* Clear stale masks from other tests, then return to the initial + * state: uninstall resets the all-reasons default. */ wolfIP_filter_set_mask(0); wolfIP_filter_set_eth_mask(0); wolfIP_filter_set_ip_mask(0); wolfIP_filter_set_tcp_mask(0); wolfIP_filter_set_udp_mask(0); wolfIP_filter_set_icmp_mask(0); - - filter_cb_calls = 0; - wolfIP_filter_set_callback(test_filter_cb, NULL); + wolfIP_filter_set_callback(NULL, NULL); wolfIP_filter_init_metadata(&meta); meta.ip_proto = WOLFIP_FILTER_PROTO_TCP; + + /* Fresh install, no mask configuration: consulted for all reasons. */ + filter_cb_calls = 0; + wolfIP_filter_set_callback(test_filter_cb, NULL); wolfIP_filter_dispatch(WOLFIP_FILT_RECEIVING, &s, 0, NULL, 0, &meta); ck_assert_int_eq(filter_cb_calls, 1); @@ -271,6 +275,15 @@ START_TEST(test_filter_fresh_callback_consulted_before_mask_configured) wolfIP_filter_dispatch(WOLFIP_FILT_RECEIVING, &s, 0, NULL, 0, &meta); ck_assert_int_eq(filter_cb_calls, 0); + /* Explicit zero configured *before* installing the callback must be + * honored, not overridden by the all-reasons default. */ + wolfIP_filter_set_callback(NULL, NULL); + wolfIP_filter_set_mask(0); + filter_cb_calls = 0; + wolfIP_filter_set_callback(test_filter_cb, NULL); + wolfIP_filter_dispatch(WOLFIP_FILT_RECEIVING, &s, 0, NULL, 0, &meta); + ck_assert_int_eq(filter_cb_calls, 0); + wolfIP_filter_set_callback(NULL, NULL); wolfIP_filter_set_mask(0); } diff --git a/src/wolfip.c b/src/wolfip.c index 7c8d9b6f..7b6ec7b1 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -838,20 +838,18 @@ static uint32_t wolfip_filter_mask_tcp; static uint32_t wolfip_filter_mask_udp; static uint32_t wolfip_filter_mask_icmp; static int wolfip_filter_lock; -/* Set while a callback is registered and every reason mask is still at its - * zero default; cleared by the first explicit mask configuration. Until - * cleared, dispatch consults the callback for every reason so a freshly +/* Set by the first explicit mask configuration (any value, including 0) + * after the last uninstall; reset by uninstalling the callback. While + * unset, dispatch consults the callback for every reason so a freshly * installed filter cannot fail open. */ static int wolfip_filter_mask_touched; void wolfIP_filter_set_callback(wolfIP_filter_cb cb, void *arg) { + if (cb == NULL) + wolfip_filter_mask_touched = 0; wolfip_filter_cb = cb; wolfip_filter_arg = arg; - wolfip_filter_mask_touched = (cb == NULL) || (wolfip_filter_mask != 0) || - (wolfip_filter_mask_eth != 0) || (wolfip_filter_mask_ip != 0) || - (wolfip_filter_mask_tcp != 0) || (wolfip_filter_mask_udp != 0) || - (wolfip_filter_mask_icmp != 0); } void wolfIP_filter_set_mask(uint32_t mask) diff --git a/wolfip-filter.h b/wolfip-filter.h index a1a956d8..79ce525e 100644 --- a/wolfip-filter.h +++ b/wolfip-filter.h @@ -71,21 +71,22 @@ typedef int (*wolfIP_filter_cb)(void *arg, const struct wolfIP_filter_event *eve * reasons selected in the reason masks; a reason bit left clear in a * configured mask means the stack silently allows that traffic (no * callback, no error). To keep a freshly installed filter from failing - * open, a callback registered while every reason mask is still zero is - * consulted for all reasons until the first explicit mask configuration - * (wolfIP_filter_set_mask() or any per-proto setter); afterwards only - * the configured reasons are consulted. Passing a NULL callback - * uninstalls it (masks are left as configured). + * open, a callback installed without any explicit mask configuration + * since the last uninstall is consulted for all reasons until the first + * explicit mask configuration (wolfIP_filter_set_mask() or any per-proto + * setter, including an explicit zero); afterwards only the configured + * reasons are consulted. Passing a NULL callback uninstalls it and resets + * this all-reasons default (masks keep their values). */ void wolfIP_filter_set_callback(wolfIP_filter_cb cb, void *arg); /* - * Select the reasons the callback is consulted for (all protocols). - * This is the first explicit mask configuration: it ends the - * all-reasons default of a freshly registered callback. Once configured, - * an all-zero mask disables all notifications and silently allows every - * packet and socket event. Per-proto masks (eth/ip/tcp/udp/icmp) take - * precedence over this one when non-zero. + * Select the reasons the callback is consulted for (all protocols). Any + * call — including an explicit zero — counts as an explicit mask + * configuration and ends the all-reasons default of a freshly installed + * callback. Once configured, an all-zero mask disables all notifications + * and silently allows every packet and socket event. Per-proto masks + * (eth/ip/tcp/udp/icmp) take precedence over this one when non-zero. */ void wolfIP_filter_set_mask(uint32_t mask); void wolfIP_filter_set_eth_mask(uint32_t mask); From 36f82fd63bc052b161577fe1ae1fa3626ef93db3 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 17 Aug 2026 14:58:48 +0200 Subject: [PATCH 20/21] F-8568: guard the discover retry jitter window against small bases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit the first-retry schedule subtracted the 1 s jitter half-window from the unsigned backoff base before adding last_tick. With a configured base smaller than the jitter (e.g. DHCP_DISCOVER_TIMEOUT overridden to 200) and a small tick count, the sum wraps modulo 2^64 and the retry is scheduled far in the future (585 million years for base 200, tick 0). Factor the computation into dhcp_discover_retry_delay(): the centered ±1 s window is anchored at zero when the base is smaller than the jitter half-window, keeping the delay bounded for any configured base. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_dns_dhcp.c | 22 ++++++++++++++++++++++ src/wolfip.c | 19 ++++++++++++++++--- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 1c06359b..bfe4be18 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -526,6 +526,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_dns_callback_rejects_a_record_with_wrong_rdlength); tcase_add_test(tc_utils, test_dhcp_parse_offer_option_overload); tcase_add_test(tc_utils, test_dhcp_discover_first_retry_delay_rfc2131); + tcase_add_test(tc_utils, test_dhcp_discover_retry_delay_small_base_no_underflow); tcase_add_test(tc_utils, test_dns_wrapper_apis); tcase_add_test(tc_utils, test_wolfip_static_instance_apis); tcase_add_test(tc_utils, test_wolfip_ip_is_multicast_variants); diff --git a/src/test/unit/unit_tests_dns_dhcp.c b/src/test/unit/unit_tests_dns_dhcp.c index ab254f09..829cc442 100644 --- a/src/test/unit/unit_tests_dns_dhcp.c +++ b/src/test/unit/unit_tests_dns_dhcp.c @@ -1159,6 +1159,28 @@ START_TEST(test_dhcp_discover_first_retry_delay_rfc2131) } END_TEST +/* A configured discover base smaller than the jitter half-window must not + * underflow the centered jitter window (which would schedule the retry + * far in the future). */ +START_TEST(test_dhcp_discover_retry_delay_small_base_no_underflow) +{ + struct wolfIP s; + uint64_t delay; + + wolfIP_init(&s); + mock_link_init(&s); + + /* Default base: 4 s ± 1 s window. */ + delay = dhcp_discover_retry_delay(&s, 4000U); + ck_assert_uint_ge(delay, 3000U); + ck_assert_uint_le(delay, 5000U); + + /* Base below the jitter half-window stays bounded. */ + delay = dhcp_discover_retry_delay(&s, 200U); + ck_assert_uint_le(delay, 2000U); +} +END_TEST + START_TEST(test_sock_connect_tcp_src_port_low) { diff --git a/src/wolfip.c b/src/wolfip.c index 7b6ec7b1..d7569ce2 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -7928,6 +7928,21 @@ static uint64_t dhcp_backoff_delay(const struct wolfIP *s, uint32_t base_ms) return delay; } +/* DHCPDISCOVER retry delay: the backoff base with the RFC's uniform + * ±1 s jitter centered on it. When the configured base is smaller than + * the jitter half-window the window is anchored at zero instead, so a + * small base cannot underflow into a far-future retry. */ +static uint64_t dhcp_discover_retry_delay(const struct wolfIP *s, + uint32_t base_ms) +{ + uint64_t base = dhcp_backoff_delay(s, base_ms); + uint64_t jitter = wolfIP_getrandom() % (2U * DHCP_DISCOVER_JITTER_MS + 1U); + + base = (base > DHCP_DISCOVER_JITTER_MS) ? + base - DHCP_DISCOVER_JITTER_MS : 0U; + return base + jitter; +} + static void dhcp_schedule_retry_timer(struct wolfIP *s, uint64_t deadline) { uint64_t next; @@ -8671,9 +8686,7 @@ static int dhcp_send_discover(struct wolfIP *s) * is 4 s, randomized uniformly by plus or minus 1 s; the base doubles * per attempt. */ dhcp_schedule_timer_at(s, s->last_tick + - dhcp_backoff_delay(s, DHCP_DISCOVER_TIMEOUT) - - DHCP_DISCOVER_JITTER_MS + - (wolfIP_getrandom() % (2U * DHCP_DISCOVER_JITTER_MS + 1U))); + dhcp_discover_retry_delay(s, DHCP_DISCOVER_TIMEOUT)); s->dhcp_state = DHCP_DISCOVER_SENT; return 0; } From 58345d83c4bbf2f4d01d14d0f7cec0b9aaf70396 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 17 Aug 2026 16:21:40 +0200 Subject: [PATCH 21/21] addressed 3 skoll suggestions: - materialize DHCP options split across overload region boundaries (RFC 2132 section 9.3 stream continuation) - cover static-route forwarding hairpin drop and 0.0.0.0/0 default route - disarm mock_send_eagain_armed in mock_link_capture_reset --- src/test/unit/unit.c | 4 + src/test/unit/unit_shared.c | 3 + src/test/unit/unit_tests_dns_dhcp.c | 103 +++++++++++++++++++++ src/test/unit/unit_tests_ip_arp_recv.c | 95 ++++++++++++++++++++ src/wolfip.c | 119 +++++++++++++++++++------ 5 files changed, 297 insertions(+), 27 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index bfe4be18..a83298c4 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -525,6 +525,8 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_dns_send_query_rejects_empty_labels); tcase_add_test(tc_utils, test_dns_callback_rejects_a_record_with_wrong_rdlength); tcase_add_test(tc_utils, test_dhcp_parse_offer_option_overload); + tcase_add_test(tc_utils, + test_dhcp_parse_offer_option_split_across_region_boundary); tcase_add_test(tc_utils, test_dhcp_discover_first_retry_delay_rfc2131); tcase_add_test(tc_utils, test_dhcp_discover_retry_delay_small_base_no_underflow); tcase_add_test(tc_utils, test_dns_wrapper_apis); @@ -1414,6 +1416,8 @@ Suite *wolf_suite(void) tcase_add_test(tc_core, test_ip_recv_ipaddr_any_dst_is_local); tcase_add_test(tc_core, test_ip_recv_forward_arp_hit_sends_immediately); tcase_add_test(tc_core, test_ip_recv_forward_static_route_uses_gateway); + tcase_add_test(tc_core, test_ip_recv_forward_static_route_hairpin_dropped); + tcase_add_test(tc_core, test_ip_recv_forward_static_route_default); tcase_add_test(tc_core, test_ip_recv_forward_declared_length_checked); tcase_add_test(tc_core, test_ip_recv_forward_unconfigured_iface_skipped); tcase_add_test(tc_core, test_ip_recv_forward_link_local_src_rpf_drop); diff --git a/src/test/unit/unit_shared.c b/src/test/unit/unit_shared.c index 3d822e84..fdab7be1 100644 --- a/src/test/unit/unit_shared.c +++ b/src/test/unit/unit_shared.c @@ -114,6 +114,9 @@ void mock_link_capture_reset(void) last_frame_sent_size = 0; last_frame_sent_count = 0; mock_sent_frames_count = 0; + /* Disarm so a test that fails before consuming the flag cannot + * poison the next transmit in a later test. */ + mock_send_eagain_armed = 0; } static int mock_poll(struct wolfIP_ll_dev *dev, void *frame, uint32_t len) diff --git a/src/test/unit/unit_tests_dns_dhcp.c b/src/test/unit/unit_tests_dns_dhcp.c index 829cc442..0af55c95 100644 --- a/src/test/unit/unit_tests_dns_dhcp.c +++ b/src/test/unit/unit_tests_dns_dhcp.c @@ -3346,6 +3346,109 @@ START_TEST(test_dhcp_parse_offer_option_overload) ck_assert_uint_eq(s.ipconf[TEST_PRIMARY_IF].mask, 0xFFFFFF00U); } END_TEST + +/* An option split across a region boundary is a continuation of the + * option stream (RFC 2132 §9.3), not the start of a new option list: + * the server id must be parsed across the split in each boundary + * shape. */ +START_TEST(test_dhcp_parse_offer_option_split_across_region_boundary) +{ + struct wolfIP s; + struct dhcp_msg msg; + uint8_t *opt; + uint8_t *sn; + uint8_t *fl; + uint32_t opt_len; + int ret; + + /* --- Scenario 1: the server id's code byte is the last byte of + * the options field; its length, data and END continue in sname. --- */ + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + s.dhcp_xid = 0x1234; + memset(&msg, 0, sizeof(msg)); + msg.op = BOOT_REPLY; + msg.magic = ee32(DHCP_MAGIC); + msg.xid = ee32(s.dhcp_xid); + msg.yiaddr = ee32(0x0A00000AU); + opt = (uint8_t *)msg.options; + opt[0] = DHCP_OPTION_MSG_TYPE; opt[1] = 1; opt[2] = DHCP_OFFER; + opt += 3; + opt[0] = 52 /* DHCP_OPTION_OVERLOAD */; opt[1] = 1; opt[2] = 2; + opt += 3; + *opt++ = DHCP_OPTION_SERVER_ID; /* last byte of the options field */ + opt_len = (uint32_t)(opt - (uint8_t *)msg.options); + sn = (uint8_t *)msg.sname; + sn[0] = 4; /* length continues in sname */ + sn[1] = 0x0A; sn[2] = 0x00; sn[3] = 0x00; sn[4] = 0x64; + sn[5] = DHCP_OPTION_END; + + ret = dhcp_parse_offer(&s, &msg, DHCP_HEADER_LEN + opt_len); + ck_assert_int_eq(ret, 0); + ck_assert_uint_eq(s.dhcp_server_ip, 0x0A000064U); + ck_assert_uint_eq(s.dhcp_ip, 0x0A00000AU); + ck_assert_int_eq(s.dhcp_state, DHCP_REQUEST_SENT); + + /* --- Scenario 2: code + length are the last two bytes of the + * options field; the four data bytes split 2 + 2 into sname. --- */ + memset(&s, 0, sizeof(s)); + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + s.dhcp_xid = 0x1234; + memset(&msg, 0, sizeof(msg)); + msg.op = BOOT_REPLY; + msg.magic = ee32(DHCP_MAGIC); + msg.xid = ee32(s.dhcp_xid); + msg.yiaddr = ee32(0x0A00000AU); + opt = (uint8_t *)msg.options; + opt[0] = DHCP_OPTION_MSG_TYPE; opt[1] = 1; opt[2] = DHCP_OFFER; + opt += 3; + opt[0] = 52 /* DHCP_OPTION_OVERLOAD */; opt[1] = 1; opt[2] = 2; + opt += 3; + *opt++ = DHCP_OPTION_SERVER_ID; + *opt++ = 4; /* last two bytes of the options field */ + opt_len = (uint32_t)(opt - (uint8_t *)msg.options); + sn = (uint8_t *)msg.sname; + sn[0] = 0x0A; sn[1] = 0x00; /* first half of the data */ + sn[2] = 0x00; sn[3] = 0x64; /* second half */ + sn[4] = DHCP_OPTION_END; + + ret = dhcp_parse_offer(&s, &msg, DHCP_HEADER_LEN + opt_len); + ck_assert_int_eq(ret, 0); + ck_assert_uint_eq(s.dhcp_server_ip, 0x0A000064U); + + /* --- Scenario 3: overload = both; the server id starts at the + * end of sname (pads precede it) and its data runs into file. --- */ + memset(&s, 0, sizeof(s)); + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + s.dhcp_xid = 0x1234; + memset(&msg, 0, sizeof(msg)); + msg.op = BOOT_REPLY; + msg.magic = ee32(DHCP_MAGIC); + msg.xid = ee32(s.dhcp_xid); + msg.yiaddr = ee32(0x0A00000AU); + opt = (uint8_t *)msg.options; + opt[0] = DHCP_OPTION_MSG_TYPE; opt[1] = 1; opt[2] = DHCP_OFFER; + opt += 3; + opt[0] = 52 /* DHCP_OPTION_OVERLOAD */; opt[1] = 1; opt[2] = 3; + opt += 3; + opt_len = (uint32_t)(opt - (uint8_t *)msg.options); + sn = (uint8_t *)msg.sname; + fl = (uint8_t *)msg.file; + sn[60] = DHCP_OPTION_SERVER_ID; sn[61] = 4; + sn[62] = 0x0A; sn[63] = 0x00; /* data starts in sname */ + fl[0] = 0x00; fl[1] = 0x64; /* ...and ends in file */ + fl[2] = DHCP_OPTION_END; + + ret = dhcp_parse_offer(&s, &msg, DHCP_HEADER_LEN + opt_len); + ck_assert_int_eq(ret, 0); + ck_assert_uint_eq(s.dhcp_server_ip, 0x0A000064U); +} +END_TEST START_TEST(test_fifo_push_and_pop) { struct fifo f; struct pkt_desc *desc, *desc2; diff --git a/src/test/unit/unit_tests_ip_arp_recv.c b/src/test/unit/unit_tests_ip_arp_recv.c index 3b9b7343..14f1b46c 100644 --- a/src/test/unit/unit_tests_ip_arp_recv.c +++ b/src/test/unit/unit_tests_ip_arp_recv.c @@ -294,6 +294,101 @@ START_TEST(test_ip_recv_forward_static_route_uses_gateway) } END_TEST +/* A transit packet whose only matching static route points back at the + * ingress interface must be dropped (hairpin), not forwarded back onto + * the interface it arrived on. */ +START_TEST(test_ip_recv_forward_static_route_hairpin_dropped) +{ + struct wolfIP s; + uint8_t frame[ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN]; + struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)frame; + uint16_t *udp; + ip4 primary_ip = 0x0A000001U; /* 10.0.0.1 on if1 (ingress) */ + ip4 secondary_ip = 0xC0A80101U; /* 192.168.1.1 on if2 */ + ip4 dest_ip = 0x0A020005U; /* 10.2.0.5 — route on if1 only */ + ip4 src_ip = 0x0A000002U; + + setup_stack_with_two_ifaces(&s, primary_ip, secondary_ip); + wolfIP_filter_set_callback(NULL, NULL); + ck_assert_int_eq(wolfIP_route_add(&s, TEST_PRIMARY_IF, 0x0A020000U, 24, + IPADDR_ANY), 0); + + last_frame_sent_size = 0; + + memset(frame, 0, sizeof(frame)); + memcpy(ip->eth.dst, s.ll_dev[TEST_PRIMARY_IF].mac, 6); + memcpy(ip->eth.src, "\x01\x02\x03\x04\x05\x06", 6); + ip->eth.type = ee16(ETH_TYPE_IP); + ip->ver_ihl = 0x45; + ip->ttl = 64; + ip->proto = WI_IPPROTO_UDP; + ip->len = ee16(IP_HEADER_LEN + UDP_HEADER_LEN); + ip->src = ee32(src_ip); + ip->dst = ee32(dest_ip); + fix_ip_checksum(ip); + udp = (uint16_t *)(frame + ETH_HEADER_LEN + IP_HEADER_LEN); + udp[0] = ee16(9999); udp[1] = ee16(53); + udp[2] = ee16(UDP_HEADER_LEN); udp[3] = 0; + + ip_recv(&s, TEST_PRIMARY_IF, ip, (uint32_t)sizeof(frame)); + + /* The route matches but points at the ingress interface: drop. */ + ck_assert_uint_eq(last_frame_sent_size, 0); +} +END_TEST + +/* A 0.0.0.0/0 default route must catch transit destinations that no + * connected subnet covers and forward them via the route's gateway. */ +START_TEST(test_ip_recv_forward_static_route_default) +{ + struct wolfIP s; + uint8_t frame[ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN]; + struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)frame; + struct wolfIP_ip_packet *sent; + uint16_t *udp; + ip4 primary_ip = 0x0A000001U; /* 10.0.0.1 on if1 */ + ip4 secondary_ip = 0xC0A80101U; /* 192.168.1.1 on if2 */ + ip4 dest_ip = 0x08080808U; /* 8.8.8.8 — default route only */ + ip4 src_ip = 0x0A000002U; + ip4 gw_ip = 0xC0A801FEU; /* 192.168.1.254, route gateway */ + static const uint8_t gw_mac[6] = {0x20, 0x21, 0x22, 0x23, 0x24, 0x25}; + + setup_stack_with_two_ifaces(&s, primary_ip, secondary_ip); + wolfIP_filter_set_callback(NULL, NULL); + ck_assert_int_eq(wolfIP_route_add(&s, TEST_SECOND_IF, 0x00000000U, 0, + gw_ip), 0); + + /* ARP is known for the gateway. */ + arp_store_neighbor(&s, TEST_SECOND_IF, gw_ip, gw_mac); + + last_frame_sent_size = 0; + + memset(frame, 0, sizeof(frame)); + memcpy(ip->eth.dst, s.ll_dev[TEST_PRIMARY_IF].mac, 6); + memcpy(ip->eth.src, "\x01\x02\x03\x04\x05\x06", 6); + ip->eth.type = ee16(ETH_TYPE_IP); + ip->ver_ihl = 0x45; + ip->ttl = 64; + ip->proto = WI_IPPROTO_UDP; + ip->len = ee16(IP_HEADER_LEN + UDP_HEADER_LEN); + ip->src = ee32(src_ip); + ip->dst = ee32(dest_ip); + fix_ip_checksum(ip); + udp = (uint16_t *)(frame + ETH_HEADER_LEN + IP_HEADER_LEN); + udp[0] = ee16(9999); udp[1] = ee16(53); + udp[2] = ee16(UDP_HEADER_LEN); udp[3] = 0; + + ip_recv(&s, TEST_PRIMARY_IF, ip, (uint32_t)sizeof(frame)); + + /* Forwarded out if2 to the gateway, IP dst preserved, TTL 63. */ + ck_assert_uint_gt(last_frame_sent_size, 0); + ck_assert_mem_eq(last_frame_sent + 0, gw_mac, 6); + sent = (struct wolfIP_ip_packet *)last_frame_sent; + ck_assert_uint_eq(ee32(sent->dst), dest_ip); + ck_assert_uint_eq(sent->ttl, 63); +} +END_TEST + /* The declared IPv4 total length must match the bytes actually received: * a frame whose header claims more payload than it carries is dropped * (it must not be relayed with an inconsistent length), and a frame with diff --git a/src/wolfip.c b/src/wolfip.c index d7569ce2..d2cbcf7e 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -8147,6 +8147,15 @@ struct dhcp_opt_stream { uint8_t region; /* 0 options, 1 sname, 2 file */ uint8_t overload; int strict; /* -1 on malformed, or treat as end of stream */ + /* Option in progress across a region boundary, materialized here as + * it is read: the sname and file fields live in separate parts of + * the message, so an option whose bytes span a boundary cannot be + * handed out as a contiguous [code, len, data...] block in place. + * fill is NULL when no option is in progress; the buffer holds one + * full option at most (1 + 1 + 255 bytes). */ + uint8_t *fill; + uint8_t part_in_region0; + uint8_t part_buf[257]; }; static void dhcp_opt_stream_init(struct dhcp_opt_stream *st, @@ -8165,6 +8174,37 @@ static void dhcp_opt_stream_init(struct dhcp_opt_stream *st, st->region = 0; st->overload = 0; st->strict = strict; + st->fill = NULL; + st->part_in_region0 = 0; +} + +/* Advance the stream to the next region selected by option 52. Return 1 + * when a region was entered, 0 when no further region exists. */ +static int dhcp_opt_stream_next_region(struct dhcp_opt_stream *st) +{ + if (st->region == 0) { + if (st->overload & 2) { + st->region = 1; + st->ptr = st->region_sname; + st->end = st->region_sname_end; + return 1; + } + if (st->overload & 1) { + st->region = 2; + st->ptr = st->region_file; + st->end = st->region_file_end; + return 1; + } + } + else if (st->region == 1) { + if (st->overload & 1) { + st->region = 2; + st->ptr = st->region_file; + st->end = st->region_file_end; + return 1; + } + } + return 0; } /* Return 1 with the out-params set to the next option (option 255 is @@ -8177,6 +8217,44 @@ static int dhcp_opt_stream_next(struct dhcp_opt_stream *st, uint8_t *code, while (1) { uint8_t c; uint8_t l; + if (st->fill != NULL) { + /* Option in progress across region boundaries: pull bytes + * from the current region into part_buf until the option is + * complete. Its total size (2 + part_buf[1]) is known once + * the header bytes have been collected. */ + while (st->ptr < st->end && + (st->fill < st->part_buf + 2 || + st->fill < st->part_buf + 2 + st->part_buf[1])) + *st->fill++ = *st->ptr++; + if (st->fill >= st->part_buf + 2 && + st->fill >= st->part_buf + 2 + st->part_buf[1]) { + if (st->part_in_region0 && + st->part_buf[0] == DHCP_OPTION_OVERLOAD) { + if (st->part_buf[1] != 1 || st->part_buf[2] < 1 || + st->part_buf[2] > 3) { + st->fill = NULL; + if (st->strict) + return -1; + return 0; + } + st->overload = st->part_buf[2]; + } + *code = st->part_buf[0]; + *len = st->part_buf[1]; + *data = st->part_buf; + st->fill = NULL; + return 1; + } + /* Region exhausted mid-option: continue into the next + * overloaded region, or end the stream. */ + if (!dhcp_opt_stream_next_region(st)) { + st->fill = NULL; + if (st->strict) + return -1; + return 0; + } + continue; + } if (st->ptr + 1 > st->end) goto region_end; c = st->ptr[0]; @@ -8191,11 +8269,17 @@ static int dhcp_opt_stream_next(struct dhcp_opt_stream *st, uint8_t *code, st->ptr++; continue; } - if (st->ptr + 2 > st->end) - goto region_end; + if (st->ptr + 2 > st->end || + st->ptr + 2 + st->ptr[1] > st->end) { + /* Option header or data runs past the region end. RFC 2132 + * §9.3 makes the sname/file fields a continuation of the + * option stream, so materialize the option across the + * boundary instead of dropping it. */ + st->fill = st->part_buf; + st->part_in_region0 = (st->region == 0); + continue; + } l = st->ptr[1]; - if (st->ptr + 2 + l > st->end) - goto region_end; if (st->region == 0 && c == DHCP_OPTION_OVERLOAD) { /* RFC 2132 §9.3: length 1, values 1 (file), 2 (sname), 3. */ if (l != 1 || st->ptr[2] < 1 || st->ptr[2] > 3) @@ -8212,30 +8296,11 @@ static int dhcp_opt_stream_next(struct dhcp_opt_stream *st, uint8_t *code, region_end: /* Region exhausted without END: continue into the overloaded * fields (sname, then file) if option 52 selected them. */ - if (st->region == 0) { - if (st->overload & 2) { - st->region = 1; - st->ptr = st->region_sname; - st->end = st->region_sname_end; - continue; - } - if (st->overload & 1) { - st->region = 2; - st->ptr = st->region_file; - st->end = st->region_file_end; - continue; - } - } else if (st->region == 1) { - if (st->overload & 1) { - st->region = 2; - st->ptr = st->region_file; - st->end = st->region_file_end; - continue; - } + if (!dhcp_opt_stream_next_region(st)) { + if (st->strict) + return -1; + return 0; } - if (st->strict) - return -1; - return 0; } }