tawasuyu

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 1411Shell 141Markdown 85Rust 842918 Sutichaykuna

⎇ main crates/netup/src/main.rs Rust · 144 siq'ikuna · 5.9 KB Ñawpa willay Pi imata llamkarqan ch'uya

netup: levantar el LOOPBACK — nadie lo hacía, y el síntoma era un timeout de 30 s 36be67fa9 · Sergio ·

1//! netup — configuración de red mínima Rust-nativa para el userland de takana (Etapa C).
2//!
3//! Reemplaza el `ip` de busybox + la IP estática hardcodeada del driver de rebuild por un binario
4//! propio: levanta la interfaz, negocia un lease DHCPv4 y aplica IP/ruta/DNS vía netlink — todo
5//! síncrono, sin tokio, dependiendo sólo de libc (ABI estable del kernel). Como hammerd/arje, es
6//! código takana-propio: no hay un cliente DHCP Rust "maduro" para adoptar al estilo ripgrep.
7//!
8//! Uso: `netup [interfaz]`  (sin argumento, autodetecta la primera interfaz no-loopback).
9
10mod dhcp;
11mod netlink;
12
13use std::process::ExitCode;
14
15fn main() -> ExitCode {
16    match run() {
17        Ok(()) => ExitCode::SUCCESS,
18        Err(e) => {
19            eprintln!("netup: {e}");
20            ExitCode::FAILURE
21        }
22    }
23}
24
25fn run() -> Result<(), Box<dyn std::error::Error>> {
26    let iface = match std::env::args().nth(1) {
27        Some(a) => a,
28        None => detect_iface().ok_or("no encuentro ninguna interfaz no-loopback en /sys/class/net")?,
29    };
30    let ifindex = read_sysfs_int(&iface, "ifindex")? as i32;
31    let mac = read_mac(&iface)?;
32    println!("netup: interfaz {iface} (index {ifindex}, mac {})", fmt_mac(mac));
33
34    let mut nl = netlink::Netlink::open()?;
35
36    // ── EL LOOPBACK, PRIMERO. Nadie más lo levanta. ──────────────────────────────────────────
37    // En un sistema con systemd/OpenRC esto lo hace el init; con arje-zero no lo hacía NADIE, y
38    // `lo` quedaba `DOWN` y sin dirección. Medido el 2026-09-10 en la caja de producción: caddy
39    // escuchando en `0.0.0.0:80` y `curl http://127.0.0.1/` **con TIMEOUT de 30 s** — no
40    // "connection refused", que se diagnostica en un minuto, sino un cuelgue que parece del
41    // servidor. Cualquier servicio que hable con 127.0.0.1 (un repo local, un backend detrás de un
42    // proxy, un socket TCP entre demonios) fallaba igual.
43    //
44    // Va acá porque `netup` es el que configura la red, y no es fatal: si ya estaba levantado, los
45    // errores son EEXIST y no deben tumbar el arranque.
46    if let Ok(lo) = read_sysfs_int("lo", "ifindex") {
47        let lo = lo as i32;
48        if let Err(e) = nl.link_up(lo) {
49            eprintln!("netup: aviso, no pude levantar lo: {e}");
50        }
51        if let Err(e) = nl.add_addr(lo, [127, 0, 0, 1], 8) {
52            eprintln!("netup: aviso, no pude direccionar lo: {e}");
53        }
54    }
55
56    nl.link_up(ifindex)?;
57    // Esperar el carrier (virtio/e1000 lo levantan en ms, pero damos margen).
58    wait_carrier(&iface, std::time::Duration::from_secs(5));
59
60    let lease = dhcp::run(&iface, mac)?;
61    println!(
62        "netup: lease {} /{} gw {} dns {} ({}s) de {}",
63        fmt_ip(lease.ip),
64        lease.prefix,
65        lease.router.map(fmt_ip).unwrap_or_else(|| "-".into()),
66        if lease.dns.is_empty() { "-".into() } else { lease.dns.iter().map(|d| fmt_ip(*d)).collect::<Vec<_>>().join(",") },
67        lease.lease_secs,
68        fmt_ip(lease.server),
69    );
70
71    nl.add_addr(ifindex, lease.ip, lease.prefix)?;
72    if let Some(gw) = lease.router {
73        // La puerta puede NO estar dentro del prefijo (Hetzner Cloud da /32 + gw 172.31.1.1). En ese
74        // caso el default falla con ENETUNREACH salvo que antes exista una ruta on-link a la puerta.
75        // Se intenta siempre: si ya era alcanzable, es una ruta más específica y no molesta. No es
76        // fatal —un EEXIST no debe tumbar la red— pero el default SÍ lo es.
77        if let Err(e) = nl.add_onlink_host_route(ifindex, gw) {
78            eprintln!("netup: aviso, no pude crear la ruta on-link a {}: {e}", fmt_ip(gw));
79        }
80        nl.add_default_route(ifindex, gw)?;
81    }
82    if !lease.dns.is_empty() {
83        write_resolv_conf(&lease.dns)?;
84    }
85    println!("netup: red configurada en {iface}");
86    Ok(())
87}
88
89/// Primera interfaz de /sys/class/net que no sea loopback (type 772 = ARPHRD_LOOPBACK).
90fn detect_iface() -> Option<String> {
91    let mut names: Vec<String> = std::fs::read_dir("/sys/class/net")
92        .ok()?
93        .filter_map(|e| e.ok())
94        .map(|e| e.file_name().to_string_lossy().into_owned())
95        .filter(|n| n != "lo")
96        .filter(|n| read_sysfs_int(n, "type").map(|t| t != 772).unwrap_or(true))
97        .collect();
98    names.sort(); // determinismo (eth0 < eth1)
99    names.into_iter().next()
100}
101
102fn read_sysfs_int(iface: &str, attr: &str) -> Result<i64, Box<dyn std::error::Error>> {
103    let s = std::fs::read_to_string(format!("/sys/class/net/{iface}/{attr}"))?;
104    Ok(s.trim().parse()?)
105}
106
107fn read_mac(iface: &str) -> Result<[u8; 6], Box<dyn std::error::Error>> {
108    let s = std::fs::read_to_string(format!("/sys/class/net/{iface}/address"))?;
109    let bytes: Vec<u8> = s
110        .trim()
111        .split(':')
112        .filter_map(|h| u8::from_str_radix(h, 16).ok())
113        .collect();
114    if bytes.len() != 6 {
115        return Err(format!("MAC inesperada para {iface}: {}", s.trim()).into());
116    }
117    Ok([bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5]])
118}
119
120/// Espera hasta `timeout` a que /sys/class/net/<if>/carrier sea 1 (link físico/virtio listo).
121fn wait_carrier(iface: &str, timeout: std::time::Duration) {
122    let start = std::time::Instant::now();
123    while start.elapsed() < timeout {
124        if let Ok(c) = std::fs::read_to_string(format!("/sys/class/net/{iface}/carrier")) {
125            if c.trim() == "1" {
126                return;
127            }
128        }
129        std::thread::sleep(std::time::Duration::from_millis(100));
130    }
131}
132
133fn write_resolv_conf(dns: &[[u8; 4]]) -> std::io::Result<()> {
134    let body: String = dns.iter().map(|d| format!("nameserver {}\n", fmt_ip(*d))).collect();
135    std::fs::write("/etc/resolv.conf", body)
136}
137
138fn fmt_ip(ip: [u8; 4]) -> String {
139    format!("{}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3])
140}
141
142fn fmt_mac(m: [u8; 6]) -> String {
143    format!("{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", m[0], m[1], m[2], m[3], m[4], m[5])
144}