LAN.Lib
2.2.191
See the version list below for details.
dotnet add package LAN.Lib --version 2.2.191
NuGet\Install-Package LAN.Lib -Version 2.2.191
<PackageReference Include="LAN.Lib" Version="2.2.191" />
<PackageVersion Include="LAN.Lib" Version="2.2.191" />
<PackageReference Include="LAN.Lib" />
paket add LAN.Lib --version 2.2.191
#r "nuget: LAN.Lib, 2.2.191"
#:package LAN.Lib@2.2.191
#addin nuget:?package=LAN.Lib&version=2.2.191
#tool nuget:?package=LAN.Lib&version=2.2.191
LAN.Lib
Zero-dependency LAN peer discovery for .NET: a symmetric UDP announce beacon plus a
self-expiring peer table, over a single shared broadcast domain, filtered by service name.
TimeProvider-driven (so beacon cadence and expiry are deterministic in tests), AOT/trim
friendly, with a one-line DI extension for the .NET generic host. Beside it, the two halves of
asking a peer's person for something: the invite handshake's answering side, and the grants an
accepted invite leaves behind; and who an address is, by its forward-confirmed host name.
Extracted and generalised from the LAN play in the chess project.
What it does
- Symmetric discovery. Every node broadcasts a small ASCII announce beacon (1 s) and
listens for everyone else's, keeping a live table of peers that self-expires (5 s) when a
node goes quiet. Both cadences run off an injected
TimeProvider. - One shared broadcast domain. All apps broadcast on one UDP port (
38821) with one magic prefix; a consumer filters to the service it cares about (IPeerTable.PeersOf("...")).ReuseAddresslets several apps share the port on one host — no port-per-app. - Every interface, not just the routed one. The announce goes out once per up, non-loopback
interface, to that interface's directed broadcast address. A single send to
255.255.255.255leaves a multi-homed host on whichever interface wins the route — commonly a Hyper-V, WSL, Docker or VPN adapter rather than the LAN — while receiving (bound toAny) still hears every interface. That asymmetry made such a host see its peers while staying invisible to them. - Stable node identity. A node can mint a persisted node id once (
StableNodeIdPath) and advertise it in every beacon, so a consumer can bind to a node and recognise it across restarts even as its address changes. Separate from the per-process peer id, which exists only to filter a node's own beacon echo. - Extensible. The announce carries an open
key=valueproperty bag (URL-encoded values), so an app can advertise extra facts — a version, a capability flag — with no wire-format bump. - Foreign-datagram safe. A magic + version prefix means a datagram that isn't ours (or is malformed) is ignored, never misparsed — the shared port coexists with anything else on it.
Discovery is UDP-only by design: its whole job is to tell an app which address:port to open a session/control channel to (TCP, WebSocket, HTTP, …). That channel is the app's concern.
Invites and grants
Neither carries anything over the network: an invite arrives however the app receives it (chess: a line on a TCP connection; TianWen: an HTTP route, answered by a different client on the same machine), and the rules around it are what is shared.
LanInvites<T>is the answering side. One invite waits at a time, since a person answers one question at a time, so a secondTryOfferis refused while one waits. An invite lives only while its asker is there: the app callsWithdrawwhen it learns the asker left (a socket closing), orSeenon each poll, with a presence lapse after which an unseen invite is withdrawn, so nothing is granted to someone who walked away. An answer names the invite it answers, so an answer meant for a withdrawn invite never accepts the one that replaced it. The asker reads the outcome back by id (OutcomeOf) forOutcomeRetentionafter the answer. Every read is lock-free (the state is one immutable value replaced by compare-and-swap), so a UI may readPendingevery frame.LanGrantsis what an acceptance that should last turns into:GrantAsyncmints a 256-bit bearer token and hands it out once, the store (ILanGrantStore, the app's own storage) keeps only its SHA-256,TryVerifychecks a presented token in constant time, andRevokeAsyncends it. A write the store refuses did not happen, and writes are one at a time so none is lost. Over plain HTTP a token can be read off the wire: this stops "anyone who can reach the port", not a hostile network, which is TLS's job.LanHostNamessays who an address is, for a peer that cannot ask (another application on the LAN): the name it reverse-resolves to, counted only when that name resolves back to the same address, so it survives a DHCP renewal and a stray PTR record cannot claim it. Each address's answer, a miss included, is cached forCacheLifetime, and a lookup is bounded byLookupBudget, so a polling client costs one lookup and a resolver that never answers holds nothing.
var invites = new LanInvites<Requester>(TimeProvider.System, presenceLapse: TimeSpan.FromSeconds(15));
using var grants = new LanGrants(new MyGrantFile(path), TimeProvider.System);
await grants.LoadAsync(ct);
// The asker's request arrives:
if (!invites.TryOffer("Laptop (Seb)", requester, out var invite)) { /* busy: answering another */ }
// The person answers, on the machine that owns the thing asked for:
if (invites.Answer(invite.Id, accept: true) is { } accepted)
{
var issued = await grants.GrantAsync(accepted.Label, ct); // hand issued.Token to the asker, once
}
// Every later request:
if (!grants.TryVerify(presentedToken, out var grant)) { /* refused */ }
Usage
using LAN.Lib;
// On a node that others discover and bind to (e.g. a headless server):
services.AddLanDiscovery(o =>
{
o.ServiceName = "tianwen-server"; // announced + filtered on
o.ServicePort = 1888; // the port peers should reach us at
o.NodeName = Environment.MachineName; // human display name
o.StableNodeIdPath = Path.Combine(appData, "node-id.txt"); // mint-once, persisted
});
// On a consumer that only wants to see who's out there (never appears itself):
services.AddLanDiscovery(o =>
{
o.ServiceName = "tianwen-gui";
o.Announce = false; // listen-only
});
// Anywhere, read the live peer list (a fresh snapshot each call; Changed hints when to re-read):
IPeerTable peers = provider.GetRequiredService<IPeerTable>();
foreach (var peer in peers.PeersOf("tianwen-server"))
Console.WriteLine($"{peer.DisplayName} @ {peer.EndPoint} (node {peer.NodeId})");
AddLanDiscovery registers ILanTransport (via TryAdd, so a bespoke/fake transport can be
registered first), the LanDiscovery beacon + peer table (as IPeerTable), and a hosted
service that runs them for the host's lifetime (sending a polite bye on shutdown).
Testing
LanDiscovery needs no host and no real sockets: construct it directly with an in-memory
ILanTransport and a FakeTimeProvider, then Advance the clock to drive beacon cadence and
expiry deterministically. LanInvites<T>, LanGrants and LanHostNames need neither: a
FakeTimeProvider and, for grants, an in-memory ILanGrantStore, for names an IHostNameResolver
answering from tables. See src/LAN.Lib.Tests.
License
MIT — see LICENSE.
| Product | Versions 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. |
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.0)
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 |
|---|---|---|
| 2.2.221 | 663 | 10/14/2026 |
| 2.2.201 | 96 | 10/13/2026 |
| 2.2.191 | 1,019 | 10/9/2026 |
| 2.2.181 | 1,700 | 9/29/2026 |
| 2.1.161 | 363 | 9/27/2026 |
| 2.0.141 | 2,092 | 9/7/2026 |
| 2.0.131 | 106 | 9/3/2026 |
| 2.0.121 | 46 | 9/3/2026 |
| 1.2.101 | 335 | 8/20/2026 |
| 1.2.91 | 232 | 8/4/2026 |
| 1.2.81 | 83 | 8/2/2026 |
| 1.1.61 | 70 | 8/2/2026 |
| 1.1.51 | 250 | 7/25/2026 |
| 1.1.31 | 48 | 7/24/2026 |
| 1.1.21 | 45 | 7/24/2026 |
| 1.0.11 | 50 | 7/23/2026 |