/** * Custom VNC Protocol Implementation * This is a lightweight, custom protocol designed to bypass detection * while providing high-performance remote desktop capabilities. * * Protocol Format: * [Frame Header] [Compressed Data] * * Frame Header (8 bytes): * - 4 bytes: frame_id (uint32) * - 2 bytes: width (uint16) * - 2 bytes: height (uint16) * - 1 byte: compression_type (0=none, 1=lz4) * - 1 byte: flags (bitmask) * * Data Format: * - Full frame: raw RGBA pixels * - Diff frame: RLE-encoded changed regions */ export interface FrameHeader { frameId: number; width: number; height: number; compressionType: CompressionType; flags: number; timestamp: number; } export enum CompressionType { NONE = 0, LZ4 = 1, RLE = 2, } export interface VNCFrame { header: FrameHeader; data: Uint8Array; isFullFrame: boolean; } export interface InputEvent { type: 'mousemove' | 'mousedown' | 'mouseup' | 'keydown' | 'keyup' | 'wheel'; data: InputData; timestamp: number; } export interface InputData { x?: number; y?: number; button?: number; keyCode?: number; deltaX?: number; deltaY?: number; } export interface ProtocolConfig { maxWidth: number; maxHeight: number; targetFPS: number; quality: number; useCompression: boolean; } const DEFAULT_CONFIG: ProtocolConfig = { maxWidth: 1280, maxHeight: 720, targetFPS: 30, quality: 80, useCompression: true, }; export class VNCProtocol { private config: ProtocolConfig; private frameId: number = 0; private lastFrameData: Uint8Array | null = null; constructor(config: Partial = {}) { this.config = { ...DEFAULT_CONFIG, ...config }; } /** * Encode a frame for transmission * Uses diff-based encoding for efficiency */ encodeFrame(pixels: Uint8Array, width: number, height: number): VNCFrame { this.frameId++; const isFullFrame = this.lastFrameData === null || pixels.length !== this.lastFrameData.length; let data: Uint8Array; let compressionType: CompressionType = CompressionType.NONE; if (isFullFrame) { // Full frame capture if (this.config.useCompression) { data = this.compressLZW(pixels); compressionType = CompressionType.LZ4; } else { data = pixels; } } else { // Diff-based encoding - only send changed regions const diffData = this.encodeDiff(this.lastFrameData!, pixels); if (diffData.length < pixels.length * 0.3) { // Diff is smaller, use it if (this.config.useCompression) { data = this.compressLZW(diffData); compressionType = CompressionType.LZ4; } else { data = diffData; } } else { // Full frame is more efficient if (this.config.useCompression) { data = this.compressLZW(pixels); compressionType = CompressionType.LZ4; } else { data = pixels; } } } this.lastFrameData = new Uint8Array(pixels); const header: FrameHeader = { frameId: this.frameId, width, height, compressionType, flags: isFullFrame ? 1 : 0, timestamp: Date.now(), }; return { header, data, isFullFrame }; } /** * Decode a received frame */ decodeFrame(header: FrameHeader, data: Uint8Array): Uint8Array { let decompressed: Uint8Array; switch (header.compressionType) { case CompressionType.LZ4: decompressed = this.decompressLZW(data); break; case CompressionType.RLE: decompressed = this.decodeRLE(data); break; default: decompressed = data; } if (header.flags & 1) { // Full frame return decompressed; } else { // Diff frame - apply to last frame return this.applyDiff(this.lastFrameData!, decompressed); } } /** * Encode changed regions using RLE */ private encodeDiff(oldData: Uint8Array, newData: Uint8Array): Uint8Array { const chunks: Uint8Array[] = []; const pixelCount = oldData.length / 4; let i = 0; while (i < pixelCount) { // Find changed pixels let changedStart = -1; let changedEnd = -1; for (let j = i; j < pixelCount; j++) { const oldIdx = j * 4; const newIdx = j * 4; if (oldData[oldIdx] !== newData[newIdx] || oldData[oldIdx + 1] !== newData[newIdx + 1] || oldData[oldIdx + 2] !== newData[newIdx + 2]) { if (changedStart === -1) { changedStart = j; } changedEnd = j + 1; } else if (changedStart !== -1) { // End of changed region break; } } if (changedStart !== -1) { // Output unchanged run length if (changedStart > i) { chunks.push(this.encodeRunLength(i, changedStart - i, true)); } // Output changed pixels const changedCount = changedEnd - changedStart; chunks.push(this.encodeRunLength(changedStart, changedCount, false)); i = changedEnd; } else { i++; } } // Calculate total size let totalSize = 0; chunks.forEach(c => totalSize += c.length); // Combine chunks const result = new Uint8Array(totalSize); let offset = 0; chunks.forEach(c => { result.set(c, offset); offset += c.length; }); return result; } /** * Encode a run of pixels */ private encodeRunLength(start: number, count: number, isUnchanged: boolean): Uint8Array { // Header: 1 byte flags + 4 bytes start + 4 bytes count const result = new Uint8Array(9); result[0] = isUnchanged ? 0 : 1; result[1] = (start >> 0) & 0xFF; result[2] = (start >> 8) & 0xFF; result[3] = (start >> 16) & 0xFF; result[4] = (start >> 24) & 0xFF; result[5] = (count >> 0) & 0xFF; result[6] = (count >> 8) & 0xFF; result[7] = (count >> 16) & 0xFF; result[8] = (count >> 24) & 0xFF; if (!isUnchanged) { // Copy pixel data const pixels = new Uint8Array(count * 4); for (let i = 0; i < count; i++) { const srcIdx = (start + i) * 4; const dstIdx = i * 4; pixels[dstIdx] = this.lastFrameData![srcIdx + 0]; pixels[dstIdx + 1] = this.lastFrameData![srcIdx + 1]; pixels[dstIdx + 2] = this.lastFrameData![srcIdx + 2]; pixels[dstIdx + 3] = 255; // Alpha } const newResult = new Uint8Array(result.length + pixels.length); newResult.set(result); newResult.set(pixels, result.length); return newResult; } return result; } /** * Apply diff to last frame */ private applyDiff(lastFrame: Uint8Array, diffData: Uint8Array): Uint8Array { const newFrame = new Uint8Array(lastFrame); const view = new DataView(diffData.buffer); let offset = 0; while (offset < diffData.length) { const flags = diffData[offset]; const start = view.getUint32(offset + 1, true); const count = view.getUint32(offset + 5, true); offset += 9; if (flags === 1) { // Changed pixels for (let i = 0; i < count; i++) { const dstIdx = (start + i) * 4; newFrame[dstIdx] = diffData[offset + i * 4]; newFrame[dstIdx + 1] = diffData[offset + i * 4 + 1]; newFrame[dstIdx + 2] = diffData[offset + i * 4 + 2]; newFrame[dstIdx + 3] = 255; } offset += count * 4; } // flags === 0 means unchanged, skip } return newFrame; } /** * Simple LZW-style compression * Optimized for RGBA pixel data */ private compressLZW(data: Uint8Array): Uint8Array { const chunkSize = 64; const chunks: Uint8Array[] = []; for (let i = 0; i < data.length; i += chunkSize) { const chunk = data.slice(i, Math.min(i + chunkSize, data.length)); chunks.push(chunk); } // Store chunks with run-length encoding for repeated bytes const result: number[] = []; for (const chunk of chunks) { // Mark compressed chunk result.push(1); // Compress the chunk let runLength = 1; for (let i = 1; i < chunk.length; i++) { if (chunk[i] === chunk[i - 1]) { runLength++; } else { if (runLength > 2) { // Run-length encoded result.push(chunk[i - 1], runLength); } else { // Raw bytes for (let j = 0; j < runLength; j++) { result.push(chunk[i - 1 - j]); } } runLength = 1; } } // Handle last run if (runLength > 2) { result.push(chunk[chunk.length - 1], runLength); } else { for (let j = 0; j < runLength; j++) { result.push(chunk[chunk.length - 1 - j]); } } } return new Uint8Array(result); } /** * Decompress LZW-compressed data */ private decompressLZW(data: Uint8Array): Uint8Array { const result: number[] = []; let i = 0; while (i < data.length) { if (data[i] === 1) { // Compressed chunk i++; while (i < data.length && data[i] !== 1 && i < data.length) { const byte = data[i]; if (i + 1 < data.length && typeof data[i + 1] === 'number') { const runLength = data[i + 1]; for (let j = 0; j < runLength; j++) { result.push(byte); } i += 2; } else { result.push(byte); i++; } } } else { // Raw byte result.push(data[i]); i++; } } return new Uint8Array(result); } /** * Decode RLE-encoded data */ private decodeRLE(data: Uint8Array): Uint8Array { const result: number[] = []; const view = new DataView(data.buffer); let offset = 0; while (offset < data.length) { const isRun = data[offset] === 1; const start = view.getUint32(offset + 1, true); const count = view.getUint32(offset + 5, true); offset += 9; if (isRun) { for (let i = 0; i < count; i++) { const byte = data[offset + i]; result.push(byte); } offset += count; } } return new Uint8Array(result); } /** * Serialize frame for network transmission */ serializeFrame(frame: VNCFrame): Uint8Array { const headerSize = 12; const totalSize = headerSize + frame.data.length; const result = new Uint8Array(totalSize); const view = new DataView(result.buffer); view.setUint32(0, frame.header.frameId, true); view.setUint16(4, frame.header.width, true); view.setUint16(6, frame.header.height, true); view.setUint8(8, frame.header.compressionType); view.setUint8(9, frame.header.flags); view.setUint32(10, frame.header.timestamp, true); result.set(frame.data, headerSize); return result; } /** * Deserialize frame from network data */ deserializeFrame(data: Uint8Array): VNCFrame { const view = new DataView(data.buffer); const header: FrameHeader = { frameId: view.getUint32(0, true), width: view.getUint16(4, true), height: view.getUint16(6, true), compressionType: view.getUint8(8), flags: view.getUint9, // Note: corrected from frame.header.flags timestamp: view.getUint32(10, true), }; const frameData = data.slice(12); const decoded = this.decodeFrame(header, frameData); this.lastFrameData = decoded; return { header, data: decoded, isFullFrame: (header.flags & 1) === 1, }; } /** * Serialize input event for transmission */ serializeInput(event: InputEvent): Uint8Array { const encoder = new TextEncoder(); const jsonStr = JSON.stringify({ ...event, timestamp: event.timestamp || Date.now(), }); return encoder.encode(jsonStr); } /** * Deserialize input event from network data */ deserializeInput(data: Uint8Array): InputEvent { const decoder = new TextDecoder(); const jsonStr = decoder.decode(data); return JSON.parse(jsonStr) as InputEvent; } /** * Create header for full frame request */ createFullFrameRequest(): Uint8Array { const view = new DataView(new ArrayBuffer(4)); view.setUint32(0, 0, true); return new Uint8Array(view.buffer); } }