defmodule VintageNet.Connectivity.InternetChecker do
use GenServer
require Logger
alias VintageNet.Connectivity.{CheckLogic, Inspector, TCPPing}
alias VintageNet.RouteManager
@moduledoc """
This GenServer monitors a network interface for Internet connectivity
Internet connectivity is determined by reachability to an IP address.
If that address is reachable then other this updates a property to
reflect that. Otherwise, the network interface is assumed to merely
have LAN connectivity if it's up.
"""
@typedoc false
@type state() :: %{
ifname: VintageNet.ifname(),
hosts: [{VintageNet.any_ip_address(), non_neg_integer()}],
status: CheckLogic.state(),
inspector: Inspector.cache()
}
@doc """
Start the connectivity checker GenServer
"""
@spec start_link(VintageNet.ifname()) :: GenServer.on_start()
def start_link(ifname) do
GenServer.start_link(__MODULE__, ifname)
end
@impl GenServer
def init(ifname) do
connectivity = VintageNet.get(["interface", ifname, "connection"])
state = %{
ifname: ifname,
hosts: get_internet_host_list(),
status: CheckLogic.init(connectivity),
inspector: %{}
}
{:ok, state, {:continue, :continue}}
end
@impl GenServer
def handle_continue(:continue, %{ifname: ifname} = state) do
VintageNet.subscribe(lower_up_property(ifname))
# Always run ifup and ifdown depending on the interface even
# if it's redundant. There may have been a crash and this will
# get our connectivity status back in sync.
new_state =
if VintageNet.get(lower_up_property(ifname)) do
state |> ifup() |> report_connectivity("ifup")
else
state |> ifdown() |> report_connectivity("ifdown")
end
{:noreply, new_state, new_state.status.interval}
end
@impl GenServer
def handle_info(:timeout, state) do
new_state = state |> check_connectivity() |> report_connectivity("timeout")
{:noreply, new_state, new_state.status.interval}
end
def handle_info(
{VintageNet, ["interface", ifname, "lower_up"], _old_value, false, _meta},
%{ifname: ifname} = state
) do
new_state = state |> ifdown() |> report_connectivity("ifdown")
{:noreply, new_state, new_state.status.interval}
end
def handle_info(
{VintageNet, ["interface", ifname, "lower_up"], _old_value, true, _meta},
%{ifname: ifname} = state
) do
new_state = state |> ifup() |> report_connectivity("ifup")
{:noreply, new_state, new_state.status.interval}
end
def handle_info(
{VintageNet, ["interface", ifname, "lower_up"], _old_value, nil, _meta},
%{ifname: ifname} = state
) do
# The interface was completely removed!
new_state = state |> ifdown() |> report_connectivity("removed!")
{:noreply, new_state, new_state.status.interval}
end
defp ifdown(state) do
%{state | status: CheckLogic.ifdown(state.status)}
end
defp ifup(state) do
%{state | status: CheckLogic.ifup(state.status)}
end
defp check_connectivity(state) do
{status, new_cache} = Inspector.check_internet(state.ifname, state.inspector)
if status == :available or TCPPing.ping(state.ifname, hd(state.hosts)) == :ok do
%{state | status: CheckLogic.check_succeeded(state.status), inspector: new_cache}
else
%{
state
| status: CheckLogic.check_failed(state.status),
hosts: rotate_list(state.hosts),
inspector: new_cache
}
end
end
defp report_connectivity(state, why) do
# It's desirable to set these even if redundant since the checks in this
# modules are authoritative. I.e., the internet isn't connected unless we
# declare it detected.The following call
# will optimize out redundant updates if they really are redundant.
RouteManager.set_connection_status(state.ifname, state.status.connectivity, why)
state
end
defp lower_up_property(ifname) do
["interface", ifname, "lower_up"]
end
# Rotate a list left
@doc false
@spec rotate_list(list()) :: list()
def rotate_list([]), do: []
def rotate_list(hosts), do: tl(hosts) ++ [hd(hosts)]
defp get_internet_host_list() do
hosts = legacy_internet_host() ++ Application.get_env(:vintage_net, :internet_host_list)
good_hosts = Enum.flat_map(hosts, &normalize_internet_host/1)
if good_hosts == [] do
Logger.warn("VintageNet: `:internet_host_list` is invalid. Using defaults")
[{1, 1, 1, 1}, 80]
else
good_hosts
end
end
defp legacy_internet_host() do
case Application.get_env(:vintage_net, :internet_host) do
nil ->
[]
host ->
Logger.warn(
"VintageNet: Legacy :internet_host key is in use. Please change this to `internet_host_list: [{#{inspect(host)}, 80}]."
)
[{host, 80}]
end
end
defp normalize_internet_host({host, port}) when port > 0 and port < 65536 do
case VintageNet.IP.ip_to_tuple(host) do
{:ok, host_as_tuple} -> [{host_as_tuple, port}]
_anything_else -> []
end
end
defp normalize_internet_host(other) do
Logger.warn("VintageNet: Dropping invalid Internet destination (#{inspect(other)})")
end
end