defmodule VintageNet.IP.DhcpdConfig do
@moduledoc """
This is a helper module for VintageNet.Technology implementations that use
a DHCP server.
DHCP server parameters are:
* `:start` - Start of the lease block
* `:end` - End of the lease block
* `:max_leases` - The maximum number of leases
* `:decline_time` - The amount of time that an IP will be reserved (leased to nobody)
* `:conflict_time` -The amount of time that an IP will be reserved
* `:offer_time` - How long an offered address is reserved (seconds)
* `:min_lease` - If client asks for lease below this value, it will be rounded up to this value (seconds)
* `:auto_time` - The time period at which udhcpd will write out leases file.
* `:static_leases` - list of `{mac_address, ip_address}`
* `:options` - a map DHCP response options to set. Such as:
* `:dns` - IP_LIST
* `:domain` - STRING - [0x0f] client's domain suffix
* `:hostname` - STRING
* `:mtu` - NUM
* `:router` - IP_LIST
* `:search` - STRING_LIST - [0x77] search domains
* `:serverid` - IP (defaults to the interface's IP address)
* `:subnet` - IP (as a subnet mask)
> #### :options {: .info}
> Options may also be passed in as integers. These are passed directly to the DHCP server
> and their values are strings that are not interpreted by VintageNet. Use this to support
> custom DHCP header options. For more details on DHCP response options see RFC 2132
## Example
```
VintageNet.configure("wlan0", %{
type: VintageNetWiFi,
vintage_net_wifi: %{
networks: [
%{
mode: :ap,
ssid: "test ssid",
key_mgmt: :none
}
]
},
dhcpd: %{
start: "192.168.24.2",
end: "192.168.24.10",
options: %{
dns: ["1.1.1.1", "1.0.0.1"],
subnet: "192.168.24.255",
router: ["192.168.24.1"]
}
}
})
```
"""
alias VintageNet.{Command, IP}
alias VintageNet.Interface.RawConfig
@ip_list_options [:dns, :router]
@ip_options [:serverid, :subnet]
@int_options [:mtu]
@string_options [:hostname, :domain]
@string_list_options [:search]
@list_options @ip_list_options ++ @string_list_options
@doc """
Normalize the DHCPD parameters in a configuration.
"""
@spec normalize(map()) :: map()
def normalize(%{dhcpd: dhcpd} = config) do
# Normalize IP addresses
new_dhcpd =
dhcpd
|> Map.update(:start, {192, 168, 0, 20}, &IP.ip_to_tuple!/1)
|> Map.update(:end, {192, 168, 0, 254}, &IP.ip_to_tuple!/1)
|> normalize_static_leases()
|> normalize_options()
|> Map.take([
:start,
:end,
:max_leases,
:decline_time,
:conflict_time,
:offer_time,
:min_lease,
:auto_time,
:static_leases,
:options
])
%{config | dhcpd: new_dhcpd}
end
def normalize(config), do: config
defp normalize_static_leases(%{static_leases: leases} = dhcpd_config) do
new_leases = Enum.map(leases, &normalize_lease/1)
%{dhcpd_config | static_leases: new_leases}
end
defp normalize_static_leases(dhcpd_config), do: dhcpd_config
defp normalize_lease({hwaddr, ipa}) do
{hwaddr, IP.ip_to_tuple!(ipa)}
end
defp normalize_options(%{options: options} = dhcpd_config) do
new_options = for option <- options, into: %{}, do: normalize_option(option)
%{dhcpd_config | options: new_options}
end
defp normalize_options(dhcpd_config), do: dhcpd_config
defp normalize_option({ip_option, ip})
when ip_option in @ip_options do
{ip_option, IP.ip_to_tuple!(ip)}
end
defp normalize_option({ip_list_option, ip_list})
when ip_list_option in @ip_list_options and is_list(ip_list) do
{ip_list_option, Enum.map(ip_list, &IP.ip_to_tuple!/1)}
end
defp normalize_option({string_list_option, string_list})
when string_list_option in @string_list_options and is_list(string_list) do
{string_list_option, Enum.map(string_list, &to_string/1)}
end
defp normalize_option({list_option, one_item})
when list_option in @list_options and not is_list(one_item) do
# Fix super-easy mistake of not passing a list when there's only one item
normalize_option({list_option, [one_item]})
end
defp normalize_option({int_option, value})
when int_option in @int_options and
is_integer(value) do
{int_option, value}
end
defp normalize_option({string_option, string})
when string_option in @string_options do
{string_option, to_string(string)}
end
defp normalize_option({other_option, string})
when is_integer(other_option) and is_binary(string) do
{other_option, to_string(string)}
end
defp normalize_option({bad_option, _value}) do
raise ArgumentError,
"Unknown dhcpd option '#{bad_option}'. Options unknown to VintageNet can be passed in as integers."
end
@doc """
Add udhcpd configuration commands for running a DHCP server
"""
@spec add_config(RawConfig.t(), map(), keyword()) :: RawConfig.t()
def add_config(
%RawConfig{
ifname: ifname,
files: files,
child_specs: child_specs
} = raw_config,
%{dhcpd: dhcpd_config},
opts
) do
tmpdir = Keyword.fetch!(opts, :tmpdir)
udhcpd_conf_path = Path.join(tmpdir, "udhcpd.conf.#{ifname}")
new_files =
files ++
[
{udhcpd_conf_path, udhcpd_contents(ifname, dhcpd_config, tmpdir)}
]
new_child_specs =
child_specs ++
[
Supervisor.child_spec(
{MuonTrap.Daemon,
[
"udhcpd",
[
"-f",
udhcpd_conf_path
],
Command.add_muon_options(
stderr_to_stdout: true,
log_output: :debug,
env: BEAMNotify.env(name: "vintage_net_comm", report_env: true)
)
]},
id: :udhcpd
)
]
%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 udhcpd_contents(ifname, dhcpd, tmpdir) do
pidfile = Path.join(tmpdir, "udhcpd.#{ifname}.pid")
lease_file = Path.join(tmpdir, "udhcpd.#{ifname}.leases")
initial = """
interface #{ifname}
pidfile #{pidfile}
lease_file #{lease_file}
notify_file #{BEAMNotify.bin_path()}
"""
config = Enum.map(dhcpd, &to_udhcpd_string/1)
IO.iodata_to_binary([initial, "\n", config, "\n"])
end
defp to_udhcpd_string({:start, val}) do
"start #{IP.ip_to_string(val)}\n"
end
defp to_udhcpd_string({:end, val}) do
"end #{IP.ip_to_string(val)}\n"
end
defp to_udhcpd_string({:max_leases, val}) do
"max_leases #{val}\n"
end
defp to_udhcpd_string({:decline_time, val}) do
"decline_time #{val}\n"
end
defp to_udhcpd_string({:conflict_time, val}) do
"conflict_time #{val}\n"
end
defp to_udhcpd_string({:offer_time, val}) do
"offer_time #{val}\n"
end
defp to_udhcpd_string({:min_lease, val}) do
"min_lease #{val}\n"
end
defp to_udhcpd_string({:auto_time, val}) do
"auto_time #{val}\n"
end
defp to_udhcpd_string({:static_leases, leases}) do
Enum.map(leases, fn {mac, ip} ->
"static_lease #{mac} #{IP.ip_to_string(ip)}\n"
end)
end
defp to_udhcpd_string({:options, options}) do
for option <- options do
["opt ", to_udhcpd_option_string(option), "\n"]
end
end
defp to_udhcpd_option_string({option, ip}) when option in @ip_options do
[to_string(option), " ", IP.ip_to_string(ip)]
end
defp to_udhcpd_option_string({option, ip_list}) when option in @ip_list_options do
[to_string(option), " " | ip_list_to_iodata(ip_list)]
end
defp to_udhcpd_option_string({option, string_list}) when option in @string_list_options do
[to_string(option), " " | Enum.intersperse(string_list, " ")]
end
defp to_udhcpd_option_string({option, value}) when option in @int_options do
[to_string(option), " ", to_string(value)]
end
defp to_udhcpd_option_string({option, string}) when option in @string_options do
[to_string(option), " ", string]
end
defp to_udhcpd_option_string({other_option, string}) when is_integer(other_option) do
[to_string(other_option), " ", string]
end
defp ip_list_to_iodata(ip_list) do
ip_list
|> Enum.map(&IP.ip_to_string/1)
|> Enum.intersperse(" ")
end
end