For anyone maintaining JVNC or writing a compatible client. Start with Architecture & Code Map for the hot paths and the feature framework; this page is the wire-level reference.
| Project | Contents |
|---|---|
JVNC.Core |
Protocol shared by both apps: Rfb.cs (constants, big-endian helpers), RfbPixelFormat.cs, FileTransfer.cs, Security.cs (pre-handshake, TOTP, certificates, allow list, lockout, address classes), Dpapi.cs, JsonSettings.cs, AppLog.cs. No UI, no Win32 except DPAPI. |
JVNC.Server |
WPF tray app. RfbServer.cs (listener, per-client reader task + update task, security handshake), ScreenCapture.cs (GDI BitBlt + cursor compositing), InputInjector.cs (SetCursorPos + SendInput), MainWindow, SecurityWindow. |
JVNC.Client |
WPF viewer. RfbClient.cs (handshake, receive loop decoding into a client-side framebuffer, senders), MainWindow (painting, input coalescing, full screen), WinKeyHook.cs, KeySym.cs, SecurityWindow. |
Installer/JVNC.Installer |
Self-contained WPF setup program; the app files are embedded at build time. |
docs/ |
These manuals (Markdown source; render.py produces HTML). |
Build: dotnet build JVNC.sln -c Release. Package: package.ps1. Installer: Installer\build-installer.ps1.
Target net8.0-windows, no NuGet packages. Version in Directory.Build.props.
Standard RFB 3.8: RFB 003.008\n exchange, security type None (1), ClientInit/ServerInit, then
normal messages. Encodings offered by the client: JVNC JPEG (private), CopyRect (1), Zlib (6) and Raw (0).
Any third-party viewer works (it gets CopyRect too if it advertises it).
The server always performs the standard RFB 003.008 version exchange, then offers security types:
250 (JVNC, private) always; 1 (None) when no layer is on; otherwise 19 (VeNCrypt) when TLS is
on or 2 (VNC Authentication) when it is off — the latter two only if Allow standard VNC viewers
is on and neither client certificates nor TOTP are required. After the chosen sub-handshake the server
sends the normal u32 SecurityResult, then ClientInit/ServerInit proceed.
X509Vnc (260) when a password is
required or X509None (259) otherwise; TLS with the server certificate; then, for X509Vnc, the
VNC Auth challenge inside TLS.Legacy: servers before 1.0.5 greeted with JVNC-000.01\n + flags before the RFB version; the
client still understands that greeting.
If TLS: both sides start a TLS 1.2/1.3 handshake on the raw socket (SslStream). The server
presents server.pfx; if bit1 is set it requests a client certificate and validates the
thumbprint against its approved list. The client validates the server by pinned thumbprint.
HMAC-SHA256(key =
PBKDF2-SHA256(password, salt, 100000, 32), msg = nonce); server → u32 status.HMAC-SHA256(key = UTF-8(6-digit code),
msg = nonce); server → u32 status (0 ok, 1 fail, then closes). Server accepts codes for the
current 30 s window ±1.Failures at any step count towards the per-address lockout (5 → 15 min ban). Before any of this the server applies the IP allow list and the public-address rule.
Message types 250–253, used only between JVNC client and server, both directions unless noted.
| Type | Name | Body |
|---|---|---|
| 250 | FileHeader | pad[3], u32 nameLen, name (UTF-8), u64 size |
| 251 | FileData | pad[3], u32 len, data (≤ 256 KB per message) |
| 252 | FileRequest (client → server) | pad[3] — ask the server user to pick a file to send |
| 253 | FileStatus (server → client) | pad[3], u32 len, UTF-8 text |
| 240 | Capabilities (both) | pad[3], u8 count, u8 types[] — extension types the sender can receive; client first, server replies |
| 241 | MonitorsRequest (client → server) | pad[3] |
| 245 | Monitors (server → client) | pad[3], u8 count, per monitor: i32 x, i32 y, u16 w, u16 h, u8 primary, u8 nameLen, name (framebuffer coordinates) |
| 242 | AudioEnable (client → server) | u8 enable, u8 quality (0 stereo 48 kHz, 1 mono 24 kHz), pad[1] |
| 246 | AudioFormat (server → client) | pad[3], u32 sampleRate, u8 channels, u8 codec (1 = IMA ADPCM), pad[2] |
| 243 | Privacy (both) | u8 on/state, pad[2] — client requests, server echoes the actual state |
| 244 | Chat (both) | pad[3], u32 len, UTF-8 text (≤ 64 KB) |
| 249 | HostInfo (server → client) | pad[3], u8 macCount, 6-byte MACs, u32 nameLen, name — for Wake-on-LAN |
| 248 | Clipboard (both) | pad[3], u32 len, UTF-8 text (≤ 1 MB). Server also accepts RFB ClientCutText from stock viewers. |
| 200 | PointerEx (client → server) | u8 kind (1 touch, 2 pen), u8 flags (1 down, 2 up, 4 barrel, 8 eraser, 16 in-range/hover), pad[1], u16 x, u16 y, u16 pressure (0..1024), u32 id. Injected with InjectTouchInput / InjectSyntheticPointerInput; mouse fallback. |
| 201 | PointerRel (client → server) | u8 buttons (RFB mask, wheel bits 3/4 pulse), i16 dx, i16 dy — relative mouse mode (MOUSEEVENTF_MOVE). |
| 202 | Focus (server → client) | pad[3], i32 x, i32 y, i32 w, i32 h — foreground window rect in framebuffer coordinates; sent on change and once after capabilities. |
| 203 | Ping (both) | pad[3], u32 seq, u64 stamp — the server echoes the message verbatim; the client computes RTT. |
| 204 | LinkCap (client → server) | pad[3], u32 bytesPerMs (0 = measured link) — per-session bandwidth limit used by the frame budget. |
| 254 | TransferCtl (both) | u8 sub, pad[2], u32 len, body. Sub 1 Meta (u8 kind 0 file/1 clipboard file/2 clipboard image (zlib CF_DIB)/3 folder entry, u8 last, pad[2], u64 offset, u32 batch, u32 relLen, relPath) describes the NEXT FileHeader; 2 Probe (u64 size, u32 nameLen, name) asks for a partial; 3 Have (u64 offset, name) answers; 4 Cancel (receiver → sender, empty); 5 Abort (sender → receiver, empty; partial kept as <name>.<size>.jvncpart); 6 Saved (receiver → sender: u32 len, UTF-8 local path of a kind-4 HotFolder capture). Kind 4 HotFolder = capture: saved with a stable name (overwritten) in the receiver's hot-folder save folder, path put on its clipboard. Peers without 254 get plain FileHeader/FileData only. |
| 247 | AudioData (server → client) | pad[3], u32 len, u32 sampleFrames, ADPCM nibbles (channels interleaved per frame, low nibble first; encoder/decoder state continues across messages) |
A file is a header followed by data messages until size bytes have arrived. Chunks are separate
messages so input and screen updates interleave with transfers.
EncodingJvncJpeg (0x4A564E43:
u32 len + baseline JPEG, WIC). When a frame's changed area is ≥ 12 % of the requested region (and
≥ 250k px) the server encodes that frame's rects as JPEG (q85, q60 above 2 Mpx) and marks the covered
64×64 tiles lossy. Every frame stamps a per-tile last-change time; lossy tiles untouched for 250 ms are
re-sent lossless (≤ 3 Mpx per clean-up frame), either piggy-backed on a small lossless frame or as a
clean-up frame while the screen is otherwise idle — so a moving video stays lossy while the rest of the
screen is exact. Change detection tolerates per-channel noise ≤ DiffTolerance (default 8) so HDR /
dithering flicker does not count as change (RowDiffers: exact SequenceEqual fast path, vectorised
tolerance compare only for rows that differ; a tile counts as changed once ≥ DiffMinBytes (12) bytes exceed it, so isolated sparkle is ignored but a caret is not).ScreenCapture.Capture() first asks DesktopDuplicator (DXGI Desktop Duplication,
Windows 8+). Per output: AcquireNextFrame(0); a new frame is CopyResourced to a staging texture, mapped
and copied into the next DIB buffer; outputs with no new frame are copied from the previous buffer; when no
output presented and the cursor is unchanged the call returns false and the update loop just polls again.
The first frame after (re)creation is a BitBlt baseline so unpresented outputs still have content. Any
failure (ACCESS_LOST on mode change / secure desktop / UAC, rotated display, DuplicateOutput refused) disposes
the duplicator and BitBlt serves until a retry (2-5 s) succeeds. The COM calls are raw vtable function
pointers (slots and IIDs from the Windows 10 SDK headers) - no NuGet, no COM wrappers. Why: GDI BitBlt on
Windows 11 reads the DWM desktop plane; when a browser/video is on a hardware overlay plane (MPO) BitBlt
returns the stale/half-updated content under it in alternating rows - the "tearing on every monitor" that
DwmFlush never reliably fixed. Turn it off with UseDesktopDuplication: false in server.json to compare.DetectScroll looks at every changed 64-px tile: a
fingerprint of its middle row is compared with the previous frame's rows up to 160 above/below (the last
accepted shift is tried first); a fingerprint hit is verified by an exact block compare. Matching
adjacent tiles with the same shift become one CopyRect (16 bytes on the wire) from (x, y+d); the rest
of the run stays a pixel rect. Sources must lie inside the requested region and be neither lossy nor
deferred (the client must hold them verbatim). One sign of shift per frame; copies are written before
pixel rects, top-down when content moved up and bottom-up when it moved down, so no copy reads a row a
previous copy in the same update overwrote. At most 48 tiles per frame pay the full search; the rest only
try the frame's shift, so a window drag (no vertical match) costs little. Client CopyWithin walks rows
in overlap-safe order.frame bytes / elapsed for frames ≥ 48 KB is the effective
throughput (Session.LinkBytesPerMs, EWMA, starts at 20 MB/s). Per frame it derives a byte budget
(80 ms of link time, clamped 192 KB..24 MB) and estimates the frame from measured bytes-per-pixel
(LosslessBpp, JpegBpp, EWMA over encoded rects). Over budget, SplitToBudget sorts the rects by
distance from the last pointer position, keeps as many as fit, and flags the others in DirtyTiles;
Diff treats dirty tiles as changed so they go out with the following requests even if the screen is
still. JPEG quality follows the link (85 above ~6 MB/s, 60 above ~2.5 MB/s, else 40) and a lossless
frame estimated at more than twice the budget is sent as JPEG instead. Full (non-incremental) frames are
never split. LinkCapBytesPerMs in server.json caps the measured rate for testing (0 = off).ZLibStream reads ahead and may leave
a rect's sync-flush marker unread until the next rect (ChunkSource is append-only for this
reason).RfbClient.Framebuffer on the receive thread and paints dirty rects on the
UI thread at Input priority, capped at ~40 paints/s.SetViewRegion); a single-monitor view
therefore costs only that monitor. A pending non-incremental request is never downgraded by a later
incremental one (needed when the region changes).AudioCapture (server) = WASAPI loopback of the default render endpoint via raw COM interop,
float/PCM mix → linear resample + downmix → 20 ms 16-bit blocks → IMA ADPCM (4:1) → MsgAudioData
batched to ~60 ms. WaveOutPlayer (client) = winmm waveOut with per-block headers; late blocks are dropped.SetCursorPos (absolute SendInput moves are held at monitor edges by
Windows 11's "ease cursor movement between displays") and uses SendInput for buttons/wheel.KEYEVENTF_UNICODE; while Ctrl/Alt/Win is held they are mapped to virtual keys via VkKeyScan
so hotkeys fire. Non-printable keys map through KeySymToVk, with scan codes and the extended
flag where required. On disconnect the server releases everything held.WinKeyHook (WH_KEYBOARD_LL) in the client forwards Win and Win+key chords and
swallows them locally while the client is foreground and the screen image has focus.Per client: a reader task parses messages and injects input immediately; an update task
waits for a pending FramebufferUpdateRequest and does capture/diff/encode/send; all socket writes go
through Session.WriteLock. File sends run as background tasks taking the write lock per chunk.
Dialogs on either side are shown asynchronously (Dispatcher.InvokeAsync); incoming file data streams
to a temp file until the user decides.
A headless stress client (see the session notes / Installer folder history) connects, sweeps the
pointer, optionally opens a self-dragging "churn" window and a fragmenting TCP proxy, and reports
fps and any receive-loop failure. Security layers are exercised by editing server.json (TLS,
client cert, TOTP secret, allow list) and running the harness with JVNC_TOTP / JVNC_TRUST
environment variables. Run the churn test for 60 s after any change to encoding or the receive loop.
Core constants + RfbServer.Encode + RfbClient.ReadRect.Rfb.cs, parse in both message loops,
document them here.JVNC.Server (except DPAPI in Core), UI out of Rfb* classes, and no NuGet.