gioser

tawasuyu/brahman

Rust 582TOML 222Markdown 83JSON 198635 Definiciones

feat(renaser): Fase 18 — red: virtio-net y el primer hola al exterior

renaser hablaba consigo mismo. Esta fase abre una boca y una oreja al
exterior con una tarjeta de red, reutilizando el `KernelHal` del
disco y el mapeador MMIO (la pieza estructural que hizo esto posible).

- `drivers/red`: monta `VirtIONet<KernelHal, PciTransport, 16>`,
  expone `enviar(frame)` y `drenar_rx(callback)`. Sin pila TCP/IP —
  solo Ethernet crudo; la composición de paquetes la hace el llamante.
- `componer_arp_request(mac, ip, objetivo)` construye el saludo
  inicial: «¿quien tiene 10.0.2.2?» dirigido al gateway de QEMU.
- `interrupts::registrar_irq_red` + handler `irq_red`, gemelo del de
  disco. La IRQ del dispositivo activa `red::atender_irq`, que hace
  `ack_interrupt` y suelta la línea.
- `tarea_red` en el reactor: al arrancar envía el ARP, después cada
  fotograma drena la cola RX y vuelca cada paquete a COM1.
- QEMU args ganan `-netdev user,id=net0 -device virtio-net-pci`.

Verificado con `-object filter-dump,...,file=/tmp/red.pcap`:
  red :: virtio-net :: MAC 52:54:00:12:34:56 :: IRQ Some(11)
  red :: ARP REQUEST enviado :: ¿quien tiene 10.0.2.2?
  red :: RX 64 bytes :: src=52:55:0a:00:02:02 type=0x0806

El src del paquete entrante (`52:55:0a:00:02:02`) codifica `10.0.2.2`
dentro del MAC — es el gateway de QEMU respondiendo. Renaser ya habla
con el exterior.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

bdd088b89ebc722d6679e01ef8c5c9b174f797fc · por sergio · (hace 5 meses) · padre 60553bec4 · ver el árbol en este commit

Delta semántico · 31

renaser/boot/src/main.rs

renaser/kernel/src/drivers/mod.rs

  • nueva mod red pub

renaser/kernel/src/drivers/red.rs

renaser/kernel/src/interrupts.rs

renaser/kernel/src/main.rs

9 archivos · +420 −6

diff --git a/renaser/CHANGELOG.md b/renaser/CHANGELOG.md
index fd5c765..d88a7fc 100644
--- a/renaser/CHANGELOG.md
+++ b/renaser/CHANGELOG.md
@@ -1119,3 +1119,40 @@ sigue ahí. La huella vive en el grafo de objetos, como todo lo demás.
   texto justo donde quedó. El `almacen` reporta 24 objetos en el grafo
   (frente a 9 antes de escribir) y `raiz presente`: cada `guardar` anexó
   una versión al log direccionado por contenido.
