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:
+31
-6
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user