Skip to content

Commit

Permalink
Support complete replacement of metric calculation
Browse files Browse the repository at this point in the history
This adds a new key, `:route_metric_fun`, that let's users get rid of
the default metric calculator and use their own. The default calculator
probably works for most users, but once you find yourself outside of
what works with the defaults, it's probably easiest to code a solution
that exactly works with your setup rather than try to encourage the
default calculator to work for you.
  • Loading branch information
fhunleth committed Aug 19, 2021
1 parent baf7a7c commit d380400
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 35 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ persistence_secret | A 16-byte secret or an MFA for getting a secret
internet_host_list | IP address/ports to try to connect to for checking Internet connectivity. Defaults to a list of large public DNS providers. E.g., `[{{1, 1, 1, 1}, 53}]`.
regulatory_domain | ISO 3166-1 alpha-2 country (`00` for global, `US`, etc.)
additional_name_servers | List of DNS servers to be used in addition to any supplied by an interface. E.g., `[{1, 1, 1, 1}, {8, 8, 8, 8}]`
route_metric_fun | Customize how network interfaces are prioritized. See `VintageNet.Route.DefaultMetric.compute_metric/2`

## Network interface configuration

Expand Down
2 changes: 1 addition & 1 deletion lib/vintage_net/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ defmodule VintageNet.Application do
dispatcher: &VintageNet.OSEventDispatcher.dispatch/2},
VintageNet.InterfacesMonitor,
{VintageNet.NameResolver, args},
VintageNet.RouteManager,
{VintageNet.RouteManager, args},
{Registry, keys: :unique, name: VintageNet.Interface.Registry},
VintageNet.InterfacesSupervisor
]
Expand Down
10 changes: 10 additions & 0 deletions lib/vintage_net/route.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ defmodule VintageNet.Route do
Types for handling routing tables
"""

alias VintageNet.Route.InterfaceInfo

@typedoc """
Metric (priority) for a routing table entry
"""
Expand Down Expand Up @@ -50,4 +52,12 @@ defmodule VintageNet.Route do
A list of routing table entries
"""
@type entries :: [entry()]