+
+## Fase 18 — Red: virtio-net y el primer hola al exterior — 2026-05-23
+
+renaser hablaba consigo mismo. Con la misma plantilla del disco —enumerar
+PCI, montar el transporte virtio, ceder a `virtio-drivers` el diálogo de
+bajo nivel— y reutilizando el `KernelHal` y el mapeador `memory::mmio`,
+el kernel abre ahora una boca y una oreja al exterior: una tarjeta de red.
+
+### Añadido
+- **Driver `drivers/red`** — monta `VirtIONet<KernelHal, PciTransport, 16>`,
+  enruta su IRQ vía el PIC, expone `enviar(frame)` y `drenar_rx(callback)`.
+  La cabeza del archivo guarda el MAC del dispositivo y las constantes de
+  IP de QEMU user-mode (`10.0.2.15` invitado, `10.0.2.2` gateway).
+- **`componer_arp_request(mac, ip, objetivo)`** — construye un frame
+  Ethernet + ARP listo para enviar.
+- **`interrupts::registrar_irq_red`** + `extern "x86-interrupt" irq_red`
+  — mismo patrón que la IRQ del disco. Acknowledge en el dispositivo,
+  EOI al PIC.
+- **`KernelHal` se hace `pub`** para que `red` lo reutilice como su Hal
+  de DMA — la misma arena de marcos físicos que el disco. El mapeador
+  MMIO (Fase 13.5) cubre los BAR del nuevo dispositivo sin tocar nada.
+- **`tarea_red`**: tras 10 fotogramas para que la cola RX se estabilice,
+  envía un ARP request al gateway de QEMU. Después, cada fotograma drena
+  la cola RX y vuelca cada paquete a COM1.
+- **QEMU args**: `-netdev user,id=net0 -device virtio-net-pci,netdev=net0`
+  (user-mode networking, NAT virtual al host).
+
+### Verificado
+- QEMU con `-object filter-dump,...,file=/tmp/red.pcap`. Trazas COM1:
+  ```
+  red :: virtio-net :: MAC 52:54:00:12:34:56 :: IRQ Some(11)
+  red :: ARP REQUEST enviado :: ¿quien tiene 10.0.2.2?
+  red :: RX 64 bytes :: dst=52:54:00:12:34:56 src=52:55:0a:00:02:02 type=0x0806
+  ```
+  El src del paquete entrante codifica la IP del gateway dentro del MAC
+  (`52:55:0a:00:02:02` = QEMU prefix + `10.0.2.2`): la respuesta ARP es
+  legítima. Renaser ya habla con la red.
diff --git a/renaser/CLAUDE.md b/renaser/CLAUDE.md
index f616ec0..823a4a1 100644
--- a/renaser/CLAUDE.md
+++ b/renaser/CLAUDE.md
@@ -89,9 +89,10 @@ clic-para-enfocar—, la Fase 15 COMPLETA —la voz del sistema: acorde al
 arrancar, repique al lanzar o cerrar, bajo al desalojar, con prioridad
 sobre `sys_tono`— la Fase 16 COMPLETA —la barra viva: botón «+»
 lanzador a la izquierda y reloj `mm:ss` a la derecha que late cada
-segundo— y la Fase 17 COMPLETA —`bitacora`, editor de texto que persiste
-entre arranques en el grafo de objetos (tipografía 8×8 embebida)—.
-Todo verificado en QEMU. Ver `ROADMAP.md`.
+segundo— la Fase 17 COMPLETA —`bitacora`, editor de texto que persiste entre
+arranques en el grafo de objetos (tipografía 8×8 embebida)— y la Fase 18
+COMPLETA —red: virtio-net + ARP al gateway de QEMU + recepción de
+paquetes registrada por COM1—. Todo verificado en QEMU. Ver `ROADMAP.md`.
 
 ## Flujo de trabajo
 
diff --git a/renaser/DIARIO.md b/renaser/DIARIO.md
index b8bdc8f..b1ee7ad 100644
--- a/renaser/DIARIO.md
+++ b/renaser/DIARIO.md
@@ -568,6 +568,20 @@ apagar y volver a encender, el papel vuelve a su sitio con cada palabra
 intacta. Apaga, enciende, sigue escribiendo. La casa no olvida lo que se
 le confía.
 
+## El primer saludo afuera — la red
+
+Hasta hoy, la casa era un islote completo: tenía cuartos, voces, manos y
+ahora memoria, pero no había forma de hablar con nada de lo que estuviera
+afuera. Hoy le crece una pequeña puerta lateral —una tarjeta de red— y por
+ella sale un saludo. El primero: una pregunta sencilla a quien hubiese
+escuchando. «¿Quién es 10.0.2.2?», dice. Y la red contesta: «yo». La casa
+toma nota, lo deja escrito en su libro de marcas, y se queda atenta.
+
+A partir de aquí queda mucho por construir —entender lo que llega, hablar
+en idiomas más altos como TCP, abrir capacidades para que sus inquilinos
+también puedan dialogar— pero lo grueso ya está. La casa dejó de hablar
+sola.
+
 ---
 
 *El diario continúa. La próxima página la escribirá la próxima jornada.*
