396 lines
13 KiB
Zig
396 lines
13 KiB
Zig
//! RFC-0015: MIMIC_QUIC Skin (HTTP/3 over QUIC)
|
|
//!
|
|
//! Modern replacement for WebSockets with 0-RTT connection establishment.
|
|
//! Uses QUIC over UDP with HTTP/3 framing — looks like standard browser traffic.
|
|
//!
|
|
//! Advantages over WebSockets:
|
|
//! - 0-RTT connection resumption (no TCP handshake latency)
|
|
//! - Built-in TLS 1.3 (no separate upgrade)
|
|
//! - Connection migration (survives IP changes)
|
|
//! - Better congestion control (not stuck in TCP head-of-line blocking)
|
|
//! - Harder to block (UDP port 443, looks like HTTP/3)
|
|
//!
|
|
//! References:
|
|
//! - RFC 9000: QUIC
|
|
//! - RFC 9114: HTTP/3
|
|
//! - RFC 9293: Connection Migration
|
|
|
|
const std = @import("std");
|
|
const png = @import("png.zig");
|
|
|
|
/// QUIC Header Types
|
|
const QuicHeaderType = enum {
|
|
long, // Initial, Handshake, 0-RTT
|
|
short, // 1-RTT packets
|
|
retry, // Retry packets
|
|
version_negotiation,
|
|
};
|
|
|
|
/// QUIC Long Header (for handshake)
|
|
pub const QuicLongHeader = packed struct {
|
|
header_form: u1 = 1, // Always 1 for long header
|
|
fixed_bit: u1 = 1, // Must be 1
|
|
packet_type: u2, // Initial(0), 0-RTT(1), Handshake(2), Retry(3)
|
|
version_specific: u4, // Type-specific bits
|
|
version: u32, // QUIC version (e.g., 0x00000001 for v1)
|
|
dcil: u4, // Destination Connection ID Length - 1
|
|
scil: u4, // Source Connection ID Length - 1
|
|
// Connection IDs follow (variable length)
|
|
// Length + Packet Number + Payload follow
|
|
};
|
|
|
|
/// QUIC Short Header (for 1-RTT data)
|
|
pub const QuicShortHeader = packed struct {
|
|
header_form: u1 = 0, // Always 0 for short header
|
|
fixed_bit: u1 = 1,
|
|
spin_bit: u1, // Latency spin bit
|
|
reserved: u2 = 0, // Must be 0
|
|
key_phase: u1, // Key update phase
|
|
packet_number_length: u2, // Length of packet number - 1
|
|
// Destination Connection ID follows (implied from context)
|
|
// Packet Number + Payload follow
|
|
};
|
|
|
|
/// MIMIC_QUIC Skin — HTTP/3 over QUIC
|
|
pub const MimicQuicSkin = struct {
|
|
allocator: std.mem.Allocator,
|
|
|
|
// QUIC Connection State
|
|
version: u32 = 0x00000001, // QUIC v1
|
|
dst_cid: [20]u8, // Destination Connection ID
|
|
src_cid: [20]u8, // Source Connection ID
|
|
next_packet_number: u64 = 0,
|
|
|
|
// HTTP/3 Settings
|
|
settings: Http3Settings,
|
|
|
|
// PNG for traffic shaping
|
|
png_state: ?png.PngState,
|
|
|
|
pub const Http3Settings = struct {
|
|
max_field_section_size: u64 = 8192,
|
|
qpack_max_table_capacity: u64 = 4096,
|
|
qpack_blocked_streams: u64 = 100,
|
|
};
|
|
|
|
const Self = @This();
|
|
|
|
pub fn init(allocator: std.mem.Allocator, png_state: ?png.PngState) !Self {
|
|
var self = Self{
|
|
.allocator = allocator,
|
|
.dst_cid = undefined,
|
|
.src_cid = undefined,
|
|
.settings = .{},
|
|
.png_state = png_state,
|
|
};
|
|
|
|
// Generate random Connection IDs (in production: crypto-secure)
|
|
// Using deterministic values for reproducibility
|
|
@memset(&self.dst_cid, 0xAB);
|
|
@memset(&self.src_cid, 0xCD);
|
|
|
|
return self;
|
|
}
|
|
|
|
pub fn deinit(_: *Self) void {}
|
|
|
|
/// Wrap LWF frame as HTTP/3 stream data over QUIC
|
|
pub fn wrap(self: *Self, allocator: std.mem.Allocator, lwf_frame: []const u8) ![]u8 {
|
|
// Apply PNG padding if available
|
|
var payload = lwf_frame;
|
|
var padded: ?[]u8 = null;
|
|
|
|
if (self.png_state) |*png_state| {
|
|
const target_size = png_state.samplePacketSize();
|
|
if (target_size > lwf_frame.len) {
|
|
padded = try self.addPadding(allocator, lwf_frame, target_size);
|
|
payload = padded.?;
|
|
}
|
|
png_state.advancePacket();
|
|
}
|
|
defer if (padded) |p| allocator.free(p);
|
|
|
|
// Build HTTP/3 DATA frame
|
|
const http3_frame = try self.buildHttp3DataFrame(allocator, payload);
|
|
defer allocator.free(http3_frame);
|
|
|
|
// Wrap in QUIC short header (1-RTT)
|
|
return try self.buildQuicShortPacket(allocator, http3_frame);
|
|
}
|
|
|
|
/// Unwrap QUIC packet back to LWF frame
|
|
pub fn unwrap(self: *Self, allocator: std.mem.Allocator, wire_data: []const u8) !?[]u8 {
|
|
if (wire_data.len < 5) return null;
|
|
|
|
// Parse QUIC header
|
|
const is_long_header = (wire_data[0] & 0x80) != 0;
|
|
if (is_long_header) {
|
|
// Long header — likely Initial or Handshake, drop for now
|
|
// In production: handle handshake
|
|
return null;
|
|
}
|
|
|
|
// Short header — extract payload
|
|
const pn_len: u3 = @as(u3, @intCast(wire_data[0] & 0x03)) + 1;
|
|
const header_len = 1 + 20 + @as(usize, pn_len); // flags + DCID + PN
|
|
|
|
if (wire_data.len <= header_len) return null;
|
|
|
|
const payload = wire_data[header_len..];
|
|
|
|
// Parse HTTP/3 frame
|
|
const lwf = try self.parseHttp3DataFrame(allocator, payload);
|
|
if (lwf == null) return null;
|
|
|
|
// Remove padding if applicable
|
|
if (self.png_state) |_| {
|
|
const unpadded = try self.removePadding(allocator, lwf.?);
|
|
allocator.free(lwf.?);
|
|
return unpadded;
|
|
}
|
|
|
|
return lwf;
|
|
}
|
|
|
|
/// Build HTTP/3 DATA frame (RFC 9114)
|
|
fn buildHttp3DataFrame(_: *Self, allocator: std.mem.Allocator, data: []const u8) ![]u8 {
|
|
// HTTP/3 Frame Format:
|
|
// Length (variable) | Type (variable) | Flags (1) | Body
|
|
|
|
const frame_type: u64 = 0x00; // DATA frame
|
|
const frame_len: u64 = data.len;
|
|
|
|
// Calculate encoded sizes
|
|
const type_len = encodeVarintLen(frame_type);
|
|
const len_len = encodeVarintLen(frame_len);
|
|
|
|
const frame = try allocator.alloc(u8, type_len + len_len + data.len);
|
|
|
|
// Encode Length
|
|
var offset: usize = 0;
|
|
offset += encodeVarint(frame[0..], frame_len);
|
|
|
|
// Encode Type
|
|
offset += encodeVarint(frame[offset..], frame_type);
|
|
|
|
// Copy body
|
|
@memcpy(frame[offset..], data);
|
|
|
|
return frame;
|
|
}
|
|
|
|
/// Parse HTTP/3 DATA frame
|
|
fn parseHttp3DataFrame(_: *Self, allocator: std.mem.Allocator, data: []const u8) !?[]u8 {
|
|
if (data.len < 2) return null;
|
|
|
|
// Parse Length
|
|
var offset: usize = 0;
|
|
const frame_len = try decodeVarint(data, &offset);
|
|
|
|
// Parse Type
|
|
const frame_type = try decodeVarint(data, &offset);
|
|
|
|
// We only handle DATA frames (type 0x00)
|
|
if (frame_type != 0x00) return null;
|
|
|
|
if (data.len < offset + frame_len) return null;
|
|
|
|
const body = data[offset..][0..frame_len];
|
|
return try allocator.dupe(u8, body);
|
|
}
|
|
|
|
/// Build QUIC short header packet (1-RTT)
|
|
fn buildQuicShortPacket(self: *Self, allocator: std.mem.Allocator, payload: []const u8) ![]u8 {
|
|
// Short Header Format:
|
|
// Flags (1) | DCID (implied) | Packet Number (1-4) | Payload
|
|
|
|
const pn_len: u2 = 3; // 4-byte packet numbers
|
|
const packet_number = self.next_packet_number;
|
|
self.next_packet_number += 1;
|
|
|
|
// Header byte
|
|
// Bits: 1 (Fixed) | 0 (Spin) | 00 (Reserved) | 0 (Key phase) | 11 (PN len = 4)
|
|
const header_byte: u8 = 0x40 | @as(u8, pn_len);
|
|
|
|
const packet = try allocator.alloc(u8, 1 + 20 + 4 + payload.len);
|
|
|
|
// Write header
|
|
packet[0] = header_byte;
|
|
|
|
// Write Destination Connection ID
|
|
@memcpy(packet[1..21], &self.dst_cid);
|
|
|
|
// Write Packet Number (4 bytes)
|
|
std.mem.writeInt(u32, packet[21..25], @truncate(packet_number), .big);
|
|
|
|
// Write payload
|
|
@memcpy(packet[25..], payload);
|
|
|
|
return packet;
|
|
}
|
|
|
|
// PNG Padding helpers (same as other skins)
|
|
fn addPadding(_: *Self, allocator: std.mem.Allocator, data: []const u8, target_size: u16) ![]u8 {
|
|
if (target_size <= data.len) return try allocator.dupe(u8, data);
|
|
|
|
const padded = try allocator.alloc(u8, target_size);
|
|
std.mem.writeInt(u16, padded[0..2], @as(u16, @intCast(data.len)), .big);
|
|
@memcpy(padded[2..][0..data.len], data);
|
|
|
|
var i: usize = 2 + data.len;
|
|
while (i < target_size) : (i += 1) {
|
|
padded[i] = @as(u8, @truncate(i * 7));
|
|
}
|
|
|
|
return padded;
|
|
}
|
|
|
|
fn removePadding(_: *Self, allocator: std.mem.Allocator, padded: []const u8) ![]u8 {
|
|
if (padded.len < 2) return try allocator.dupe(u8, padded);
|
|
|
|
const original_len = std.mem.readInt(u16, padded[0..2], .big);
|
|
if (original_len > padded.len - 2) return try allocator.dupe(u8, padded);
|
|
|
|
const result = try allocator.alloc(u8, original_len);
|
|
@memcpy(result, padded[2..][0..original_len]);
|
|
return result;
|
|
}
|
|
};
|
|
|
|
/// QUIC Variable-Length Integer Encoding (RFC 9000)
|
|
fn encodeVarintLen(value: u64) usize {
|
|
if (value <= 63) return 1;
|
|
if (value <= 16383) return 2;
|
|
if (value <= 1073741823) return 4;
|
|
return 8;
|
|
}
|
|
|
|
fn encodeVarint(buf: []u8, value: u64) usize {
|
|
if (value <= 63) {
|
|
buf[0] = @as(u8, @intCast(value));
|
|
return 1;
|
|
} else if (value <= 16383) {
|
|
const encoded: u16 = @as(u16, @intCast(value)) | 0x4000;
|
|
std.mem.writeInt(u16, buf[0..2], encoded, .big);
|
|
return 2;
|
|
} else if (value <= 1073741823) {
|
|
const encoded: u32 = @as(u32, @intCast(value)) | 0x80000000;
|
|
std.mem.writeInt(u32, buf[0..4], encoded, .big);
|
|
return 4;
|
|
} else {
|
|
const encoded: u64 = value | 0xC000000000000000;
|
|
std.mem.writeInt(u64, buf[0..8], encoded, .big);
|
|
return 8;
|
|
}
|
|
}
|
|
|
|
fn decodeVarint(data: []const u8, offset: *usize) !u64 {
|
|
if (data.len <= offset.*) return error.Truncated;
|
|
|
|
const first = data[offset.*];
|
|
const prefix = first >> 6;
|
|
|
|
var result: u64 = 0;
|
|
switch (prefix) {
|
|
0 => {
|
|
result = first & 0x3F;
|
|
offset.* += 1;
|
|
},
|
|
1 => {
|
|
if (data.len < offset.* + 2) return error.Truncated;
|
|
result = std.mem.readInt(u16, data[offset.*..][0..2], .big) & 0x3FFF;
|
|
offset.* += 2;
|
|
},
|
|
2 => {
|
|
if (data.len < offset.* + 4) return error.Truncated;
|
|
result = std.mem.readInt(u32, data[offset.*..][0..4], .big) & 0x3FFFFFFF;
|
|
offset.* += 4;
|
|
},
|
|
3 => {
|
|
if (data.len < offset.* + 8) return error.Truncated;
|
|
result = std.mem.readInt(u64, data[offset.*..][0..8], .big) & 0x3FFFFFFFFFFFFFFF;
|
|
offset.* += 8;
|
|
},
|
|
else => unreachable,
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// ============================================================================
|
|
// TESTS
|
|
// ============================================================================
|
|
|
|
test "QUIC varint encode/decode" {
|
|
// Test all size classes
|
|
const test_values = [_]u64{ 0, 63, 64, 16383, 16384, 1073741823, 1073741824, 4611686018427387903 };
|
|
|
|
var buf: [8]u8 = undefined;
|
|
|
|
for (test_values) |value| {
|
|
const len = encodeVarint(&buf, value);
|
|
var offset: usize = 0;
|
|
const decoded = try decodeVarint(&buf, &offset);
|
|
|
|
try std.testing.expectEqual(value, decoded);
|
|
try std.testing.expectEqual(len, offset);
|
|
}
|
|
}
|
|
|
|
test "HTTP/3 DATA frame roundtrip" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
var skin = try MimicQuicSkin.init(allocator, null);
|
|
defer skin.deinit();
|
|
|
|
const data = "Hello, HTTP/3!";
|
|
const frame = try skin.buildHttp3DataFrame(allocator, data);
|
|
defer allocator.free(frame);
|
|
|
|
const parsed = try skin.parseHttp3DataFrame(allocator, frame);
|
|
defer if (parsed) |p| allocator.free(p);
|
|
|
|
try std.testing.expect(parsed != null);
|
|
try std.testing.expectEqualStrings(data, parsed.?);
|
|
}
|
|
|
|
test "MIMIC_QUIC wrap/unwrap roundtrip" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
var skin = try MimicQuicSkin.init(allocator, null);
|
|
defer skin.deinit();
|
|
|
|
const lwf = "LWF test frame";
|
|
const wrapped = try skin.wrap(allocator, lwf);
|
|
defer allocator.free(wrapped);
|
|
|
|
// Should have QUIC short header + HTTP/3 frame
|
|
try std.testing.expect(wrapped.len > lwf.len);
|
|
|
|
// Verify short header
|
|
try std.testing.expect((wrapped[0] & 0x80) == 0); // Short header flag
|
|
|
|
const unwrapped = try skin.unwrap(allocator, wrapped);
|
|
defer if (unwrapped) |u| allocator.free(u);
|
|
|
|
try std.testing.expect(unwrapped != null);
|
|
try std.testing.expectEqualStrings(lwf, unwrapped.?);
|
|
}
|
|
|
|
test "MIMIC_QUIC with PNG padding" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
const secret = [_]u8{0x42} ** 32;
|
|
const png_state = png.PngState.initFromSharedSecret(secret);
|
|
|
|
var skin = try MimicQuicSkin.init(allocator, png_state);
|
|
defer skin.deinit();
|
|
|
|
const lwf = "A";
|
|
const wrapped = try skin.wrap(allocator, lwf);
|
|
defer allocator.free(wrapped);
|
|
|
|
// Should be padded to target size
|
|
try std.testing.expect(wrapped.len > lwf.len + 25); // Header + padding
|
|
}
|