Packages
Nerves Networking Module
Retired package: Deprecated - See nerves_network for new projects, legacy projects can reference github:ghitchens/nerves_networking instead
Current section
Files
Jump to
Current section
Files
lib/networking/subsystem.ex
defmodule Nerves.Networking.Subsystem do
@moduledoc """
Implements an ethernet subsystem based on using the `ip` and `udhcpc` CLI.
Designed for embedded systems to provide simple ipv4 ethernet access.
This doesn't rely on using `udhcpc` as a daemon, but instead simply invokes
it to perform low level configuration, assuming the
`Nerves.Networking` module handles daemon-level functionality like
DHCP renewal, etc.
Major Limitations:
- does not yet support ipv6
- horrible error checking for low level commands (assumes they work)
"""
require Logger
@type reason :: any
@type ip_address :: String.t
@type interface :: String.t
@udhcpc_script_path "/tmp/udhcpc.sh"
@resolv_conf_path (case Mix.env do
:test -> "/tmp/resolv.conf"
_ -> "/etc/resolv.conf"
end)
@default_hostname "nerves"
@useful_dhcp_keys [
:status, :interface, :ip, :subnet, :mask, :timezone, :router,
:timesvr, :dns, :hostname, :domain, :ipttl, :broadcast, :ntpsrv,
:opt53, :lease, :dhcptype, :serverid, :message
]
@doc "Initialize the ethernet subsystem"
@spec initialize() :: :ok | {:error, reason}
def initialize do
Logger.debug "initializing Networking.Subsystem"
ensure_udhcpc_setup!
:ok
end
@doc "Set the link state of an interface up or down"
@spec link_set(interface, :up | :down) :: :ok
def link_set(interface, :up) do
ip_cmd "link set #{interface} up"
end
def link_set(interface, :down) do
ip_cmd "link set #{interface} down"
end
@doc "Return the MAC address of the specified interface"
@spec mac_address(interface) :: String.t
def mac_address(interface) do
File.read! "/sys/class/net/#{interface}/address"
end
@doc "add an address to the interface"
def add_address(interface, ip, mask) do
ip_cmd "addr add #{ip}/#{mask} dev #{interface}"
end
@doc "clear the list of addresses for the interface"
@spec flush_addresses(interface) :: :ok
def flush_addresses(interface) do
ip_cmd "addr flush dev #{interface}"
end
@doc "set the router (default gateway) for the inteface"
@spec set_router(interface, ip_address) :: :ok
def set_router(interface, router) do
ip_cmd "route add default via #{router} dev #{interface}"
end
@doc "set resolver configuration"
@spec set_resolv_conf(String.t, String.t) :: :ok
def set_resolv_conf(dns, domain \\ nil) do
resolv_conf = resolv_domain(domain) <> resolv_dns(dns) <> "\n"
Logger.debug "setting resolver config: #{resolv_conf}"
File.write! @resolv_conf_path, resolv_conf
:ok
end
# given a string like "4.4.4.4 8.8.8.8", return resolv.conf lines with newlines
def resolv_dns(dns) do
dns
|> String.split(" ")
|> Enum.map(&("nameserver #{&1}"))
|> Enum.join("\n")
end
# given a domain return the resolver domain line with newline
defp resolv_domain(domain) do
case domain do
"" -> ""
nil -> ""
d -> "domain #{d}\n"
end
end
@doc """
Makes a dhcp request on the specified interface with optional hostname, and
returns `Dict` with standardized keys for the result of the DHCP request.
Uses `udhcpc` in non-daemon mode to handle dhcp.
"""
@spec dhcp_request(interface, String.t, String.t) :: Dict.t
def dhcp_request(interface, host, retries) do
"udhcpc -n -q -f -s #{@udhcpc_script_path} --timeout=#{dhcp_timeout(retries)} --interface=#{interface} -x hostname: #{hostname(host)}"
|> os_cmd
|> parse_udhcpc_response
|> filter_to_only_useful_dhcp_keys
end
# convert the response from udhcpc to Dict form by walking through resulting
# responses to select the last (most relelvant) response, and convert to Dict
# form
defp parse_udhcpc_response(response) do
case (Regex.scan ~r/\[.*\]/sU, response) do
[_, [last_response]] ->
Regex.scan(~r/(\w+='.+')\n/U, last_response)
|> Enum.map(&cleanup_kv/1)
_ ->
Logger.error "#{__MODULE__} udhcpc invalid response: #{response}"
[]
end
end
# respond only with keys that are useful for dhcp
defp filter_to_only_useful_dhcp_keys(dict) do
Enum.filter(dict, fn({k,_v}) -> Enum.member?(@useful_dhcp_keys, k) end)
end
# transform "foo='bar'" to {:foo, "bar"} to parse udhcpc result
defp cleanup_kv([_, kqval]) do
[key, qval] = String.split(kqval, "=")
[_, val] = Regex.run(~r/'(.+)'/s, qval)
{String.to_atom(key), val}
end
# write a script for udhcpc to report dhcp results with
defp ensure_udhcpc_setup! do
udhcpc_script="#!/bin/sh\necho [\necho status=\\'$1\\'\nset\necho ]\n"
File.write! @udhcpc_script_path, udhcpc_script
File.chmod! @udhcpc_script_path, 0o777
end
defp ip_cmd(cmd) do
"/sbin/ip #{cmd}"
|> os_cmd
|> ip_response_to_status
end
# TODO this is horrible - lack of any kind of error checking
defp ip_response_to_status(_ip_response) do
:ok
end
# TODO below should likely be moved to public in Nerves.Utils
defp os_cmd(cmd) do
response = unlogged_os_cmd(cmd)
#Logger.debug "#{__MODULE__} os_cmd: #{inspect cmd} -> #{inspect response}"
response
end
defp unlogged_os_cmd(cmd) do
cmd
|> :erlang.binary_to_list
|> os_module.cmd
|> :erlang.list_to_binary
end
# config underlying :os module to allow creating a mock for :os.cmd
defp os_module do
Application.get_env :nerves_networking, :os_module, :os
end
defp hostname(nil) do
n = node
|> to_string
|> String.split("@")
|> Enum.at(1)
case n do
nil -> @default_hostname
host -> host
end
end
defp hostname(host), do: host
# For the first DHCPDISCOVER use default 3 second timeout
# For subsequent use longer timeout
defp dhcp_timeout(0), do: 3
defp dhcp_timeout(_), do: 10
end