Skip to content

Local Network Traversal - Multicast Discovery #1

@joshuakarp

Description

@joshuakarp

Created by @CMCDragonkai

Specification

Untitled-2023-06-09-1740
There are two types of Data Flow in the MDNS System, Polling (Pull), and Announcements/Responses (Push). When a Node joins the MDNS group, the records are pushed to all other nodes. However, for the joined node to discover other nodes, it needs to conduct polling queries that other nodes respond to.

Sending Queries

image
The MDNS spec states that query records can have additional records, but we won't care to do this as it isn't necessary.
Queries won't have any other records in the query record, much like a standard DNS packet (albeit an mdns query packet can contain multiple questions).

In the case that a responder is binded to 2 interfaces that are connected to the same network (such as a laptop with WiFi + ethernet connected), the queries asking for the ip for a hostname of the responder will receive multiple responses with different ip addresses.
Untitled-2023-06-09-1740 excalidraw

This behavior is documented in: RFC 6762 14.

Control Flow

Unlike other mDNS libraries, we're going to use an AsyncIterator in order to have the consumer to have more control over the querying. An example of this would be:

async function* query({...}: Service, minimumDelay: number = 1, maximumDelay: number = 3600) {
   let delay = minimumDelay;
   while (true) {
    await this.sendPacket(...);
    delay *= 2;
    yield delay;
  }
}

The query system has been decided to have it's runtime contained within MDNS rather than being consumer-driven. This means that scheduled background queries will have to be managed by a TaskManager (similar to polykey)

Data Flow

Untitled-2023-06-09-1740(1)

Receiving Announcements/Responses (Pull)

Data Flow

Because queries are basically fire and forget, the main part comes in the form of receiving query responses from the multicast group. Hence, our querier needs to be able to collect records with a fan-in approach using a muxer that is reactive:

Untitled-2023-06-09-1740(3)

This can also be interpreted as a series of state transitions to completely build a service.
Untitled-2023-06-09-1740(3)

There also needs to be consideration that if the threshold for a muxer to complete is not reached, that additional queries are sent off in order to reach the finished state.
Untitled-2023-06-09-1740(2)

The decision tree for such would be as follows:
Untitled-2023-06-09-1740(4)

Control Flow

Instances of MDNS will extend EventTarget in order to emit events for service discovery/removal/etc.

class MDNS extends EventTarget {
}

The cache will be managed using a timer that is set to the soonest record TTL, rather than a timer for each record. The cache will also need to be an LRU in order to make sure that malicious responders cannot overwhelm it.

Sending Announcements

Control Flow

This will need to be experimented with a little. Currently the decisions are:

  • registerService cannot be called before start is called.
  • create should take in services in place of this.
  • stop should deregister all services
  • destroy should remove everything from the instance
class MDNS extends EventTarget {
  create()
  start()
  stop()
  register()
  deregister()
}

Types

Messages can be Queries or Announcements or Responses.
This can be expressed as:

type MessageType = "query" | "announcement" | "response";
type Message = [MessageType, ResourceRecord] & ["query", QuestionRecord];
const message = ["query", {...}];

Parser / Generator

The Parsing and Generation together are not isomorphic, as different parsed UInt8array packets can result in the same packet structure.

Every worker parser function will return the value wrapped in an object of this type:

type Parsed<T> = {
  data: T;
  remainder: UInt8Array;
}

