Skip to content

Commit

Permalink
Add SSL based internet connectivity checker
Browse files Browse the repository at this point in the history
This adds the ability to detect sneaky firewalls or anything else that
may give false positives on internet connectivity by checking
that a valid SSL connection can be made to a list of hosts.

Signed-off-by: Connor Rigby <[email protected]>
  • Loading branch information
ConnorRigby committed May 2, 2024
1 parent f98ea12 commit e34b05c
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 8 deletions.
26 changes: 26 additions & 0 deletions lib/vintage_net/connectivity/host_list.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,32 @@ defmodule VintageNet.Connectivity.HostList do
end
end

@doc """
Load the internet hostname list from the application environment
"""
@spec load_hostnames(keyword()) :: [{String.t(), 1..65535}]
def load_hostnames(config \\ Application.get_all_env(:vintage_net)) do
config_list = internet_hostname_list(config)

if config_list == [] do
Logger.warning("VintageNet: empty or invalid `:internet_host_list` so using defaults")
[{"google.com", 443}]
else
config_list
end
end

defp internet_hostname_list(config) do
case config[:internet_hostname_list] do
host_list when is_list(host_list) ->
host_list

_ ->
Logger.warning("VintageNet: :internet_hostname_list must be a list")
[]
end
end

defp internet_host_list(config) do
case config[:internet_host_list] do
host_list when is_list(host_list) ->
Expand Down
35 changes: 30 additions & 5 deletions lib/vintage_net/connectivity/internet_checker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,23 @@ defmodule VintageNet.Connectivity.InternetChecker do
"""
use GenServer

alias VintageNet.Connectivity.{CheckLogic, HostList, Inspector, TCPPing}
alias VintageNet.Connectivity.{
CheckLogic,
HostList,
Inspector,
SSLConnect,
TCPPing
}

alias VintageNet.RouteManager
require Logger

@typedoc false
@type state() :: %{
ifname: VintageNet.ifname(),
configured_hosts: [{VintageNet.any_ip_address(), non_neg_integer()}],
ping_list: [{:inet.ip_address(), non_neg_integer()}],
configured_hosts: [{VintageNet.any_ip_address(), 1..65535}],
ping_list: [{:inet.ip_address(), 1..65535}],
hostname_list: [{String.t(), 1..65535}],
check_logic: CheckLogic.state(),
inspector: Inspector.cache(),
status: Inspector.status()
Expand All @@ -39,6 +47,7 @@ defmodule VintageNet.Connectivity.InternetChecker do
ifname: ifname,
configured_hosts: HostList.load(),
ping_list: [],
hostname_list: [],
check_logic: CheckLogic.init(connectivity),
inspector: %{},
status: :unknown
Expand Down Expand Up @@ -111,12 +120,14 @@ defmodule VintageNet.Connectivity.InternetChecker do
# 1. Reset status to unknown
# 2. See if we can determine internet-connectivity via TCP stats
# 3. If still unknown, refresh the ping list
# 4. If still unknown, ping. This step is definitive.
# 4a. If still unknown, ping. This step can be tricked by really stubborn firewalls.
# 4b. If still unknown, connect via SSL. This step cannot be fooled.
# 5. Record whether there's internet
state
|> reset_status()
|> check_inspector()
|> reload_ping_list()
|> reload_hostname_list()
|> ping_if_unknown()
|> update_check_logic()
end
Expand All @@ -142,14 +153,28 @@ defmodule VintageNet.Connectivity.InternetChecker do

defp reload_ping_list(state), do: state

defp reload_hostname_list(%{status: :unknown, hostname_list: []} = state) do
hostname_list = HostList.load_hostnames()
%{state | hostname_list: hostname_list}
end

defp reload_hostname_list(state), do: state

defp ping_if_unknown(%{status: :unknown, ping_list: [who | rest]} = state) do
case TCPPing.ping(state.ifname, who) do
:ok -> %{state | status: :internet}
_error -> %{state | status: :no_internet, ping_list: rest}
end
end

defp ping_if_unknown(%{status: :unknown, ping_list: []} = state) do
defp ping_if_unknown(%{status: :unknown, ping_list: [], hostname_list: [who | rest]} = state) do
case SSLConnect.connect(state.ifname, who) do
:ok -> %{state | status: :internet}
_error -> %{state | status: :no_internet, hostname_list: rest}
end
end

defp ping_if_unknown(%{status: :unknown, ping_list: [], hostname_list: []} = state) do
# Ping list being empty is due to the user only providing hostnames and
# DNS resolution not working.
%{state | status: :no_internet}
Expand Down
47 changes: 47 additions & 0 deletions lib/vintage_net/connectivity/ssl_connect.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
defmodule VintageNet.Connectivity.SSLConnect do
@moduledoc """
Test connectivity by making a connection using SSL
Connectivity with a remote host can be checked by making a SSL connection to
it. The connection either works, the connection is refused, or it times out.
The first two cases indicate connectivity.
"""

import VintageNet.Connectivity.TCPPing, only: [get_interface_address: 2]

@connect_timeout 5_000

@doc """
Check connectivity with another device
The "connect" is a SSL connection attempt from the specified interface to
an IP address and port. Failures to connect don't necessarily mean that the
Internet is down, but it's likely especially if the server that's specified
in the configuration is highly available.
"""
@spec connect(VintageNet.ifname(), {String.t(), port}) :: :ok | {:error, :inet.posix()}
def connect(ifname, {hostname, port}) do
with {:ok, src_ip} <- get_interface_address(ifname, :inet),
{:ok, ssl} <-
:ssl.connect(
to_charlist(hostname),
port,
[
verify: :verify_peer,
cacerts: :public_key.cacerts_get(),
active: false,
ip: src_ip
],
@connect_timeout
) do
_ = :ssl.close(ssl)
:ok
else
{:error, reason} ->
{:error, reason}

posix_error ->
{:error, posix_error}
end
end
end
11 changes: 9 additions & 2 deletions lib/vintage_net/connectivity/tcp_ping.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ defmodule VintageNet.Connectivity.TCPPing do
"""
@ping_timeout 5_000

