defmodule VintageNet.InterfacesMonitor do
@moduledoc """
Monitor available interfaces
Currently this works by polling the system for what interfaces are visible.
They may or may not be configured.
"""
use GenServer
# require Logger
alias VintageNet.InterfacesMonitor.{HWPath, Info}
defmodule State do
@moduledoc false
defstruct port: nil,
interface_info: %{}
end
@spec start_link(any()) :: GenServer.on_start()
def start_link(args) do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end
@doc """
Force clear all addresses
This is useful to notify everyone that an address should not be used
immediately. This can be used to fix a race condition where the blip
for an address going away to coming back isn't reported.
"""
@spec force_clear_ipv4_addresses(VintageNet.ifname()) :: :ok
def force_clear_ipv4_addresses(ifname) do
GenServer.call(__MODULE__, {:force_clear_ipv4_addresses, ifname})
end
@impl GenServer
def init(_args) do
executable = :code.priv_dir(:vintage_net) ++ '/if_monitor'
case File.exists?(executable) do
true ->
port =
Port.open({:spawn_executable, executable}, [
{:packet, 2},
:use_stdio,
:binary,
:exit_status
])
{:ok, %State{port: port}}
false ->
# This is only done for testing on OSX
{:ok, %State{}}
end
end
@impl GenServer
def handle_call({:force_clear_ipv4_addresses, ifname}, _from, state) do
{ifindex, old_info} = get_by_ifname(state, ifname)
new_info = Info.delete_ipv4_addresses(old_info)
if old_info != new_info do
new_info = Info.update_address_properties(new_info)
new_state = %{state | interface_info: Map.put(state.interface_info, ifindex, new_info)}
{:reply, :ok, new_state}
else
{:reply, :ok, state}
end
end
@impl GenServer
def handle_info({_port, {:data, raw_report}}, state) do
report = :erlang.binary_to_term(raw_report)
# Logger.debug("if_monitor: #{inspect(report, limit: :infinity)}")
new_state = handle_report(state, report)
{:noreply, new_state}
end
defp handle_report(state, {:newlink, ifname, ifindex, link_report}) do
new_info =
get_or_create_info(state, ifindex, ifname)
|> Info.newlink(link_report)
|> Info.update_link_properties()
%{state | interface_info: Map.put(state.interface_info, ifindex, new_info)}
end
defp handle_report(state, {:dellink, ifname, ifindex, _link_report}) do
Info.clear_properties(ifname)
%{state | interface_info: Map.delete(state.interface_info, ifindex)}
end
defp handle_report(state, {:newaddr, ifindex, address_report}) do
new_info =
get_or_create_info(state, ifindex)
|> Info.newaddr(address_report)
|> Info.update_address_properties()
%{state | interface_info: Map.put(state.interface_info, ifindex, new_info)}
end
defp handle_report(state, {:deladdr, ifindex, address_report}) do
new_info =
get_or_create_info(state, ifindex)
|> Info.deladdr(address_report)
|> Info.update_address_properties()
%{state | interface_info: Map.put(state.interface_info, ifindex, new_info)}
end
defp get_by_ifname(state, ifname) do
Enum.find_value(state.interface_info, fn {ifindex, info} ->
case info do
%{ifname: ^ifname} -> {ifindex, info}
_ -> nil
end
end)
end
defp get_or_create_info(state, ifindex, ifname) do
case Map.fetch(state.interface_info, ifindex) do
{:ok, %{ifname: ^ifname} = info} ->
info
{:ok, %{ifname: old_ifname} = info} ->
Info.clear_properties(old_ifname)
%{info | ifname: ifname}
|> Info.update_present()
|> Info.update_address_properties()
_missing ->
hw_path = HWPath.query(ifname)
Info.new(ifname, hw_path)
|> Info.update_present()
end
end
defp get_or_create_info(state, ifindex) do
case Map.fetch(state.interface_info, ifindex) do
{:ok, info} ->
info
_missing ->
# Race between address and link notifications?
Info.new("__unknown")
end
end
end