Skip to content

Commit

Permalink
Elevate DHCP options to their own module
Browse files Browse the repository at this point in the history
This is a large change that removes the use of unmodified string maps
for representing DHCP options. Now that DHCP options are intentionally
part of the public interface, the keys used are now atoms with values
that have documented types.

Additionally, this makes some functions and modules involved with DHCP
options more obviously private. They need to be public to be easily
tested, but they may change in the future.
  • Loading branch information
fhunleth committed Jan 22, 2023
1 parent 6f6ef21 commit 624e18f
Show file tree
Hide file tree
Showing 9 changed files with 339 additions and 153 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ Property | Values | Description
`lower_up` | `true` or `false` | This indicates whether the physical layer is "up". E.g., a cable is connected or WiFi associated
`mac_address` | "11:22:33:44:55:66" | The interface's MAC address as a string
`addresses` | [address_info] | This is a list of all of the addresses assigned to this interface
`dhcp_options` | `%{...}` | When DHCP is in use, the processed response information and options is stored here. See `t:VintageNet.DHCP.Options.t/0`

Specific types of interfaces provide more parameters.

Expand Down
181 changes: 181 additions & 0 deletions lib/vintage_net/dhcp/options.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
defmodule VintageNet.DHCP.Options do
@moduledoc """
DHCP Options
"""

alias VintageNet.IP
require Logger

@typedoc """
A map of options and other information reported by udhcpc
Here's an example:
```elixir
%{
broadcast: {192, 168, 7, 255},
dns: {192, 168, 7, 1},
domain: "hunleth.lan",
hostname: "nerves-9780",
ip: {192, 168, 7, 190},
lease: 86400,
mask: 24,
router: {192, 168, 7, 1},
serverid: {192, 168, 7, 1},
siaddr: {192, 168, 7, 1},
subnet: {255, 255, 255, 0}
}
```
"""
@type t() :: %{
optional(:ip) => :inet.ip_address(),
optional(:mask) => non_neg_integer(),
optional(:siaddr) => :inet.ip_address(),
optional(:subnet) => :inet.ip_address(),
optional(:timezone) => String.t(),
optional(:router) => [:inet.ip_address()],
optional(:dns) => [:inet.ip_address()],
optional(:lprsrv) => [:inet.ip_address()],
optional(:hostname) => String.t(),
optional(:bootsize) => String.t(),
optional(:domain) => String.t(),
optional(:swapsrv) => :inet.ip_address(),
optional(:rootpath) => String.t(),
optional(:ipttl) => non_neg_integer(),
optional(:mtu) => non_neg_integer(),
optional(:broadcast) => :inet.ip_address(),
optional(:routes) => [:inet.ip_address()],
optional(:nisdomain) => String.t(),
optional(:nissrv) => [:inet.ip_address()],
optional(:ntpsrv) => [:inet.ip_address()],
optional(:wins) => String.t(),
optional(:lease) => non_neg_integer(),
optional(:serverid) => :inet.ip_address(),
optional(:message) => String.t(),
optional(:renewal_time) => non_neg_integer(),
optional(:rebind_time) => non_neg_integer(),
optional(:vendor) => String.t(),
optional(:tftp) => String.t(),
optional(:bootfile) => String.t(),
optional(:userclass) => String.t(),
optional(:tzstr) => String.t(),
optional(:tzdbstr) => String.t(),
optional(:search) => String.t(),
optional(:sipsrv) => String.t(),
optional(:staticroutes) => [:inet.ip_address()],
optional(:vlanid) => String.t(),
optional(:vlanpriority) => non_neg_integer(),
optional(:pxeconffile) => String.t(),
optional(:pxepathprefix) => String.t(),
optional(:reboottime) => String.t(),
optional(:ip6rd) => String.t(),
optional(:msstaticroutes) => String.t(),
optional(:wpad) => String.t()
}

# Extract and translate udhcpc environment variables to DHCP options
@doc false
@spec udhcpc_to_options(%{String.t() => String.t()}) :: t()
def udhcpc_to_options(info) do
info
|> Map.new(&transform_udhcpc_option/1)
|> Map.delete(:discard)
end

