301 lines
9.6 KiB
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);
|
|
}
|