diff --git a/renaser/ROADMAP.md b/renaser/ROADMAP.md
index 8973ea5..fef51bb 100644
--- a/renaser/ROADMAP.md
+++ b/renaser/ROADMAP.md
@@ -303,8 +303,25 @@ texto sigue ahí. Verificada en QEMU.
 - `CELDA_TASKBAR_ANCHO` baja de 150 a 130 px para que las ocho pestañas
   quepan holgadas con el lanzador y el reloj.
 
-Líneas abiertas posteriores: reciclado de las ranuras de ventana cerradas;
-audio con varias voces (PCM) más allá del tono único de la bocina.
+## Fase 18 — red: virtio-net y el primer hola al exterior (completada)
+
+renaser hablaba solo. Esta fase abre una boca y una oreja al exterior con
+virtio-net, reutilizando el `KernelHal` del disco y el mapeador MMIO. Al
+arrancar, el kernel envía un ARP request al gateway de QEMU; lee la
+respuesta y la registra. Verificada con captura pcap.
+
+- Driver `drivers/red`: monta `VirtIONet<KernelHal, PciTransport, 16>`,
+  expone `enviar(frame)` y `drenar_rx(callback)`. Sin pila TCP/IP — solo
+  ethernet crudo —; la composición de paquetes se hace en el llamante.
+- `componer_arp_request`: helper para construir el ARP request inicial.
+- `interrupts::registrar_irq_red` + handler de IDT, gemelo del de disco.
+- Tarea `tarea_red` en el reactor: envía el ARP al arrancar, drena RX en
+  cada fotograma y vuelca cada paquete a COM1.
+- QEMU args ganan `-netdev user,id=net0 -device virtio-net-pci`.
+
+Líneas abiertas posteriores: capacidades `sys_net_*` para que los apps
+también hablen; una pila mínima ARP/IP/UDP; reciclado de las ranuras de
+ventana cerradas; audio con varias voces (PCM).
 
 ## Principios que persisten entre fases
 