# udhcpc passes DHCP options via environment variables and there's a lot of noise.
# Transform known keys to atoms and mark unknown or unsupported ones as `:discard`
defp transform_udhcpc_option({k, v}) do
# See https://elixir.bootlin.com/busybox/1.35.0/source/networking/udhcp/common.c#L97
# See https://www.rfc-editor.org/rfc/rfc2132 for descriptions
udhcpc_option_map = %{
# DHCP fields
"ip" => {:ip, &IP.ip_to_tuple/1},
"mask" => {:mask, &parse_int/1},
"siaddr" => {:siaddr, &IP.ip_to_tuple/1},
# DHCP options
"subnet" => {:subnet, &IP.ip_to_tuple/1},
"timezone" => {:timezone, &identity/1},
"router" => {:router, &parse_ip_list/1},
# "opt4" => :timesrv,
# "opt5" => :namesrv,
"dns" => {:dns, &parse_ip_list/1},
# "opt7" => :logsrv,
# "opt8" => :cookiesrv,
"lprsrv" => {:lprsrv, &parse_ip_list/1},
"hostname" => {:hostname, &identity/1},
"bootsize" => {:bootsize, &identity/1},
"domain" => {:domain, &identity/1},
"swapsrv" => {:swapsrv, &IP.ip_to_tuple/1},
"rootpath" => {:rootpath, &identity/1},
"ipttl" => {:ipttl, &parse_int/1},
"mtu" => {:mtu, &parse_int/1},
"broadcast" => {:broadcast, &IP.ip_to_tuple/1},
"routes" => {:routes, &parse_ip_list/1},
"nisdomain" => {:nisdomain, &identity/1},
"nissrv" => {:nissrv, &parse_ip_list/1},
"ntpsrv" => {:ntpsrv, &parse_ip_list/1},
"wins" => {:wins, &identity/1},
"lease" => {:lease, &parse_int/1},
"serverid" => {:serverid, &IP.ip_to_tuple/1},
"message" => {:message, &identity/1},
"opt58" => {:renewal_time, &parse_hex/1},
"opt59" => {:rebind_time, &parse_hex/1},
"vendor" => {:vendor, &identity/1},
"tftp" => {:tftp, &identity/1},
"bootfile" => {:bootfile, &identity/1},
"opt77" => {:userclass, &identity/1},
"tzstr" => {:tzstr, &identity/1},
"tzdbstr" => {:tzdbstr, &identity/1},
"search" => {:search, &identity/1},
"sipsrv" => {:sipsrv, &identity/1},
"staticroutes" => {:staticroutes, &parse_ip_list/1},
"vlanid" => {:vlanid, &identity/1},
"vlanpriority" => {:vlanpriority, &parse_int/1},
"pxeconffile" => {:pxeconffile, &identity/1},
"pxepathprefix" => {:pxepathprefix, &identity/1},
"reboottime" => {:reboottime, &identity/1},
"ip6rd" => {:ip6rd, &identity/1},
"msstaticroutes" => {:msstaticroutes, &identity/1},
"wpad" => {:wpad, &identity/1}
# opt50 is used to request a client IP and used internally by udhcpc, so skip.
# opt53 is the message type, so it's not an option
# opt57 is the max message length, so it's not an option
# See https://elixir.bootlin.com/busybox/1.35.0/source/networking/udhcp/common.c#L83
}

with {:ok, {key, parser}} <- Map.fetch(udhcpc_option_map, k),
{:ok, result} <- parser.(v) do
{key, result}
else
_error -> {:discard, nil}
end
end

defp summarize_ok_tuples(ok_tuples, results \\ [])
defp summarize_ok_tuples([], results), do: {:ok, Enum.reverse(results)}
defp summarize_ok_tuples([{:ok, v} | rest], acc), do: summarize_ok_tuples(rest, [v | acc])
defp summarize_ok_tuples([{:error, _} = error | _rest], _), do: error

defp parse_ip_list(str) do
str
|> String.split(" ", trim: true)
|> Enum.map(&IP.ip_to_tuple/1)
|> summarize_ok_tuples()
end

defp parse_int(str) do
case Integer.parse(str) do
{v, ""} -> {:ok, v}
_ -> {:error, "Expecting integer, got #{str}."}
end
end

defp parse_hex(str) do
case Integer.parse(str, 16) do
{v, ""} -> {:ok, v}
_ -> {:error, "Expecting hex, got #{str}."}
end
end

defp identity(str), do: {:ok, str}
end
44 changes: 21 additions & 23 deletions lib/vintage_net/interface/udhcpc.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ defmodule VintageNet.Interface.Udhcpc do
@moduledoc false
@behaviour VintageNet.OSEventDispatcher.UdhcpcHandler

alias VintageNet.{Command, InterfacesMonitor, IP, NameResolver, RouteManager}
alias VintageNet.Command
alias VintageNet.DHCP.Options
alias VintageNet.InterfacesMonitor
alias VintageNet.IP
alias VintageNet.NameResolver
alias VintageNet.RouteManager

require Logger

@spec deconfig(binary, any) :: :ok
@doc """
Handle deconfig reports from udhcpc
"""
Expand Down Expand Up @@ -65,19 +69,16 @@ defmodule VintageNet.Interface.Udhcpc do
leasefail(ifname, info)
end

defp broadcast_args(%{"broadcast" => broadcast}), do: ["broadcast", broadcast]
defp broadcast_args(%{broadcast: broadcast}), do: ["broadcast", IP.ip_to_string(broadcast)]
defp broadcast_args(_), do: []

defp netmask_args(%{"subnet" => subnet}), do: ["netmask", subnet]
defp netmask_args(%{subnet: subnet}), do: ["netmask", IP.ip_to_string(subnet)]
defp netmask_args(_), do: []

defp build_ifconfig_args(ifname, %{"ip" => ip} = info) do
[ifname, ip] ++ broadcast_args(info) ++ netmask_args(info)
end

