- name
- shiny-discovery
- description
- Generate code using Shiny.Net.Discovery for cross-platform local network discovery - mDNS/DNS-SD (Bonjour/Zeroconf), SSDP/UPnP, and WS-Discovery (ONVIF) browsing, resolution, and publishing on iOS, Android, Mac Catalyst, macOS, Windows, and Linux
- auto_invoke
- true
- triggers
- ["mDNS","DNS-SD","Bonjour","Zeroconf","SSDP","UPnP","DLNA","M-SEARCH","ssdp:discover","ssdp:all","upnp:rootdevice","WS-Discovery","WSD","WSDAPI","ONVIF","ONVIF camera","IP camera discovery","find a printer on the network","find a media server","find a Chromecast","find a Sonos","service discovery","network discovery","local network discovery","discover devices on the network","find devices on LAN","advertise a service","publish a service","IMdnsManager","ISsdpManager","IWsDiscoveryManager","SsdpDevice","[Truncated]"]
# Shiny.Net.Discovery Skill
You are an expert in Shiny.Net.Discovery, a cross-platform local network discovery library covering
three protocols: **mDNS/DNS-SD** (Bonjour/Zeroconf), **SSDP/UPnP**, and **WS-Discovery**.
## When to Use This Skill
Invoke this skill when the user wants to:
- Discover services or devices on the local network (printers, Chromecasts, IoT devices, peer apps)
- Advertise their own app as a discoverable service
- Find UPnP/DLNA devices, routers, Sonos, Roku, or smart TVs (SSDP)
- Find ONVIF cameras, WSD printers/scanners, or Windows machines (WS-Discovery)
- Read TXT record metadata, or a UPnP device description
- Resolve a known service instance to a host, port, and IP addresses
- Ask about Bonjour/mDNS on iOS without the multicast entitlement
- Register any of the three protocols in DI
## Choosing a protocol
| Looking for | Use | Registration |
|-------------|-----|--------------|
| Apple devices, AirPlay, IPP printers, peer apps, your own services | mDNS/DNS-SD | `AddMdns()` |
| Routers, UPnP/DLNA media servers, Sonos, Roku, smart TVs | SSDP | `AddSsdp()` |
| ONVIF IP cameras, WSD printers/scanners, Windows machines | WS-Discovery | `AddWsDiscovery()` |
They are independent - register only what you need. A device that speaks one usually does not
speak the others.
## Library Overview
| Item | Value |
|------------|-------------------------------------------------------------------------------------------|
| GitHub | https://github.com/shinyorg/shiny |
| NuGet | `Shiny.Net.Discovery` |
| Namespace | `Shiny.Net.Discovery` (types); `Shiny` (registration extensions) |
| Platforms | iOS, Mac Catalyst, macOS, Android, Windows, Linux, server .NET |
### How each platform is backed
**mDNS** delegates to the OS wherever there is an API:
| Platform | Implementation | Why |
|------------------------------|-------------------------------|-----------------------------------------------------------|
| iOS / Mac Catalyst / macOS | `NSNetService` (Bonjour) | Goes through the system mDNSResponder, so **no** `com.apple.developer.networking.multicast` entitlement |
| Android | `NsdManager` | No `CHANGE_WIFI_MULTICAST_STATE` and no `WifiManager.MulticastLock` |
| Windows / Linux / server .NET| Managed responder on UDP 5353 | No OS DNS-SD API to lean on; dependency-free, AOT-safe |
**SSDP and WS-Discovery are managed on every platform**, including iOS and Android. This is not a
design choice - neither `NsdManager` nor Apple's Bonjour stack can speak these protocols, and no
OS exposes any other API for them. They therefore use raw UDP multicast everywhere.
### ⚠️ The entitlement rule differs by protocol
**For mDNS: do not tell users they need the iOS multicast entitlement.** That requirement applies
to raw multicast sockets, which the mDNS implementation deliberately avoids on Apple platforms.
**For SSDP and WS-Discovery the opposite is true** - they *do* need it on iOS, and it is
approval-gated by Apple:
| Platform | mDNS | SSDP / WS-Discovery |
|----------|------|---------------------|
| iOS | nothing | **`com.apple.developer.networking.multicast`** - request at https://developer.apple.com/contact/request/networking-multicast, granted per developer team. Cannot be tested in the simulator |
| Mac Catalyst / macOS | sandbox network entitlements | same sandbox entitlements; **not** the multicast entitlement (iOS only). macOS 15+ also prompts for Local Network |
| Android | nothing | `CHANGE_WIFI_MULTICAST_STATE` (the lock is acquired for you) **plus** `ACCESS_LOCAL_NETWORK` when targeting SDK 37+ |
| Windows | firewall; MSIX capability | same |
| Linux | firewall | same; Docker bridge networking does not work |
A missing permission throws `DiscoveryPermissionException` with a message naming exactly what to
add, rather than silently returning nothing.
## Setup
```csharp
using Shiny;
builder.Services.AddMdns(); // IMdnsManager
builder.Services.AddSsdp(); // ISsdpManager
builder.Services.AddWsDiscovery(); // IWsDiscoveryManager
```
Register only the protocols you use. `AddSsdp()` also registers a named `HttpClient`
(`SsdpConstants.HttpClientName`) for fetching device descriptions, with redirects and cookies
disabled deliberately - you can reconfigure it with `services.AddHttpClient(SsdpConstants.HttpClientName)`.
### iOS / Mac Catalyst / macOS — Info.plist (required)
Browsing silently returns **nothing** if the service type is not declared. Both keys are required:
```xml
<key>NSLocalNetworkUsageDescription</key>
<string>This app discovers nearby devices on your local network.</string>
<key>NSBonjourServices</key>
<array>
<string>_myapp._tcp</string>
<string>_http._tcp</string>
</array>
```
Every service type the app browses for **and** publishes must be listed in `NSBonjourServices`.
The local-network permission prompt appears automatically on first use; there is no API to query
or pre-request it.
### Mac Catalyst / macOS — App Sandbox entitlements (csproj)
For mDNS, iOS needs no entitlement at all. Mac Catalyst and macOS run sandboxed, so network access
must be declared — browsing/resolving needs the client entitlement, publishing also needs the
server one. These apply to all three protocols:
```xml
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">
<CustomEntitlements Include="com.apple.security.network.client" Type="Boolean" Value="true" />
<!-- publish/advertise only -->
<CustomEntitlements Include="com.apple.security.network.server" Type="Boolean" Value="true" />
</ItemGroup>
```
Still **not** `com.apple.developer.networking.multicast` — that is for raw multicast sockets.
### Android — AndroidManifest.xml
No runtime permission is needed, but declare:
```xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
```
### Windows / Linux
The managed responder binds UDP 5353. It sets `SO_REUSEADDR` (plus `SO_REUSEPORT` on Unix) so it
coexists with `avahi-daemon`, `mDNSResponder`, or the Windows DNS client. Ensure the firewall
allows UDP 5353.
A packaged Windows app (MSIX/WinUI) also needs the local-network capability in
`Package.appxmanifest`, otherwise the responder sends and receives nothing:
```xml
<Capabilities>
<Capability Name="privateNetworkClientServer" />
</Capabilities>
```
### SSDP / WS-Discovery — extra platform setup
Everything above still applies. These two protocols additionally need the following, because they
use raw multicast rather than an OS discovery API.
**iOS — the multicast entitlement (blocking).** Apple grants this per developer team on request;
budget days to weeks. Without it, sends fail and nothing is received.
```xml
<!-- Entitlements.plist -->
<key>com.apple.developer.networking.multicast</key>
<true/>
```
Also set `NSLocalNetworkUsageDescription` in Info.plist. **This cannot be tested in the simulator** -
local network privacy is not enforced there, so the simulator neither reproduces the failure nor
proves the fix. `NSBonjourServices` is irrelevant to SSDP/WSD; it only gates mDNS.
**Android:**
```xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- required, or sends succeed and nothing is ever received over Wi-Fi -->
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<!-- runtime permission, mandatory when targeting SDK 37 (Android 17) or later -->
<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />
```
The `WifiManager.MulticastLock` is acquired and released automatically for the lifetime of a
browse or publication - do **not** write code to manage it. `ACCESS_LOCAL_NETWORK` is a runtime
(dangerous) permission in the `NEARBY_DEVICES` group, so request it before discovering.
**Ports:** SSDP is UDP 1900, WS-Discovery is UDP 3702, both on group 239.255.255.250 (and
`ff02::c`). On Windows the system `SSDPSRV` and `FDResPub` services already hold those ports;
`SO_REUSEADDR` is set so this coexists with them.
**Docker:** bridge networking does not work - NAT does not rewrite the addresses embedded in
`LOCATION` and `XAddrs`. Use `--network host` or macvlan.
## mDNS API Reference
### IMdnsManager
```csharp
public interface IMdnsManager
{
IAsyncEnumerable<MdnsBrowseResult> Browse(MdnsBrowseConfig config, CancellationToken ct = default);
Task<MdnsService?> Resolve(string instanceName, string serviceType, TimeSpan? timeout = null, CancellationToken ct = default);
Task<IMdnsPublication> Publish(MdnsServiceRegistration registration, CancellationToken ct = default);
}
```
Extension helpers (`MdnsExtensions`):
```csharp
IAsyncEnumerable<MdnsBrowseResult> Browse(string serviceType, CancellationToken ct = default);
Task<IReadOnlyList<MdnsService>> BrowseOnce(string serviceType, TimeSpan? scanTime = null, CancellationToken ct = default);
Task<IMdnsPublication> Publish(string instanceName, string serviceType, int port, CancellationToken ct = default);
```
### Browsing (live)
`Browse` **never completes on its own** — it runs until the token is cancelled. Always pass a
`CancellationToken`.
```csharp
await foreach (var result in mdns.Browse("_http._tcp", ct))
{
switch (result.Status)
{
case MdnsBrowseStatus.Found:
// upsert keyed on result.Service.FullName - a re-announcing service emits Found again
devices[result.Service.FullName] = result.Service;
break;
case MdnsBrowseStatus.Lost:
// only InstanceName/ServiceType/Domain are populated on a Lost result
devices.Remove(result.Service.FullName);
break;
}
}
```
### Browsing (one-shot scan)
```csharp
var found = await mdns.BrowseOnce("_ipp._tcp", TimeSpan.FromSeconds(5), ct);
foreach (var printer in found)
Console.WriteLine($"{printer.InstanceName} => {printer.GetEndPoint()}");
```
### Publishing
You are responsible for having something listening on the port — publishing only advertises it.
```csharp
await using var publication = await mdns.Publish(
new MdnsServiceRegistration("Allan's Laptop", "_myapp._tcp", 8080)
{
TxtRecords = new Dictionary<string, string>
{
["version"] = "2",
["path"] = "/api"
}
},
ct
);
// ALWAYS read the name back - the responder renames on conflict ("Allan's Laptop (2)")
Console.WriteLine($"advertising as {publication.InstanceName}");
```
Disposing the publication sends a goodbye packet and stops advertising.
### Resolving a known instance
```csharp
var service = await mdns.Resolve("Allan's Laptop", "_myapp._tcp", TimeSpan.FromSeconds(5), ct);
if (service?.IsResolved == true)
await socket.ConnectAsync(service.GetEndPoint()!);
```
Returns `null` when the instance did not answer within the timeout.
## mDNS Models
### MdnsService
| Member | Notes |
|-----------------|---------------------------------------------------------------------------|
| `InstanceName` | Human readable, unescaped; may contain spaces, dots, and UTF8 |
| `ServiceType` | eg `_http._tcp` |
| `Domain` | Almost always `local` |
| `HostName` | SRV target, eg `printer.local`; null when unresolved |
| `Port` | 0 when unresolved |
| `Addresses` | `IReadOnlyList<IPAddress>`; often both IPv4 and IPv6 |
| `TxtRecords` | Case-insensitive key/value map; empty when there are none |
| `FullName` | `"Allan's Laptop._myapp._tcp.local"` — use this as the dictionary key |
| `IsResolved` | `Port > 0 && Addresses.Count > 0` |
| `GetEndPoint(family?)` | First matching `IPEndPoint`, or null when unresolved |
TXT helpers:
```csharp
string? path = service.GetTxt("path");
int version = service.GetTxt<int>("version", 1); // any IParsable<T>, falls back on miss
bool secure = service.GetTxt<bool>("secure");
```
### MdnsBrowseConfig
```csharp
new MdnsBrowseConfig("_http._tcp")
{
GitHub에서 보기