diff --git a/renaser/boot/src/main.rs b/renaser/boot/src/main.rs
index 8cb309c..b91762d 100644
--- a/renaser/boot/src/main.rs
+++ b/renaser/boot/src/main.rs
@@ -327,7 +327,13 @@ fn lanzar_qemu(imagen: &Path, ovmf: &str) -> Result<(), String> {
         .arg("--no-reboot")
         // El disco de objetos, como dispositivo virtio-blk sobre el bus PCI.
         .arg("-drive").arg(format!("format=raw,file={NOMBRE_DISCO},if=none,id=drv0"))
-        .arg("-device").arg("virtio-blk-pci,drive=drv0");
+        .arg("-device").arg("virtio-blk-pci,drive=drv0")
+        // FASE 18 :: la tarjeta de red — `user mode networking` de QEMU, un
+        // NAT virtual hacia el host. Sin opciones extra: gateway en 10.0.2.2,
+        // DHCP/DNS en 10.0.2.3, el invitado en 10.0.2.15. El kernel envia un
+        // ARP request al gateway en cuanto arranca como prueba de vida.
+        .arg("-netdev").arg("user,id=net0")
+        .arg("-device").arg("virtio-net-pci,netdev=net0");
 
     // Cualquier argumento extra tras `--` se reenvia a QEMU intacto.
     // Ejemplo: `cargo run -p boot -- -display none -d int`.
diff --git a/renaser/kernel/src/drivers/mod.rs b/renaser/kernel/src/drivers/mod.rs
index 25a8258..faa26dd 100644
--- a/renaser/kernel/src/drivers/mod.rs
+++ b/renaser/kernel/src/drivers/mod.rs
@@ -13,9 +13,12 @@
 //                  (Fase 12).
 //    * `raton`   — el raton PS/2: el dispositivo auxiliar del 8042 + IRQ12,
 //                  paquetes de 3 bytes (Fase 13).
+//    * `red`     — la tarjeta virtio-net sobre PCI: ethernet crudo,
+//                  primer ARP al gateway de QEMU (Fase 18).
 // =============================================================================
 
 pub mod altavoz;
 pub mod disco;
 pub mod pci;
 pub mod raton;
+pub mod red;
diff --git a/renaser/kernel/src/drivers/red.rs b/renaser/kernel/src/drivers/red.rs
new file mode 100644
index 0000000..9291b56
--- /dev/null
+++ b/renaser/kernel/src/drivers/red.rs
@@ -0,0 +1,242 @@
+// =============================================================================
+//  renaser :: kernel/src/drivers/red.rs — Fase 18 :: virtio-net
+// -----------------------------------------------------------------------------
+//  El kernel deja de hablar solo consigo mismo. Con el mismo patron del disco
+//  —enumerar PCI, montar el transporte de virtio, ceder a `virtio-drivers` el
+//  diálogo de bajo nivel— renaser abre una boca y una oreja al exterior: una
+//  tarjeta de red virtio.
+//
+//  En esta primera version el kernel envia un ARP request al gateway de
+//  QEMU (10.0.2.2) en cuanto arranca, y registra por COM1 cada paquete que
+//  recibe. No hay pila TCP/IP — solo ethernet crudo. El proximo paso natural
+//  seria una capa de capacidades `sys_net_*` para que los apps tambien
+//  hablen, pero esa es otra fase.
+// =============================================================================
+
+use core::sync::atomic::{AtomicU64, AtomicU8, Ordering};
+
+use spin::{Mutex, Once};
+use virtio_drivers::device::net::VirtIONet;
+use virtio_drivers::transport::pci::bus::{Command, DeviceFunction, PciRoot};
+use virtio_drivers::transport::pci::PciTransport;
+use x86_64::instructions::interrupts;
+
+use super::disco::KernelHal;
+use super::pci::CamPuertos;
+
+/// Vendor ID de VirtIO; Device IDs de un dispositivo de red (legacy + modern).
+const VENDOR_VIRTIO: u16 = 0x1AF4;
+const VIRTIO_NET_IDS: [u16; 2] = [0x1000, 0x1041];
+
+/// Tamaño maximo de paquete que reservamos por bufer (MTU 1500 + algo de holgura
+/// para cabeceras virtio y futuras VLAN).
+const PAQUETE_MAX: usize = 1600;
+
+/// Profundidad de las colas RX y TX. 16 es pequeño pero suficiente para el
+/// trafico de un demo.
+const PROFUNDIDAD_COLA: usize = 16;
+
+/// EtherType experimental (rango 0x88B5-0x88B6, reservado por IEEE para uso
+/// local). renaser lo usaria si quisiera definir su propio protocolo.
+pub const ETHER_TYPE_RENASER: u16 = 0x88B5;
+/// EtherType de ARP.
+pub const ETHER_TYPE_ARP: u16 = 0x0806;
+
+/// Direccion fisica de la tarjeta de red, en seis bytes MAC.
+pub type Mac = [u8; 6];
+
+/// IP de la maquina renaser, en QEMU user-mode networking (10.0.2.0/24).
+pub const IP_RENASER: [u8; 4] = [10, 0, 2, 15];
+/// IP del gateway que QEMU expone hacia el host.
+pub const IP_GATEWAY: [u8; 4] = [10, 0, 2, 2];
+
+/// La tarjeta de red, ya montada. Envuelve a `VirtIONet` para que pueda vivir
+/// en un `static`.
+struct Tarjeta(VirtIONet<KernelHal, PciTransport, PROFUNDIDAD_COLA>);
+
+// SEGURIDAD: `Tarjeta` encierra punteros crudos a las colas virtio y al MMIO
+// del dispositivo. renaser es de un solo nucleo y todo acceso a la tarjeta se
+// serializa tras el `Mutex` global. Los accesos cooperativos se hacen con las
+// interrupciones acalladas para que la IRQ del dispositivo jamas las dispute.
+unsafe impl Send for Tarjeta {}
+
+/// La tarjeta global. Se monta una sola vez, en `montar`.
+static TARJETA: Once<Mutex<Tarjeta>> = Once::new();
+
+/// La direccion MAC que el dispositivo nos asigno, cacheada para consulta.
+static MAC: Once<Mac> = Once::new();
+
+/// La linea de IRQ asignada al dispositivo por el firmware.
+static IRQ_RED: AtomicU8 = AtomicU8::new(0);
+
+/// Cuenta de paquetes recibidos desde el arranque.
+static PAQUETES_RX: AtomicU64 = AtomicU64::new(0);
+/// Cuenta de paquetes transmitidos desde el arranque.
+static PAQUETES_TX: AtomicU64 = AtomicU64::new(0);
+
+// =============================================================================
+//  Montaje
+// =============================================================================
+
+/// Enumera el bus PCI, localiza el virtio-net, monta su transporte moderno y
+/// lo deja tras el `Mutex` global. Descubre su linea de IRQ y la enruta.
+/// Devuelve la MAC que el dispositivo nos confiere. Toda falla se devuelve.
+pub fn montar() -> Result<Mac, &'static str> {
+    let mut raiz = PciRoot::new(CamPuertos);
+
+    // 1. Localizar el primer virtio-net en el bus.
+    let mut hallado: Option<DeviceFunction> = None;
+    'busqueda: for bus in 0..=255u8 {
+        for (device_function, info) in raiz.enumerate_bus(bus) {
+            if info.vendor_id == VENDOR_VIRTIO && VIRTIO_NET_IDS.contains(&info.device_id) {
+                hallado = Some(device_function);
+                break 'busqueda;
+            }
+        }
+    }
+    let device_function = hallado.ok_or("virtio-net no hallado en el bus PCI")?;
+
+    // 2. Habilitar E/S, MMIO y BUS-MASTER en la configuracion PCI.
+    raiz.set_command(
+        device_function,
+        Command::IO_SPACE | Command::MEMORY_SPACE | Command::BUS_MASTER,
+    );
+
+    // 3. Montar el transporte PCI moderno y el dispositivo de red.
+    let transporte = PciTransport::new::<KernelHal, _>(&mut raiz, device_function)
+        .map_err(|_| "no se pudo montar el transporte PCI de virtio-net")?;
+    let mut nic =
+        VirtIONet::<KernelHal, _, PROFUNDIDAD_COLA>::new(transporte, PAQUETE_MAX)
+            .map_err(|_| "no se pudo inicializar el dispositivo virtio-net")?;
+
+    let mac = nic.mac_address();
+    nic.enable_interrupts();
+
+    TARJETA.call_once(|| Mutex::new(Tarjeta(nic)));
+    MAC.call_once(|| mac);
+
+    // 4. Descubrir la linea de IRQ y enrutarla.
+    let irq = super::pci::linea_irq(device_function);
+    if (2..=15).contains(&irq) {
+        crate::interrupts::registrar_irq_red(irq);
+        crate::pic::desenmascarar(irq);
+        IRQ_RED.store(irq, Ordering::SeqCst);
+    }
+
+    Ok(mac)
+}
+
+// =============================================================================
+//  IRQ
+// =============================================================================
+
+/// Punto de entrada DESDE el manejador de IRQ de la red. Acknowledge en el
+/// dispositivo —para que la linea baje— y se sale.
+pub fn atender_irq() {
+    if let Some(tarjeta) = TARJETA.get() {
+        // SEGURIDAD: en contexto de IRQ las interrupciones ya estan acalladas;
+        // tomar el cerrojo aqui no puede interbloquear con las tareas, que
+        // siempre lo toman con `interrupts::without_interrupts`.
+        let _ = tarjeta.lock().0.ack_interrupt();
+    }
+}
+
+/// La linea de IRQ del dispositivo, si el firmware enruto una util.
+pub fn irq() -> Option<u8> {
+    let v = IRQ_RED.load(Ordering::SeqCst);
+    if v == 0 {
+        None
+    } else {
+        Some(v)
+    }
+}
+
+// =============================================================================
+//  Consulta y E/S — la interfaz para las tareas cooperativas
+// =============================================================================
+
+/// La MAC del dispositivo. `None` si la tarjeta aun no se ha montado.
+#[allow(dead_code)]
+pub fn mac() -> Option<Mac> {
+    MAC.get().copied()
+}
+
+/// Numero de paquetes recibidos desde el arranque.
+#[allow(dead_code)]
+pub fn paquetes_rx() -> u64 {
+    PAQUETES_RX.load(Ordering::Relaxed)
+}
+
+/// Numero de paquetes transmitidos desde el arranque.
+#[allow(dead_code)]
+pub fn paquetes_tx() -> u64 {
+    PAQUETES_TX.load(Ordering::Relaxed)
+}
+
+/// Envia un frame Ethernet crudo (cabecera + payload, sin CRC — el dispositivo
+/// se la añade). El llamante construye el frame entero.
+pub fn enviar(frame: &[u8]) -> Result<(), &'static str> {
+    let tarjeta = TARJETA.get().ok_or("red no montada")?;
+    interrupts::without_interrupts(|| {
+        let mut tarjeta = tarjeta.lock();
+        let mut tx = tarjeta.0.new_tx_buffer(frame.len());
+        tx.packet_mut().copy_from_slice(frame);
+        tarjeta.0.send(tx).map_err(|_| "envio fallido")?;
+        PAQUETES_TX.fetch_add(1, Ordering::Relaxed);
+        Ok(())
+    })
+}
+
+/// Drena los paquetes RX pendientes y aplica `callback` a cada uno. Cada
+/// bufer se recicla a la cola RX al terminar — el dispositivo tiene siempre
+/// receptores listos para la proxima IRQ.
+pub fn drenar_rx<F: FnMut(&[u8])>(mut callback: F) {
+    let Some(tarjeta) = TARJETA.get() else {
+        return;
+    };
+    interrupts::without_interrupts(|| {
+        let mut tarjeta = tarjeta.lock();
+        loop {
+            if !tarjeta.0.can_recv() {
+                break;
+            }
+            let rx = match tarjeta.0.receive() {
+                Ok(r) => r,
+                Err(_) => break,
+            };
+            callback(rx.packet());
+            let _ = tarjeta.0.recycle_rx_buffer(rx);
+            PAQUETES_RX.fetch_add(1, Ordering::Relaxed);
+        }
+    });
+}
+
+// =============================================================================
+//  Composicion de un ARP request — el primer paquete que renaser saluda
+// =============================================================================
+
+/// Compone un frame Ethernet con una peticion ARP que pregunta por la MAC del
+/// host `objetivo_ip`. El gateway de QEMU lo responde — su replica entra por
+/// la cola RX y se registra en COM1 desde la tarea cooperativa de la red.
+pub fn componer_arp_request(
+    nuestro_mac: Mac,
+    nuestro_ip: [u8; 4],
+    objetivo_ip: [u8; 4],
+) -> [u8; 42] {
+    let mut frame = [0u8; 42];
+    // Cabecera Ethernet.
+    frame[0..6].copy_from_slice(&[0xff; 6]); // destino: broadcast
+    frame[6..12].copy_from_slice(&nuestro_mac);
+    frame[12..14].copy_from_slice(&ETHER_TYPE_ARP.to_be_bytes());
+    // Payload ARP (28 bytes).
+    frame[14..16].copy_from_slice(&1u16.to_be_bytes()); // HW type: Ethernet
+    frame[16..18].copy_from_slice(&0x0800u16.to_be_bytes()); // proto: IPv4
+    frame[18] = 6; // HW len
+    frame[19] = 4; // proto len
+    frame[20..22].copy_from_slice(&1u16.to_be_bytes()); // opcode: REQUEST
+    frame[22..28].copy_from_slice(&nuestro_mac); // sender MAC
+    frame[28..32].copy_from_slice(&nuestro_ip); // sender IP
+                                                  // bytes 32..38: target MAC, se quedan a cero
+    frame[38..42].copy_from_slice(&objetivo_ip); // target IP
+    frame
+}
diff --git a/renaser/kernel/src/interrupts.rs b/renaser/kernel/src/interrupts.rs
index 47f436b..92d40b1 100644
--- a/renaser/kernel/src/interrupts.rs
+++ b/renaser/kernel/src/interrupts.rs
@@ -26,6 +26,9 @@ static IDT: CeldaSync<InterruptDescriptorTable> =
 /// legitima del disco vive en el vector 0 (reservado a las excepciones).
 static VECTOR_DISCO: AtomicU8 = AtomicU8::new(0);
 
