MatrixAI/js-mdns

MDNS API

amydevs opened this issue · 10 comments

Requirements of this design

  1. Draft of mDNS universal responder API interface that takes into the consideration the abstractions that exist on the native APIs of certain platforms.

Additional context

Look to homebridge for inspiration:

Specification

The MDNS is a CDSS because.

  1. The MDNS.start starts the responding/listening and announcing. The 3 types of packets: Response, Query and Announcement.
  2. The MDNS.stop sends a goodbye packet and stops the all sockets and deletes the cache.
class MDNS extends EventTarget {
  // Starts the MDNS responder. This will work differently on different platforms. For platforms that already have a system-wide MDNS responder, this will do nothing. Else, sockets will be bound to interfaces for interacting with the multicast group address.
  start: ()  => Promise<void>

  // Unregister all services, hosts, and sockets. For platforms with a built-in mDNS responder, this will not actually stop the responder.
  stop: () => Promise<void>

  // The most important method, this is used to register a service. All platforms support service registration of some kind. Note that some platforms may resolve service name conflicts automatically. This will have to be dealt with later. The service handle has a method that is able to then later unregister the service.
  registerService: (options: { name: string, type: string, protocol: 'udp' | 'tcp', port: number, txt?: Record<string, string> }) => Promise<void>

  unregisterService: (name: string, type: string, protocol: 'udp' | 'tcp') => Promise<void>

  // Query for all services of a type and protocol, the results will be emitted to eventtarget of the instance of this class.
  startQuery: (type: string, protocol: 'udp' | 'tcp') => void

 // Query for all services of a type and protocol, the results will be emitted to eventtarget of the instance of this class.
  stopQuery: (type: string, protocol: 'udp' | 'tcp') => void
}

The event names should be:

register - a service is announced
deregister - a service is "unannounced"
stop - run at the end of stop
destroy - run at the end of destroy
error - any asynchronous errors that might occur

If you call registerService it should still emit the event even if you ensure that you don't receive back the same multicast UDP packet, this ensures the event behaviour is consistent.

@amydevs in the future, assign to yourself, and also set it in the right column. Right now I've put it into in-progress.

Can you also add this issue into the epic too?

I noticed some other issues will be left for the future.

Also this should not be an epic.

The other issues are not part of this. This is just the API design for the release of this library.

The other issues relating to implementation on other operating systems should be separate issues.

This is just for the API design of MDNS.

When you start/stop the MDNS object.

You should have these parameters:

  • group?: Host | [Host, Host] - should default to [224.0.0.251, ff02::fb].
  • host?: Host - should default to ::
  • port?: Port - should default to 5353
  • reuseAddr?: boolean - should default to true
  • resolveHostname? - defaults to utils.resolveHostname - bring this in from js-quic

There are some changes in behaviour depending on what is set.

The default behaviour is that MDNS binds to the dual stack address, and thus is listening on all interfaces IPv4 and IPv6.

It sends packets to both the IPv4 and IPv6 multicast group, as defined above.

The port is the standard MDNS port of 5353.

The reuseAddr means that it can bind to the same port that existing MDNS stacks on the operating system is already bound on.

At the beginning of the start. You should use reuseAddr as false to detect if the port is already bound. If it is not bound, that's great you just use the socket as is. But if it is bound you need to turn off any usage of the unicast flag on queries. This is because multiple MDNS stacks cannot see the same unicast response. Thus all your queries must not have a unicast flag set.

If the reuseAddr is false, then you just fail if there's an existing MDNS stack involved.

If the group is set to the tuple of IPv4 and IPv6, then you must throw an error if the host resolves to only IPv4. The tuple of IPv4 and IPv6 is only acceptable if the host resolves to the :: dual stack address. This means you should lift some functions out of js-quic, in particular the resolveHostname function and compare the host with the group to see if it allowed. These combinations are allowed:

  • IPv4 group with IPv4 host
  • IPv6 group with IPv6 host
  • IPv4 and IPv6 group with :: host
  • IPv4 mapped IPv6 group with IPv4 mapped IPv6 host - this last one should be tested (dotted decimal and hex variants)

By having the above API, we can have an MDNS stack that works even when there is an existing MDNS stack. We can also work as the initial MDNS stack.

We can also work with a completely different MDNS group and port. And if this the case, then we won't be discoverable by bonjour or other stuff, but it's not that important, cause only PK agents need to discover other PK agents.

Plus we can be guaranteed to have multicast queries and unicast responses which is more efficient than multicast queries and multicast responses.

From the PK application, it would always want to have multicast responses so that all agents know about the new agent on the network. But the library should default the unicast flag to true, because that's more efficient.

The only thing missing here is:

  • startQuery
  • stopQuery
  1. MDNS.start() -
    public async start({
      host = '::' as Host,
      port = 5353 as Port,
      ipv6Only = false,
      group = ['224.0.0.251', 'ff02::fb'] as Host[],
      hostname = `${os.hostname()}.local` as Hostname,
      reuseAddr = true,
     }: Promise<void>;
    
  2. MDNS.stop - public async stop(): Promise<void> this stops the socket binding and queries and sends the goodbye packet.

The lifecycle for MDNS can be like this:

const mdns = new MDNS({ services }); // SERVICE STATE CREATE DESTROY
await mdns.start(); // CACHE STATE START AND STOP
await mdns.stop();

Tasks:

  1. Change all private methods to protected.
  2. Default empty object, so that functions can be called with no paramaters.
  3. Change CreateDestroyStartStop to StartStop instead. Constructor will take services state.