187 lines
7.4 KiB
Markdown
187 lines
7.4 KiB
Markdown
# 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"`:
|
|
|
|
```rust
|
|
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:
|
|
|
|
```powershell
|
|
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:
|
|
|
|
```powershell
|
|
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.
|