+/// Vector de la IDT asignado a la IRQ de la red (Fase 18). Mismo patron.
+static VECTOR_RED: AtomicU8 = AtomicU8::new(0);
+
 /// Construye y activa la Interrupt Descriptor Table.
 ///
 /// Debe invocarse una sola vez, durante el arranque, DESPUES de [`gdt::init`].
@@ -78,6 +81,17 @@ pub fn registrar_irq_disco(irq: u8) {
     idt[vector].set_handler_fn(irq_disco);
 }
 
+/// Registra el manejador de la IRQ de la red virtio-net en la IDT (Fase 18).
+/// Gemelo de [`registrar_irq_disco`]: las mismas condiciones de arranque
+/// secuencial garantizan la mutacion segura.
+pub fn registrar_irq_red(irq: u8) {
+    let vector = pic::vector_irq(irq);
+    VECTOR_RED.store(vector, Ordering::SeqCst);
+    // SEGURIDAD: ver `registrar_irq_disco`.
+    let idt: &'static mut InterruptDescriptorTable = unsafe { &mut *IDT.puntero() };
+    idt[vector].set_handler_fn(irq_red);
+}
+
 // =============================================================================
 //  REFLEJOS DE EXCEPCION — las rutinas a las que la CPU salta ante cada fallo
 // =============================================================================
