Add detailed analysis and documentation for RSNet.dll download functionality

- Introduced a comprehensive markdown document outlining the API surface of RSNet.dll related to downloading video files.
- Documented initialization, connection, record querying, and download processes.
- Provided insights into the exported functions, their parameters, and expected behaviors.
- Included practical implications and recommendations for implementing a downloader script using C# interop.
- Highlighted the necessary struct layouts and callback mechanisms for effective integration with the DLL.
This commit is contained in:
MaddoScientisto 2026-04-17 21:19:46 +02:00
commit de077ca5d5
34 changed files with 2084 additions and 152 deletions

6
.github/copilot-instructions.md vendored Normal file
View file

@ -0,0 +1,6 @@
# Repository Instructions
- When using Ghidra MCP tools in this repository, do not run destructive actions unless the user explicitly asks for them.
- Treat these actions as destructive and forbidden by default during analysis: deleting functions, deleting data, patching bytes, applying edit plans that remove or overwrite program structures, or any other write operation that changes binary semantics.
- During analysis, allowed write actions are limited to renaming functions or data and adding comments.
- Prefer read-only inspection first. If a write action seems necessary beyond renames/comments, stop and ask the user before proceeding.

9
.vscode/mcp.json vendored Normal file
View file

@ -0,0 +1,9 @@
{
"servers": {
"ghidra-web": {
"url": "http://127.0.0.1:8081/sse",
"type": "http"
}
},
"inputs": []
}

BIN
RSNet.dll Normal file

Binary file not shown.

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<FILE_INFO>
<BASIC_INFO>
<STATE NAME="CONTENT_TYPE" TYPE="string" VALUE="Program" />
<STATE NAME="PARENT" TYPE="string" VALUE="/" />
<STATE NAME="FILE_ID" TYPE="string" VALUE="c0a86451d8fa89550806671100" />
<STATE NAME="FILE_TYPE" TYPE="int" VALUE="0" />
<STATE NAME="READ_ONLY" TYPE="boolean" VALUE="false" />
<STATE NAME="NAME" TYPE="string" VALUE="RSNet.dll" />
</BASIC_INFO>
</FILE_INFO>

Binary file not shown.

Binary file not shown.

View file

@ -1,4 +1,6 @@
VERSION=1
/
NEXT-ID:0
00000001:RSNet.dll:c0a86451d8fa89550806671100
00000000:Surveillance_client.exe:c0a86480f39a29301594652600
NEXT-ID:2
MD5:d41d8cd98f00b204e9800998ecf8427e

View file

@ -1,5 +1,6 @@
VERSION=1
/
00000001:RSNet.dll:c0a86451d8fa89550806671100
00000000:Surveillance_client.exe:c0a86480f39a29301594652600
NEXT-ID:1
NEXT-ID:2
MD5:d41d8cd98f00b204e9800998ecf8427e

View file

@ -1,2 +0,0 @@
IADD:00000000:/Surveillance_client.exe
IDSET:/Surveillance_client.exe:c0a86480f39a29301594652600

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<FILE_INFO>
<BASIC_INFO>
<STATE NAME="CONTENT_TYPE" TYPE="string" VALUE="ProgramUserData" />
<STATE NAME="PARENT" TYPE="string" VALUE="/" />
<STATE NAME="FILE_ID" TYPE="string" VALUE="c0a86451e45f97977061057400" />
<STATE NAME="FILE_TYPE" TYPE="int" VALUE="0" />
<STATE NAME="READ_ONLY" TYPE="boolean" VALUE="false" />
<STATE NAME="NAME" TYPE="string" VALUE="udf_c0a86451d8fa89550806671100" />
</BASIC_INFO>
</FILE_INFO>

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,2 @@
IADD:00000001:/udf_c0a86451d8fa89550806671100
IDSET:/udf_c0a86451d8fa89550806671100:c0a86451e45f97977061057400

74
SurveillanceClient/.gitignore vendored Normal file
View file

@ -0,0 +1,74 @@
# Build results
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# User-specific files
*.user
*.suo
*.userosscache
*.sln.docstates
*.rsuser
# Visual Studio
.vs/
*.VisualState.xml
*.pidb
*.svclog
# JetBrains Rider
.idea/
*.DotSettings.user
# VS Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# NuGet
*.nupkg
*.snupkg
.nuget/
packages/
!packages/build/
project.lock.json
project.fragment.lock.json
artifacts/
# MSBuild Binary and Structured Log
*.binlog
# ReSharper
_ReSharper*/
*.[Rr]e[Ss]harper
# Test results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
coverage*.json
coverage*.xml
coverage*.info
# Local secrets / configuration overrides (NEVER COMMIT CREDENTIALS)
appsettings.Development.json
appsettings.Local.json
appsettings.*.Local.json
secrets.json
secrets.*.json
*.secrets
.env
.env.*
# OS
Thumbs.db
ehthumbs.db
Desktop.ini
.DS_Store
# Downloaded video output (local runtime artifact)
downloads/
output/

View file

@ -0,0 +1,14 @@
<Project>
<PropertyGroup>
<TargetFramework>net10.0-windows</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<!-- Required by source-generated P/Invoke (LibraryImport). -->
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!-- RSNet.dll is 32-bit (x86). Consumers of the native interop must run as x86. -->
<PlatformTarget>x86</PlatformTarget>
<Platforms>x86</Platforms>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,62 @@
# SurveillanceClient
.NET 10 host for driving the vendor `RSNet.dll` (32-bit) to download
recorded video from a surveillance NVR/DVR.
## Layout
- `src/SurveillanceClient.Core` — class library that wraps the `RSNet.dll`
native exports behind a service interface (`IRsNetClient`).
- `src/SurveillanceClient.Cli` — console host used for iterative,
manual testing of the Core library against a real device.
## Platform
`RSNet.dll` is a 32-bit Visual Studio DLL using `__stdcall`. Every
project in this solution targets `x86` via `Directory.Build.props`.
## Running
The vendor DLL and its dependencies live in
`i:\Apps\Surveillance_client`. The CLI sets that directory as the
native DLL search path at startup so the DLL and its `.ini` config
are found.
## Credentials
Credentials are **never committed**. Configure them via .NET user
secrets (recommended) or a `appsettings.Local.json` file (ignored by
`.gitignore`).
### User secrets (recommended)
```powershell
cd src/SurveillanceClient.Cli
dotnet user-secrets set "Device:Host" "192.168.100.92"
dotnet user-secrets set "Device:Port" "9000"
dotnet user-secrets set "Device:Username" "admin"
dotnet user-secrets set "Device:Password" "your-password-here"
```
### appsettings.Local.json (alternative)
Create `src/SurveillanceClient.Cli/appsettings.Local.json`:
```json
{
"Device": {
"Host": "192.168.100.92",
"Port": 9000,
"Username": "admin",
"Password": "your-password-here"
}
}
```
## Build / Run
```powershell
cd SurveillanceClient
dotnet build
dotnet run --project src/SurveillanceClient.Cli
```

View file

@ -0,0 +1,11 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/SurveillanceClient.Core/SurveillanceClient.Core.csproj" />
<Project Path="src/SurveillanceClient.Cli/SurveillanceClient.Cli.csproj" />
</Folder>
<Folder Name="/solution-items/">
<File Path=".gitignore" />
<File Path="Directory.Build.props" />
<File Path="README.md" />
</Folder>
</Solution>

View file

@ -0,0 +1,37 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using SurveillanceClient.Cli;
using SurveillanceClient.Core;
var builder = Host.CreateApplicationBuilder(args);
// Local, never-committed overrides for credentials etc.
builder.Configuration.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false);
if (builder.Environment.IsDevelopment())
{
builder.Configuration.AddUserSecrets<Program>(optional: true);
}
builder.Services.AddSurveillanceClientCore();
builder.Services.AddHostedService<TestHarness>();
using var host = builder.Build();
try
{
await host.RunAsync();
}
catch (Exception ex)
{
host.Services.GetRequiredService<ILogger<Program>>()
.LogCritical(ex, "Unhandled exception");
return 1;
}
return 0;
/// <summary>Marker type used to anchor user-secrets and logger categories.</summary>
public partial class Program;

