import assert from "assert" ; import createDebug from "debug" ; import dgram , { Socket } from "dgram" ; import { AddressInfo } from "net" ; import { CiaoService } from "./CiaoService" ; import { DNSPacket , DNSProbeQueryDefinition , DNSQueryDefinition , DNSResponseDefinition , OpCode , PacketType , RCode , } from "./coder/DNSPacket" ; import { InterfaceName , IPFamily , NetworkInterface , NetworkManager , NetworkManagerEvent , NetworkUpdate , } from "./NetworkManager" ; import { getNetAddress } from "./util/domain-formatter" ; import { InterfaceNotFoundError , ServerClosedError } from "./util/errors" ; import { PromiseTimeout } from "./util/promise-utils" ; const debug = createDebug ( "ciao:MDNSServer" ) ; export interface EndpointInfo { address : string ; port : number ; interface : string ; } export interface SendFulfilledResult { status : "fulfilled" ; interface : InterfaceName ; } export interface SendRejectedResult { status : "rejected" ; interface : InterfaceName ; reason : Error ; } export interface SendTimeoutResult { status : "timeout" ; interface : InterfaceName ; } export type SendResult = SendFulfilledResult | SendRejectedResult ; export type TimedSendResult = SendFulfilledResult | SendRejectedResult | SendTimeoutResult ; export type SendCallback = ( error ? : Error | null ) => void ; param results - Array of  SendResult export function SendResultFailedRatio ( results ) { if ( results . length === 0 ) { return 0 ; } let failedCount = 0 ; for ( const result of results ) { if ( result . status !== "fulfilled" ) { failedCount ++ ; } } return failedCount / results . length ; } export function SendResultFormatError ( results , prefix ? , includeStack = false ) { let failedCount = 0 ; for ( const result of results ) { if ( result . status !== "fulfilled" ) { failedCount ++ ; } } if ( ! prefix ) { prefix = "Failed to send packets" ; } if ( failedCount < results . length ) { prefix += ` (${ failedCount }/${ results . length }):` ; } else { prefix += ":" ; } if ( includeStack ) { let string = "=============================\n" + prefix ; for ( const result of results ) { if ( result . status === "rejected" ) { string += "\n--------------------\n" + "Failed to send packet on interface " + result . interface + ": " + result . reason . stack ; } else if ( result . status === "timeout" ) { string += "\n--------------------\n" + "Sending packet on interface " + result . interface + " timed out!" ; } } string += "\n=============================" ; return string ; } else { let string = prefix ; for ( const result of results ) { if ( result . status === "rejected" ) { string += "\n- Failed to send packet on interface " + result . interface + ": " + result . reason . message ; } else if ( result . status === "timeout" ) { string += "\n- Sending packet on interface " + result . interface + " timed out!" ; } } return string ; } } /**
 * Defines the options passed to the underlying mdns server.
 */ export interface MDNSServerOptions { /**
     * If specified, the mdns server will only listen on the specified interfaces (allowlist).
     * It can be supplied as a string (representing a single interface) or as an array of strings
     * to define multiple interfaces.
     * The interface can be defined by specifying the interface name (like 'en0') or
     * by specifying an ip address.
     */ interface ? : string | string [ ] ; /**
     * If specified, the mdns server will not include any ipv6 address records
     * and not bind any udp6 sockets.
     * This is handy if you want to "bind" on 0.0.0.0 only.
     */ disableIpv6 ? : boolean ; } export interface PacketHandler { handleQuery ( packet , rinfo ) : void ; handleResponse ( packet , rinfo ) : void ; } /**
 * This class can be used to create a mdns server to send and receive mdns packets on the local network.
 *
 * Currently only udp4 sockets will be advertised.
 */ export class MDNSServer { public static readonly DEFAULT_IP4_HEADER = 20 ; public static readonly DEFAULT_IP6_HEADER = 40 ; public static readonly UDP_HEADER = 8 ; public static readonly MDNS_PORT = 5353 ; public static readonly MDNS_TTL = 255 ; public static readonly MULTICAST_IPV4 = "224.0.0.251" ; public static readonly MULTICAST_IPV6 = "FF02::FB" ; public static readonly SEND_TIMEOUT = 200 ; private readonly handler : PacketHandler ; private readonly networkManager : NetworkManager ; private readonly sockets : Map < InterfaceName , Socket > = new Map ( ) ; private readonly sentPackets : Map < InterfaceName , string [ ] > = new Map ( ) ; private suppressUnicastResponseFlag = false ; private bound = false ; private closed = false ; constructor ( handler , options ? ) { assert ( handler , "handler cannot be undefined" ) ; this . handler = handler ; this . networkManager = new NetworkManager ( { interface : options && options . interface , excludeIpv6 : options && options . disableIpv6 , excludeIpv6Only : true , } ) ; this . networkManager . on ( NetworkManagerEvent . NETWORK_UPDATE , this . handleUpdatedNetworkInterfaces . bind ( this ) ) ; } public getNetworkManager ( ) { return this . networkManager ; } public getBoundInterfaceNames ( ) { return this . sockets . keys ( ) ; } public async bind ( ) { if ( this . closed ) { throw new Error ( "Cannot rebind closed server!" ) ; } this . suppressUnicastResponseFlag = true ; await this . networkManager . waitForInit ( ) ; const promises = [ ] ; for ( const [ name , networkInterface ] of this . networkManager . getInterfaceMap ( ) ) { const socket = this . createDgramSocket ( name , true ) ; const promise = this . bindSocket ( socket , networkInterface , IPFamily . IPv4 ) . catch ( reason => { console . log ( "Could not bind detected network interface: " + reason . stack ) ; } ) ; promises . push ( promise ) ; } return Promise . all ( promises ) . then ( ( ) => { this . bound = true ; } ) ; } public shutdown ( ) { this . networkManager . shutdown ( ) ; for ( const socket of this . sockets . values ( ) ) { socket . close ( ) ; } this . bound = false ; this . closed = true ; this . sockets . clear ( ) ; } public sendQueryBroadcast ( query , service ) { const packets = DNSPacket . createDNSQueryPackets ( query ) ; if ( packets . length > 1 ) { debug ( "Query broadcast is split into %d packets!" , packets . length ) ; } const promises = [ ] ; for ( const packet of packets ) { promises . push ( this . sendOnAllNetworksForService ( packet , service ) ) ; } return Promise . all ( promises ) . then ( ( values ) => { const results = [ ] ; for ( const value of values ) { results . concat ( value ) ; } return results ; } ) ; } public sendResponseBroadcast ( response , service ) { const packet = DNSPacket . createDNSResponsePacketsFromRRSet ( response ) ; return this . sendOnAllNetworksForService ( packet , service ) ; } public sendResponse ( response , endpoint , callback ? ) ; public sendResponse ( response , interfaceName , callback ? ) ; public sendResponse ( response , endpointOrInterface , callback ? ) { this . send ( response , endpointOrInterface ) . then ( result => { if ( result . status === "rejected" ) { if ( callback ) { callback ( new Error ( "Encountered socket error on " + result . reason . name + ": " + result . reason . message ) ) ; } else { MDNSServer . logSocketError ( result . interface , result . reason ) ; } } else if ( callback ) { callback ( ) ; } } ) ; } private sendOnAllNetworksForService ( packet , service ) { this . checkUnicastResponseFlag ( packet ) ; const message = packet . encode ( ) ; this . assertBeforeSend ( message , IPFamily . IPv4 ) ; const promises = [ ] ; for ( const [ name , socket ] of this . sockets ) { if ( ! service . advertisesOnInterface ( name ) ) { continue ; } const promise = new Promise < SendResult > ( resolve => { socket . send ( message , MDNSServer . MDNS_PORT , MDNSServer . MULTICAST_IPV4 , error => { if ( error ) { if ( ! MDNSServer . isSilencedSocketError ( error ) ) { resolve ( { status : "rejected" , interface : name , reason : error , } ) ; return ; } } else { this . maintainSentPacketsInterface ( name , message ) ; } resolve ( { status : "fulfilled" , interface : name , } ) ; } ) ; } ) ; promises . push ( Promise . race ( [ promise , PromiseTimeout ( MDNSServer . SEND_TIMEOUT ) . then ( ( ) => < SendTimeoutResult > { status : "timeout" , interface : name , } ) , ] ) ) ; } return Promise . all ( promises ) ; } public send ( packet , endpointOrInterface ) { this . checkUnicastResponseFlag ( packet ) ; const message = packet . encode ( ) ; this . assertBeforeSend ( message , IPFamily . IPv4 ) ; let address ; let port ; let name ; if ( typeof endpointOrInterface === "string" ) { address = MDNSServer . MULTICAST_IPV4 ; port = MDNSServer . MDNS_PORT ; name = endpointOrInterface ; } else { address = endpointOrInterface . address ; port = endpointOrInterface . port ; name = endpointOrInterface . interface ; } const socket = this . sockets . get ( name ) ; if ( ! socket ) { throw new InterfaceNotFoundError ( `Could not find socket for given network interface '${ name }'` ) ; } return new Promise < SendResult > ( resolve => { socket ! . send ( message , port , address , error => { if ( error ) { if ( ! MDNSServer . isSilencedSocketError ( error ) ) { resolve ( { status : "rejected" , interface : name , reason : error , } ) ; return ; } } else { this . maintainSentPacketsInterface ( name , message ) ; } resolve ( { status : "fulfilled" , interface : name , } ) ; } ) ; } ) ; } private checkUnicastResponseFlag ( packet ) { if ( this . suppressUnicastResponseFlag && packet . type === PacketType . QUERY ) { packet . questions . forEach ( record => record . unicastResponseFlag = false ) ; } } private assertBeforeSend ( message , family ) { if ( this . closed ) { throw new ServerClosedError ( "Cannot send packets on a closed mdns server!" ) ; } assert ( this . bound , "Cannot send packets before server is not bound!" ) ; const ipHeaderSize = family === IPFamily . IPv4 ? MDNSServer . DEFAULT_IP4_HEADER : MDNSServer . DEFAULT_IP6_HEADER ; assert ( ipHeaderSize + MDNSServer . UDP_HEADER + message . length <= 9000 , "DNS cannot exceed the size of 9000 bytes even with IP Fragmentation!" ) ; } private maintainSentPacketsInterface ( name , packet ) { const base64 = packet . toString ( "base64" ) ; const packets = this . sentPackets . get ( name ) ; if ( ! packets ) { this . sentPackets . set ( name , [ base64 ] ) ; } else { packets . push ( base64 ) ; } } private checkIfPacketWasPreviouslySentFromUs ( name , packet ) { const base64 = packet . toString ( "base64" ) ; const packets = this . sentPackets . get ( name ) ; if ( packets ) { const index = packets . indexOf ( base64 ) ; if ( index !== - 1 ) { packets . splice ( index , 1 ) ; return true ; } } return false ; } private createDgramSocket ( name , reuseAddr = false , type = "udp4" ) { const socket = dgram . createSocket ( { type : type , reuseAddr : reuseAddr , } ) ; socket . on ( "message" , this . handleMessage . bind ( this , name ) ) ; socket . on ( "error" , error => { if ( ! MDNSServer . isSilencedSocketError ( error ) ) { MDNSServer . logSocketError ( name , error ) ; } } ) ; return socket ; } private bindSocket ( socket , networkInterface , family ) { return new Promise ( ( resolve , reject ) => { const errorHandler = ( error ) : void => reject ( new Error ( "Failed to bind on interface " + networkInterface . name + ": " + error . message ) ) ; socket . once ( "error" , errorHandler ) ; socket . on ( "close" , ( ) => { this . sockets . delete ( networkInterface . name ) ; } ) ; socket . bind ( MDNSServer . MDNS_PORT , ( ) => { socket . setRecvBufferSize ( 800 * 1024 ) ; socket . removeListener ( "error" , errorHandler ) ; const multicastAddress = family === IPFamily . IPv4 ? MDNSServer . MULTICAST_IPV4 : MDNSServer . MULTICAST_IPV6 ; const interfaceAddress = family === IPFamily . IPv4 ? networkInterface . ipv4 : networkInterface . ipv6 ; assert ( interfaceAddress , "Interface address for " + networkInterface . name + " cannot be undefined!" ) ; try { socket . addMembership ( multicastAddress , interfaceAddress ! ) ; socket . setMulticastInterface ( interfaceAddress ! ) ; socket . setMulticastTTL ( MDNSServer . MDNS_TTL ) ; socket . setTTL ( MDNSServer . MDNS_TTL ) ; socket . setMulticastLoopback ( true ) ; this . sockets . set ( networkInterface . name , socket ) ; resolve ( ) ; } catch ( error ) { try { socket . close ( ) ; } catch ( error ) { debug ( "Error while closing socket which failed to bind. Error may be expected: " + error . message ) ; } reject ( new Error ( "Error binding socket on " + networkInterface . name + ": " + error . stack ) ) ; } } ) ; } ) ; } private handleMessage ( name , buffer , rinfo ) { if ( ! this . bound ) { return ; } const networkInterface = this . networkManager . getInterface ( name ) ; if ( ! networkInterface ) { debug ( "Received packet on non existing network interface: %s!" , name ) ; return ; } if ( this . checkIfPacketWasPreviouslySentFromUs ( networkInterface . name , buffer ) ) { return ; } const ip4Netaddress = getNetAddress ( rinfo . address , networkInterface . ip4Netmask ! ) ; if ( networkInterface . loopback ) { if ( ip4Netaddress !== networkInterface . ipv4Netaddress ) { return ; } } else if ( this . networkManager . isLoopbackNetaddressV4 ( ip4Netaddress ) ) { debug ( "Received packet on interface '%s' which is not coming from the same subnet: %o" , name , { address : rinfo . address , netaddress : ip4Netaddress , interface : networkInterface . ipv4 } ) ; return ; } let packet ; try { packet = DNSPacket . decode ( rinfo , buffer ) ; } catch ( error ) { debug ( "Received a malformed packet from %o on interface %s. This might or might not be a problem. " + "Here is the received packet for debugging purposes '%s'. " + "Packet decoding failed with %s" , rinfo , name , buffer . toString ( "base64" ) , error . stack ) ; return ; } if ( packet . opcode !== OpCode . QUERY ) { return ; } if ( packet . rcode !== RCode . NoError ) { return ; } const endpoint = { address : rinfo . address , port : rinfo . port , interface : name , } ; if ( packet . type === PacketType . QUERY ) { try { this . handler . handleQuery ( packet , endpoint ) ; } catch ( error ) { console . warn ( "Error occurred handling incoming (on " + name + ") dns query packet: " + error . stack ) ; } } else if ( packet . type === PacketType . RESPONSE ) { if ( rinfo . port !== MDNSServer . MDNS_PORT ) { return ; } try { this . handler . handleResponse ( packet , endpoint ) ; } catch ( error ) { console . warn ( "Error occurred handling incoming (on " + name + ") dns response packet: " + error . stack ) ; } } } private static isSilencedSocketError ( error ) { const silenced = error . message . includes ( "EADDRNOTAVAIL" ) || error . message . includes ( "EHOSTDOWN" ) || error . message . includes ( "ENETUNREACH" ) || error . message . includes ( "EHOSTUNREACH" ) || error . message . includes ( "EPERM" ) || error . message . includes ( "EINVAL" ) ; if ( silenced ) { debug ( "Silenced and ignored error (This is/should not be a problem, this message is only for informational purposes): " + error . message ) ; } return silenced ; } private static logSocketError ( name , error ) { console . warn ( `Encountered MDNS socket error on socket '${ name }' : ${ error . stack }` ) ; return ; } private handleUpdatedNetworkInterfaces ( networkUpdate ) { if ( networkUpdate . removed ) { for ( const networkInterface of networkUpdate . removed ) { const socket = this . sockets . get ( networkInterface . name ) ; this . sockets . delete ( networkInterface . name ) ; if ( socket ) { socket . close ( ) ; } } } if ( networkUpdate . changes ) { for ( const change of networkUpdate . changes ) { const socket = this . sockets . get ( change . name ) ; assert ( socket , "Couldn't find socket for network change!" ) ; if ( ! change . outdatedIpv4 && change . updatedIpv4 ) { assert . fail ( "Reached illegal state! IPv4 address changed from undefined to defined!" ) ; } else if ( change . outdatedIpv4 && ! change . updatedIpv4 ) { assert . fail ( "Reached illegal state! IPV4 address change from defined to undefined!" ) ; } else if ( change . outdatedIpv4 && change . updatedIpv4 ) { try { socket ! . dropMembership ( MDNSServer . MULTICAST_IPV4 , change . outdatedIpv4 ) ; } catch ( error ) { debug ( "Thrown expected error when dropping outdated address membership: " + error . message ) ; } try { socket ! . addMembership ( MDNSServer . MULTICAST_IPV4 , change . updatedIpv4 ) ; } catch ( error ) { debug ( "Thrown expected error when adding new address membership: " + error . message ) ; } socket ! . setMulticastInterface ( change . updatedIpv4 ) ; } } } if ( networkUpdate . added ) { for ( const networkInterface of networkUpdate . added ) { const socket = this . createDgramSocket ( networkInterface . name , true ) ; this . bindSocket ( socket , networkInterface , IPFamily . IPv4 ) . catch ( reason => { console . log ( "Could not bind detected network interface: " + reason . stack ) ; } ) ; } } } }