RemoteViewer.ConnectKit
0.9.2
dotnet add package RemoteViewer.ConnectKit --version 0.9.2
NuGet\Install-Package RemoteViewer.ConnectKit -Version 0.9.2
<PackageReference Include="RemoteViewer.ConnectKit" Version="0.9.2" />
<PackageVersion Include="RemoteViewer.ConnectKit" Version="0.9.2" />
<PackageReference Include="RemoteViewer.ConnectKit" />
paket add RemoteViewer.ConnectKit --version 0.9.2
#r "nuget: RemoteViewer.ConnectKit, 0.9.2"
#:package RemoteViewer.ConnectKit@0.9.2
#addin nuget:?package=RemoteViewer.ConnectKit&version=0.9.2
#tool nuget:?package=RemoteViewer.ConnectKit&version=0.9.2
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 quickstart — https://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");
Secretderives 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 fromTenantKey, which the operator issues to identify your company.Signalingis the coordination server ashost:1618. When set, adeviceIdis a logical name (resolved via the server's roster) instead of ahost:port.Company/TenantKeyidentify 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
CompanyandTenantKey(with the exact settings to paste). The coordination-server host issig.ck.remote-viewer.com:1618. - Self-hosted — run the
ConnectKit.Serverdeployable on your own infrastructure (a single-tenant server needs noCompany/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 IConnectTransport → IByteChannel 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
ConnectKitClientin 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
SecureChannelmultiplexes 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 intoPeerCrypto.IConnectTransportis 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 exchangePeerCrypto— AES-256-GCM channel with per-direction noncesPeerChannel— length-prefixed framed receive loopWsPeerChannel+GatewayEnvelope— the WSS gateway room clientTcpHolePunching.*(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 | Versions 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. |
-
net8.0
- BouncyCastle.Cryptography (>= 2.5.1)
-
net9.0
- BouncyCastle.Cryptography (>= 2.5.1)
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 |