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.2 KiB
Rust
218 lines
7.2 KiB
Rust
//! Netlink (NETLINK_ROUTE) mínimo y SÍNCRONO sobre libc: levantar un link, añadir una dirección IPv4 y
|
|
//! una ruta por defecto. Hand-roll de los mensajes RTM_* (ABI estable del kernel) en vez de tirar de
|
|
//! `rtnetlink` (async/tokio) — el workspace es 100% sync y así no agregamos deps que vendorar.
|
|
|
|
use std::io;
|
|
use std::mem::size_of;
|
|
use std::os::unix::io::RawFd;
|
|
|
|
// --- constantes de la ABI netlink (estables) ---
|
|
const RTM_NEWLINK: u16 = 16;
|
|
const RTM_NEWADDR: u16 = 20;
|
|
const RTM_NEWROUTE: u16 = 24;
|
|
const NLM_F_REQUEST: u16 = 0x001;
|
|
const NLM_F_ACK: u16 = 0x004;
|
|
const NLM_F_REPLACE: u16 = 0x100;
|
|
const NLM_F_CREATE: u16 = 0x400;
|
|
const NLMSG_ERROR: u16 = 0x2;
|
|
const IFA_ADDRESS: u16 = 1;
|
|
const IFA_LOCAL: u16 = 2;
|
|
const RTA_OIF: u16 = 4;
|
|
const RTA_GATEWAY: u16 = 5;
|
|
const RT_SCOPE_UNIVERSE: u8 = 0;
|
|
const RT_TABLE_MAIN: u8 = 254;
|
|
const RTPROT_BOOT: u8 = 3;
|
|
const RTN_UNICAST: u8 = 1;
|
|
|
|
// libc trae nlmsghdr/nlmsgerr/ifinfomsg pero NO estos tres de rtnetlink; los definimos (layout ABI).
|
|
#[repr(C)]
|
|
struct Rtattr {
|
|
rta_len: u16,
|
|
rta_type: u16,
|
|
}
|
|
#[repr(C)]
|
|
struct Ifaddrmsg {
|
|
ifa_family: u8,
|
|
ifa_prefixlen: u8,
|
|
ifa_flags: u8,
|
|
ifa_scope: u8,
|
|
ifa_index: u32,
|
|
}
|
|
#[repr(C)]
|
|
struct Rtmsg {
|
|
rtm_family: u8,
|
|
rtm_dst_len: u8,
|
|
rtm_src_len: u8,
|
|
rtm_tos: u8,
|
|
rtm_table: u8,
|
|
rtm_protocol: u8,
|
|
rtm_scope: u8,
|
|
rtm_type: u8,
|
|
rtm_flags: u32,
|
|
}
|
|
|
|
fn nlmsg_align(len: usize) -> usize {
|
|
(len + 3) & !3
|
|
}
|
|
|
|
unsafe fn as_bytes<T>(p: &T) -> &[u8] {
|
|
std::slice::from_raw_parts(p as *const T as *const u8, size_of::<T>())
|
|
}
|
|
|
|
/// Socket netlink + número de secuencia para los requests.
|
|
pub struct Netlink {
|
|
fd: RawFd,
|
|
seq: u32,
|
|
}
|
|
|
|
impl Drop for Netlink {
|
|
fn drop(&mut self) {
|
|
unsafe { libc::close(self.fd) };
|
|
}
|
|
}
|
|
|
|
impl Netlink {
|
|
pub fn open() -> io::Result<Self> {
|
|
let fd = unsafe {
|
|
libc::socket(
|
|
libc::AF_NETLINK,
|
|
libc::SOCK_RAW | libc::SOCK_CLOEXEC,
|
|
libc::NETLINK_ROUTE,
|
|
)
|
|
};
|
|
if fd < 0 {
|
|
return Err(io::Error::last_os_error());
|
|
}
|
|
let mut sa: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
|
|
sa.nl_family = libc::AF_NETLINK as u16;
|
|
let r = unsafe {
|
|
libc::bind(
|
|
fd,
|
|
&sa as *const _ as *const libc::sockaddr,
|
|
size_of::<libc::sockaddr_nl>() as u32,
|
|
)
|
|
};
|
|
if r < 0 {
|
|
let e = io::Error::last_os_error();
|
|
unsafe { libc::close(fd) };
|
|
return Err(e);
|
|
}
|
|
Ok(Self { fd, seq: 0 })
|
|
}
|
|
|
|
/// Empaqueta nlmsghdr + cuerpo + atributos, lo manda al kernel y espera el ACK (NLMSG_ERROR con
|
|
/// error==0). `body` es el struct de familia (ifinfomsg/ifaddrmsg/rtmsg); `attrs` los rtattr.
|
|
fn request(&mut self, msg_type: u16, flags: u16, body: &[u8], attrs: &[(u16, Vec<u8>)]) -> io::Result<()> {
|
|
self.seq += 1;
|
|
let mut buf: Vec<u8> = Vec::with_capacity(256);
|
|
buf.resize(size_of::<libc::nlmsghdr>(), 0); // hueco para el header, se rellena al final
|
|
buf.extend_from_slice(body);
|
|
while buf.len() % 4 != 0 {
|
|
buf.push(0);
|
|
}
|
|
for (atype, data) in attrs {
|
|
let rta_len = (size_of::<Rtattr>() + data.len()) as u16;
|
|
let rta = Rtattr { rta_len, rta_type: *atype };
|
|
buf.extend_from_slice(unsafe { as_bytes(&rta) });
|
|
buf.extend_from_slice(data);
|
|
let pad = nlmsg_align(rta_len as usize) - rta_len as usize;
|
|
buf.extend(std::iter::repeat(0u8).take(pad));
|
|
}
|
|
let hdr = libc::nlmsghdr {
|
|
nlmsg_len: buf.len() as u32,
|
|
nlmsg_type: msg_type,
|
|
nlmsg_flags: flags,
|
|
nlmsg_seq: self.seq,
|
|
nlmsg_pid: 0,
|
|
};
|
|
buf[..size_of::<libc::nlmsghdr>()].copy_from_slice(unsafe { as_bytes(&hdr) });
|
|
|
|
let mut k: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
|
|
k.nl_family = libc::AF_NETLINK as u16; // nl_pid 0 ⇒ el kernel
|
|
let sent = unsafe {
|
|
libc::sendto(
|
|
self.fd,
|
|
buf.as_ptr() as *const libc::c_void,
|
|
buf.len(),
|
|
0,
|
|
&k as *const _ as *const libc::sockaddr,
|
|
size_of::<libc::sockaddr_nl>() as u32,
|
|
)
|
|
};
|
|
if sent < 0 {
|
|
return Err(io::Error::last_os_error());
|
|
}
|
|
self.recv_ack()
|
|
}
|
|
|
|
fn recv_ack(&mut self) -> io::Result<()> {
|
|
let mut rbuf = [0u8; 8192];
|
|
let n = unsafe {
|
|
libc::recv(
|
|
self.fd,
|
|
rbuf.as_mut_ptr() as *mut libc::c_void,
|
|
rbuf.len(),
|
|
0,
|
|
)
|
|
};
|
|
if n < 0 {
|
|
return Err(io::Error::last_os_error());
|
|
}
|
|
let n = n as usize;
|
|
if n < size_of::<libc::nlmsghdr>() {
|
|
return Err(io::Error::new(io::ErrorKind::InvalidData, "respuesta netlink truncada"));
|
|
}
|
|
let hdr: libc::nlmsghdr = unsafe { std::ptr::read_unaligned(rbuf.as_ptr() as *const _) };
|
|
if hdr.nlmsg_type == NLMSG_ERROR {
|
|
// nlmsgerr.error: 0 = éxito, negativo = -errno.
|
|
let err: libc::nlmsgerr =
|
|
unsafe { std::ptr::read_unaligned(rbuf.as_ptr().add(size_of::<libc::nlmsghdr>()) as *const _) };
|
|
if err.error != 0 {
|
|
return Err(io::Error::from_raw_os_error(-err.error));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Levanta el link (equivalente a `ip link set <if> up`).
|
|
pub fn link_up(&mut self, ifindex: i32) -> io::Result<()> {
|
|
let mut ifi: libc::ifinfomsg = unsafe { std::mem::zeroed() };
|
|
ifi.ifi_family = libc::AF_UNSPEC as u8;
|
|
ifi.ifi_index = ifindex;
|
|
ifi.ifi_flags = libc::IFF_UP as u32;
|
|
ifi.ifi_change = libc::IFF_UP as u32;
|
|
self.request(RTM_NEWLINK, NLM_F_REQUEST | NLM_F_ACK, unsafe { as_bytes(&ifi) }, &[])
|
|
}
|
|
|
|
/// Añade una dirección IPv4 con su prefijo (equivalente a `ip addr add ip/prefix dev <if>`).
|
|
pub fn add_addr(&mut self, ifindex: i32, ip: [u8; 4], prefix: u8) -> io::Result<()> {
|
|
let mut ifa: Ifaddrmsg = unsafe { std::mem::zeroed() };
|
|
ifa.ifa_family = libc::AF_INET as u8;
|
|
ifa.ifa_prefixlen = prefix;
|
|
ifa.ifa_scope = RT_SCOPE_UNIVERSE;
|
|
ifa.ifa_index = ifindex as u32;
|
|
self.request(
|
|
RTM_NEWADDR,
|
|
NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_REPLACE,
|
|
unsafe { as_bytes(&ifa) },
|
|
&[(IFA_LOCAL, ip.to_vec()), (IFA_ADDRESS, ip.to_vec())],
|
|
)
|
|
}
|
|
|
|
/// Añade la ruta por defecto (equivalente a `ip route add default via <gw> dev <if>`).
|
|
pub fn add_default_route(&mut self, ifindex: i32, gw: [u8; 4]) -> io::Result<()> {
|
|
let mut rtm: Rtmsg = unsafe { std::mem::zeroed() };
|
|
rtm.rtm_family = libc::AF_INET as u8;
|
|
rtm.rtm_table = RT_TABLE_MAIN;
|
|
rtm.rtm_protocol = RTPROT_BOOT;
|
|
rtm.rtm_scope = RT_SCOPE_UNIVERSE;
|
|
rtm.rtm_type = RTN_UNICAST;
|
|
self.request(
|
|
RTM_NEWROUTE,
|
|
NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE,
|
|
unsafe { as_bytes(&rtm) },
|
|
&[(RTA_GATEWAY, gw.to_vec()), (RTA_OIF, (ifindex as u32).to_ne_bytes().to_vec())],
|
|
)
|
|
}
|
|
}
|