@type ping_error_reason :: :if_not_found | :no_ipv4_address | :inet.posix()
@type ping_error_reason :: :if_not_found | :no_suitable_ip_address | :inet.posix()

@doc """
Check connectivity with another device
Expand Down Expand Up @@ -51,7 +51,14 @@ defmodule VintageNet.Connectivity.TCPPing do
end
end

defp get_interface_address(ifname, family) do
@doc """
Helper function for getting an ip address attached to an interface for use with the
`ip` keyword argument for :gen_tcp.connect/3 and related functions.
"""
@spec get_interface_address(VintageNet.ifname(), :inet | :inet6) ::
{:ok, VintageNet.any_ip_address()}
| {:error, :inet.posix() | :if_not_found | :no_suitable_ip_address}
def get_interface_address(ifname, family) do
with {:ok, addresses} <- :inet.getifaddrs(),
{:ok, params} <- find_addresses_on_interface(addresses, ifname) do
find_ip_addr(params, family)
Expand Down
5 changes: 4 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,17 @@ defmodule VintageNet.MixProject do
# Neustar
{{156, 154, 70, 5}, 53}
],
internet_hostname_list: [
{"google.com", 443}
],
regulatory_domain: "00",
# Contain processes in cgroups by setting to:
# [cgroup_base: "vintage_net", cgroup_controllers: ["cpu"]]
muontrap_options: [],
power_managers: [],
route_metric_fun: &VintageNet.Route.DefaultMetric.compute_metric/2
],
extra_applications: [:logger, :crypto],
extra_applications: [:logger, :crypto, :ssl, :public_key],
mod: {VintageNet.Application, []}
]
end
Expand Down
14 changes: 14 additions & 0 deletions test/vintage_net/connectivity/ssl_connect_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
defmodule VintageNet.Connectivity.SSLConnectTest do
use ExUnit.Case, async: true

alias VintageNet.Connectivity.SSLConnect
alias VintageNetTest.Utils

test "connect to known host" do
ifname = Utils.get_ifname_for_tests()

assert SSLConnect.connect(ifname, {"google.com", 443}) == :ok
assert SSLConnect.connect(ifname, {"github.com", 443}) == :ok
assert SSLConnect.connect(ifname, {"superfakedomain", 443}) == {:error, :nxdomain}
end
end

0 comments on commit e34b05c

Please sign in to comment.