Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remote Access Tunnels #15

Merged
merged 7 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,17 @@ FROM ubuntu:noble as app

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && apt-get -y install locales socat libconfuse-dev libarchive-dev && apt-get clean
RUN apt-get update \
&& apt-get -y install \
locales \
socat \
libconfuse-dev \
libarchive-dev \
iproute2 \
iptables \
wireguard \
openssh-server \
&& apt-get clean
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && \
locale-gen
ENV LANG en_US.UTF-8
Expand All @@ -67,7 +77,7 @@ RUN echo "peridio:peridio" | chpasswd

ENV PERIDIO_CONFIG_FILE=/etc/peridiod/peridio.json

RUN mkdir -p /etc/peridiod
RUN mkdir -p /etc/peridiod/hooks
RUN mkdir -p /boot
RUN echo "echo \"Reboot Requested\"" > /usr/bin/reboot && chmod +x /usr/bin/reboot

Expand All @@ -77,6 +87,8 @@ COPY --from=build /opt/app/support/peridio.json /etc/peridiod/peridio.json
COPY --from=build /opt/app/support/uboot.env /etc/peridiod/uboot.env
COPY --from=build /opt/app/support/fw_env.config /etc/fw_env.config
COPY --from=build /opt/app/support/peridiod.img /etc/peridiod/peridiod.img
COPY --from=build /opt/app/support/pre-up.sh /etc/peridiod/hooks/pre-up.sh
COPY --from=build /opt/app/support/pre-down.sh /etc/peridiod/hooks/pre-down.sh
COPY --from=build /opt/fwup/src/fwup /usr/bin/fwup

WORKDIR /opt/peridiod
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,11 @@ docker build --tag peridio/peridiod .
Running the container:

```bash
podman run -it --rm --env PERIDIO_CERTIFICATE="$(cat /path/to/end-entity-certificate.pem)" --env PERIDIO_PRIVATE_KEY="$(cat /path/to/end-entity-private-key.pem)" peridio/peridiod:latest
podman run -it --rm --env PERIDIO_CERTIFICATE="$(cat /path/to/end-entity-certificate.pem)" --env PERIDIO_PRIVATE_KEY="$(cat /path/to/end-entity-private-key.pem)" --cap-add=NET_ADMIN peridio/peridiod:latest
mobileoverlord marked this conversation as resolved.
Show resolved Hide resolved
```

The `--cap-add=NET_ADMIN` is required for testing remote access tunnels. This is required because peridiod will create new wireguard network interfaces and needs to execute commands with iptables. If this flag is omitted, the feature will not function properly.

The container will be built using the `peridio.json` configuration file in the support directory. For testing you can modify this as you please. It is configured by default to allow testing for the remote shell and even firmware updates using deployments. You can create firmware to test for deployments using the following:

