From be383ea485def684cb5886530efcd5333548743d Mon Sep 17 00:00:00 2001 From: Sergio Date: Thu, 10 Sep 2026 20:09:10 +0000 Subject: [PATCH] =?UTF-8?q?netup:=20la=20puerta=20puede=20estar=20FUERA=20?= =?UTF-8?q?del=20prefijo=20=E2=80=94=20sin=20ruta=20on-link=20la=20caja=20?= =?UTF-8?q?queda=20con=20IP=20y=20sin=20salida?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Medido en la caja `takana` (Hetzner Cloud, hel1) el 2026-09-10. Hetzner entrega la IPv4 así: inet 2.29.29.217/32 scope global eth0 default via 172.31.1.1 dev eth0 172.31.1.1 dev eth0 scope link <-- ESTA es la que faltaba La dirección es un /32 y la puerta no pertenece a ninguna red conectada. `add_default_route` mandaba `RTA_GATEWAY` con scope UNIVERSE y nada más, así que el kernel rechazaba la ruta. **Y el modo de fallo es el peor**: la máquina arranca, `netup` consigue el lease, se pone la IP, arje-zero levanta sshd... y no se puede entrar, porque las respuestas no tienen por dónde salir. Un `ssh` desde fuera da `Connection timed out` y desde dentro no hay nada que se vea roto. Se agrega `add_onlink_host_route` (dst=/32, scope LINK) y se llama ANTES del default. Es lo que hace `ip route add dev scope link`, y es literalmente lo que el propio rescue de Hetzner tiene en su tabla. No es fatal si falla —un EEXIST no debe tumbar la red—; el default sí lo sigue siendo. **Control causal**, en un netns con una dummy y una /32: ip route add default via 172.31.1.1 dev dummy0 => Error: Nexthop has invalid gateway. ip route add 172.31.1.1 dev dummy0 scope link => ok ip route add default via 172.31.1.1 dev dummy0 => ACEPTADO O sea: no es que "ayude", es que era exactamente eso. netup se validaba contra el DHCP de QEMU slirp, que da un /24 con la puerta dentro — por eso nunca apareció hasta tocar una nube de verdad. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x --- crates/netup/src/main.rs | 7 +++++++ crates/netup/src/netlink.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/crates/netup/src/main.rs b/crates/netup/src/main.rs index 8833a29a..e6bff1fa 100644 --- a/crates/netup/src/main.rs +++ b/crates/netup/src/main.rs @@ -49,6 +49,13 @@ fn run() -> Result<(), Box> { nl.add_addr(ifindex, lease.ip, lease.prefix)?; if let Some(gw) = lease.router { + // La puerta puede NO estar dentro del prefijo (Hetzner Cloud da /32 + gw 172.31.1.1). En ese + // caso el default falla con ENETUNREACH salvo que antes exista una ruta on-link a la puerta. + // Se intenta siempre: si ya era alcanzable, es una ruta más específica y no molesta. No es + // fatal —un EEXIST no debe tumbar la red— pero el default SÍ lo es. + if let Err(e) = nl.add_onlink_host_route(ifindex, gw) { + eprintln!("netup: aviso, no pude crear la ruta on-link a {}: {e}", fmt_ip(gw)); + } nl.add_default_route(ifindex, gw)?; } if !lease.dns.is_empty() { diff --git a/crates/netup/src/netlink.rs b/crates/netup/src/netlink.rs index 87989fa1..00d53586 100644 --- a/crates/netup/src/netlink.rs +++ b/crates/netup/src/netlink.rs @@ -17,9 +17,11 @@ 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; @@ -200,6 +202,31 @@ impl Netlink { } /// 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;