netup = binario hammer-propio (sync, sólo libc, sin tokio) que configura la red: levanta el link (netlink RTM_NEWLINK), negocia un lease DHCPv4 a mano sobre UDP (DISCOVER/OFFER/REQUEST/ACK con flag broadcast), y aplica IP + ruta default + /etc/resolv.conf (RTM_NEWADDR/RTM_NEWROUTE). Hand-roll de netlink y DHCP porque no hay cliente DHCP Rust "maduro" para adoptar al estilo ripgrep, y el workspace es 100% sync (rtnetlink arrastraría tokio). Reemplaza el `ip` estático de busybox. Validado in-VM contra el DHCP de QEMU slirp: ✓ lease 10.0.2.15 + ruta + DNS + egress TCP. Autodetecta la NIC (primera no-loopback) y espera carrier por sysfs. Infra de prueba: - scripts/drive-netup.py: bootea la VM, corre netup y verifica lease/NAT/DNS. SLIRP=1 ⇒ red user-mode (cero setup de host); si no, tap del lab. - scripts/labnet.sh: lab de LAN real opcional (tap directo + dnsmasq + NAT) para fidelidad de hardware; no requerido para validar netup. - scripts/netdiag.sh: diagnóstico del camino DHCP del lab (tcpdump+nft+dnsmasq). Lección: para validar el CÓDIGO conviene slirp primero (cero plomería de host); la LAN real (firewall INPUT, sutilezas del bridge, perms) es fidelidad posterior. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
218 lines
7.5 KiB
Rust
218 lines
7.5 KiB
Rust
//! 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<u32> {
|
|
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<u8>)]) -> Vec<u8> {
|
|
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<u8, Vec<u8>>)> {
|
|
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<Lease> {
|
|
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<std::collections::HashMap<u8, Vec<u8>>> {
|
|
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})")))
|
|
}
|