@@ -159,3 +173,11 @@ extern "x86-interrupt" fn irq_disco(_marco: InterruptStackFrame) {
     // PCI es de nivel — anunciar el fin sin haber bajado la linea la reavivaria.
     pic::fin_de_interrupcion(VECTOR_DISCO.load(Ordering::SeqCst));
 }
+
+/// IRQ de la red — virtio-net (Fase 18). Llego un paquete (o un envio termino):
+/// acknowledge en el dispositivo —que baja la linea— y EOI al PIC. Las tareas
+/// cooperativas drenan despues la cola RX y consumen los paquetes.
+extern "x86-interrupt" fn irq_red(_marco: InterruptStackFrame) {
+    crate::drivers::red::atender_irq();
+    pic::fin_de_interrupcion(VECTOR_RED.load(Ordering::SeqCst));
+}
diff --git a/renaser/kernel/src/main.rs b/renaser/kernel/src/main.rs
index b7eb9e3..f612bb4 100644
--- a/renaser/kernel/src/main.rs
+++ b/renaser/kernel/src/main.rs
@@ -174,6 +174,54 @@ async fn tarea_compositor() {
     }
 }
 
+/// FASE 18 — la prueba viva del enlace de red. Envia un ARP request al
+/// gateway de QEMU (10.0.2.2) y registra por COM1 cada paquete que llegue de
+/// vuelta. El primer "hola" de renaser hacia el exterior.
+async fn tarea_red(mac: drivers::red::Mac) {
+    // Dejar un par de fotogramas para que la cola RX se estabilice.
+    for _ in 0..10 {
+        async_system::reloj::EsperaFrame::nueva().await;
+    }
+    // Componer y enviar el ARP request: «¿quien tiene 10.0.2.2?».
+    let frame = drivers::red::componer_arp_request(
+        mac,
+        drivers::red::IP_RENASER,
+        drivers::red::IP_GATEWAY,
+    );
+    match drivers::red::enviar(&frame) {
+        Ok(()) => {
+            let _ = writeln!(
+                baliza::Serie,
+                "red :: ARP REQUEST enviado :: ¿quien tiene 10.0.2.2?"
+            );
+        }
+        Err(motivo) => {
+            let _ = writeln!(baliza::Serie, "red :: envio fallido :: {motivo}");
+        }
+    }
+    // Loop perpetuo: drenar la cola RX y registrar cada paquete en COM1.
+    loop {
+        async_system::reloj::EsperaFrame::nueva().await;
+        drivers::red::drenar_rx(|payload| {
+            if payload.len() < 14 {
+                return;
+            }
+            let etype = u16::from_be_bytes([payload[12], payload[13]]);
+            let src = &payload[6..12];
+            let dst = &payload[0..6];
+            let _ = writeln!(
+                baliza::Serie,
+                "red :: RX {} bytes :: dst={:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x} \
+                 src={:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x} type={:#06x}",
+                payload.len(),
+                dst[0], dst[1], dst[2], dst[3], dst[4], dst[5],
+                src[0], src[1], src[2], src[3], src[4], src[5],
+                etype,
+            );
+        });
+    }
+}
+
 /// FASE 6.2 — la prueba viva de la E/S asincrona. Esta tarea del reactor lee el
 /// sector 0 del disco SIN bloquear: cede la CPU mientras el disco trabaja —las
 /// apps siguen pintando entre tanto— y la IRQ del disco la reanuda cuando el