The point of this is so that whatever hasn't been parsed get returned in .remainder so we don't keep track of the offset manually. This means that each worker function also needs to take in a second uint8array representing the original data structure.

  1. DNS Packet Parser Generator Utilities
  • Parser - parsePacket(Uint8array): Packet
    • Headers - parseHeader(Uint8array): {id: ..., flags: PacketFlags, counts: {...}}
    • Id - parseId(Uint8array): number
    • Flags - parseFlags(Uint8Array): PacketFlags
    • Counts - parseCount(Uint8Array): number
    • Question Records - parseQuestionRecords(Uint8Array): {...}
      • parseQuestionRecord(Uint8Array): {...}
    • Resource Records - parseResourceRecords(Uint8Array): {...}
      • parseResourceRecord(Uint8Array): {...}
      • parseResourceRecordName(Uint8Array): string
      • parseResourceRecordType(Uint8Array): A/CNAME
      • parseResourceRecordClass(Uint8Array): IN
      • parseResourceRecordLength(Uint8array): number
      • parseResourceRecordData(Uint8array): {...}
        • parseARecordData(Uint8array): {...}
        • parseAAAARecordData(Uint8array): {...}
        • parseCNAMERecordData(Uint8array): {...}
        • parseSRVRecordData(Uint8array): {...}
        • parseTXTRecordData(Uint8array): Map<string, string>
        • parseOPTRecordData(Uint8array): {...}
        • parseNSECRecordData(Uint8array): {...}
    • String Pointer Cycle Detection
      • Everytime a string is parsed, we take reference of the beginning and end of the string so that pointers cannot point to a start of a string that would infinite loop. A separate index table for the path of the dereferences to make sure deadlock doesn't happen.
    • Errors at each parsing function instead of letting the data view failing
      • ErrorDNSParse - Generic error with message that contains information for different exceptions. Ie. id parse failed at ...
    • Record Keys - parseResourceRecordKey and parseQuestionRecordKey and parseRecordKey - parseLabels.
  • Generator - generatePacket(Packet): UInt8Array
    • Header generateHeader(id, flags, counts...)
      • Id
      • Flags - generateFlags({ ... }): Uint8Array
      • Counts - generateCount(number): Uint8Array
    • Question Records - generateQuestionRecords(): Uint8Array - flatMap(generateQuestion)
      • generateQuestionRecord(): Uint8Array
    • Resource Records (KV) - generateResourceRecords()
      • generateRecord(): Uint8array -
      • generateRecordName - "abc.com" - ...RecordKey
      • generateRecordType - A/CNAME
      • generateRecordClass - IN
      • generateRecordLength
      • generateRecordData
        • generateARecordData(string): Uint8array
        • generateAAAARecordData(string): Uint8array
        • generateCNAMERecordData(string): Uint8array
        • generateSRVRecordData(SRVRecordValue): Uint8array
        • generateTXTRecordData(Map<string, string>): Uint8array
        • generateOPTRecordData(Uint8array): Uint8array
        • generateNSECRecordData(): Uint8array
  • Integrated into MDNS
  1. MDNS
  • Querying
    • MDNS.query()
    • query services of a type
    • MDNS.registerService()
    • MDNS.unregisterService()
  • Responding
    • Listening to queries
    • Responding to all queries with all records
    • Respond to unicast
    • Truncated bit

Testing

We can use two MDNS instances to interact with each other to test both query and respond on separate ports.

Additional Context

The following discussion from 'Refactoring Network Module' MR should be addressed:

Tasks

  • Parser - 5.5 days
    • Packet Header - 0.5 days
    • Packet Flags - 0.5 days
    • Questions - 0.5 days
    • Resource Records - 4 days
  • Generator 5.5 days
    • Packet Header - 0.5 days
    • Packet Flags - 0.5 days
    • Questions - 0.5 days
    • Resource Records - 4 days
  • MDNSCache - 2.5 days
    • Multi-Keyed Maps for ResourceRecord Querying - 0.5 days
    • TTL Expiry Record Invalidation - 0.5 days
    • Reverse Host to Record Mapping - 0.5 days
    • LRU to Prevent DoS - 0.5 days
    • Support use as local resource record in-memory database - 0.5 days
  • TaskManager - ? days
    • Migrate to in-memory - ? days
  • MDNS - 11.5 days
    • UDP Networking Logic - 2 days
      • Socket Binding to Multiple Interfaces - 1 days
      • Error Handling - 1 days
    • Querier - 4 days
      • Service Querying - 2.5 days
        • Record Aggregation for Muxxing Together Services - 0.5 days
        • Querying For a Service's Missing Records - 0.5 days
        • Emitting Expired Services - 0.5 days
      • Unicast 1.5 days
        • Checking for Unicast Availability - 1 days
        • Sending queries with unicast enabled - 0.5 days
    • Responder 5.5 days
      • Service Registration - 0.5 days
      • Filter Messages Received from Multicast Interface Loopback - 0.5 days
      • Unicast
        • Responding to Unicast Queries - 0.5 days

Metadata

Metadata

Assignees

Labels

epicBig issue with multiple subissuesr&d:polykey:core activity 4End to End Networking behind Consumer NAT Devices

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions