gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1572Shell 203Rust 109Markdown 993728 Sutichaykuna

⎇ 6886f810bddd29481e1d84b982d98e783b578bdc crates/netup/src/dhcp.rs Rust · 217 siq'ikuna · 7.5 KB Ñawpa willay Pi imata llamkarqan ch'uya

crates/netup: red mínima Rust-nativa (Etapa C pieza 5) 6886f810b · sergio ·

1//! Cliente DHCPv4 mínimo y SÍNCRONO: DISCOVER → OFFER → REQUEST → ACK sobre un UDP socket bindeado a
2//! la interfaz (SO_BINDTODEVICE) con broadcast. El servidor responde en broadcast (ponemos el flag
3//! BROADCAST), así no hace falta un raw/AF_PACKET socket para recibir sin IP todavía.
4
5use std::io;
6use std::net::{Ipv4Addr, SocketAddrV4, UdpSocket};
7use std::os::unix::io::AsRawFd;
8
9const OP_REQUEST: u8 = 1;
10const HTYPE_ETHER: u8 = 1;
11const HLEN_ETHER: u8 = 6;
12const MAGIC: [u8; 4] = [99, 130, 83, 99]; // 0x63825363
13const FLAG_BROADCAST: u16 = 0x8000;
14
15// Opciones DHCP.
16const OPT_SUBNET: u8 = 1;
17const OPT_ROUTER: u8 = 3;
18const OPT_DNS: u8 = 6;
19const OPT_REQ_IP: u8 = 50;
20const OPT_LEASE: u8 = 51;
21const OPT_MSG_TYPE: u8 = 53;
22const OPT_SERVER_ID: u8 = 54;
23const OPT_PARAM_LIST: u8 = 55;
24const OPT_END: u8 = 255;
25
26// Tipos de mensaje (opción 53).
27const DHCPDISCOVER: u8 = 1;
28const DHCPOFFER: u8 = 2;
29const DHCPREQUEST: u8 = 3;
30const DHCPACK: u8 = 5;
31
32/// El resultado de la negociación: lo que aplicaremos por netlink + resolv.conf.
33pub struct Lease {
34    pub ip: [u8; 4],
35    pub prefix: u8,
36    pub router: Option<[u8; 4]>,
37    pub dns: Vec<[u8; 4]>,
38    pub lease_secs: u32,
39    pub server: [u8; 4],
40}
41
42/// xid (transaction id) desde /dev/urandom: el servidor lo refleja, lo usamos para casar la respuesta.
43fn random_xid() -> io::Result<u32> {
44    let mut b = [0u8; 4];
45    let mut f = std::fs::File::open("/dev/urandom")?;
46    io::Read::read_exact(&mut f, &mut b)?;
47    Ok(u32::from_ne_bytes(b))
48}
49
50/// Construye un paquete DHCP (cabecera BOOTP de 236 B + magic cookie + opciones).
51fn build(msg_type: u8, xid: u32, mac: [u8; 6], extra: &[(u8, Vec<u8>)]) -> Vec<u8> {
52    let mut p = vec![0u8; 236];
53    p[0] = OP_REQUEST;
54    p[1] = HTYPE_ETHER;
55    p[2] = HLEN_ETHER;
56    // hops=0, secs=0, ciaddr/yiaddr/siaddr/giaddr=0 (ya en cero)
57    p[4..8].copy_from_slice(&xid.to_be_bytes());
58    p[10..12].copy_from_slice(&FLAG_BROADCAST.to_be_bytes());
59    p[28..34].copy_from_slice(&mac); // chaddr
60    p.extend_from_slice(&MAGIC);
61    // opción 53 (tipo de mensaje) primero.
62    p.extend_from_slice(&[OPT_MSG_TYPE, 1, msg_type]);
63    for (code, data) in extra {
64        p.push(*code);
65        p.push(data.len() as u8);
66        p.extend_from_slice(data);
67    }
68    // opción 55: lista de parámetros que pedimos.
69    p.extend_from_slice(&[OPT_PARAM_LIST, 4, OPT_SUBNET, OPT_ROUTER, OPT_DNS, OPT_LEASE]);
70    p.push(OPT_END);
71    p
72}
73
74/// Recorre las opciones TLV tras la cabecera+cookie. Devuelve (msg_type, opciones por código).
75fn parse_options(pkt: &[u8]) -> Option<(u8, std::collections::HashMap<u8, Vec<u8>>)> {
76    if pkt.len() < 240 || pkt[236..240] != MAGIC {
77        return None;
78    }
79    let mut opts = std::collections::HashMap::new();
80    let mut i = 240;
81    while i < pkt.len() {
82        let code = pkt[i];
83        if code == OPT_END {
84            break;
85        }
86        if code == 0 {
87            i += 1; // pad
88            continue;
89        }
90        if i + 1 >= pkt.len() {
91            break;
92        }
93        let len = pkt[i + 1] as usize;
94        if i + 2 + len > pkt.len() {
95            break;
96        }
97        opts.insert(code, pkt[i + 2..i + 2 + len].to_vec());
98        i += 2 + len;
99    }
100    let mt = opts.get(&OPT_MSG_TYPE).and_then(|v| v.first().copied())?;
101    Some((mt, opts))
102}
103
104fn ipv4(v: &[u8]) -> Option<[u8; 4]> {
105    if v.len() >= 4 {
106        Some([v[0], v[1], v[2], v[3]])
107    } else {
108        None
109    }
110}
111
112fn prefix_from_mask(mask: [u8; 4]) -> u8 {
113    mask.iter().map(|b| b.count_ones() as u8).sum()
114}
115
116/// Negocia un lease en `ifname`. `mac` es la MAC de la interfaz (para chaddr).
117pub fn run(ifname: &str, mac: [u8; 6]) -> io::Result<Lease> {
118    let sock = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 68))?;
119    sock.set_broadcast(true)?;
120    sock.set_read_timeout(Some(std::time::Duration::from_secs(5)))?;
121    // SO_BINDTODEVICE: amarra el socket a la interfaz (sin IP todavía no hay ruta que elija salida).
122    let cname = std::ffi::CString::new(ifname).unwrap();
123    let r = unsafe {
124        libc::setsockopt(
125            sock.as_raw_fd(),
126            libc::SOL_SOCKET,
127            libc::SO_BINDTODEVICE,
128            cname.as_ptr() as *const libc::c_void,
129            (ifname.len() + 1) as libc::socklen_t,
130        )
131    };
132    if r < 0 {
133        return Err(io::Error::last_os_error());
134    }
135    let bcast = SocketAddrV4::new(Ipv4Addr::BROADCAST, 67);
136    let xid = random_xid()?;
137
138    // DISCOVER → OFFER (reintentos: las VMs/servidores pueden tardar en contestar el primer paquete).
139    let discover = build(DHCPDISCOVER, xid, mac, &[]);
140    let mut buf = [0u8; 2048];
141    let offer = recv_matching(&sock, &discover, &bcast, xid, DHCPOFFER, &mut buf, 4)?;
142    // yiaddr (la IP ofrecida) vive en la cabecera BOOTP; recv_matching lo dejó en LAST_YIADDR.
143    let yi = LAST_YIADDR.with(|c| *c.borrow());
144    let server = offer
145        .get(&OPT_SERVER_ID)
146        .and_then(|v| ipv4(v))
147        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "OFFER sin server-id"))?;
148
149    // REQUEST (pidiendo el yiaddr ofrecido y citando el server-id) → ACK.
150    let request = build(
151        DHCPREQUEST,
152        xid,
153        mac,
154        &[(OPT_REQ_IP, yi.to_vec()), (OPT_SERVER_ID, server.to_vec())],
155    );
156    let ack = recv_matching(&sock, &request, &bcast, xid, DHCPACK, &mut buf, 4)?;
157    let ack_ip = LAST_YIADDR.with(|c| *c.borrow());
158
159    let prefix = ack
160        .get(&OPT_SUBNET)
161        .and_then(|v| ipv4(v))
162        .map(prefix_from_mask)
163        .unwrap_or(24);
164    let router = ack.get(&OPT_ROUTER).and_then(|v| ipv4(v));
165    let dns = ack
166        .get(&OPT_DNS)
167        .map(|v| v.chunks_exact(4).map(|c| [c[0], c[1], c[2], c[3]]).collect())
168        .unwrap_or_default();
169    let lease_secs = ack
170        .get(&OPT_LEASE)
171        .and_then(|v| if v.len() >= 4 { Some(u32::from_be_bytes([v[0], v[1], v[2], v[3]])) } else { None })
172        .unwrap_or(0);
173
174    Ok(Lease { ip: ack_ip, prefix, router, dns, lease_secs, server })
175}
176
177// yiaddr (la IP ofrecida) vive en la cabecera BOOTP, no en las opciones; lo guardamos al parsear cada
178// paquete para que `run` lo recupere sin reestructurar el flujo.
179thread_local! {
180    static LAST_YIADDR: std::cell::RefCell<[u8; 4]> = const { std::cell::RefCell::new([0; 4]) };
181}
182
183/// Manda `out` y espera un paquete con el xid y el tipo esperados, reintentando `tries` veces.
184fn recv_matching(
185    sock: &UdpSocket,
186    out: &[u8],
187    dst: &SocketAddrV4,
188    xid: u32,
189    want_type: u8,
190    buf: &mut [u8],
191    tries: u32,
192) -> io::Result<std::collections::HashMap<u8, Vec<u8>>> {
193    for _ in 0..tries {
194        sock.send_to(out, dst)?;
195        loop {
196            match sock.recv_from(buf) {
197                Ok((n, _)) => {
198                    let pkt = &buf[..n];
199                    if n < 240 || u32::from_be_bytes([pkt[4], pkt[5], pkt[6], pkt[7]]) != xid {
200                        continue; // no es nuestra transacción
201                    }
202                    if let Some((mt, opts)) = parse_options(pkt) {
203                        if mt == want_type {
204                            LAST_YIADDR.with(|c| c.borrow_mut().copy_from_slice(&pkt[16..20])); // yiaddr
205                            return Ok(opts);
206                        }
207                    }
208                }
209                Err(e) if e.kind() == io::ErrorKind::WouldBlock || e.kind() == io::ErrorKind::TimedOut => {
210                    break // timeout → reintentar el send
211                }
212                Err(e) => return Err(e),
213            }
214        }
215    }
216    Err(io::Error::new(io::ErrorKind::TimedOut, format!("sin respuesta DHCP (tipo {want_type})")))
217}