defp ip_subnet(%{"ip" => address, "mask" => mask}) do
{:ok, our_ip} = IP.ip_to_tuple(address)
{our_ip, String.to_integer(mask)}
@doc false
@spec ifconfig_args(VintageNet.ifname(), Options.t()) :: [String.t()]
def ifconfig_args(ifname, info) do
[ifname, IP.ip_to_string(info.ip)] ++ broadcast_args(info) ++ netmask_args(info)
end

@doc """
Expand All @@ -94,14 +95,11 @@ defmodule VintageNet.Interface.Udhcpc do
# fi
# /sbin/ifconfig $interface $ip $BROADCAST $NETMASK

ifconfig_args = build_ifconfig_args(ifname, info)
_ = Command.cmd("ifconfig", ifconfig_args)

case info["router"] do
[first_router | _rest] ->
ip_subnet = ip_subnet(info)
_ = Command.cmd("ifconfig", ifconfig_args(ifname, info))

{:ok, default_gateway} = IP.ip_to_tuple(first_router)
case info[:router] do
[default_gateway | _rest] ->
ip_subnet = {info.ip, info.mask}

RouteManager.set_route(ifname, [ip_subnet], default_gateway)

Expand All @@ -127,12 +125,12 @@ defmodule VintageNet.Interface.Udhcpc do

domain =
cond do
Map.has_key?(info, "search") ->
Map.has_key?(info, :search) ->
# prefer rfc3359 domain search list (option 119) if available
Map.get(info, "search")
Map.get(info, :search)

Map.has_key?(info, "domain") ->
Map.get(info, "domain")
Map.has_key?(info, :domain) ->
Map.get(info, :domain)

true ->
""
Expand All @@ -142,7 +140,7 @@ defmodule VintageNet.Interface.Udhcpc do
# echo adding dns $i
# echo "nameserver $i # $interface" >> $RESOLV_CONF
# done
dns = Map.get(info, "dns", [])
dns = Map.get(info, :dns, [])

NameResolver.setup(ifname, domain, dns)
:ok
Expand Down
5 changes: 2 additions & 3 deletions lib/vintage_net/name_resolver.ex
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ defmodule VintageNet.NameResolver do
This replaces any entries in the `/etc/resolv.conf` for this interface.
"""
@spec setup(String.t(), String.t() | nil, [VintageNet.any_ip_address()]) :: :ok
@spec setup(String.t(), String.t() | nil, [:inet.ip_address()]) :: :ok
def setup(ifname, domain, name_servers) do
GenServer.call(__MODULE__, {:setup, ifname, domain, name_servers})
end
Expand Down Expand Up @@ -99,8 +99,7 @@ defmodule VintageNet.NameResolver do

@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}
ifentry = %{domain: domain, name_servers: name_servers}

state = %{state | entries: Map.put(state.entries, ifname, ifentry)}
refresh(state)
Expand Down
35 changes: 9 additions & 26 deletions lib/vintage_net/os_event_dispatcher.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defmodule VintageNet.OSEventDispatcher do
@moduledoc false
alias VintageNet.DHCP.Options
require Logger

@doc """
Expand All @@ -14,33 +15,23 @@ defmodule VintageNet.OSEventDispatcher do
OS environment.
"""
@spec dispatch([String.t()], %{String.t() => String.t()}) :: :ok

def dispatch([op], %{"interface" => ifname} = info)
when op in ["deconfig", "leasefail", "nak", "renew", "bound"] do
# udhcpc update
handler = Application.get_env(:vintage_net, :udhcpc_handler)
dhcp_options = Options.udhcpc_to_options(info)

case op do
"deconfig" ->
PropertyTable.delete(VintageNet, ["interface", ifname, "dhcp_options"])

"bound" ->
dhcp_options =
info
|> Enum.filter(fn {k, _} -> k != String.upcase(k) end)
|> Enum.into(%{})

PropertyTable.put(VintageNet, ["interface", ifname, "dhcp_options"], dhcp_options)

_ ->
:ok
if op in ["deconfig", "leasefail", "nak"] do
PropertyTable.delete(VintageNet, ["interface", ifname, "dhcp_options"])
else
PropertyTable.put(VintageNet, ["interface", ifname, "dhcp_options"], dhcp_options)
end

new_info = info |> key_to_list("dns") |> key_to_list("router")

apply(handler, String.to_atom(op), [ifname, new_info])
apply(handler, String.to_atom(op), [ifname, dhcp_options])
end

def dispatch([lease_file], _env) do
# udhcpd update
case extract_lease_file_ifname(lease_file) do
{:ok, ifname} ->
handler = Application.get_env(:vintage_net, :udhcpd_handler)
Expand All @@ -64,12 +55,4 @@ defmodule VintageNet.OSEventDispatcher do
_ -> :error
end
end

# This preserves the behavior of an earlier version of this code.
defp key_to_list(info, key) do
case Map.fetch(info, key) do
{:ok, s} -> %{info | key => String.split(s, " ", trim: true)}
:error -> info
end
end
end
Loading

0 comments on commit 624e18f

Please sign in to comment.