Files

7.4 KiB

Open printer_driver_adapter.dll

A drop-in replacement for RustDesk's closed printer adapter, so remote printing works in custom-branded builds.

Why this exists

RustDesk's printer_driver_adapter.dll verifies the calling executable's Authenticode signature inside init() and refuses to run for anything not signed by RustDesk. Evidence from the shipped binary:

  • imports WinVerifyTrust (wintrust.dll), CertGetNameStringW, GetModuleFileNameW
  • links codesign-verify-rs, a crate whose only purpose is Authenticode checks
  • carries one hardcoded identity constant: PURSLANEHUABINGRUSTDESK followed by two SHA-256 digests. Purslane Ltd is RustDesk's company; Huabing is the founder.

An unsigned custom build fails with:

ERROR [src\server.rs:160] printer service init failed: Failed to init printer driver

which is the fn_init() != 0 branch. Signing with your own certificate does not help — the check is an allow-list of specific identities, not "is it signed".

Everything else in RustDesk's printing chain is open source and works. Only the capture shim is gated, and it is replaceable: this crate reimplements the same four-function ABI with no signature check.

The ABI

From src/server/printer_service.rs. Symbols are resolved by these exact undecorated names, so all exports are #[no_mangle] extern "C":

pub type Init        = fn(tag_name: *const i8) -> i32;   // 0 = success
pub type Uninit      = fn();
pub type GetPrnData  = fn(dur_mills: u32, data: *mut *mut i8, data_len: *mut u32);
pub type FreePrnData = fn(data: *mut i8);

Verified: this DLL's PE export table is byte-for-byte the same set of names as RustDesk's own (init, uninit, get_prn_data, free_prn_data).

How capture works

No custom print driver, therefore no WHQL signing:

  print job
      │
      ▼
  virtual printer  ──►  Local Port whose NAME IS A FILE PATH
   (signed driver)         %ProgramData%\<Tag>\printer-spool\job.prn
                                    │
                                    ▼
                        this adapter polls the directory,
                        opens each file with share_mode = 0
                        (fails while the spooler still holds it),
                        returns the bytes, deletes the file
                                    │
                                    ▼
                        printer_service::run()  ──►  on_printer_data()
                                    │
                                    ▼
                        existing open-source transport → controller prints

A Local Port whose name is a full file path makes the spooler write output straight to that file with no "save as" prompt. The exclusive-open test is how job completion is detected — no timers, no guessing.

init() clears the spool directory, so a stale job can never be replayed onto the next connection, and any file present afterwards is by definition new.

Build

cargo build --release

Output: target\release\printer_driver_adapter.dll (~140 KB, no dependencies — std only).

How the build wires it in

DVForge does all of this automatically for Windows builds — nothing here needs to be run by hand.

builder/orchestrator.py → _install_open_printer_adapter()
cargo-builds this crate and copies the DLL into the Flutter Release/ folder, overwriting the signature-checked one that _ensure_windows_printer_driver() downloaded. It runs before the MSI harvest and the portable packer, so the replacement ships inside both installers.
builder/customize.py → _apply_printer_port()
repoints the printer's port at the spool file. Two implementations exist and both are patched, because patching only one leaves the printer on the wrong port:
  • libs/remote_printer/src/lib.rs — Rust, used by --install-remote-printer and the in-app Settings button.
  • res/msi/CustomActions/RemotePrinter.cpp — a full C++ reimplementation used by the MSI's InstallPrinter custom action. This is the one that runs during a normal installer run.

Only the port changes. The printer keeps its name, and the driver stays RustDesk v4 Printer Driver.

It runs after _apply_appname(), since both files have the app name substituted into them.

Verifying a build

After installing, confirm the adapter loaded:

Get-ChildItem "C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\<App>\log" `
  -Recurse -Filter *.log | Select-String "printer service"

Expect printer service initialized. The old failure reads printer service init failed: Failed to init printer driver.

Confirm the port is the spool file, not a named port:

Get-Printer -Name "<App> Printer" | Select-Object Name,DriverName,PortName

PortName should be C:\ProgramData\<App>\printer-spool\job.prn.

Then connect from another machine, print on the controlled one, and check %ProgramData%\<App>\printer-spool\adapter.log for a captured line and the server log for Got prn data, data len:.

Which driver to render with

The controller prints received bytes via PrintXPSRawData() (src/platform/windows.rs:4261), so the payload must be a valid XPS package.

  • RustDesk v4 Printer Driver — already installed if you ever ran the app's printer install, and it declares XpsFormat=XPS (MS-XPS), which is exactly what the receiving end expects. Note its render filter is signed by Microsoft ("Windows Hardware Compatibility Publisher"), not RustDesk — the signature gate is only in the adapter, so this driver has no objection to who calls it. Best format match. Check redistribution terms before shipping it yourself.
  • Microsoft XPS Document Writer v4 — inbox on every Windows machine and unambiguously redistributable, but emits OpenXPS (.oxps), not MS-XPS. May need conversion before PrintXPSRawData accepts it.

Resolved 2026-08-19 by inspecting a real captured job. A test print through Microsoft XPS Document Writer v4 produced a valid OPC package whose root relationship is:

Type="http://schemas.openxps.org/oxps/v1.0/fixedrepresentation"

That is OpenXPS, not MS-XPS — confirming the mismatch. Use RustDesk v4 Printer Driver instead, which declares XpsFormat=XPS. If you must stay on the inbox driver, the fallback is converting OXPS to MS-XPS, or swapping send_raw_data_to_printer on the receiving end — which is your own code.

PrintXPSRawData writes diagnostics to C:\Windows\temp\test_rustdesk.log on failure; check there if a job arrives but does not print.

Status

Piece State
Four-function ABI, exports match upstream verified
Capture, completion detection, cleanup verified by test suite
Panic safety across the FFI boundary catch_unwind on every export
Loads and initialises inside the real server not yet run
Spooler renders a job into the file port verified — 37 KB job captured
End-to-end print to a remote machine not yet run
XPS vs OpenXPS payload resolved — inbox v4 driver emits OpenXPS, use RustDesk's driver

Notes

panic = "abort" is deliberately not set. A panic escaping a cdylib into the RustDesk service would take the whole service down, so every export wraps its body in catch_unwind instead.

Licensing: this is clean-room work against an ABI declared in RustDesk's own AGPL-3.0 source. It contains no RustDesk code and defeats no protection on their binary — it replaces it.