Tailcat.Link 0.3.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Tailcat.Link --version 0.3.0
                    
NuGet\Install-Package Tailcat.Link -Version 0.3.0
                    
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="Tailcat.Link" Version="0.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Tailcat.Link" Version="0.3.0" />
                    
Directory.Packages.props
<PackageReference Include="Tailcat.Link" />
                    
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 Tailcat.Link --version 0.3.0
                    
#r "nuget: Tailcat.Link, 0.3.0"
                    
#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 Tailcat.Link@0.3.0
                    
#: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=Tailcat.Link&version=0.3.0
                    
Install as a Cake Addin
#tool nuget:?package=Tailcat.Link&version=0.3.0
                    
Install as a Cake Tool

Two machines that cannot see each other — different networks, no port forwarding, no VPN, no account — pair once with a short code and stay in touch for as long as they are switched on.

The link survives what a long-lived connection actually meets: a Wi-Fi network changing under either end, a relay going away, and either machine rebooting. Nothing has to be re-entered, and there is nothing to call when it drops.

Pair once

On the machine to be reached:

await using ILink link = await TailcatLink.HostAsync("my-app");
Console.WriteLine(link.InvitationCode);      // show once, as text or a barcode
link.OnRequest(command => Run(command));     // answer whatever is asked

On the machine doing the reaching, with that code the first time only:

await using ILink link = await TailcatLink.JoinAsync("my-app", code);
string answer = await link.RequestAsync("status");

Every later start needs nothing from anybody:

await using ILink link = await TailcatLink.JoinAsync("my-app");

Both ends are equal once paired: each can ask, each can answer, and each can send a message the other did not ask for (NotifyAsync).

Send a file, whatever its size

Requests are messages: both machines hold all of one at once, so they are capped at sixteen megabytes. A file is not a message. SendAsync takes a stream of any size — a 20 GB video is an ordinary use of it — and neither machine ever holds more than a few megabytes of it.

On the machine receiving:

link.OnTransfer(async (transfer, ct) =>
    await transfer.SaveToAsync(Path.Combine(inbox, transfer.SuggestedFileName), null, ct));

On the machine sending:

await link.SendFileAsync(@"D:\wakacjeilm.mkv",
    progress: new Progress<TransferProgress>(p => Console.Write($"
{p.Fraction:P0}")));

There is nothing to chunk, and nothing to restart. A session that dies mid-file is answered by asking the other machine where it got to and carrying on from exactly there, into the same handler, which never learns that anything happened — so a laptop that changes Wi-Fi network during a twenty-gigabyte transfer resumes mid-file rather than starting again.

SendBytesAsync is the same thing for an array already in memory, for when what you have is two gigabytes rather than a path. SaveTransfersTo is the whole receiving side for an application that just wants the files in a directory.

The three things worth knowing:

  • The reader sets the pace. Bytes move no faster than the receiving handler consumes them, so sending to a slow disk costs memory on neither machine.
  • Resuming needs content that can be rewound — a file or an array can be; a socket, or a stream being generated as it is sent, cannot, and a transfer from one of those fails when its session does rather than delivering something with a hole in it.
  • The sender's SendAsync returns when the receiving handler has returned. A transfer reported as sent is one the other machine has finished dealing with, and a handler that throws fails the sender's call instead of being retried.

What it handles for you

  • A pairing that survives a restart. The identity key is generated once and stored — encrypted to the user account with DPAPI on Windows, mode 0600 inside a 0700 directory on Unix, written through a temporary file so a power cut cannot leave half of one.
  • An address that stays valid. A host pins the relay region it first measured, so the code published once keeps pointing at it however far the machine moves.
  • Reconnection. A relay outage, a network change, a peer that rebooted, and a peer that has been away for a day all look the same from here — a session that stopped answering — and all get the same answer.
  • Detection that works. Writing into a dead session succeeds, so silence is what a machine that has gone away looks like: a heartbeat and a per-request timeout are what notice.
  • Requests that survive a reconnection. A request is re-sent across a reconnection but not re-run: it carries an id, and a peer that already answered replies from memory.
  • Transfers that survive one mid-file. The receiving machine keeps what has arrived, and the handler reading it, for ten minutes: a transfer that comes back on a later session continues into it rather than starting a second one.
  • Pairing that cannot be stolen. The code carries a secret that expires, and the first machine to use it is pinned — everyone after it is refused.

Where it sits

Tailcat.Link is the friendly layer on top of Tailcat.Net, which meets a peer at one of Tailscale's DERP relays, authenticates it with sealed messages, punches a direct UDP path when it can, and carries QUIC over whichever path is better. The relay only ever sees QUIC packets it cannot read.

Requires .NET 10. QUIC — which is what carries a session onto a direct path — needs Windows 11 or Server 2022 and later, macOS, or Linux with libmsquic installed; .NET does not carry its own copy. Where it is missing, including on Windows 10, the link still works: the two ends negotiate relay1 instead and the session stays on the relay, slower but no different to use. See docs/relay1.md.

A browser can hold one of these links too, over the same relay1: the host is written exactly as above and never learns which arrived. The JavaScript client lives in the repository under clients/browser and is not published on npm.

Tailcat.Net and the two layers under it are not published separately; their assemblies ship inside this package, so one reference is the whole thing.

Known limits

Hole punching between two different NATs is verified: a home connection and an LTE carrier NAT moved off the relay onto a direct path, 69 ms down to 30 ms. Between two sufficiently hostile NATs there is no direct path at all, and then the session stays on the relay — which works, but is slower and carries every byte past somebody else's server.

The authentication design has not been reviewed by anyone outside this project.

Licence

BSD-3-Clause. Parts are ported from tailscale/tailcat and carry Tailscale's copyright; see the LICENSE file in the package. Not affiliated with or endorsed by Tailscale Inc.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  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 (3)

Showing the top 3 NuGet packages that depend on Tailcat.Link:

Package Downloads
Tailcat.TestSupport

Test doubles for Tailcat.Link: an in-memory DERP relay, a node gateway factory that builds real nodes against it, and a manually advanced TimeProvider. Together they stand a whole link up in a unit test — pairing, reconnection, transfers and all — with no network, no second machine and no sleeping.

Tailcat.Link.Json

Sends and answers requests over a Tailcat link as JSON instead of bytes, through System.Text.Json's source-generated metadata so it survives trimming and ahead-of-time compilation. A separate package on purpose: ILink stays exactly as narrow as it is, and nobody who wants bytes pays for a serializer.

Tailcat.Link.Extensions.DependencyInjection

Registers a Tailcat link host in the service collection and gives it the application's own lifetime, so nothing has to hand-roll start-up, shutdown and dispose ordering against a supervision loop.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.5.3 59 9/17/2026
0.5.2 55 9/16/2026
0.5.1 83 9/15/2026
0.5.0 79 9/15/2026
0.4.3 121 9/10/2026
0.4.2 108 9/10/2026
0.4.1 116 9/10/2026
0.4.0 130 9/9/2026
0.3.1 99 9/4/2026
0.3.0 101 9/4/2026
0.2.0 84 9/4/2026
0.1.0 98 9/3/2026

First release. Pairing, a durable session over DERP with a direct QUIC path when hole punching succeeds, and a stored identity that survives a reboot. Hole punching between two different NATs is untested; see the repository README.