```bash
Expand Down
1 change: 1 addition & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ config :peridiod,
}}

config :logger, level: :debug
config :peridio_rat, wireguard_client: Peridio.RAT.WireGuard.Default

import_config "#{Mix.env()}.exs"
110 changes: 94 additions & 16 deletions lib/peridiod/configurator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ defmodule Peridiod.Configurator do
params: %{},
remote_shell: false,
remote_iex: false,
remote_access_tunnels: %{},
socket: [],
ssl: []
ssl: [],
sdk_client: nil

@type t() :: %__MODULE__{
device_api_host: String.t(),
Expand All @@ -41,8 +43,10 @@ defmodule Peridiod.Configurator do
params: map(),
remote_iex: boolean,
remote_shell: boolean,
remote_access_tunnels: map(),
socket: any(),
ssl: [:ssl.tls_client_option()]
ssl: [:ssl.tls_client_option()],
sdk_client: %{}
}
end

Expand Down Expand Up @@ -129,6 +133,13 @@ defmodule Peridiod.Configurator do
:device_api_ca_certificate_path,
Application.app_dir(:peridiod, "priv/peridio-cert.pem")
)
|> Map.put(
:remote_access_tunnels,
rat_merge_config(
config.remote_access_tunnels,
Map.get(config_file, "remote_access_tunnels", %{})
)
)
|> override_if_set(
:device_api_ca_certificate_path,
config_file["device_api"]["certificate_path"]
Expand Down Expand Up @@ -163,26 +174,93 @@ defmodule Peridiod.Configurator do
cacertfile: config.device_api_ca_certificate_path
)

case config.key_pair_source do
"file" ->
Configurator.File.config(config.key_pair_config, config)
config =
case config.key_pair_source do
"file" ->
Configurator.File.config(config.key_pair_config, config)

"pkcs11" ->
Configurator.PKCS11.config(config.key_pair_config, config)

"pkcs11" ->
Configurator.PKCS11.config(config.key_pair_config, config)
"uboot-env" ->
Configurator.UBootEnv.config(config.key_pair_config, config)

"uboot-env" ->
Configurator.UBootEnv.config(config.key_pair_config, config)
"env" ->
Configurator.Env.config(config.key_pair_config, config)

"env" ->
Configurator.Env.config(config.key_pair_config, config)
type ->
error("Unknown key pair type: #{type}")
end

type ->
Logger.error("Unknown key pair type: #{type}")
end
adapter = {Tesla.Adapter.Mint, transport_opts: config.ssl}

sdk_client =
PeridioSDK.Client.new(
device_api_host: "https://#{config.device_api_host}",
adapter: adapter,
release_prn: "",
release_version: ""
)

Map.put(config, :sdk_client, sdk_client)
end

defp override_if_set(%{} = config, _key, value) when is_nil(value), do: config
defp override_if_set(%{} = config, key, value), do: Map.replace(config, key, value)

def rat_merge_config(rat_config, rat_config_file) do
hooks_config = Map.get(rat_config, :hooks, %{})

hooks =
rat_default_hooks()
|> Map.merge(hooks_config)
|> override_if_set(:pre_up, rat_config_file["hooks"]["pre_up"])
|> override_if_set(:post_up, rat_config_file["hooks"]["post_up"])
|> override_if_set(:pre_down, rat_config_file["hooks"]["pre_down"])
|> override_if_set(:post_down, rat_config_file["hooks"]["post_down"])

%{
enabled: rat_config_file["enabled"] || rat_config[:enabled] || false,
port_range: rat_config_file["port_range"] || rat_config[:port_range] |> encode_port_range(),
ipv4_cidrs: rat_config_file["ipv4_cidrs"] || rat_config[:ipv4_cidrs] |> encode_ipv4_cidrs(),
service_ports: rat_config_file["service_ports"] || rat_config[:service_ports] || [],
persistent_keepalive:
rat_config_file["persistent_keepalive"] || rat_config[:persistent_keepalive] || 25,
hooks: hooks
}
end

def rat_default_hooks() do
priv_dir = Application.app_dir(:peridiod, "priv")

%{
pre_up: "#{priv_dir}/pre-up.sh",
post_up: "#{priv_dir}/post-up.sh",
pre_down: "#{priv_dir}/pre-down.sh",
post_down: "#{priv_dir}/post-down.sh"
}
end

def encode_port_range(nil), do: Peridio.RAT.Network.default_port_ranges()

def encode_port_range(range) do
[r_start, r_end] = String.split(range, "-") |> Enum.map(&String.to_integer/1)
Range.new(r_start, r_end)
end

def encode_ipv4_cidrs(nil), do: Peridio.RAT.Network.default_ip_address_cidrs()

def encode_ipv4_cidrs([_ | _] = cidrs) do
Enum.map(cidrs, &Peridio.RAT.Network.CIDR.from_string!/1)
end

def deep_merge(map1, map2) when is_map(map1) and is_map(map2) do
Map.merge(map1, map2, fn _key, val1, val2 ->
deep_merge(val1, val2)
end)
end

defp override_if_set(%Config{} = config, _key, value) when is_nil(value), do: config
defp override_if_set(%Config{} = config, key, value), do: Map.replace(config, key, value)
def deep_merge(_val1, val2), do: val2

defp add_socket_opts(config) do
# PhoenixClient requires these SSL options be passed as
Expand Down
58 changes: 58 additions & 0 deletions lib/peridiod/socket.ex
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,11 @@ defmodule Peridiod.Socket do
socket =
new_socket()
|> assign(params: config.params)
|> assign(device_api_host: config.device_api_host)
|> assign(sdk_client: config.sdk_client)
|> assign(remote_iex: config.remote_iex)
|> assign(remote_shell: config.remote_shell)
|> assign(remote_access_tunnels: config.remote_access_tunnels)
|> assign(remote_console_pid: nil)
|> assign(remote_console_timer: nil)
|> connect!(opts)
Expand Down Expand Up @@ -170,6 +173,61 @@ defmodule Peridiod.Socket do
end
end

def handle_message(
@device_topic,
"tunnel_request",
%{"tunnel_prn" => tunnel_prn} = payload,
%{assigns: %{remote_access_tunnels: %{enabled: false}}} = socket
) do
Logger.warning(
"Remote Access Tunnel requested but not enabled on the device: #{inspect(payload)}"
)

Peridiod.Tunnel.close_request(socket.assigns.sdk_client, tunnel_prn, "feature_not_enabled")
{:ok, socket}
end

def handle_message(
@device_topic,
"tunnel_request",
%{"tunnel_prn" => tunnel_prn} = request,
socket
) do
dport = request["device_tunnel_port"] || 22
service_ports = socket.assigns.remote_access_tunnels.service_ports

if dport in service_ports do
Peridiod.Tunnel.create(
socket.assigns.sdk_client,
tunnel_prn,
dport,
socket.assigns.remote_access_tunnels
)
else
Logger.warning(
"Remote Access Tunnel requested for port #{dport} but not enabled in service port list: #{inspect(service_ports)}"
)

Peridiod.Tunnel.close_request(socket.assigns.sdk_client, tunnel_prn, "dport_not_allowed")
end

{:ok, socket}
end

def handle_message(
@device_topic,
"tunnel_close",
_request,
%{assigns: %{remote_access_tunnels: %{enabled: false}}} = socket
) do
{:ok, socket}
end

def handle_message(@device_topic, "tunnel_close", %{"tunnel_prn" => tunnel_prn}, socket) do
Peridio.RAT.close_tunnel(tunnel_prn)
{:ok, socket}
end

##
# Console API messages
#
Expand Down
91 changes: 91 additions & 0 deletions lib/peridiod/tunnel.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
defmodule Peridiod.Tunnel do
# Cloud -> Device: Request to open a tunnel
# Device -> Cloud: Response public key, available cidrs, and available ports
# Cloud -> Device: Response interface / peer information
require Logger
alias Peridio.RAT.Network

def create(client, tunnel_prn, dport, rat_config) do
opts =
[
dport: dport,
ipv4_cidrs: rat_config.ipv4_cidrs,
port_range: rat_config.port_range
]

interface =
Peridio.RAT.WireGuard.generate_key_pair()
|> Peridio.RAT.WireGuard.Interface.new()

case Peridiod.Tunnel.configure_request(client, opts, interface, tunnel_prn) do
{:ok, resp} ->
{:ok, expires_at, _} = DateTime.from_iso8601(resp.body["data"]["expires_at"])

peer = %Peridio.RAT.WireGuard.Peer{
ip_address: resp.body["data"]["server_proxy_ip_address"],
endpoint: resp.body["data"]["server_tunnel_ip_address"],
port: resp.body["data"]["server_proxy_port"],
public_key: resp.body["data"]["server_public_key"],
persistent_keepalive: rat_config.persistent_keepalive
}

ip_address =
resp.body["data"]["device_proxy_ip_address"]
|> String.split(".")
|> Enum.map(&String.to_integer/1)
|> List.to_tuple()
|> Peridio.RAT.Network.IP.new()

interface = Map.put(interface, :ip_address, ip_address)
args = [interface.id, opts[:dport]] |> Enum.join(" ")

hooks = """
PreUp = #{rat_config.hooks.pre_up} #{args}
PostUp = #{rat_config.hooks.post_up} #{args}
PreDown = #{rat_config.hooks.pre_down} #{args}
PostDown = #{rat_config.hooks.post_down} #{args}
"""

Peridio.RAT.open_tunnel(tunnel_prn, interface, peer, expires_at: expires_at, hooks: hooks)

error ->
Logger.error("Remote Tunnel Error #{inspect(error)}")
end
end

def configure_request(client, opts, interface, tunnel_prn) do
tunnel_opts = %{
cidr_blocks: Network.available_cidrs(opts[:ipv4_cidrs]),
port_ranges: Network.available_ports(opts[:port_range]),
device_proxy_port: interface.port,
device_tunnel_port: opts[:dport],
device_public_key: interface.public_key
}

PeridioSDK.DeviceAPI.Tunnels.configure(client, tunnel_prn, tunnel_opts)
end

def close_request(client, tunnel_prn, reason) do
PeridioSDK.DeviceAPI.Tunnels.update(client, tunnel_prn, %{
state: "closed",
close_reason: reason
})
end

defimpl Jason.Encoder, for: Range do
def encode(range, opts) do
first = range.first
last = range.last

range_string = "#{first}-#{last}"

Jason.Encode.string(range_string, opts)
end
end

defimpl Jason.Encoder, for: Peridio.RAT.Network.CIDR do
def encode(cidr, opts) do
Jason.Encode.string(Peridio.RAT.Network.CIDR.to_string(cidr), opts)
end
end
end
4 changes: 3 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule Peridiod.MixProject do
def project do
[
app: :peridiod,
version: "2.4.2",
version: "2.5.0-dev.1",
elixir: "~> 1.15",
start_permanent: Mix.env() == :prod,
deps: deps(),
Expand All @@ -24,6 +24,8 @@ defmodule Peridiod.MixProject do
defp deps do
[
{:extty, "~> 0.2"},
{:peridio_rat, github: "peridio/peridio-rat", branch: "main"},
{:peridio_sdk, github: "peridio/peridio-elixir", branch: "main"},
{:muontrap, "~> 1.3"},
{:circuits_uart, "~> 1.5"},
{:castore, "~> 1.0"},
Expand Down
Loading