defmodule VintageNet.Route.Calculator do
@moduledoc """
This module computes the desired routing table contents
It's used by the RouteManager to update the Linux routing tables when interfaces
come online or change state. See the RouteManager docs for a discussion of how
routes are configured.
The functions in this module have no side effects so that it's easier
to test that routing scenarios result in correct Linux routing table
configurations.
"""
alias VintageNet.Route
alias VintageNet.Route.InterfaceInfo
@type table_indices :: %{VintageNet.ifname() => Route.table_index()}
@type interface_infos :: %{VintageNet.ifname() => InterfaceInfo.t()}
@doc """
Initialize state carried between calculations
"""
@spec init() :: table_indices()
def init() do
%{}
end
@doc """
Return the table indices used for routing based on source IP.
"""
@spec rule_table_index_range() :: Range.t()
def rule_table_index_range() do
max_index = 100 + VintageNet.max_interface_count() - 1
100..max_index
end
@doc """
Compute a Linux routing table configuration
The entries are ordered so that List.myers_difference/2 can be used to
minimize the routing table changes.
"""
@spec compute(table_indices(), interface_infos(), Route.route_metric_fun()) ::
{table_indices(), Route.entries()}
def compute(table_indices, infos, route_metric_fun) do
{new_table_indices, entries} =
Enum.reduce(infos, {table_indices, []}, &make_entries(&1, &2, route_metric_fun))
sorted_entries = Enum.sort(entries, &sort/2)
{new_table_indices, sorted_entries}
end
# Sort order
#
# 1. Rules
# 2. Local routes
# 3. Default routes
#
# The most important part is that local routes get created before default
# routes. Linux disallows default routes that can't be supported and the
# local routes are needed for that.
defp sort_priority(:rule), do: 0
defp sort_priority(:local_route), do: 1
defp sort_priority(:default_route), do: 2
defp sort(a, b) when elem(a, 0) == elem(b, 0) do
a <= b
end
defp sort(a, b) do
priority_a = elem(a, 0) |> sort_priority()
priority_b = elem(b, 0) |> sort_priority()
priority_a <= priority_b
end
defp make_entries({ifname, info}, {table_indices, entries}, route_metric_fun) do
{new_table_indices, table_index} = get_table_index(ifname, table_indices)
metric = route_metric_fun.(ifname, info)
new_entries = routing_table_entries(metric, ifname, table_index, info)
{new_table_indices, new_entries ++ entries}
end
defp routing_table_entries(:disabled, _ifname, _table_index, _info) do
[]
end
defp routing_table_entries(metric, ifname, table_index, info) do
# Every packet with a source IP address of this interface should use the
# routing table "table_index" instead of the "main" one. That lets users
# communicate bidirectionally on interfaces that wouldn't be used by default.
# For example, consider a WiFi/Ethernet case: without this, responses to a
# TCP connection initiated over WiFi could be sent via Ethernet since
# Ethernet is generally preferred over WiFi. However, that would be strange
# to have packets received over WiFi be responded to via Ethernet.
rules = for {ip, _subnet} <- info.ip_subnets, do: {:rule, table_index, ip}
# The local routes ensure that any packets sent to a LAN go out the
# appropriate interface. These need to be ordered by metric so that if two
# or more interfaces connect to the same LAN, they're prioritized.
local_routes =
if info.ip_subnets != [] do
{ip, subnet_bits} = hd(info.ip_subnets)
[
{:local_route, ifname, ip, subnet_bits, 0, table_index},
{:local_route, ifname, ip, subnet_bits, metric, :main}
]
else
[]
end
# If a default gateway is set, add it to the source-routed table for the
# interface and to the main routing table. In a multiple interface setup,
# the main routing table will have more than one default gateway and the
# metric will determine which one is used.
tables =
if info.default_gateway != nil and rules != [] do
[
{:default_route, ifname, info.default_gateway, 0, table_index},
{:default_route, ifname, info.default_gateway, metric, :main}
]
else
[]
end
rules ++ local_routes ++ tables
end
defp get_table_index(ifname, table_indices) do
case Map.get(table_indices, ifname) do
nil ->
index = allocate_table_index(table_indices)
new_table_indices = Map.put(table_indices, ifname, index)
{new_table_indices, index}
index ->
{table_indices, index}
end
end
defp allocate_table_index(table_indices) do
# This shouldn't be called all that often and table_indices should
# be small (number of real network interfaces), so performance "shouldn't"
# matter...
used = Map.values(table_indices)
case Enum.find(rule_table_index_range(), fn n -> not Enum.member?(used, n) end) do
nil ->
raise "VintageNet.Route.Calculator ran out of table indices. This is probably due to more than `:max_interface_count` in use simultaneously."
picked ->
picked
end
end
end