495 lines
13 KiB
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");
|
|
}
|
|
}
|