@collight/dns-sd
Version:
A DNS Service Discovery implementation in modern TypeScript
741 lines (720 loc) • 28.3 kB
TypeScript
import { default as default_2 } from 'multicast-dns';
import { default as default_3 } from 'debug';
import { EventEmitter } from 'events';
import { MulticastDNS } from 'multicast-dns';
import { RemoteInfo } from 'dgram';
import { ResponsePacket } from 'multicast-dns';
import { SrvAnswer } from 'dns-packet';
import { StringAnswer } from 'dns-packet';
import { TxtAnswer } from 'dns-packet';
/**
* Browses the local network for mDNS/DNS-SD service instances.
*
* Emits:
* - `up`: when a new service appears.
* - `down`: when a previously seen service sends a "goodbye" (TTL = 0).
* - `update`: when an existing service has received new records
*/
export declare class Browser extends EventEmitter<BrowserEventMap> {
static TLD: string;
static WILDCARD: string;
services: DiscoveredService[];
private mdns;
private filter?;
private onresponse?;
constructor(mdns: MulticastDNS, options?: BrowserOptions);
private get queryNames();
/**
* Starts the browser and begins listening for mDNS service announcements.
* - Has no effect if the browser is already started.
*/
start(): void;
/**
* Stops the browser from listening to mDNS service announcements.
* - Has no effect if the browser is already stopped
*/
stop(): void;
/**
* Sends an active query to refresh the list of discovered services.
* - Triggers a new PTR record query for the configured service name
* - Causes mDNS responders to reply with their current service information
* - Useful for manual refresh when you suspect stale service data
*/
update(): void;
private match;
/**
* TODO: {@link https://datatracker.ietf.org/doc/html/rfc6762#section-5.2 | Continuous Multicast DNS Querying}
*/
private setupTTLTimer;
private addService;
private updateService;
private removeService;
}
declare interface BrowserEventMap {
up: [service: DiscoveredService];
down: [service: DiscoveredService];
update: [curr: DiscoveredService, prev: DiscoveredService];
}
export declare type BrowserFilter = {
/**
* Network protocol used by the service, either TCP or UDP.
* @default 'tcp'
*/
protocol: 'tcp' | 'udp';
/**
* The service type to browse for. This corresponds to the DNS-SD service type without the leading underscore.
* @example 'http', 'ipp'
*/
type: string;
/**
* Optional service subtypes to filter for. When provided, the browser targets this instance specifically.
* @example ['foo', 'bar']
*/
subtypes?: string[];
/**
* Optional specific instance name to filter for. When provided, the browser targets this instance specifically.
* @example 'MyPrinter'
*/
name?: string | RegExp;
/**
* Optional TXT record key-value pairs to filter discovered services.
* Only services whose TXT records match all keys and values will be emitted.
*/
txt?: Record<string, string | RegExp>;
};
export declare interface BrowserOptions {
filter?: BrowserFilter;
}
declare const debug_2: default_3.Debugger;
export { debug_2 as debug }
/**
* Decodes an input buffer or array of buffers (or strings) containing
* `"key=value"` pairs into a single object mapping keys to values.
*
* Each input item is expected to be a string or Buffer representing one key-value pair.
* Items that do not match the `"key=value"` pattern are ignored.
*
* @param buffer - A string, Buffer, or an array of these, each containing `"key=value"` format data.
* @returns An object where each key is mapped to its corresponding decoded value.
*
* @example
* ```ts
* decodeTXT([Buffer.from("foo=bar"), "baz=qux"])
* // { foo: "bar", baz: "qux" }
*
* decodeTXT(Buffer.from("hello=world"))
* // { hello: "world" }
*
* decodeTXT(["invalid", "key=value"])
* // { key: "value" } // "invalid" ignored
* ```
*/
export declare function decodeTXT(buffer: string | Buffer | (string | Buffer)[], binary: false): Record<string, string>;
export declare function decodeTXT(buffer: string | Buffer | (string | Buffer)[], binary: true): Record<string, Buffer>;
/**
* Represents a discovered mDNS/DNS-SD service instance, as defined in the DNS-Based Service Discovery specification.
*
* A `DiscoveredService` is constructed from multiple DNS resource records typically found in an mDNS response:
* - A {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4.1 | PTR record} that points to the instance's FQDN.
* - An {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4.1.2 | SRV record} that provides the target host and port.
* - A {@link https://datatracker.ietf.org/doc/html/rfc6763#section-6 | TXT record} containing key-value metadata.
* - One or more {@link https://datatracker.ietf.org/doc/html/rfc6762#section-5.4 | A or AAAA records} for IP addresses.
* - Optionally, additional {@link https://datatracker.ietf.org/doc/html/rfc6763#section-7.1 | subtype PTR records} for selective discovery.
*
* All records are typically retrieved from a single mDNS response.
*
* @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4 | RFC 6763 § 4: Service Instances}
* @see {@link https://datatracker.ietf.org/doc/html/rfc2782 | RFC 2782 - DNS SRV Records}
* @see {@link https://datatracker.ietf.org/doc/html/rfc6762 | RFC 6762 - Multicast DNS}
*/
export declare class DiscoveredService {
/**
* Short instance name of the service (first label of the FQDN).
* @example "MyPrinter"
* @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4.1.1}
*/
readonly name: string;
/**
* Fully qualified domain name (e.g., MyPrinter._ipp._tcp.local).
* @example "MyPrinter._ipp._tcp.local"
* @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4.1.1}
*/
readonly fqdn: string;
/**
* Hostname of the machine offering the service (SRV record target).
* @example "MyPrinter.local"
* @see {@link https://datatracker.ietf.org/doc/html/rfc2782}
*/
readonly host: string;
/**
* TCP or UDP port on which the service is running.
* @example 631
* @see {@link https://datatracker.ietf.org/doc/html/rfc2782}
*/
readonly port: number;
/**
* Network info about where the service response came from.
* @example { address: "192.168.1.101", family: "IPv4", port: 5353, size: 412 }
* @see {@link https://datatracker.ietf.org/doc/html/rfc6762#section-5}
*/
readonly referer: RemoteInfo;
/**
* Time-to-live value in seconds.
* @example 120
* @see {@link https://datatracker.ietf.org/doc/html/rfc6762#section-10}
*/
readonly ttl: number | undefined;
/**
* Last time the service was seen (in milliseconds since the Unix epoch).
* @example 1634567890000
*/
readonly lastSeen: number;
/**
* Creates a list of `DiscoveredService` instances from a DNS response packet.
*
* This function filters out records with expired TTL, finds matching PTR records for the given service name,
* and builds structured `DiscoveredService` objects by resolving their associated SRV, TXT, A/AAAA, and subtype PTR records.
*
* @internal
* @param packet - The full mDNS response packet containing DNS records.
* @param referer - Network information about the source of the packet.
* @returns An array of discovered service instances matching the given name.
*/
static fromResponse(packet: ResponsePacket, referer: RemoteInfo): DiscoveredService[];
/**
* Constructs a `DiscoveredService` instance from a PTR record and its associated resource records.
*
* This method finds the corresponding SRV record for service connection details, optional TXT records
* for metadata, subtype PTR records for selective discovery, and A/AAAA records for resolving IP addresses.
*
* @internal
* @param ptr - The PTR record pointing to a service instance.
* @param answers - A list of resource records from the DNS response to search through.
* @param referer - The network information of the source of the response.
* @returns A `DiscoveredService` instance or `null` if a valid SRV record is not found.
*/
private static fromPTR;
/**
* The registered service type (e.g., _http, _ipp).
* @example "ipp"
* @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4.1.2}
*/
type?: string;
/**
* Network protocol used by the service (_tcp or _udp).
* @example "tcp"
* @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4.1.2}
*/
protocol?: string;
/**
* Subtypes advertised by the service for selective discovery.
* @example ["scanner", "fax"]
* @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-7.1}
*/
subtypes: string[];
/**
* Resolved IPv4/IPv6 addresses of the service host.
* @example ["192.168.1.42", "fe80::1c2a:3bff:fe4e:1234"]
* @see {@link https://datatracker.ietf.org/doc/html/rfc6762#section-5.4}
*/
addresses: string[];
/**
* Decoded TXT record key-value pairs.
* @example { "note": "Office printer", "paper": "A4" }
* @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-6}
*/
txt?: Record<string, string>;
/**
* Decoded binary TXT record key-value pairs.
* @example { "note": <Buffer 04 6e 6f 74 65 0b 4f 66 69 63 65 20 50 72 69 6e 74 65 72> }
* @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-6}
*/
binaryTxt?: Record<string, Buffer>;
/**
* Raw TXT record data as received over the network.
* @example <Buffer 04 6e 6f 74 65 0b 4f 66 66 69 63 65 20 50 72 69 6e 74 65 72>
* @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-6}
*/
rawTxt?: string | Buffer | (string | Buffer)[];
/**
* Timer for checking TTL expiration.
*/
ttlTimer?: NodeJS.Timeout;
private constructor();
get expired(): boolean;
}
/**
* High-level API for Multicast DNS Service Discovery and Advertisement.
*
* The `DNSSD` class provides convenient methods for:
* - Publishing services to the local network
* - Browsing and discovering services
* - Unpublishing and cleaning up all registered services
* - Managing underlying mDNS server lifecycle
*
* It wraps a lower-level `MDNSServer` and coordinates interactions with
* the service `Registry` and service `Browser`.
*
* @example
* const dnssd = new DNSSD()
*
* // Advertise a new service
* dnssd.publish({ name: 'My Printer', type: 'printer', port: 9100 })
*
* // Discover services
* dnssd.startBrowser({ type: 'printer' }, service => {
* console.log('Found service:', service)
* })
*/
export declare class DNSSD {
readonly server: MDNSServer;
readonly registry: Registry;
constructor(options?: default_2.Options);
/**
* Creates a new service instance without publishing it to the network.
*
* @param options - Configuration options for the service.
* @returns A new `Service` instance linked to this registry.
*/
makeService(options: ServiceOptions): Service;
/**
* Publishes a service to the network using the given options.
*
* @param options - Configuration options for the service.
* @returns A promise that resolves with the published service.
*/
publish(options: ServiceOptions): Promise<Service>;
/**
* Unpublishes all services that were previously published.
*
* Sends goodbye messages and clears internal state.
*/
unpublishAll(): Promise<void>;
/**
* Creates a new mDNS browser for discovering services on the network.
*
* @param options - Optional configuration for the browser.
* @returns A new `Browser` instance.
*/
makeBrowser(options?: BrowserOptions): Browser;
/**
* Creates and starts a browser for discovering services, with an optional handler for discovered services.
*
* @param options - Optional configuration for the browser.
* @param onServiceUp - Optional callback invoked when a service is discovered.
* @returns A started `Browser` instance.
*/
startBrowser(options?: BrowserOptions, onServiceUp?: (service: DiscoveredService) => void): Browser;
/**
* Discovers a single service matching the provided criteria, with a timeout.
*
* Stops discovery after the first match or when the timeout is reached.
*
* @param options - Optional browser options to filter services.
* @param timeoutMs - Maximum time in milliseconds to wait for a match.
* @returns A promise that resolves with the discovered service or `null` if none found.
*/
findOne(options?: BrowserOptions, timeoutMs?: number): Promise<DiscoveredService | null>;
/**
* Destroys the DNSSD instance, stopping all services and shutting down the server.
*
* No further operations should be performed after destruction.
*/
destroy(): Promise<void>;
}
/**
* Encodes a record of key-value string pairs into an array of Buffer objects,
* where each buffer contains a string in the format `"key=value"`.
*
* @param data - An object whose string keys and values will be encoded.
* Defaults to an empty object.
* @returns An array of Buffer instances, each representing one `"key=value"` pair.
*
* @example
* ```ts
* encodeTXT({ foo: "bar", baz: "qux" })
* // [Buffer.from("foo=bar"), Buffer.from("baz=qux")]
* ```
*/
export declare function encodeTXT(data?: Record<string, string | number | boolean | Buffer>): Buffer[];
declare type EventMap = {
responded: [packet: default_2.ResponseOutgoingPacket, error: Error | null, bytes?: number];
};
/**
* Represents a DNS Resource Record relevant for mDNS responses.
*/
export declare type MDNSRecord = StringAnswer | TxtAnswer | SrvAnswer;
/**
* An mDNS server that responds to DNS queries on the local network.
*
* Maintains a registry of published records and automatically responds
* to queries using the multicast-dns protocol.
*
* Emits:
* - `responded` when a response is sent to a query
*
* @example
* ```ts
* const server = new MDNSServer({}, console.error)
* server.register([{ name: '_http._tcp.local', type: 'PTR', data: 'MyService._http._tcp.local' }])
* ```
*
* @see https://datatracker.ietf.org/doc/html/rfc6762
*/
export declare class MDNSServer extends EventEmitter<EventMap> {
mdns: MulticastDNS;
private typeToRecords;
/**
* @param options - Configuration passed to the underlying `multicast-dns` instance.
*/
constructor(options: default_2.Options);
/**
* Registers a list of DNS records to be used in future responses.
*
* Duplicate records (based on type, name, and deep equality of data) are ignored.
*
* @param records - An array of mDNS records to register.
*/
register(records: MDNSRecord[]): void;
/**
* Unregisters a set of previously registered DNS records.
*
* Matching is done by `record.name` only (not deeply).
*
* @param records - Records to remove from the server registry.
*/
unregister(records: MDNSRecord[]): void;
/**
* Returns all records of a given type matching a name.
*
* If the name is not fully qualified (no dot), only the first segment of the record name is compared.
*
* @param type - DNS record type.
* @param name - Name to match against.
* @returns Matching records.
*/
private getRecordsOf;
/**
* Responds to incoming mDNS queries with matching registered records.
* Handles `ANY` queries and populates additional records for `PTR → SRV → A/AAAA + TXT` dependencies,
*
* @param query - The incoming mDNS query packet.
*
* @see {@link https://www.rfc-editor.org/rfc/rfc6763#section-12}
*/
private respond;
}
/**
* Compares two strings for equality in a case-insensitive manner,
* but only ASCII uppercase letters (`A-Z`) are converted to lowercase before comparison.
*/
export declare function nameEquals(a: string, b: string): boolean;
export declare class Registry {
static MAX_PROBE_AUTO_RESOLVE_ATTEMPTS: number;
static REANNOUNCE_MAX_MS: number;
static REANNOUNCE_FACTOR: number;
private server;
private startedServices;
constructor(server: MDNSServer);
/**
* Creates a new `Service` instance with the given options,
* and links it to this registry.
*
* @param options - The configuration options for the service.
* @returns A new `Service` instance.
*/
makeService(options: ServiceOptions): Service;
/**
* Publishes a service to the network using the given options.
* This includes optional probing and sending announcement packets.
*
* @param options - The configuration options for the service.
* @returns A promise that resolves with the published `Service`.
*/
publish(options: ServiceOptions): Promise<Service>;
/**
* Stops and unpublishes all currently running services.
*
* Sends goodbye messages and clears internal service state.
*/
unpublishAll(): Promise<void>;
/**
* Marks all services as destroyed without unpublishing them.
*
* Used when permanently shutting down the registry and disable future operations.
*/
destroy(): void;
/**
* Internal handler invoked when a service is started.
*
* This will initiate probing (if enabled) and trigger the initial announcement.
*
* @internal
* @param service - The service being started.
* @param options - Optional configuration to override probing behavior.
*/
onServiceStart(service: Service): Promise<void>;
/**
* Internal handler invoked when a service is stopped.
*
* Sends goodbye messages and updates internal state.
*
* @internal
* @param service - The service being stopped.
*/
onServiceStop(service: Service): Promise<void>;
/**
* Probes the network to detect service name conflicts before announcing.
*
* Follows RFC 6762 §8.1–8.3 to ensure uniqueness by sending multiple queries
* and listening for conflicting responses.
*
* @param service - The service to probe for potential name conflicts.
*/
private probe;
/**
* Announces a newly registered service to the network.
*
* Sends initial responses immediately, then exponentially spaced re-announcements
* (3s, 9s, 27s...) up to one hour intervals per RFC 6762 §8.3.
*
* @param service - The service to announce.
*
* @see {@link https://datatracker.ietf.org/doc/html/rfc6762#section-8.3 | Announcing}
*/
private announce;
/**
* Send goodbye messages for a list of services and unregistering them.
*
* Each record's TTL is set to 0 to indicate that the service is going offline.
*
* @param services - Array of services to remove from the network.
*
* @see {@link https://datatracker.ietf.org/doc/html/rfc6762#section-10.1 | Goodbye Packets}
*/
private goodbye;
}
/**
* Represents a service registered on the local network via mDNS/DNS-SD.
*
* This class manages the service's DNS resource records such as PTR, SRV, TXT, A, and AAAA,
* constructed according to the DNS-Based Service Discovery specification.
*
* @see {@link https://datatracker.ietf.org/doc/html/rfc6763 | RFC 6763 - DNS-Based Service Discovery}
* @see {@link https://datatracker.ietf.org/doc/html/rfc6762 | RFC 6762 - Multicast DNS}
*/
export declare class Service extends EventEmitter<ServiceEventMap> implements Required<ServiceOptions> {
static TLD: string;
protocol: string;
type: string;
name: string;
subtypes: string[];
host: string;
port: number;
txt: Record<string, string | number | boolean | Buffer>;
ttl: number;
probe: boolean;
probeAutoResolve: boolean;
disableIPv6: boolean;
fqdn: string;
started: boolean;
published: boolean;
destroyed: boolean;
registry?: Registry;
constructor(options: ServiceOptions);
/**
* Starts the registered service, initiating its publication on the network.
*
* If the service has already been started, this method does nothing.
*/
start(): Promise<void>;
/**
* Stops the registered service and removes it from the network.
*/
stop(): Promise<void>;
/**
* Generates all DNS resource records representing this service instance.
*
* This includes:
* - PTR record for the service type
* - SRV record with host and port
* - TXT record with metadata
* - PTR record advertising the service type in the _services._dns-sd._udp.local domain
* - PTR records for any declared subtypes
* - A and AAAA records for each network interface address (unless IPv6 is disabled)
*
* @returns An array of DNS resource records suitable for publishing via mDNS.
*/
getRecords(): MDNSRecord[];
/**
* Constructs the main PTR record pointing to this instance's FQDN.
*
* This advertises the presence of a service instance under the service type domain.
*
* @returns A PTR record for service instance discovery.
* @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4.1 | RFC 6763 §4.1 - Structured Service Instance Names}
*/
private getRecordPTR;
/**
* Constructs a subtype PTR record to support selective instance discovery by subtype.
*
* This record maps from a subtype-specific domain to the instance FQDN.
*
* @param subtype - The subtype identifier (e.g., "printer").
* @returns A subtype-specific PTR record.
* @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-7.1 | RFC 6763 §7.1 - Selective Instance Enumeration (Subtypes)}
*/
private getRecordPTRSubtype;
/**
* Constructs a PTR record under the special _services._dns-sd._udp.local name.
*
* This enables enumeration of available service types on the local network.
*
* @returns A PTR record pointing to this service type.
* @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-9 | RFC 6763 §9 - Service Type Enumeration}
*/
private getRecordPTRServiceTypeEnumeration;
/**
* Constructs an SRV record that contains the target host and port of this service.
*
* This enables clients to locate the host and communication endpoint.
*
* @returns An SRV record for this service instance.
* @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-4.1 | RFC 6763 §4.1 - Structured Service Instance Names}
* @see {@link https://datatracker.ietf.org/doc/html/rfc2782 | RFC 2782 - A DNS RR for specifying the location of services (DNS SRV)}
*/
private getRecordSRV;
/**
* Constructs a TXT record carrying metadata key-value pairs about the service.
*
* Keys should be lowercase and limited in length. Empty value fields are valid.
*
* @returns A TXT record representing service metadata.
*
* The name of a TXT record is the same name as the SRV record
* @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-6 | RFC 6763 §6 - Data Syntax for DNS-SD TXT Records}
*/
private getRecordTXT;
/**
* Constructs an A record mapping the hostname to an IPv4 address.
*
* @param ip - The IPv4 address associated with the service host.
* @returns An A record for IPv4 address resolution.
* @see {@link https://datatracker.ietf.org/doc/html/rfc6762#section-6 | RFC 6762 §6 - Responding}
*/
private getRecordA;
/**
* Constructs an AAAA record mapping the hostname to an IPv6 address.
*
* @param ip - The IPv6 address associated with the service host.
* @returns An AAAA record for IPv6 address resolution.
* @see {@link https://datatracker.ietf.org/doc/html/rfc6762#section-6 | RFC 6762 §6 - Responding}
*/
private getRecordAAAA;
}
declare interface ServiceEventMap {
up: [];
down: [];
}
/**
* Options used to configure a service instance.
*/
export declare interface ServiceOptions {
/**
* The protocol used by the service, typically "tcp" or "udp".
* @default 'tcp'
*/
protocol?: string;
/**
* The service type (e.g., "http", "ipp").
* Used to form the full service type domain.
*/
type: string;
/**
* Optional list of subtype identifiers for selective discovery.
*/
subtypes?: string[];
/**
* The instance name of the service.
* This will be sanitized by replacing dots with dashes.
* @example 'MyPrinter'
*/
name: string;
/**
* The hostname of the machine offering the service.
* @default os.hostname()
*/
host?: string;
/**
* The port number on which the service is listening.
*/
port: number;
/**
* Optional TXT record key-value pairs to advertise service metadata.
*/
txt?: Record<string, string | number | boolean | Buffer>;
/**
* The TTL (time to live) in seconds for the all records.
* @remarks The same TTL is set to all records (PTR, SRV, TXT, A, AAAA) for simplicity and compatibility.
* @default 4500
*/
ttl?: number;
/**
* Whether to perform probing to detect name conflicts before publishing.
* @default true
*/
probe?: boolean;
/**
* Whether to automatically resolve name conflicts by appending a number to the service name.
* @default true
*/
probeAutoResolve?: boolean;
/**
* Whether to disable publishing IPv6 addresses for this service.
* @default false
*/
disableIPv6?: boolean;
}
/**
* Represents a network service type with optional protocol and subtype.
*/
export declare class ServiceType {
readonly name?: string;
readonly protocol?: string;
readonly subtype?: string;
constructor(name?: string, protocol?: string, subtype?: string);
/**
* Parses a service type string into a ServiceType instance.
*
* Expected formats include:
* - "_http._tcp"
* - "_printer._sub._http._tcp" (a subtype "printer" of "_http._tcp")
*
* @param {string} input - The service type string to parse.
* @returns {ServiceType} The parsed ServiceType instance.
*
* @example
* ServiceType.fromString('_http._tcp');
* // { name: "http", protocol: "tcp", subtype: undefined }
*
* ServiceType.fromString('_printer._sub._http._tcp');
* // { name: "http", protocol: "tcp", subtype: "printer" }
*
* @see {@link https://datatracker.ietf.org/doc/html/rfc6763#section-7.1 | RFC 6763 §7.1 Selective Instance Enumeration (Subtypes)}
*/
static fromString(input: string): ServiceType;
/**
* Serializes the ServiceType instance into a string following DNS-SD format.
*
* @returns {string} The serialized service type string.
*
* @example
* const svc = new ServiceType('http', 'tcp');
* console.log(svc.toString()); // _http._tcp
*
* const svcWithSubtype = new ServiceType('http', 'tcp', 'printer');
* console.log(svcWithSubtype.toString()); // _printer._sub._http._tcp
*/
toString(): string;
}
export { }