Current section
Files
Jump to
Current section
Files
lib/mdns/server.ex
defmodule Mdns.Server do
use GenServer
require Logger
alias Mdns.Utilities.Network
@response_packet %DNS.Record{
header: %DNS.Header{
aa: true,
qr: true,
opcode: 0,
rcode: 0
},
anlist: []
}
defmodule State do
defstruct udp: nil,
services: [],
ip: {0, 0, 0, 0}
end
defmodule Service do
defstruct domain: "_nerves._tcp.local",
data: "_myapp._tcp.local",
ttl: 120,
type: :ptr
end
def start_link do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
def add_service(%Service{} = service) do
GenServer.call(__MODULE__, {:add_service, service})
end
def set_ip(ip) do
GenServer.call(__MODULE__, {:ip, ip})
end
def start(opts \\ []) do
GenServer.call(__MODULE__, {:start, opts})
end
def stop() do
GenServer.call(__MODULE__, :stop)
end
def init(:ok) do
{:ok, %State{}}
end
def handle_call({:start, opts}, _from, state) do
interface = opts[:interface] || {0, 0, 0, 0}
udp_options = [
:binary,
active: true,
add_membership: {Network.mdns_group, interface},
multicast_if: interface,
multicast_loop: true,
multicast_ttl: 255,
reuseaddr: true
] ++ Network.reuse_port()
{:ok, udp} = :gen_udp.open(Network.mdns_port, udp_options)
{:reply, :ok, %State{state | udp: udp}}
end
def handle_call(:stop, _from, state) do
if state.udp do
:gen_udp.close(state.udp)
end
{:reply, :ok, %State{state | udp: nil}}
end
def handle_call({:ip, ip}, _from, state) do
{:reply, :ok, %State{state | ip: ip}}
end
def handle_call({:add_service, service}, _from, state) do
{:reply, :ok, %State{state | :services => Enum.uniq([service | state.services])}}
end
def handle_info({:udp, _socket, src_ip, src_port, packet}, state) do
{:noreply, handle_packet(src_ip, src_port, packet, state)}
end
def handle_packet(src_ip, src_port, packet, state) do
record = DNS.Record.decode(packet)
case record.header.qr do
false -> handle_query(src_ip, src_port, record, state)
_ -> state
end
end
def handle_query(src_ip, src_port, record, state) do
Enum.flat_map(record.qdlist, fn %DNS.Query{} = q ->
Enum.reduce(state.services, [], fn service, answers ->
cond do
service.type == :a || service.domain == to_string(q.domain) ->
data =
case service.data do
:ip ->
state.ip
_ ->
case String.valid?(service.data) do
true -> to_charlist(service.data)
_ -> service.data
end
end
[
%DNS.Resource{
class: :in,
type: service.type,
ttl: service.ttl,
data: data,
domain: to_charlist(service.domain)
}
| answers
]
true ->
answers
end
end)
end)
|> send_service_response(mdns_destination(src_ip, src_port), record, state)
state
end
def send_service_response(services, {src_ip, src_port}, record, state) do
cond do
length(services) > 0 ->
header = %DNS.Header{@response_packet.header | :id => record.header.id }
packet = %DNS.Record{@response_packet | :anlist => services, :header => header}
:gen_udp.send(state.udp, src_ip, src_port, DNS.Record.encode(packet))
true ->
nil
end
end
# See RFC 6762 6.7 "Legacy Unicast Responses"
# If the source UDP port in a received Multicast DNS query is not port
# 5353, this indicates that the querier originating the query is a
# simple resolver (...). In this case, the Multicast DNS responder MUST
# send a UDP response directly back to the querier, via unicast, to the
# query packet's source IP address and port.
defp mdns_destination(_src_ip, 5353), do: {Network.mdns_group, Network.mdns_port}
defp mdns_destination(src_ip, src_port), do: {src_ip, src_port}
end