Packages

A standalone Elixir implementation of the Libp2p networking stack

Current section

Files

Jump to
libp2p_elixir lib libp2p connection.ex
Raw

lib/libp2p/connection.ex

defmodule Libp2p.Connection do
@moduledoc """
One libp2p connection process.
Owns:
- TCP socket
- Noise secure channel state (`Libp2p.SecureConn`)
- Yamux session state (`Libp2p.Yamux.Session`)
Exposes basic stream send/recv primitives used by higher-level protocol handlers.
"""
use GenServer
alias Libp2p.{ConnUpgrade, SecureConn}
alias Libp2p.Transport.Tcp
alias Libp2p.Yamux.Session
@type t :: pid()
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts) do
GenServer.start_link(__MODULE__, opts)
end
@doc "Block until the connection has completed the upgrade pipeline."
@spec await_ready(t(), timeout()) :: :ok | {:error, term()}
def await_ready(conn, timeout \\ 20_000) do
GenServer.call(conn, :await_ready, timeout)
end
@doc "Open a new outbound yamux stream."
@spec open_stream(t()) :: {:ok, non_neg_integer()} | {:error, term()}
def open_stream(conn) do
try do
GenServer.call(conn, :open_stream)
catch
:exit, {:timeout, _} -> {:error, :timeout}
:exit, :timeout -> {:error, :timeout}
:exit, {:noproc, _} -> {:error, :noproc}
:exit, :noproc -> {:error, :noproc}
end
end
@doc "Return the remote peer id (from Noise) once ready."
@spec remote_peer_id(t()) :: {:ok, binary()} | {:error, term()}
def remote_peer_id(conn) do
GenServer.call(conn, :remote_peer_id)
end
@spec peer_store(t()) :: term()
def peer_store(conn) do
GenServer.call(conn, :peer_store)
end
@doc "Send bytes on a yamux stream."
@spec stream_send(t(), non_neg_integer(), binary()) :: :ok | {:error, term()}
def stream_send(conn, stream_id, data) when is_integer(stream_id) and is_binary(data) do
try do
GenServer.call(conn, {:stream_send, stream_id, data})
catch
:exit, {:timeout, _} -> {:error, :timeout}
:exit, :timeout -> {:error, :timeout}
:exit, {:noproc, _} -> {:error, :noproc}
:exit, :noproc -> {:error, :noproc}
end
end
@doc "Receive bytes for a yamux stream (may return partial data)."
@spec stream_recv(t(), non_neg_integer(), timeout()) :: {:ok, binary()} | {:error, term()}
def stream_recv(conn, stream_id, timeout \\ 5_000) when is_integer(stream_id) do
try do
GenServer.call(conn, {:stream_recv, stream_id}, timeout)
catch
:exit, {:timeout, _} -> {:error, :timeout}
:exit, :timeout -> {:error, :timeout}
:exit, {:noproc, _} -> {:error, :noproc}
:exit, :noproc -> {:error, :noproc}
end
end
@doc "Close (FIN) a yamux stream."
@spec stream_close(t(), non_neg_integer()) :: :ok | {:error, term()}
def stream_close(conn, stream_id) when is_integer(stream_id) do
GenServer.call(conn, {:stream_close, stream_id})
end
@doc "Set the process that will receive stream events."
@spec set_stream_handler(t(), non_neg_integer(), pid()) :: :ok
def set_stream_handler(conn, stream_id, pid) do
GenServer.call(conn, {:set_stream_handler, stream_id, pid})
end
@impl true
def init(opts) do
st = %{
swarm: Keyword.fetch!(opts, :swarm),
socket: Keyword.fetch!(opts, :socket),
direction: Keyword.fetch!(opts, :direction),
identity: Keyword.fetch!(opts, :identity),
peer_store: Keyword.fetch!(opts, :peer_store),
secure: nil,
yamux: nil,
streams: %{},
ready_waiters: [],
started?: false,
remote_peer_id: nil
}
# Swarm will transfer socket ownership then send :start_upgrade.
{:ok, st}
end
@impl true
def handle_continue(:upgrade, st) do
upgrade =
case st.direction do
:inbound -> ConnUpgrade.upgrade_inbound(st.socket, st.identity, timeout: 30_000)
:outbound -> ConnUpgrade.upgrade_outbound(st.socket, st.identity, timeout: 30_000)
end
case upgrade do
{:ok, %SecureConn{} = secure, %Session{} = yamux, remote_peer_id} ->
:ok = :inet.setopts(st.socket, active: :once)
st = %{st | secure: secure, yamux: yamux}
st = %{st | remote_peer_id: remote_peer_id}
send(st.swarm, {:connection_ready, self(), remote_peer_id})
Enum.each(st.ready_waiters, fn from -> GenServer.reply(from, :ok) end)
{:noreply, %{st | ready_waiters: []}}
{:error, reason} ->
_ = Tcp.close(st.socket)
Enum.each(st.ready_waiters, fn from -> GenServer.reply(from, {:error, reason}) end)
{:stop, reason, st}
end
end
@impl true
def handle_call(:await_ready, from, %{secure: nil} = st) do
{:noreply, %{st | ready_waiters: [from | st.ready_waiters]}}
end
def handle_call(:await_ready, _from, st) do
{:reply, :ok, st}
end
def handle_call(:remote_peer_id, _from, %{remote_peer_id: nil} = st), do: {:reply, {:error, :not_ready}, st}
def handle_call(:remote_peer_id, _from, st), do: {:reply, {:ok, st.remote_peer_id}, st}
def handle_call(:peer_store, _from, st), do: {:reply, st.peer_store, st}
def handle_call(:__local_identity__, _from, st), do: {:reply, st.identity, st}
def handle_call(:open_stream, _from, %{secure: nil} = st), do: {:reply, {:error, :not_ready}, st}
def handle_call(:open_stream, _from, st) do
{id, out, yamux2} = Session.open_stream(st.yamux)
case SecureConn.send(st.secure, out) do
{:ok, secure2} ->
st = %{st | secure: secure2, yamux: yamux2}
st = ensure_stream(st, id)
{:reply, {:ok, id}, st}
{:error, reason} ->
{:reply, {:error, reason}, st}
end
end
def handle_call({:open_stream, initial_data}, _from, st) do
{id, out, yamux2} = Session.open_stream_with_data(st.yamux, initial_data)
case SecureConn.send(st.secure, out) do
{:ok, secure2} ->
st = %{st | secure: secure2, yamux: yamux2}
st = ensure_stream(st, id)
{:reply, {:ok, id}, st}
{:error, reason} ->
{:reply, {:error, reason}, st}
end
end
def handle_call({:stream_send, id, data}, _from, st) do
st = ensure_stream(st, id)
{out, yamux2} = Session.send_data(st.yamux, id, data)
case SecureConn.send(st.secure, out) do
{:ok, secure2} -> {:reply, :ok, %{st | secure: secure2, yamux: yamux2}}
{:error, reason} -> {:reply, {:error, reason}, st}
end
end
def handle_call({:send_stream, id, data}, from, st) do
handle_call({:stream_send, id, data}, from, st)
end
def handle_call({:stream_close, id}, _from, st) do
st = ensure_stream(st, id)
{out, yamux2} = Session.close_stream(st.yamux, id)
case SecureConn.send(st.secure, out) do
{:ok, secure2} ->
st = %{st | secure: secure2, yamux: yamux2}
{:reply, :ok, mark_stream_closed(st, id)}
{:error, reason} ->
{:reply, {:error, reason}, st}
end
end
def handle_call({:close_stream, id}, from, st) do
handle_call({:stream_close, id}, from, st)
end
def handle_call({:stream_recv, id}, from, st) do
st = ensure_stream(st, id)
s = st.streams[id]
cond do
s.closed? and s.buf == <<>> ->
{:reply, {:error, :closed}, st}
s.buf != <<>> ->
{:reply, {:ok, s.buf}, put_stream(st, id, %{s | buf: <<>>})}
true ->
{:noreply, put_stream(st, id, %{s | waiters: [from | s.waiters]})}
end
end
def handle_call({:set_stream_handler, id, pid}, _from, st) do
st = ensure_stream(st, id)
s = st.streams[id]
# If buffer has data, push it immediately
s =
if s.buf != <<>> do
send(pid, {:libp2p, :stream_data, self(), id, s.buf})
%{s | buf: <<>>}
else
s
end
{:reply, :ok, put_stream(st, id, %{s | owner: pid})}
end
@impl true
def handle_info({:tcp, sock, data}, %{socket: sock, secure: %SecureConn{} = secure, yamux: %Session{} = yamux} = st) do
:ok = :inet.setopts(sock, active: :once)
secure = SecureConn.ingest(secure, data)
{msgs, secure} = SecureConn.drain(secure)
{st, secure, yamux} =
Enum.reduce(msgs, {st, secure, yamux}, fn pt, {st_acc, sec_acc, yamux_acc} ->
{events, out, yamux2} = Session.feed(yamux_acc, pt)
st2 = handle_yamux_events(st_acc, events)
sec2 =
if out == <<>> do
sec_acc
else
case SecureConn.send(sec_acc, out) do
{:ok, sec_ok} -> sec_ok
{:error, _} -> sec_acc
end
end
{st2, sec2, yamux2}
end)
{:noreply, %{st | secure: secure, yamux: yamux}}
end
def handle_info({:tcp_closed, sock}, %{socket: sock} = st) do
{:stop, :tcp_closed, st}
end
def handle_info(:start_upgrade, %{started?: false} = st) do
{:noreply, %{st | started?: true}, {:continue, :upgrade}}
end
def handle_info(:start_upgrade, st) do
{:noreply, st}
end
def handle_info(_msg, st), do: {:noreply, st}
# ...
# ...
defp ensure_stream(st, id) do
if Map.has_key?(st.streams, id) do
st
else
put_stream(st, id, %{buf: <<>>, waiters: [], closed?: false, owner: nil})
end
end
defp put_stream(st, id, stream_state) do
%{st | streams: Map.put(st.streams, id, stream_state)}
end
defp mark_stream_closed(st, id) do
# Notify owner
s = Map.get(st.streams, id, %{buf: <<>>, waiters: [], closed?: true, owner: nil})
if is_pid(s.owner), do: send(s.owner, {:libp2p, :stream_closed, self(), id})
put_stream(st, id, %{s | closed?: true})
end
defp handle_yamux_events(st, events) do
Enum.reduce(events, st, fn
{:stream_open, id}, st_acc ->
st_acc = ensure_stream(st_acc, id)
send(st_acc.swarm, {:inbound_stream, self(), id})
st_acc
{:stream_data, id, data}, st_acc ->
st_acc = ensure_stream(st_acc, id)
s = st_acc.streams[id]
if is_pid(s.owner) do
send(s.owner, {:libp2p, :stream_data, self(), id, data})
st_acc
else
s = %{s | buf: s.buf <> data}
{st_acc, s} = maybe_wake_waiter(st_acc, id, s)
put_stream(st_acc, id, s)
end
{:stream_close, id}, st_acc ->
st_acc = ensure_stream(st_acc, id)
s = %{st_acc.streams[id] | closed?: true}
if is_pid(s.owner), do: send(s.owner, {:libp2p, :stream_closed, self(), id})
{st_acc, s} = maybe_wake_waiter(st_acc, id, s)
put_stream(st_acc, id, s)
{:stream_reset, id}, st_acc ->
st_acc = ensure_stream(st_acc, id)
s = %{st_acc.streams[id] | closed?: true}
if is_pid(s.owner), do: send(s.owner, {:libp2p, :stream_closed, self(), id})
{st_acc, s} = maybe_wake_waiter(st_acc, id, s)
put_stream(st_acc, id, s)
end)
end
defp maybe_wake_waiter(st, _id, s) do
case s.waiters do
[] ->
{st, s}
[from | rest] ->
if s.buf != <<>> do
GenServer.reply(from, {:ok, s.buf})
{st, %{s | buf: <<>>, waiters: rest}}
else
if s.closed? do
GenServer.reply(from, {:error, :closed})
{st, %{s | waiters: rest}}
else
{st, s}
end
end
end
end
end