# 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
- **Stream and message modes** - Consume responses as a lazy `Stream` or receive them as process messages
- **IPv4 and IPv6** - Full support for both address families
- **Automatic record parsing** - A, AAAA, PTR, SRV, TXT, CNAME, NS, MX, SOA, NSEC
- **Additional records included** - Responses include records from the Additional section
- **NSEC early termination** - Queries complete early when negative responses are received
- **Multi-interface support** - Query on all interfaces or a specific one
- **Unicast responses** - QU bit support for reduced network traffic
## Installation
Add `madness` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:madness, "~> 0.1.0"}
]
end
```
## Quick Start
```elixir
# 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:
```elixir
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:
```elixir
{: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:
```elixir
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 |
| `:timeout` | `5000` | Query timeout in milliseconds |
| `:family` | `:inet` | `:inet` for IPv4, `:inet6` for IPv6 |
| `:interface` | `:any` | Interface name (`"en0"`), index, or `:any` |
| `:unicast_response` | `true` | 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:
```elixir
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