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).
This commit is contained in:
Red Bear OS
2026-07-25 21:38:31 +09:00
parent e54484e553
commit 29166263bb
+31 -6
View File
@@ -121,12 +121,18 @@ fn dhcp(iface: &str, verbose: bool) -> Result<(), String> {
).subsec_nanos(); ).subsec_nanos();
let socket = try_fmt!(UdpSocket::bind(("0.0.0.0", 68)), "failed to bind udp"); 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 // 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 // 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 // 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 // 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+. // 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_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"); 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); init_dhcp_header(&mut discover, current_mac, tid);
let disc_opts: &[u8] = &[OPT_MESSAGE_TYPE, 1, DHCPDISCOVER, OPT_END]; let disc_opts: &[u8] = &[OPT_MESSAGE_TYPE, 1, DHCPDISCOVER, OPT_END];
discover.options[..disc_opts.len()].copy_from_slice(disc_opts); 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"); } if verbose { println!("DHCP: Sent Discover"); }
} }
@@ -195,7 +201,7 @@ fn dhcp(iface: &str, verbose: bool) -> Result<(), String> {
OPT_END, OPT_END,
]; ];
request.options[..req_opts.len()].copy_from_slice(req_opts); 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"); } if verbose { println!("DHCP: Sent Request"); }
} }
@@ -223,7 +229,10 @@ fn dhcp(iface: &str, verbose: bool) -> Result<(), String> {
renew.ciaddr = offer.yiaddr; renew.ciaddr = offer.yiaddr;
let rn_opts: &[u8] = &[OPT_MESSAGE_TYPE, 1, DHCPREQUEST, OPT_END]; let rn_opts: &[u8] = &[OPT_MESSAGE_TYPE, 1, DHCPREQUEST, OPT_END];
renew.options[..rn_opts.len()].copy_from_slice(rn_opts); 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(); socket.set_read_timeout(Some(t2.saturating_sub(t1))).ok();
@@ -242,13 +251,12 @@ fn dhcp(iface: &str, verbose: bool) -> Result<(), String> {
Err(_) => { Err(_) => {
if verbose { println!("DHCP: entering REBIND state"); } if verbose { println!("DHCP: entering REBIND state"); }
let bind_socket = try_fmt!(UdpSocket::bind(("0.0.0.0", 68)), "failed to bind rebind"); 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(); let mut rebind = Dhcp::default();
init_dhcp_header(&mut rebind, current_mac, tid.wrapping_add(2)); init_dhcp_header(&mut rebind, current_mac, tid.wrapping_add(2));
rebind.ciaddr = offer.yiaddr; rebind.ciaddr = offer.yiaddr;
let rb_opts: &[u8] = &[OPT_MESSAGE_TYPE, 1, DHCPREQUEST, OPT_END]; let rb_opts: &[u8] = &[OPT_MESSAGE_TYPE, 1, DHCPREQUEST, OPT_END];
rebind.options[..rb_opts.len()].copy_from_slice(rb_opts); 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(); bind_socket.set_read_timeout(Some(Duration::from_secs(10))).ok();
if let Ok(_) = bind_socket.recv(&mut ack_data) { if let Ok(_) = bind_socket.recv(&mut ack_data) {
if verbose { println!("DHCP: rebound"); } 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)) 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::<Dhcp>())
};
socket.send_to(data, addr).map(|_| ()).map_err(|e| format!("send_to: {}", e))
}
impl Default for Dhcp { impl Default for Dhcp {
fn default() -> Self { fn default() -> Self {
Dhcp { Dhcp {