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