lib/vintage_net/ip/dnsd_config.ex

defmodule VintageNet.IP.DnsdConfig do
  @moduledoc """
  This is a helper module for VintageNet.Technology implementations that use
  the Busybox DNS server.

  DNS functionality is only supported for IPv4 configurations using static IP
  addresses.

  DNS server parameters are:

  * `:port` - The port to use (defaults to 53)
  * `:ttl` - DNS record TTL in seconds (defaults to 120)
  * `:records` - DNS A records (required)

  The `:records` option is a list of name/IP address tuples. For example:

  ```
  [{"example.com", {1, 2, 3, 4}}]
  ```

  Only IPv4 addresses are supported. Addresses may be specified as strings or
  tuples, but will be normalized to tuple form before being applied.
  """

  alias VintageNet.{Command, IP}
  alias VintageNet.Interface.RawConfig

  @doc """
  Normalize the DNSD parameters in a configuration.
  """
  @spec normalize(map()) :: map()
  def normalize(%{ipv4: %{method: :static}, dnsd: dnsd} = config) do
    # Normalize IP addresses
    new_dnsd =
      dnsd
      |> Map.update(:records, [], &normalize_records/1)
      |> Map.take([
        :records,
        :port,
        :ttl
      ])

    %{config | dnsd: new_dnsd}
  end

  def normalize(%{dnsd: _something_else} = config) do
    # DNSD won't be started if not an IPv4 static configuration
    Map.drop(config, [:dnsd])
  end

  def normalize(config), do: config

  defp normalize_records(records) do
    Enum.map(records, &normalize_record/1)
  end

  defp normalize_record({name, ipa}) do
    {name, IP.ip_to_tuple!(ipa)}
  end

  @doc """
  Add dnsd configuration commands for running a DNSD server
  """
  @spec add_config(RawConfig.t(), map(), keyword()) :: RawConfig.t()
  def add_config(
        %RawConfig{
          ifname: ifname,
          files: files,
          child_specs: child_specs
        } = raw_config,
        %{ipv4: %{method: :static, address: address}, dnsd: dnsd_config},
        opts
      ) do
    tmpdir = Keyword.fetch!(opts, :tmpdir)
    dnsd_conf_path = Path.join(tmpdir, "dnsd.conf.#{ifname}")

    new_files = [{dnsd_conf_path, dnsd_contents(dnsd_config)} | files]

    dnsd_args =
      [
        "-c",
        dnsd_conf_path,
        "-i",
        IP.ip_to_string(address)
      ]
      |> add_port(dnsd_config)
      |> add_ttl(dnsd_config)

    new_child_specs =
      child_specs ++
        [
          Supervisor.child_spec(
            {MuonTrap.Daemon,
             [
               "dnsd",
               dnsd_args,
               Command.add_muon_options(stderr_to_stdout: true, log_output: :debug)
             ]},
            id: :dnsd
          )
        ]

    %RawConfig{raw_config | files: new_files, child_specs: new_child_specs}
  end

  def add_config(raw_config, _config_without_dhcpd, _opts), do: raw_config

  defp dnsd_contents(%{records: records}) do
    Enum.map(records, &record_to_string/1)
    |> IO.iodata_to_binary()
  end

  defp record_to_string({name, ipa}) do
    "#{name} #{IP.ip_to_string(ipa)}\n"
  end

  defp add_port(dnsd_args, %{port: port}) do
    ["-p", to_string(port) | dnsd_args]
  end

  defp add_port(dnsd_args, _dnsd_config), do: dnsd_args

  defp add_ttl(dnsd_args, %{ttl: ttl}) do
    ["-t", to_string(ttl) | dnsd_args]
  end

  defp add_ttl(dnsd_args, _dnsd_config), do: dnsd_args
end