// SPDX-License-Identifier: LSL-1.0 // Copyright (c) 2026 Markus Maiwald // Stewardship: Self Sovereign Society Foundation // // This file is part of the Nexus Sovereign Core. // See legal/LICENSE_SOVEREIGN.md for license terms. //! Rumpk NPL: Live Wire (ARP/ICMP Responder) //! //! A simple network stack implementation for NPL fibers. //! Responds to ARP requests and ICMP echo requests (pings). //! //! SAFETY: Directly manipulates Ethernet/IP/ICMP packet headers in IonSlabs. const std = @import("std"); // 1. The SysTable Contract (Must match Kernel!) const ION_BASE = 0x83000000; const IonPacket = extern struct { data: u64, // Virtual Addr (ptr) phys: u64, // Physical Addr len: u16, id: u16, }; const CmdPacket = extern struct { kind: u32, arg: u32, }; const RingBufferPacket = extern struct { head: u32, tail: u32, mask: u32, data: [256]IonPacket, }; const RingBufferCmd = extern struct { head: u32, tail: u32, mask: u32, data: [256]CmdPacket, }; const SysTable = extern struct { magic: u32, reserved: u32, s_rx: *RingBufferPacket, s_tx: *RingBufferPacket, s_event: *RingBufferPacket, s_cmd: *RingBufferCmd, s_input: *RingBufferPacket, // Function Pointers (8 * 8 bytes) fn_vfs_open: u64, fn_vfs_read: u64, fn_vfs_list: u64, fn_vfs_write: u64, fn_vfs_close: u64, fn_log: u64, fn_pledge: u64, // Framebuffer & Yield (32 bytes) fb_addr: u64, fb_width: u32, fb_height: u32, fb_stride: u32, fb_bpp: u32, fn_yield: u64, // Crypto (16 bytes) fn_siphash: u64, fn_ed25519_verify: u64, // Phase 36.2: Network Membrane (The Veins) s_net_rx: *RingBufferPacket, s_net_tx: *RingBufferPacket, // Phase 36.3: Shared ION (16 bytes) fn_ion_alloc: u64, fn_ion_free: u64, }; fn get_systable() *SysTable { return @ptrFromInt(ION_BASE); } // Minimal Shims extern fn write(fd: c_int, buf: [*]const u8, count: usize) isize; const YIELD_LOC = 0x83000FF0; fn fiber_yield() void { const ptr: *const *const fn () callconv(.c) void = @ptrFromInt(YIELD_LOC); const func = ptr.*; func(); } fn print(text: []const u8) void { _ = write(1, text.ptr, text.len); } fn print_u64(val: u64) void { // SAFETY(Ping): Buffer populated by `std.fmt.bufPrint` before use. var buf: [32]u8 = undefined; const s = std.fmt.bufPrint(&buf, "{}", .{val}) catch "ERR"; print(s); } fn print_hex(val: u16) void { // SAFETY(Ping): Buffer populated by `std.fmt.bufPrint` before use. var buf: [16]u8 = undefined; const s = std.fmt.bufPrint(&buf, "{x}", .{val}) catch "ERR"; print(s); } // Network Structures // Network Structures (Unrolled for Packed Compat) // Network Structures (Unrolled for Packed Compat) const EthHeader = packed struct { dst0: u8, dst1: u8, dst2: u8, dst3: u8, dst4: u8, dst5: u8, src0: u8, src1: u8, src2: u8, src3: u8, src4: u8, src5: u8, type: u16, }; const ArpHeader = packed struct { hw_type: u16, proto_type: u16, hw_len: u8, proto_len: u8, opcode: u16, smac0: u8, smac1: u8, smac2: u8, smac3: u8, smac4: u8, smac5: u8, sip0: u8, sip1: u8, sip2: u8, sip3: u8, tmac0: u8, tmac1: u8, tmac2: u8, tmac3: u8, tmac4: u8, tmac5: u8, tip0: u8, tip1: u8, tip2: u8, tip3: u8, }; const IpHeader = packed struct { ver_ihl: u8, tos: u8, len: u16, id: u16, frag: u16, ttl: u8, proto: u8, csum: u16, src0: u8, src1: u8, src2: u8, src3: u8, dst0: u8, dst1: u8, dst2: u8, dst3: u8, }; const IcmpHeader = packed struct { type: u8, code: u8, csum: u16, id: u16, seq: u16, }; // Utils: Network Byte Order (Big Endian) inline fn ntohs(n: u16) u16 { return @byteSwap(n); } inline fn htons(n: u16) u16 { return @byteSwap(n); } // My Config // 52:54:00:12:34:56 (QEMU Default usually) -> No, we set that for host. // Let's assume we are 52:54:00:12:34:57 (Guest). const MY_MAC = [6]u8{ 0x52, 0x54, 0x00, 0x12, 0x34, 0x57 }; const MY_IP = [4]u8{ 10, 0, 2, 15 }; fn checksum(buf: []u8) u16 { var sum: u32 = 0; var i: usize = 0; while (i < buf.len - 1) : (i += 2) { const word = @as(u16, buf[i]) << 8 | @as(u16, buf[i + 1]); // Big Endian Checksum sum += word; } if (buf.len % 2 == 1) { sum += @as(u16, buf[i]) << 8; } while ((sum >> 16) != 0) { sum = (sum & 0xFFFF) + (sum >> 16); } return ~@as(u16, @truncate(sum)); } export fn main() c_int { const sys = get_systable(); print("[LIVE] Waiting for Packets (ARP/ICMP)...\n"); // Verify SysTable Magic if (sys.magic != 0x4E585553) { print("[LIVE] PANIC: SysTable Magic Mismatch!\n"); return 1; } const rx_ring = sys.s_net_rx; const tx_ring = sys.s_net_tx; while (true) { // 1. Poll RX while (true) { const head = @atomicLoad(u32, &rx_ring.head, .monotonic); const tail = @atomicLoad(u32, &rx_ring.tail, .monotonic); if (head != tail) { // We have a packet! const pkt = rx_ring.data[tail & rx_ring.mask]; print("[LIVE] RX Packet Len: "); print_u64(pkt.len); print("\n"); // Process it handle_packet(pkt, tx_ring); // Consume @atomicStore(u32, &rx_ring.tail, tail + 1, .release); // Don't break, try to drain more if available } else { break; // RX Empty } } // 2. Yield fiber_yield(); } return 0; } fn handle_packet(pkt: IonPacket, tx_ring: *RingBufferPacket) void { // NOTE: VirtIO Header already stripped by Kernel ion_ingress (Phase 36.2) const data_ptr: [*]u8 = @ptrFromInt(pkt.data); const data: []u8 = data_ptr[0..pkt.len]; if (data.len < @sizeOf(EthHeader)) return; const eth = @as(*align(1) EthHeader, @ptrCast(data.ptr)); const eth_type = ntohs(eth.type); print("[LIVE] EthType: 0x"); print_hex(eth_type); print("\n"); if (eth_type == 0x0806) { // ARP handle_arp(data, eth, tx_ring, pkt); } else if (eth_type == 0x0800) { // IPv4 handle_ipv4(data, eth, tx_ring, pkt); } } fn handle_arp(data: []u8, eth: *align(1) EthHeader, tx_ring: *RingBufferPacket, pkt: IonPacket) void { // Ethernet Header = 14 bytes, ARP Header = 28 bytes, Total = 42 bytes const ETH_HLEN: usize = 14; const ARP_HLEN: usize = 28; const required_len = ETH_HLEN + ARP_HLEN; print("[LIVE] ARP Check: data.len="); print_u64(data.len); print(" required="); print_u64(required_len); print("\n"); if (data.len < required_len) return; const arp = @as(*align(1) ArpHeader, @ptrCast(data.ptr + ETH_HLEN)); // Check if Request (1) and Target IP Matches // We unroll the check manually or use bytes const opcode_val = ntohs(arp.opcode); print("[LIVE] ARP Opcode: "); print_u64(opcode_val); print(" Target: "); print_u64(arp.tip0); print("."); print_u64(arp.tip1); print("."); print_u64(arp.tip2); print("."); print_u64(arp.tip3); print("\n"); // Just check manually if (opcode_val == 1 and arp.tip0 == MY_IP[0] and arp.tip1 == MY_IP[1] and arp.tip2 == MY_IP[2] and arp.tip3 == MY_IP[3]) { print("[LIVE] ARP Request for ME! Replying...\n"); // Craft Reply IN PLACE // 1. Eth Header eth.dst0 = eth.src0; eth.dst1 = eth.src1; eth.dst2 = eth.src2; eth.dst3 = eth.src3; eth.dst4 = eth.src4; eth.dst5 = eth.src5; eth.src0 = MY_MAC[0]; eth.src1 = MY_MAC[1]; eth.src2 = MY_MAC[2]; eth.src3 = MY_MAC[3]; eth.src4 = MY_MAC[4]; eth.src5 = MY_MAC[5]; // 2. ARP Header arp.opcode = htons(2); // Reply // Target = Sender (Swap) arp.tmac0 = arp.smac0; arp.tmac1 = arp.smac1; arp.tmac2 = arp.smac2; arp.tmac3 = arp.smac3; arp.tmac4 = arp.smac4; arp.tmac5 = arp.smac5; arp.tip0 = arp.sip0; arp.tip1 = arp.sip1; arp.tip2 = arp.sip2; arp.tip3 = arp.sip3; // Sender = Me arp.smac0 = MY_MAC[0]; arp.smac1 = MY_MAC[1]; arp.smac2 = MY_MAC[2]; arp.smac3 = MY_MAC[3]; arp.smac4 = MY_MAC[4]; arp.smac5 = MY_MAC[5]; arp.sip0 = MY_IP[0]; arp.sip1 = MY_IP[1]; arp.sip2 = MY_IP[2]; arp.sip3 = MY_IP[3]; // 3. Send - Create TX packet pointing to Ethernet frame (not VirtIO header) // pkt.data points to slab start (VirtIO header at offset 0) // data.ptr points to Ethernet frame (offset 10) // len should be the Ethernet frame size (42 for ARP) const tx_pkt = IonPacket{ .data = @intFromPtr(data.ptr), // Points to Ethernet frame .phys = 0, .len = 42, // Ethernet frame length for ARP .id = pkt.id, }; send_packet(tx_ring, tx_pkt); } } fn handle_ipv4(data: []u8, eth: *align(1) EthHeader, tx_ring: *RingBufferPacket, pkt: IonPacket) void { const ETH_HLEN: usize = 14; const IP_HLEN: usize = 20; if (data.len < ETH_HLEN + IP_HLEN) return; const ip = @as(*align(1) IpHeader, @ptrCast(data.ptr + ETH_HLEN)); print("[LIVE] IP Dst: "); print_u64(ip.dst0); print("."); print_u64(ip.dst1); print("."); print_u64(ip.dst2); print("."); print_u64(ip.dst3); print(" Proto: "); print_u64(ip.proto); print("\n"); // Check if destined for us if (ip.dst0 != MY_IP[0] or ip.dst1 != MY_IP[1] or ip.dst2 != MY_IP[2] or ip.dst3 != MY_IP[3]) return; if (ip.proto == 1) { // ICMP const ip_header_len = (ip.ver_ihl & 0x0F) * 4; const icmp_offset = ETH_HLEN + ip_header_len; if (data.len < icmp_offset + @sizeOf(IcmpHeader)) return; const icmp = @as(*align(1) IcmpHeader, @ptrCast(data.ptr + icmp_offset)); print("[LIVE] ICMP Type: "); print_u64(icmp.type); print("\n"); if (icmp.type == 8) { // Echo Request print("[LIVE] Ping! Ponging...\n"); // 1. Eth Header eth.dst0 = eth.src0; eth.dst1 = eth.src1; eth.dst2 = eth.src2; eth.dst3 = eth.src3; eth.dst4 = eth.src4; eth.dst5 = eth.src5; eth.src0 = MY_MAC[0]; eth.src1 = MY_MAC[1]; eth.src2 = MY_MAC[2]; eth.src3 = MY_MAC[3]; eth.src4 = MY_MAC[4]; eth.src5 = MY_MAC[5]; // 2. IP Header ip.dst0 = ip.src0; ip.dst1 = ip.src1; ip.dst2 = ip.src2; ip.dst3 = ip.src3; ip.src0 = MY_IP[0]; ip.src1 = MY_IP[1]; ip.src2 = MY_IP[2]; ip.src3 = MY_IP[3]; // Recalculate IP Checksum? Checksum spans header only. // Since we only swapped src/dst (symetric), simple sum *might* be same, but carry could differ. // Safe way: recompute. ip.csum = 0; // ip.csum = htons(checksum(data[@sizeOf(EthHeader) .. @sizeOf(EthHeader) + ip_header_len])); // Actually, many drivers offload csum, but we are Sovereign. // For now, let's assume valid csum or linux accepts 0 in some cases? No. // Let's do a proper csum. // Wait, "checksum" func above expects Network Byte Order? // "The word at buf[i] << 8" implies buf[i] is High Byte. // Yes, standard Internet Checksum algorithm works on byte stream. // 3. ICMP Header icmp.type = 0; // Echo Reply // Checksum update: // Delta update: Old type (800) -> New Type (000). Delta = -8. // New Csum = Old Csum + 8. (One's complement math is tricky). // Recompute is safer. icmp.csum = 0; const icmp_len = ntohs(ip.len) - ip_header_len; icmp.csum = htons(checksum(data[icmp_offset .. icmp_offset + icmp_len])); // 4. Send send_packet(tx_ring, pkt); } } } fn send_packet(tx_ring: *RingBufferPacket, pkt: IonPacket) void { // Atomic Push to TX const head = @atomicLoad(u32, &tx_ring.head, .monotonic); const tail = @atomicLoad(u32, &tx_ring.tail, .monotonic); const mask = tx_ring.mask; const next = (head + 1) & mask; if (next != tail) { tx_ring.data[head & mask] = pkt; @atomicStore(u32, &tx_ring.head, next, .release); print("[LIVE] TX Pushed! Len="); print_u64(pkt.len); print("\n"); } else { print("[LIVE] TX Full! Dropping.\n"); } }