@typedoc """
Compute a route metric value from information about the interface
See `VintageNet.Route.DefaultMetric.compute_metric/2` for an example. This can be
set using the `:route_metric_fun` application environment key.
"""
@type route_metric_fun() :: (VintageNet.ifname(), InterfaceInfo.t() -> metric())
end
14 changes: 8 additions & 6 deletions lib/vintage_net/route/calculator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ defmodule VintageNet.Route.Calculator do
"""

alias VintageNet.Route
alias VintageNet.Route.{DefaultMetric, InterfaceInfo}
alias VintageNet.Route.InterfaceInfo

@type table_indices :: %{VintageNet.ifname() => Route.table_index()}

Expand Down Expand Up @@ -41,9 +41,11 @@ defmodule VintageNet.Route.Calculator do
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()) :: {table_indices(), Route.entries()}
def compute(table_indices, infos) do
{new_table_indices, entries} = Enum.reduce(infos, {table_indices, []}, &make_entries(&1, &2))
@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)

Expand Down Expand Up @@ -73,9 +75,9 @@ defmodule VintageNet.Route.Calculator do
priority_a <= priority_b
end

defp make_entries({ifname, info}, {table_indices, entries}) do
defp make_entries({ifname, info}, {table_indices, entries}, route_metric_fun) do
{new_table_indices, table_index} = get_table_index(ifname, table_indices)
metric = DefaultMetric.compute_metric(ifname, info)
metric = route_metric_fun.(ifname, info)

new_entries = routing_table_entries(metric, ifname, table_index, info)

Expand Down
50 changes: 33 additions & 17 deletions lib/vintage_net/route_manager.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ defmodule VintageNet.RouteManager do
require Logger

alias VintageNet.Interface.NameUtilities
alias VintageNet.Route.{Calculator, InterfaceInfo, IPRoute, Properties}
alias VintageNet.Route
alias VintageNet.Route.{Calculator, DefaultMetric, InterfaceInfo, IPRoute, Properties}

@moduledoc """
This module manages the default route.
Expand Down Expand Up @@ -35,14 +36,21 @@ defmodule VintageNet.RouteManager do
```
"""

defmodule State do
@moduledoc false

defstruct interfaces: nil, route_state: nil, routes: []
end
@typedoc false
@type state() :: %{
interfaces: %{VintageNet.ifname() => InterfaceInfo.t()},
route_state: Calculator.table_indices(),
routes: Route.entries(),
route_metric_fun: Route.route_metric_fun()
}

@doc """
Start the route manager.
Start the route manager
Options:
* `:route_metric_fun` - a 2-arity function that takes a ifname and `VintageNet.Route.InterfaceInfo`
and returns `VintageNet.Route.metric()`
"""
@spec start_link(keyword) :: GenServer.on_start()
def start_link(args) do
Expand Down Expand Up @@ -107,21 +115,32 @@ defmodule VintageNet.RouteManager do
## GenServer

@impl GenServer
def init(_args) do
def init(args) do
route_metric_fun = args[:route_metric_fun] |> check_compute_metric()

# Fresh slate
IPRoute.clear_all_routes()
IPRoute.clear_all_rules(Calculator.rule_table_index_range())

state =
%State{
%{
interfaces: %{},
route_state: Calculator.init()
route_state: Calculator.init(),
route_metric_fun: route_metric_fun,
routes: []
}
|> update_route_tables()

{:ok, state}
end

defp check_compute_metric(fun) when is_function(fun, 2), do: fun

defp check_compute_metric(_other) do
Logger.error("RouteManager: Expecting :route_metric_fun to be a 2-arity function")
&DefaultMetric.compute_metric/2
end

@impl GenServer
def handle_call({:set_route, ifname, ip_subnets, default_gateway}, _from, state) do
if interface_info_changed?(state, ifname, ip_subnets, default_gateway) do
Expand Down Expand Up @@ -206,12 +225,8 @@ defmodule VintageNet.RouteManager do
end

# Only process routes if the status changes
defp update_connection_status(
%State{interfaces: interfaces} = state,
ifname,
new_status
) do
case interfaces[ifname] do
defp update_connection_status(state, ifname, new_status) do
case state.interfaces[ifname] do
nil ->
Logger.warn(
"RouteManager: set_connection_status to #{inspect(new_status)} on unknown ifname: #{ifname}"
Expand All @@ -236,7 +251,8 @@ defmodule VintageNet.RouteManager do

defp update_route_tables(state) do
# See what changed and then run it.
{new_route_state, new_routes} = Calculator.compute(state.route_state, state.interfaces)
{new_route_state, new_routes} =
Calculator.compute(state.route_state, state.interfaces, state.route_metric_fun)

route_delta = List.myers_difference(state.routes, new_routes)

Expand Down
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ defmodule VintageNet.MixProject do
# Contain processes in cgroups by setting to:
# [cgroup_base: "vintage_net", cgroup_controllers: ["cpu"]]
muontrap_options: [],
power_managers: []
power_managers: [],
route_metric_fun: &VintageNet.Route.DefaultMetric.compute_metric/2
],
extra_applications: [:logger, :crypto],
mod: {VintageNet.Application, []}
Expand Down
24 changes: 14 additions & 10 deletions test/vintage_net/route/calculator_test.exs
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
defmodule VintageNet.Route.CalculatorTest do
use ExUnit.Case

alias VintageNet.Route.{Calculator, InterfaceInfo}
alias VintageNet.Route.{Calculator, DefaultMetric, InterfaceInfo}

doctest Calculator

defp compute(state, interfaces) do
Calculator.compute(state, interfaces, &DefaultMetric.compute_metric/2)
end

test "no interfaces" do
state = Calculator.init()

assert {%{}, []} == Calculator.compute(state, %{})
assert {%{}, []} == compute(state, %{})
end

test "one interface" do
Expand All @@ -31,7 +35,7 @@ defmodule VintageNet.Route.CalculatorTest do
{:local_route, "eth0", {192, 168, 1, 50}, 24, 10, :main},
{:default_route, "eth0", {192, 168, 1, 1}, 0, 100},
{:default_route, "eth0", {192, 168, 1, 1}, 10, :main}
]} == Calculator.compute(state, interfaces)
]} == compute(state, interfaces)
end

test "a disconnected interface" do
Expand All @@ -49,7 +53,7 @@ defmodule VintageNet.Route.CalculatorTest do
}
}

assert {%{"eth0" => 100}, []} == Calculator.compute(state, interfaces)
assert {%{"eth0" => 100}, []} == compute(state, interfaces)
end

test "interface w/o addresses" do
Expand All @@ -65,7 +69,7 @@ defmodule VintageNet.Route.CalculatorTest do
}
}

assert {%{"eth0" => 100}, []} == Calculator.compute(state, interfaces)
assert {%{"eth0" => 100}, []} == compute(state, interfaces)
end

test "interface w/o default gateway" do
Expand All @@ -87,7 +91,7 @@ defmodule VintageNet.Route.CalculatorTest do
{:local_route, "eth0", {192, 168, 1, 50}, 24, 0, 100},
{:local_route, "eth0", {192, 168, 1, 50}, 24, 50, :main}
]} ==
Calculator.compute(state, interfaces)
compute(state, interfaces)
end

test "two interfaces, both internet" do
Expand Down Expand Up @@ -122,7 +126,7 @@ defmodule VintageNet.Route.CalculatorTest do
{:default_route, "eth0", {192, 168, 1, 1}, 10, :main},
{:default_route, "wlan0", {192, 168, 1, 1}, 0, 101},
{:default_route, "wlan0", {192, 168, 1, 1}, 20, :main}
]} == Calculator.compute(state, interfaces)
]} == compute(state, interfaces)
end

test "two interfaces, bad ethernet" do
Expand Down Expand Up @@ -157,7 +161,7 @@ defmodule VintageNet.Route.CalculatorTest do
{:default_route, "eth0", {192, 168, 1, 1}, 50, :main},
{:default_route, "wlan0", {192, 168, 1, 1}, 0, 101},
{:default_route, "wlan0", {192, 168, 1, 1}, 20, :main}
]} == Calculator.compute(state, interfaces)
]} == compute(state, interfaces)
end

test "one interface and many addresses" do
Expand Down Expand Up @@ -186,7 +190,7 @@ defmodule VintageNet.Route.CalculatorTest do
{:local_route, "eth0", {192, 168, 1, 50}, 24, 10, :main},
{:default_route, "eth0", {192, 168, 1, 1}, 0, 100},
{:default_route, "eth0", {192, 168, 1, 1}, 10, :main}
]} == Calculator.compute(state, interfaces)
]} == compute(state, interfaces)
end

test "rule table index range is as expected" do
Expand Down Expand Up @@ -237,6 +241,6 @@ defmodule VintageNet.Route.CalculatorTest do
{:default_route, "eth1", {192, 168, 1, 1}, 11, :main},
{:default_route, "eth2", {192, 168, 2, 1}, 0, 102},
{:default_route, "eth2", {192, 168, 2, 1}, 12, :main}
]} == Calculator.compute(state, interfaces)
]} == compute(state, interfaces)
end
end

0 comments on commit d380400

Please sign in to comment.