From 29166263bbf1d03026ede041e6662ca5a58f3530 Mon Sep 17 00:00:00 2001 From: Red Bear OS Date: Sat, 25 Jul 2026 21:38:31 +0900 Subject: [PATCH] dhcpd: stop calling connect() to broadcast (filter drops OFFER) UDP connect() to 255.255.255.255:67 sets the socket's source-filter to broadcast peers only. The DHCP server (e.g. QEMU SLIRP at 10.0.2.2) responds from its own IP, which the filter rejects. The OFFER never arrives at recv(), the 8-second read timeout expires, dhcpd gives up, and the system reaches login with no IP. Per-packet send_to() with no source-filter restores the canonical four-message handshake. Applied at all four broadcast points: DISCOVER, REQUEST, RENEW, REBIND. Keep the legacy send_dhcp() helper around for any future caller that wants the kernel-default destination semantics (currently unused; the new send_dhcp_to is the path all broadcast traffic takes). Verified host build clean (7 pre-existing warnings unrelated to this change; target build requires the cookbook toolchain and is exercised in the canonical build pipeline). --- dhcpd/src/main.rs | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/dhcpd/src/main.rs b/dhcpd/src/main.rs index 27c3a8baae..3b7421c450 100644 --- a/dhcpd/src/main.rs +++ b/dhcpd/src/main.rs @@ -121,12 +121,18 @@ fn dhcp(iface: &str, verbose: bool) -> Result<(), String> { ).subsec_nanos(); let socket = try_fmt!(UdpSocket::bind(("0.0.0.0", 68)), "failed to bind udp"); - try_fmt!(socket.connect(SocketAddr::from(([255, 255, 255, 255], 67))), "failed to connect"); + let broadcast = SocketAddr::from(([255, 255, 255, 255], 67)); // 8s (was 30s): a DHCP server that is present answers a DISCOVER in well // under a second, so a long read timeout only serves to stall boot when no // server responds (e.g. QEMU user-net where the limited broadcast is not // routed, or a link with no DHCP). Failing fast keeps the network stage off // the critical path to login instead of hanging the boot for ~30s+. + // + // Do NOT call connect() to the broadcast address. UDP connect() + // filters incoming packets by source address — an OFFER from the + // DHCP server's actual IP (e.g. QEMU SLIRP at 10.0.2.2) would be + // dropped because it doesn't match the connected broadcast peer. + // Use send_to() per-packet and let recv() accept any source. try_fmt!(socket.set_read_timeout(Some(Duration::new(8, 0))), "failed to set read timeout"); try_fmt!(socket.set_write_timeout(Some(Duration::new(8, 0))), "failed to set write timeout"); @@ -142,7 +148,7 @@ fn dhcp(iface: &str, verbose: bool) -> Result<(), String> { init_dhcp_header(&mut discover, current_mac, tid); let disc_opts: &[u8] = &[OPT_MESSAGE_TYPE, 1, DHCPDISCOVER, OPT_END]; discover.options[..disc_opts.len()].copy_from_slice(disc_opts); - try_fmt!(send_dhcp(&discover, &socket), "failed to send discover"); + try_fmt!(send_dhcp_to(&discover, &socket, broadcast), "failed to send discover"); if verbose { println!("DHCP: Sent Discover"); } } @@ -195,7 +201,7 @@ fn dhcp(iface: &str, verbose: bool) -> Result<(), String> { OPT_END, ]; request.options[..req_opts.len()].copy_from_slice(req_opts); - try_fmt!(send_dhcp(&request, &socket), "failed to send request"); + try_fmt!(send_dhcp_to(&request, &socket, broadcast), "failed to send request"); if verbose { println!("DHCP: Sent Request"); } } @@ -223,7 +229,10 @@ fn dhcp(iface: &str, verbose: bool) -> Result<(), String> { renew.ciaddr = offer.yiaddr; let rn_opts: &[u8] = &[OPT_MESSAGE_TYPE, 1, DHCPREQUEST, OPT_END]; renew.options[..rn_opts.len()].copy_from_slice(rn_opts); - try_fmt!(send_dhcp(&renew, &socket), "failed to send renew"); + try_fmt!( + send_dhcp_to(&renew, &socket, broadcast), + "failed to send renew" + ); } socket.set_read_timeout(Some(t2.saturating_sub(t1))).ok(); @@ -242,13 +251,12 @@ fn dhcp(iface: &str, verbose: bool) -> Result<(), String> { Err(_) => { if verbose { println!("DHCP: entering REBIND state"); } let bind_socket = try_fmt!(UdpSocket::bind(("0.0.0.0", 68)), "failed to bind rebind"); - try_fmt!(bind_socket.connect(SocketAddr::from(([255, 255, 255, 255], 67))), "rebind connect"); let mut rebind = Dhcp::default(); init_dhcp_header(&mut rebind, current_mac, tid.wrapping_add(2)); rebind.ciaddr = offer.yiaddr; let rb_opts: &[u8] = &[OPT_MESSAGE_TYPE, 1, DHCPREQUEST, OPT_END]; rebind.options[..rb_opts.len()].copy_from_slice(rb_opts); - let _ = send_dhcp(&rebind, &bind_socket); + let _ = send_dhcp_to(&rebind, &bind_socket, broadcast); bind_socket.set_read_timeout(Some(Duration::from_secs(10))).ok(); if let Ok(_) = bind_socket.recv(&mut ack_data) { if verbose { println!("DHCP: rebound"); } @@ -332,6 +340,23 @@ fn send_dhcp(pkt: &Dhcp, socket: &UdpSocket) -> Result<(), String> { socket.send(data).map(|_| ()).map_err(|e| format!("send: {}", e)) } +/// `send_to(addr, ...)` variant — used for the DISCOVER, REQUEST, and +/// RENEW/REBIND transmissions, which all target 255.255.255.255:67. +/// `connect()`-based sends drop incoming packets from off-broadcast +/// sources, which would lose the OFFER/ACK the DHCP server emits from +/// its own IP. Per-packet `send_to` + unfiltered `recv` is the only +/// shape that actually completes the four-message handshake. +fn send_dhcp_to( + pkt: &Dhcp, + socket: &UdpSocket, + addr: SocketAddr, +) -> Result<(), String> { + let data = unsafe { + std::slice::from_raw_parts(pkt as *const Dhcp as *const u8, std::mem::size_of::()) + }; + socket.send_to(data, addr).map(|_| ()).map_err(|e| format!("send_to: {}", e)) +} + impl Default for Dhcp { fn default() -> Self { Dhcp {