An open API service indexing awesome lists of open source software.

https://github.com/reactivecore/vnc-rdx


https://github.com/reactivecore/vnc-rdx

Last synced: about 1 month ago
JSON representation

Awesome Lists containing this project

README

          

# vncex

> **⚠ Beta — AI-assisted code, no warranty, unstable wire format**
>
> - This codebase was written with substantial AI assistance and has
> **not been exhaustively human-reviewed**. Treat it accordingly.
> - Provided **as-is, without warranty of any kind** — see
> [`LICENSE-MIT`](LICENSE-MIT) and [`LICENSE-APACHE`](LICENSE-APACHE).
> - The RDX wire protocol is **not stable** while in beta. Minor
> version bumps may break compatibility. If you depend on this
> crate, pin a specific git revision.

A VNC/RFB proxy pair that tunnels sessions over a custom framed
protocol called **RDX** (Remote Desktop eXchange). Two thin binaries
flank an RDX TCP link:

```
VNC viewer ── RFB ─▶ client ── RDX/TCP ─▶ server --source vnc ── RFB ─▶ VNC server
```

The `server` binary can also capture a local Linux desktop directly,
removing the upstream VNC server from the path:

```
server --source local ◀── (Wayland portal | X11 XTEST) ── local display

└── RDX/TCP ─▶ client ─▶ VNC viewer (or any other RDX peer)
```

A browser viewer is also supported via the optional WebSocket
endpoint on `server`; see "Browser viewer" below.

The **protocol is the first-class artifact**. The canonical wire-format
spec lives in [`docs/protocol.md`](docs/protocol.md); this crate is its
executable mirror. If the code disagrees with the spec, the code is
wrong.

## Crate layout

Dependencies flow **downward only** through these layers:

| Layer | Purpose |
|---|---|
| `bin/` | Two thin binary entry points: `client` (viewer side) and `server` (display-source side). |
| `client/` | Viewer-facing bridge: speaks RFB to a local VNC viewer and RDX to the peer `server`. Pure consumer — no encoders. |
| `server/` | Display-source bridges, with two flavours: `server::upstream_vnc` proxies a remote VNC server; `server::local` captures from a local Wayland/X11 backend in `server::backend`. The optional `server::ws_listen` HTTP/WebSocket endpoint works with either source. |
| `common/` | Shared library: `cache`, `change`, `session`, `rfb`, `transport`, `geom`, `config`, `error`, `metrics`, plus the bridge-side encoders (`framebuffer`, `tiler`, `video`, `auth`) and small `bridge_helpers`. Codec-agnostic; no binary-specific code. |
| `proto/` | Pure codec: frame header, message bodies, handshake. No I/O, no async (except the `tokio_util::codec` traits). Kept usable from a future wasm/browser client. |

Files aim for **≤ 200 lines of non-test code**. When a module grows
past that, it becomes a same-named directory with submodules organised
by protocol section (see `proto/message/` for the canonical example).

## The wire-codec contract

Every wire-format value implements one of two traits from
`proto::wire`:

```rust
/// A self-contained wire value that owns its full byte slice.
/// Implementors are message bodies from `docs/protocol.md` §4.
pub trait WireCodec: Sized {
fn encode(&self, out: &mut BytesMut);
fn decode(src: &[u8]) -> Result;
}

/// A fixed-layout header that precedes a variable-length body.
/// Decoding returns the tail so the caller can parse the body.
pub trait WireHeader: Sized {
fn encode(&self, out: &mut BytesMut);
fn decode(src: &[u8]) -> Result<(Self, &[u8])>;
}
```

Use `WireCodec` for message bodies (the common case). Use `WireHeader`
for fixed-layout preambles that front a variable payload, such as
`TileDataHeader`, `TileDeltaHeader`, and `rfb::PixelFormat`.

Do not re-invent `encode`/`decode` as inherent methods — if you are
adding a wire type, implement one of these traits so the contract
can't drift.

