Skip to content

Latest commit

 

History

History
2409 lines (1956 loc) · 87.4 KB

userland.md

File metadata and controls

2409 lines (1956 loc) · 87.4 KB

NeoDOS Userland Networking + System Configuration + Package Architecture

Versión: v0.3 (implementado) Fecha: 2026-07-05 Estado: Implementado — NET-1.5/1.6/1.8/1.15 completados


Índice

  1. Visión General
  2. net.nxl — Librería de Red Userland
  3. Herramientas NXE de Red
  4. NeoInit System Configuration
  5. Registry Usage Model
  6. NeoPkg — Package System
  7. Necesidades del Kernel
  8. Roadmap
  9. Diagramas de Flujo
  10. Consideraciones de Implementación
  11. Apéndice: Cambios por Archivo

1. Visión General

Este documento define la arquitectura para exponer el subsistema de red del kernel a procesos userland mediante NXL libraries, establece el modelo de configuración del sistema usando Registry + filesystem, y sienta las bases del sistema de paquetes (NeoPkg).

1.1 Principios de diseño

Principio Descripción
Separación de capas net.nxl no toca hardware, no conoce e1000, no implementa TCP/IP
Configuración estructural en Registry Solo estructura del sistema, no datos de usuario
Datos de usuario en NeoFS Logs, perfiles, documentos, configuraciones editables
Componentes CORE protegidos NeoPkg no permite eliminar kernel/NeoInit/NeoFS
API unificada via Ob Todas las operaciones de red usan ob_create(ObType::Socket), ob_set_info, ob_query_info
Carga bajo demanda net.nxl se carga solo cuando se necesita, no al boot
No bloqueo en userland Llamadas bloqueantes (recv, connect) usan KWait/ObWait

1.2 Stack completo

Aplicaciones NXE                          ← Ring 3
  (ipconfig.nxe, ping.nxe, dhcp.nxe)
        |
        v
System Libraries NXL                      ← Ring 3
  (net.nxl)
        |
        v
libneodos (Ob API wrappers)               ← Ring 3
  sys_ob_create, sys_ob_set_info, sys_ob_query_info
        |
        v  (INT 0x80)
├──────────────────────────────────────┤
│ Kernel syscall handlers               │  ← Ring 0
│   handler_ob_create → ObType::Socket  │
│   handler_ob_set_info → SocketSend    │
│   handler_ob_query_info → NicInfo     │
├──────────────────────────────────────┤
        |
        v
Kernel Socket Manager                    ← Ring 0
  (src/net/socket.rs)
        |
        v
Kernel TCP/IP Stack                      ← Ring 0
  (src/net/{tcp,udp,ipv4,icmp,arp,ethernet}.rs)
        |
        v
Kernel NIC Registry / e1000 driver       ← Ring 0
  (src/net/{nic,e1000}.rs)
        |
        v
Hardware (e1000 NIC)                     ← Hardware

2. net.nxl — Librería de Red Userland

2.1 Responsabilidad

net.nxl es la API userland para operaciones de red. Se carga en el slot NXL 3 (0x1e0c0000) bajo demanda.

No debe:

  • Acceder a hardware (MMIO, PCI, DMA)
  • Conocer e1000 ni ningún NIC driver específico
  • Implementar protocolos TCP/IP, ARP, ICMP, DHCP (eso es kernel o app)
  • Gestionar buffers DMA o descriptores de anillo
  • Hacer operaciones bloqueantes fuera del mecanismo ObWait

Debe proporcionar:

  • Abstracción de interfaces de red (listar, consultar estado)
  • Abstracción de sockets (crear, conectar, enviar, recibir, cerrar)
  • Consulta de estadísticas de red
  • Configuración de IP/gateway (delegado al kernel via ob_set_info)
  • (Futuro) resolución DNS

2.2 API de net.nxl

Cada función de net.nxl llama a syscalls Ob. La tabla de exports de net.nxl se lee como:

let net = *(0x1e0c0000 as *const NetAbiTable);

2.2.1 Structs compartidas (implementadas en v0.3)

/// ABI del kernel via ObInfoClass::NicInfo (class 20).
/// Cada entrada: nic_id(4) + mac(6) + ip(4) + link_up(1) = 15 bytes.
#[repr(C)]
pub struct NetIfaceInfo {
    pub nic_id: u32,
    pub mac: [u8; 6],
    pub ip: [u8; 4],
    pub link_up: u8,
}

/// Dirección IPv4 + puerto para bind/connect.
#[repr(C)]
pub struct SocketAddrV4 {
    pub ip: [u8; 4],
    pub port: u16,
}

/// Planificado (no implementado):
/// - NetSocketInfo (socket_type, direction, local/remote addr, recv_available, tcp_state)
/// - NetIfaceStats (rx/tx packets/bytes, errores)
/// - NetStats global

2.2.2 Funciones de interfaz (implementadas)

// Desde libnet-nxl (C ABI, retornan i32):
pub extern "C" fn net_iface_count() -> u32;
pub unsafe extern "C" fn net_iface_info(idx: u32, info: *mut NetIfaceInfo) -> i32;
pub extern "C" fn net_iface_stats(idx: u32, stats: *mut NetIfaceStats) -> i32;  // #373
pub extern "C" fn net_get_ip(iface: u32) -> u32;
pub extern "C" fn net_get_gateway(iface: u32) -> u32;
pub extern "C" fn net_get_mask(iface: u32) -> u32;
pub extern "C" fn net_get_dhcp_bound() -> i32;

Implementación: net.nxl abre \Global\Info\CpuInfo como fd comodín y consulta ObInfoClass::NicInfo (class 20). El kernel devuelve un array de registros NicInfoRaw de 15 bytes cada uno. No existe \Global\Info\NicInfo — el handler NicInfo es global y funciona con cualquier fd válido.

