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 815Rust 56Shell 37Markdown 291229 Definiciones

⎇ main crates/netup/src/netlink.rs Rust · 244 líneas · 8.6 KB Historia Quién tocó qué crudo

netup: la puerta puede estar FUERA del prefijo — sin ruta on-link la caja queda con IP y sin salida be383ea48 · Sergio ·

1//! Netlink (NETLINK_ROUTE) mínimo y SÍNCRONO sobre libc: levantar un link, añadir una dirección IPv4 y
2//! una ruta por defecto. Hand-roll de los mensajes RTM_* (ABI estable del kernel) en vez de tirar de
3//! `rtnetlink` (async/tokio) — el workspace es 100% sync y así no agregamos deps que vendorar.
4
5use std::io;
6use std::mem::size_of;
7use std::os::unix::io::RawFd;
8
9// --- constantes de la ABI netlink (estables) ---
10const RTM_NEWLINK: u16 = 16;
11const RTM_NEWADDR: u16 = 20;
12const RTM_NEWROUTE: u16 = 24;
13const NLM_F_REQUEST: u16 = 0x001;
14const NLM_F_ACK: u16 = 0x004;
15const NLM_F_REPLACE: u16 = 0x100;
16const NLM_F_CREATE: u16 = 0x400;
17const NLMSG_ERROR: u16 = 0x2;
18const IFA_ADDRESS: u16 = 1;
19const IFA_LOCAL: u16 = 2;
20const RTA_DST: u16 = 1;
21const RTA_OIF: u16 = 4;
22const RTA_GATEWAY: u16 = 5;
23const RT_SCOPE_UNIVERSE: u8 = 0;
24const RT_SCOPE_LINK: u8 = 253;
25const RT_TABLE_MAIN: u8 = 254;
26const RTPROT_BOOT: u8 = 3;
27const RTN_UNICAST: u8 = 1;
28
29// libc trae nlmsghdr/nlmsgerr/ifinfomsg pero NO estos tres de rtnetlink; los definimos (layout ABI).
30#[repr(C)]
31struct Rtattr {
32    rta_len: u16,
33    rta_type: u16,
34}
35#[repr(C)]
36struct Ifaddrmsg {
37    ifa_family: u8,
38    ifa_prefixlen: u8,
39    ifa_flags: u8,
40    ifa_scope: u8,
41    ifa_index: u32,
42}
43#[repr(C)]
44struct Rtmsg {
45    rtm_family: u8,
46    rtm_dst_len: u8,
47    rtm_src_len: u8,
48    rtm_tos: u8,
49    rtm_table: u8,
50    rtm_protocol: u8,
51    rtm_scope: u8,
52    rtm_type: u8,
53    rtm_flags: u32,
54}
55
56fn nlmsg_align(len: usize) -> usize {
57    (len + 3) & !3
58}
59
60unsafe fn as_bytes<T>(p: &T) -> &[u8] {
61    std::slice::from_raw_parts(p as *const T as *const u8, size_of::<T>())
62}
63
64/// Socket netlink + número de secuencia para los requests.
65pub struct Netlink {
66    fd: RawFd,
67    seq: u32,
68}
69
70impl Drop for Netlink {
71    fn drop(&mut self) {
72        unsafe { libc::close(self.fd) };
73    }
74}
75
76impl Netlink {
77    pub fn open() -> io::Result<Self> {
78        let fd = unsafe {
79            libc::socket(
80                libc::AF_NETLINK,
81                libc::SOCK_RAW | libc::SOCK_CLOEXEC,
82                libc::NETLINK_ROUTE,
83            )
84        };
85        if fd < 0 {
86            return Err(io::Error::last_os_error());
87        }
88        let mut sa: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
89        sa.nl_family = libc::AF_NETLINK as u16;
90        let r = unsafe {
91            libc::bind(
92                fd,
93                &sa as *const _ as *const libc::sockaddr,
94                size_of::<libc::sockaddr_nl>() as u32,
95            )
96        };
97        if r < 0 {
98            let e = io::Error::last_os_error();
99            unsafe { libc::close(fd) };
100            return Err(e);
101        }
102        Ok(Self { fd, seq: 0 })
103    }
104
105    /// Empaqueta nlmsghdr + cuerpo + atributos, lo manda al kernel y espera el ACK (NLMSG_ERROR con
106    /// error==0). `body` es el struct de familia (ifinfomsg/ifaddrmsg/rtmsg); `attrs` los rtattr.
107    fn request(&mut self, msg_type: u16, flags: u16, body: &[u8], attrs: &[(u16, Vec<u8>)]) -> io::Result<()> {
108        self.seq += 1;
109        let mut buf: Vec<u8> = Vec::with_capacity(256);
110        buf.resize(size_of::<libc::nlmsghdr>(), 0); // hueco para el header, se rellena al final
111        buf.extend_from_slice(body);
112        while buf.len() % 4 != 0 {
113            buf.push(0);
114        }
115        for (atype, data) in attrs {
116            let rta_len = (size_of::<Rtattr>() + data.len()) as u16;
117            let rta = Rtattr { rta_len, rta_type: *atype };
118            buf.extend_from_slice(unsafe { as_bytes(&rta) });
119            buf.extend_from_slice(data);
120            let pad = nlmsg_align(rta_len as usize) - rta_len as usize;
121            buf.extend(std::iter::repeat(0u8).take(pad));
122        }
123        let hdr = libc::nlmsghdr {
124            nlmsg_len: buf.len() as u32,
125            nlmsg_type: msg_type,
126            nlmsg_flags: flags,
127            nlmsg_seq: self.seq,
128            nlmsg_pid: 0,
129        };
130        buf[..size_of::<libc::nlmsghdr>()].copy_from_slice(unsafe { as_bytes(&hdr) });
131
132        let mut k: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
133        k.nl_family = libc::AF_NETLINK as u16; // nl_pid 0 ⇒ el kernel
134        let sent = unsafe {
135            libc::sendto(
136                self.fd,
137                buf.as_ptr() as *const libc::c_void,
138                buf.len(),
139                0,
140                &k as *const _ as *const libc::sockaddr,
141                size_of::<libc::sockaddr_nl>() as u32,
142            )
143        };
144        if sent < 0 {
145            return Err(io::Error::last_os_error());
146        }
147        self.recv_ack()
148    }
149
150    fn recv_ack(&mut self) -> io::Result<()> {
151        let mut rbuf = [0u8; 8192];
152        let n = unsafe {
153            libc::recv(
154                self.fd,
155                rbuf.as_mut_ptr() as *mut libc::c_void,
156                rbuf.len(),
157                0,
158            )
159        };
160        if n < 0 {
161            return Err(io::Error::last_os_error());
162        }
163        let n = n as usize;
164        if n < size_of::<libc::nlmsghdr>() {
165            return Err(io::Error::new(io::ErrorKind::InvalidData, "respuesta netlink truncada"));
166        }
167        let hdr: libc::nlmsghdr = unsafe { std::ptr::read_unaligned(rbuf.as_ptr() as *const _) };
168        if hdr.nlmsg_type == NLMSG_ERROR {
169            // nlmsgerr.error: 0 = éxito, negativo = -errno.
170            let err: libc::nlmsgerr =
171                unsafe { std::ptr::read_unaligned(rbuf.as_ptr().add(size_of::<libc::nlmsghdr>()) as *const _) };
172            if err.error != 0 {
173                return Err(io::Error::from_raw_os_error(-err.error));
174            }
175        }
176        Ok(())
177    }
178
179    /// Levanta el link (equivalente a `ip link set <if> up`).
180    pub fn link_up(&mut self, ifindex: i32) -> io::Result<()> {
181        let mut ifi: libc::ifinfomsg = unsafe { std::mem::zeroed() };
182        ifi.ifi_family = libc::AF_UNSPEC as u8;
183        ifi.ifi_index = ifindex;
184        ifi.ifi_flags = libc::IFF_UP as u32;
185        ifi.ifi_change = libc::IFF_UP as u32;
186        self.request(RTM_NEWLINK, NLM_F_REQUEST | NLM_F_ACK, unsafe { as_bytes(&ifi) }, &[])
187    }
188
189    /// Añade una dirección IPv4 con su prefijo (equivalente a `ip addr add ip/prefix dev <if>`).
190    pub fn add_addr(&mut self, ifindex: i32, ip: [u8; 4], prefix: u8) -> io::Result<()> {
191        let mut ifa: Ifaddrmsg = unsafe { std::mem::zeroed() };
192        ifa.ifa_family = libc::AF_INET as u8;
193        ifa.ifa_prefixlen = prefix;
194        ifa.ifa_scope = RT_SCOPE_UNIVERSE;
195        ifa.ifa_index = ifindex as u32;
196        self.request(
197            RTM_NEWADDR,
198            NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_REPLACE,
199            unsafe { as_bytes(&ifa) },
200            &[(IFA_LOCAL, ip.to_vec()), (IFA_ADDRESS, ip.to_vec())],
201        )
202    }
203
204    /// Añade la ruta por defecto (equivalente a `ip route add default via <gw> dev <if>`).
205    /// Ruta de scope LINK a un host — la que vuelve alcanzable una puerta que NO cae dentro del
206    /// prefijo de la dirección.
207    ///
208    /// Hace falta de verdad, no por prolijidad: **Hetzner Cloud entrega la IPv4 con prefijo `/32` y
209    /// una puerta (`172.31.1.1`) que no pertenece a ninguna red conectada** (medido el 2026-09-10 en
210    /// la caja `takana`). Sin esta ruta previa, el `RTM_NEWROUTE` del default falla con
211    /// `ENETUNREACH` y la máquina queda **con IP y sin salida**: contesta el DHCP, levanta sshd, y
212    /// no se puede entrar. Es lo que hace `ip route add <gw> dev <if> scope link` antes del default,
213    /// y lo que la propia configuración del rescue de Hetzner muestra en `ip route`.
214    pub fn add_onlink_host_route(&mut self, ifindex: i32, dst: [u8; 4]) -> io::Result<()> {
215        let mut rtm: Rtmsg = unsafe { std::mem::zeroed() };
216        rtm.rtm_family = libc::AF_INET as u8;
217        rtm.rtm_dst_len = 32;
218        rtm.rtm_table = RT_TABLE_MAIN;
219        rtm.rtm_protocol = RTPROT_BOOT;
220        rtm.rtm_scope = RT_SCOPE_LINK;
221        rtm.rtm_type = RTN_UNICAST;
222        self.request(
223            RTM_NEWROUTE,
224            NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE,
225            unsafe { as_bytes(&rtm) },
226            &[(RTA_DST, dst.to_vec()), (RTA_OIF, (ifindex as u32).to_ne_bytes().to_vec())],
227        )
228    }
229
230    pub fn add_default_route(&mut self, ifindex: i32, gw: [u8; 4]) -> io::Result<()> {
231        let mut rtm: Rtmsg = unsafe { std::mem::zeroed() };
232        rtm.rtm_family = libc::AF_INET as u8;
233        rtm.rtm_table = RT_TABLE_MAIN;
234        rtm.rtm_protocol = RTPROT_BOOT;
235        rtm.rtm_scope = RT_SCOPE_UNIVERSE;
236        rtm.rtm_type = RTN_UNICAST;
237        self.request(
238            RTM_NEWROUTE,
239            NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE,
240            unsafe { as_bytes(&rtm) },
241            &[(RTA_GATEWAY, gw.to_vec()), (RTA_OIF, (ifindex as u32).to_ne_bytes().to_vec())],
242        )
243    }
244}