diff --git a/farmbot_os/config/target/dev.exs b/farmbot_os/config/target/dev.exs index 890ae616..3e622a11 100644 --- a/farmbot_os/config/target/dev.exs +++ b/farmbot_os/config/target/dev.exs @@ -10,22 +10,7 @@ config :vintage_net, persistence: VintageNet.Persistence.Null, config: [ {"wlan0", %{type: VintageNet.Technology.Null}}, - {"usb0", - %{ - type: FarmbotOS.Platform.Target.Configurator.CaptivePortal, - ipv4: %{ - method: :static, - address: "192.168.25.1", - netmask: "255.255.255.0" - }, - dnsmasq: %{ - domain: "farmbot", - server: "192.168.25.1", - address: "192.168.25.1", - start: "192.168.25.2", - end: "192.168.25.10" - } - }} + {"usb0", %{type: VintageNetDirect}} ] config :mdns_lite, diff --git a/farmbot_os/config/target/prod.exs b/farmbot_os/config/target/prod.exs index 916e6c2f..271c8607 100644 --- a/farmbot_os/config/target/prod.exs +++ b/farmbot_os/config/target/prod.exs @@ -10,22 +10,7 @@ config :vintage_net, persistence: VintageNet.Persistence.Null, config: [ {"wlan0", %{type: VintageNet.Technology.Null}}, - {"usb0", - %{ - type: FarmbotOS.Platform.Target.Configurator.CaptivePortal, - ipv4: %{ - method: :static, - address: "192.168.25.1", - netmask: "255.255.255.0" - }, - dnsmasq: %{ - domain: "farmbot", - server: "192.168.25.1", - address: "192.168.25.1", - start: "192.168.25.2", - end: "192.168.25.10" - } - }} + {"usb0", %{type: VintageNetDirect}} ] config :mdns_lite, diff --git a/farmbot_os/lib/farmbot_os/configurator/captive_dns.ex b/farmbot_os/lib/farmbot_os/configurator/captive_dns.ex new file mode 100644 index 00000000..21ff89f5 --- /dev/null +++ b/farmbot_os/lib/farmbot_os/configurator/captive_dns.ex @@ -0,0 +1,79 @@ +defmodule FarmbotOS.Configurator.CaptiveDNS do + use GenServer + alias __MODULE__, as: State + + defstruct [:dns_socket, :dns_port, :ifname] + + def start_link(ifname, port) do + GenServer.start_link(__MODULE__, [ifname, port]) + end + + @impl GenServer + def init([ifname, port]) do + send(self(), :open_dns) + # use charlist here because :inet module works with charlists + {:ok, %State{dns_port: port, ifname: to_charlist(ifname)}} + end + + @impl GenServer + # open a UDP socket on port 53 + def handle_info(:open_dns, state) do + case :gen_udp.open(state.dns_port, [:binary, active: true, reuseaddr: true]) do + {:ok, socket} -> + {:noreply, %State{state | dns_socket: socket}} + + error -> + {:stop, error, state} + end + end + + # binary dns message from the socket + def handle_info( + {:udp, socket, ip, port, packet}, + %{dns_socket: socket} = state + ) do + record = DNS.Record.decode(packet) + {answers, state} = handle_dns(record.qdlist, [], state) + response = DNS.Record.encode(%{record | anlist: answers}) + _ = :gen_udp.send(socket, ip, port, response) + {:noreply, state} + end + + # recursively check for dns queries, respond to each of them with the local ip address. + + # respond to `a` with our current ip address + defp handle_dns( + [%{type: :a} = q | rest], + answers, + state + ) do + ifname = state.ifname + {:ok, interfaces} = :inet.getifaddrs() + {^ifname, ifinfo} = List.keyfind(interfaces, ifname, 0) + + addr = + Enum.find_value(ifinfo, fn + {:addr, {_, _, _, _} = ipv4_addr} -> ipv4_addr + _ -> false + end) + + answer = make_record(q.domain, q.type, 120, addr) + handle_dns(rest, [answer | answers], state) + end + + # stop recursing when qdlist is fully enumerated + defp handle_dns([], answers, state) do + {Enum.reverse(answers), state} + end + + defp make_record(domain, type, ttl, data) do + %DNS.Resource{ + domain: domain, + class: :in, + type: type, + ttl: ttl, + data: data + } + end +end +FarmbotOS.Configurator.CaptiveDNS.start_link("lo0", 4040) \ No newline at end of file diff --git a/farmbot_os/mix.exs b/farmbot_os/mix.exs index c7c030a6..1b2c4c6a 100644 --- a/farmbot_os/mix.exs +++ b/farmbot_os/mix.exs @@ -92,6 +92,7 @@ defmodule FarmbotOS.MixProject do {:cors_plug, "~> 2.0"}, {:plug_cowboy, "~> 2.1"}, {:phoenix_html, "~> 2.13"}, + {:dns, "~> 2.1"}, # Nerves stuff. {:nerves, "~> 1.5", runtime: false}, diff --git a/farmbot_os/platform/target/configurator/captive_portal.ex b/farmbot_os/platform/target/configurator/captive_portal.ex index fb92763f..9a0bcd3e 100644 --- a/farmbot_os/platform/target/configurator/captive_portal.ex +++ b/farmbot_os/platform/target/configurator/captive_portal.ex @@ -6,6 +6,7 @@ defmodule FarmbotOS.Platform.Target.Configurator.CaptivePortal do @behaviour VintageNet.Technology require FarmbotCore.Logger + alias FarmbotOS.Configurator.CaptiveDNS @impl VintageNet.Technology def normalize(%{vintage_net_wifi: _} = config) do @@ -24,7 +25,7 @@ defmodule FarmbotOS.Platform.Target.Configurator.CaptivePortal do ifname |> vintage_wifi(normalized, opts) - |> dnsmasq(opts) + |> add_captive_dns() end def to_raw_config(ifname, config, opts) do @@ -32,7 +33,7 @@ defmodule FarmbotOS.Platform.Target.Configurator.CaptivePortal do ifname |> vintage_ethernet(normalized, opts) - |> dnsmasq(opts) + |> add_captive_dns() end @impl VintageNet.Technology @@ -45,67 +46,12 @@ defmodule FarmbotOS.Platform.Target.Configurator.CaptivePortal do VintageNetWiFi.ioctl(ifname, ioctl, args) end - defp dnsmasq( - %{ifname: ifname, source_config: %{dnsmasq: config}} = raw_config, - opts - ) do - tmpdir = Keyword.fetch!(opts, :tmpdir) - killall = Keyword.fetch!(opts, :bin_killall) - dnsmasq = System.find_executable("dnsmasq") - dnsmasq_conf_path = Path.join(tmpdir, "dnsmasq.conf.#{ifname}") - dnsmasq_lease_file = Path.join(tmpdir, "dnsmasq.leases.#{ifname}") - dnsmasq_pid_file = Path.join(tmpdir, "dnsmasq.pid.#{ifname}") - - dnsmasq_conf_contents = """ - interface=#{ifname} - except-interface=lo - localise-queries - bogus-priv - bind-interfaces - listen-address=#{config[:address]} - server=#{config[:address]} - address=/#/#{config[:address]} - dhcp-option=6,#{config[:address]} - dhcp-range=#{config[:start]},#{config[:end]},12h - """ - - files = [ - {dnsmasq_conf_path, dnsmasq_conf_contents} + defp add_captive_dns(%{ifname: ifname} = raw_config) do + child_specs = [ + {CaptiveDNS, [ifname, 53]} ] - up_cmds = [ - {:run, dnsmasq, - [ - "-K", - "-l", - dnsmasq_lease_file, - "-x", - dnsmasq_pid_file, - "-C", - dnsmasq_conf_path - ]} - ] - - down_cmds = [ - {:run, killall, ["-q", "-9", "dnsmasq"]} - ] - - updated_raw_config = %{ - raw_config - | files: raw_config.files ++ files, - up_cmds: raw_config.up_cmds ++ up_cmds, - down_cmds: raw_config.down_cmds ++ down_cmds, - cleanup_files: - raw_config.cleanup_files ++ - [dnsmasq_conf_path, dnsmasq_lease_file, dnsmasq_pid_file] - } - - updated_raw_config - end - - defp dnsmasq(%{} = raw_config, _opts) do - FarmbotCore.Logger.error(1, "DNSMASQ Disabled") - raw_config + %{raw_config | child_specs: raw_config.child_specs ++ child_specs} end defp vintage_wifi(ifname, config, opts) do diff --git a/farmbot_os/platform/target/network.ex b/farmbot_os/platform/target/network.ex index ecd7c2fa..23558d28 100644 --- a/farmbot_os/platform/target/network.ex +++ b/farmbot_os/platform/target/network.ex @@ -36,10 +36,7 @@ defmodule FarmbotOS.Platform.Target.Network do address: "192.168.24.1", netmask: "255.255.255.0" }, - dnsmasq: %{ - domain: "farmbot", - server: "192.168.24.1", - address: "192.168.24.1", + dhcpd: %{ start: "192.168.24.2", end: "192.168.24.10" } diff --git a/farmbot_os/test/farmbot_os/captive_dns_test.exs b/farmbot_os/test/farmbot_os/captive_dns_test.exs new file mode 100644 index 00000000..1482951b --- /dev/null +++ b/farmbot_os/test/farmbot_os/captive_dns_test.exs @@ -0,0 +1,13 @@ +defmodule FarmbotOS.Configurator.CaptiveDNSTest do + use ExUnit.Case, async: false + + @dig System.find_executable("dig") + if @dig do + test "all dns queries resolve to local ip address" do + {:ok, dns_server} = FarmbotOS.Configurator.CaptiveDNS.start_link("lo0", 4040) + res = :os.cmd('dig -p 4040') + refute String.contains?(to_string(res), "no servers could be reached") + end + end + +end \ No newline at end of file