RemoteViewer.ConnectKit 0.9.2

dotnet add package RemoteViewer.ConnectKit --version 0.9.2
                    
NuGet\Install-Package RemoteViewer.ConnectKit -Version 0.9.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="RemoteViewer.ConnectKit" Version="0.9.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RemoteViewer.ConnectKit" Version="0.9.2" />
                    
Directory.Packages.props
<PackageReference Include="RemoteViewer.ConnectKit" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add RemoteViewer.ConnectKit --version 0.9.2
                    
#r "nuget: RemoteViewer.ConnectKit, 0.9.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package RemoteViewer.ConnectKit@0.9.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=RemoteViewer.ConnectKit&version=0.9.2
                    
Install as a Cake Addin
#tool nuget:?package=RemoteViewer.ConnectKit&version=0.9.2
                    
Install as a Cake Tool

ConnectKit

"Connect to my device, anywhere" — as an SDK.

ConnectKit extracts RemoteViewer v2's connection core — NAT traversal, an end-to-end-encrypted transport, and signaling — into a standalone, embeddable library. Instead of shipping a finished remote-access product, ConnectKit sells the hard part underneath it: getting an authenticated, encrypted data channel between two endpoints that may each sit behind NAT.

await using var ck = new ConnectKitClient(new ConnectKitOptions { Secret = "s3cr3t" });
await using var channel = await ck.ConnectAsync("192.168.0.42:5310");

channel.OnData += bytes => Console.WriteLine($"got {bytes.Length} bytes");
channel.Send(Encoding.UTF8.GetBytes("hello"));   // encrypted, delivered as one message

That is the whole runtime surface. What rides the channel — screen frames, files, sensor data, control — is entirely the integrator's concern. ConnectKit only guarantees the pipe.


Install

dotnet add package RemoteViewer.ConnectKit

The package id is RemoteViewer.ConnectKit; the namespace is ConnectKit (using ConnectKit;). It targets net8.0 and net9.0, and its only dependency is Bouncy Castle (for J-PAKE). ConnectKit is commercial software — see LICENSE.txt; installation requires accepting the license.

New here? Follow the 5-minute quickstarthttps://ck.remote-viewer.com/start.html (also shipped as GETTING-STARTED.md in this package).


Connecting across NAT

The one-liner above dials a host:port on the same LAN. To connect two endpoints that each sit behind NAT, point ConnectKit at a coordination server and dial by device name — ConnectKit then handles discovery, hole-punching, and the encrypted-relay fallbacks for you:

// The device — the side that waits to be reached
await using var ck = new ConnectKitClient(new ConnectKitOptions {
    Secret     = "<shared secret both peers know>",   // derives the E2E key (J-PAKE); must match on both sides
    Signaling  = "sig.ck.remote-viewer.com:1618",       // the coordination server (a DNS name is fine)
    Company    = "<your company id>",                  // your tenant on a shared/hosted server
    TenantKey  = "ck_...",                              // the key that authenticates that company
    DeviceName = "front-desk-pc",                       // the name peers dial
});
await using var channel = await ck.AcceptAsync();