## Building and testing

```sh
cargo build # dev build (client + server stub backends)
cargo build --release
cargo build --no-default-features # drop zstd (pure-Rust fallback: RAW + JPEG)
cargo build --features websocket # browser RDX endpoint (see below)

# Native display backends for `server --source local` (Linux). Each
# pulls in its own deps: x11rb for X11, ashpd + pipewire + wl-copy /
# wl-paste for Wayland. Without these features the binary still
# builds but `--source local` falls back to a stub backend that
# refuses every connection.
cargo build --features x11 --bin server
cargo build --features wayland --bin server
cargo build --features "wayland x11" --bin server

cargo test # full unit + integration suite
cargo test --features websocket # adds tests/websocket_bridge.rs
cargo clippy --all-targets -- -D warnings # must be clean before commit
cargo fmt
cargo doc --no-deps # check rustdoc coverage

(cd web && npm ci && npm test) # JS viewer unit + replay tests
(cd web && npm run check) # tsc --noEmit --checkJs (JSDoc)
```

## End-to-end smoke test

```sh
# Terminal 1: server side, talking to a real VNC server.
server --source vnc --listen 127.0.0.1:7900 --connect :5900

# Terminal 2: client side, accepting a local VNC viewer.
client --listen 127.0.0.1:5901 --connect 127.0.0.1:7900
```

Then point a VNC viewer at `127.0.0.1:5901`.

### Manual testing `server --source local`

`--source local` replaces the upstream-VNC half of the smoke test
with a direct capture of the local desktop. Build with the
appropriate backend feature, then:

```sh
# Terminal 1, on the host whose desktop you want to share.
# Picks Wayland if WAYLAND_DISPLAY/XDG_SESSION_TYPE=wayland is set,
# else X11 if $DISPLAY is set. Override with --backend wayland|x11.
cargo run --features wayland --bin server -- --source local --listen 127.0.0.1:7900

# Terminal 2: the same client bridge as above, accepting a VNC viewer.
client --listen 127.0.0.1:5901 --connect 127.0.0.1:7900
```

Then point a VNC viewer at `127.0.0.1:5901`. On Wayland the portal
will pop a permission prompt the first time it runs.

Backend prerequisites:

- **X11 (`--features x11`):** `XTEST`, `XKB`, and `RANDR` extensions —
every modern X server has these. Set `DISPLAY` (and `XAUTHORITY`
when running over SSH). See
[`docs/x11-backend.md`](docs/x11-backend.md) for the capture /
input / clipboard design and the comparison vs the Wayland path.
- **Wayland (`--features wayland`):** an `xdg-desktop-portal`
implementation with the RemoteDesktop + ScreenCast portals (GNOME,
KDE, and most other desktop sessions ship one). The clipboard path
shells out to `wl-copy` / `wl-paste` from `wl-clipboard`; install
that package or expect clipboard sync to be a no-op. See
[`docs/wayland-backend.md`](docs/wayland-backend.md) for the
portal/PipeWire/GBM design and known upstream issues
(HiDPI fractional-scale pointer behaviour, keyboard-layout
dependence, and per-compositor caveats).

#### Password authentication

`server --source local` can require a password before letting a
viewer reach the session. When configured, the server advertises
`CAP_AUTH_FORWARDING` in the RDX HELLO and runs an RFB SecVNC
challenge through the existing AUTH_FRAME pipe before the session
is initialised. The viewer side needs no flag — `client`
auto-detects the cap bit and routes its local VNC viewer through
the auth pipe. (For `--source vnc`, the upstream VNC server's own
auth applies and `--password` on the proxy is ignored.)

Three knobs, in priority order (CLI > env > file):

```sh
# Highest priority: pass on the command line (visible in `ps`).
server --source local --listen 127.0.0.1:7900 --password hunter2

# Or via environment.
RDX_PASSWORD=hunter2 server --source local --listen 127.0.0.1:7900

# Or read from a file (newline-trimmed).
server --source local --listen 127.0.0.1:7900 --password-file ~/.server.pw
```

