UNPKG

@collight/dns-sd

Version:

A DNS Service Discovery implementation in modern TypeScript

741 lines (720 loc) 28.3 kB
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 { }