lib/vintage_net/route/default_metric.ex

defmodule VintageNet.Route.DefaultMetric do
  @moduledoc """
  Default module for prioritizing network interfaces

  The priority order is:

  1. Internet-connected interfaces are chosen before LAN-connected interfaces
  2. Wired Ethernet, then wifi, then mobile and then any other interfaces
  3. The interface's weight is used to resolve ties. By default, the weight is
     derived from the interfaces index. E.g., `eth0`'s weight is 0 (highest priority)
     and `eth1`'s weight is 1 (next highest)
  """

  alias VintageNet.Route
  alias VintageNet.Route.InterfaceInfo

  # Priority order list
  #
  # `{:ethernet, :internet}` - Wired ethernet that's Internet connected
  # `{:ethernet, :_}` - Wired ethernet with any status
  # `{:_, :internet}` - Any Internet-connected network interface
  @prioritization [
    {:ethernet, :internet},
    {:wifi, :internet},
    {:mobile, :internet},
    {:_, :internet},
    {:ethernet, :lan},
    {:wifi, :lan},
    {:mobile, :lan},
    {:_, :lan}
  ]

  @doc """
  Compute the routing metric for an interface with a status

  This uses the prioritization list to figure out what number should
  be used for the Linux routing table metric. It could also be `:disabled`
  to indicate that a route shouldn't be added to the Linux routing tables
  at all.
  """
  @spec compute_metric(VintageNet.ifname(), InterfaceInfo.t()) :: Route.metric() | :disabled
  def compute_metric(_ifname, %InterfaceInfo{status: :disconnected} = _info) do
    # Short cut disconnected interfaces
    :disabled
  end

  def compute_metric(_ifname, %InterfaceInfo{} = info) do
    case Enum.find_index(@prioritization, fn option ->
           matches_option?(option, info.interface_type, info.status)
         end) do
      nil ->
        :disabled

      value ->
        # Don't return 0, since that looks like the metric wasn't set. Also space out the numbers.
        # (Lower numbers are higher priority)
        (value + 1) * 10 + info.weight
    end
  end

  defp matches_option?({type, status}, type, status), do: true
  defp matches_option?({:_, status}, _type, status), do: true
  defp matches_option?({type, :_}, type, _status), do: true
  defp matches_option?(_option, _type, _status), do: false
end