**Caveat:** SecVNC is DES — fine for non-public networks; pair
with an SSH tunnel if confidentiality matters. RFB caps the
password at 8 bytes; longer passwords are silently truncated
(matches every other VNC implementation).

## Browser viewer (WebSocket endpoint)

The `server` binary optionally serves a binary WebSocket alongside its
TCP RDX listener so a browser can speak RDX directly — no separate
`client` process needed on the consumer side. The viewer is a pure-ESM
JavaScript module under [`web/`](web/) and is embedded in the binary;
opening `http://server:port/` shows the demo page. Works with both
`--source vnc` and `--source local`.

Build with the feature flag and pass `--ws-listen`:

```sh
cargo build --release --features websocket
target/release/server --source vnc \
--listen 127.0.0.1:7900 \
--ws-listen 127.0.0.1:8080 \
--connect :5900
```

Now navigate to `http://127.0.0.1:8080/` in any modern browser.

The viewer decodes every pixel codec the bridge emits (`RAW_BGRA`,
`ZSTD_BGRA`, `JPEG_LOW`, `SOLID_COLOR`, `PALETTE_RAW`, `PALETTE_ZSTD`),
applies `TILE_DELTA`, and handles both aligned and sub-tile `TILE_COPY`.
It also negotiates the §9.6 video transport when the browser supports
it — a WebCodecs `VideoDecoder` (hardware-accelerated in practice;
HEVC / AV1 / VP9, probed via `isConfigSupported`) — which is the path
you get on macOS, where the bridge's default `HardwareOnly` video
policy yields HEVC. The `/ws` endpoint rejects cross-origin upgrades;
only bind `--ws-listen` to a trusted interface (loopback for local
previews).

On a fatal client-side error (e.g. a `VideoDecoder` failure) the viewer
ships a one-shot diagnostic bundle to the bridge over the `STATS`
channel — so the cause shows up in the bridge log without the browser
console. Pass `?diag=0` (or `sendDiagnostics: false` to `RdxClient`) to
disable the channel entirely.

The JS module type-checks via JSDoc + `// @ts-check` (no runtime
TypeScript dependency); CI runs `node --test` and `tsc --noEmit
--checkJs` against `web/`.

## Contributor rules

Contributor rules, project-wide invariants, and the "where state really
lives" map are in [`CLAUDE.md`](CLAUDE.md). Read that file before
touching the tile cache, stream reassembly, or the session state
machine — it documents non-obvious invariants that the type system
does not enforce on its own.

## Using as a library

`vncex` ships both a binary (the proxy CLI) and a library. The crate
is **not published to crates.io** during beta — depend on it as a git
dependency and pin a revision so a wire-format change in `main` can't
break your build:

```toml
[dependencies]
vncex = { git = "https://github.com/reactivecore/vnc-rdx", rev = "" }
```

The public surface is the set of modules re-exported from `lib.rs`:
`client`, `server`, `proto`, `common` (which itself re-exports
`cache`, `change`, `config`, `error`, `geom`, `metrics`, `rfb`,
`session`, `transport`, `framebuffer`, `tiler`, `video`, `auth`),
plus `Error` / `Result`. See `cargo doc --open` for the rendered API.

## License

Dual-licensed under either of

- **MIT License** ([`LICENSE-MIT`](LICENSE-MIT) or
https://opensource.org/licenses/MIT)
- **Apache License, Version 2.0** ([`LICENSE-APACHE`](LICENSE-APACHE) or
https://www.apache.org/licenses/LICENSE-2.0)

at your option. Copyright © 2026 Reactive Core GmbH.

Unless you explicitly state otherwise, any contribution intentionally
submitted for inclusion in this crate by you, as defined in the
Apache-2.0 license, shall be dual-licensed as above, without any
additional terms or conditions.