Madness

A query-only mDNS (multicast DNS) client for Elixir.

Madness sends DNS queries over multicast UDP and returns responses as lazy streams or asynchronous messages. It supports both IPv4 and IPv6 networks.

Features

Installation

Add madness to your list of dependencies in mix.exs:

def deps do
  [
    {:madness, "~> 0.1.0"}
  ]
end

Quick Start

# Discover HTTP services on the local network
Madness.request({"_http._tcp.local", :ptr})
|> Enum.to_list()

# Get the address of a specific device
Madness.request({"mydevice.local", :a})
|> Enum.take(1)

# Query for multiple record types at once
Madness.request([
  {"mydevice.local", :a},
  {"mydevice.local", :aaaa}
])
|> Enum.to_list()

Usage

Stream Mode (default)

Returns a lazy stream that yields Madness.Record structs:

Madness.request({"_http._tcp.local", :ptr})
|> Stream.filter(&(&1.type == :ptr))
|> Enum.to_list()

Message Mode

Returns {:ok, ref} and sends messages to the calling process:

{:ok, ref} = Madness.request({"_http._tcp.local", :ptr}, into: :self)

receive do
  {^ref, %Madness.Record{} = record} -> IO.inspect(record)
  {^ref, :done} -> IO.puts("Query complete")
end

This mode is useful for GenServer integration:

def handle_info({ref, %Madness.Record{} = record}, state) do
  # Process record...
  {:noreply, state}
end

def handle_info({ref, :done}, state) do
  # Query complete
  {:noreply, state}
end

Options

Option Default Description
:into:stream:stream for lazy enumerable, :self for process messages
:timeout5000 Query timeout in milliseconds
:family:any:any for both IPv4 and IPv6, :inet for IPv4 only, :inet6 for IPv6 only
:interface:any Interface name ("en0"), index, or :any
:unicast_responsetrue Request unicast responses (QU bit)

Additional Records

mDNS responders typically include related records in the Additional section. For example, a PTR query response often includes SRV, TXT, and address records:

Madness.request({"_http._tcp.local", :ptr})
|> Enum.group_by(& &1.type)
# => %{
#   ptr: [%Record{data: "MyServer._http._tcp.local", ...}],
#   srv: [%Record{data: {0, 0, 80, "myserver.local"}, ...}],
#   txt: [%Record{data: ["path=/"], ...}],
#   a: [%Record{data: {192, 168, 1, 100}, ...}]
# }

Check metadata.section to distinguish :answer from :additional records.

Early Termination

For non-PTR queries, Madness exits early when all questions are answered. This includes NSEC negative responses - if a device responds with an NSEC record indicating a record type doesn’t exist, the query completes without waiting for the full timeout.

PTR queries always wait for the timeout since multiple responders may reply.

Common Service Types

Service Description
_http._tcp.local HTTP servers
_https._tcp.local HTTPS servers
_ipp._tcp.local IPP printers
_airplay._tcp.local AirPlay devices
_raop._tcp.local AirPlay audio
_smb._tcp.local SMB file shares
_afpovertcp._tcp.local AFP file shares
_ssh._tcp.local SSH servers
_services._dns-sd._udp.local Browse all services

Record Types

Type Data Format Example
:a IPv4 tuple {192, 168, 1, 100}
:aaaa IPv6 tuple {0x2001, 0xdb8, 0, 0, 0, 0, 0, 1}
:ptr Domain string "MyPrinter._http._tcp.local"
:srv{priority, weight, port, target}{0, 0, 80, "server.local"}
:txt List of strings ["path=/", "version=1.0"]
:cname Domain string "alias.local"
:nsec MapSet of type codes MapSet.new([1, 16])

License

MIT