Current section
Files
Jump to
Current section
Files
lib/rns/interface/tcp_server.ex
defmodule RNS.Interface.TCPServer do
@moduledoc """
TCP server interface.
This is a simple tcp server, which will spawn a process for each connection.
"""
@type params() :: map()
require Logger
@spec start(params(), RNS.Config.transport()) :: pid()
def start(params, transport) do
spawn(__MODULE__, :init, [params, transport])
end
# Callbacks
@spec init(params(), RNS.Config.transport()) :: no_return()
def init(%{"port" => port} = params, transport) do
opts = opts(params)
case :gen_tcp.listen(port, [:binary, {:reuseaddr, true}]) do
{:ok, listen_socket} ->
Logger.debug("TCP Server now listening on port #{port}...")
loop(listen_socket, transport, opts)
{:error, error} ->
Logger.warning("TCP server on port #{port} failed to start with reason #{inspect(error)}")
exit(error)
end
end
@spec loop(:inet.socket(), RNS.Config.transport(), Keyword.t()) :: no_return()
def loop(listen_socket, transport, opts) do
{:ok, client_socket} = :gen_tcp.accept(listen_socket)
pid = RNS.Interface.TCPServer.Client.start(client_socket, transport, opts)
:gen_tcp.controlling_process(client_socket, pid)
loop(listen_socket, transport, opts)
end
@spec opts(map()) :: Keyword.t()
defp opts(params) do
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 TCP server interface, '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
[mode: mode]
end
end
defmodule RNS.Interface.TCPServer.Client do
@moduledoc """
TCP server client handler interface.
This genserver is spawned for each connection to the TCP server.
"""
@type state() :: {RNS.Interface.t(), :inet.socket(), RNS.Config.transport()}
use GenServer
@spec start(:inet.socket(), RNS.Config.transport(), Keyword.t()) :: pid()
def start(socket, transport, opts) do
{:ok, pid} = GenServer.start(__MODULE__, {socket, transport, opts})
pid
end
# Callbacks
@impl true
@spec init({:inet.socket(), RNS.Config.transport(), Keyword.t()}) :: {:ok, state()}
def init({socket, transport, opts}) do
{:ok, {host, port}} = :inet.peername(socket)
interface = RNS.Interface.new("TCPServerClient@#{host}:#{port}", self(), opts)
# Tell the interface manager the interface has started and send the pid and interface struct.
RNS.InterfaceManager.interface_started(interface)
{:ok, {interface, socket, transport}}
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
end