lib/vintage_net_direct.ex

defmodule VintageNetDirect do
  @moduledoc """
  Support for directly connected Ethernet configurations

  Direct Ethernet connections are those where the network connects only two
  devices. Examples include a virtual Ethernet interface being run over a USB
  cable. This is a popular Nerves configuration for development where the USB
  cable provides power and networking to a Raspberry Pi, Beaglebone or other
  "USB Gadget"-capable board. Another example would be a direct Ethernet
  connection between a device and a development computer.  Such a connection is
  handy when a router isn't readily available.

  The VintageNet Technology works by assigning a static IP address to the
  Ethernet interface on this side of the connection and running a DHCP server
  to assign an IP address to the other side of the cable.  IP addresses are
  computed based on the hostname and interface name. A /30 subnet is used for
  the two IP addresses for each side of the cable to try to avoid conflicts
  with IP subnets used on either computer. The DHCP server in use is very
  simple and assigns the same IP address every time.

  Note that many decisions were made to make this use case work well. If
  you're thinking about use cases with more than just the one cable and two
  endpoints, you'll want to look elsewhere.

  Configurations for this technology are maps with a `:type` field set to
  `VintageNetDirect`. `VintageNetDirect`-specific options are in a map under
  the `:vintage_net_direct` key (formerly the `:gadget` key). These include:

  * `:hostname` - if non-nil, this overrides the hostname used for computing
    a unique IP address for this interface. If unset, `:inet.gethostname/0`
    is used.

  Most users should specify the following configuration:

  ```elixir
  %{type: VintageNetDirect}
  ```
  """
  @behaviour VintageNet.Technology

  alias VintageNet.Interface.RawConfig
  alias VintageNet.IP.IPv4Config

  @impl VintageNet.Technology
  def normalize(%{type: __MODULE__} = config) do
    normalized =
      get_specific_options(config)
      |> normalize_options()

    %{type: __MODULE__, vintage_net_direct: normalized}
  end

  defp get_specific_options(config) do
    Map.get(config, :vintage_net_direct) || Map.get(config, :gadget)
  end

  defp normalize_options(%{hostname: hostname}) when is_binary(hostname) do
    %{hostname: hostname}
  end

  defp normalize_options(_specific_config), do: %{}

  @impl VintageNet.Technology
  def to_raw_config(ifname, %{type: __MODULE__} = config, opts) do
    normalized_config = normalize(config)

    # Derive the subnet based on the ifname, but allow the user to force a hostname
    subnet =
      case normalized_config.vintage_net_direct do
        %{hostname: hostname} ->
          OneDHCPD.IPCalculator.default_subnet(ifname, hostname)

        _ ->
          OneDHCPD.IPCalculator.default_subnet(ifname)
      end

    ipv4_config = %{
      ipv4: %{
        method: :static,
        address: OneDHCPD.IPCalculator.our_ip_address(subnet),
        prefix_length: OneDHCPD.IPCalculator.prefix_length()
      }
    }

    %RawConfig{
      ifname: ifname,
      type: __MODULE__,
      source_config: normalized_config,
      required_ifnames: [ifname],
      child_specs: [{OneDHCPD.Server, [ifname, [subnet: subnet]]}]
    }
    |> IPv4Config.add_config(ipv4_config, opts)
  end

  @impl VintageNet.Technology
  def ioctl(_ifname, _command, _args) do
    {:error, :unsupported}
  end

  @impl VintageNet.Technology
  def check_system(_opts) do
    # TODO
    :ok
  end
end