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 1389Shell 137Markdown 80Rust 802602 Definiciones

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

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 <gw> dev <if> 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x

be383ea485def684cb5886530efcd5333548743d · por Sergio · (hace 1 mes) · padre 8db076bf3 · ver el árbol en este commit

Delta semántico · 4

crates/netup/src/main.rs

  • cambió por dentro fn run

crates/netup/src/netlink.rs

2 archivos · +34 −0

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<dyn std::error::Error>> {
 
     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 <gw> dev <if>`).
+    /// 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 <gw> dev <if> 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;