Skip to content

Commit

Permalink
Add behaviour for ssl ping allowing users to specify their own implem…
Browse files Browse the repository at this point in the history
…entation

Signed-off-by: Connor Rigby <[email protected]>
  • Loading branch information
ConnorRigby committed May 7, 2024
1 parent 4781d22 commit 7a83e52
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 27 deletions.
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ resolvconf | Path to `/etc/resolv.conf`
persistence | Module for persisting network configurations
persistence_dir | Path to a directory for storing persisted configurations
persistence_secret | A 16-byte secret or a function or MFArgs (module, function, arguments tuple) for getting a secret
internet_host_list | IP address or hostnames and 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}]`.
internet_host_list | IP address or hostnames and ports to try to connect to for checking Internet connectivity. Defaults to a list of large public DNS providers. E.g., `[{:tcp_ping, host: {1, 1, 1, 1}, port: 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`
Expand Down Expand Up @@ -474,7 +474,7 @@ For example,

```elixir
config :vintage_net,
internet_host_list: [{"abcdefghijk-ats.iot.us-east-1.amazonaws.com", 443}]
internet_host_list: [{:tcp_ping, host: "abcdefghijk-ats.iot.us-east-1.amazonaws.com", port: 443}]
```

The use of the connectivity checker is specified by the technology. Both the
Expand All @@ -484,6 +484,29 @@ GenServer to the `:child_specs` configuration returned by the technology. E.g.,
`child_specs: [{VintageNet.Connectivity.InternetChecker, "eth0"}]`. Most users
do not need to be concerned about this.

### SSL Internet Connectivity Checking

If devices are deployed in a particularly locked-down environment where connections via
TCP could be intercepted by a firewall or similar, users may want to use the `:ssl_ping` option
instead,

```elixir
config :vintage_net,
internet_host_list: [{:ssl_ping, host: "google.com", port: 443}]
```

**NOTE**: using this module defaults to using the `VintageNet.Connectivity.SSLPing.PublicKey` module
for supplying the initial ssl connection options. This uses `:public_key.cacerts_get()` on OTP 25 and
greater. Usage of this function to get the cacerts is potentially dangerous as the certs bundled by
`:public_key` will eventually expire, causing the `ping` to fail in the future. Users who wish to
use the `:ssl_ping` module should specify their own connect opts by means of a module that implements
the `VintageNet.Connectivity.SSLPing.ConnectOptions` behaviour:

```elixir
config :vintage_net,
internet_host_list: [{:ssl_ping, host: "abcdefghijk-ats.iot.us-east-1.amazonaws.com", port: 443, connect_options_impl: MySSLPingConnectOptsImpl}]
```

## Power Management

Some devices require additional work to be done for them to become available.
Expand Down
33 changes: 11 additions & 22 deletions lib/vintage_net/connectivity/ssl_ping.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@ defmodule VintageNet.Connectivity.SSLPing do
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.
This module should be configured with a `:connect_opts_mfa` option. It should
implement the `VintageNet.Connectivity.SSLPing.ConnectOptions` behaviour.
The default implementation will use `:public_key.cacerts_get()` which can
potentially be insecure.
"""

@behaviour VintageNet.Connectivity.Ping

import VintageNet.Connectivity.TCPPing, only: [get_interface_address: 2]
alias VintageNet.Connectivity.HostList
alias VintageNet.Connectivity.SSLPing.PublicKey
require Logger

@connect_timeout 5_000
Expand All @@ -23,11 +29,12 @@ defmodule VintageNet.Connectivity.SSLPing do
Internet is down, but it's likely especially if the server that's specified
in the configuration is highly available.
"""
@impl VintageNet.Connectivity.Ping
@spec ping(VintageNet.ifname(), HostList.options()) :: :ok | {:error, :inet.posix()}
def ping(ifname, opts) do
host = Keyword.fetch!(opts, :host)
port = Keyword.fetch!(opts, :port)
initial_opts = get_initial_opts(opts)
initial_opts = get_connect_options(opts)

with {:ok, src_ip} <- get_interface_address(ifname, :inet),
{:ok, ssl} <-
Expand All @@ -48,32 +55,14 @@ defmodule VintageNet.Connectivity.SSLPing do
end
end

defp get_initial_opts(opts) do
{module, fun, args} =
Keyword.get(opts, :connect_opts_mfa, {__MODULE__, :default_connect_opts, []})

apply(module, fun, args)
defp get_connect_options(opts) do
module = Keyword.get(opts, :connect_options_impl, PublicKey)
module.connect_options()
end

defp connect_opts(initial_opts, src_ip) do
initial_opts
|> Keyword.put(:active, false)
|> Keyword.put(:ip, src_ip)
end

@doc false
@spec default_connect_opts() :: Keyword.t()
if :erlang.system_info(:otp_release) in [~c"21", ~c"22", ~c"23", ~c"24"] do
def default_connect_opts() do
Logger.warning("SSLConnect support on OTP 24 is limited due to lack of cacerts")
[]
end
else
def default_connect_opts() do
[
cacerts: :public_key.cacerts_get(),
verify: :verify_peer
]
end
end
end
16 changes: 16 additions & 0 deletions lib/vintage_net/connectivity/ssl_ping/connect_options.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
defmodule VintageNet.Connectivity.SSLPing.ConnectOptions do
@moduledoc """
Implement this behaviour for the use with the SSLPing module. This allows users
to configure how the `:ssl.connect/3` behaves. For example, if using Amazon AWS IOT,
users will want to provide a `:cacerts` option with the list of certs.
"""

@doc """
Callback to be called before `:ssl.connect/3`. Implementations should return
the following options in most cases:
* `:cacerts` - List of cacerts to be used in verification.
* `verify: :verify_peer` - Upon connect, verify the other connection.
"""
@callback connect_options() :: [:ssl.tls_client_option()]
end
30 changes: 30 additions & 0 deletions lib/vintage_net/connectivity/ssl_ping/public_key.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
defmodule VintageNet.Connectivity.SSLPing.PublicKey do
@moduledoc """
Insecure connect options for SSLPing connectivity checker. This module
is an example, and should not be used in production devices. It uses
`:public_key.cacerts` which will likely be valid at the time of firmware
creation, however they will become invalid and unable to update in the
future without a firmware upgrade.
"""

@behaviour VintageNet.Connectivity.SSLPing.ConnectOptions
require Logger

@doc false
@impl VintageNet.Connectivity.SSLPing.ConnectOptions
if :erlang.system_info(:otp_release) in [~c"21", ~c"22", ~c"23", ~c"24"] do
def connect_options() do
Logger.warning("SSLPing support on OTP 24 is limited due to lack of cacerts")
[]
end
else
def connect_options() do
Logger.warning("SSLPing using :public_key for :cacerts. This is potentially insecure.")

[
cacerts: :public_key.cacerts_get(),
verify: :verify_peer
]
end
end
end
3 changes: 3 additions & 0 deletions lib/vintage_net/connectivity/tcp_ping.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ defmodule VintageNet.Connectivity.TCPPing do
way usually works unless a device is behind a strict firewall, but there's
usually at least one IP address/port on the Internet that they allow.
"""
@behaviour VintageNet.Connectivity.Ping

alias VintageNet.Connectivity.HostList

@ping_timeout 5_000
Expand All @@ -28,6 +30,7 @@ defmodule VintageNet.Connectivity.TCPPing do
Source IP-based routing is required for the TCP connect to go out the right
network interface. This is configured by default when using VintageNet.
"""
@impl VintageNet.Connectivity.Ping
@spec ping(VintageNet.ifname(), HostList.options()) :: :ok | {:error, ping_error_reason()}
def ping(ifname, opts) do
host = Keyword.fetch!(opts, :host)
Expand Down
6 changes: 3 additions & 3 deletions test/vintage_net/connectivity/ssl_ping_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ defmodule VintageNet.Connectivity.SSLPingTest do
test "connect to known host" do
ifname = Utils.get_ifname_for_tests()

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

0 comments on commit 7a83e52

Please sign in to comment.