README.md

# 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