rumpk/libs/libertaria/lwf_membrane.zig

301 lines
9.6 KiB
Zig

// SPDX-License-Identifier: LCL-1.0
// Copyright (c) 2026 Markus Maiwald
// Stewardship: Self Sovereign Society Foundation
//
// This file is part of the Nexus Commonwealth.
// See legal/LICENSE_COMMONWEALTH.md for license terms.
//! Project LibWeb: LWF Membrane Client
//!
//! Userland-side LWF frame handler. Runs in the Membrane where std is
//! available. Consumes validated LWF frames from the dedicated ION
//! channel (s_lwf_rx) and produces encrypted outbound frames (s_lwf_tx).
//!
//! This module bridges:
//! - ION ring (SysTable s_lwf_rx/s_lwf_tx) for zero-copy kernel IPC
//! - Upstream LWF codec (libertaria-stack lwf.zig) for full encode/decode
//! - Noise Protocol for transport encryption/decryption
//!
//! Architecture:
//! VirtIO-net → NetSwitch (validates header) → chan_lwf_rx → [this module]
//! [this module] → chan_lwf_tx → NetSwitch → VirtIO-net
//!
//! NOTE: This file is NOT compiled freestanding. It targets the Membrane
//! (userland) and has access to std.mem.Allocator.
const std = @import("std");
// =========================================================
// ION Slab Constants (must match ion/memory.nim)
// =========================================================
const SLAB_SIZE: usize = 2048;
const SYSTABLE_ADDR: usize = if (builtin.cpu.arch == .aarch64) 0x50000000 else 0x83000000;
const ETH_HEADER_SIZE: usize = 14;
// =========================================================
// LWF Header Constants (duplicated from lwf_adapter.zig
// for use with std — adapter is freestanding, this is not)
// =========================================================
pub const HEADER_SIZE: usize = 88;
pub const TRAILER_SIZE: usize = 36;
pub const MIN_FRAME_SIZE: usize = HEADER_SIZE + TRAILER_SIZE;
pub const LWF_MAGIC = [4]u8{ 'L', 'W', 'F', 0 };
pub const LWF_VERSION: u8 = 0x02;
pub const Flags = struct {
pub const ENCRYPTED: u8 = 0x01;
pub const SIGNED: u8 = 0x02;
pub const RELAYABLE: u8 = 0x04;
pub const HAS_ENTROPY: u8 = 0x08;
pub const FRAGMENTED: u8 = 0x10;
pub const PRIORITY: u8 = 0x20;
};
// =========================================================
// Frame Processing Result
// =========================================================
pub const FrameError = error{
TooSmall,
InvalidMagic,
InvalidVersion,
PayloadOverflow,
DecryptionFailed,
NoSession,
SlabTooSmall,
};
pub const ProcessedFrame = struct {
service_type: u16,
payload: []const u8, // Points into slab — valid until ion_free
session_id: [16]u8,
dest_hint: [24]u8,
source_hint: [24]u8,
sequence: u32,
flags: u8,
encrypted: bool,
};
// =========================================================
// LWF Membrane Client
// =========================================================
pub const LwfClient = struct {
/// Callback type for incoming LWF frames
pub const FrameHandler = *const fn (frame: ProcessedFrame) void;
on_frame: ?FrameHandler,
pub fn init() LwfClient {
return .{
.on_frame = null,
};
}
/// Register a callback for incoming LWF frames
pub fn setHandler(self: *LwfClient, handler: FrameHandler) void {
self.on_frame = handler;
}
/// Parse an LWF frame from a raw ION slab buffer.
/// The buffer starts AFTER the Ethernet header (NetSwitch strips it
/// to EtherType, but the ION packet still contains the full Ethernet
/// frame — so caller must offset by 14 bytes).
pub fn parseFrame(data: [*]const u8, len: u16) FrameError!ProcessedFrame {
if (len < HEADER_SIZE) return error.TooSmall;
// Magic check
if (data[0] != 'L' or data[1] != 'W' or data[2] != 'F' or data[3] != 0)
return error.InvalidMagic;
// Version check (offset 77)
if (data[77] != LWF_VERSION) return error.InvalidVersion;
// Parse fields
const payload_len = readU16Big(data[74..76]);
const total_needed = HEADER_SIZE + @as(usize, payload_len) + TRAILER_SIZE;
if (total_needed > len) return error.PayloadOverflow;
var frame: ProcessedFrame = undefined;
frame.service_type = readU16Big(data[72..74]);
frame.sequence = readU32Big(data[68..72]);
frame.flags = data[78];
frame.encrypted = (frame.flags & Flags.ENCRYPTED) != 0;
frame.payload = data[HEADER_SIZE .. HEADER_SIZE + payload_len];
@memcpy(&frame.dest_hint, data[4..28]);
@memcpy(&frame.source_hint, data[28..52]);
@memcpy(&frame.session_id, data[52..68]);
return frame;
}
/// Build an outbound LWF frame into a slab buffer.
/// Returns the total frame size written.
pub fn buildFrame(
buf: []u8,
service_type: u16,
payload: []const u8,
session_id: [16]u8,
dest_hint: [24]u8,
source_hint: [24]u8,
sequence: u32,
flags: u8,
) FrameError!usize {
const total = HEADER_SIZE + payload.len + TRAILER_SIZE;
if (buf.len < total) return error.SlabTooSmall;
// Zero header + trailer regions
@memset(buf[0..HEADER_SIZE], 0);
@memset(buf[HEADER_SIZE + payload.len ..][0..TRAILER_SIZE], 0);
// Magic
buf[0] = 'L';
buf[1] = 'W';
buf[2] = 'F';
buf[3] = 0;
// Dest/Source hints
@memcpy(buf[4..28], &dest_hint);
@memcpy(buf[28..52], &source_hint);
// Session ID
@memcpy(buf[52..68], &session_id);
// Sequence
writeU32Big(buf[68..72], sequence);
// Service type + payload len
writeU16Big(buf[72..74], service_type);
writeU16Big(buf[74..76], @truncate(payload.len));
// Frame class (auto-select based on total size)
buf[76] = if (total <= 128) 0x00 // micro
else if (total <= 512) 0x01 // mini
else if (total <= 1350) 0x02 // standard
else if (total <= 4096) 0x03 // big
else 0x04; // jumbo
// Version
buf[77] = LWF_VERSION;
// Flags
buf[78] = flags;
// Payload
@memcpy(buf[HEADER_SIZE..][0..payload.len], payload);
return total;
}
};
// =========================================================
// Integer Helpers
// =========================================================
fn readU16Big(bytes: *const [2]u8) u16 {
return (@as(u16, bytes[0]) << 8) | @as(u16, bytes[1]);
}
fn readU32Big(bytes: *const [4]u8) u32 {
return (@as(u32, bytes[0]) << 24) |
(@as(u32, bytes[1]) << 16) |
(@as(u32, bytes[2]) << 8) |
@as(u32, bytes[3]);
}
fn writeU16Big(buf: *[2]u8, val: u16) void {
buf[0] = @truncate(val >> 8);
buf[1] = @truncate(val);
}
fn writeU32Big(buf: *[4]u8, val: u32) void {
buf[0] = @truncate(val >> 24);
buf[1] = @truncate(val >> 16);
buf[2] = @truncate(val >> 8);
buf[3] = @truncate(val);
}
// =========================================================
// Tests
// =========================================================
test "parseFrame valid" {
var buf: [512]u8 = undefined;
const payload = "Hello Noise";
const sz = try LwfClient.buildFrame(
&buf,
0x0001, // DATA_TRANSPORT
payload,
[_]u8{0xAA} ** 16, // session
[_]u8{0xBB} ** 24, // dest
[_]u8{0xCC} ** 24, // src
42,
0,
);
const frame = try LwfClient.parseFrame(&buf, @truncate(sz));
try std.testing.expectEqual(@as(u16, 0x0001), frame.service_type);
try std.testing.expectEqual(@as(u32, 42), frame.sequence);
try std.testing.expectEqualSlices(u8, payload, frame.payload);
try std.testing.expectEqual(@as(u8, 0xAA), frame.session_id[0]);
try std.testing.expect(!frame.encrypted);
}
test "parseFrame encrypted flag" {
var buf: [512]u8 = undefined;
const sz = try LwfClient.buildFrame(
&buf,
0x0003, // IDENTITY_SIGNAL
"encrypted_payload",
[_]u8{0} ** 16,
[_]u8{0} ** 24,
[_]u8{0} ** 24,
1,
Flags.ENCRYPTED | Flags.SIGNED,
);
const frame = try LwfClient.parseFrame(&buf, @truncate(sz));
try std.testing.expect(frame.encrypted);
try std.testing.expectEqual(@as(u16, 0x0003), frame.service_type);
}
test "buildFrame auto frame class" {
var buf: [2048]u8 = undefined;
// Micro (total <= 128)
_ = try LwfClient.buildFrame(&buf, 0, "", [_]u8{0} ** 16, [_]u8{0} ** 24, [_]u8{0} ** 24, 0, 0);
try std.testing.expectEqual(@as(u8, 0x00), buf[76]); // micro
// Standard (total > 512)
var payload: [500]u8 = undefined;
@memset(&payload, 0x42);
_ = try LwfClient.buildFrame(&buf, 0, &payload, [_]u8{0} ** 16, [_]u8{0} ** 24, [_]u8{0} ** 24, 0, 0);
try std.testing.expectEqual(@as(u8, 0x02), buf[76]); // standard
}
test "parseFrame rejects bad magic" {
var buf: [512]u8 = undefined;
_ = try LwfClient.buildFrame(&buf, 0, "x", [_]u8{0} ** 16, [_]u8{0} ** 24, [_]u8{0} ** 24, 0, 0);
buf[0] = 'X';
try std.testing.expectError(error.InvalidMagic, LwfClient.parseFrame(&buf, 160));
}
test "buildFrame roundtrip preserves hints" {
var buf: [512]u8 = undefined;
const dest = [_]u8{0xDE} ** 24;
const src = [_]u8{0x5A} ** 24;
const sess = [_]u8{0xF0} ** 16;
_ = try LwfClient.buildFrame(&buf, 0x0800, "audio", sess, dest, src, 99, Flags.PRIORITY);
const frame = try LwfClient.parseFrame(&buf, 200);
try std.testing.expectEqual(@as(u16, 0x0800), frame.service_type);
try std.testing.expectEqual(@as(u32, 99), frame.sequence);
try std.testing.expectEqualSlices(u8, &dest, &frame.dest_hint);
try std.testing.expectEqualSlices(u8, &src, &frame.source_hint);
try std.testing.expectEqualSlices(u8, &sess, &frame.session_id);
}