@@ -511,6 +559,25 @@ fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
     drivers::raton::init(ancho_lienzo, alto_lienzo);
     traza("raton :: listo");
 
+    // --- 6.7. FASE 18 :: montar la tarjeta virtio-net. Si el firmware no
+    //          enruta una linea de IRQ util o no hay dispositivo, el resto
+    //          del arranque sigue — la red NO es critica.
+    let mac_red = drivers::red::montar();
+    match mac_red {
+        Ok(mac) => {
+            let _ = writeln!(
+                baliza::Serie,
+                "red :: virtio-net :: MAC {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x} :: IRQ {:?}",
+                mac[0], mac[1], mac[2], mac[3], mac[4], mac[5],
+                drivers::red::irq(),
+            );
+        }
+        Err(motivo) => {
+            let _ = writeln!(baliza::Serie, "red :: virtio-net :: {motivo}");
+        }
+    }
+    traza("red :: listo");
+
     // --- 7. FASE 7 :: levantar el reactor y poblar el userspace DESDE EL
     //        GRAFO. El kernel ya no empotra los modulos WASM: lee el
     //        Manifiesto de Genesis que `boot` sembro en la imagen de disco e
@@ -530,6 +597,11 @@ fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
     // disco de forma ASINCRONA: la demostracion de que la IRQ del disco
     // conduce la E/S sin detener a las aplicaciones visuales.
     ejecutor.spawn(tarea_sonda_disco());
+    // FASE 18 :: si la tarjeta de red se monto, una tarea le envia un ARP
+    // request al gateway y registra por COM1 los paquetes entrantes.
+    if let Ok(mac) = mac_red {
+        ejecutor.spawn(tarea_red(mac));
+    }
     // FASE 15 :: la voz del sistema da los buenos dias con un acorde de Do
     // mayor. La tarea del compositor lo hara sonar nota a nota una vez que
     // el reactor arranque y las interrupciones empiecen a llegar.