rumpk/npl/ping_ion.zig

495 lines
13 KiB
Zig

// 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");
}
}