//! 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_DST: u16 = 1; const RTA_OIF: u16 = 4; const RTA_GATEWAY: u16 = 5; const RT_SCOPE_UNIVERSE: u8 = 0; const RT_SCOPE_LINK: u8 = 253; 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(p: &T) -> &[u8] { std::slice::from_raw_parts(p as *const T as *const u8, size_of::()) } /// 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 { 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::() 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)]) -> io::Result<()> { self.seq += 1; let mut buf: Vec = Vec::with_capacity(256); buf.resize(size_of::(), 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::() + 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::()].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::() 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::() { 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::()) 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 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 `). 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 dev `). /// Ruta de scope LINK a un host — la que vuelve alcanzable una puerta que NO cae dentro del /// prefijo de la dirección. /// /// Hace falta de verdad, no por prolijidad: **Hetzner Cloud entrega la IPv4 con prefijo `/32` y /// una puerta (`172.31.1.1`) que no pertenece a ninguna red conectada** (medido el 2026-09-10 en /// la caja `takana`). Sin esta ruta previa, el `RTM_NEWROUTE` del default falla con /// `ENETUNREACH` y la máquina queda **con IP y sin salida**: contesta el DHCP, levanta sshd, y /// no se puede entrar. Es lo que hace `ip route add dev scope link` antes del default, /// y lo que la propia configuración del rescue de Hetzner muestra en `ip route`. pub fn add_onlink_host_route(&mut self, ifindex: i32, dst: [u8; 4]) -> io::Result<()> { let mut rtm: Rtmsg = unsafe { std::mem::zeroed() }; rtm.rtm_family = libc::AF_INET as u8; rtm.rtm_dst_len = 32; rtm.rtm_table = RT_TABLE_MAIN; rtm.rtm_protocol = RTPROT_BOOT; rtm.rtm_scope = RT_SCOPE_LINK; rtm.rtm_type = RTN_UNICAST; self.request( RTM_NEWROUTE, NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE, unsafe { as_bytes(&rtm) }, &[(RTA_DST, dst.to_vec()), (RTA_OIF, (ifindex as u32).to_ne_bytes().to_vec())], ) } 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())], ) } }