import assert from "assert" ; import createDebug from "debug" ; import { EventEmitter } from "events" ; import net from "net" ; import { DNSResponseDefinition , RType } from "./coder/DNSPacket" ; import { AAAARecord } from "./coder/records/AAAARecord" ; import { ARecord } from "./coder/records/ARecord" ; import { NSECRecord } from "./coder/records/NSECRecord" ; import { PTRRecord } from "./coder/records/PTRRecord" ; import { SRVRecord } from "./coder/records/SRVRecord" ; import { TXTRecord } from "./coder/records/TXTRecord" ; import { ResourceRecord } from "./coder/ResourceRecord" ; import { Protocol , Responder } from "./index" ; import { InterfaceName , IPAddress , NetworkManager , NetworkUpdate } from "./NetworkManager" ; import { Announcer } from "./responder/Announcer" ; import { dnsLowerCase } from "./util/dns-equal" ; import * as domainFormatter from "./util/domain-formatter" ; import { formatReverseAddressPTRName } from "./util/domain-formatter" ; import Timeout = NodeJS . Timeout ; const debug = createDebug ( "ciao:CiaoService" ) ; const numberedServiceNamePattern = /^(.*) \((\d+)\)$/ ; const numberedHostnamePattern = /^(.*)-\((\d+)\)(\.\w{2,}.)$/ ; /**
 * This enum defines some commonly used service types.
 * This is also referred to as service name (as of RFC 6763).
 * A service name must not be longer than 15 characters (RFC 6763 7.2).
 */ export const enum ServiceType { AIRDROP = "airdrop" , AIRPLAY = "airplay" , AIRPORT = "airport" , COMPANION_LINK = "companion-link" , DACP = "dacp" , HAP = "hap" , HOMEKIT = "homekit" , HTTP = "http" , HTTP_ALT = "http_alt" , IPP = "ipp" , IPPS = "ipps" , RAOP = "raop" , scanner = "scanner" , TOUCH_ABLE = "touch-able" , DNS_SD = "dns-sd" , PRINTER = "printer" } /**
 * Service options supplied when creating a new ciao service.
 */ export interface ServiceOptions { /**
     * Instance name of the service
     */ name : string ; /**
     * Type of the service.
     */ type : ServiceType | string ; /**
     * Optional array of subtypes of the service.
     * Refer to  ServiceType for some known examples.
      subtypes ? : ( ServiceType | string ) [ ] ; /**
     * Port of the service.
     * If not supplied it must be set later via  updatePort BEFORE advertising the service.
      port ? : number ; /**
     * The protocol the service uses. Default is TCP.
     */ protocol ? : Protocol ; /**
     * Defines a hostname under which the service can be reached.
     * The specified hostname must not include the TLD.
     * If undefined the service name will be used as default.
     */ hostname ? : string ; /**
     * If defined, a txt record will be published with the given service.
     */ txt ? : ServiceTxt ; /**
     * Adds ability to set custom domain. Will default to "local".
     * The domain will also be automatically appended to the hostname.
     */ domain ? : string ; /**
     * If defined it restricts the service to be advertised on the specified
     * ip addresses or interface names.
     *
     * If a interface name is specified, ANY address on that given interface will be advertised
     * (if a IP address of the given interface is also given in the array, it will be overridden).
     * If a IP address is specified, the service will only be advertised for the given addresses.
     *
     * Interface names and addresses can be mixed in the array.
     * If an ip address is given, the ip address must be valid at the time of service creation.
     *
     * If the service is set to advertise on a given interface, though the MDNSServer is
     * configured to ignore this interface, the service won't be advertised on the interface.
     */ restrictedAddresses ? : ( InterfaceName | IPAddress ) [ ] ; /**
     * The service won't advertise ipv6 address records.
     * This can be used to simulate binding on 0.0.0.0.
     * May be combined with  restrictedAddresses .
      disabledIpv6 ? : boolean ; } /**
 * A service txt consist of multiple key=value pairs,
 * which get advertised on the network.
 */ export type ServiceTxt = Record < string , any > ; private export const enum ServiceState { UNANNOUNCED = "unannounced" , PROBING = "probing" , PROBED = "probed" , ANNOUNCING = "announcing" , ANNOUNCED = "announced" } private export interface ServiceRecords { ptr : PTRRecord ; subtypePTRs ? : PTRRecord [ ] ; metaQueryPtr : PTRRecord ; srv : SRVRecord ; txt : TXTRecord ; serviceNSEC : NSECRecord ; a : Record < InterfaceName , ARecord > ; aaaa : Record < InterfaceName , AAAARecord > ; aaaaR : Record < InterfaceName , AAAARecord > ; aaaaULA : Record < InterfaceName , AAAARecord > ; reverseAddressPTRs : Record < IPAddress , PTRRecord > ; addressNSEC : NSECRecord ; } /**
 * Events thrown by a CiaoService
 */ export const enum ServiceEvent { /**
     * Event is called when the Prober identifies that the name for the service is already used
     * and thus resolve the name conflict by adjusting the name (e.g. adding '(2)' to the name).
     * This change must be persisted and thus a listener must hook up to this event
     * in order for the name to be persisted.
     */ NAME_CHANGED = "name-change" , /**
     * Event is called when the Prober identifies that the hostname for the service is already used
     * and thus resolve the name conflict by adjusting the hostname (e.g. adding '(2)' to the hostname).
     * The name change must be persisted. As the hostname is an optional parameter, it is derived
     * from the service name if not supplied.
     * If you supply a custom hostname (not automatically derived from the service name) you must
     * hook up a listener to this event in order for the hostname to be persisted.
     */ HOSTNAME_CHANGED = "hostname-change" } private export const enum InternalServiceEvent { PUBLISH = "publish" , UNPUBLISH = "unpublish" , REPUBLISH = "republish" , RECORD_UPDATE = "records-update" , RECORD_UPDATE_ON_INTERFACE = "records-update-interface" } private export type PublishCallback = ( error ? : Error ) => void ; private export type UnpublishCallback = ( error ? : Error ) => void ; private export type RecordsUpdateCallback = ( error ? : Error | null ) => void ; export declare interface CiaoService { on ( event , listener ) : this ; on ( event , listener ) : this ; private on ( event , listener ) : this ; private on ( event , listener ) : this ; private on ( event , listener ) : this ; private on ( event , listener ) : this ; private on ( event , listener ) : this ; private emit ( event , name ) : boolean ; private emit ( event , hostname ) : boolean ; private emit ( event , callback ) : boolean ; private emit ( event , callback ) : boolean ; private emit ( event , callback ? ) : boolean ; private emit ( event , response , callback ? ) : boolean ; private emit ( event , name , records , callback ? ) : boolean ; } /**
 * The CiaoService class represents a service which can be advertised on the network.
 *
 * A service is identified by it's fully qualified domain name (FQDN), which consist of
 * the service name, the service type, the protocol and the service domain (.local by default).
 *
 * The service defines a hostname and a port where the advertised service can be reached.
 *
 * Additionally a TXT record can be published, which can contain information (in form of key-value pairs),
 * which might be useful to a querier.
 *
 * A CiaoService class is always bound to a  Responder and can be created using the
 *  Responder . createService method in the Responder class.
 * Once the instance is created,  advertise can be called to announce the service on the network.
  export class CiaoService extends EventEmitter { private readonly networkManager : NetworkManager ; private name : string ; private readonly type : ServiceType | string ; private readonly subTypes ? : string [ ] ; private readonly protocol : Protocol ; private readonly serviceDomain : string ; private fqdn : string ; private loweredFqdn : string ; private readonly typePTR : string ; private readonly loweredTypePTR : string ; private readonly subTypePTRs ? : string [ ] ; private hostname : string ; private loweredHostname : string ; private port ? : number ; private readonly restrictedAddresses ? : Map < InterfaceName , IPAddress [ ] > ; private readonly disableIpv6 ? : boolean ; private txt : Buffer [ ] ; private txtTimer ? : Timeout ; private serviceState = ServiceState . UNANNOUNCED ; /**
     * If service is in state  ServiceState . ANNOUNCING the  Announcer responsible for the
     * service will be linked here. This is need to cancel announcing when for example the service
     * should be terminated and we sill aren't fully announced yet.
     *  private is controlled by the  Responder instance
      currentAnnouncer ? : Announcer ; private serviceRecords ? : ServiceRecords ; private destroyed = false ; /**
     * Constructs a new service. Please use  Responder . createService to create new service.
     * When calling the constructor a callee must listen to certain events in order to provide
     * correct functionality.
     *  private constructor ( networkManager , options ) { super ( ) ; assert ( networkManager , "networkManager is required" ) ; assert ( options , "parameters options is required" ) ; assert ( options . name , "service options parameter 'name' is required" ) ; assert ( options . type , "service options parameter 'type' is required" ) ; assert ( options . type . length <= 15 , "service options parameter 'type' must not be longer than 15 characters" ) ; this . networkManager = networkManager ; this . name = options . name ; this . type = options . type ; this . subTypes = options . subtypes ; this . protocol = options . protocol || Protocol . TCP ; this . serviceDomain = options . domain || "local" ; this . fqdn = this . formatFQDN ( ) ; this . loweredFqdn = dnsLowerCase ( this . fqdn ) ; this . typePTR = domainFormatter . stringify ( { type : this . type , protocol : this . protocol , domain : this . serviceDomain , } ) ; this . loweredTypePTR = dnsLowerCase ( this . typePTR ) ; if ( this . subTypes ) { this . subTypePTRs = this . subTypes . map ( subtype => domainFormatter . stringify ( { subtype : subtype , type : this . type , protocol : this . protocol , domain : this . serviceDomain , } ) ) . map ( dnsLowerCase ) ; } this . hostname = domainFormatter . formatHostname ( options . hostname || this . name , this . serviceDomain ) . replace ( / /g , "-" ) ; this . loweredHostname = dnsLowerCase ( this . hostname ) ; this . port = options . port ; if ( options . restrictedAddresses ) { assert ( options . restrictedAddresses . length , "The service property 'restrictedAddresses' cannot be an empty array!" ) ; this . restrictedAddresses = new Map ( ) ; for ( const entry of options . restrictedAddresses ) { if ( net . isIP ( entry ) ) { if ( entry === "0.0.0.0" || entry === "::" ) { throw new Error ( `[${ this . fqdn }] Unspecified ip address (${ entry }) cannot be used to restrict on to!` ) ; } const interfaceName = NetworkManager . resolveInterface ( entry ) ; if ( ! interfaceName ) { throw new Error ( `[${ this . fqdn }] Could not restrict service to address ${ entry } as we could not resolve it to an interface name!` ) ; } const current = this . restrictedAddresses . get ( interfaceName ) ; if ( current ) { if ( current . length && ! current . includes ( entry ) ) { current . push ( entry ) ; } } else { this . restrictedAddresses . set ( interfaceName , [ entry ] ) ; } } else { this . restrictedAddresses . set ( entry , [ ] ) ; } } } this . disableIpv6 = options . disabledIpv6 ; this . txt = options . txt ? CiaoService . txtBuffersFromRecord ( options . txt ) : [ ] ; this . incrementName ( true ) ; } /**
     * This method start the advertising process of the service:
     *  - The service name (and hostname) will be probed unique on all interfaces (as defined in RFC 6762 8.1).
     *  - Once probed unique the service will be announced (as defined in RFC 6762 8.3).
     *
     *  The returned promise resolves once the last announcement packet was successfully sent on all network interfaces.
     *  The promise might be rejected caused by one of the following reasons:
     *    - A probe query could not be sent successfully
     *    - Prober could not find a unique service name while trying for a minute (timeout)
     *    - One of the announcement packets could not be sent successfully
     */ public advertise ( ) { assert ( ! this . destroyed , "Cannot publish destroyed service!" ) ; assert ( this . port , "Service port must be defined before advertising the service on the network!" ) ; if ( this . listeners ( ServiceEvent . NAME_CHANGED ) . length === 0 ) { debug ( "[%s] WARN: No listeners found for a potential name change on the 'name-change' event!" , this . name ) ; } return new Promise ( ( resolve , reject ) => { this . emit ( InternalServiceEvent . PUBLISH , error => error ? reject ( error ) : resolve ( ) ) ; } ) ; } returns public end ( ) { assert ( ! this . destroyed , "Cannot end destroyed service!" ) ; if ( this . serviceState === ServiceState . UNANNOUNCED ) { return Promise . resolve ( ) ; } return new Promise ( ( resolve , reject ) => { this . emit ( InternalServiceEvent . UNPUBLISH , error => error ? reject ( error ) : resolve ( ) ) ; } ) ; } /**
     * This method must be called if you want to free the memory used by this service.
     * The service instance is not usable anymore after this call.
     *
     * If the service is still announced, the service will first be removed
     * from the network by calling  end .
     *
     *  returns public async destroy ( ) { await this . end ( ) ; this . destroyed = true ; this . removeAllListeners ( ) ; } returns public getFQDN ( ) { return this . fqdn ; } returns public getTypePTR ( ) { return this . typePTR ; } returns public getLowerCasedSubtypePTRs ( ) { return this . subTypePTRs ; } returns public getHostname ( ) { return this . hostname ; } returns public getPort ( ) { return this . port || - 1 ; } returns private public getTXT ( ) { return this . txt ; } private used for internal comparison  dnsLowerCase public getLowerCasedFQDN ( ) { return this . loweredFqdn ; } private used for internal comparison  dnsLowerCase public getLowerCasedTypePTR ( ) { return this . loweredTypePTR ; } private used for internal comparison  dnsLowerCase public getLowerCasedHostname ( ) { return this . loweredHostname ; } param { ServiceTxt } txt param { boolean } silent public updateTxt ( txt , silent = false ) { assert ( ! this . destroyed , "Cannot update destroyed service!" ) ; assert ( txt , "txt cannot be undefined" ) ; this . txt = CiaoService . txtBuffersFromRecord ( txt ) ; debug ( "[%s] Updating txt record%s..." , this . name , silent ? " silently" : "" ) ; if ( this . serviceState === ServiceState . ANNOUNCING ) { this . rebuildServiceRecords ( ) ; if ( silent ) { return ; } if ( this . currentAnnouncer ! . hasSentLastAnnouncement ( ) ) { this . currentAnnouncer ! . awaitAnnouncement ( ) . then ( ( ) => { this . queueTxtUpdate ( ) ; } ) ; } } else if ( this . serviceState === ServiceState . ANNOUNCED ) { this . rebuildServiceRecords ( ) ; if ( silent ) { return ; } this . queueTxtUpdate ( ) ; } } private queueTxtUpdate ( ) { if ( this . txtTimer ) { return ; } else { this . txtTimer = setTimeout ( ( ) => { this . txtTimer = undefined ; if ( this . serviceState !== ServiceState . ANNOUNCED ) { return ; } this . emit ( InternalServiceEvent . RECORD_UPDATE , { answers : [ this . txtRecord ( ) ] , additionals : [ this . serviceNSECRecord ( ) ] , } ) ; } , 50 ) ; } } param { number } port public updatePort ( port ) { assert ( this . serviceState === ServiceState . UNANNOUNCED , "Port number cannot be changed when service is already advertised!" ) ; this . port = port ; } param name private public updateName ( name ) { if ( this . serviceState === ServiceState . UNANNOUNCED ) { this . name = name ; this . fqdn = this . formatFQDN ( ) ; this . loweredFqdn = dnsLowerCase ( this . fqdn ) ; return Promise . resolve ( ) ; } else { return this . end ( ) . then ( ( ) => { this . name = name ; this . fqdn = this . formatFQDN ( ) ; this . loweredFqdn = dnsLowerCase ( this . fqdn ) ; return this . advertise ( ) ; } ) ; } } private static txtBuffersFromRecord ( txt ) { const result = [ ] ; Object . entries ( txt ) . forEach ( ( [ key , value ] ) => { const entry = key + "=" + value ; result . push ( Buffer . from ( entry ) ) ; } ) ; return result ; } param networkUpdate private handleNetworkInterfaceUpdate ( networkUpdate ) { assert ( ! this . destroyed , "Cannot update network of destroyed service!" ) ; if ( this . serviceState !== ServiceState . ANNOUNCED ) { if ( this . serviceState === ServiceState . ANNOUNCING ) { this . rebuildServiceRecords ( ) ; if ( this . currentAnnouncer ! . hasSentLastAnnouncement ( ) ) { this . currentAnnouncer ! . awaitAnnouncement ( ) . then ( ( ) => { this . handleNetworkInterfaceUpdate ( networkUpdate ) ; } ) ; } } return ; } this . rebuildServiceRecords ( ) ; if ( networkUpdate . added || networkUpdate . changes ) { this . emit ( InternalServiceEvent . REPUBLISH , error => { if ( error ) { console . log ( "FATAL Error occurred trying to reannounce service " + this . fqdn + "! We can't recover from this!" ) ; console . log ( error . stack ) ; process . exit ( 1 ) ; } } ) ; } } private must only be called by the  Prober incrementName ( nameCheckOnly ? ) { if ( this . serviceState !== ServiceState . UNANNOUNCED ) { throw new Error ( "Service name can only be incremented when in state UNANNOUNCED!" ) ; } const oldName = this . name ; const oldHostname = this . hostname ; let nameBase ; let nameNumber ; let hostnameBase ; let hostnameTLD ; let hostnameNumber ; const nameMatcher = this . name . match ( numberedServiceNamePattern ) ; if ( nameMatcher ) { nameBase = nameMatcher [ 1 ] ; nameNumber = parseInt ( nameMatcher [ 2 ] ) ; assert ( nameNumber , `Failed to extract name number from ${ this . name }. Resulted in ${ nameNumber }` ) ; } else { nameBase = this . name ; nameNumber = 1 ; } const hostnameMatcher = this . hostname . match ( numberedHostnamePattern ) ; if ( hostnameMatcher ) { hostnameBase = hostnameMatcher [ 1 ] ; hostnameTLD = hostnameMatcher [ 3 ] ; hostnameNumber = parseInt ( hostnameMatcher [ 2 ] ) ; assert ( hostnameNumber , `Failed to extract hostname number from ${ this . hostname }. Resulted in ${ hostnameNumber }` ) ; } else { const lastDot = this . hostname . substring ( 0 , this . hostname . length - 1 ) . lastIndexOf ( "." ) ; hostnameBase = this . hostname . slice ( 0 , lastDot ) ; hostnameTLD = this . hostname . slice ( lastDot ) ; hostnameNumber = 1 ; } if ( ! nameCheckOnly ) { nameNumber ++ ; hostnameNumber ++ ; } const newNumber = Math . max ( nameNumber , hostnameNumber ) ; this . name = newNumber === 1 ? nameBase : `${ nameBase } (${ newNumber })` ; this . hostname = newNumber === 1 ? `${ hostnameBase }${ hostnameTLD }` : `${ hostnameBase }-(${ newNumber })${ hostnameTLD }` ; this . loweredHostname = dnsLowerCase ( this . hostname ) ; this . fqdn = this . formatFQDN ( ) ; this . loweredFqdn = dnsLowerCase ( this . fqdn ) ; if ( this . name !== oldName || this . hostname !== oldHostname ) { debug ( "[%s] Service changed name '%s' -> '%s', '%s' -> '%s'" , this . name , oldName , this . name , oldHostname , this . hostname ) ; } if ( ! nameCheckOnly ) { this . rebuildServiceRecords ( ) ; } } private informAboutNameUpdates ( ) { const nameCalled = this . emit ( ServiceEvent . NAME_CHANGED , this . name ) ; const hostnameCalled = this . emit ( ServiceEvent . HOSTNAME_CHANGED , domainFormatter . removeTLD ( this . hostname ) ) ; if ( ! nameCalled && ! hostnameCalled ) { console . warn ( `CIAO: [${ this . name }] Service changed name but nobody was listening on the 'name-change' event!` ) ; } } private formatFQDN ( ) { if ( this . serviceState !== ServiceState . UNANNOUNCED ) { throw new Error ( "Name can't be changed after service was already announced!" ) ; } const fqdn = domainFormatter . stringify ( { name : this . name , type : this . type , protocol : this . protocol , domain : this . serviceDomain , } ) ; assert ( fqdn . length <= 255 , "A fully qualified domain name cannot be longer than 255 characters" ) ; return fqdn ; } private rebuildServiceRecords ( ) { assert ( this . port , "port must be set before building records" ) ; debug ( "[%s] Rebuilding service records..." , this . name ) ; const aRecordMap = { } ; const aaaaRecordMap = { } ; const aaaaRoutableRecordMap = { } ; const aaaaUniqueLocalRecordMap = { } ; const reverseAddressMap = { } ; let subtypePTRs = undefined ; for ( const [ name , networkInterface ] of this . networkManager . getInterfaceMap ( ) ) { if ( ! this . advertisesOnInterface ( name , true ) ) { continue ; } let restrictedAddresses = this . restrictedAddresses ? this . restrictedAddresses . get ( name ) : undefined ; if ( restrictedAddresses && restrictedAddresses . length === 0 ) { restrictedAddresses = undefined ; } if ( networkInterface . ipv4 && ( ! restrictedAddresses || restrictedAddresses . includes ( networkInterface . ipv4 ) ) ) { aRecordMap [ name ] = new ARecord ( this . hostname , networkInterface . ipv4 , true ) ; reverseAddressMap [ networkInterface . ipv4 ] = new PTRRecord ( formatReverseAddressPTRName ( networkInterface . ipv4 ) , this . hostname ) ; } if ( networkInterface . ipv6 && ! this . disableIpv6 && ( ! restrictedAddresses || restrictedAddresses . includes ( networkInterface . ipv6 ) ) ) { aaaaRecordMap [ name ] = new AAAARecord ( this . hostname , networkInterface . ipv6 , true ) ; reverseAddressMap [ networkInterface . ipv6 ] = new PTRRecord ( formatReverseAddressPTRName ( networkInterface . ipv6 ) , this . hostname ) ; } if ( networkInterface . globallyRoutableIpv6 && ! this . disableIpv6 && ( ! restrictedAddresses || restrictedAddresses . includes ( networkInterface . globallyRoutableIpv6 ) ) ) { aaaaRoutableRecordMap [ name ] = new AAAARecord ( this . hostname , networkInterface . globallyRoutableIpv6 , true ) ; reverseAddressMap [ networkInterface . globallyRoutableIpv6 ] = new PTRRecord ( formatReverseAddressPTRName ( networkInterface . globallyRoutableIpv6 ) , this . hostname ) ; } if ( networkInterface . uniqueLocalIpv6 && ! this . disableIpv6 && ( ! restrictedAddresses || restrictedAddresses . includes ( networkInterface . uniqueLocalIpv6 ) ) ) { aaaaUniqueLocalRecordMap [ name ] = new AAAARecord ( this . hostname , networkInterface . uniqueLocalIpv6 , true ) ; reverseAddressMap [ networkInterface . uniqueLocalIpv6 ] = new PTRRecord ( formatReverseAddressPTRName ( networkInterface . uniqueLocalIpv6 ) , this . hostname ) ; } } if ( this . subTypePTRs ) { subtypePTRs = [ ] ; for ( const ptr of this . subTypePTRs ) { subtypePTRs . push ( new PTRRecord ( ptr , this . fqdn ) ) ; } } this . serviceRecords = { ptr : new PTRRecord ( this . typePTR , this . fqdn ) , subtypePTRs : subtypePTRs , metaQueryPtr : new PTRRecord ( Responder . SERVICE_TYPE_ENUMERATION_NAME , this . typePTR ) , srv : new SRVRecord ( this . fqdn , this . hostname , this . port ! , true ) , txt : new TXTRecord ( this . fqdn , this . txt , true ) , serviceNSEC : new NSECRecord ( this . fqdn , this . fqdn , [ RType . TXT , RType . SRV ] , 4500 , true ) , a : aRecordMap , aaaa : aaaaRecordMap , aaaaR : aaaaRoutableRecordMap , aaaaULA : aaaaUniqueLocalRecordMap , reverseAddressPTRs : reverseAddressMap , addressNSEC : new NSECRecord ( this . hostname , this . hostname , [ RType . A , RType . AAAA ] , 120 , true ) , } ; } param name param skipAddressCheck private advertisesOnInterface ( name , skipAddressCheck ? ) { return ! this . restrictedAddresses || this . restrictedAddresses . has ( name ) && ( skipAddressCheck || ! ! this . serviceRecords ?. a [ name ] || ! ! this . serviceRecords ?. aaaa [ name ] || ! ! this . serviceRecords ?. aaaaR [ name ] || ! ! this . serviceRecords ?. aaaaULA [ name ] ) ; } private ptrRecord ( ) { return this . serviceRecords ! . ptr . clone ( ) ; } private subtypePtrRecords ( ) { return this . serviceRecords ! . subtypePTRs ? ResourceRecord . clone ( this . serviceRecords ! . subtypePTRs ) : [ ] ; } private metaQueryPtrRecord ( ) { return this . serviceRecords ! . metaQueryPtr . clone ( ) ; } private srvRecord ( ) { return this . serviceRecords ! . srv . clone ( ) ; } private txtRecord ( ) { return this . serviceRecords ! . txt . clone ( ) ; } private aRecord ( name ) { const record = this . serviceRecords ! . a [ name ] ; return record ? record . clone ( ) : undefined ; } private aaaaRecord ( name ) { const record = this . serviceRecords ! . aaaa [ name ] ; return record ? record . clone ( ) : undefined ; } private aaaaRoutableRecord ( name ) { const record = this . serviceRecords ! . aaaaR [ name ] ; return record ? record . clone ( ) : undefined ; } private aaaaUniqueLocalRecord ( name ) { const record = this . serviceRecords ! . aaaaULA [ name ] ; return record ? record . clone ( ) : undefined ; } private allAddressRecords ( ) { const records = [ ] ; Object . values ( this . serviceRecords ! . a ) . forEach ( record => { records . push ( record . clone ( ) ) ; } ) ; Object . values ( this . serviceRecords ! . aaaa ) . forEach ( record => { records . push ( record . clone ( ) ) ; } ) ; Object . values ( this . serviceRecords ! . aaaaR ) . forEach ( record => { records . push ( record . clone ( ) ) ; } ) ; Object . values ( this . serviceRecords ! . aaaaULA ) . forEach ( record => { records . push ( record . clone ( ) ) ; } ) ; return records ; } private addressNSECRecord ( ) { return this . serviceRecords ! . addressNSEC . clone ( ) ; } private serviceNSECRecord ( shortenTTL = false ) { const record = this . serviceRecords ! . serviceNSEC . clone ( ) ; if ( shortenTTL ) { record . ttl = 120 ; } return record ; } param address private hasAddress ( address ) { return ! ! this . serviceRecords ! . reverseAddressPTRs [ address ] ; } }