// The client — the side that dials
await using var ck = new ConnectKitClient(new ConnectKitOptions {
    Secret    = "<shared secret both peers know>",
    Signaling = "<coordination-server host>:1618",
    Company   = "<your company id>",
    TenantKey = "ck_...",
});
await using var channel = await ck.ConnectAsync("front-desk-pc");
  • Secret derives the end-to-end key (J-PAKE password). Both peers must use the same value; a mismatch fails the connection rather than downgrading to plaintext. The server never sees it, nor your data. You choose it and share it between your two peers (e.g. a pairing code) — it is separate from TenantKey, which the operator issues to identify your company.
  • Signaling is the coordination server as host:1618. When set, a deviceId is a logical name (resolved via the server's roster) instead of a host:port.
  • Company / TenantKey identify and authenticate your tenant on a shared/hosted server, hard-isolated from other companies. Omit both when connecting to a single-tenant, self-hosted server.

ConnectKit tries a direct UDP/TCP hole-punch first, then a TURN-style relay, then a WSS gateway — all end-to-end encrypted — so the channel comes up even through symmetric NATs and restrictive firewalls, and the calling code never changes.

Getting access

Signaling, Company, and TenantKey come from whoever runs the coordination server:

  • Hosted — sign up at https://ck.remote-viewer.com: enter a company id and your email, confirm the emailed link, and you get your Company and TenantKey (with the exact settings to paste). The coordination-server host is sig.ck.remote-viewer.com:1618.
  • Self-hosted — run the ConnectKit.Server deployable on your own infrastructure (a single-tenant server needs no Company/TenantKey).

Status — v0.9 (release candidate for 1.0)

The connection and cryptography core is reused verbatim from RemoteViewer v2 (already shipping in production across Windows/Linux/macOS/Android/iOS), so it is well past a preview; the packaged public API is small and has had its review pass. The 0.x prefix reserves the right to adjust that API before the 1.0 stability commitment.

Area State
Public API (ConnectKitClient / IConnectChannel / options / events) ✅ done
End-to-end key exchange (J-PAKE, password-authenticated) ✅ reused from v2, verified
Encrypted data channel (AES-256-GCM per message) ✅ reused from v2, verified
Length-prefixed framing ✅ reused from v2
Transport: DirectTcp (LAN / loopback, no NAT) ✅ done
Transport: DirectUdp (reliable-UDP stack — for paths that pass UDP but block TCP) ✅ done, verified end to end
Transport: Introducer (discovery + TCP hole-punch) ✅ done, verified end to end
Transport: Introducer UDP hole-punch (reflexive discovery + reliable-UDP, Transport = Udp) ✅ done, verified end to end
Transport: Gateway (WSS relay — always reachable) ✅ done, verified end to end
Gateway fallback wired into the hole-punch path (blocked networks) ✅ done
Auto co-races TCP + UDP punch, converged by first-frame arbitration ✅ done, verified end to end
Fallback tier: UDP relay (TURN-style, for symmetric NAT), over the reliable-UDP stack ✅ done, verified end to end
Managed coordination server + self-service tenant portal + per-tenant usage metering ✅ done
Language bindings beyond .NET ⏳ later

Every transport runs through the same IConnectTransportIByteChannel seam, so the calling code is identical. Selection by options: Signaling set → hole-punch (with Gateway as its fallback if also set); Gateway only → pure WSS relay; neither → direct. The Transport option then picks the punch flavour: Tcp = TCP hole-punch, Udp = UDP hole-punch (reflexive discovery via the introducer's StunLite responder, candidate exchange over its UdpCand relay, NAT openers, then a session on the vendored reliable-ordered ARQ ReliableUdp — the same one that backs the direct-UDP path — so a datagram link satisfies the same reliable, in-order IByteChannel contract TCP does), and Auto co-races both and takes whichever connects, so a network that blocks TCP punching silently rides UDP instead. When a symmetric NAT defeats even the UDP punch, Auto falls further to a TURN-style UDP relay (or force it with Relay): the introducer mints an allocation token, both peers register (ALLOC) at the co-located relay, and the very same ReliableUdp session runs over the forwarder — reliable UDP even when no direct path exists. The gateway (WSS) stays the final always-reachable resort.

Co-racing separate connections needs care: the dialer commits to one and runs the handshake there, so the device must land on the same one. It does, by first-frame arbitration — the device accepts the inbound tier that first delivers a frame (the dialer's handshake round), discarding the tiers the dialer didn't use. Early frames survive the wait because every channel buffers through a shared FrameInbox.


Try it (from the source repository)

The commands below run the bundled sample project and assume you've cloned the source repo. If you just installed the package, the snippets above are all you need — write against ConnectKitClient in your own app. Everything from here on is for exploring or contributing to the source.

# Self-checking: runs both peers in one process over loopback and asserts the round trip.
dotnet run --project samples/ConnectKit.Sample -- demo

# The same, but the direct path runs over the reliable-UDP transport instead of TCP.
CK_TRANSPORT=udp dotnet run --project samples/ConnectKit.Sample -- demo

# Prove the reliable-UDP layer directly: ordered, byte-exact delivery under injected loss + reorder.
dotnet run --project samples/ConnectKit.Sample -- selftest

Two-process shape (two terminals — prefix both with CK_TRANSPORT=udp to run over UDP):

# terminal A — the device: listen and echo everything back, uppercased
dotnet run --project samples/ConnectKit.Sample -- device 5310 s3cr3t

# terminal B — the client: dial the device and exchange messages
dotnet run --project samples/ConnectKit.Sample -- connect 127.0.0.1:5310 s3cr3t

A mismatched secret is rejected on both sides — ConnectKit fails the connection rather than silently downgrading to plaintext.

Over an introducer (connect by name, across NAT)

Point both peers at an introducer with CK_SIGNALING; now a deviceId is a logical name, resolved via the introducer's roster, and the two peers reach each other by TCP hole-punching:

# terminal A — run an introducer (the RemoteViewer v2 one listens on :1618)

# terminal B — the device registers under a name
CK_SIGNALING=127.0.0.1:1618 dotnet run --project samples/ConnectKit.Sample -- device device1 s3cr3t

# terminal C — the client dials that name (no host:port)
CK_SIGNALING=127.0.0.1:1618 dotnet run --project samples/ConnectKit.Sample -- connect device1 s3cr3t

Set CK_DEBUG=1 to trace the register → introduce → punch sequence. The same ConnectAsync(name) call works over either transport — only the ConnectKitOptions.Signaling field differs.

Prefix both peers with CK_TRANSPORT=udp (i.e. ConnectKitOptions.Transport = Udp) to traverse by UDP hole-punch instead — for a network that passes UDP but blocks TCP. The peers discover their reflexive UDP address from the introducer's StunLite responder, exchange candidates over its UdpCand relay, punch, and run the encrypted channel over ReliableUdp. CK_DEBUG=1 shows the candidate exchange and the winning path. CK_TRANSPORT=relay forces the TURN-style UDP relay (for a symmetric NAT — both peers ALLOC at the introducer's co-located relay and ReliableUdp runs over the forwarder). Leave CK_TRANSPORT unset (Auto) to co-race TCP + UDP punch and fall through to the relay, then the gateway, as needed.

Over the WSS gateway (pure relay, no NAT traversal)

The gateway is both a standalone transport and the fallback for blocked networks. Both peers connect out to it and relay through a room derived from the group + device name — nothing else to coordinate:

# terminal A — run the gateway (the RemoteViewer v2 one listens on :8080, /ws, open in dev)

# terminal B — device
CK_GATEWAY=ws://127.0.0.1:8080/ws dotnet run --project samples/ConnectKit.Sample -- device device1 s3cr3t

# terminal C — client
CK_GATEWAY=ws://127.0.0.1:8080/ws dotnet run --project samples/ConnectKit.Sample -- connect device1 s3cr3t

Set both CK_SIGNALING and CK_GATEWAY to use hole-punch first with the gateway as an automatic fallback: the client tries the punch for ~3s, then races a gateway attempt; whichever connects first wins.


How it fits together

   your app  ──▶  ConnectKitClient ──▶ IConnectChannel  (Send / OnData / state)
                        │
                        ├─ SecureChannel   J-PAKE handshake → AES-256-GCM → framing   [reused v2 primitives]
                        │
                        └─ IConnectTransport ──▶ IByteChannel  (the seams: how, and the pipe)
                             ├─ DirectTcpTransport      LAN / loopback
                             ├─ DirectUdpTransport      LAN / loopback over the reliable-UDP stack
                             ├─ IntroducerTransport     discover-by-name + TCP/UDP punch + UDP relay (+ gateway)
                             │                            └─ UdpPuncher   reflexive discovery, NAT punch, TURN-style relay → ReliableUdp
                             └─ GatewayTransport        WSS relay — always reachable
  • SecureChannel multiplexes the three J-PAKE rounds and then encrypted application messages over one stream. The handshake is an I/O-free state machine (JpakeHandshake): pump it, send what it emits, feed it what arrives; on completion it installs the derived key into PeerCrypto.
  • IConnectTransport is the only thing that changes between "same LAN" and "anywhere". Everything above it — the handshake, the crypto, the framing, the public API — is transport-agnostic.

Reused vs. new

The security- and correctness-critical core is reused verbatim from RemoteViewer v2 (already shipping across Windows/Linux/macOS/Android/iOS), not reimplemented:

  • JpakeHandshake, JPakeKeyExchange, JPakeWire — password-authenticated key exchange
  • PeerCrypto — AES-256-GCM channel with per-direction nonces
  • PeerChannel — length-prefixed framed receive loop
  • WsPeerChannel + GatewayEnvelope — the WSS gateway room client
  • TcpHolePunching.* (Internal/Net/) — the vendored socket + message + framing engine used for the introducer control link and the inbound listener

New in ConnectKit: the stable public API (ConnectKitClient, IConnectChannel), the IByteChannel seam + adapters (SocketByteChannel, WsByteChannel), SecureChannel (wires the primitives into a clean channel over any transport), and the three transports — including IntroducerTransport, a clean register → introduce → punch orchestration (with gateway fallback) that replaces the 5,000-line v2 IntroducerConnection monolith.


Layout

src/ConnectKit/            the SDK
  ConnectKitClient.cs      entry point (dial / accept)
  IConnectChannel.cs       runtime surface
  ConnectKitOptions.cs     configuration
  ConnectionState.cs
  Transport/               IConnectTransport + DirectTcp/DirectUdp/Introducer/Gateway transports,
                           UdpPuncher (introducer UDP hole-punch), SocketByteChannel, UdpByteChannel,
                           WsByteChannel, PunchHelpers
  Secure/                  SecureChannel (handshake + crypto + framing over any IByteChannel)
  Internal/                IByteChannel seam + reused v2 primitives
                           (J-PAKE / PeerCrypto / PeerChannel / WsPeerChannel / GatewayEnvelope)
                           ReliableUdp (reliable-ordered ARQ over UDP), FrameInbox (buffer + first-frame signal)
  Internal/Net/            vendored TcpHolePunching engine (sockets / messages / framing)
samples/ConnectKit.Sample/ demo / selftest / device / connect
tests/ConnectKit.Tests/    NUnit: ReliableUdp, FrameInbox, SecureChannel, PunchHelpers, direct-transport E2E

Build & test

dotnet build ConnectKit.slnx -c Release
dotnet test ConnectKit.slnx

The test suite is offline and server-free: it exercises the reliable-UDP ARQ (under injected loss/reorder), the handshake + crypto, the signaling parsers, and a full direct-transport round trip over loopback. The introducer / gateway / relay tiers need a live server, so they're verified through the sample (see above).

Requires the .NET 9 SDK. The only third-party dependency is Bouncy Castle (for J-PAKE).


Reference: the ConnectKit product plan (RemoteViewer v2, Session 13). The "buy the connection layer in one call" positioning is unchanged.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.9.2 0 8/30/2026