# `SmolNet`
[🔗](https://github.com/ausimian/smolnet/blob/0.7.1/lib/smol_net.ex#L1)

An OTP-friendly embedded network stack powered by
[`smoltcp`](https://github.com/smoltcp-rs/smoltcp).

Each stack is an independent, supervised native network namespace. Stacks
exchange complete raw IPv4 or IPv6 packets with a caller-provided link process.

## Links

A link carries a stack's packets over an application-defined transport. It
receives outbound batches as `{:smol_stack, link_ref, :egress, packets}` and
hands inbound packets back with `ingress/2`. Each side watches the other. The
stack monitors its link and applies its `:link_down` policy when the link
exits. A link calls `monitor/1` to receive an ordinary `:DOWN` message when
its stack stops, whether through `stop_stack/1` or a crash, so it can exit
instead of running a transport in front of a stack that is gone.

A link cannot refuse a batch once the stack has sent it. A link with a
bounded queue can instead start its stack with `:egress_credit` and grant
more with `grant_egress/3` as it forwards packets. The stack then never sends
more than the link has granted, and holds the rest back in its sockets.

## TCP endpoints and errors

Low-level TCP endpoints use explicit `:socket`-style maps:

    %{family: :inet, addr: {192, 0, 2, 2}, port: 443}
    %{family: :inet6, addr: {0xfd00, 0, 0, 0, 0, 0, 0, 2}, port: 443}

`flowinfo` may be omitted or set to zero. A link-local `fe80::/10` address
requires a positive integer `scope_id` identifying its raw-IP link zone;
global addresses require `scope_id: 0` (the default).

Stable validation and bind errors are `:unsupported_family`,
`:unsupported_socket`, `:invalid_options`, `:invalid_address`,
`:invalid_port`, `:invalid_backlog`, `:invalid_data`, `:invalid_length`,
`:invalid_timeout`, `:message_too_large`,
`:invalid_how`, `:scope_required`, `:invalid_scope`, `:address_in_use`,
`:address_not_available`, and `:ephemeral_ports_exhausted`. Connection and
stream lifecycle errors are
`:network_unreachable`, `:connection_refused`, `:connection_reset`,
`:connection_timeout`, `:already_connected`, `:not_bound`,
`:not_connected`, `:busy`, `:closed`, `:invalid_socket`, and
`:invalid_socket_state`.

An established connection fails with `:connection_timeout` when its peer
answers nothing for 924.6 s while it has data or a FIN outstanding, or
data a zero window holds back, as Linux gives up by default; or when its
keep-alive probes go unanswered (see `setopt/3`). A pending `recv/3` or
`send/3` fails with it at once, and later calls return it too. An idle
connection, with nothing outstanding, never times out without keep-alive.
The timeout is fixed.

# `accept`

```elixir
@spec accept(SmolNet.Socket.t()) :: {:ok, SmolNet.Socket.t()} | {:error, atom()}
```

Accepts a TCP child, waiting indefinitely by default.

# `accept`

```elixir
@spec accept(SmolNet.Socket.t(), :nowait | timeout()) ::
  {:ok, SmolNet.Socket.t()}
  | {:select, :socket.select_info()}
  | {:error, atom()}
```

Accepts with a finite, infinite, or nonblocking timeout.

`:nowait` returns a read-direction `{:select, select_info}` retry hint. Each
accepted child has a fresh stable identity and is independent of the
listener after it is returned.

# `bind`

```elixir
@spec bind(
  SmolNet.Socket.t(),
  SmolNet.Socket.sockaddr_in() | SmolNet.Socket.sockaddr_in6()
) ::
  :ok | {:error, atom()}
```

Binds a TCP or UDP socket to an endpoint of its family.

Port zero allocates from the bounded range 49152..50175. Ports are unique
within one protocol and address family, so TCP and UDP may share a numeric
port. Allocation failure is reported as `:ephemeral_ports_exhausted`.

# `cancel`

```elixir
@spec cancel(SmolNet.Socket.t(), :socket.select_info()) ::
  :ok | :already_sent | :not_found | {:error, :closed | :invalid_socket}
```

Cancels the exact pending nonblocking operation identified by `select_info`.

# `close`

```elixir
@spec close(SmolNet.Socket.t()) :: :ok | {:error, atom()}
```

Closes a socket and permanently invalidates its public handle.

# `connect`

```elixir
@spec connect(
  SmolNet.Socket.t(),
  SmolNet.Socket.sockaddr_in() | SmolNet.Socket.sockaddr_in6()
) ::
  :ok | {:error, atom()}
```

Connects a TCP or UDP socket, waiting indefinitely by default.

# `connect`

```elixir
@spec connect(
  SmolNet.Socket.t(),
  SmolNet.Socket.sockaddr_in() | SmolNet.Socket.sockaddr_in6(),
  :nowait | timeout()
) :: :ok | {:select, :socket.select_info()} | {:error, atom()}
```

Connects a TCP or UDP socket with a finite, infinite, or nonblocking timeout.

TCP `:nowait` returns a one-shot `{:select, select_info}` retry hint. Finite
and infinite waits run entirely in the caller and monitor the owning stack.
UDP connect stores a peer immediately; connected sends must use that peer and
received datagrams from other peers are discarded.

# `getopt`

```elixir
@spec getopt(SmolNet.Socket.t(), {:tcp, :nodelay} | {:socket, :keepalive}) ::
  {:ok, boolean()} | {:error, atom()}
```

Returns a TCP socket option set with `setopt/3`.

`{:tcp, :nodelay}` returns `{:ok, true}` when Nagle's algorithm is
disabled, and `{:socket, :keepalive}` when keep-alive is on; on a
listener, whether the children it accepts will have it so.

# `grant_egress`

```elixir
@spec grant_egress(SmolNet.Stack.Ref.t(), non_neg_integer(), non_neg_integer()) ::
  :ok | {:error, :invalid_egress_credit | :egress_credit_disabled | :closed}
```

Grants a stack started with `:egress_credit` more egress.

Credit counts both packets and bytes, and grants add up. The stack hands the
link a packet only while the credit left covers it in both, and every batch
it sends uses credit up. When credit runs out the stack holds egress back
rather than dropping it: TCP data stays in the socket's send buffer and UDP
datagrams in the socket's transmit ring, so senders see the backpressure
they would see from a slow peer, and nothing is lost. A grant sends whatever
it releases straight away.

A link typically grants back what it has forwarded:

    def handle_info({:smol_stack, :my_link, :egress, packets}, state) do
      Enum.each(packets, &transmit(state, &1))
      :ok = SmolNet.grant_egress(state.stack, length(packets), IO.iodata_length(packets))
      {:noreply, state}
    end

A stack waiting for credit does no work until the next grant, so timers that
need to send, such as TCP retransmissions, also wait for it. Replies to
ingress may still queue inside the stack while it waits, up to its
`:output_packets` limit; once that queue is full, ingress waits for credit
too.

`packets` and `bytes` are non-negative integers, each at most `0xFFFF_FFFF`.
Returns `{:error, :egress_credit_disabled}` for a stack started without
`:egress_credit`. `stack_info/1` reports the credit left as
`native.result.egress_credit`.

# `ingress`

```elixir
@spec ingress(SmolNet.Stack.Ref.t(), binary()) :: :ok | {:error, atom()}
@spec ingress(SmolNet.Stack.Ref.t(), [binary()]) ::
  {:ok, non_neg_integer()} | {:error, atom()}
```

Hands raw IPv4 or IPv6 packets from the stack's link feeder to the stack.

Each stack has one serialized feeder. This call returns after the stack owner
validates and accepts the input, then native processing runs before the stack
accepts another message. A binary preserves the single-packet `:ok` result.
A list is admitted atomically and returns `{:ok, packet_count}`; an empty list
is a no-op. A batch exceeding `:input_packets` or `:bytes_copied` is rejected
with `{:error, :batch_too_large}`. The feeder must bound its own transport
input.

# `listen`

```elixir
@spec listen(SmolNet.Socket.t(), pos_integer()) :: :ok | {:error, atom()}
```

Turns a bound TCP socket into a reusable bounded listener.

Backlog must be in `1..128`. The accepted-child queue is capped at that
value, while the native listening pool is capped at four sockets.

# `monitor`

```elixir
@spec monitor(SmolNet.Stack.Ref.t()) :: reference()
```

Monitors a stack from the calling process.

Returns an ordinary monitor reference. When the stack stops for any reason,
whether through `stop_stack/1`, a crash, or its own `:link_down` policy, the
caller receives `{:DOWN, ref, :process, object, reason}` once the complete
runtime bundle has terminated. Match on `ref`: `object` and `reason` describe
internal processes and are not part of the contract. A stack that has already
stopped produces the message immediately, as `Process.monitor/1` does for a
dead process. Remove the monitor with `Process.demonitor/2`.

A link calls this so that it can exit when its stack stops, letting its
supervisor rebuild both:

    monitor = SmolNet.monitor(stack)

    receive do
      {:DOWN, ^monitor, :process, _object, _reason} -> exit(:stack_down)
    end

# `open`

```elixir
@spec open(:inet6 | :inet, :stream | :dgram, :tcp | :udp, keyword()) ::
  {:ok, SmolNet.Socket.t()} | {:error, atom()}
```

Opens a bounded low-level TCP stream or UDP datagram socket on `stack`.

Family and kind are explicit and immutable. TCP and UDP support both IPv4
and IPv6. TCP accepts socket-style `:rcvbuf` and `:sndbuf` options from
1 KiB through 1 MiB; both default to 256 KiB and remain fixed after open.

# `peername`

```elixir
@spec peername(SmolNet.Socket.t()) ::
  {:ok, SmolNet.Socket.sockaddr_in() | SmolNet.Socket.sockaddr_in6()}
  | {:error, atom()}
```

Returns the peer endpoint for a connected TCP or UDP socket.

# `recv`

```elixir
@spec recv(SmolNet.Socket.t(), non_neg_integer()) ::
  {:ok, binary()} | {:error, atom() | {atom(), binary()}}
```

Receives TCP stream data, waiting indefinitely by default.

# `recv`

```elixir
@spec recv(SmolNet.Socket.t(), non_neg_integer(), :nowait | timeout()) ::
  {:ok, binary()}
  | {:select, :socket.select_info()}
  | {:select, {:socket.select_info(), binary()}}
  | {:error, atom() | {atom(), binary()}}
```

Receives TCP stream data with a finite, infinite, or nonblocking timeout.

Positive lengths are exact for synchronous calls unless peer EOF returns the
final shorter buffered value. Length zero returns one bounded currently
available chunk. Nonblocking partial exact reads return
`{:select, {select_info, partial_binary}}`.

# `recvfrom`

```elixir
@spec recvfrom(SmolNet.Socket.t(), non_neg_integer()) ::
  {:ok, SmolNet.Socket.datagram()} | {:error, atom()}
```

Receives one UDP datagram, waiting indefinitely by default.

# `recvfrom`

```elixir
@spec recvfrom(SmolNet.Socket.t(), non_neg_integer(), :nowait | timeout()) ::
  {:ok, SmolNet.Socket.datagram()}
  | {:select, :socket.select_info()}
  | {:error, atom()}
```

Receives one UDP datagram with a finite, infinite, or nonblocking timeout.

Length zero returns the complete datagram. A positive length truncates a
larger datagram, discards its remainder, and sets `truncated: true`. Source
and actual local-destination endpoints are always returned.

# `send`

```elixir
@spec send(SmolNet.Socket.t(), iodata()) ::
  :ok | {:error, atom() | {atom(), binary()}}
```

Sends a complete TCP byte stream, waiting indefinitely by default.

# `send`

```elixir
@spec send(SmolNet.Socket.t(), iodata(), :nowait | timeout()) ::
  :ok
  | {:select, {:socket.select_info(), binary()}}
  | {:error, atom() | {atom(), binary()}}
```

Sends TCP stream data with a finite, infinite, or nonblocking timeout.

The native stack copies at most one bounded chunk. A nonblocking partial
result is `{:select, {select_info, unsent_binary}}`; the caller retains and
retries that remainder. A timed synchronous send that made progress returns
`{:error, {:timeout, unsent_binary}}`.

# `sendto`

```elixir
@spec sendto(
  SmolNet.Socket.t(),
  iodata(),
  SmolNet.Socket.sockaddr_in() | SmolNet.Socket.sockaddr_in6()
) ::
  :ok | {:error, atom()}
```

Sends one complete UDP datagram, waiting indefinitely by default.

# `sendto`

```elixir
@spec sendto(
  SmolNet.Socket.t(),
  iodata(),
  SmolNet.Socket.sockaddr_in() | SmolNet.Socket.sockaddr_in6(),
  :nowait | timeout()
) :: :ok | {:select, :socket.select_info()} | {:error, atom()}
```

Sends one complete UDP datagram with a finite, infinite, or nonblocking timeout.

The datagram is either accepted in full or not accepted. `:nowait` returns a
write-direction select hint when the bounded native transmit ring is full.

# `setopt`

```elixir
@spec setopt(SmolNet.Socket.t(), {:tcp, :nodelay} | {:socket, :keepalive}, boolean()) ::
  :ok | {:error, atom()}
```

Sets a socket option on a TCP socket.

The options are, as in `:socket.setopt/3`:

  * `{:tcp, :nodelay}`: `true` disables Nagle's algorithm, so a small
    write is sent while an earlier one is still unacknowledged, and
    `false`, the default, enables it again. Disabling it sends a segment
    it was holding back at once.
  * `{:socket, :keepalive}`: `true` sends keep-alive probes on a
    connection that has received nothing for 2 hours, one every 75 s, and
    fails it with `:connection_timeout` once 9 have gone unanswered, as
    Linux does by default; `false`, the default, sends none. The timing is
    fixed.

Set them before connecting, or at any time after. On a listener they apply
to the children `accept/2` returns from then on, which each take the
listener's settings as they are returned; changing one on a child
afterwards does not affect the listener.

A UDP socket returns `{:error, :invalid_socket_state}`, and any other
option or value `{:error, :invalid_options}`.

# `shutdown`

```elixir
@spec shutdown(SmolNet.Socket.t(), :read | :write | :read_write) ::
  :ok | {:error, atom()}
```

Shuts down the read half, write half, or both halves of a TCP socket.

Write shutdown drives a FIN and rejects later sends while preserving allowed
reads. Read shutdown rejects later receives.

# `sockname`

```elixir
@spec sockname(SmolNet.Socket.t()) ::
  {:ok, SmolNet.Socket.sockaddr_in() | SmolNet.Socket.sockaddr_in6()}
  | {:error, atom()}
```

Returns the bound endpoint for a TCP or UDP socket.

# `stack_info`

```elixir
@spec stack_info(SmolNet.Stack.Ref.t()) :: {:ok, map()} | {:error, :closed}
```

Returns ingress, link, timer, and native stack metrics.

# `start_stack`

```elixir
@spec start_stack(keyword()) :: {:ok, SmolNet.Stack.Ref.t()} | {:error, term()}
```

Starts a raw-IP network stack.

The returned reference is opaque and owns the complete temporary runtime
bundle. Configure packet output with `egress: {pid, link_ref}`. Each bounded
native output batch is delivered as
`{:smol_stack, link_ref, :egress, [packet, ...]}`.

IPv4 addresses use `{{a, b, c, d}, prefix_length}` and IPv6 addresses use
`{{s1, s2, s3, s4, s5, s6, s7, s8}, prefix_length}`. Routes use
`{destination, prefix_length, gateway}`; destination and gateway must have
the same family. One stack may contain both families.

Native work limits can be reduced with the `:limits` option. It accepts a map
containing any of `:bytes_copied`, `:input_packets`, `:output_packets`,
`:ready_events`, and `:maintenance_work`; unspecified values retain their
safe defaults. `:input_packets` defaults to one and may be raised to 32 for
bounded batched ingress. `:bytes_copied` bounds, separately, the bytes one
native call copies for its operation (a send, a receive, or an ingress
batch) and the bytes of the packets it hands the link, so a send of the
whole limit still emits its first segments in the same call.

`:sockets` in the same map is the most sockets the stack holds at once. It
defaults to 64 and may be raised to 512. It counts native backing sockets:
one per TCP or UDP socket, one per member of a TCP listener's accept pool
(up to 4), and one per configured address for a UDP socket bound to a
wildcard address. A TCP socket that closes first keeps its slot until
TIME-WAIT ends, about 10 s after the close, so a stack whose sockets close
first sustains about `sockets / 10` new connections per second. An open
beyond the limit returns `{:error, :system_limit}`. Each slot holds its
buffers from open until the slot is freed: a TCP socket's receive and send
buffers (256 KiB each by default, up to 1 MiB each) and 32 KiB for a UDP
socket. At the default buffer sizes, 64 TCP sockets hold about 32 MiB.
Whatever the limit, a stack's socket buffers total at most 128 MiB, what
64 TCP sockets with the largest buffers hold; an open that would pass that
also returns `{:error, :system_limit}`. So at the default sizes a stack
holds at most 256 TCP sockets, and 512 need buffers averaging at most
128 KiB each way. `stack_info/1` reports the
slots in use as `native.result.native_socket_count`, the limit as
`native.result.native_socket_capacity`, the closed TCP sockets still
holding one as `native.result.closing_tcp_socket_count`, and buffer bytes
against their cap as `native.result.socket_buffer_bytes` and
`native.result.socket_buffer_capacity`.

`:egress_credit` limits how much egress the link must accept. It defaults to
`:infinity`, which sends every batch as soon as it is ready. A
`{packets, bytes}` tuple of non-negative integers, each at most
`0xFFFF_FFFF`, is the credit the stack starts with; see `grant_egress/3`.
An invalid value is rejected with `:invalid_egress_credit`.

`SmolNet.Loopback.start_link/1` starts a stack whose egress is a link back
into itself, returns both references, and needs no external transport.

# `stop_stack`

```elixir
@spec stop_stack(SmolNet.Stack.Ref.t()) :: :ok | {:error, :closed}
```

Stops a stack and its complete runtime bundle.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
