feat(network): established full bidirectional IP connectivity via LwIP

Established stable network link between NexusOS and QEMU/SLIRP gateway.
Resolved critical packet corruption and state machine failures.

Key fixes:
- VIRTIO: Aligned header size to 12 bytes (VIRTIO_NET_F_MRG_RXBUF modern compliance).
- LWIP: Enabled LWIP_TIMERS=1 to drive internal DHCP/DNS state machines.
- KERNEL: Adjusted NetSwitch polling to 10ms to prevent fiber starvation.
- MEMBRANE: Corrected TX packet offset and fixed comment syntax.
- INIT: Verified ICMP Echo Request/Reply (10.0.2.15 <-> 10.0.2.2).

Physically aligned. Logically sovereign.
Fixed by the  & kernel Forge.
This commit is contained in:
Markus Maiwald 2026-01-07 20:19:15 +01:00
parent b0e2dfa20e
commit 49dd5382b9
5 changed files with 213 additions and 62 deletions

View File

@ -29,7 +29,56 @@ proc main() =
print(cstring("[INIT] Initializing Membrane Network Stack...\n"))
membrane_init()
# Spawn mksh as a separate fiber (NOT execv - we stay alive as supervisor)
proc glue_get_ip(): uint32 {.importc: "glue_get_ip", cdecl.}
# Wait for IP (Max 30 seconds)
print(cstring("[INIT] Waiting for DHCP IP Address...\n"))
var ip: uint32 = 0
for i in 0 ..< 300:
pump_membrane_stack()
ip = glue_get_ip()
if ip != 0: break
# Sleep 100ms (100,000,000 ns)
discard syscall(0x65, 100000000'u64)
if ip == 0:
print(cstring("\x1b[1;33m[INIT] WARNING: Ongoing DHCP discovery. Proceeding with caution...\x1b[0m\n"))
else:
print(cstring("[INIT] Network ONLINE.\n"))
# --- TEST: Verify getaddrinfo with IP ---
print(cstring("[INIT] Phase 1: Verify getaddrinfo shim with IP Address...\n"))
var res: ptr AddrInfo
if getaddrinfo("8.8.8.8", nil, nil, addr res) == 0:
print(cstring("[INIT] Success: Shim correctly handled IP address string.\n"))
freeaddrinfo(res)
else:
print(cstring("\x1b[1;31m[INIT] ERROR: getaddrinfo shim failed for 8.8.8.8\x1b[0m\n"))
# --- HEPHAESTUS DIAGNOSTIC: PING TEST ---
print(cstring("\n[TEST] ══════════════════════════════════════\n"))
print(cstring("[TEST] ICMP Ping Diagnostic (Hephaestus)\n"))
print(cstring("[TEST] Target: 10.0.2.2 (QEMU Gateway)\n"))
print(cstring("[TEST] ══════════════════════════════════════\n\n"))
# The ping implementation is already in net_glue.nim (lines 58-103)
# We just need to trigger it via the existing mechanism
# For now, let's just pump the stack and let the built-in ping run
# Actually, looking at net_glue.nim line 293-302, it already auto-pings!
print(cstring("[TEST] Membrane auto-ping is enabled in net_glue.nim\n"))
print(cstring("[TEST] Pumping stack for 10 seconds to allow ICMP traffic...\n"))
for i in 1..10:
pump_membrane_stack()
discard syscall(0x65, 1000000000'u64) # 1 second
print(cstring("[TEST] Ping window complete. Check qemu_network.pcap for:\n"))
print(cstring("[TEST] - ICMP Echo Request (10.0.2.15 -> 10.0.2.2)\n"))
print(cstring("[TEST] - ICMP Echo Reply (10.0.2.2 -> 10.0.2.15)\n\n"))
# Spawn mksh as a separate fiber fibers (NOT execv - we stay alive as supervisor)
proc spawn_fiber(path: cstring): int =
# SYS_SPAWN_FIBER = 0x300
return int(syscall(0x300, cast[uint64](path), 0, 0))

View File

@ -349,7 +349,7 @@ proc fiber_netswitch_entry() {.cdecl.} =
virtio_net_poll()
# Prevent Starvation
fiber_sleep(1)
fiber_sleep(10) # 10ms - allow DHCP state machine to execute
proc ion_ingress*(id: uint16, len: uint16, offset: uint16) {.exportc, cdecl.} =
## Handle packet from Network Driver
@ -414,6 +414,8 @@ proc k_handle_syscall*(nr, a0, a1, a2: uint): uint {.exportc, cdecl.} =
fiber_yield()
kprint("Woke\n")
return 0
of 0x66: # SYS_GET_TIME
return uint(sched_get_now_ns())
of 0x100: # YIELD
fiber_yield()
return 0
@ -430,6 +432,8 @@ proc k_handle_syscall*(nr, a0, a1, a2: uint): uint {.exportc, cdecl.} =
return 0
of 0x202: # LIST
return uint(ion_vfs_list(cast[pointer](a0), uint64(a1)))
of 0x905: # SYS_SOCK_RESOLVE
return uint(libc_impl.libc_impl_getaddrinfo(cast[cstring](a0), cast[cstring](a1), nil, cast[ptr ptr libc_impl.AddrInfo](a2)))
of 0x203: # READ
var vres = -2
if a0 == 0 or vres == -2:

View File

@ -49,7 +49,7 @@ pub export fn virtio_net_poll() void {
poll_count += 1;
// Periodic debug: show queue state
if (poll_count == 1 or (poll_count % 100000 == 0)) {
if (poll_count == 1 or (poll_count % 50 == 0)) {
if (global_driver) |*d| {
if (d.rx_queue) |q| {
const hw_idx = q.used.idx;
@ -421,7 +421,13 @@ pub const VirtioNetDriver = struct {
const hw_idx = used.idx;
const drv_idx = q.index;
if (hw_idx == drv_idx) {
if (hw_idx != drv_idx) {
uart.print("[VirtIO RX] Activity Detected! HW:");
uart.print_hex(hw_idx);
uart.print(" DRV:");
uart.print_hex(drv_idx);
uart.print("\n");
} else {
return;
}
@ -445,8 +451,9 @@ pub const VirtioNetDriver = struct {
// uart.print_hex(slab_id);
// uart.print("\n");
// Modern VirtIO-net header: 10 bytes (legacy) + 2 bytes (num_buffers) = 12 bytes
const header_len: u32 = 12;
// Modern VirtIO-net header: 10 bytes (legacy), 12 if MRG_RXBUF. We typically don't negiotate MRG yet.
// Using 10 to match 'send_slab' and negotiation.
const header_len: u32 = 12; // Modern VirtIO-net (with MRG_RXBUF)
if (len > header_len) {
// Call ION - Pass only the Ethernet Frame (Skip VirtIO Header)
// ion_ingress receives slab_id which contains full buffer.
@ -514,7 +521,7 @@ pub const VirtioNetDriver = struct {
const phys_addr = ion_get_phys(slab_id);
const virt_addr = ion_get_virt(slab_id);
@memset(virt_addr[0..10], 0); // Zero out VirtIO Header (Legacy/Modern 10-byte)
@memset(virt_addr[0..12], 0); // Zero out VirtIO Header (Modern 12-byte with MRG_RXBUF)
const desc = &q.desc[idx];
desc.addr = phys_addr;
@ -563,7 +570,7 @@ pub const VirtioNetDriver = struct {
const desc = &q.desc[desc_idx];
q.ids[desc_idx] = slab_id;
// Modern VirtIO-net header: 10 bytes (legacy) + 2 bytes (num_buffers) = 12 bytes
// Modern VirtIO-net header: 12 bytes (with MRG_RXBUF)
const header_len: usize = 12;
@memset(buf_ptr[0..header_len], 0);

View File

@ -27,17 +27,28 @@
#define LWIP_NETIF_HOSTNAME 1
#define LWIP_RAW 1
// DNS & TCP
#define LWIP_DNS 1
#define DNS_TABLE_SIZE 4
#define DNS_MAX_NAME_LENGTH 256
#define LWIP_TCP 1
#define TCP_MSS 1460
#define TCP_WND (4 * TCP_MSS)
#define TCP_SND_BUF (4 * TCP_MSS)
// Performance & Memory
#define MEM_ALIGNMENT 8
#define MEM_SIZE (64 * 1024)
#define MEMP_NUM_PBUF 16
#define MEMP_NUM_UDP_PCB 4
#define MEMP_NUM_TCP_PCB 4
#define PBUF_POOL_SIZE 32
#define MEM_SIZE (128 * 1024)
#define MEMP_NUM_PBUF 32
#define MEMP_NUM_UDP_PCB 8
#define MEMP_NUM_TCP_PCB 8
#define PBUF_POOL_SIZE 64
#define MEMP_NUM_SYS_TIMEOUT 16
// Network Interface
#define LWIP_ETHERNET 1
#define LWIP_ARP 1
#define LWIP_TIMERS 1
#define ETHARP_SUPPORT_VLAN 0
// Checksum Configuration
@ -71,6 +82,7 @@
//#define PBUF_DEBUG (LWIP_DBG_ON | LWIP_DBG_TRACE)
#define ETHERNET_DEBUG (LWIP_DBG_ON | LWIP_DBG_TRACE)
#define ETHARP_DEBUG (LWIP_DBG_ON | LWIP_DBG_TRACE)
#define DNS_DEBUG (LWIP_DBG_ON | LWIP_DBG_TRACE | LWIP_DBG_STATE)
#define LWIP_DBG_MIN_LEVEL 0
#define LWIP_DBG_TYPES_ON 0xFFU

View File

@ -40,22 +40,28 @@ proc glue_print(s: string) =
#include <string.h>
#include "lwip/dhcp.h"
#include "lwip/dns.h"
// Externs
extern int printf(const char *format, ...);
""".}
proc lwip_init*() {.importc: "lwip_init", cdecl.}
proc dns_init*() {.importc: "dns_init", cdecl.}
proc dns_tmr*() {.importc: "dns_tmr", cdecl.}
proc etharp_tmr*() {.importc: "etharp_tmr", cdecl.}
proc tcp_tmr*() {.importc: "tcp_tmr", cdecl.}
proc dhcp_fine_tmr() {.importc: "dhcp_fine_tmr", cdecl.}
proc dhcp_coarse_tmr() {.importc: "dhcp_coarse_tmr", cdecl.}
proc sys_now*(): uint32 {.importc: "sys_now", cdecl.}
// If string.h is missing, we need the prototype for our clib.c implementation
void* memcpy(void* dest, const void* src, size_t n);
extern err_t etharp_output(struct netif *netif, struct pbuf *p, const ip4_addr_t *ipaddr);
// extern err_t netif_loopif_init(struct netif *netif);
{.emit: """
// --- PING IMPLEMENTATION ---
static struct raw_pcb *ping_pcb;
static u16_t ping_seq_num;
const char* lwip_strerr(err_t err) { return "LwIP Error"; }
// --- PING IMPLEMENTATION (Phase 36c) ---
static struct raw_pcb *ping_pcb;
static u16_t ping_seq_num;
static u8_t ping_recv(void *arg, struct raw_pcb *pcb, struct pbuf *p, const ip_addr_t *addr) {
LWIP_UNUSED_ARG(arg);
LWIP_UNUSED_ARG(pcb);
@ -66,7 +72,7 @@ static u8_t ping_recv(void *arg, struct raw_pcb *pcb, struct pbuf *p, const ip_a
return 1; // Eat the packet
}
static void ping_send(const ip_addr_t *addr) {
void ping_send(const ip_addr_t *addr) {
if (!ping_pcb) {
ping_pcb = raw_new(IP_PROTO_ICMP);
if (ping_pcb) {
@ -96,12 +102,7 @@ static void ping_send(const ip_addr_t *addr) {
}
""".}
proc lwip_init*() {.importc: "lwip_init", cdecl.}
proc etharp_tmr*() {.importc: "etharp_tmr", cdecl.}
proc tcp_tmr*() {.importc: "tcp_tmr", cdecl.}
proc dhcp_fine_tmr() {.importc: "dhcp_fine_tmr", cdecl.}
proc dhcp_coarse_tmr() {.importc: "dhcp_coarse_tmr", cdecl.}
proc sys_now*(): uint32 {.importc: "sys_now", cdecl.}
# ... (Types and ION hooks) ...
type
SockState* = enum
@ -124,24 +125,27 @@ proc ion_linkoutput(netif: pointer, p: pointer): int32 {.exportc, cdecl.} =
return -1 # ERR_MEM
# Copy pbuf chain into a single ION slab
var offset = 0
# LwIP provides complete Ethernet frames (14-byte header + payload)
# VirtIO-net requires 12-byte header at start of buffer (Modern with MRG_RXBUF)
var offset = 12 # Start after VirtIO header space
{.emit: """
struct pbuf *curr = (struct pbuf *)`p`;
while (curr != NULL) {
if (`offset` + curr->len > 2000) break;
// DEBUG: Verify payload
// unsigned char* pl = (unsigned char*)curr->payload;
// glue_print(" Payload Byte: ");
// glue_print_hex((uint64_t)pl[0]);
memcpy((void*)((uintptr_t)`pkt`.data + `offset` + 12), curr->payload, curr->len);
// Copy Ethernet frame directly (includes header)
memcpy((void*)((uintptr_t)`pkt`.data + `offset`), curr->payload, curr->len);
`offset` += curr->len;
curr = curr->next;
}
""".}
pkt.len = uint16(offset) + 12
# Zero out VirtIO-net header (first 12 bytes - Modern with MRG_RXBUF)
{.emit: """
memset((void*)`pkt`.data, 0, 12);
""".}
pkt.len = uint16(offset) # Total: 12 (VirtIO) + Ethernet frame
if not ion_net_tx(pkt):
ion_user_free(pkt)
@ -174,13 +178,19 @@ proc ion_netif_init(netif: pointer): int32 {.exportc, cdecl.} =
# --- Membrane Globals ---
var g_netif: pointer
var last_tcp_tmr, last_arp_tmr, last_dhcp_fine, last_dhcp_coarse: uint32
var last_tcp_tmr, last_arp_tmr, last_dhcp_fine, last_dhcp_coarse, last_dns_tmr: uint32
var membrane_started = false
proc membrane_init*() {.exportc, cdecl.} =
if membrane_started: return
membrane_started = true
let now = sys_now()
last_tcp_tmr = now
last_arp_tmr = now
last_dhcp_fine = now
last_dhcp_coarse = now
last_dns_tmr = now
glue_print("[Membrane] Initialization...\n")
ion_user_init()
@ -188,9 +198,16 @@ proc membrane_init*() {.exportc, cdecl.} =
# 1. LwIP Stack Init
glue_print("[Membrane] Calling lwip_init()...\n")
lwip_init()
glue_print("[Membrane] lwip_init() returned.\n")
{.emit: "printf(\"[Membrane] LwIP Byte Order: %d (LE=%d, BE=%d)\\n\", BYTE_ORDER, LITTLE_ENDIAN, BIG_ENDIAN);".}
{.emit: "lwip_platform_diag(\"[Membrane] DIAG TEST: %s\\n\", \"OK\");".}
dns_init() # Initialize DNS resolver
# Set Fallback DNS (8.8.8.8)
{.emit: """
static ip_addr_t dns_server;
IP4_ADDR(ip_2_ip4(&dns_server), 8, 8, 8, 8);
dns_setserver(0, &dns_server);
""".}
glue_print("[Membrane] lwip_init() returned. DNS Initialized.\n")
# 2. Setup Netif
{.emit: """
@ -213,8 +230,11 @@ proc membrane_init*() {.exportc, cdecl.} =
glue_print("[Membrane] Network Stack Operational (Waiting for DHCP IP...)\n")
proc glue_get_ip*(): uint32 {.exportc, cdecl.} =
## Returns current IP address in host byte order
{.emit: "return ip4_addr_get_u32(netif_ip4_addr((struct netif *)`g_netif`));".}
var last_notified_ip: uint32 = 0
var dhcp_retried = false
var last_ping_time: uint32 = 0
proc glue_print_hex(v: uint64) =
@ -230,7 +250,6 @@ proc glue_print_hex(v: uint64) =
proc pump_membrane_stack*() {.exportc, cdecl.} =
## The Pulse of the Membrane. Call frequently to handle timers and RX.
# glue_print("[Membrane] Pump\n")
let now = sys_now()
@ -242,15 +261,16 @@ proc pump_membrane_stack*() {.exportc, cdecl.} =
glue_print_hex(uint64(ip_addr))
glue_print("\n")
last_notified_ip = ip_addr
# Force DHCP Retry if no IP after 3 seconds
if now > 3000 and not dhcp_retried and ip_addr == 0:
dhcp_retried = true
glue_print("[Membrane] Forcing DHCP Restart...\n")
{.emit: "dhcp_start((struct netif *)`g_netif`);".}
# 1. LwIP Timers (Raw API needs manual polling)
{.emit: """
static int debug_tick = 0;
if (debug_tick++ % 200 == 0) {
printf("[Membrane] sys_now: %u\n", `now`);
}
""".}
if now - last_tcp_tmr >= 250:
tcp_tmr()
last_tcp_tmr = now
@ -258,6 +278,20 @@ proc pump_membrane_stack*() {.exportc, cdecl.} =
etharp_tmr()
last_arp_tmr = now
# DHCP Timers
if now - last_dhcp_fine >= 500:
dhcp_fine_tmr()
last_dhcp_fine = now
if now - last_dhcp_coarse >= 60000:
glue_print("[Membrane] DHCP Coarse Timer\n")
dhcp_coarse_tmr()
last_dhcp_coarse = now
# DNS Timer (every 1000ms)
if now - last_dns_tmr >= 1000:
dns_tmr()
last_dns_tmr = now
# Phase 37a: ICMP Ping Verification
if now - last_ping_time > 1000:
last_ping_time = now
@ -269,18 +303,7 @@ proc pump_membrane_stack*() {.exportc, cdecl.} =
IP4_ADDR(&gateway, 10, 0, 2, 2);
ping_send(&gateway);
""".}
# DHCP Timers
if now - last_dhcp_fine >= 500:
# glue_print("[Membrane] DHCP Fine Timer\n")
dhcp_fine_tmr()
last_dhcp_fine = now
if now - last_dhcp_coarse >= 60000:
glue_print("[Membrane] DHCP Coarse Timer\n")
dhcp_coarse_tmr()
last_dhcp_coarse = now
# 2. RX Ingress
var pkt: IonPacket
# glue_print("[Membrane] Exit Pump\n")
@ -303,13 +326,13 @@ proc pump_membrane_stack*() {.exportc, cdecl.} =
printf("[Membrane] ERROR: Ingress pkt.data is NULL!\n");
pbuf_free(p);
} else {
// OFFSET FIX: Kernel already applied VirtIO offset (12 bytes) to pkt.data
pbuf_take(p, (void*)((uintptr_t)`pkt`.data), `pkt`.len);
if (netif_default->input(p, netif_default) != ERR_OK) {
pbuf_free(p);
}
}
} else {
printf("[Membrane] CRITICAL: pbuf_alloc FAILED! (POOL OOM?)\n");
}
""".}
ion_user_free(pkt)
@ -511,3 +534,59 @@ proc glue_close*(sock: ptr NexusSock): int {.exportc, cdecl.} =
}
""".}
return 0
# --- DNS GLUE (C Implementation) ---
{.emit: """
static ip_addr_t g_dns_ip;
static int g_dns_status = 0; // 0=idle, 1=pending, 2=done, -1=error
static void my_dns_callback(const char *name, const ip_addr_t *ipaddr, void *callback_arg) {
if (ipaddr != NULL) {
g_dns_ip = *ipaddr;
g_dns_status = 2; // Success
} else {
g_dns_status = -1; // Error
}
}
int glue_resolve_start(char* hostname) {
ip_addr_t ip;
err_t err;
g_dns_status = 1; // Pending default
// Ensure we have a DNS server
const ip_addr_t *ns = dns_getserver(0);
if (ns == NULL || ip_addr_isany(ns)) {
printf("[Membrane] No DNS server available. Using fallback 8.8.8.8\n");
static ip_addr_t fallback;
IP_ADDR4(&fallback, 8, 8, 8, 8);
dns_setserver(0, &fallback);
ns = dns_getserver(0);
}
printf("[Membrane] Resolving '%s' via DNS: %s\n", hostname, ipaddr_ntoa(ns));
err = dns_gethostbyname(hostname, &ip, my_dns_callback, NULL);
if (err == ERR_OK) {
g_dns_ip = ip;
g_dns_status = 2; // Done
return 0;
} else if (err == ERR_INPROGRESS) {
return 1;
} else {
printf("[Membrane] dns_gethostbyname FAILED with error: %d\n", (int)err);
g_dns_status = -1;
return -1;
}
}
int glue_resolve_check(u32_t *ip_out) {
if (g_dns_status == 1) return 1;
if (g_dns_status == 2) {
*ip_out = ip4_addr_get_u32(&g_dns_ip);
return 0;
}
return -1;
}
""".}