defmodule RNS.Interface.TCPClient do @moduledoc """ TCP client interface. This is a simple tcp client interface, that will connect to a TCP server. """ @type params() :: map() @type state() :: {RNS.Interface.t(), :inet.socket(), RNS.Config.transport()} require Logger use GenServer @spec start(params(), RNS.Config.transport()) :: {:ok, pid()} def start(params, transport) do GenServer.start(__MODULE__, {params, transport}) end # Callbacks @impl true @spec init({params(), RNS.Config.transport()}) :: {:ok, state()} | {:stop, atom()} def init({%{"host" => host, "port" => port} = params, transport}) do family = case params["ipv6"] do true -> :inet6 false -> :inet nil -> :inet ipv6 -> Logger.warning( "TCP client interface on #{host}:#{port} will use ipv4 because an invalid value has been given for the parameter 'ipv6': #{inspect(ipv6)}. A boolean was expected, we will default to false(ipv4)." ) :inet end case :gen_tcp.connect(String.to_charlist(host), port, [:binary, family]) do {:ok, socket} -> interface = interface(host, port, params) # Tell the interface manager the interface has started and send the pid and interface struct. RNS.InterfaceManager.interface_started(interface) {:ok, {interface, socket, transport}} {:error, error} -> Logger.warning( "TCP client interface failed to connect to #{host} on port #{port}: #{inspect(error)}." ) {:stop, error} end end def init({_, _}) do Logger.error( "TCP client interface failed to start: the parameters 'host' and 'port' are required, one or all of them was not provided." ) {:stop, :not_all_params} end @impl true @spec handle_info({:inbound, RNS.Packet.t(), RNS.Interface.t()}, state()) :: {:noreply, state()} def handle_info({:inbound, packet, _}, state) do socket = elem(state, 1) framed_raw = RNS.Packet.raw(packet) |> RNS.HDLC.frame() :gen_tcp.send(socket, framed_raw) {:noreply, state} end @impl true @spec handle_info({:tcp, :inet.socket(), binary()}, state()) :: {:noreply, state()} def handle_info({:tcp, _socket, packet}, {interface, _, transport} = state) do RNS.PacketHandler.start(fn -> RNS.HDLC.unframe(packet) end, interface, transport) {:noreply, state} end @impl true @spec handle_info({:tcp_closed, :inet.socket()}, state()) :: {:stop, :tcp_closed} def handle_info({:tcp_closed, _socket}, _state) do {:stop, :tcp_closed} end @impl true @spec handle_info({:tcp_closed, :inet.socket()}, state()) :: {:stop, atom()} def handle_info({:tcp_error, _socket, error}, _state) do {:stop, error} end @impl true @spec handle_info({:transport_change, RNS.Config.transport()}, state()) :: {:noreply, state()} def handle_info({:transport_change, transport}, {interface, socket, _old_transport}) do new_state = {interface, socket, transport} {:noreply, new_state} end @spec interface(String.t(), pos_integer(), map()) :: RNS.Interface.t() defp interface(host, port, params) do id = "TCPClient@#{host}:#{port}" mode = case params["mode"] do nil -> :full "full" -> :full "access_point" -> :access_point "gateway" -> :gateway "roaming" -> :roaming "boundary" -> :boundary mode -> Logger.warning(""" Unknown interface mode '#{mode}' on interface #{id}, 'full' will be used instead! The interface mode can be 'full', 'access_point', 'gateway', 'roaming' or 'boundary'. You can read more about interface modes at https://reticulum.network/manual/interfaces.html#interface-modes. """) :full end RNS.Interface.new(id, self(), mode: mode) end end