//! RFC-0015: Transport Skins Interface //! //! Pluggable censorship-resistant transport layer. //! Each skin wraps LWF frames to mimic benign traffic patterns. const std = @import("std"); const png = @import("png.zig"); /// Transport skin interface /// All skins implement this common API pub const TransportSkin = union(enum) { raw: RawSkin, mimic_https: MimicHttpsSkin, // mimic_dns: MimicDnsSkin, // mimic_video: MimicVideoSkin, // stego_image: StegoImageSkin, const Self = @This(); /// Initialize skin from configuration pub fn init(config: SkinConfig) !Self { return switch (config.skin_type) { .Raw => Self{ .raw = try RawSkin.init(config) }, .MimicHttps => Self{ .mimic_https = try MimicHttpsSkin.init(config) }, // .MimicDns => ... // .MimicVideo => ... // .StegoImage => ... }; } /// Cleanup skin resources pub fn deinit(self: *Self) void { switch (self.*) { .raw => |*skin| skin.deinit(), .mimic_https => |*skin| skin.deinit(), } } /// Wrap LWF frame for transmission /// Returns owned slice (caller must free) pub fn wrap(self: *Self, allocator: std.mem.Allocator, lwf_frame: []const u8) ![]u8 { return switch (self.*) { .raw => |*skin| skin.wrap(allocator, lwf_frame), .mimic_https => |*skin| skin.wrap(allocator, lwf_frame), }; } /// Unwrap received data to extract LWF frame /// Returns owned slice (caller must free) pub fn unwrap(self: *Self, allocator: std.mem.Allocator, wire_data: []const u8) !?[]u8 { return switch (self.*) { .raw => |*skin| skin.unwrap(allocator, wire_data), .mimic_https => |*skin| skin.unwrap(allocator, wire_data), }; } /// Get skin name for logging/debugging pub fn name(self: Self) []const u8 { return switch (self) { .raw => "RAW", .mimic_https => "MIMIC_HTTPS", // .mimic_dns => "MIMIC_DNS", // .mimic_video => "MIMIC_VIDEO", // .stego_image => "STEGO_IMAGE", }; } /// Get bandwidth overhead estimate (0.0 = 0%, 1.0 = 100%) pub fn overheadEstimate(self: Self) f64 { return switch (self) { .raw => 0.0, .mimic_https => 0.05, // ~5% TLS + WS overhead // .mimic_dns => 2.0, // ~200% encoding overhead // .mimic_video => 0.10, // ~10% container overhead // .stego_image => 10.0, // ~1000% overhead }; } }; /// Skin configuration pub const SkinConfig = struct { skin_type: SkinType, allocator: std.mem.Allocator, // For MIMIC_HTTPS cover_domain: ?[]const u8 = null, // SNI domain real_endpoint: ?[]const u8 = null, // Actual relay ws_path: ?[]const u8 = null, // WebSocket path // For PNG (all skins) png_state: ?png.PngState = null, pub const SkinType = enum { Raw, MimicHttps, // MimicDns, // MimicVideo, // StegoImage, }; }; // ============================================================================ // Skin 0: RAW (Unrestricted Networks) // ============================================================================ pub const RawSkin = struct { allocator: std.mem.Allocator, const Self = @This(); pub fn init(config: SkinConfig) !Self { return Self{ .allocator = config.allocator, }; } pub fn deinit(_: *Self) void { // No cleanup needed for RawSkin } /// Raw: No wrapping, just copy pub fn wrap(_: *Self, allocator: std.mem.Allocator, lwf_frame: []const u8) ![]u8 { return try allocator.dupe(u8, lwf_frame); } /// Raw: No unwrapping, just copy pub fn unwrap(_: *Self, allocator: std.mem.Allocator, wire_data: []const u8) !?[]u8 { return try allocator.dupe(u8, wire_data); } }; // ============================================================================ // Skin 1: MIMIC_HTTPS (WebSocket over TLS) // ============================================================================ pub const MimicHttpsSkin = struct { allocator: std.mem.Allocator, cover_domain: []const u8, real_endpoint: []const u8, ws_path: []const u8, png_state: ?png.PngState, /// WebSocket frame types const WsOpcode = enum(u4) { Continuation = 0x0, Text = 0x1, Binary = 0x2, Close = 0x8, Ping = 0x9, Pong = 0xA, }; const Self = @This(); pub fn init(config: SkinConfig) !Self { return Self{ .allocator = config.allocator, .cover_domain = config.cover_domain orelse "cdn.cloudflare.com", .real_endpoint = config.real_endpoint orelse "relay.libertaria.network", .ws_path = config.ws_path orelse "/api/v1/stream", .png_state = config.png_state, }; } pub fn deinit(_: *Self) void { // No cleanup needed for MimicHttpsSkin } /// Wrap LWF frame in WebSocket binary frame with PNG padding pub fn wrap(self: *Self, allocator: std.mem.Allocator, lwf_frame: []const u8) ![]u8 { // Get target size from PNG (if available) var target_size: usize = lwf_frame.len; var padding_len: usize = 0; if (self.png_state) |*png_state| { target_size = png_state.samplePacketSize(); if (target_size > lwf_frame.len + 14) { // 14 = WebSocket header max padding_len = target_size - lwf_frame.len - 14; } png_state.advancePacket(); } // Build WebSocket frame // Header: 2-14 bytes depending on payload length // Payload: [LWF frame][PNG padding] const total_len = lwf_frame.len + padding_len; const frame_size = self.calculateWsFrameSize(total_len); var frame = try allocator.alloc(u8, frame_size); errdefer allocator.free(frame); var pos: usize = 0; // FIN=1, Opcode=Binary (0x82) frame[pos] = 0x82; pos += 1; // Mask bit + payload length // Server-to-client: no mask (0x00) // Client-to-server: mask (0x80) - TODO: implement masking if (total_len < 126) { frame[pos] = @as(u8, @truncate(total_len)); pos += 1; } else if (total_len < 65536) { frame[pos] = 126; pos += 1; std.mem.writeInt(u16, frame[pos..][0..2], @as(u16, @truncate(total_len)), .big); pos += 2; } else { frame[pos] = 127; pos += 1; std.mem.writeInt(u64, frame[pos..][0..8], total_len, .big); pos += 8; } // Payload: LWF frame + padding @memcpy(frame[pos..][0..lwf_frame.len], lwf_frame); pos += lwf_frame.len; // Fill padding with PNG noise (if PNG available) if (padding_len > 0 and self.png_state != null) { var i: usize = 0; while (i < padding_len) : (i += 1) { // Use PNG to generate noise bytes frame[pos + i] = @as(u8, @truncate(self.png_state.?.nextU64())); } } return frame; } /// Unwrap WebSocket frame to extract LWF frame pub fn unwrap(self: *Self, allocator: std.mem.Allocator, wire_data: []const u8) !?[]u8 { if (wire_data.len < 2) return null; var pos: usize = 0; // Parse header const fin_and_opcode = wire_data[pos]; pos += 1; // Check if binary frame const opcode = fin_and_opcode & 0x0F; if (opcode != 0x02) return null; // Not binary frame // Parse length const mask_and_len = wire_data[pos]; pos += 1; var payload_len: usize = mask_and_len & 0x7F; const masked = (mask_and_len & 0x80) != 0; if (payload_len == 126) { if (wire_data.len < pos + 2) return null; payload_len = std.mem.readInt(u16, wire_data[pos..][0..2], .big); pos += 2; } else if (payload_len == 127) { if (wire_data.len < pos + 8) return null; payload_len = std.mem.readInt(u64, wire_data[pos..][0..8], .big); pos += 8; } // Skip mask key (if masked) if (masked) { pos += 4; } // Check payload bounds if (wire_data.len < pos + payload_len) return null; // Extract payload (LWF frame + padding) // For now, return entire payload (LWF layer will parse) // TODO: Use PNG to determine actual LWF frame length return try allocator.dupe(u8, wire_data[pos..][0..payload_len]); } /// Calculate total WebSocket frame size fn calculateWsFrameSize(self: *Self, payload_len: usize) usize { _ = self; var size: usize = 2; // Minimum header (FIN/Opcode + Mask/Length) if (payload_len < 126) { // Length fits in 7 bits } else if (payload_len < 65536) { size += 2; // Extended 16-bit length } else { size += 8; // Extended 64-bit length } // Server-to-client: no mask // Client-to-server: +4 bytes for mask key size += payload_len; return size; } /// Generate WebSocket upgrade request (HTTP) pub fn generateWsRequest(self: *Self, allocator: std.mem.Allocator, sec_websocket_key: []const u8) ![]u8 { return try std.fmt.allocPrint(allocator, "GET {s} HTTP/1.1\r\n" ++ "Host: {s}\r\n" ++ "Upgrade: websocket\r\n" ++ "Connection: Upgrade\r\n" ++ "Sec-WebSocket-Key: {s}\r\n" ++ "Sec-WebSocket-Version: 13\r\n" ++ "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\r\n" ++ "\r\n", .{ self.ws_path, self.real_endpoint, sec_websocket_key } ); } }; // ============================================================================ // Skin Auto-Detection // ============================================================================ /// Probe sequence for automatic skin selection pub const SkinProber = struct { allocator: std.mem.Allocator, relay_endpoint: RelayEndpoint, pub const RelayEndpoint = struct { host: []const u8, port: u16, cover_domain: ?[]const u8 = null, }; pub fn init(allocator: std.mem.Allocator, endpoint: RelayEndpoint) SkinProber { return .{ .allocator = allocator, .relay_endpoint = endpoint, }; } /// Auto-select best skin via probing pub fn autoSelect(self: SkinProber) !TransportSkin { // 1. Try RAW UDP (100ms timeout) if (try self.probeRaw(100)) { return TransportSkin.init(.{ .skin_type = .Raw, .allocator = self.allocator, }); } // 2. Try HTTPS WebSocket (500ms timeout) if (try self.probeHttps(500)) { return TransportSkin.init(.{ .skin_type = .MimicHttps, .allocator = self.allocator, .cover_domain = self.relay_endpoint.cover_domain, .real_endpoint = self.relay_endpoint.host, }); } // 3. Fallback to HTTPS anyway (most reliable) return TransportSkin.init(.{ .skin_type = .MimicHttps, .allocator = self.allocator, .cover_domain = self.relay_endpoint.cover_domain, .real_endpoint = self.relay_endpoint.host, }); } fn probeRaw(self: SkinProber, timeout_ms: u32) !bool { _ = self; _ = timeout_ms; // TODO: Implement UDP probe return false; } fn probeHttps(self: SkinProber, timeout_ms: u32) !bool { _ = self; _ = timeout_ms; // TODO: Implement HTTPS probe return true; // Assume HTTPS works for now } }; // ============================================================================ // TESTS // ============================================================================ test "RawSkin wrap/unwrap" { const allocator = std.testing.allocator; var skin = try RawSkin.init(.{ .skin_type = .Raw, .allocator = allocator, }); defer skin.deinit(); const lwf = "Hello LWF"; const wrapped = try skin.wrap(allocator, lwf); defer allocator.free(wrapped); const unwrapped = try skin.unwrap(allocator, wrapped); defer allocator.free(unwrapped.?); try std.testing.expectEqualStrings(lwf, unwrapped.?); } test "MimicHttpsSkin WebSocket frame structure" { const allocator = std.testing.allocator; var skin = try MimicHttpsSkin.init(.{ .skin_type = .MimicHttps, .allocator = allocator, .cover_domain = "cdn.example.com", .real_endpoint = "relay.example.com", .ws_path = "/stream", }); defer skin.deinit(); const lwf = [_]u8{0x4C, 0x57, 0x46, 0x00}; // "LWF\0" const wrapped = try skin.wrap(allocator, &lwf); defer allocator.free(wrapped); // Check WebSocket frame header try std.testing.expectEqual(@as(u8, 0x82), wrapped[0]); // FIN=1, Binary try std.testing.expect(wrapped.len >= 2 + lwf.len); // Verify unwrap returns payload const unwrapped = try skin.unwrap(allocator, wrapped); defer allocator.free(unwrapped.?); try std.testing.expectEqualSlices(u8, &lwf, unwrapped.?[0..lwf.len]); } test "TransportSkin union dispatch" { const allocator = std.testing.allocator; var skin = try TransportSkin.init(.{ .skin_type = .Raw, .allocator = allocator, }); defer skin.deinit(); const lwf = "Test"; const wrapped = try skin.wrap(allocator, lwf); defer allocator.free(wrapped); try std.testing.expectEqualStrings("RAW", skin.name()); try std.testing.expectEqual(@as(f64, 0.0), skin.overheadEstimate()); }