Skip to content

Instantly share code, notes, and snippets.

@ConnorRigby
Created July 28, 2020 18:59
Show Gist options
  • Save ConnorRigby/a5a59ff1a39d290f9a8cc54664088001 to your computer and use it in GitHub Desktop.
Save ConnorRigby/a5a59ff1a39d290f9a8cc54664088001 to your computer and use it in GitHub Desktop.
defmodule DoubleDNS do
@moduledoc "DoubleDNS"
use GenServer
require Logger
@mdns_group {224, 0, 0, 251}
@mdns_port 5353
@dns_port 53
defmodule State do
defstruct multicast_if: nil,
mdns_socket: nil,
mdns_entries: [],
dns_socket: nil,
dot_local: nil
end
@query_packet %DNS.Record{
header: %DNS.Header{},
qdlist: []
}
def find_epmd(socket) do
packet = %DNS.Record{
@query_packet
| :qdlist => [
%DNS.Query{domain: '_epmd._tcp', type: :ptr, class: :in}
]
}
p = DNS.Record.encode(packet)
:ok = :gen_udp.send(socket, @mdns_group, @mdns_port, p)
end
def start_link(multicast_if) do
GenServer.start_link(__MODULE__, multicast_if, name: __MODULE__)
end
@impl GenServer
def init({_, _, _, _} = multicast_if) do
{:ok, hostname} = :inet.gethostname()
dot_local = "#{hostname}.local"
send(self(), :open_mdns)
send(self(), :open_dns)
{:ok, %State{multicast_if: multicast_if, dot_local: dot_local}}
end
@impl GenServer
def handle_info(:open_mdns, state) do
udp_options = [
:binary,
broadcast: true,
active: true,
ip: {0, 0, 0, 0},
ifaddr: {0, 0, 0, 0},
add_membership: {@mdns_group, {0, 0, 0, 0}},
multicast_if: state.multicast_if,
multicast_loop: true,
multicast_ttl: 32,
reuseaddr: true
]
case :gen_udp.open(@mdns_port, udp_options) do
{:ok, socket} ->
{:noreply, %State{state | mdns_socket: socket}}
error ->
{:stop, {:multicast, error}, state}
end
end
def handle_info(:open_dns, state) do
case :gen_udp.open(@dns_port, [:binary, active: true, reuseaddr: true]) do
{:ok, socket} ->
{:noreply, %State{state | dns_socket: socket}}
error ->
{:stop, error, state}
end
end
def handle_info({:udp, socket, ip, _port, packet}, %{mdns_socket: socket} = state) do
record = DNS.Record.decode(packet)
state = handle_mdns(record.anlist, ip, state)
# IO.inspect(record, label: "multicast packet")
{:noreply, state}
end
def handle_info({:udp, socket, ip, port, packet}, %{dns_socket: socket} = state) do
record = DNS.Record.decode(packet)
{answers, state} = handle_dns(record.qdlist, ip, [], state)
response = DNS.Record.encode(%{record | anlist: answers})
_ = :gen_udp.send(socket, ip, port, response)
{:noreply, state}
end
def handle_mdns([%{type: :a, domain: domain, data: data} = resource | rest], ip, state) do
# IO.inspect(resource, label: "adding mdns resource")
_ = find_epmd(state.mdns_socket)
index =
Enum.find_index(state.mdns_entries, fn
%{domain: ^domain, data: ^data} -> true
_ -> false
end)
mdns_entries =
if index do
List.update_at(state.mdns_entries, index, fn _ -> resource end)
else
[resource | state.mdns_entries]
end
handle_mdns(rest, ip, %{state | mdns_entries: mdns_entries})
end
# %DNS.Resource{
# bm: [],
# class: :in,
# cnt: 0,
# data: {0, 0, 4369, 'meshy-080c.local'},
# domain: 'meshy-080c._epmd._tcp.local',
# func: false,
# tm: :undefined,
# ttl: 120,
# type: :srv
# }
def handle_mdns([%{type: :srv, data: {_, _, 4369, node_name}} | rest], ip, state) do
if String.equivalent?(node_name, state.dot_local) do
handle_mdns(rest, ip, state)
else
node_name = :"meshy@#{node_name}"
if node_name in Node.list() do
handle_mdns(rest, ip, state)
else
Logger.info("Trying to connect to: #{node_name}")
case Node.connect(node_name) do
true -> Logger.info("Connected to #{node_name}")
false -> Logger.error("Failed to connect to #{node_name}")
end
handle_mdns(rest, ip, state)
end
end
end
def handle_mdns([_unknown | rest], ip, state) do
# IO.inspect(unknown, label: "mdns packet from: #{inspect(ip)}")
handle_mdns(rest, ip, state)
end
def handle_mdns([], _ip, state) do
state
end
def handle_dns([%{type: :a, domain: domain} = q | rest], ip, results, state) do
if String.equivalent?(domain, state.dot_local) do
# IO.inspect(q, lable: "responding to query with dot_local")
answer = make_record(q.domain, q.type, 120, state.multicast_if)
handle_dns(rest, ip, [answer | results], state)
else
answers =
Enum.filter(state.mdns_entries, fn
%{domain: ^domain} -> true
_ -> false
end)
|> Enum.map(fn
entry ->
make_record(q.domain, q.type, 120, entry.data)
end)
IO.inspect(q, label: "responding to query with: #{inspect(answers)}")
handle_dns(rest, ip, answers ++ results, state)
end
end
def handle_dns([_unknown | rest], ip, results, state) do
# IO.inspect(unknown, label: "dns packet from: #{inspect(ip)}")
handle_dns(rest, ip, results, state)
end
def handle_dns([], _, results, state) do
{Enum.reverse(results), state}
end
def make_record(domain, type, ttl, data) do
%DNS.Resource{
domain: domain,
class: :in,
type: type,
ttl: ttl,
data: data
}
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment