defmodule VintageNet.NameResolver do
@moduledoc """
This module manages the contents of "/etc/resolv.conf".
This file is used by the C standard library and by Erlang for resolving
domain names. Since both C programs and Erlang can do resolution, debugging
problems in this area can be confusing due to varying behavior based on who's
resolving at the time. See the `/etc/erl_inetrc` file on the target to review
Erlang's configuration.
This module assumes exclusive ownership on "/etc/resolv.conf", so if any
other code in the system tries to modify the file, their changes will be lost
on the next update.
It is expected that each network interface provides a configuration. This
module will track configurations to network interfaces so that it can reflect
which resolvers are around. Resolver order isn't handled.
"""
use GenServer
alias VintageNet.IP
alias VintageNet.Resolver.ResolvConf
require Logger
defmodule State do
@moduledoc false
defstruct [:path, :entries, :additional_name_servers]
end
@doc """
Start the resolv.conf manager.
Accepted args:
* `resolvconf` - path to the resolvconf file
* `additional_name_servers` - list of additional servers
"""
@spec start_link(keyword) :: GenServer.on_start()
def start_link(args) do
relevant_args = Keyword.take(args, [:resolvconf, :additional_name_servers])
GenServer.start_link(__MODULE__, relevant_args, name: __MODULE__)
end
@doc """
Stop the resolv.conf manager.
"""
@spec stop() :: :ok
def stop() do
GenServer.stop(__MODULE__)
end
@doc """
Set the search domain and name server list for the specified interface.
This replaces any entries in the `/etc/resolv.conf` for this interface.
"""
@spec setup(String.t(), String.t() | nil, [VintageNet.any_ip_address()]) :: :ok
def setup(ifname, domain, name_servers) do
GenServer.call(__MODULE__, {:setup, ifname, domain, name_servers})
end
@doc """
Clear all entries in "/etc/resolv.conf" that are associated with
the specified interface.
"""
@spec clear(String.t()) :: :ok
def clear(ifname) do
GenServer.call(__MODULE__, {:clear, ifname})
end
@doc """
Completely clear out "/etc/resolv.conf".
"""
@spec clear_all() :: :ok
def clear_all() do
GenServer.call(__MODULE__, :clear_all)
end
## GenServer
@impl GenServer
def init(args) do
resolvconf_path = Keyword.get(args, :resolvconf)
additional_name_servers =
Keyword.get(args, :additional_name_servers, [])
|> Enum.reduce([], &ip_to_tuple_safe/2)
|> Enum.reverse()
state = %State{
path: resolvconf_path,
entries: %{},
additional_name_servers: additional_name_servers
}
write_resolvconf(state)
{:ok, state}
end
@impl GenServer
def handle_call({:setup, ifname, domain, name_servers}, _from, state) do
servers = Enum.map(name_servers, &IP.ip_to_tuple!/1)
ifentry = %{domain: domain, name_servers: servers}
state = %{state | entries: Map.put(state.entries, ifname, ifentry)}
write_resolvconf(state)
{:reply, :ok, state}
end
@impl GenServer
def handle_call({:clear, ifname}, _from, state) do
state = %{state | entries: Map.delete(state.entries, ifname)}
write_resolvconf(state)
{:reply, :ok, state}
end
@impl GenServer
def handle_call(:clear_all, _from, state) do
state = %{state | entries: %{}}
write_resolvconf(state)
{:reply, :ok, state}
end
defp write_resolvconf(%State{
path: path,
entries: entries,
additional_name_servers: additional_name_servers
}) do
File.write!(path, ResolvConf.to_config(entries, additional_name_servers))
end
@spec ip_to_tuple_safe(VintageNet.any_ip_address(), [:inet.ip_address()]) :: [
:inet.ip_address()
]
defp ip_to_tuple_safe(ip, acc) do
case IP.ip_to_tuple(ip) do
{:error, reason} ->
Logger.error("Failed to parse IP address: #{inspect(ip)} (#{reason})")
acc
{:ok, ip} ->
[ip | acc]
end
end
end