View file

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<RootNamespace>SurveillanceClient.Cli</RootNamespace>
<AssemblyName>SurveillanceClient.Cli</AssemblyName>
<IsPackable>false</IsPackable>
<UserSecretsId>surveillance-client-cli-4f0a1c0d</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SurveillanceClient.Core\SurveillanceClient.Core.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,87 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SurveillanceClient.Core.Configuration;
using SurveillanceClient.Core.Services;
namespace SurveillanceClient.Cli;
/// <summary>
/// Iterative manual-test harness. Runs once at startup against the
/// configured device, logs status callbacks, then requests shutdown.
/// Extended step-by-step as the native API is proven out.
/// </summary>
internal sealed class TestHarness : BackgroundService
{
private readonly ILogger<TestHarness> _logger;
private readonly IRsNetClient _client;
private readonly DeviceOptions _device;
private readonly IHostApplicationLifetime _lifetime;
public TestHarness(
ILogger<TestHarness> logger,
IRsNetClient client,
IOptions<DeviceOptions> device,
IHostApplicationLifetime lifetime)
{
_logger = logger;
_client = client;
_device = device.Value;
_lifetime = lifetime;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
await RunAsync(stoppingToken).ConfigureAwait(false);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// graceful shutdown
}
catch (Exception ex)
{
_logger.LogError(ex, "Test harness failed");
}
finally
{
_lifetime.StopApplication();
}
}
private async Task RunAsync(CancellationToken ct)
{
_logger.LogInformation("=== Stage 1: Initialize RSNet.dll ===");
await _client.InitializeAsync(ct).ConfigureAwait(false);
_logger.LogInformation("RSNet initialized successfully.");
if (string.IsNullOrWhiteSpace(_device.Host) ||
string.IsNullOrWhiteSpace(_device.Username))
{
_logger.LogWarning(
"Device credentials not configured. Set them via user-secrets or appsettings.Local.json " +
"to run the connection stage. Skipping connect.");
return;
}
_logger.LogInformation(
"=== Stage 2: Connect to {Host}:{Port} as {User} ===",
_device.Host, _device.Port, _device.Username);
using var session = _client.Connect(_device);
session.StatusChanged += (_, e) =>
_logger.LogInformation("Session status callback: code=0x{Code:X} ({CodeDec})", e.Code, e.Code);
_logger.LogInformation("Session handle: 0x{Handle:X}. Waiting up to 15s for login callbacks...",
session.Handle.ToInt64());
try
{
await Task.Delay(TimeSpan.FromSeconds(15), ct).ConfigureAwait(false);
}
catch (OperationCanceledException) { }
_logger.LogInformation("=== Stage 2 complete. Stop here until login status codes are mapped. ===");
}
}

View file

@ -0,0 +1,18 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Warning",
"SurveillanceClient": "Debug"
}
},
"RsNet": {
"NativeDirectory": "i:\\Apps\\Surveillance_client"
},
"Device": {
"Host": "",
"Port": 9000,
"Username": "",
"Password": ""
}
}

View file

@ -0,0 +1,15 @@
namespace SurveillanceClient.Core.Configuration;
/// <summary>
/// Credentials and endpoint for a target surveillance device.
/// Supplied via user secrets or local config — never committed.
/// </summary>
public sealed class DeviceOptions
{
public const string SectionName = "Device";
public string Host { get; set; } = string.Empty;
public ushort Port { get; set; } = 9000;
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}

View file

@ -0,0 +1,22 @@
namespace SurveillanceClient.Core.Configuration;
/// <summary>
/// Runtime options for locating and loading <c>RSNet.dll</c>.
/// </summary>
public sealed class RsNetOptions
{
public const string SectionName = "RsNet";
/// <summary>
/// Absolute path to the directory that contains <c>RSNet.dll</c>
/// and its companion files (e.g. <c>RSNet.ini</c>, P2P helpers).
/// Typically the vendor install directory.
/// </summary>
public string NativeDirectory { get; set; } = @"i:\Apps\Surveillance_client";
/// <summary>
/// Optional encryption value passed to <c>RSNetSetEncription</c>.
/// <c>null</c> skips the call.
/// </summary>
public uint? Encryption { get; set; }
}

View file

