Versión: v0.3 (implementado) Fecha: 2026-07-05 Estado: Implementado — NET-1.5/1.6/1.8/1.15 completados
- Visión General
- net.nxl — Librería de Red Userland
- Herramientas NXE de Red
- NeoInit System Configuration
- Registry Usage Model
- NeoPkg — Package System
- Necesidades del Kernel
- Roadmap
- Diagramas de Flujo
- Consideraciones de Implementación
- Apéndice: Cambios por Archivo
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).
| 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 |
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
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
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);/// 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
// 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.
// 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_creategenera un path único (\NetSock-{hex}) porqueob_createrequiere path en namespace.sock_typese codifica enattrs(bits 0-7).bind/connectrecibenip: u32(big-endian) yport: u16(host order), no un struct.sendretorna bytes enviados como valor positivo.recvusaObInfoClass::SocketRecv(class 23). No implementaob_waitpara bloqueo.closellamaObSetInfoClass::SocketCloseque libera el socket en el kernel.net_get_tcp_statusynet_get_socket_addrexisten 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};#[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.
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:
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)
}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)
}// 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:
patheliminado desocket_create— se genera automáticamenteippasado comou32(big-endian) en vez de*const u8array- Se añadieron
get_ip,get_gateway,get_mask,get_dhcp_bound get_tcp_statusyget_socket_addrno tienen wrapper (accesibles vía ObInfoClass directamente)set_gatewayaplica el gateway a la NIC víaObSetInfoClass::SetNicGateway(28); la persistencia en Registry la hace el caller (p. ej.dhcpdescribeGateway)
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
}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.
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(crateno_stdsin dependencias, host-testeable) define el contrato puro: nombres canónicos (VALUE_*),NetConfig,parse_ip,format_ip,DEFAULT_MASKybuild_interface_path.libnet::configañ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).
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:
hostnamemuestra 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:
- 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 conSetHostname(persistencia +hostname/sys_get_hostname). - 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).
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 conob_query_info(fd, SocketAccept)y se cierra sin afectar al listener. ob_waitcon 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\.
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)
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:
socket_sendcon tipo Raw → construir Ethernet + IPv4 con proto del payload ICMPnet_handle_incoming_packetcon proto ICMP → mirar si hay socket Raw escuchando en esa IP y entregar payload
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:
- UDP socket funcional (bind puerto 68, enviar a 255.255.255.255:67)
- Broadcast ethernet (dest FF:FF:FF:FF:FF:FF)
- El kernel construya header UDP+IP+Ethernet desde
socket_send - Receive path: UDP paquetes entrantes dispatch al socket bind al puerto 68
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).
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.
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.
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.nxehardcoded - No usa Registry
- No inicia servicios (net, logger, etc.)
- No se puede configurar sin recompilar
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>
netapplieres el aplicador residente (servicioNetApplier) ydhcpdel cliente DHCP;netcfges solo la CLI de configuración. Ver §4.4.
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(); }
}
}
}
}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.
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/Gatewayy 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; servicioNetApplier.
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).
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
netdse 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 connetdAuto), así que se restaura aAuto.netdsigue registrado y arrancable; el flake restante de DHCP (S3) es independiente denetd.
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
yieldy monitorización de enlace (subida/bajada por NIC). No aplica configuración (eso esnetapplier). - Fuera de alcance de v1: DHCP (
dhcpd), DNS, routing y firewall. - Binario
C:\System\Tools\netd.nxe; servicioNetden el hive por defecto. - Identidad y frontera se deciden en #362; implementación en #372.
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.
| 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 | ❌ |
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.
\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"
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:
- Síncrono: Cada
cm_set_value→ marcar hive dirty.cm_flush_keyse llama desde el shell (FLUSHREGcommand), o desde NeoInit antes de spawn shell, o en shutdown. - Periódico: Un demonio de kernel (work queue) cada N segundos si dirty.
- 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.
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.
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.
┌──────────────────────────────────────────────────────────┐
│ 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
}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
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",
];- Solo procesos con token admin pueden ejecutar
pkg.nxe pkg.nxeverificais_current_admin()al arrancar- Paquetes System requieren flag
/Fpara REMOVE - Paquetes Core no se pueden REMOVE ni con
/F - Verificación de checksum post-instalación
- (Futuro) Firmas digitales con ed25519
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
| 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 |
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(ð, &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(ð, &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())
})
}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)
}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()
}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);
}Fase 1 — Transmit path (socket → NIC):
Archivos a modificar:
src/net/socket.rs—socket_send()construye headers y transmitesrc/net/udp.rs— funciónbuild_udp_datagram()+calc_udp_checksum()src/net/tcp.rs— funciónbuild_tcp_segment()(segmento TCP con seq/ack)src/net/arp.rs— funciónresolve_mac_with_arp()(síncrona con timeout)src/net/ethernet.rs— funciónbuild_ethernet_frame()
Fase 2 — UDP dispatch (NIC → socket UDP):
Archivos a modificar:
src/net/mod.rs—net_handle_incoming_packet()añadir rama UDPsrc/net/socket.rs—find_socket_by_port()para UDPsrc/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 TCPsrc/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:
socket_connect()→ envía SYN → estado SYN_SENT- NIC recibe SYN+ACK → kernel pone socket en estado Established → wake connect waiter
socket_send()→ envía segmentos TCP con seq++ y ACK- NIC recibe ACK → actualiza estado TCP
socket_close()→ envía FIN → estado FIN_WAIT
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
| # | 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 |
| # | 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 |
Nota: el arranque de red lo realizan dos servicios del Service Manager:
Dhcpc(dhcpd.nxe) hace DHCP y publica el lease en el Registry, yNetApplier(netapplier.nxe) es el aplicador residente que lleva la configuración a la NIC.netcfges 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
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(ðernet_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
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
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) };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)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)
}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
}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;
}
}
}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)
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();
}
}| 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 |
| 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 |
✅ |
| 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 |
✅ |
| 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) | ❌ |
| 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.