A lightweight SSDP (Simple Service Discovery Protocol) implementation for Node.js with full TypeScript support.
npm install @gibme/ssdp
or
yarn add @gibme/ssdp
https://gibme-npm.github.io/ssdp
Discover SSDP services on the network with automatic periodic searching.
discover events for ssdp:alive, ssdp:update, and search replieswithdraw events for ssdp:byebye notificationsAnnounce SSDP services on the network with automatic periodic notifications.
M-SEARCH requests for:
upnp:rootdeviceuuid:<uuid>ssdp:allimport { Browser } from '@gibme/ssdp';
const browser = await Browser.create({
interval: 5_000,
services: ['urn:schemas-upnp-org:device:MediaServer:1']
});
browser.on('discover', (service, payload, remote, local) => {
console.log('Discovered:', { service, remote: remote.address });
});
browser.on('withdraw', (service, payload, remote, local) => {
console.log('Withdrawn:', { service, remote: remote.address });
});
// Trigger an immediate search
browser.searchNow();
// Subscribe to additional services dynamically
browser.subscribe('urn:schemas-upnp-org:device:InternetGatewayDevice:1');
// Unsubscribe from a service
browser.unsubscribe('urn:schemas-upnp-org:device:MediaServer:1');
// Clean up when done
browser.destroy();
import { Advertiser } from '@gibme/ssdp';
const advertiser = await Advertiser.create({
interval: 5_000,
services: {
'urn:schemas-upnp-org:device:MediaServer:1': {
'LOCATION': 'http://192.168.1.100:8080/description.xml'
}
}
});
// Announce a new service dynamically
advertiser.announce('urn:schemas-upnp-org:service:ContentDirectory:1', {
'LOCATION': 'http://192.168.1.100:8080/content.xml'
});
// Withdraw a specific service
advertiser.withdraw('urn:schemas-upnp-org:service:ContentDirectory:1');
// Trigger an immediate notification for all services
advertiser.announceNow();
// Clean up when done (sends ssdp:byebye for all services)
advertiser.destroy();
Control which hosts can discover your services:
import { Advertiser } from '@gibme/ssdp';
const allowedSubnets = ['192.168.1.', '10.0.0.'];
const advertiser = await Advertiser.create({
interval: 10_000,
services: {
'my:custom:service': {}
},
authenticationProvider: (ipAddress) => {
return allowedSubnets.some(subnet => ipAddress.startsWith(subnet));
}
});
For direct control over SSDP messaging:
import { SSDP } from '@gibme/ssdp';
const socket = await SSDP.create({ loopback: true, ttl: 2 });
socket.on('search', (payload, local, remote) => {
console.log('Search from:', remote.address, 'for:', payload.getHeader('ST'));
});
socket.on('notification', (payload, local, remote) => {
console.log('Notification:', payload.getHeader('NT'), payload.getHeader('NTS'));
});
socket.on('reply', (payload, local, remote) => {
console.log('Reply:', payload.getHeader('ST'));
});
// Send a search
await socket.search('ssdp:all');
// Send a notification
await socket.notify('my:service:type', { USN: 'uuid:my-device::my:service:type' });
// Send a byebye
await socket.bye('my:service:type', { USN: 'uuid:my-device::my:service:type' });
socket.destroy();
| Option | Type | Default | Description |
|---|---|---|---|
host |
string |
auto | Network interface to bind to |
loopback |
boolean |
false |
Whether to receive messages sent by this instance |
linkLocalOnly |
boolean |
false |
When true, drop inbound packets whose source is not on a local-link subnet (RFC 6762 §11-style hardening). SSDP runs on the administratively-scoped group 239.255.255.250 and traditionally accepts routed multicast, so the default is false. Set true for single-segment deployments that want the stricter origin check. |
| Option | Type | Default | Description |
|---|---|---|---|
interval |
number |
60000 |
How often (ms) to search the network |
services |
string | string[] |
— | Service types to subscribe to |
| Option | Type | Default | Description |
|---|---|---|---|
interval |
number |
60000 |
How often (ms) to send notifications |
uuid |
string |
auto (v4) | UUID for this device instance. Default is a fresh RFC 9562 v4 (random) UUID. UPnP DA expects device UUIDs to be stable across reboots; if you want that, generate one at install time, persist it, and pass it here. Avoid v1 (leaks MAC) and v7 (leaks creation timestamp via every USN multicast). |
services |
Record<string, HeadersInit> |
— | Services to advertise with their headers |
authenticationProvider |
(ip: string) => boolean | Promise<boolean> |
() => true |
Gates unicast replies to incoming M-SEARCH requests by remote IP. Returns true to allow the response, false to silently drop. Throws are treated as deny. Does not gate outbound multicast NOTIFY/ssdp:byebye traffic; see silentMode. |
silentMode |
boolean |
false |
When true, suppresses periodic multicast NOTIFY ssdp:alive and ssdp:byebye traffic entirely. The advertiser still answers unicast M-SEARCH requests that pass authenticationProvider. Use this when discoverability should be initiated by the requester rather than announced. |
Browser will surface those announcements to its discover and withdraw events. Consumers MUST validate any side effect (fetching a LOCATION URL, connecting to a service, trusting metadata) independently and treat SSDP discovery as a hint, not as a trust signal.authenticationProvider only governs unicast M-SEARCH replies. It does not affect periodic outbound NOTIFY or ssdp:byebye messages. If a deployment needs the device to remain silent on the wire and respond only to authorized direct queries, set silentMode: true in addition to providing an authenticationProvider.M-SEARCH is delayed by a random interval in [0, MX] seconds to mitigate the multicast-amplification reply storm that occurs when many advertisers respond to a single discovery probe. The MX value is parsed from the incoming request and clamped to [1, 5] per UPnP DA.M-SEARCH * HTTP/1.1, NOTIFY * HTTP/1.1, and HTTP/1.1 <status> [<reason>] are accepted; all other datagrams are silently dropped. Header lines with whitespace between the field name and colon are rejected per RFC 9112 §5.1.@gibme/multicast layer per RFC 6762 §11 outbound hardening. The previous ttl option is no longer accepted.USN header of every advertisement. The default is UUID v4 (random); supply a stable UUID via the uuid option if you need cross-reboot identity stability.drop event on both SSDP and Browser (currently only fires when linkLocalOnly: true and a packet's source IP failed the local-subnet check).MIT