net_iface_stats devuelve contadores por interfaz (#373, clase NetStats = 28): paquetes/bytes RX/TX y errores, en el mismo orden que NicInfo.

2.2.3 Funciones de socket (implementadas)

// Desde libnet-nxl (C ABI, retornan i32, negativo = error):
pub extern "C" fn net_socket_create(sock_type: u32) -> i32;
pub extern "C" fn net_socket_bind(fd: i32, ip: u32, port: u16) -> i32;
pub extern "C" fn net_socket_connect(fd: i32, ip: u32, port: u16) -> i32;
pub extern "C" fn net_socket_listen(fd: i32) -> i32;
pub unsafe extern "C" fn net_socket_send(fd: i32, data: *const u8, len: u32) -> i32;
pub unsafe extern "C" fn net_socket_recv(fd: i32, buf: *mut u8, max: u32) -> i32;
pub extern "C" fn net_socket_close(fd: i32) -> i32;

Particularidades de la implementación:

  • net_socket_create genera un path único (\NetSock-{hex}) porque ob_create requiere path en namespace. sock_type se codifica en attrs (bits 0-7).
  • bind/connect reciben ip: u32 (big-endian) y port: u16 (host order), no un struct.
  • send retorna bytes enviados como valor positivo.
  • recv usa ObInfoClass::SocketRecv (class 23). No implementa ob_wait para bloqueo.
  • close llama ObSetInfoClass::SocketClose que libera el socket en el kernel.
  • net_get_tcp_status y net_get_socket_addr existen vía ObInfoClass pero no tienen wrapper en libnet-nxl todavía.
// Equivalente userspace vía libneodos (wrappers existentes):
use libneodos::syscall::{ob_socket_create, ob_socket_connect, ob_socket_bind,
                         ob_socket_listen, ob_socket_send, ob_socket_recv, ob_socket_close};

2.2.4 Errores de net.nxl

#[repr(i64)]
pub enum NetError {
    Ok = 0,
    NotFound = -2,
    AccessDenied = -4,
    InvalidFd = -5,
    NotSupported = -7,
    WouldBlock = -8,     // no hay datos disponibles (non-blocking)
    NoMemory = -3,
    ConnectionRefused = -100,
    ConnectionReset = -101,
    ConnectionTimeout = -102,
    NetworkDown = -103,
    HostUnreachable = -104,
    DnsResolveFailed = -200,
}

Nota: Los códigos negativos > -16 son específicos de red, no entran en conflicto con SyscallError (-1 a -15) porque los syscall wrappers traducen los códigos de error del kernel (que siempre están en -1..-15) a errores de net.nxl.

2.2.5 net_socket_recv — diseño detallado

net_socket_recv es la función más compleja. El kernel actual no tiene un ObInfoClass específico para receive de datos de socket. Dos opciones:

Opción A: Nuevo ObInfoClass::SocketRecv = 23

Añadir en el kernel ObInfoClass::SocketRecv = 23. El handler en src/syscall/ob/:

_ if info_class == ObInfoClass::SocketRecv as u32 => {
    // Obtener socket_id del ObObject native_id
    let ob = ob_lookup(fd)?;
    let socket_id = ob.native_id as u32;

    let mut mgr = SOCKET_MANAGER.lock();
    let socket = mgr.get_socket_mut(socket_id).ok_or(Err(...))?;

    // Copiar datos de recv_buf al buffer de usuario
    let available = socket.recv_buf.len().min(user_buf.len());
    user_buf[..available].copy_from_slice(&socket.recv_buf[..available]);
    socket.recv_buf.drain(..available);

    Ok(available as i64)
}

Opción B: ob_wait para bloqueo, luego SocketInfo query

  • ob_wait(fd) se señaliza cuando llegan datos al socket
  • Después de ob_wait, consultar SocketInfo (class 17) que incluye recv_available
  • Leer datos mediante lectura páginada (no implementada)

Decisión: Usar Opción A (SocketRecv class) por simplicidad. El ob_wait se usará para bloqueo cuando recv_buf esté vacío, con el mecanismo wake_socket_readers() existente.

Flujo completo de net_socket_recv:

pub fn net_socket_recv(fd: u8, buf: &mut [u8]) -> Result<usize, NetError> {
    // Consultar si hay datos disponibles
    let mut info_buf = [0u8; 32];  // NetSocketInfo
    let n = sys_ob_query_info(fd, ObInfoClass::SocketInfo, &mut info_buf)?;
    let info: &NetSocketInfo = unsafe { &*(info_buf.as_ptr() as *const NetSocketInfo) };

    if info.recv_available > 0 {
        // Hay datos: leerlos
        let n = sys_ob_query_info(fd, ObInfoClass::SocketRecv, buf)?;
        return Ok(n);
    }

    if info.direction == 4 /* Closed */ {
        return Ok(0);  // EOF
    }

    // No hay datos: bloquear hasta que lleguen
    // Nota: ob_wait requiere que el socket esté en KWait como SyncEvent
    // El kernel señaliza SyncEvent cuando datos llegan al recv_buf
    sys_ob_wait(fd)?;  // bloquea hasta que datos disponibles

    // Reintentar lectura
    let n = sys_ob_query_info(fd, ObInfoClass::SocketRecv, buf)?;
    Ok(n)
}

2.3 Tabla de exports de net.nxl (implementada en v0.3)

// libnet-nxl/src/main.rs — genera net.nxl, slot 0x1e0c0000

pub const NET_NXL_BASE: u64 = 0x1e0c0000;
pub const NET_ABI_VERSION: u32 = 1;

#[repr(C)]
pub struct NetAbiTable {
    pub version: u32,

    // === Interfaces ===
    pub iface_count: extern "C" fn() -> u32,
    pub iface_info: unsafe extern "C" fn(u32, *mut NetIfaceInfo) -> i32,
    pub iface_stats: extern "C" fn(u32, *mut NetIfaceStats) -> i32,  // stub

    // === Sockets ===
    pub socket_create: extern "C" fn(u32) -> i32,
    pub socket_bind: extern "C" fn(i32, u32, u16) -> i32,
    pub socket_connect: extern "C" fn(i32, u32, u16) -> i32,
    pub socket_listen: extern "C" fn(i32) -> i32,
    pub socket_send: unsafe extern "C" fn(i32, *const u8, u32) -> i32,
    pub socket_recv: unsafe extern "C" fn(i32, *mut u8, u32) -> i32,
    pub socket_close: extern "C" fn(i32) -> i32,

    // === Configuración ===
    pub set_ip: extern "C" fn(u32, u32, u32) -> i32,
    pub set_gateway: extern "C" fn(u32, u32) -> i32,  // aplica el gateway a la NIC (SetNicGateway=28)

    // === Estado ===
    pub get_ip: extern "C" fn(u32) -> u32,
    pub get_gateway: extern "C" fn(u32) -> u32,
    pub get_mask: extern "C" fn(u32) -> u32,
    pub get_dhcp_bound: extern "C" fn() -> i32,  // stub (retorna 0)
    _reserved: [u64; 7],
}

Diferencias con el diseño original:

  • path eliminado de socket_create — se genera automáticamente
  • ip pasado como u32 (big-endian) en vez de *const u8 array
  • Se añadieron get_ip, get_gateway, get_mask, get_dhcp_bound
  • get_tcp_status y get_socket_addr no tienen wrapper (accesibles vía ObInfoClass directamente)
  • set_gateway aplica el gateway a la NIC vía ObSetInfoClass::SetNicGateway (28); la persistencia en Registry la hace el caller (p. ej. dhcpd escribe Gateway)

2.4 net.nxl acceso a libneodos

net.nxl necesita syscall wrappers. La solución es que libneodos.h exponga la dirección base del NXL de libneodos como parte del AbiTable, o net.nxl use directamente inline asm para syscalls (como hace console.nxl).

Decisión: net.nxl usará inline asm directamente para las 4 syscalls Ob que necesita (ob_create, ob_set_info, ob_query_info, ob_wait, ob_close). Esto evita dependencias entre NXLs y mantiene net.nxl autocontenido.

// Dentro de net.nxl (libnet/):
fn ob_set_info(fd: u8, class: u32, buf: *const u8, len: usize) -> i64 {
    let r: i64;
    unsafe {
        core::arch::asm!(
            "push rbx", "push rcx", "push rdx", "push r8",
            "mov rbx, {fd}", "mov rcx, {class}", "mov rdx, {buf}", "mov r8, {len}",
            "mov rax, 63", "int 0x80",
            "pop r8", "pop rdx", "pop rcx", "pop rbx",
            fd = in(reg) fd as u64,
            class = in(reg) class as u64,
            buf = in(reg) buf as u64,
            len = in(reg) len as u64,
            out("rax") r,
            options(nostack),
        );
    }
    r
}

2.5 Integración de net.nxl con el shell

NeoShell debe poder cargar net.nxl cuando se ejecuta un comando de red. Propuesta: el shell detecta que el comando no es un built-in ni un .nxe en PATH, pero si es un comando de red conocido, carga net.nxl y ejecuta.

Simplificación: Cada NXE de red (ipconfig.nxe, ping.nxe) es un binario independiente que carga net.nxl via loadlib() al arrancar. No hay integración en el shell — el shell solo dispatching a PATH como ahora.

2.6 libnet::config — backend de configuración compartido (#363)

netcfg, dhcpd, ipconfig, netapplier (y el futuro neocfg) no deben conocer la ruta ...\Network\Interfaces\<n> ni los nombres canónicos de sus valores. Todos pasan por libnet::config, el backend/API único:

neocfg / netcfg / dhcpd / ipconfig / netapplier
                    │
                    ▼
              libnet::config
                │         │
        Registry (Cm)   net.nxl SetNicIp/SetNicGateway
  • libnet-config (crate no_std sin dependencias, host-testeable) define el contrato puro: nombres canónicos (VALUE_*), NetConfig, parse_ip, format_ip, DEFAULT_MASK y build_interface_path.
  • libnet::config añade el adaptador de syscalls: load/store/ publish_lease/apply/apply_current/link_up/interface_ip.

Esto evita duplicar la lógica de configuración y permite que neocfg configure la red sin conocer internals de NIC. El Registry sigue siendo la única fuente de verdad (sin segunda fuente).

2.7 Hostname — detección automática (#487)

El hostname del sistema vive en CurrentControlSet\Control\ComputerName y se lee/escribe con sys_get_hostname / sys_set_hostname (SetHostname, admin). Arranca con el placeholder NeoDOS-PC (libnet::DEFAULT_HOSTNAME).

  • Manual: hostname muestra el nombre; hostname <nombre> lo fija (sys_set_hostname, persistido en el Registry). El kernel valida/normaliza (RFC 952/1035: [A-Za-z0-9._-], sin - al inicio/final, ≤63) y devuelve error legible para nombres inválidos.

dhcpd lo descubre automáticamente al obtener concesión, en este orden:

  1. DHCP opción 12 (Host Name) — se solicita en la parameter request list y, si el servidor la devuelve, se sanea (solo [A-Za-z0-9._-], sin separadores al principio/final, ≤63) y se publica con SetHostname (persistencia + hostname / sys_get_hostname).
  2. Fallback derivado de la MAC — si no hay opción 12 (o no hubo DHCP), se usa neodos-<12 hex> (p. ej. neodos-525400123456), de modo que la máquina siempre tiene un nombre estable.

La lógica pura (sanitize_hostname_into / derived_hostname_into / hostname_byte_ok / HOSTNAME_MAX) vive en libnet-config (host-testeable); el kernel valida el mismo alfabeto en SetHostname. Nunca se sobrescribe un hostname que el usuario ya haya fijado (solo se actúa si el actual está vacío o sigue siendo el placeholder). Un hostname propio resuelve localmente (#485).


3. Herramientas NXE de Red

3.0 netsrv.nxe / netcli.nxe — cliente y servidor TCP reales

Utilidades Ring 3 que usan la ABI pública de sockets (ObType::Socket, ob_create + ObSetInfoClass::Socket* + ObInfoClass::SocketRecv/SocketAccept).

NETCLI <ip> <puerto>     conecta, envía PING!, verifica el eco y sale
NETCLI selftest          round-trip cliente/servidor por loopback (sin NIC)
NETCLI                   (sin args) ejecuta selftest

NETSRV <puerto> [max]    bind+listen, acepta y hace eco de cada conexión
NETSRV selftest          round-trip accept/echo por loopback
NETSRV                   (sin args) ejecuta selftest
  • El listener permanece en Listen; cada conexión se acepta con ob_query_info(fd, SocketAccept) y se cierra sin afectar al listener.
  • ob_wait con timeout finito (sys_ob_wait_timeout): 0 = indefinido; con timeout, la espera termina al expirar (el llamador reevalúa la disponibilidad; no hay código de retorno distinto para el timeout).
  • Empaquetado por el flujo normal de imagen en C:\System\Tools\.

3.1 ipconfig.nxe

IPCONFIG [/ALL | /?]

  (sin args)   Una línea por adaptador: IP/máscara (+ gateway)
  /ALL         Detalle completo: descripción, driver, PCI, enlace, MAC, IP,
               máscara (efectiva), gateway, DHCP server, DNS x3 (incl. vacíos),
               DHCP Origen/APIPA, concesión obtenida/expira/renovación,
               dominio, RX/TX stats, aviso de divergencia Registro-vs-NIC y
               aviso de gateway fuera de subred
  /?           Esta ayuda (también ante flag desconocido, salida 0)

Código (esquema):

fn main() {
    let _ = loadlib("C:\\System\\Libraries\\net.nxl");
    let net = *(0x1e0c0000 as *const NetAbiTable);

    let count = (net.net_interface_count)();
    if count <= 0 { print("No network interfaces found"); return; }

    for i in 0..count {
        let mut info = [0u8; 100];
        (net.net_get_interface_info)(i, info.as_mut_ptr(), info.len());
        let info: &NetInterfaceInfo = unsafe { &*(info.as_ptr() as *const NetInterfaceInfo) };

        print("Adapter {}:", i);
        print("  Driver:     {}", driver_name_str(&info.driver_name));
        print("  MAC:        {:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", ...);
        print("  IPv4:       {}.{}.{}.{}", info.ipv4[0], ...);
        print("  Mask:       {}.{}.{}.{}", ...);
        print("  Gateway:    {}.{}.{}.{}", ...);
        print("  Status:     {}", if link_up != 0 { "Up" } else { "Down" });
        print("  RX/TX:      {} / {} packets", info.rx_packets, info.tx_packets);
    }
}

Ubicación: userbin/ipconfig/ → C:\System\Tools\ipconfig.nxe

Dependencias:

  • libneodos (sys_write, loadlib)
  • net.nxl (net_interface_count, net_get_interface_info)

3.2 ping.nxe

PING <host> [/n count] [/w timeout_ms] [/4]

  <host>       Dirección IPv4 (x.x.x.x)
  /n count     Número de pings (por defecto 4)
  /w timeout   Timeout en ms (por defecto 1000)
  /4           Forzar IPv4

Implementación:

ping.nxe usa un socket raw (type 3) para enviar ICMP echo request.

fn ping(ip: [u8; 4], count: u32, timeout_ms: u32) {
    let net = load_net_nxl();

    // 1. Crear socket raw
    let fd = (net.net_socket_create)(b"\\Ping\0" as *const u8, 3 /* RAW */);

    // 2. Construir ICMP echo request
    // ICMP header: type(8) code(0) checksum(2) id(2) seq(2) payload
    let mut pkt = [0u8; 64];
    pkt[0] = 8;  // Echo Request
    pkt[1] = 0;  // Code
    // checksum en pkt[2..4] se calcula después
    let id = 0x1234u16;
    pkt[4..6].copy_from_slice(&id.to_be_bytes());
    pkt[6..8].copy_from_slice(&seq.to_be_bytes());
    // payload opcional (timestamp)
    pkt[8..16].copy_from_slice(b"NeoDOS!");
    // Calcular checksum ICMP
    let checksum = icmp_checksum(&pkt[..64]);
    pkt[2..4].copy_from_slice(&checksum.to_be_bytes());

    // 3. Conectar raw socket al destino
    // Para raw, "conectar" significa fijar la IP destino en el socket
    let addr = NetSocketAddr { ip, port: 0, _pad: [0;2] };
    (net.net_socket_connect)(fd, &addr as *const _ as *const u8)?;

    // 4. Enviar ping
    let t1 = get_ticks();
    (net.net_socket_send)(fd, &pkt)?;

    // 5. Recibir respuesta
    let mut reply = [0u8; 256];
    match (net.net_socket_recv)(fd, &mut reply) {
        Ok(n) if n >= 20 => {
            // Parsear respuesta IP+ICMP
            let t2 = get_ticks();
            let rtt = t2 - t1;
            print("Reply from {}.{}.{}.{}: bytes={} time={}ms", ip[0], ..., n, rtt);
        }
        Ok(0) => print("Request timed out."),
        Err(e) => print("Ping failed: {}", e),
    }

    // 6. Cerrar
    (net.net_socket_close)(fd);
}

Nota sobre raw sockets: El kernel debe aceptar que un socket raw (SocketType::Raw) pueda enviar datos que se encapsulan directamente en Ethernet (con type 0x0800 para IPv4). El kernel construye el header Ethernet + IP si el socket tiene tipo Raw y se ha "conectado" a una IP destino. Es decir, para Raw:

  • SocketSend → construir Ethernet + IPv4 con proto=1 (ICMP) + payload
  • No construir header TCP/UDP
  • El checksum IP lo calcula el kernel

Si raw socket es demasiado complejo (primera iteración):

Alternativa simplificada: Añadir ObSetInfoClass::Ping(23) que toma una IP destino y un payload, y el kernel construye ICMP echo request completa, lo envía, espera reply y devuelve el RTT. Esto sería un atajo para tener ping rápido sin implementar raw sockets. Pero rompe la arquitectura limpia. No se recomienda.

Decisión: Implementar raw sockets correctamente. El kernel:

  1. socket_send con tipo Raw → construir Ethernet + IPv4 con proto del payload ICMP
  2. net_handle_incoming_packet con proto ICMP → mirar si hay socket Raw escuchando en esa IP y entregar payload

3.3 dhcp.nxe

DHCP [/RENEW] [/RELEASE]

  Sin args: Obtiene configuración DHCP y la aplica
  /RENEW: Renueva concesión
  /RELEASE: Libera concesión

Formato DHCP (RFC 2131):

Un paquete DHCP se transmite sobre UDP (puerto 67 server, 68 cliente). Formato BOOTP extendido (236 bytes fijos + opciones variables):

Byte 0:     op (1=request, 2=reply)
Byte 1:     htype (1=ethernet)
Byte 2:     hlen (6)
Byte 3:     hops (0)
Byte 4-7:   xid (transaction id, random)
Byte 8-9:   secs (0)
Byte 10-11: flags (0x8000 = broadcast)
Byte 12-15: ciaddr (client IP, 0.0.0.0 en Discover)
Byte 16-19: yiaddr (your IP, filled by server)
Byte 20-23: siaddr (server IP)
Byte 24-27: giaddr (gateway IP)
Byte 28-43: chaddr (client MAC, 16 bytes)
Byte 44-235: padding (ceros)
Byte 236+:  magic cookie (0x63825363) + opciones DHCP

Código esquemático:

fn dhcp_discover(net: &NetAbiTable) -> Result<DhcpOffer, NetError> {
    let fd = (net.net_socket_create)(b"\\DhcpClient\0", 2 /* UDP */)?;

    // Bind al puerto 68 (cliente DHCP)
    let local = NetSocketAddr { ip: [0,0,0,0], port: 68u16.to_be(), _pad: [0;2] };
    (net.net_socket_bind)(fd, &local as *const _ as *const u8)?;

    // Conectar al broadcast (255.255.255.255:67)
    let broadcast = NetSocketAddr { ip: [255,255,255,255], port: 67u16.to_be(), _pad: [0;2] };
    (net.net_socket_connect)(fd, &broadcast as *const _ as *const u8)?;

    // Construir DHCP Discover
    let mut pkt = [0u8; 300];
    pkt[0] = 1;  // op = BOOTREQUEST
    pkt[1] = 1;  // htype = ethernet
    pkt[2] = 6;  // hlen = 6
    // xid = random
    let xid = rdrand();
    pkt[4..8].copy_from_slice(&xid.to_be_bytes());
    pkt[10] = 0x80; pkt[11] = 0x00;  // flags = broadcast
    // chaddr = nuestra MAC
    let mac = get_our_mac(net);
    pkt[28..34].copy_from_slice(&mac);
    // magic cookie + DHCP option 53 = Discover (1)
    pkt[236..240].copy_from_slice(&[0x63, 0x82, 0x53, 0x63]);  // magic
    pkt[240] = 53; pkt[241] = 1; pkt[242] = 1;  // DHCP Discover
    pkt[243] = 255;  // end option
    let pkt_len = 244;

    // Enviar Discover
    (net.net_socket_send)(fd, &pkt[..pkt_len])?;

    // Esperar DHCP Offer
    let mut reply = [0u8; 300];
    let n = (net.net_socket_recv)(fd, &mut reply)?;

    // Parsear Offer: yiaddr en bytes 16-19, server IP en bytes 20-23
    // Opciones DHCP: buscar option 53 = Offer (2), option 1 = subnet mask, option 3 = gateway, option 6 = DNS

    // Enviar DHCP Request...
    // Esperar DHCP ACK...

    // Devolver configuración obtenida
}

fn dhcp_apply(net: &NetAbiTable, config: &DhcpConfig) {
    // Guardar en Registry
    // cm_open_key + cm_set_value para IPAddress, Gateway, DNS, SubnetMask

    // Aplicar IP
    (net.net_set_ip)(0, &config.ip)?;
    (net.net_set_gateway)(0, &config.gateway)?;
}

Limitación: DHCP requiere que el kernel tenga:

  1. UDP socket funcional (bind puerto 68, enviar a 255.255.255.255:67)
  2. Broadcast ethernet (dest FF:FF:FF:FF:FF:FF)
  3. El kernel construya header UDP+IP+Ethernet desde socket_send
  4. Receive path: UDP paquetes entrantes dispatch al socket bind al puerto 68

3.4 nslookup.nxe — resolución DNS (implementado)

NSLOOKUP <hostname> [dns-server]

  Resuelve un hostname a una o más direcciones IPv4.
  Sin segundo argumento usa los servidores DNS configurados.

La resolución DNS no vive en net.nxl ni dentro de cada herramienta. El protocolo se implementa una sola vez en una librería compartida y nslookup y ping la consumen:

libdns   (crate no_std, sin dependencias)
  ├─ encode_name / decode_name
  ├─ build_query / parse_response   (A, CNAME, RCODE, truncation)
  ├─ validate_hostname / parse_dotted_ip
  └─ DnsError, DnsAnswer, DnsTransport, resolve_with_servers()

libnet   (librería de red userland)
  ├─ NetTransport     → sockets UDP vía net.nxl (puerto 53)
  ├─ configured_servers() → Registry: DnsServer, DnsServer2, DnsServer3
  ├─ resolve(hostname)            → DnsResult { server, addresses, ttl }
  ├─ resolve_with_server(host, s)
  └─ caché acotada (16 entradas, por host + servidor)

nslookup.nxe   ─┐
ping.nxe        ├─ usan libnet::dns
(ntpd, ...)    ─┘

Configuración (Registry):

\Registry\Machine\System\CurrentControlSet\Services\Network\Interfaces\0
  DnsServer   REG_DWORD/REG_SZ   (servidor 1, preferente)
  DnsServer2  REG_DWORD/REG_SZ   (servidor 2, opcional)
  DnsServer3  REG_DWORD/REG_SZ   (servidor 3, opcional)

Se distingue entre configured DNS (lo que hay en el Registry) y effective DNS (el servidor que realmente respondió, devuelto en DnsResult.server). 0.0.0.0 se trata siempre como "no configurado": nunca se envía una consulta a esa dirección.

Errores distintos (libdns::DnsError): NoConfig, NoServer, InvalidHostname, Timeout, ServerUnreachable, NxDomain, MalformedResponse, Truncated, ServerFailure, NoARecord, Network. Cada uno se muestra con su propio mensaje localizado.

Consultas a los servidores: se prueban en orden. NXDOMAIN corta la búsqueda de inmediato; un timeout o servidor inalcanzable pasa al siguiente. Solo se implementa resolución A/IPv4 (y seguimiento de CNAME); no hay fallback TCP (una respuesta con el bit TC se reporta como Truncated). La espera de respuesta usa un presupuesto de tiempo RDTSC (no hay ob_wait para sockets ni sleep con duración en userland).

3.5 ping.nxe — integración con DNS (implementado)

ping <host> acepta tanto una IPv4 literal como un hostname:

hostname → libnet::dns::resolve() → IPv4 → sys_icmp_ping() (sin cambios)

ping 10.0.1.1 conserva exactamente el comportamiento anterior y los flags /n y /t. Si el argumento parece una IP numérica pero es inválida se mantiene el error de IP; en caso contrario se intenta resolver por DNS y se muestra el error DNS específico.

3.6 ntpd.nxe — daemon de sincronización horaria (implementado)

ntpd es un servicio residente (StartType=Auto, gestionado por el Service Manager) que sincroniza el reloj del sistema con servidores NTP/SNTP. No configura nada: lee Ntpd\Parameters del Registry, consulta el servidor y aplica la hora vía ob_set_datetime() (objeto \Global\Info\DateTime).

ntpd.nxe
  ├─ libntp     (SNTP: request/reply, offset/delay, civil time — host-testeable)
  ├─ libnet::dns (resolución de servidores)
  └─ libneodos   (sockets UDP, Registry, ob_set_datetime)

La lógica de protocolo vive en libntp, igual que el DNS vive en libdns; el daemon solo aporta transporte, configuración y estado. La documentación completa (configuración, ciclo de vida, estado y limitaciones) está en docs/services/ntpd.md.

Modo one-shot (ntpdate). Invocado como ntpd once (o -q/--once), ntpd realiza una única sincronización y sale (0 si sincronizó, 1 si no). Es el papel de ntpdate: no hay un binario aparte, se reutiliza el mismo daemon.

Servidores. La lista se configura por Registry en Ntpd\Parameters\Servers (separados por ;). El hive por defecto trae pool.ntp.org;time.cloudflare.com;time.google.com — los fallbacks van en la configuración, no en el código, para que el primer sync sea fiable detrás de NAT. Interval (segundos) y Timeout (ms) también son valores del Registry.


4. NeoInit System Configuration

4.1 Estado actual

NeoInit (userbin/neoinit/src/main.rs, 79 líneas) es un supervisor minimalista:

fn spawn() -> Result<u32, i64> {
    let path_str = "\\Global\\FileSystem\\C:\\Programs\\neoshell.nxe";
    let attrs = 0xFFu64 | (0xFFu64 << 8) | (0xFFu64 << 16);
    let fd = syscall::sys_ob_create(path_str, 1, None, attrs)?;
    let _ = syscall::sys_ob_wait(fd);
    Ok(0)
}

Problemas:

  • Path C:\Programs\neoshell.nxe hardcoded
  • No usa Registry
  • No inicia servicios (net, logger, etc.)
  • No se puede configurar sin recompilar

4.2 Migración a Registry

NeoInit debe leer su configuración de \Registry\Machine\System\CurrentControlSet\Services\NeoInit.

Jerarquía Registry:

\Registry\Machine\System\CurrentControlSet\Services\NeoInit
├── DefaultShell        REG_SZ   "C:\Programs\neoshell.nxe"
├── EnableVT            REG_DWORD  1
├── AutoStartServices   REG_SZ   ""       (obsoleto; ver Service Manager)
├── EnableTests         REG_DWORD  0
└── EnableNetworkTest   REG_DWORD  0

Claves de soporte para servicios (Service Manager):

\Registry\Machine\System\CurrentControlSet\Services
├── Dhcpc
│   ├── ImagePath       REG_SZ   "C:\System\Tools\dhcpd.nxe"
│   ├── BinaryPath      REG_SZ   "C:\System\Tools\dhcpd.nxe"
│   └── StartType       REG_DWORD  2   (Auto)   — DHCP; publica el lease
│
├── NetApplier
│   ├── BinaryPath      REG_SZ   "C:\System\Tools\netapplier.nxe"
│   └── StartType       REG_DWORD  2   (Auto)   — aplica la config a la NIC
│
├── NeoInit
│   └── ...
│
└── <future_services>

netapplier es el aplicador residente (servicio NetApplier) y dhcpd el cliente DHCP; netcfg es solo la CLI de configuración. Ver §4.4.

4.3 Nueva implementación de NeoInit

use libneodos::syscall;

const REG_ROOT: &str = "\\Registry\\Machine\\System\\CurrentControlSet\\Services\\NeoInit";

fn cm_open_key(path: &str) -> Result<u8, i64> {
    syscall::sys_cm_open_key(path)
}

fn cm_query_str(fd: u8, name: &str, default: &str) -> [u8; 260] {
    let mut buf = [0u8; 260];
    if let Ok(n) = syscall::sys_cm_query_value(fd, name, &mut buf) {
        let mut result = [0u8; 260];
        let len = n.min(259);
        result[..len].copy_from_slice(&buf[..len]);
        result
    } else {
        let bytes = default.as_bytes();
        let len = bytes.len().min(259);
        result[..len].copy_from_slice(&bytes[..len]);
        result
    }
}

fn cm_query_dword(fd: u8, name: &str, default: u32) -> u32 {
    let mut buf = [0u8; 4];
    if let Ok(_) = syscall::sys_cm_query_value(fd, name, &mut buf) {
        u32::from_le_bytes(buf)
    } else {
        default
    }
}

/// Hacer spawn de un proceso y no esperarlo (detach).
fn spawn_detached(path: &str) -> Result<u32, i64> {
    let ob_path = alloc::format!("\\Global\\FileSystem\\{}", path);
    let attrs = 0xFFu64 | (0xFFu64 << 8) | (0xFFu64 << 16);
    let fd = syscall::sys_ob_create(&ob_path, 1, None, attrs)?;
    // No hacer ob_wait — proceso en background
    syscall::sys_close(fd)?;
    Ok(0)  // no devolvemos PID real sin ob_wait
}

/// Esperar a que la red esté lista.
fn wait_for_network(timeout_ms: u32) -> bool {
    // Cargar net.nxl
    let _ = syscall::sys_loadlib("C:\\System\\Libraries\\net.nxl");
    let net = unsafe { &*(0x1e0c0000 as *const NetAbiTable) };

    let start = get_ticks_ms();
    loop {
        let count = (net.net_interface_count)();
        if count > 0 {
            // Hay NIC — comprobar si tiene IP válida
            let mut info = [0u8; 100];
            (net.net_get_interface_info)(0, info.as_mut_ptr(), info.len());
            let info: &NetInterfaceInfo = unsafe { &*(info.as_ptr() as *const NetInterfaceInfo) };
            // IP 0.0.0.0 significa no configurado → esperar DHCP
            if info.ipv4 != [0,0,0,0] {
                return true;
            }
            // Si IP sigue siendo 0.0.0.0, esperar
        }
        if get_ticks_ms() - start > timeout_ms {
            return false;  // timeout
        }
        syscall::sys_sleep_ex();  // yield
    }
}

fn get_ticks_ms() -> u64 {
    // Leer timer tick count via ob_open + ob_query_info
    // Simplicidad: usar aproximación
    0
}

#[no_mangle]
pub extern "C" fn _start() -> ! {
    // 1. Banner
    write_str(b"NeoInit v0.2.0 (PID 1)\r\n");

    // 2. Abrir Registry
    let reg_fd = match cm_open_key(REG_ROOT) {
        Ok(fd) => fd,
        Err(_) => {
            write_str(b"[neoinit] WARNING: Registry not available, using defaults\r\n");
            0xFF  // fd inválido
        }
    };

    // 3. Leer DefaultShell
    let shell_path = if reg_fd != 0xFF {
        cm_query_str(reg_fd, "DefaultShell", "C:\\Programs\\neoshell.nxe")
    } else {
        *b"C:\\Programs\\neoshell.nxe\0"
    };

    // 4. Leer auto-start services
    let services_str = if reg_fd != 0xFF {
        cm_query_str(reg_fd, "AutoStartServices", "")   // obsoleto (ver Service Manager)
    } else {
        [0u8; 260]
    };

    // 5. Leer EnableVT
    let enable_vt = if reg_fd != 0xFF {
        cm_query_dword(reg_fd, "EnableVT", 1)
    } else {
        1
    };

    // 6. Iniciar servicios de auto-arranque
    if services_str[0] != 0 {
        write_str(b"[neoinit] Starting services...\r\n");
        let svc_list = core::str::from_utf8(&services_str).unwrap_or("");
        for svc_name in svc_list.split(';') {
            let svc_name = svc_name.trim();
            if svc_name.is_empty() { continue; }

            // Leer Path del servicio desde Registry
            let svc_reg_path = alloc::format!(
                "\\Registry\\Machine\\System\\CurrentControlSet\\Services\\{}", svc_name
            );
            if let Ok(svc_fd) = cm_open_key(&svc_reg_path) {
                let svc_path = cm_query_str(svc_fd, "Path", "");
                if svc_path[0] != 0 {
                    let path_str = core::str::from_utf8(&svc_path).unwrap_or("").trim_end_matches('\0');
                    write_str(b"[neoinit] Starting service: ");
                    write_str(path_str.as_bytes());
                    write_str(b"\r\n");

                    match spawn_detached(path_str) {
                        Ok(_) => write_str(b"  -> started\r\n"),
                        Err(e) => {
                            write_str(b"  -> FAILED: ");
                            // print errno
                            write_str(b"\r\n");
                        }
                    }
                }
                let _ = syscall::sys_close(svc_fd);
            }
        }
    }

    // 7. Cargar net.nxl si hay servicios de red
    // (el aplicador lo hará por su cuenta, pero podemos precargar)
    if services_str.iter().any(|&b| b == b'n' || b == b'N') {
        let _ = syscall::sys_loadlib("C:\\System\\Libraries\\net.nxl");
    }

    // 8. Esperar red si configurado
    let wait_net = if reg_fd != 0xFF {
        cm_query_dword(reg_fd, "WaitForNetwork", 0)
    } else {
        0
    };
    if wait_net != 0 {
        write_str(b"[neoinit] Waiting for network...\r\n");
        let timeout = cm_query_dword(reg_fd, "BootTimeout", 30) * 1000;
        if wait_for_network(timeout) {
            write_str(b"[neoinit] Network ready.\r\n");
        } else {
            write_str(b"[neoinit] WARNING: Network timeout, continuing.\r\n");
        }
    }

    // 9. Spawn shell (loop supervisor)
    let shell_path_cstr = core::str::from_utf8(&shell_path).unwrap_or("C:\\Programs\\neoshell.nxe");
    write_str(b"[neoinit] DefaultShell: ");
    write_str(shell_path_cstr.as_bytes());
    write_str(b"\r\n");

    if enable_vt != 0 {
        set_vt(0);
    }

    loop {
        match spawn(shell_path_cstr) {
            Ok(pid) => {
                write_str(b"[neoinit] shell exited, respawning...\r\n");
            }
            Err(e) => {
                write_str(b"[neoinit] spawn failed, retrying...\r\n");
                // Esperar antes de reintentar
                for _ in 0..100_000_000 { core::hint::spin_loop(); }
            }
        }
    }
}

4.4 netcfg.nxe — CLI de configuración (implementado)

netcfg.nxe es exclusivamente la CLI de configuración de red one-shot: lee, valida, escribe el Registry cuando corresponde, aplica explícitamente cuando corresponde, muestra estado y sale. Nunca queda residente y no es un servicio (ver #365).

Comandos (userbin/netcfg/src/main.rs):

netcfg                          (sin args) aplica la config del Registry y sale
netcfg /apply                   aplica la config del Registry a la NIC y sale
netcfg /setip <ip> <mask> [gw]  IPv4 estática (DHCP off) + aplicar
netcfg /setmask <mask>          máscara + aplicar
netcfg /setgateway <gw>         gateway + aplicar
netcfg /setdns <s1> [s2] [s3]   DNS (escribe el Registry + flush)
netcfg /dhcp on|off             activa/desactiva DHCP
netcfg /reset                   limpia IP/mask/gw/DNS; DHCP on
netcfg /resetdns                limpia DNS
netcfg /status                  Registry vs NIC (NicInfo del kernel)
netcfg /test                    valida la config (dry-run)
netcfg /? | help                ayuda

El Registry es la única fuente de verdad. netcfg lee/escribe la configuración a través del backend compartido libnet::config (§2.6, #363) y, para los comandos que lo requieren, aplica el cambio de inmediato vía SetNicIp = 27 / SetNicGateway = 28. La aplicación continua (lease DHCP, cambios desde neocfg, etc.) la hace el aplicador residente (§4.4.1), no netcfg.

dhcpd (Dhcpc) hace el DORA y publica el lease en el Registry; el aplicador es quien lo lleva a la NIC. Así no hay dos programas aplicando lo mismo (ver #320/#365).

Diseño histórico: el diseño original describía netcfg como servicio que hacía el DHCP y salía; después pasó a daemon (servicio Netcfg) y luego a one-shot (ver #320). Desde #365 netcfg es solo CLI y el rol residente se separó en netapplier.

4.4.1 netapplier.nxe — aplicador residente (implementado)

netapplier.nxe es el Network Configuration Applier: un servicio Ring 3 residente (StartType = Auto, servicio NetApplier) que mantiene la NIC sincronizada con Network\Interfaces\0. Es el único aplicador; no es netcfg (CLI) ni el worker Ring 0 netpump (bombeo RX del kernel). Se eligió el nombre netapplier (y no netcfgd) para no recoplar la identidad del daemon con la CLI netcfg.

dhcpd ──► Registry ──► netapplier ──► NIC runtime
netcfg ─► Registry / apply explícito
  • Lee y aplica a través del backend compartido libnet::config (§2.6, #363).
  • Cada iteración (~decenas de ms) lee IPAddress/SubnetMask/Gateway y el estado de enlace (NicInfo).
  • Si los valores cambian o el enlace sube (fl. 0→1), aplica SetNicIp = 27 / SetNicGateway = 28.
  • Máscara ausente o 0 → /24 (#306/#314).
  • DHCPEnabled = 1 → aplica el lease que publicó dhcpd.
  • Binario C:\System\Tools\netapplier.nxe; servicio NetApplier.

La notificación de cambios de configuración (en lugar del polling del aplicador) y la capa de servicios netd (Ring 3) se trackean por separado (#364/#362).

4.4.2 netd.nxe — servicio de red Ring 3 (MVP, #372)

netd.nxe es la capa de servicios de red Ring 3 (servicio Netd). No es el bombeo de RX: ese sigue siendo el worker Ring 0 netpump del kernel (network_poll_all()), que es independiente de que se programe un proceso Ring 3.

Nota (#355/#376): el autostart de netd se desactivó temporalmente (StartType = Demand) mientras #376 (boot stall) estaba sin causa raíz. Con el fix de #355 (anti-starvation de kernel threads Ring-0) el boot stall S2 ya no se reproduce (0/16 arranques con netd Auto), así que se restaura a Auto. netd sigue registrado y arrancable; el flake restante de DHCP (S3) es independiente de netd.

netd    (Ring 3) ── observa NIC / link state (NicInfo) y expone estado
netpump (Ring 0) ── network_poll_all() + arp_tick + tcp_tick  (RX pump)
  • v1: identidad de proceso/servicio, bucle con yield y monitorización de enlace (subida/bajada por NIC). No aplica configuración (eso es netapplier).
  • Fuera de alcance de v1: DHCP (dhcpd), DNS, routing y firewall.
  • Binario C:\System\Tools\netd.nxe; servicio Netd en el hive por defecto.
  • Identidad y frontera se deciden en #362; implementación en #372.

4.5 Integración con la shell: servicios en background

Los servicios iniciados por NeoInit son procesos independientes que corren en background. No hay interacción directa con la shell. Cuando el shell está activo, los servicios simplemente existen como procesos.

Para comunicación shell↔servicio:

  • Pipe nombrado vía Ob (future)
  • Registry (lectura de estado)
  • EventBus (eventos de sistema)

En la primera versión, los servicios son fire-and-forget.


5. Registry Usage Model

5.1 Separación Registry vs Filesystem

Almacén Registry NeoFS
Config estructural del sistema ✅ DefaultShell, servicios, interfaces de red, paquetes instalados ❌
Hardware info ✅ DeviceMap, driver bindings ❌
Perfiles de usuario ✅ SID, grupos, paths de perfil (referencias) ✅ Datos reales
Preferencias de usuario ❌ ✅ C:\Users\<name>\*.cfg
Config de aplicaciones ❌ (solo claves small) ✅ C:\Programs\<app>\*.cfg
Documentos ❌ ✅ C:\Data\, C:\Users\<name>\
Logs ❌ ✅ C:\Logs\
Cache temporal ❌ ✅ C:\Temp\
Binarios ❌ ✅ NXE, NEM, NXL
Estado de paquetes ✅ Versiones, dependencias, archivos instalados ✅ Archivos reales (.npkg)
Config de red ✅ IP, gateway, DNS, DHCP flag ❌

5.2 Regla de oro

Registry = qué es el sistema (estructura). NeoFS = con qué trabaja el sistema (contenido).

Registry para valores pequeños (<4 KB), estructura jerárquica, y referencias. NeoFS para datos, logs, binarios, configuraciones editables.

5.3 Jerarquía completa propuesta

\Registry
├── Machine
│   ├── Hardware
│   │   ├── Description
│   │   │   └── System           REG_SZ  "NeoDOS x86_64 QEMU"
│   │   ├── DeviceMap
│   │   │   ├── Keyboard         REG_SZ  "\\Device\\Keyboard0"
│   │   │   ├── Serial           REG_SZ  "\\Device\\Serial0"
│   │   │   └── Framebuffer      REG_SZ  "\\Device\\Video0"
│   │   └── ResourceMap
│   │       └── PnPManager       REG_SZ  (lista de dispositivos detectados)
│   │
│   ├── System
│   │   ├── CurrentControlSet
│   │   │   ├── Services
│   │   │   │   ├── NeoInit      (ver sección 4.2)
│   │   │   │   ├── NetApplier   (BinaryPath, StartType, DisplayName)
│   │   │   │   └── ...
│   │   │   │
│   │   │   ├── Control
│   │   │   │   ├── WaitForNetwork    REG_DWORD  0/1
│   │   │   │   ├── BootTimeout       REG_DWORD  30
│   │   │   │   └── CrashBehavior     REG_DWORD  0=panic, 1=reboot
│   │   │   │
│   │   │   └── Environment
│   │   │       ├── Path              REG_SZ  "C:\\Programs;C:\\System\\Bin"
│   │   │       ├── Temp              REG_SZ  "C:\\Temp"
│   │   │       └── Home              REG_SZ  "C:\\Users\\Default"
│   │   │
│   │   └── Network
│   │       └── Interfaces
│   │           └── 0
│   │               ├── IPAddress     REG_DWORD  10.0.30.20
│   │               ├── SubnetMask    REG_DWORD  255.255.255.0
│   │               ├── Gateway       REG_DWORD  10.0.30.1
│   │               ├── DnsServer     REG_DWORD  10.0.30.10
│   │               ├── DnsServer2    REG_DWORD  0
│   │               ├── DnsServer3    REG_DWORD  0
│   │               ├── DHCPEnabled   REG_DWORD  1
│   │               ├── DHCPBound     REG_DWORD  1
│   │               ├── DHCPServer    REG_DWORD  10.0.30.1
│   │               ├── LeaseTime     REG_DWORD  4000
│   │               ├── LeaseObtained REG_DWORD  <unix secs>
│   │               ├── T1Renew        REG_DWORD  0 (=unset)
│   │               ├── T2Rebind       REG_DWORD  0 (=unset)
│   │               ├── Domain        REG_SZ     "home.arpa"
│   │               ├── Broadcast     REG_DWORD  0 (=unset)
│   │               ├── NtpServer     REG_DWORD  0 (=unset)
│   │               ├── MTU           REG_DWORD  0 (=unset)
│   │               ├── MACAddress    REG_SZ  "52:54:00:12:34:56"
│   │               └── DriverBinding REG_SZ  "\\Device\\Nic\\0"
│   │
│   └── Packages                    (ver sección 6)
│       └── ...
│
└── User
    └── S-1-5-21-0-0-0-1000        (SID del usuario por defecto)
        ├── ProfilePath            REG_SZ  "C:\\Users\\Default"
        └── Groups                 REG_MULTI_SZ  "Users;Network"

5.4 Persistencia a disco — cm_flush_key

cm_flush_key actualmente es no-op. La persistencia debe implementarse así:

pub fn cm_flush_key(key_native_id: u64) -> Result<(), ()> {
    let (hive_idx, _cell_idx) = decode_cell(key_native_id);
    let cm = CM_MANAGER.lock();
    if (hive_idx as usize) >= cm.hives.len() {
        return Err(());
    }
    let hm = &cm.hives[hive_idx as usize];

    // Serializar hive a bytes
    let data = hm.hive.serialize();

    // Escribir a archivo en NeoFS
    // Path: C:\System\Registry\<hive_name>.hiv
    let file_path = alloc::format!("C:\\System\\Registry\\{}.hiv", hm.name);
    globals::with_vfs(|vfs| {
        let (drive_idx, node) = vfs.resolve_path(&file_path)?;
        vfs.write(drive_idx, node.inode, 0, &data)
    }).ok();

    Ok(())
}

Formato de serialización del hive:

┌──────────────────────────────────┐
│ Magic: "NEOH" (4 bytes)          │
│ Version: u32 (1)                 │
│ Cell count: u32                  │
│ Cell data: variable              │
│   Por cada celda:                │
│   ├── Cell type: u32 (0=free,    │
│   │   1=key, 2=value, 3=security)│
│   ├── Cell payload (variable)    │
│   └── Padding to align           │
└──────────────────────────────────┘

¿Cuándo se persiste? No en cada set_value. Opciones:

  1. Síncrono: Cada cm_set_value → marcar hive dirty. cm_flush_key se llama desde el shell (FLUSHREG command), o desde NeoInit antes de spawn shell, o en shutdown.
  2. Periódico: Un demonio de kernel (work queue) cada N segundos si dirty.
  3. A petición: Solo cuando se llama cm_flush_key (syscall RAX=57).

Decisión: Opción 1 + 3. cm_set_value marca el hive como dirty. Antes de spawn NeoShell, NeoInit llama cm_flush_key para guardar. El kernel llama flush automático en shutdown (sys_poweroff). El usuario puede llamar FLUSHREG manualmente.

5.5 Creación de valores por defecto

En main.rs, después de Phase 3.881 (init_cm), crear valores por defecto:

#[link_section = ".text.boot"]
pub fn create_default_registry_values() {
    use crate::cm::{encode_cell, cm_create_key, cm_set_value};
    use crate::cm::hive::{REG_SZ, REG_DWORD, REG_MULTI_SZ};

    // Abrir raíz del SYSTEM hive
    let root_native = encode_cell(0, 0);  // hive 0, cell 0

    // CurrentControlSet
    let ccs = match cm_create_key(root_native, "CurrentControlSet") {
        Ok(id) => id,
        Err(_) => return,  // ya existe
    };

    // Services\NeoInit
    let services = cm_create_key(ccs, "Services").unwrap_or(ccs);
    let neoinit = cm_create_key(services, "NeoInit").unwrap_or(services);
    cm_set_value(neoinit, "DefaultShell", REG_SZ, b"C:\\Programs\\neoshell.nxe").ok();
    cm_set_value(neoinit, "AutoStartServices", REG_MULTI_SZ, b"").ok();
    cm_set_value(neoinit, "EnableVT", REG_DWORD, &1u32.to_le_bytes()).ok();
    cm_set_value(neoinit, "VTCount", REG_DWORD, &4u32.to_le_bytes()).ok();

    // Services\NetApplier — único aplicador residente (netcfg es solo CLI)
    let applier = cm_create_key(services, "NetApplier").unwrap_or(services);
    cm_set_value(applier, "BinaryPath", REG_SZ, b"C:\\System\\Tools\\netapplier.nxe").ok();
    cm_set_value(applier, "StartType", REG_DWORD, &2u32.to_le_bytes()).ok();
    cm_set_value(applier, "Description", REG_SZ, b"Network configuration applier").ok();

    // Control
    let control = cm_create_key(ccs, "Control").unwrap_or(ccs);
    cm_set_value(control, "WaitForNetwork", REG_DWORD, &0u32.to_le_bytes()).ok();
    cm_set_value(control, "BootTimeout", REG_DWORD, &30u32.to_le_bytes()).ok();

    // Environment
    let env = cm_create_key(ccs, "Environment").unwrap_or(ccs);
    cm_set_value(env, "Path", REG_SZ, b"C:\\Programs;C:\\System\\Bin").ok();
    cm_set_value(env, "Temp", REG_SZ, b"C:\\Temp").ok();

    // Network\Interfaces\0
    let network = cm_create_key(root_native, "Network").unwrap_or(root_native);
    let ifaces = cm_create_key(network, "Interfaces").unwrap_or(network);
    let if0 = cm_create_key(ifaces, "0").unwrap_or(ifaces);
    cm_set_value(if0, "IPAddress", REG_DWORD, &0u32.to_le_bytes()).ok();
    cm_set_value(if0, "SubnetMask", REG_DWORD, &0u32.to_le_bytes()).ok();
    cm_set_value(if0, "Gateway", REG_DWORD, &0u32.to_le_bytes()).ok();
    cm_set_value(if0, "DnsServer", REG_DWORD, &0u32.to_le_bytes()).ok();
    cm_set_value(if0, "DHCPEnabled", REG_DWORD, &1u32.to_le_bytes()).ok();
}

Protección: Solo se crean si no existen (el cm_create_key ya retorna error si existe). Se llaman una vez en el primer boot.


6. NeoPkg — Package System

6.1 Visión general

NeoPkg es el sistema de gestión de paquetes de NeoDOS. Opera a nivel userland (pkg.nxe), con validación del kernel para componentes CORE.

6.2 Formato .npkg (binario)

┌──────────────────────────────────────────────────────────┐
│ Magic: 4 bytes "NPKG"                                    │
│ Header version: u16 = 1                                  │
│ Manifest offset: u32                                     │
│ Manifest size: u32                                       │
│ File entry count: u32                                    │
│ Data offset: u32 (start of file data section)            │
│ Padding to 64 bytes                                      │
├──────────────────────────────────────────────────────────┤
│ Manifest (JSON-like, UTF-8, null-terminated)             │
│   name=netutils                                          │
│   version=1.0.0                                          │
│   type=User                                              │
│   arch=x86_64                                            │
│   description=Networking utilities                       │
│   depends=libneodos>=0.46;net.nxl>=0.1                   │
│   installed_size=131072                                  │
├──────────────────────────────────────────────────────────┤
│ File entries (20 bytes each):                            │
│   [0] dest_path_offset=4, dest_path_len=20,              │
│       file_size=8192, file_data_offset=1024               │
│   [1] dest_path_offset=24, dest_path_len=16,             │
│       file_size=6144, file_data_offset=9216               │
│   ...                                                     │
├──────────────────────────────────────────────────────────┤
│ File data (raw, concatenated)                             │
│   [data for entry 0]                                     │
│   [data for entry 1]                                     │
│   ...                                                     │
└──────────────────────────────────────────────────────────┘

Manifest detallado:

name=netutils
version=1.0.0
type=User           # Core | System | User
arch=x86_64
description=Networking utilities (ping, ipconfig, dhcp)
depends=libneodos>=0.46;net.nxl>=0.1
conflicts=          # nombre de paquete con el que conflictúa
installed_size=131072
post_install=       # comando opcional post-instalación (ej: "REGSET ...")
maintainer=NeoDOS Team
homepage=https://neodos.io/packages/netutils
checksum_alg=SHA256  # algoritmo usado en file entries

File entry (20 bytes cada uno):

#[repr(C)]
struct NpkgFileEntry {
    dest_relative_path_offset: u32,  // offset en string table
    dest_relative_path_len: u16,
    file_size: u32,
    file_data_offset: u32,           // desde start of data section
    checksum: [u8; 4],              // CRC32 de los datos
}

6.3 Herramientas pkg.nxe

PKG INSTALL <file.npkg> [/F] [/S]

  Instala un paquete. /F = force (ignora dependencias),
  /S = silent (no preguntar confirmación)

PKG REMOVE <name> [/F]

  Elimina un paquete. /F = force (elimina incluso System).

PKG LIST [/ALL]

  Lista paquetes instalados. Sin /ALL solo muestra User.
  Con /ALL muestra todos (Core, System, User).

PKG INFO <name>

  Información detallada de un paquete instalado.

PKG VERIFY <name>

  Verifica checksums de archivos instalados.

PKG INSTALL — algoritmo:

1. Parsear .npkg (validar magic, manifest, estructura)
2. Verificar dependencias contra Registry\Machine\Packages
3. Si falta dependencia → error (o /F para ignorar)
4. Si conflictos → error
5. Para cada file entry:
   a. Construir path destino: C:\Programs\<relative_path>
   b. Verificar que no existe (o preguntar sobrescribir)
   c. Crear directorios si necesario (ob_create Directory)
   d. Escribir archivo (ob_create File + ob_set_info WriteContent)
   e. Verificar checksum
6. Si post_install no vacío: ejecutar comando
7. Registrar en Registry:
   \Registry\Machine\Packages\<name>
     Version = "1.0.0"
     Type = "User"
     InstalledFiles = REg_multi_sz "..."
     Dependencies = "..."
     InstalledAt = "2026-07-01 12:00:00"
     Status = "Active"

PKG REMOVE — algoritmo:

1. Abrir \Registry\Machine\Packages\<name>
2. Leer "Type" — si Core → ERROR (protegido)
3. Leer "Type" — si System y no /F → preguntar confirmación
4. Leer InstalledFiles → lista de archivos
5. Para cada archivo:
   a. ob_destroy(file_fd) o ob_set_info(FileDelete)
   b. Verificar que se eliminó
6. Eliminar clave Registry:
   \Registry\Machine\Packages\<name>
7. Si hay post_remove en el paquete original: ejecutar

6.4 Protección CORE

Mecanismo 1 — Registry flag:

El kernel, al crear la clave \Registry\Machine\Packages\*, valida que el tipo Core no se pueda eliminar via cm_delete_key. Si un proceso userland intenta borrar un paquete Core:

// En syscall cm_delete_key handler (RAX=56):
let (hive_idx, cell_idx) = decode_cell(key_native_id);
let cm = CM_MANAGER.lock();
let hm = &cm.hives[hive_idx as usize];

// Verificar si esta clave es un paquete Core
if let Some(val) = hm.hive.query_value(cell_idx, "Type") {
    if val.as_str() == Ok("Core") {
        return Err(());  // denegado
    }
}

Mecanismo 2 — MODE_CORE en NeoFS:

Los archivos instalados como Core tienen el flag MODE_CORE en el inodo. El kernel deniega ob_set_info(FileDelete) y ob_destroy sobre archivos con MODE_CORE. Esto funciona aunque el Registry esté corrupto.

// En handler_ob_destroy:
let node = vfs.resolve(...)?;
if node.mode & MODE_CORE != 0 {
    return Err(SyscallError::AccessDenied);
}

Mecanismo 3 — Lista hardcoded en kernel:

El kernel mantiene una lista de componentes Core que no se pueden eliminar bajo ninguna circunstancia:

const CORE_COMPONENTS: &[&str] = &[
    "NeoInit", "NeoShell", "NeoFS",
    "libneodos", "kernel.elf",
];

6.5 Seguridad

  • Solo procesos con token admin pueden ejecutar pkg.nxe
  • pkg.nxe verifica is_current_admin() al arrancar
  • Paquetes System requieren flag /F para REMOVE
  • Paquetes Core no se pueden REMOVE ni con /F
  • Verificación de checksum post-instalación
  • (Futuro) Firmas digitales con ed25519

6.6 Ubicaciones en NeoFS

C:\Packages\                ← archivos .npkg almacenados
C:\Programs\                ← NXEs instalados
C:\System\Libraries\       ← NXLs instalados
C:\System\Drivers\         ← NEMs instalados
C:\System\Registry\        ← hives serializados (.hiv)
C:\System\Config\          ← config post-instalación
C:\Logs\pkg.log            ← log de operaciones pkg

7. Necesidades del Kernel

7.1 Estado actual del kernel (TCP/IP)

Componente Archivo Estado Detalle
Ethernet src/net/ethernet.rs ✅ Header 14B, type ETH_TYPE_ARP/IPV4, FCS computation
ARP src/net/arp.rs ✅ Cache 64 entradas, 300s timeout, request/reply, static entries
IPv4 src/net/ipv4.rs ✅ Header 20B, checksum, build_ipv4_header(), sin fragmentación
ICMP src/net/icmp.rs ✅ Echo request/reply, build_echo_reply(), checksum
UDP src/net/udp.rs ⚠️ Header 8B, checksum, sin dispatch de paquetes
TCP src/net/tcp.rs ⚠️ State machine (11 estados), buffers, sin handshake real
TCP send/recv src/net/tcp.rs ⚠️ tcp_send() escribe en send_buf local, no transmite
e1000 drivers/e1000/ (NEM) ✅ Probe, MMIO, RX/TX rings, poll_packet, send_packet
Socket manager src/net/socket.rs ✅ Alloc/bind/connect/listen/send/recv/close/wake
Socket→NIC TX src/net/socket.rs ❌ socket_send() no llama a NIC, solo escribe en send_buf local
NIC→Socket RX src/net/mod.rs ❌ net_handle_incoming_packet no rutea TCP/UDP a sockets
ObType::Socket src/object/types.rs ✅ type=18, ObInfoClass 17-20, ObSetInfoClass 18-22
ObSocket handler src/syscall/ob/ ✅ handlers para create/set/query de sockets

7.2 Gaps críticos para red userland

Gap 1: Transmit path (socket → NIC)

Estado: socket_send() en src/net/socket.rs:178 escribe datos en send_buf pero nunca construye headers Ethernet/IP/UDP/TCP ni llama a nic.send_packet().

Código actual:

pub fn socket_send(id: u32, data: &[u8]) -> Result<usize, ()> {
    let mut mgr = SOCKET_MANAGER.lock();
    let socket = mgr.get_socket_mut(id).ok_or(())?;
    if socket.direction != SocketDirection::Connected { return Err(()); }
    if socket.socket_type == SocketType::Tcp {
        if let Some(tcp_id) = socket.tcp_conn_id {
            return crate::net::tcp::tcp_send(tcp_id, data);
        }
    }
    socket.send_buf.extend_from_slice(data);  // ← solo buffer local
    Ok(data.len())
}

Código necesario:

pub fn socket_send(id: u32, data: &[u8]) -> Result<usize, ()> {
    let mut mgr = SOCKET_MANAGER.lock();
    let socket = mgr.get_socket_mut(id).ok_or(())?;
    if socket.direction != SocketDirection::Connected { return Err(()); }

    // 1. Obtener NIC para este socket (la primera NIC disponible)
    let nic_id = socket.nic_id.unwrap_or(0);
    let nic_mac = get_nic_mac(nic_id);
    let nic_ip = get_nic_ip(nic_id);

    // 2. Construir Ethernet header
    let dst_mac = resolve_dest_mac(socket.remote.ip, nic_id);  // ARP lookup
    let eth = EthernetHeader::new(dst_mac, nic_mac, ETH_TYPE_IPV4);

    // 3. Construir paquete según tipo
    let packet: Vec<u8> = match socket.socket_type {
        SocketType::Tcp => {
            let tcp_id = socket.tcp_conn_id.ok_or(())?;
            let tcp_data = build_tcp_segment(tcp_id, data)?;
            // tcp_data ya incluye header TCP
            let ipv4 = build_ipv4_header(nic_ip, socket.remote.ip, IPV4_PROTO_TCP,
                tcp_data.len(), 0);
            build_ethernet_frame(&eth, &ipv4, &tcp_data)
        }
        SocketType::Udp => {
            let udp_data = build_udp_datagram(socket.local.port, socket.remote.port, data);
            let ipv4 = build_ipv4_header(nic_ip, socket.remote.ip, IPV4_PROTO_UDP,
                udp_data.len(), 0);
            build_ethernet_frame(&eth, &ipv4, &udp_data)
        }
        SocketType::Raw => {
            // Raw: el payload ya incluye el header IP + protocolo
            // Pero necesitamos Ethernet header (MAC destino)
            let mut frame = Vec::with_capacity(ETH_HDR_LEN + data.len());
            frame.extend_from_slice(eth.as_bytes());
            frame.extend_from_slice(data);
            frame
        }
    };

    // 4. Enviar por NIC
    let mut registry = NIC_REGISTRY.lock();
    if let Some(nic) = registry.get_mut(nic_id) {
        nic.send_packet(&packet).ok();
    }

    Ok(data.len())
}

Funciones helper necesarias:

// En src/net/ethernet.rs:
pub fn build_ethernet_frame(eth: &EthernetHeader, ip: &Ipv4Header, payload: &[u8]) -> Vec<u8> {
    let mut frame = Vec::with_capacity(ETH_HDR_LEN + IPV4_HDR_MIN_LEN + payload.len());
    frame.extend_from_slice(eth.as_bytes());
    frame.extend_from_slice(ip.as_bytes());
    frame.extend_from_slice(payload);
    frame
}

// En src/net/udp.rs:
pub fn build_udp_datagram(src_port: u16, dst_port: u16, payload: &[u8]) -> Vec<u8> {
    let udp_len = UDP_HDR_LEN + payload.len();
    let mut pkt = Vec::with_capacity(udp_len);
    let hdr = UdpHeader::new(src_port, dst_port, udp_len as u16);
    pkt.extend_from_slice(hdr.as_bytes());
    pkt.extend_from_slice(payload);
    // Calcular checksum UDP (pseudo-header IPv4)
    pkt
}

// En src/net/arp.rs o nic.rs:
pub fn resolve_dest_mac(ip: Ipv4Addr, nic_id: u32) -> MacAddr {
    arp_lookup(ip).unwrap_or_else(|| {
        // Si no está en cache, enviar ARP request (síncrono = bloqueante)
        arp_request(ip, nic_id);
        // Esperar respuesta (timeout 1s) o devolver broadcast
        arp_lookup(ip).unwrap_or(MacAddr::broadcast())
    })
}

Gap 2: Receive path (NIC → socket)

Estado: net_handle_incoming_packet() maneja ARP e ICMP echo request, pero no rutea paquetes TCP/UDP a sockets.

Código actual (en src/net/mod.rs:72-191):

net_handle_incoming_packet(nic_id, packet)
  → parse Ethernet header
  → if ARP: handle arp request/reply
  → if IPv4:
      → parse IPv4 header
      → if ICMP: handle echo request → send reply
      → (TCP/UDP: no dispatch)

Código necesario:

// Después de parsear IPv4 y verificar protocolo:
if ip_hdr.protocol() == IPV4_PROTO_TCP {
    let tcp_payload = &payload[..];
    // Parse TCP header
    let tcp_hdr: &TcpHeader = unsafe { &*(tcp_payload.as_ptr() as *const TcpHeader) };
    let tcp_data = &tcp_payload[tcp_hdr.data_offset()..];

    // Buscar socket por (dst_ip, dst_port)
    let mut mgr = SOCKET_MANAGER.lock();
    let socket_id = find_socket_by_port(mgr, ip_hdr.dst_ip(), tcp_hdr.dst_port());

    if let Some(socket_id) = socket_id {
        let socket = mgr.get_socket_mut(socket_id).unwrap();

        if socket.direction == SocketDirection::Connected {
            // Push data a recv_buf
            socket.recv_buf.extend_from_slice(tcp_data);
            // Wake readers
            mgr.wake_socket_readers(socket_id);
        }
        // Para Listen → accept (future)
    }
    // No socket → enviar RST (future)
}

if ip_hdr.protocol() == IPV4_PROTO_UDP {
    let udp_payload = &payload[..];
    let udp_hdr: &UdpHeader = unsafe { &*(udp_payload.as_ptr() as *const UdpHeader) };
    let udp_data = &udp_payload[UDP_HDR_LEN..];

    // Buscar socket por (dst_port)
    let mut mgr = SOCKET_MANAGER.lock();
    let socket_id = find_socket_by_port_raw(mgr, udp_hdr.dst_port());

    if let Some(socket_id) = socket_id {
        let socket = mgr.get_socket_mut(socket_id).unwrap();
        socket.recv_buf.extend_from_slice(udp_data);
        mgr.wake_socket_readers(socket_id);
    } else {
        // No UDP socket en ese puerto → enviar ICMP Port Unreachable
        send_icmp_port_unreachable(nic_id, ip_hdr, udp_hdr);
    }
}

Funciones helper:

fn find_socket_by_port(mgr: &SocketManager, dst_ip: Ipv4Addr, dst_port: u16) -> Option<u32> {
    mgr.sockets.iter().flatten().find(|s| {
        s.direction == SocketDirection::Connected
        && s.socket_type == SocketType::Tcp
        && s.local.port() == dst_port
        && s.remote.ip == dst_ip  // opcional para filtrado estricto
    }).map(|s| s.id)
}

fn find_socket_by_port_raw(_mgr: &SocketManager, _port: u16) -> Option<u32> {
    // Búsqueda más permisiva (cualquier IP, puerto match)
}

Gap 3: ARP para transmisión

Cuando construimos un paquete para enviar, necesitamos la MAC destino. Si no está en cache ARP, debemos hacer ARP request síncrono.

pub fn resolve_mac_with_arp(ip: Ipv4Addr, nic_id: u32) -> MacAddr {
    // 1. Buscar en cache ARP
    if let Some(mac) = arp_lookup(ip) {
        return mac;
    }

    // 2. No en cache → enviar ARP request
    let mac = get_nic_mac(nic_id);
    let broadcast = MacAddr::broadcast();
    let arp_req = arp_make_packet(ARP_OP_REQUEST, mac, get_nic_ip(nic_id), broadcast, ip);

    // Enviar
    let mut registry = NIC_REGISTRY.lock();
    let _ = registry.get_mut(nic_id).map(|nic| nic.send_packet(&arp_req));
    drop(registry);

    // 3. Esperar respuesta (spinning o yield con timeout)
    for _ in 0..100_000 {
        if let Some(mac) = arp_lookup(ip) {
            return mac;
        }
        core::hint::spin_loop();
        // Alternativa: yield y check in loop con time limit
    }

    // 4. Timeout: broadcast
    MacAddr::broadcast()
}

Gap 4: Socket creation con nic_id

Cuando se crea un socket (sys_ob_create con ObType::Socket), el kernel debe asignar automáticamente una NIC al socket (la primera NIC disponible):

// En handler_ob_create, case ObType::Socket:
let socket_type = ((attrs & 0xFF) as u8).into();
let port = ((attrs >> 8) & 0xFFFF) as u16;

let mut mgr = SOCKET_MANAGER.lock();
let id = mgr.alloc_socket(socket_type).ok_or(Err(...))?;
let socket = mgr.get_socket_mut(id).unwrap();

// Asignar NIC por defecto
let nic_id = {
    let registry = NIC_REGISTRY.lock();
    if registry.nic_count() > 0 {
        Some(0u32)
    } else {
        None
    }
};
socket.nic_id = nic_id;

if port != 0 {
    socket.local = SocketAddrV4::new(get_nic_ip(0), port);
}

7.3 Plan de implementación por fases

Fase 1 — Transmit path (socket → NIC):

Archivos a modificar:

  • src/net/socket.rs — socket_send() construye headers y transmite
  • src/net/udp.rs — función build_udp_datagram() + calc_udp_checksum()
  • src/net/tcp.rs — función build_tcp_segment() (segmento TCP con seq/ack)
  • src/net/arp.rs — función resolve_mac_with_arp() (síncrona con timeout)
  • src/net/ethernet.rs — función build_ethernet_frame()

Fase 2 — UDP dispatch (NIC → socket UDP):

Archivos a modificar:

  • src/net/mod.rs — net_handle_incoming_packet() añadir rama UDP
  • src/net/socket.rs — find_socket_by_port() para UDP
  • src/net/icmp.rs — build_port_unreachable() para ICMP Port Unreachable

Fase 3 — TCP dispatch (NIC → socket TCP):

Archivos a modificar:

  • src/net/mod.rs — net_handle_incoming_packet() añadir rama TCP
  • src/net/tcp.rs — manejar paquetes entrantes (SYN, ACK, data, FIN, RST)
  • src/net/socket.rs — find_socket_by_port() para TCP

Fase 4 — TCP real (three-way handshake):

Arquitectura completa de la conexión TCP:

  1. socket_connect() → envía SYN → estado SYN_SENT
  2. NIC recibe SYN+ACK → kernel pone socket en estado Established → wake connect waiter
  3. socket_send() → envía segmentos TCP con seq++ y ACK
  4. NIC recibe ACK → actualiza estado TCP
  5. socket_close() → envía FIN → estado FIN_WAIT

7.4 Timeline de dependencias

Fase 1 (kernel): Transmit path (socket_send → NIC)
  ├── socket_send construye Ethernet+IP+protocolo
  ├── UDP datagram builder
  └── ARP resolve síncrono
  ↓
Fase 2 (kernel): UDP receive (NIC → socket recv_buf)
  ├── net_handle_incoming_packet UDP dispatch
  ├── UDP socket lookup por puerto
  └── wake_socket_readers
  ↓
Fase 3 (kernel): TCP receive (NIC → socket)
  ├── TCP packet dispatch in net_handle_incoming_packet
  └── TCP state machine real (handshake)
  ↓
Fase 4 (libneodos): Exponer ob_type::SOCKET, info classes, wrappers
  ↓
Fase 5 (net.nxl): API userland completa
  ↓
Fase 6 (aplicaciones): ipconfig.nxe, ping.nxe, dhcp.nxe
  ↓
Fase 7 (NeoInit): Migración a Registry, servicios auto-start
  ↓
Fase 8 (pkg.nxe): Sistema de paquetes v1

8. Roadmap

8.1 Antes de v1.0 (orden de implementación)

# Fase Tarea Archivos Esfuerzo
1 F1 Kernel: Ethernet frame builder ethernet.rs Pequeño
2 F1 Kernel: UDP datagram builder udp.rs Pequeño
3 F1 Kernel: ARP resolve síncrono para TX arp.rs Medio
4 F1 Kernel: socket_send transmite por NIC socket.rs Medio
5 F2 Kernel: UDP dispatch en receive path mod.rs, socket.rs Medio
6 F2 Kernel: ICMP Port Unreachable icmp.rs Pequeño
7 F3 Kernel: TCP receive dispatch mod.rs, tcp.rs Medio
8 F4 Kernel: TCP real three-way handshake tcp.rs, socket.rs Grande
9 F4 libneodos: SOCKET constant + wrappers libneodos/syscall.rs Pequeño
10 F5 net.nxl: librería userland de red libnet/ (nuevo) Medio
11 F6 ipconfig.nxe: herramienta userland userbin/ipconfig/ Pequeño
12 F6 ping.nxe: ICMP userland userbin/ping/ Medio
13 F6 dhcp.nxe: DHCP client userbin/dhcp/ Grande
14 F7 Registry: crear valores por defecto en boot main.rs, cm/mod.rs Pequeño
15 F7 NeoInit: leer Registry para DefaultShell userbin/neoinit/ Pequeño
16 F7 NeoInit: auto-start de servicios userbin/neoinit/ Medio
17 F7 netapplier.nxe: aplicador de red (servicio NetApplier); netcfg es la CLI userbin/netapplier/, userbin/netcfg/ Medio
18 F7 Registry: persistencia a disco (cm_flush_key) cm/mod.rs, cm/hive/ Medio
19 F8 pkg.nxe: sistema de paquetes v1 userbin/pkg/ Grande

8.2 Después de v1.0

# Tarea Descripción
20 DNS: resolución en net.nxl Consulta DNS sobre UDP
21 dnsresv.nxe Herramienta de resolución DNS
22 TCP accept() Aceptar conexiones entrantes
23 NeoPkg repositorio Servidor de paquetes oficial
24 Firmas digitales Ed25519 en .npkg
25 NeoSetup Instalador en modo texto/gráfico
26 netstat.nxe Estadísticas de red
27 traceroute.nxe Traza de ruta ICMP
28 HTTP client library Futura http.nxl

9. Diagramas de Flujo

9.1 Boot completo con red

Nota: el arranque de red lo realizan dos servicios del Service Manager: Dhcpc (dhcpd.nxe) hace DHCP y publica el lease en el Registry, y NetApplier (netapplier.nxe) es el aplicador residente que lleva la configuración a la NIC. netcfg es la CLI one-shot. Ver §4.4.

Bootloader (UEFI)
  ↓
Kernel
  ├── Phase 1-2: GDT, IDT, paging, heap, APIC, HPET, ACPI
  ├── Phase 3.0-3.85: Drivers, PCI, AHCI, GPT, FS mount
  ├── Phase 3.88: init_networking()
  │   ├── create \Device\Tcp, \Device\Udp, \Device\Nic
  │   ├── probe_e1000() → NIC 0 found
  │   ├── set NIC 0 IP = 10.0.1.80 (temporal)
  │   ├── arp_tick starts
  │   └── network_poll_all starts (in idle loop)
  │
  ├── Phase 3.881: init_cm()
  │   ├── create \Registry\Machine, \Registry\User
  │   ├── mount SYSTEM hive
  │   └── create_default_registry_values()
   │       ├── CurrentControlSet\Services\NeoInit\DefaultShell = "C:\Programs\neoshell.nxe"
   │       ├── CurrentControlSet\Services\Dhcpc\ImagePath = "C:\System\Tools\dhcpd.nxe"
   │       ├── CurrentControlSet\Services\Network\Interfaces\0\DHCPEnabled = 1
   │       └── CurrentControlSet\Control\WaitForNetwork = 0
   │
   ├── Phase 3.9: ABI freeze, validate syscalls
   │
   └── Phase 4: Spawn NeoInit (PID 1)
        │
        ▼
   NeoInit (PID 1)
        │
        ├── [kernel API: cm_open_key, cm_query_value]
        │
        ├── Abrir \Registry\Machine\System\CurrentControlSet\Services\NeoInit
        │
        ├── Leer DefaultShell → "C:\Programs\neoshell.nxe"
        │
        ├── El Service Manager (kernel) arranca los servicios System/Auto:
        │   │
        │   ├── Dhcpc (dhcpd.nxe) — DORA; publica el lease en el Registry
        │   │     ├── load_net() / sockets UDP 68 → 67
        │   │     ├── dhcp_discover() → Offer → Request → ACK
        │   │     ├── Guarda en Registry: IPAddress/SubnetMask/Gateway/DnsServer
        │   │     └── loop { yield } (servicio)
        │   └── NetApplier (netapplier.nxe) — aplica la config a la NIC
        │         └── loop { Registry → SetNicIp/SetNicGateway } (servicio)
        │
        ├── NetApplier aplica el Registry a la NIC (estático o el lease de Dhcpc)
        │     (aplicador residente; ver §4.4.1)
        │
        ├── Leer WaitForNetwork=0 → no esperar
        │
        └── Loop: spawn neoshell.nxe → wait → respawn

9.2 Flujo detallado: net_socket_send (UDP)

user: net_socket_send(fd=3, data=DHCP_Discover)

  1. net.nxl (Ring 3):
     ob_set_info(3, SOCKET_SEND=21, &data, len)

       ↓ INT 0x80

  2. Kernel: handler_ob_set_info (src/syscall/ob/)
     → info_class == SocketSend(21)
     → buscar ObObject por fd → native_id = socket_id
     → llamar socket_send(socket_id, data)

       ↓

  3. Kernel: socket_send (src/net/socket.rs)
     → lock SOCKET_MANAGER
     → get socket by id
     → socket.nic_id = 0 (primera NIC)
     → socket.remote = 255.255.255.255:67
     → socket.local = 0.0.0.0:68
     → socket_type = UDP (2)
     → direction = Connected

     → 3a. Construir UDP datagram:
         → udp_build_datagram(src_port=68, dst_port=67, data)
         → UDP header src=68 dst=67 len=... checksum=0

     → 3b. Construir IPv4 header:
         → src IP = 0.0.0.0 (o IP de NIC)
         → dst IP = 255.255.255.255
         → proto = UDP (17)
         → length = UDP_hdr + data
         → checksum

     → 3c. Resolver MAC destino:
         → dst IP = 255.255.255.255
         → ARP lookup: no encontrado (broadcast)
         → MAC destino = FF:FF:FF:FF:FF:FF

     → 3d. Construir Ethernet frame:
         → src MAC = NIC MAC
         → dst MAC = FF:FF:FF:FF:FF:FF
         → type = 0x0800 (IPv4)
         → payload = IPv4 header + UDP header + data

     → 3e. Transmitir:
         → lock NIC_REGISTRY
         → nic[0].send_packet(&ethernet_frame)
         → unlock NIC_REGISTRY

     → 3f. Liberar:
         → unlock SOCKET_MANAGER
         → return Ok(len)

       ↓

  4. Kernel: e1000 send_packet (drivers/e1000/ (NEM))
     → Wait for available TX descriptor
     → Copy packet data to DMA buffer
     → Update TX descriptor (addr, len, cmd)
     → Ring doorbell (TDT register)
     → Return Ok

       ↓

  5. Hardware: e1000 NIC transmite por cable Ethernet

9.3 Flujo detallado: net_socket_recv (UDP)

  Hardware: e1000 NIC recibe paquete Ethernet

       ↓

  1. Kernel: network_poll_all (src/net/mod.rs)
     → lock NIC_REGISTRY
     → for each NIC:
         → nic.poll_packet(&mut buf) → Some(len)
         → net_handle_incoming_packet(nic_id, &buf[..len])

       ↓

  2. Kernel: net_handle_incoming_packet
     → Parse Ethernet header → type = 0x0800 (IPv4)
     → Parse IPv4 header → proto = 17 (UDP)
     → UDP dispatch:

       → Parse UDP header
         → src_port = 67, dst_port = 68
         → udp_data = payload after UDP header

       → Buscar socket UDP con local.port == 68
         → SOCKET_MANAGER.lock()
         → sockets.iter().find(|s| s.socket_type == UDP && s.local.port == 68)
         → Socket found! id = 1
         → socket.recv_buf.extend_from_slice(udp_data)
         → mgr.wake_socket_readers(1)
         → magic = 0x0009_1000 | 1
         → scheduler.wake_blocked_on_magic(magic)
         → unlock SOCKET_MANAGER

       ↓

  3. Kernel: scheduler
     → Thread T (PID 2, dhcp.nxe) está Blocked con magic = 0x0009_1001
     → scheduler encuentra thread bloqueado
     → thread.state = Ready
     → set NEED_RESCHED

       ↓  (próximo timer tick o syscall return)

  4. Kernel: schedule()
     → Selecciona Thread T (dhcp.nxe) como next
     → Context switch to Ring 3

       ↓

  5. user: dhcp.nxe re-ejecuta net_socket_recv()
     → net.nxl:
       → ob_query_info(3, SocketRecv(23), &buf)
         → kernel: socket_recv() → copy recv_buf → return len
       → return Ok(len)

     → dhcp.nxe recibe DHCP Offer payload

10. Consideraciones de Implementación

10.1 net.nxl como NXL independiente vs linked static

Decisión: NXL independiente en slot 3 (0x1e0c0000).

Aspecto NXL separado Linked static en libneodos
Tamaño de libneodos Pequeño (solo base) Crece con net
Carga Bajo demanda Siempre cargado
Actualización Independiente Requiere recompilar libneodos
Aplicaciones sin red No cargan net.nxl Pagan memoria aunque no usen red
Complejidad Mayor (ABI entre NXLs) Menor (todo en una lib)

net.nxl se carga bajo demanda:

// En el stub de libneodos:
pub fn load_net() -> Result<u64, i64> {
    loadlib("C:\\System\\Libraries\\net.nxl")
}

O lazy desde cada NXE:

// En ipconfig.nxe:
let net_base = sys_loadlib("C:\\System\\Libraries\\net.nxl").unwrap();
let net = unsafe { &*(net_base as *const NetAbiTable) };

10.2 net.nxl acceso a syscalls sin libneodos

net.nxl no debe depender de libneodos NXL. En su lugar, usa inline asm directamente para las syscalls que necesita:

// libnet/src/syscall.rs — wrappers locales
fn ob_create(path: *const u8, obj_type: u32, fds: *mut u64, attrs: u64) -> i64 {
    let r: i64;
    unsafe {
        core::arch::asm!(
            "push rbx", "push rcx", "push rdx", "push r8",
            "mov rbx, {p}", "mov rcx, {t}", "mov rdx, {f}", "mov r8, {a}",
            "mov rax, 61", "int 0x80",
            "pop r8", "pop rdx", "pop rcx", "pop rbx",
            p = in(reg) path as u64,
            t = in(reg) obj_type as u64,
            f = in(reg) fds as u64,
            a = in(reg) attrs,
            out("rax") r,
            options(nostack),
        );
    }
    r
}
// ... similar para ob_set_info (63), ob_query_info (62), ob_wait (65), ob_close (13)

10.3 Nombrado de objetos Ob para sockets

Los sockets se crean con paths únicos en el namespace Ob:

\Ob\Socket\<pid>\<name>

Donde <name> es el path proporcionado por net_socket_create().

Ejemplos:

  • \Ob\Socket\2\DhcpClient — socket DHCP de dhcpd (PID 2)
  • \Ob\Socket\3\Ping — socket ping de ping.nxe (PID 3)

Si net_socket_create() recibe un path relativo, net.nxl lo completa:

pub fn net_socket_create(path: &str, socket_type: u32) -> Result<u8, NetError> {
    let pid = syscall::sys_getpid();
    let full_path = alloc::format!("\\Ob\\Socket\\{}\\{}", pid, path.trim_start_matches('\\'));
    // ...
    syscall::sys_ob_create(&full_path, 18, None, attrs as u64)
}

10.4 Recepción de datos: SocketRecv vs SocketInfo

Para la recepción de datos, el diseño usa ObInfoClass::SocketRecv = 23. Este nuevo info class no existe actualmente en el kernel.

Implementación en el kernel:

// En src/syscall/ob/, handler_ob_query_info:
_ if info_class == 23 /* SocketRecv */ => {
    let ob = ob_table.lock().get(fd).ok_or(SyscallError::BadF)?;
    if ob.obj_type != ObType::Socket {
        return Err(SyscallError::InvalidParam);
    }
    let socket_id = ob.native_id as u32;

    let mut mgr = SOCKET_MANAGER.lock();
    let socket = mgr.get_socket_mut(socket_id).ok_or(SyscallError::NotFound)?;

    let available = socket.recv_buf.len().min(user_buf.len());
    if available == 0 {
        return Err(SyscallError::Again);  // -EAGAIN
    }

    user_buf[..available].copy_from_slice(&socket.recv_buf[..available]);
    socket.recv_buf.drain(..available);

    Ok(available as i64)
}

ObInfoClass::SocketRecv debe añadirse al enum:

// src/object/types.rs
pub enum ObInfoClass {
    // ... existentes hasta NicInfo=20 ...
    RegistryKey = 21,
    RegistryValue = 22,
    SocketRecv = 23,       // ← nuevo
}

10.5 Puerto efímero para sockets

Cuando un socket UDP/TCP se crea sin bind explícito, el kernel debe asignar un puerto efímero automáticamente.

const EPHEMERAL_PORT_START: u16 = 49152;
const EPHEMERAL_PORT_END: u16 = 65535;
static NEXT_EPHEMERAL_PORT: AtomicU16 = AtomicU16::new(EPHEMERAL_PORT_START);

fn alloc_ephemeral_port() -> u16 {
    loop {
        let port = NEXT_EPHEMERAL_PORT.fetch_add(1, Ordering::Relaxed);
        let port = if port < EPHEMERAL_PORT_START { EPHEMERAL_PORT_START } else { port };
        // Verificar que no esté en uso (simplificado)
        if port <= EPHEMERAL_PORT_END {
            return port;
        }
    }
}

10.6 netcfg.nxe / netapplier.nxe — implementación

netcfg.nxe  (CLI one-shot)
  │
  ├── loadlib("C:\\System\\Libraries\\net.nxl")
  │
  ├── Abrir Registry:
  │   fd = cm_open_key("\\Registry\\Machine\\System\\CurrentControlSet\\Services\\Network\\Interfaces\\0")
  │   dhcp = cm_query_dword(fd, "DHCPEnabled")
  │   ip    = cm_query_dword(fd, "IPAddress")
  │   mask  = cm_query_dword(fd, "SubnetMask")
  │   gw    = cm_query_dword(fd, "Gateway")
  │   dns   = cm_query_dword(fd, "DnsServer")
  │
  ├── /setip, /setdns, /dhcp, /reset, /status, /test, /apply, ...
  │   ✓ escribe el Registry + flush y aplica cuando corresponde; luego exit(0)
  │
  └── sin args = /apply (una vez) y exit(0) — nunca residente

netapplier.nxe  (servicio NetApplier, aplicador residente)
  ├── cada ~decenas de ms lee ip/mask/gw y el link (NicInfo)
  ├── el DHCP lo publica Dhcpc en el Registry (DORA/renovación)
  ├── si cambian o el link sube (0→1):
  │     ✓ net_set_ip(0, ip, mask)
  │     ✓ net_set_gateway(0, gw)
  └── loop { Registry → NIC } (el servicio nunca termina)

10.7 Persistencia del Registry

El hive SYSTEM se serializa a disco en:

  • C:\System\Registry\SYSTEM.hiv

En boot, antes de init_cm(), el kernel intenta cargar el hive desde disco:

fn init_cm() {
    // Intentar cargar hive desde disco
    let data = read_file("C:\\System\\Registry\\SYSTEM.hiv");
    if let Ok(data) = data {
        // Parsear y reconstruir Hive desde bytes
        let hive = Hive::deserialize(&data);
        mount_hive("SYSTEM", "\\Registry\\Machine\\System", hive);
    } else {
        // No existe → crear hive nuevo
        let hive = Hive::new("SYSTEM");
        mount_hive("SYSTEM", "\\Registry\\Machine\\System", hive);
        create_default_registry_values();
    }
}

10.8 Estrategia de test

Componente Tests Herramienta
Kernel transmit path Tests unitarios de socket_send con mock NIC kernel test framework
Kernel receive path Tests con paquetes Ethernet sintéticos kernel test framework
Socket UDP dispatch Crear socket UDP, enviar paquete sintético, verificar recv_buf kernel test framework
libneodos wrappers Tests de compilación (no ejecución) cargo test (host)
net.nxl Tests unitarios de parsing (no requieren NIC) cargo test (host) con mock de syscall
ipconfig.nxe Test de integración: ejecutar ipconfig y verificar salida neodev test
ping.nxe Enviar ping a 127.0.0.1 (loopback futura) o QEMU host neodev test
dhcp.nxe Simular servidor DHCP, verificar client neodev test
NeoInit Registry Test unitario de cm_open_key + cm_query_value kernel test framework
pkg.nxe Test de instalación/remoción con paquete de prueba neodev test

11. Apéndice: Cambios por Archivo (implementados en v0.3)

11.1 Kernel source

Archivo Cambio Estado
src/object/types.rs ObInfoClass::SocketRecv = 23, ObSetInfoClass::SetNicIp = 27 ✅
src/syscall/ob/ Handler SocketRecv (class 23) en query_info — copia recv_buf, -EAGAIN ✅
src/syscall/ob/ Handler SetNicIp (class 27) en set_info — llama nic_set_ip() ✅
tools/gen-hiv Crea el servicio NetApplier (netapplier.nxe, StartType=Auto) y Dhcpc ✅
src/scheduler/mod.rs dhcp_tick() llamado desde idle loop para que DHCP progrese ✅

11.2 libneodos

Archivo Cambio Estado
src/syscall.rs ob_type::SOCKET = 18, ObInfoClass::SocketRecv=23 ✅
src/syscall.rs Wrappers: ob_socket_create/connect/bind/listen/send/recv/close ✅
src/syscall/mod.rs SocketAddrV4 struct, sys_cm_set_value (RAX=53) + ob_syscall_5! macro ✅
src/syscall.rs ObSetInfoClass::SetNicIp = 27 ✅

11.3 Nuevos proyectos

Proyecto Path Produce Estado
libnet-nxl libnet-nxl/ net.nxl → C:\System\Libraries\net.nxl (slot 3, 0x1e0c0000) ✅
libnet libnet/ Static library wrapper con lazy loading ✅
netcfg userbin/netcfg/ netcfg.nxe → CLI de configuración de red (one-shot) ✅
netapplier userbin/netapplier/ netapplier.nxe → aplicador residente (servicio NetApplier) ✅
ipconfig userbin/ipconfig/ ipconfig.nxe → muestra info de interfaces ✅
ping userbin/ping/ (pendiente) ❌
dhcp userbin/dhcp/ (pendiente — DHCP se hace en kernel vía idle loop) ❌

11.4 Scripts

Componente Cambio Estado
neodev build Build loop incluye netcfg, añadido build de libnet-nxl ✅
neodev/src/image.rs Incluye net.nxl (System\Libraries) y netcfg.nxe (System\Tools) ✅

Documento actualizado a v0.3 reflejando la implementación real (NET-1.5/1.6/1.8/1.15). Ver roadmap/improvements.md para el estado actual de cada tarea.