@ -0,0 +1,69 @@
using System.Runtime.InteropServices;
namespace SurveillanceClient.Core.Native;
/// <summary>
/// Raw P/Invoke bindings for <c>RSNet.dll</c>.
///
/// ABI notes (see docs/surveillance-client-rsnet-download-analysis.md):
/// <list type="bullet">
/// <item>Architecture: x86/32-bit. The host process MUST run as x86.</item>
/// <item>Calling convention: <c>StdCall</c> (callee cleans stack).</item>
/// <item>Strings: ANSI (<c>char*</c>), not UTF-16.</item>
/// </list>
///
/// Request structs are intentionally passed as opaque <see cref="IntPtr"/>
/// during the reverse-engineering phase. Strongly-typed wrappers may be
/// introduced as the native layouts are confirmed.
/// </summary>
internal static partial class RsNetNative
{
private const string Dll = "RSNet.dll";
[LibraryImport(Dll, StringMarshalling = StringMarshalling.Utf8)]
[UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
internal static partial void RSNetInit();
[LibraryImport(Dll)]
[UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
internal static partial void RSNetRelease();
[LibraryImport(Dll)]
[UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
internal static partial void RSNetSetEncription(uint value);
[LibraryImport(Dll)]
[UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
internal static partial IntPtr RSNetConnectionStart(IntPtr request);
[LibraryImport(Dll)]
[UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
internal static partial IntPtr RSNetConnectionStartEx(IntPtr request);
[LibraryImport(Dll)]
[UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
internal static partial int RSNetQueryRecord(IntPtr session, IntPtr queryRequest);
[LibraryImport(Dll)]
[UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
internal static partial int RSNetQueryRecordEx(IntPtr session, IntPtr queryRequest);
[LibraryImport(Dll)]
[UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
internal static partial IntPtr RSNetStartDownloadRecord(IntPtr session, IntPtr downloadRequest);
[LibraryImport(Dll)]
[UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
internal static partial IntPtr RSNetStartDownloadRecordEx(IntPtr session, IntPtr downloadRequest);
[LibraryImport(Dll)]
[UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
internal static partial void RSNetReqRecordData(IntPtr recordPlayHandle);
/// <summary>
/// Session/status callback dispatched by the DLL's login/worker thread.
/// Exact signature is provisional — see the analysis doc.
/// </summary>
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
internal delegate void RsNetStatusCallback(uint code, IntPtr userContext);
}

View file

@ -0,0 +1,34 @@
using System.Runtime.InteropServices;
namespace SurveillanceClient.Core.Native;
/// <summary>
/// Provisional layout for <c>RSNetConnectionStart</c>'s request struct.
///
/// Based on decompilation of the copy-in path inside RSNet.dll:
/// <list type="bullet">
/// <item>+0x00 pointer to ANSI address string</item>
/// <item>+0x04 16-bit port</item>
/// <item>+0x08 pointer to ANSI username string</item>
/// <item>+0x0C pointer to ANSI password string</item>
/// <item>+0x10..+0x28 misc 32-bit fields (mode/callback-related)</item>
/// </list>
///
/// This is NOT fully validated. Treat as a best-effort starting point.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 4)]
internal struct RsNetConnectRequest
{
public IntPtr AddressAnsi;
public ushort Port;
public ushort Padding0;
public IntPtr UsernameAnsi;
public IntPtr PasswordAnsi;
public uint Field10;
public uint Field14;
public uint Field18;
public uint Field1C;
public uint Field20;
public uint Field24;
public uint Field28;
}

View file

@ -0,0 +1,26 @@
using System.Runtime.InteropServices;
namespace SurveillanceClient.Core.Native;
/// <summary>
/// Win32 entry points used to control the native DLL search path before
/// loading <c>RSNet.dll</c>. The vendor DLL expects its install directory
/// on the search path so it can find <c>RSNet.ini</c> and helper binaries.
/// </summary>
internal static partial class Win32
{
[LibraryImport("kernel32.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool SetDllDirectoryW(string? lpPathName);
[LibraryImport("kernel32.dll", SetLastError = true)]
internal static partial IntPtr AddDllDirectory(
[MarshalAs(UnmanagedType.LPWStr)] string NewDirectory);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool SetDefaultDllDirectories(uint DirectoryFlags);
internal const uint LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000;
internal const uint LOAD_LIBRARY_SEARCH_USER_DIRS = 0x00000400;
}

View file

@ -0,0 +1,27 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using SurveillanceClient.Core.Configuration;
using SurveillanceClient.Core.Services;
namespace SurveillanceClient.Core;
/// <summary>
/// DI registration helpers for the SurveillanceClient Core services.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Registers <see cref="IRsNetClient"/> and binds its options from
/// configuration. The client is a singleton because <c>RSNet.dll</c>
/// holds process-wide global state.
/// </summary>
public static IServiceCollection AddSurveillanceClientCore(this IServiceCollection services)
{
services.AddOptions<RsNetOptions>().BindConfiguration(RsNetOptions.SectionName);
services.AddOptions<DeviceOptions>().BindConfiguration(DeviceOptions.SectionName);
services.TryAddSingleton<IRsNetClient, RsNetClient>();
return services;
}
}

View file

@ -0,0 +1,46 @@
using SurveillanceClient.Core.Configuration;
namespace SurveillanceClient.Core.Services;
/// <summary>
/// High-level, managed facade over <c>RSNet.dll</c>. Intended lifetime:
/// singleton per process. <see cref="InitializeAsync"/> must be called
/// before any session is opened.
/// </summary>
public interface IRsNetClient : IAsyncDisposable
{
/// <summary>Whether <c>RSNetInit</c> has been called successfully.</summary>
bool IsInitialized { get; }
/// <summary>
/// Loads <c>RSNet.dll</c> from the configured directory and calls
/// <c>RSNetInit</c>. Safe to call more than once; subsequent calls are no-ops.
/// </summary>
Task InitializeAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Opens a direct-IP session by calling <c>RSNetConnectionStart</c>.
/// The returned session has not yet necessarily completed login — the
/// DLL performs login asynchronously on a worker thread.
/// </summary>
IRsNetSession Connect(DeviceOptions device);
}
/// <summary>
/// Handle-bearing wrapper around a single <c>RSNet.dll</c> session.
/// Disposing closes the native session and releases any pinned buffers.
/// </summary>
public interface IRsNetSession : IDisposable
{
/// <summary>Native session handle returned by <c>RSNetConnectionStart</c>.</summary>
IntPtr Handle { get; }
/// <summary>Raised for each native status/event callback.</summary>
event EventHandler<RsNetStatusEventArgs>? StatusChanged;
}
/// <summary>Status event payload forwarded from the native callback.</summary>
public sealed class RsNetStatusEventArgs(uint code) : EventArgs
{
public uint Code { get; } = code;
}

View file

@ -0,0 +1,122 @@
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SurveillanceClient.Core.Configuration;
using SurveillanceClient.Core.Native;
namespace SurveillanceClient.Core.Services;
/// <inheritdoc cref="IRsNetClient"/>
internal sealed class RsNetClient : IRsNetClient
{
private readonly ILogger<RsNetClient> _logger;
private readonly RsNetOptions _options;
private readonly SemaphoreSlim _initLock = new(1, 1);
private bool _initialized;
private bool _disposed;
public RsNetClient(ILogger<RsNetClient> logger, IOptions<RsNetOptions> options)
{
_logger = logger;
_options = options.Value;
}
public bool IsInitialized => _initialized;
public async Task InitializeAsync(CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (_initialized) return;
await _initLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (_initialized) return;
if (!OperatingSystem.IsWindows())
throw new PlatformNotSupportedException("RSNet.dll is Windows-only.");
if (RuntimeInformation.ProcessArchitecture != Architecture.X86)
throw new PlatformNotSupportedException(
$"RSNet.dll is 32-bit (x86). Current process is {RuntimeInformation.ProcessArchitecture}. " +
"Build and run the host as x86.");
var dir = _options.NativeDirectory;
if (string.IsNullOrWhiteSpace(dir) || !Directory.Exists(dir))
throw new DirectoryNotFoundException(
$"RsNetOptions.NativeDirectory not found: '{dir}'. " +
"Configure it to the vendor install directory containing RSNet.dll.");
var dllPath = Path.Combine(dir, "RSNet.dll");
if (!File.Exists(dllPath))
throw new FileNotFoundException("RSNet.dll not found in NativeDirectory.", dllPath);
_logger.LogInformation("Configuring native DLL search path: {Directory}", dir);
// Use the modern search list so both RSNet.dll and its implicit
// dependencies are resolved from the vendor directory.
Win32.SetDefaultDllDirectories(
Win32.LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | Win32.LOAD_LIBRARY_SEARCH_USER_DIRS);
Win32.AddDllDirectory(dir);
// Legacy fallback for any implicit LoadLibrary calls by RSNet.dll itself.
Win32.SetDllDirectoryW(dir);
// Many vendor DLLs read <Name>.ini from the *current* working directory
// rather than the DLL's own directory. Switch CWD so RSNet.ini resolves.
var previousCwd = Environment.CurrentDirectory;
Environment.CurrentDirectory = dir;
_logger.LogDebug("CWD changed {Previous} -> {New}", previousCwd, dir);
_logger.LogInformation("Calling RSNetInit()");
RsNetNative.RSNetInit();
if (_options.Encryption is { } enc)
{
_logger.LogInformation("Calling RSNetSetEncription({Value})", enc);
RsNetNative.RSNetSetEncription(enc);
}
_initialized = true;
}
finally
{
_initLock.Release();
}
}
public IRsNetSession Connect(DeviceOptions device)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (!_initialized)
throw new InvalidOperationException(
$"{nameof(RsNetClient)} is not initialized. Call InitializeAsync first.");
ArgumentException.ThrowIfNullOrWhiteSpace(device.Host);
ArgumentException.ThrowIfNullOrWhiteSpace(device.Username);
return RsNetSession.Open(_logger, device);
}
public async ValueTask DisposeAsync()
{
if (_disposed) return;
_disposed = true;
if (_initialized)
{
try
{
_logger.LogInformation("Calling RSNetRelease()");
RsNetNative.RSNetRelease();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "RSNetRelease threw during shutdown");
}
}
_initLock.Dispose();
await Task.CompletedTask;
}
}

View file

@ -0,0 +1,122 @@
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
using SurveillanceClient.Core.Configuration;
using SurveillanceClient.Core.Native;
namespace SurveillanceClient.Core.Services;
/// <summary>
/// Owns a single <c>RSNetConnectionStart</c> session handle plus the
/// unmanaged buffers (ANSI strings, request struct) that the DLL reads
/// from during the asynchronous login worker's lifetime.
/// </summary>
internal sealed class RsNetSession : IRsNetSession
{
private readonly ILogger _logger;
private readonly List<IntPtr> _unmanagedAllocs = [];
private readonly RsNetNative.RsNetStatusCallback? _callback;
private IntPtr _handle;
private bool _disposed;
public IntPtr Handle => _handle;
public event EventHandler<RsNetStatusEventArgs>? StatusChanged;
private RsNetSession(ILogger logger, RsNetNative.RsNetStatusCallback callback)
{
_logger = logger;
_callback = callback;
}
internal static RsNetSession Open(ILogger logger, DeviceOptions device)
{
RsNetSession? session = null;
RsNetNative.RsNetStatusCallback callback = null!;
try
{
// The callback MUST be held by a managed field so the GC does not
// collect the thunk while the native worker still holds the pointer.
session = new RsNetSession(logger, (uint code, IntPtr ctx) =>
{
logger.LogDebug("RSNet status callback: code={Code} ctx={Ctx}", code, ctx);
session?.RaiseStatus(code);
});
callback = session._callback!;
var hostPtr = MarshalAnsi(session, device.Host);
var userPtr = MarshalAnsi(session, device.Username);
var passPtr = MarshalAnsi(session, device.Password);
var request = new RsNetConnectRequest
{
AddressAnsi = hostPtr,
Port = device.Port,
Padding0 = 0,
UsernameAnsi = userPtr,
PasswordAnsi = passPtr,
// Fields 0x10..0x28 are mode/callback-related and not yet
// fully validated. Leaving them zero matches the minimal
// direct-IP login path observed in Surveillance_client.exe.
};
var requestPtr = Marshal.AllocHGlobal(Marshal.SizeOf<RsNetConnectRequest>());
session._unmanagedAllocs.Add(requestPtr);
Marshal.StructureToPtr(request, requestPtr, fDeleteOld: false);
logger.LogInformation(
"RSNetConnectionStart host={Host} port={Port} user={User}",
device.Host, device.Port, device.Username);
var handle = RsNetNative.RSNetConnectionStart(requestPtr);
if (handle == IntPtr.Zero)
throw new InvalidOperationException("RSNetConnectionStart returned NULL.");
session._handle = handle;
return session;
}
catch
{
session?.Dispose();
GC.KeepAlive(callback);
throw;
}
}
private void RaiseStatus(uint code)
{
try
{
StatusChanged?.Invoke(this, new RsNetStatusEventArgs(code));
}
catch (Exception ex)
{
_logger.LogError(ex, "Status handler threw for code {Code}", code);
}
}
private static IntPtr MarshalAnsi(RsNetSession session, string? value)
{
var ptr = Marshal.StringToHGlobalAnsi(value ?? string.Empty);
session._unmanagedAllocs.Add(ptr);
return ptr;
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
// The exported stop-session API has not been positively identified in
// the current RSNet.dll symbol recovery (see analysis doc). For now
// we simply release native buffers and let RSNetRelease() at shutdown
// tear down session state. Once the stop export is confirmed, call it
// here with _handle before freeing buffers.
foreach (var p in _unmanagedAllocs)
{
if (p != IntPtr.Zero) Marshal.FreeHGlobal(p);
}
_unmanagedAllocs.Clear();
// Keep the callback alive until after native cleanup would have run.
GC.KeepAlive(_callback);
}
}

View file

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>SurveillanceClient.Core</RootNamespace>
<AssemblyName>SurveillanceClient.Core</AssemblyName>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.0" />
</ItemGroup>
</Project>

View file

@ -1,234 +1,389 @@
# Surveillance Client DPI Analysis
# Surveillance Client Server And Recorded Video Download Analysis
Date: 2026-03-25
Date: 2026-04-17
Target binary: `Surveillance_client.exe`
Analysis method: Ghidra MCP against the already-imported and analyzed program database.
Analysis method: Ghidra MCP against the already-open executable, with non-destructive analysis only. During this pass, allowed writes were limited to function renames and comments.
## Objective
Determine how the client initializes its UI, whether it already contains any DPI-awareness logic, and what practical patch points exist for making the application usable on high-resolution displays.
Determine how the client connects to remote devices and how it searches for and downloads recorded video, with the practical goal of building a small standalone downloader that can fetch an arbitrary duration from an arbitrary camera.
## Executive Summary
- The binary is a Qt Widgets desktop application, and the available imports strongly suggest an older Qt 4-era codebase.
- No direct static use of Windows DPI-awareness APIs was found.
- No obvious embedded DPI manifest strings were found in the loaded program strings.
- The application persists window geometry and display layout state through `QSettings`, including explicit `QRect` geometry records.
- That persistence layer is likely to matter for any DPI patch, because old pixel-based geometry can become incorrect or visually tiny once DPI behavior changes.
- The EXE is mostly UI, orchestration, and request-shaping code.
- The real connection, record-query, playback-control, and download logic is implemented in `RSNET.DLL`.
- Local playback and file rendering are implemented in `RSPLAY.DLL`.
- The EXE contains a clear playback/download workflow that prepares request structures and hands them to `RSNET.DLL` exports.
- The current executable analysis is enough to map the high-level flow and the parameter shapes, but not enough to recover the on-the-wire protocol. For that, `RSNet.dll` needs to be loaded into Ghidra next.
## Confirmed Entry Flow
## Installed Module Layout
The startup chain currently identified is:
The installed client directory contains the key vendor modules:
1. `___tmainCRTStartup` at `0x00708d44`
2. `qt_startup_dispatch` at `0x00709b00`
3. `qWinMain` thunk at `0x00709c02`
4. Application main routine at `0x004c7260`
- `RSNet.dll`
- `RSPlay.dll`
- `QRSosAdapter.dll`
- `QtNetwork4.dll`
Notes:
This matches the import thunks present in the EXE.
- `qt_startup_dispatch` converts `GetCommandLineW()` output into a local 8-bit argv-style buffer and forwards execution into Qt startup.
- This is a useful early patch region if a future binary modification needs to happen before the first top-level window is created.
## Confirmed DLL Boundary
## Evidence This Is An Older Qt Widgets Application
The following named functions in the EXE are import thunks, not native implementations:
The import table and strings include multiple indicators that this is not a modern Qt high-DPI-aware build:
- `RSNetQueryParam` at `0x006e96f0`
- `RSNetQueryParamEx` at `0x006e9720`
- `RSNetAlarmSubscribe` at `0x006e99a0`
- `RSPlayGetPlayFileTimeRange` at `0x006ea760`
- `QApplication`
- `QPlastiqueStyle`
- `QProxyStyle`
- `QStyleOptionViewItemV4`
- `desktop`
- `screenGeometry`
- `availableGeometry`
- `primaryScreen`
Example:
Why that matters:
- `RSNetQueryParam` is just `JMP dword ptr [0x0076e648]`
- `QPlastiqueStyle` and `QStyleOptionViewItemV4` are strong Qt 4-era signals.
- A Qt 4-era app is much less likely to support automatic per-monitor DPI scaling cleanly.
- If DPI awareness is turned on without additional UI scaling work, the usual failure mode is a crisp but very small interface.
External symbol resolution in the loaded EXE confirms these libraries and exports:
## What Was Not Found
### `RSNET.DLL`
The following searches did not produce evidence of a built-in DPI implementation:
Relevant exports confirmed in the import table:
- `SetProcessDPIAware`
- `SetProcessDpiAwareness`
- `SetThreadDpiAwarenessContext`
- `WM_DPICHANGED`
- `devicePixelRatio`
- `HighDpi`
- `QT_AUTO_SCREEN_SCALE_FACTOR`
- `AA_EnableHighDpiScaling`
- `RSNetConnectionStart`
- `RSNetConnectionStartEx`
- `RSNetQueryRecordEx`
- `RSNetStartRecordPlayEx`
- `RSNetReqRecordPlayCtrol`
- `RSNetStartDownloadRecord`
- `RSNetStartDownloadRecordEx`
- `RSNetStopDownloadRecord`
- `RSNetReqRecordData`
- `RSNetStopRecordPlay`
- `RSNetReplayRecordData`
- `RSNetReposRecordData`
- `RSNetGetDevInfo`
- `RSNetSearchDev`
I also searched program strings for common manifest content such as:
### `RSPLAY.DLL`
- `dpiAware`
- `asmv3`
- `requestedExecutionLevel`
- `trustInfo`
Relevant playback exports confirmed in the import table:
None of those were found in the loaded strings.
## Resource Layout
The PE section layout includes a resource section:
- `.rsrc: 0x00dd7000 - 0x00df05ff`
Even with a resource section present, no obvious manifest or DPI-related XML strings surfaced through the Ghidra string search during this pass.
## Settings And Geometry Findings
Several settings-related functions were isolated and renamed in the Ghidra database for future work.
### `init_app_settings_defaults` at `0x0059ca30`
This function initializes many application settings keys and default values.
Relevant confirmed keys:
- `mainwindow/fullscreen`
- `mainwindow/size`
- `window_status`
- `gen/liveviewstatus`
- `gen/autolive`
- `screenAutoSwitch/interval`
- `screenAutoSwitch/forceSync`
- `screenAutoSwitch/isOn`
Relevant confirmed defaults:
- `mainwindow/size = 0x400 x 0x300` which is `1024 x 768`
- `mainwindow/fullscreen = false`
- `RSPlayCreatePlayInstance`
- `RSPlaySetPlayWnd`
- `RSPlayStartPlay`
- `RSPlaySetCallbackMessageEx`
- `RSPlayImageCallback`
- `RSPlayOpenPlayFile`
- `RSPlayOpenPlayFileListEx`
- `RSPlayGetPlayFileTimeRange`
- `RSPlaySetCurPlayedTimeEx`
- `RSPlayInputNetFrame`
- `RSPlayStartlocalrecord`
- `RSPlayStoplocalrecord`
Interpretation:
- The main window has a fixed persisted default size in pixels.
- That is a strong warning sign for DPI patching, because a legacy saved size can remain visually small after changing DPI behavior.
- `RSNET.DLL` is the module that matters for device login, remote record queries, remote playback, and remote download.
- `RSPLAY.DLL` is mostly decode/render/local playback support, not the primary network protocol implementation.
### `load_window_status_from_settings` at `0x005a72b0`
## Confirmed EXE-Side Flow
This function reads a `QSettings` array named `window_status` and loads nested UI state.
### UI widgets involved
Confirmed per-window fields:
The strings and handlers in the EXE show a playback and file-download workflow:
- `windowGeometry`
- `isCurWindow`
- `isFullScreen`
- `isMainWindow`
- `maxTabIndex`
- `Widget_Playback`
- `Widget_Playback_FileDownload`
- `Download By Date`
- `Download By Files`
- `DownLoad Start Time`
- `MainStream`
- `SubStream`
- `playback-download`
- `QRSAdapterReqRecordPlayCtrol`
Confirmed nested view/screen fields:
Relevant strings also show signals/slots for:
- `view_status`
- `mainview_status`
- `screenIndex`
- `screenBeginIndex`
- `isScreenEnlarge`
- `autoSwitchInterval`
- `autoSwitchIndependent`
- `autoSwitchIsOn`
- `stream_status`
- `groupChannelID`
- `videoIndex`
- `isCurSelected`
- `isSoundOn`
- `Emap_status`
- `emapIDIndex`
- `searchRequest(QString)`
- `playbackResponse()`
- `fileDownload()`
- `download()`
- `stopdownload()`
### Functions renamed during analysis
The following EXE functions were renamed in the Ghidra database:
- `0x004a9690` -> `PlaybackFileDownloadWidget_connectSignals`
- `0x0049f570` -> `RecordPlaybackControl_toggleMode`
- `0x004afb30` -> `PlaybackFileDownloadWidget_download`
- `0x004b1be0` -> `PlaybackFileDownloadWidget_downloadByTime`
These names reflect verified behavior from decompilation.
## What The Download UI Does
### `PlaybackFileDownloadWidget_connectSignals`
This function wires the file-download UI:
- download button -> `download()`
- stop button -> `stopdownload()`
- cancel button -> `cancel()`
- date/time selector changes -> `GetFileNumAndSize(QDateTime)`
That establishes the UI entrypoint cleanly.
### `PlaybackFileDownloadWidget_download`
This is the main dispatcher for the download dialog.
It has two modes:
1. File-based download mode
2. Time-range download mode
In file-based mode it:
- iterates rows in the record table
- checks selected rows
- tracks row state in a per-row metadata structure
- starts a timer to process queued download items
In time-range mode it:
- reads the selected start and end `QDateTime` values from the UI
- iterates available remote record segments
- compares the requested interval against each segment
- computes overlap windows
- schedules one or more download requests accordingly
The function logs several debug markers:
- `DownloadByTime 111111111111111111`
- `DownloadByTime 222222222222222222`
- `DownloadByTime 33333333333333333333`
- `DownloadByTime 444444444444444444444`
Those correspond to different interval-overlap cases between the selected range and a returned recording segment.
### `PlaybackFileDownloadWidget_downloadByTime`
This function handles the actual by-time iteration across remote record segments.
It reconstructs begin/end timestamps from record metadata, converts them to `QDateTime`, compares them against the user-selected range, then dispatches a lower-level download request for the relevant slice.
The logic strongly suggests the client does not ask the device for an arbitrary time range in one step unless the selected window aligns with a single record segment. Instead, it walks matching segments and issues one or more segment-bounded download requests.
Implication for a standalone tool:
- arbitrary time-range download likely requires:
- querying the device for record segments first
- intersecting those with the requested interval
- issuing one or more download requests
- stitching or sequencing the resulting data if needed
## EXE-Side RSNET Wrapper Layer
The EXE contains thin wrappers around the `RSNET.DLL` APIs. These wrappers are useful because they reveal request structure shapes and callback wiring even though the real protocol lives in the DLL.
### Connection wrapper
`FUN_006eaf70`
Behavior:
- copies fields from a caller-supplied struct into a local 0x38-byte request block
- stores two callback/context values into the owning object
- calls `RSNetConnectionStartEx(&local_38)`
Interpretation:
- The application stores a substantial amount of layout state in settings.
- `windowGeometry` is especially important because it appears to be persisted as a `QRect`.
- If the process DPI mode changes, previously stored geometry may need migration, scaling, deletion, or selective reset.
- connection/login is performed through `RSNetConnectionStartEx`
- the caller passes a structured login/connect request, not loose scalar parameters
- the request likely includes address or P2P information, credentials, and callback context
### `save_window_status_to_settings` at `0x005a6170`
### Remote record query wrapper
This function writes back the same structures into `QSettings`.
`FUN_006eb6f0`
Behavior:
- allocates a small 8-byte helper object from fields `param_2[0x12]` and `param_2[0x11]`
- copies roughly 0x60 bytes of request data into a local buffer
- assigns completion callbacks
- calls `RSNetQueryRecordEx(handle, &local_64)`
Interpretation:
- Even if a DPI hack works visually at first launch, the application can overwrite the new layout with old pixel-based assumptions on exit.
- Any durable solution should consider both the load and save side of geometry persistence.
- remote record search is performed with `RSNetQueryRecordEx`
- the query request is non-trivial and includes multiple fields beyond just channel and time
- two tail fields are treated specially and likely represent callback target/context or an auxiliary selection object
### `apply_mainwindow_fullscreen_setting` at `0x0047c1f0`
### Remote playback wrapper
This function compares a key against `mainwindow/fullscreen` and activates the matching `QAction` when the value is true.
`FUN_006e9e00`
Behavior:
- creates an `RSPlay` instance via `RSPlayCreatePlayInstance(1)`
- optionally binds the playback window handle via `RSPlaySetPlayWnd`
- starts local rendering via `RSPlayStartPlay`
- registers callback handlers with `RSPlaySetCallbackMessageEx` and `RSPlayImageCallback`
- fills a request block from the provided playback parameters
- calls `RSNetStartRecordPlayEx(connectionHandle, &local_40)`
Interpretation:
- The UI state is driven through Qt actions, not direct Win32 window management.
- That reinforces the view that DPI fixes will likely need to be applied at Qt startup or Qt widget/layout level rather than through a narrow Win32 patch alone.
- remote recorded playback is a network operation through `RSNET.DLL`
- incoming data is then fed into an `RSPLAY.DLL` decode/render pipeline
- the standalone downloader probably does not need the `RSPLAY` path unless raw playback streaming is required instead of file download
## Implications For A DPI Fix
### Download wrappers
### Most likely current problem model
`FUN_006e9810`
Based on the evidence, the binary looks like a legacy Qt Widgets application with no obvious modern DPI-awareness implementation. That creates two realistic scenarios:
Behavior:
1. The process is effectively DPI-unaware and Windows bitmap scaling is not being applied in the way the user wants.
2. The process is being made DPI-aware somewhere outside the obvious static import path, causing the UI to render at legacy pixel sizes and appear tiny.
- allocates a 12-byte state object
- copies a compact request from the caller
- sets a callback function `FUN_006e8690`
- calls `RSNetStartDownloadRecord(handle, &local_14)`
This analysis did not prove which of those two runtime cases is active on the target system. It did establish that the executable itself does not appear to contain a straightforward built-in high-DPI implementation.
`FUN_006e98b0`
### Why a simple `SetProcessDPIAware` patch is risky
Behavior:
For an older Qt Widgets app:
- allocates a 12-byte state object
- copies a slightly larger request including an extra byte field
- sets callback `FUN_006e8730`
- calls `RSNetStartDownloadRecordEx(handle, &local_20)`
- enabling DPI awareness alone often produces a sharp but tiny UI
- stored `QRect` geometry may restore undersized windows
- icon sizes, style metrics, and custom-painted controls may remain pixel-based
- there is no evidence yet of `WM_DPICHANGED` handling for per-monitor updates
`FUN_006e9980`
### What a workable solution will probably require
Behavior:
At minimum, a successful patch likely needs one of these paths:
- calls `RSNetStopDownloadRecord(downloadHandle)`
- frees the associated state object
1. A compatibility-style solution that lets Windows scale the app acceptably.
2. An early startup patch plus application-wide Qt scaling adjustments before top-level widgets are shown.
3. A deeper patch that also normalizes saved geometry and possibly icon/font/style metrics.
Interpretation:
## Ghidra Database Changes Made During Analysis
- there are at least two remote download entrypoints: base and extended
- the extended form includes one extra byte field that may be stream type, record type, or a mode selector
- both return or populate a download handle that is later passed to `RSNetStopDownloadRecord`
The following functions were renamed in the active program database:
### Record data callback path
- `0x00709b00` -> `qt_startup_dispatch`
- `0x0059ca30` -> `init_app_settings_defaults`
- `0x005a72b0` -> `load_window_status_from_settings`
- `0x005a6170` -> `save_window_status_to_settings`
- `0x0047c1f0` -> `apply_mainwindow_fullscreen_setting`
`FUN_006e85b0`
Decompiler comments were also added at those entrypoints summarizing their purpose.
Behavior:
## Current Confidence Level
- if callback state is valid and `param_1 == 1`, it calls `RSNetReqRecordData(*param_2)`
- otherwise it posts a Qt event with type `0x3ed` to a target object
High confidence:
Interpretation:
- Qt-based GUI application
- older Qt lineage is likely
- no obvious static DPI-awareness implementation exists
- window geometry persistence is real and important
- remote playback/download is at least partly callback-driven
- one callback state value appears to be a target `QObject`
- `RSNetReqRecordData` is likely a pull/continue/read-next mechanism for record data transfer
Medium confidence:
### Record playback control
- the best durable fix will require more than a manifest-only change
- geometry migration/reset will likely be necessary for a clean result
`RecordPlaybackControl_toggleMode`
Low confidence without runtime testing:
Behavior:
- whether the best final mode is DPI-unaware with OS scaling, system-DPI-aware, or a custom Qt-side scaling patch
- toggles internal playback state based on UI mode values `0x10` through `0x13`
- logs `QRSAdapterReqRecordPlayCtrol`
- calls a helper that eventually dispatches `RSNetReqRecordPlayCtrol`
## Recommended Direction
Interpretation:
Treat this as a legacy-Qt DPI retrofit problem, not just a missing API call problem.
- pause/resume/seek/speed or similar playback controls go through `RSNetReqRecordPlayCtrol`
- this is playback control, not initial search or initial download setup
The next pass should focus on:
## Current Reconstructed Download Pipeline
- locating the `QApplication` construction site inside `0x004c7260`
- locating first top-level window creation/show
- tracing where `windowGeometry` is applied to the main window
- deciding whether to pursue an OS-scaling strategy or a real application-side scaling patch
From the EXE alone, the likely pipeline is:
1. Establish device/session connection through `RSNetConnectionStartEx`
2. Query available recordings with `RSNetQueryRecordEx`
3. In the UI, present file/segment rows to the user
4. For time-based download, intersect the requested interval against returned record segments
5. For each needed segment or sub-range, call either `RSNetStartDownloadRecord` or `RSNetStartDownloadRecordEx`
6. Receive progress/data/completion via callback functions and Qt events
7. Stop transfer with `RSNetStopDownloadRecord` when complete or canceled
## What This Means For A Standalone Downloader
At a high level, the minimum downloader architecture is likely:
1. Login/connect request builder
2. Record query request builder
3. Segment intersection logic for arbitrary user time ranges
4. Download request builder
5. Callback/event loop or polling layer for transfer progress and completion
The most important unresolved details are all inside `RSNet.dll`:
- exact login request structure
- whether connection uses direct IP, media port, P2P ID, or multiple modes under one API
- exact query-record structure fields
- exact download request structure fields
- callback message formats and result codes
- file format or chunk framing of downloaded data
- authentication handshake and any encryption or session-key behavior
## What Is Known Versus Unknown
### Known from EXE analysis
- The EXE does not appear to implement the wire protocol itself.
- `RSNET.DLL` is the primary networking and remote-record module.
- `RSPLAY.DLL` handles playback/render support.
- The UI supports both file-based and time-based record download.
- Arbitrary time-range download is implemented as interval intersection over returned recording segments.
- The download path uses `RSNetStartDownloadRecord` or `RSNetStartDownloadRecordEx`.
- Record search uses `RSNetQueryRecordEx`.
- Record playback uses `RSNetStartRecordPlayEx` and `RSNetReqRecordPlayCtrol`.
### Unknown until `RSNet.dll` is analyzed
- packet layout and transport protocol
- exact authentication mechanism
- exact request structure fields
- whether the client speaks directly to the device, uses P2P relay logic, or switches transport modes internally
- how downloaded bytes are framed and persisted to disk
## Recommended Next Step
Load `RSNet.dll` into Ghidra next.
That is the correct next target if the goal is to build an independent downloader rather than automate the existing EXE.
After `RSNet.dll` is loaded, the next analysis pass should focus on:
- `RSNetConnectionStartEx`
- `RSNetQueryRecordEx`
- `RSNetStartDownloadRecord`
- `RSNetStartDownloadRecordEx`
- `RSNetReqRecordData`
- `RSNetStopDownloadRecord`
- any credential, P2P, relay, encryption, or session-management helpers they call
## Credentials And Live Validation
Credentials are not required yet for the current static pass.
They will become useful later for one of these reasons:
- validating a reconstructed downloader against a real target
- comparing dynamic behavior with captured traffic
- understanding whether connection mode changes based on device type, P2P ID, or network reachability
If live testing becomes necessary, credentials should be stored in an untracked local file such as a new `.gitignore`d notes/config file inside the repository or workspace.
## Analysis State Notes
- Earlier in the session, a few function definitions were accidentally deleted by using the wrong MCP endpoint on the writable program. Those functions were restored immediately.
- A repository instruction file now explicitly forbids destructive Ghidra actions during analysis unless the user explicitly requests them.
- Current repository instruction file: `.github/copilot-instructions.md`

View file

@ -0,0 +1,911 @@
# RSNet Download Analysis
Date: 2026-04-17
Target binary: `RSNet.dll`
Related binaries:
- `Surveillance_client.exe`
- `RSPlay.dll`
Analysis method: Ghidra MCP against the loaded `RSNet.dll`, using read-only inspection plus function renames/comments only.
## Objective
Determine enough of the `RSNet.dll` API and behavior to build a simple script that can:
1. initialize the DLL
2. connect/login to a device
3. query recorded segments for a camera
4. request download of a chosen interval or file segment
5. wait for completion and retrieve the saved file
## Bottom Line
At this point, the DLL has been analyzed far enough to outline a realistic first-pass downloader script that calls `RSNet.dll` directly with `ctypes` or similar FFI.
What is established:
- `RSNet.dll` owns the real connection, login, record-query, playback, and record-download logic.
- `RSNet.dll` performs its own socket I/O and background worker management.
- the EXE is mainly a UI and adapter layer that fills structs and hands them to `RSNet.dll`
- the download worker inside `RSNet.dll` writes data locally through an internal record-file layer rather than requiring the caller to manually process frame packets
- a simple standalone script is plausible without reversing the entire wire protocol, because the DLL already encapsulates the transport
What is not fully nailed down yet:
- exact C struct definitions for all exported APIs
- the full callback signature set and all event codes
- exact output container selection logic between AVI, MP4, or the vendor private format
Even with those gaps, there is now enough to define the likely call sequence and the minimum reverse-engineering targets for a first prototype.
## Exported API Surface Relevant To Downloading
Confirmed exports in `RSNet.dll`:
- `RSNetInit` at `0x1001ae30`
- `RSNetSetEncription` at `0x1001b570`
- `RSNetConnectionStart` at `0x1001c350`
- `RSNetConnectionStartEx` at `0x1001c3a0`
- `RSNetQueryRecord` at `0x1001c640`
- `RSNetAsyncQueryRecord` at `0x1001c660`
- `RSNetQueryRecordEx` at `0x1001c680`
- `RSNetAsyncQueryRecordEx` at `0x1001c6a0`
- `RSNetAsyncQueryRecordStop` at `0x1001c6c0`
- `RSNetStartDownloadRecord` at `0x1001c780`
- `RSNetStartDownloadRecordEx` at `0x1001c810`
- `RSNetStartRecordPlay` at `0x1001c870`
- `RSNetStartRecordPlayEx` at `0x1001c9a0`
- `RSNetReqRecordData` at `0x1001ca00`
- `RSNetReqRecordPlayCtrol` at `0x1001ca40`
- `RSNetRelease` at `0x1001b4e0`
Interpretation:
- a standalone downloader should use the DLL exports directly
- there is no need to reproduce the underlying socket protocol if the goal is just to automate download on Windows where the vendor DLL is available
## Initialization
### `RSNetInit`
`RSNetInit`:
- initializes logging from `RSNet.ini`
- calls `WSAStartup(0x202, ...)`
- prepares global DLL state
Practical implication:
- any standalone script must call `RSNetInit()` before any connection or query function
### `RSNetSetEncription`
`RSNetSetEncription(value)` simply stores a global value.
Practical implication:
- there is some configurable encryption mode or toggle
- it may need to be set explicitly for some device families, but this has not yet been proven necessary for the basic flow
## Connection / Login
### Public entry points
- `RSNetConnectionStart`
- `RSNetConnectionStartEx`
Both functions:
- allocate a per-device/session object of size `0xe98`
- initialize internal locks, events, strings, and worker state
- copy caller-provided login parameters into the session object
- queue a background login/connection worker
- return a session handle pointer on success, or `NULL` on failure
### `RSNetConnectionStart`
This is the simpler path and likely the better first target for a prototype.
From decompilation, the input struct used by `RSNetConnectionStart` appears to have this rough layout:
```c
struct RSNET_CONNECT_REQ_BASE {
char *address_or_id; // offset 0x00
uint16_t port; // offset 0x04
char *username; // offset 0x08
char *password; // offset 0x0c
uint32_t field10; // offset 0x10
uint32_t field14; // offset 0x14
uint32_t field18; // offset 0x18
uint32_t field1c; // offset 0x1c
uint32_t field20; // offset 0x20
uint32_t field24; // offset 0x24
uint32_t field28; // offset 0x28
};
```
The DLL copies these into its internal session object as:
- destination string at session `+0x04`
- port at session `+0x20`
- username at session `+0x28`
- password at session `+0x44`
- several mode or callback related fields at `+0x68 .. +0x80`
### `RSNetConnectionStartEx`
The extended path supports more than direct IP login. Decompiled behavior shows:
- a connection mode field at request index `0x0b`
- if that mode is zero, the DLL treats the first string as the device address directly
- if non-zero, it stores the first string in an alternate field and forces the target address to `127.0.0.1`
- when mode is `1` or `10`, it copies an additional string from request index `0x0c`
Interpretation:
- `RSNetConnectionStartEx` likely supports P2P / relay / localhost-tunneled connection modes
- the simple downloader should avoid the `Ex` path initially unless the target system requires P2P rather than direct device access
### Login worker behavior
The session worker at `0x100074b0`:
- attempts login / session setup
- posts state changes through callback or message mechanisms
- sends heartbeats every ~5 seconds after login
- dispatches status notifications with integer codes
Observed status/event codes from the worker:
- `2` on one failure path
- `3` on disconnect/connection-lost path
- `0x65`
- `0x79`
- `0x7a`
Strings in the DLL map login-related status messages to names such as:
- `RSNetMsgLoginRequest`
- `RSNetMsgLoginSuccess`
- `RSNetMsgLoginUserLogined`
- `RSNetMsgLoginNoUserName`
- `RSNetMsgLoginPasswordError`
- `RSNetMsgLoginNoRight`
- `RSNetMsgOverMaxUser`
- `RSNetMsgLoginUserDisable`
- `RSNetMsgLoginFail`
Practical implication:
- a script should assume login is asynchronous
- a callback or polling/wait mechanism is required before issuing record queries
## Record Query
### Public entry points
- `RSNetQueryRecord`
- `RSNetQueryRecordEx`
The simpler function is:
```c
RSNetQueryRecord(session, query_req)
```
which routes into `FUN_10005ca0`.
The extended function is:
```c
RSNetQueryRecordEx(session, query_req)
```
which routes into `FUN_10005eb0`.
### Behavior
Both paths:
- build a request message with message subtype `0x6f`
- send it through the session object
- wait up to 20 seconds for a response
- decode the returned payload into caller-facing record structures through callback helpers
The record-query dispatcher distinguishes at least two output styles using the first field of the query struct:
- `0x65`
- `0x66`
Those two cases use different callbacks:
- `FUN_100046b0`
- `FUN_10004750`
`FUN_100046b0` iterates a result array in units of `0x1c` bytes.
`FUN_10004750` iterates a result array in units of `0x0c` bytes.
Interpretation:
- one query result format is likely the full record segment list (`0x1c` bytes each)
- the other is likely a compact per-day/per-file summary format (`0x0c` bytes each)
For a downloader, the `0x1c` record entries are the important one because the EXE uses `0x1c`-byte records when building playback and download requests.
### Likely record entry format
The download worker consumes entries of size `0x1c` and treats them like timestamped record descriptors.
From earlier EXE analysis and DLL behavior, each `0x1c` entry likely contains:
- start date/time fields
- end date/time fields
- channel or stream selection data
- a record type flag or subtype
This matches how the EXE performed time-range intersection over returned recording segments.
## Download
### Public entry points
- `RSNetStartDownloadRecord`
- `RSNetStartDownloadRecordEx`
These are the core APIs for a standalone downloader.
### `RSNetStartDownloadRecord`
This is the simpler form.
The EXE-side wrapper showed this request shape:
```c
struct RSNET_DOWNLOAD_REQ_BASE {
uint32_t field00;
uint32_t field04;
uint32_t field08;
uint32_t callback_or_ctx;
uint32_t callback_user;
};
```
Inside `RSNet.dll`, `FUN_10010330` adapts this into the extended worker request by supplying defaults:
- count or mode fields are normalized
- an extra byte field is forced to zero
- a `local_14 = 1` flag is inserted before forwarding to the common worker builder
Interpretation:
- the base function is just a convenience wrapper around the extended worker
- for a script, `RSNetStartDownloadRecord` is probably easier to prototype first
### `RSNetStartDownloadRecordEx`
This is the common implementation target.
The decompiled request consumer `FUN_100103b0` proves the worker expects at least:
- two required non-zero fields at offsets `+0x04` and `+0x08`
- a count field at `+0x0c`
- an extra mode byte at `+0x10`
- a sequence of seven 32-bit values copied from offsets `+0x00 .. +0x18` into the worker object
Then it:
- allocates `count * 0x1c` bytes for one array
- allocates `count * 0x108` bytes for a second array
- copies source arrays from worker request indices `0x19` and `0x1a`
- queues a background worker thread beginning at `0x100105b0`
Interpretation:
- the extended request supports batch download of one or more `0x1c` record descriptors
- each record also has an associated `0x108` byte metadata block, likely including the destination path or output-file information
This is a strong indication that a standalone downloader can submit one selected record entry at a time rather than needing a large complex batch.
## Download Worker Behavior
The worker at `0x100105b0` is the core of remote file download.
Key verified behaviors:
- waits on session/login readiness
- opens a socket and configures timeouts
- sends a request with message subtype:
- `0x83` in one mode
- `0x196` in another mode
- copies one or more `0x1c` record entries into the outgoing message
- optionally handles an additional payload block depending on mode
- receives a response header and payload
- processes download status and progress messages
- writes or routes the resulting data into an internal record-file path
Important status/message strings in the DLL:
- `RSNetMsgDownloadRecordClosed`
- `RSNetMsgDownloadRecordStoreFail`
- `RSNetMsgDownloadRecordNoFile`
- `RSNetMsgDownloadRecordOK`
- `RSNetMsgDownloadRecordPercent`
- `RSNetMsgDownloadRecordFail`
Observed message codes in the worker:
- `0x132`
- `0x133`
- `0x142`
- `0x145`
Interpretation:
- the worker sends progress and completion back to the caller via callback/message dispatch
- success/failure is not just the return value of `RSNetStartDownloadRecord*`; the returned handle only means the worker was started
## Does The DLL Save The File Itself?
Yes, that appears to be the case.
Evidence:
- the download worker is large and manages long-running transfer state
- the DLL contains internal record-file classes:
- `.?AVCBaseRecordFile@@`
- `.?AVCRSAVIRecordFile@@`
- `.?AVCRSMP4RecordFile@@`
- `.?AVCRSPrivateRecordFile@@`
- the DLL also contains AVI-writing error strings
- cleanup paths include an object with a `FILE *` at offset `0x54` and a file/path buffer with small-string optimization behavior
Practical implication:
- the script most likely does not need to manually receive frame payloads and write them itself
- instead, it should provide the request/path metadata that the DLL expects, start the download, and wait for the worker to finish writing the file
## Callback And Handle Lifetime
### Session handle
The session handle returned by `RSNetConnectionStart*`:
- owns socket state
- runs a worker thread
- dispatches status via callback or window message
Important internal callback-related session fields:
- callback window/message at offsets around `+0x68` and `+0x6c`
- callback function at `+0x78`
- callback user/context at `+0x70`
### Download handle
The download handle returned by `RSNetStartDownloadRecord*` is a heap object with:
- session pointer at `+0x20`
- event handle at `+0x2c`
- worker state, copied record entries, and additional metadata
There is a tiny internal stop/cleanup wrapper around `0x1001ca81` that:
- validates the handle
- calls a stop routine
- destroys the download object via `FUN_1001cab0`
So a full script should ideally also locate or call the exported stop-download function when cleanup is needed. The original EXE import table proves that such an export exists, even though the current symbol recovery in this DLL session did not label it cleanly.
## Practical Downloader Strategy
The simplest viable approach is not to reverse the raw network protocol. It is to call the vendor DLL directly.
## C# Interop Feasibility
Yes. C# can load `RSNet.dll` as an unmanaged native library and call its exports directly.
The DLL is a good fit for `P/Invoke` or `NativeLibrary.GetExport`-based interop because the key exports are ordinary x86 functions, not COM interfaces or C++ object methods.
### ABI details confirmed from `RSNet.dll`
- architecture: `x86/little/32`
- pointer size: `4`
- compiler family: Visual Studio
- export names are undecorated in the import table used by the EXE
- the relevant exports use callee stack cleanup, which matches `StdCall`
Observed export epilogues:
- `RSNetInit` -> `RET`
- `RSNetRelease` -> `RET`
- `RSNetConnectionStart` -> `RET 0x4`
- `RSNetConnectionStartEx` -> `RET 0x4`
- `RSNetQueryRecord` -> `RET 0x8`
- `RSNetQueryRecordEx` -> `RET 0x8`
- `RSNetStartDownloadRecord` -> `RET 0x8`
- `RSNetStartDownloadRecordEx` -> `RET 0x8`
- `RSNetReqRecordData` -> `RET 0x4`
Practical implication:
- the first C# prototype should treat the exported functions as `CallingConvention.StdCall`
- the process must be 32-bit
### Runtime constraints for a C# host process
The managed host should be built and run as:
- `x86`, not `AnyCPU`
- Windows only
- from the vendor application directory, or with that directory added to the DLL search path
The safest deployment model is:
- place the C# test app in the same directory as `RSNet.dll`
- or launch it with the current directory set to `i:/Apps/Surveillance_client`
- or call `SetDllDirectory` / `AddDllDirectory` before loading `RSNet.dll`
Why that matters:
- `RSNet.dll` depends on the VC++ runtime and the rest of the vendor installation layout
- it also expects `RSNet.ini` nearby for logging/config initialization
- strings inside the DLL reference `RSP2PClient.exe` and `RSP2PDaemon.exe`, so P2P mode likely depends on those companion files being present
For direct-IP downloading, the minimum runtime context is likely smaller than the full client install, but the easiest first run is still to execute from the installed client folder.
## Recommended C# Loading Model
There are two realistic options.
### Option 1: Static `DllImport`
Use this once the calling convention and signatures are stable enough.
Advantages:
- simplest code
- easiest to read
- easiest to debug once structs are correct
Disadvantages:
- more brittle while struct layouts are still being discovered
### Option 2: `NativeLibrary.Load` plus delegates
Use this during the reverse-engineering phase.
Advantages:
- lets you experiment with signatures more safely
- avoids hard failure at process load if a dependency is missing
- makes it easy to log export resolution one function at a time
Disadvantages:
- more boilerplate
For the current stage, Option 2 is the better choice.
## C# Interop Types To Use
### Handles
Treat these as opaque `IntPtr` values:
- session handle returned by `RSNetConnectionStart*`
- download handle returned by `RSNetStartDownloadRecord*`
Do not model them as managed classes or attempt to dereference them directly in C#.
### Strings
Current evidence suggests the DLL uses ANSI `char *` strings, not UTF-16.
In C#:
- use `CharSet.Ansi`
- marshal strings as `LPStr`
- for uncertain layouts, prefer `IntPtr` plus `Marshal.StringToHGlobalAnsi`
### Struct packing
Use:
- `StructLayout(LayoutKind.Sequential, Pack = 4)`
Reason:
- this is a 32-bit Visual Studio binary using 4-byte pointers and ordinary 32-bit alignment
### Callbacks
Use delegates marked with:
```csharp
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
```
And keep the delegate alive for the entire native operation lifetime.
Practical rule:
- store callback delegates in fields, not local variables
Otherwise the GC may collect them while the native worker thread still holds the callback pointer.
## First-Pass C# API Shape
Based on the confirmed export behavior, a first interop layer can be modeled like this:
```csharp
internal static class RsNetNative
{
[DllImport("RSNet.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
internal static extern void RSNetInit();
[DllImport("RSNet.dll", CallingConvention = CallingConvention.StdCall)]
internal static extern void RSNetRelease();
[DllImport("RSNet.dll", CallingConvention = CallingConvention.StdCall)]
internal static extern void RSNetSetEncription(uint value);
[DllImport("RSNet.dll", CallingConvention = CallingConvention.StdCall)]
internal static extern IntPtr RSNetConnectionStart(ref RsNetConnectRequest request);
[DllImport("RSNet.dll", CallingConvention = CallingConvention.StdCall)]
internal static extern IntPtr RSNetConnectionStartEx(IntPtr request);
[DllImport("RSNet.dll", CallingConvention = CallingConvention.StdCall)]
internal static extern int RSNetQueryRecord(IntPtr session, IntPtr queryRequest);
[DllImport("RSNet.dll", CallingConvention = CallingConvention.StdCall)]
internal static extern int RSNetQueryRecordEx(IntPtr session, IntPtr queryRequest);
[DllImport("RSNet.dll", CallingConvention = CallingConvention.StdCall)]
internal static extern IntPtr RSNetStartDownloadRecord(IntPtr session, IntPtr downloadRequest);
[DllImport("RSNet.dll", CallingConvention = CallingConvention.StdCall)]
internal static extern IntPtr RSNetStartDownloadRecordEx(IntPtr session, IntPtr downloadRequest);
[DllImport("RSNet.dll", CallingConvention = CallingConvention.StdCall)]
internal static extern void RSNetReqRecordData(IntPtr recordPlayHandle);
}
```
Notes:
- `RSNetConnectionStart` is the first function worth strongly typing
- `RSNetConnectionStartEx`, `RSNetQueryRecord*`, and `RSNetStartDownloadRecord*` should initially stay as `IntPtr` request pointers until the layouts are fully proven
- this reduces the chance of silent marshaling mistakes during the first prototype
## First Strongly-Typed C# Struct Worth Trying
The current best candidate for a partial typed struct is the direct-IP login request used by `RSNetConnectionStart`.
Best current guess:
```csharp
[StructLayout(LayoutKind.Sequential, Pack = 4)]
internal struct RsNetConnectRequest
{
public IntPtr AddressAnsi;
public ushort Port;
public ushort Padding0;
public IntPtr UsernameAnsi;
public IntPtr PasswordAnsi;
public uint Field10;
public uint Field14;
public uint Field18;
public uint Field1C;
public uint Field20;
public uint Field24;
public uint Field28;
}
```
Why this is reasonable:
- the decompiled function copies a pointer at offset `0x00`
- a 16-bit port value is read from offset `0x04`
- string pointers are copied from offsets `0x08` and `0x0c`
- the remaining fields are treated as 32-bit values or callback-related fields
This struct should still be treated as provisional.
## Likely C# Callback Shape
The session object stores either:
- a callback window handle and message ID
- or a callback function pointer plus a user/context value
The worker invokes the function callback in patterns like:
- `callback(code, userContext)`
- or posts a window message with `PostMessageA(hwnd, msgId, code, userContext)`
Best current guess for the login/status callback delegate:
```csharp
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
internal delegate void RsNetStatusCallback(uint code, IntPtr userContext);
```
This is not fully proven, but it matches the currently observed call sites better than any multi-parameter alternative.
## Why C# Is A Better Fit Than Raw Packet Reimplementation
For the current objective, C# plus unmanaged interop is likely better than reimplementing the network protocol because:
- the vendor DLL already knows the login handshake
- the vendor DLL already knows P2P versus direct-IP behavior
- the vendor DLL already manages record-query formatting
- the vendor DLL already performs the actual download worker loop
- the vendor DLL appears to write the file itself
That cuts the problem down to:
- loading the DLL safely
- building the request structs correctly
- handling callbacks/events
- waiting for the output file to appear and complete
## Recommended C# Downloader Architecture
### Layer 1: Native bootstrap
Responsibilities:
- locate the vendor install directory
- set DLL search path
- call `RSNetInit`
- optionally call `RSNetSetEncription`
### Layer 2: Session wrapper
Responsibilities:
- allocate unmanaged ANSI strings for device address, username, password
- build the login request
- call `RSNetConnectionStart`
- keep the callback delegate alive
- expose a `Task` or `ManualResetEventSlim` for login completion
### Layer 3: Record query wrapper
Responsibilities:
- build an unmanaged query struct
- invoke `RSNetQueryRecord`
- collect returned `0x1c` entries into managed record models
- expose them to higher-level range selection code
### Layer 4: Download wrapper
Responsibilities:
- choose the target record entry or entries
- build the native download request and per-item metadata
- call `RSNetStartDownloadRecord`
- monitor status/progress callbacks
- resolve the final output file path
### Layer 5: Cleanup
Responsibilities:
- call the native stop/cleanup entrypoints when known
- free all unmanaged strings and unmanaged buffers
- call `RSNetRelease` on process shutdown
### What a minimal C# prototype should look like
The first prototype should not attempt the whole workflow at once.
Do it in stages:
1. Load `RSNet.dll` successfully from C# in an `x86` process.
2. Call `RSNetInit()` and verify it does not crash.
3. Call `RSNetConnectionStart(...)` with a direct-IP request and log callback codes.
4. Confirm which callback code means login success.
5. Add `RSNetQueryRecord(...)` and dump raw `0x1c` entries.
6. Only then attempt `RSNetStartDownloadRecord(...)`.
That staged approach is important because the unstable part is the native struct layout, not the export resolution.
## Risks Specific To C# Interop
The main failure modes are:
- running as `AnyCPU` or `x64` and failing to load the 32-bit DLL
- incorrect calling convention
- incorrect struct packing
- wrong ANSI versus Unicode marshaling
- GC moving or collecting delegates or unmanaged buffers too early
- freeing unmanaged strings before the async worker is finished using them
- assuming login is synchronous when it is not
Operational rule:
- keep every unmanaged buffer and every callback delegate alive until the native session or download handle has definitively finished
## Implementation Checklist For A C# Version
Before coding:
- confirm the C# host runs as `x86`
- run from the vendor install folder or configure DLL search paths explicitly
- keep a copy of the vendor folder structure intact for first tests
During interop setup:
- use `StdCall`
- use `CharSet.Ansi`
- use `Pack = 4`
- represent unknown native structs as `IntPtr` first
- keep callback delegates pinned by strong references
During login:
- prefer `RSNetConnectionStart` over `RSNetConnectionStartEx`
- use direct IP and media port first
- do not assume immediate success after the function returns a non-null session handle
During record query:
- expect asynchronous or callback-driven result delivery
- log raw bytes for each `0x1c` record entry before trying to reinterpret every field semantically
During download:
- start with one record entry, not a batch
- expect completion via status event rather than via the return value alone
- inspect the output directory for the file the DLL creates
## What Still Needs To Be Recovered For A Clean C# Implementation
The unresolved items are the same as for Python FFI, but C# can tolerate them if opaque pointers are used first.
The most important remaining tasks are:
1. exact `RSNetConnectionStart` request struct validation
2. exact query-request struct layout
3. exact query callback prototype
4. exact download-request struct layout
5. exact per-item `0x108` metadata layout
6. exact stop-download export signature
7. exact mapping from numeric event codes to login/query/download states
## Revised Assessment
Yes, a C# implementation is realistic.
It is probably one of the better options if the end goal is a Windows-only utility that uses the vendor binary as-is.
The current blocker is not `can C# do it?` but `how much of the request structs do we want to strongly type before writing the prototype?`
At this point, a cautious C# prototype is feasible if it:
- uses `x86`
- calls the DLL with `StdCall`
- uses direct IP login first
- treats most request blobs as unmanaged buffers until the layouts are fully validated
### Recommended first prototype flow
1. Load `RSNet.dll` with `ctypes.WinDLL`
2. Call `RSNetInit()`
3. Optionally call `RSNetSetEncription(...)` if required by the target device family
4. Build a direct-IP login struct for `RSNetConnectionStart`
5. Wait until the async login callback reports success
6. Build a record-query request for `RSNetQueryRecord`
7. Collect returned `0x1c` record entries
8. Pick the entries intersecting the user-requested time range
9. Build a single-entry download request and call `RSNetStartDownloadRecord`
10. Wait for `RSNetMsgDownloadRecordOK` or a terminal failure event
11. Locate the output file written by the DLL
12. Release/cleanup handles
### Why direct-IP first
The `Ex` login path clearly has extra logic for P2P or relay modes.
To keep the prototype small:
- use direct device IP / media port first
- defer P2P-only targets until after the basic downloader works
## Best Current Guess At Minimal Script Design
### Language
Python with `ctypes` is the fastest path.
C# is now a credible alternative for a Windows-only implementation that loads the vendor DLL directly.
### Components
1. DLL loader and function prototypes
2. callback function declared with `ctypes.WINFUNCTYPE`
3. login request struct for `RSNetConnectionStart`
4. query request struct for `RSNetQueryRecord`
5. query result callback handling `0x1c` entries
6. download request struct for `RSNetStartDownloadRecord`
7. event loop or polling wait for download completion
### Important caveat
The exact `ctypes.Structure` layouts are not fully proven yet.
So the first implementation should be treated as an iterative prototype, not a guaranteed drop-in script.
Still, the reverse engineering so far is enough to support the following plan:
- prototype direct login with `RSNetConnectionStart`
- verify callback codes for login success and failure
- prototype `RSNetQueryRecord` with a callback that logs each `0x1c` result entry
- once one real query works, use a single returned record entry as input to `RSNetStartDownloadRecord`
That is now a tractable task.
## Gaps That Still Matter Before Writing The Actual Script
These are the highest-value unresolved items:
1. Exact layout of the `RSNetConnectionStart` input struct
2. Exact layout of the `RSNetQueryRecord` request struct and callback prototype
3. Exact layout of the `RSNetStartDownloadRecord` request struct, especially the destination-file metadata
4. Exact numeric callback codes corresponding to:
- login success
- login failure types
- query complete
- download progress
- download success
- download failure
5. Exact output file naming and format-selection rules
## Recommended Next Reverse-Engineering Steps
The next pass should focus on making the prototype script concrete:
1. Rename key `RSNet.dll` internals:
- `FUN_100050a0`
- `FUN_100051f0`
- `FUN_10005ca0`
- `FUN_10005eb0`
- `FUN_10010330`
- `FUN_100103b0`
- `FUN_100105b0`
- `FUN_100074b0`
2. Recover the exact login callback prototype from the session object fields around `+0x68`, `+0x6c`, `+0x70`, and `+0x78`
3. Recover the exact query callback prototype used by result handlers `FUN_100046b0` and `FUN_10004750`
4. Identify the structure at size `0x108` associated with each download item
5. Identify where the output file path is copied into the download-item metadata
6. Validate the call sequence against a real device using credentials
## Current Assessment
The original objective was to analyze until it became possible to figure out how to make a simple script that downloads a video file.
That threshold has been reached in architectural terms:
- use `RSNet.dll` directly
- direct-IP login first
- query recordings first
- submit one record entry into the download API
- let the DLL write the file locally
- monitor callback/status events for completion
The remaining work is no longer “what is the path?” but “what are the exact struct layouts?”
That is a much narrower problem and should be solvable in a follow-up pass, especially with live credentials or one round of prototype-and-adjust testing.