//! Cliente DHCPv4 mínimo y SÍNCRONO: DISCOVER → OFFER → REQUEST → ACK sobre un UDP socket bindeado a //! la interfaz (SO_BINDTODEVICE) con broadcast. El servidor responde en broadcast (ponemos el flag //! BROADCAST), así no hace falta un raw/AF_PACKET socket para recibir sin IP todavía. use std::io; use std::net::{Ipv4Addr, SocketAddrV4, UdpSocket}; use std::os::unix::io::AsRawFd; const OP_REQUEST: u8 = 1; const HTYPE_ETHER: u8 = 1; const HLEN_ETHER: u8 = 6; const MAGIC: [u8; 4] = [99, 130, 83, 99]; // 0x63825363 const FLAG_BROADCAST: u16 = 0x8000; // Opciones DHCP. const OPT_SUBNET: u8 = 1; const OPT_ROUTER: u8 = 3; const OPT_DNS: u8 = 6; const OPT_REQ_IP: u8 = 50; const OPT_LEASE: u8 = 51; const OPT_MSG_TYPE: u8 = 53; const OPT_SERVER_ID: u8 = 54; const OPT_PARAM_LIST: u8 = 55; const OPT_END: u8 = 255; // Tipos de mensaje (opción 53). const DHCPDISCOVER: u8 = 1; const DHCPOFFER: u8 = 2; const DHCPREQUEST: u8 = 3; const DHCPACK: u8 = 5; /// El resultado de la negociación: lo que aplicaremos por netlink + resolv.conf. pub struct Lease { pub ip: [u8; 4], pub prefix: u8, pub router: Option<[u8; 4]>, pub dns: Vec<[u8; 4]>, pub lease_secs: u32, pub server: [u8; 4], } /// xid (transaction id) desde /dev/urandom: el servidor lo refleja, lo usamos para casar la respuesta. fn random_xid() -> io::Result { let mut b = [0u8; 4]; let mut f = std::fs::File::open("/dev/urandom")?; io::Read::read_exact(&mut f, &mut b)?; Ok(u32::from_ne_bytes(b)) } /// Construye un paquete DHCP (cabecera BOOTP de 236 B + magic cookie + opciones). fn build(msg_type: u8, xid: u32, mac: [u8; 6], extra: &[(u8, Vec)]) -> Vec { let mut p = vec![0u8; 236]; p[0] = OP_REQUEST; p[1] = HTYPE_ETHER; p[2] = HLEN_ETHER; // hops=0, secs=0, ciaddr/yiaddr/siaddr/giaddr=0 (ya en cero) p[4..8].copy_from_slice(&xid.to_be_bytes()); p[10..12].copy_from_slice(&FLAG_BROADCAST.to_be_bytes()); p[28..34].copy_from_slice(&mac); // chaddr p.extend_from_slice(&MAGIC); // opción 53 (tipo de mensaje) primero. p.extend_from_slice(&[OPT_MSG_TYPE, 1, msg_type]); for (code, data) in extra { p.push(*code); p.push(data.len() as u8); p.extend_from_slice(data); } // opción 55: lista de parámetros que pedimos. p.extend_from_slice(&[OPT_PARAM_LIST, 4, OPT_SUBNET, OPT_ROUTER, OPT_DNS, OPT_LEASE]); p.push(OPT_END); p } /// Recorre las opciones TLV tras la cabecera+cookie. Devuelve (msg_type, opciones por código). fn parse_options(pkt: &[u8]) -> Option<(u8, std::collections::HashMap>)> { if pkt.len() < 240 || pkt[236..240] != MAGIC { return None; } let mut opts = std::collections::HashMap::new(); let mut i = 240; while i < pkt.len() { let code = pkt[i]; if code == OPT_END { break; } if code == 0 { i += 1; // pad continue; } if i + 1 >= pkt.len() { break; } let len = pkt[i + 1] as usize; if i + 2 + len > pkt.len() { break; } opts.insert(code, pkt[i + 2..i + 2 + len].to_vec()); i += 2 + len; } let mt = opts.get(&OPT_MSG_TYPE).and_then(|v| v.first().copied())?; Some((mt, opts)) } fn ipv4(v: &[u8]) -> Option<[u8; 4]> { if v.len() >= 4 { Some([v[0], v[1], v[2], v[3]]) } else { None } } fn prefix_from_mask(mask: [u8; 4]) -> u8 { mask.iter().map(|b| b.count_ones() as u8).sum() } /// Negocia un lease en `ifname`. `mac` es la MAC de la interfaz (para chaddr). pub fn run(ifname: &str, mac: [u8; 6]) -> io::Result { let sock = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 68))?; sock.set_broadcast(true)?; sock.set_read_timeout(Some(std::time::Duration::from_secs(5)))?; // SO_BINDTODEVICE: amarra el socket a la interfaz (sin IP todavía no hay ruta que elija salida). let cname = std::ffi::CString::new(ifname).unwrap(); let r = unsafe { libc::setsockopt( sock.as_raw_fd(), libc::SOL_SOCKET, libc::SO_BINDTODEVICE, cname.as_ptr() as *const libc::c_void, (ifname.len() + 1) as libc::socklen_t, ) }; if r < 0 { return Err(io::Error::last_os_error()); } let bcast = SocketAddrV4::new(Ipv4Addr::BROADCAST, 67); let xid = random_xid()?; // DISCOVER → OFFER (reintentos: las VMs/servidores pueden tardar en contestar el primer paquete). let discover = build(DHCPDISCOVER, xid, mac, &[]); let mut buf = [0u8; 2048]; let offer = recv_matching(&sock, &discover, &bcast, xid, DHCPOFFER, &mut buf, 4)?; // yiaddr (la IP ofrecida) vive en la cabecera BOOTP; recv_matching lo dejó en LAST_YIADDR. let yi = LAST_YIADDR.with(|c| *c.borrow()); let server = offer .get(&OPT_SERVER_ID) .and_then(|v| ipv4(v)) .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "OFFER sin server-id"))?; // REQUEST (pidiendo el yiaddr ofrecido y citando el server-id) → ACK. let request = build( DHCPREQUEST, xid, mac, &[(OPT_REQ_IP, yi.to_vec()), (OPT_SERVER_ID, server.to_vec())], ); let ack = recv_matching(&sock, &request, &bcast, xid, DHCPACK, &mut buf, 4)?; let ack_ip = LAST_YIADDR.with(|c| *c.borrow()); let prefix = ack .get(&OPT_SUBNET) .and_then(|v| ipv4(v)) .map(prefix_from_mask) .unwrap_or(24); let router = ack.get(&OPT_ROUTER).and_then(|v| ipv4(v)); let dns = ack .get(&OPT_DNS) .map(|v| v.chunks_exact(4).map(|c| [c[0], c[1], c[2], c[3]]).collect()) .unwrap_or_default(); let lease_secs = ack .get(&OPT_LEASE) .and_then(|v| if v.len() >= 4 { Some(u32::from_be_bytes([v[0], v[1], v[2], v[3]])) } else { None }) .unwrap_or(0); Ok(Lease { ip: ack_ip, prefix, router, dns, lease_secs, server }) } // yiaddr (la IP ofrecida) vive en la cabecera BOOTP, no en las opciones; lo guardamos al parsear cada // paquete para que `run` lo recupere sin reestructurar el flujo. thread_local! { static LAST_YIADDR: std::cell::RefCell<[u8; 4]> = const { std::cell::RefCell::new([0; 4]) }; } /// Manda `out` y espera un paquete con el xid y el tipo esperados, reintentando `tries` veces. fn recv_matching( sock: &UdpSocket, out: &[u8], dst: &SocketAddrV4, xid: u32, want_type: u8, buf: &mut [u8], tries: u32, ) -> io::Result>> { for _ in 0..tries { sock.send_to(out, dst)?; loop { match sock.recv_from(buf) { Ok((n, _)) => { let pkt = &buf[..n]; if n < 240 || u32::from_be_bytes([pkt[4], pkt[5], pkt[6], pkt[7]]) != xid { continue; // no es nuestra transacción } if let Some((mt, opts)) = parse_options(pkt) { if mt == want_type { LAST_YIADDR.with(|c| c.borrow_mut().copy_from_slice(&pkt[16..20])); // yiaddr return Ok(opts); } } } Err(e) if e.kind() == io::ErrorKind::WouldBlock || e.kind() == io::ErrorKind::TimedOut => { break // timeout → reintentar el send } Err(e) => return Err(e), } } } Err(io::Error::new(io::ErrorKind::TimedOut, format!("sin respuesta DHCP (tipo {want_type})"))) }