All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

0.7.1 - 2026-09-28

Fixed

  • The README's installation instructions now depend on {:smolnet, "~> 0.7"}. They named ~> 0.2, which also allows versions older than 0.7.0.

0.7.0 - 2026-09-28

SmolNet's TCP now behaves much more like Linux's on real networks, with congestion control, modern loss recovery and path MTU discovery, and :ssl runs over SmolNet directly. Several TCP defaults change; see Changed, especially the larger socket buffers, which lower how many TCP sockets a stack holds by default.

Added

  • :ssl over SmolNet: pass SmolNet.Inet.Tcp or SmolNet.Inet6.Tcp as the cb_info transport, as a client or a server, including :ssl.listen/2. Connect to an address and pass the host name as server_name_indication; :ssl.connect/4 with a host name is not supported yet. SmolNet sockets also work with :inet.monitor/1. See the new :ssl guide, ssl.md.
  • :gen_tcp's nodelay option, to turn off Nagle's algorithm, and SmolNet.setopt(socket, {:tcp, :nodelay}, true) in the low-level API. Before, SmolNet rejected it with :einval.
  • :gen_tcp's keepalive option, with Linux's default timing: a probe after 2 hours with nothing received, then one every 75 s, and :etimedout after 9 go unanswered. The low-level API has {:socket, :keepalive}. Before, SmolNet rejected it with :einval.
  • A path MTU guide, path_mtu.md, on configuring :mtu and MSS clamping for paths with a narrower hop.

Changed

  • TCP buffers now default to 256 KiB each, up from 64 KiB, so one stream over a 100 ms round trip can reach about 20 Mbit/s instead of 5. The 128 MiB per-stack buffer cap now fits 256 TCP sockets at these defaults; beyond that, opens fail with {:error, :system_limit} unless sockets ask for smaller buffers. :gen_tcp's buffer stays at 64 KiB.
  • TCP now uses CUBIC congestion control with a ten-segment initial window, as Linux does, instead of none. Through a 20 Mbit/s bottleneck a sender went from about 1.5 Mbit/s to about 18.
  • The minimum TCP retransmission timeout is now 200 ms, as on Linux, instead of 1 s. This departs from RFC 6298's suggested 1 s.
  • TCP loss recovery now matches Linux's: SACK-based recovery of several segments per round trip (RFC 6675), tail loss probes and RACK loss detection (RFC 8985), limited transmit (RFC 3042), and window growth by the bytes each ACK acknowledges. Under bursty loss, and when uploading to internet servers, transfers are several times faster.

Fixed

  • A TCP connection whose peer vanishes with data outstanding now fails with :etimedout after 924.6 s, Linux's default, instead of retransmitting forever (RFC 5482's user timeout). Idle connections are never timed out. The low-level API reports :connection_timeout.
  • A TCP send across a hop narrower than :mtu no longer stalls forever. A connection now does path MTU discovery (RFC 1191, RFC 8201): a validated ICMP "Fragmentation Needed" or "Packet Too Big" lowers its segment size, and it resends at the new size. Paths that filter these errors still need :mtu or MSS clamping; see path_mtu.md. stack_info reports new icmp_too_big_* and path_mtu_reductions counters.
  • A TCP write longer than one segment no longer waits a round trip for its last partial segment, which slowed some TLS 1.3 handshakes. Nagle's algorithm now holds a partial segment only while another is unacknowledged, as on Linux.
  • A lost segment to a Linux peer is now fast-retransmitted instead of waiting for the retransmission timer.
  • :gen_tcp and :gen_udp calls pending when a stack fails, or when its link dies under link_down: :stop, now return {:error, :enetdown}, and an active socket's owner gets tcp_error or udp_error with :enetdown. Before, they could get :closed, as after SmolNet.stop_stack/1, which still gives :closed. A partly sent write returns {:error, {:enetdown, rest}}.
  • Idle sockets no longer hold on to the memory of their last transfer. Socket processes, and SmolNet.Loopback, now hibernate after 5 s idle.

Internal

  • A real-network integration harness under integration/, not part of the Hex package, with smoke, TLS, path MTU, idle-connection, chaos, crawl and netem-matrix scenarios. They run on every pull request and on nightly and weekly schedules; see integration/README.md.

0.6.1 - 2026-09-27

Fixed

  • A TCP stream could stop for good after losing a single segment. This happened when the link ran out of egress credit just as the stack tried to resend the lost segment, and all the data the window allowed was already in flight. The resend is now retried once credit is granted.
  • A stack with no egress credit and closing TCP sockets could poll itself without end, about a million times a second, and never finish its native call. It kept any SmolNet.ingress/2 call waiting for that time, so a link that feeds ingress and grants credit from one process never sent the grant that would have ended it, and every stream on the stack stopped. The stack now waits for the grant instead.

0.6.0 - 2026-09-26

Changed

  • IPv4 header checksums are checked about seven times faster. This cuts the work a stack does per IPv4 packet by about 40% and makes bulk TCP over IPv4 about 10% faster on a loopback link. Packets with a bad checksum are still rejected with {:error, :invalid_packet}.
  • Received TCP data is copied once instead of two or three times. A 64 KiB :gen_tcp.recv/2 of queued data is about three times faster, and bulk transfers that read large chunks are up to about 20% faster.
  • SmolNet.ingress/2 no longer copies every packet in a batch. Only the packets left over when a call runs out of work budget are copied. This saves about 40 ns per 1,280-byte packet, about a third of the cost of a batch of 32.
  • Received UDP datagrams are copied in one block instead of byte by byte. Receiving an 8 KiB datagram is about three times faster, and a 1,200-byte datagram about 30% faster.
  • Outgoing packets are no longer zero-filled in full; only their first 64 bytes are cleared. Sending a 9,000-byte UDP datagram takes about 4% less time.
  • A send that uses the whole bytes_copied limit, such as a 64 KiB :gen_tcp.send/2, now sends its first segments straight away instead of waiting for the stack's next poll. On an idle connection at MTU 9,000, the first segment leaves about 10% sooner.
  • bytes_copied now limits what a native call copies in and what it sends out separately, instead of sharing one budget between them. max_bytes_copied in SmolNet.stack_info/1 reports the larger of the two.
  • A stack keeps its poll timer when the deadline doesn't change, instead of cancelling and restarting it. Those calls are about 20% faster, and a bulk transfer starts about a quarter fewer timers.

Fixed

  • In active mode, a :gen_tcp socket could fail to deliver the last bytes it received, leaving its owner waiting forever for the end of a transfer. This happened when those bytes were read at the end of a batch of reads and no more data followed, including when :gen_tcp.controlling_process/2 had just moved the socket to a new owner.

0.5.0 - 2026-09-25

Added

  • A link can now bound the egress a stack hands it. Start the stack with egress_credit: {packets, bytes} and grant more with SmolNet.grant_egress/3 as the link forwards packets. The stack never sends a batch the credit does not cover. What it cannot send waits in the sockets instead: TCP data in the send buffer and UDP datagrams in the transmit ring, so senders slow down as they would for a slow peer, and a link with a bounded queue no longer has to drop TCP data and wait out retransmissions. A stack waiting for credit does no work until the next grant. Stacks started without the option behave as before, and SmolNet.Loopback grants back what it forwards when given one.
  • A stack can now hold more than 64 sockets. Start it with limits: %{sockets: n}, up to 512, to raise the limit; the default stays
    1. The limit counts the same native sockets as before: one per socket, listener pool member, and wildcard-UDP address, including TCP sockets still in TIME-WAIT. A stack whose sockets close first sustains about n / 10 new connections per second. Each slot keeps its socket's buffers until it is freed, 128 KiB for a TCP socket at the default buffer sizes, so 512 TCP sockets can hold about 64 MiB. Whatever the limit, a stack's socket buffers total at most 128 MiB, as much as 64 sockets with the largest TCP buffers held before; an open past that returns {:error, :system_limit}. SmolNet.stack_info/1 reports the total as socket_buffer_bytes.

Changed

  • The ready_events limit no longer caps how many sockets and blocked operations a stack holds. It used to admit fewer sockets than its value and at most that many waiting operations, so lowering it could make opens, and :nowait sends, receives, accepts and connects, fail with :system_limit. It now bounds only how many readiness events one native call delivers, and sockets governs capacity.

Fixed

  • A stack under sustained readiness load could leave a blocked operation on a socket opened later than most others waiting indefinitely. When more sockets became ready in one call than the stack could queue, each overflow restarted the scan for ready sockets from the beginning, so a scan that overflowed on every call never reached the later sockets. A raised sockets limit made this easier to hit.

0.4.2 - 2026-09-25

Fixed

  • A TCP stream that loses several segments from one window now resends just the lost segments, one round trip apart, instead of stalling for smoltcp's 1 s minimum retransmission timeout and then resending everything after the first gap. Over loopback, a 4 MiB transfer through a link that drops bursts of segments fell from 10–25 s to about 0.2 s. A loss at the very end of a transfer can still wait for the 1 s timeout.
  • A TCP socket that closes first now frees its socket slot as soon as TIME-WAIT ends, about 10 s after the connection closes. It used to hold the slot until the 30 s close deadline, so a stack whose sockets closed first could sustain only about 2 new connections per second before opens failed with :system_limit.

0.4.1 - 2026-09-25

Changed

  • The advertised TCP receive window now follows the socket's receive buffer (rcvbuf/recbuf) instead of being held to a single segment, so throughput over links with real latency is no longer capped at about one segment per round trip. Over a 50 ms round trip, a default 64 KiB buffer now reaches about 1 MB/s instead of about 47 KB/s. An embedder that relied on the smaller window can lower recbuf to get it back.

0.4.0 - 2026-09-24

Added

  • SmolNet.monitor/1 returns an ordinary monitor on a stack, so a link receives a standard :DOWN message when its stack stops through SmolNet.stop_stack/1 or a crash, and can exit instead of running its transport in front of a stack that is gone.

0.3.0 - 2026-09-24

Added

  • SmolNet.ingress/2 accepts bounded packet lists, allowing burst-oriented links to cross the BEAM/native boundary and drive the stack once per batch.

0.2.1 - 2026-09-19

Fixed

  • Documentation guide links now resolve in ExDoc's Markdown output and the published package by keeping the guides at the package root.

0.2.0 - 2026-09-19

Fixed

  • The two affected test suites now distinguish liveness waits from timing assertions. Shared CI runners give operations that must eventually complete extra headroom without relaxing tests whose timeout is the behaviour under test.

  • Fixed keep-alive probes and other challenge ACKs never being sent. The stack passed the raw BEAM monotonic clock, which is negative, to smoltcp as its instant; smoltcp's challenge-ACK rate limiter compares the instant against a timer that starts at zero, so the gate never opened and a peer's TCP keep-alive probe went unanswered — iOS, for one, resets an idle connection after three. The clock now counts milliseconds since the VM started.

  • Raw-mode :gen_tcp streams are no longer bounded by packet_size and the receive buffer. A passive recv/3 that names its length now accumulates to that length however large, and send/2 of any size is accepted and written in the bounded pieces the adapter already used. Both previously returned :emsgsize and closed the socket, which :gen_tcp never does for a raw stream. Chunk reads (recv(socket, 0)), active delivery, and framed packet modes keep their bound.

  • Fixed a :gen_tcp receive that could fail with :busy against its own socket. When a passive exact-length read got part of its data together with a select for the rest, the adapter immediately asked the stack for the remainder, which the stack rejected because that read's waiter was already armed. The caller's read failed :busy, and the stray waiter made every later read fail the same way until data happened to arrive. The adapter now waits for the select it already holds.

  • Fixed a bug where a call to SmolNet.ingress/2 could hang. If the stack was still busy with earlier work when a packet arrived, it held the packet and planned to process it after its next poll. A socket call made at the same time could cancel that poll. The stack then never processed the held packet, and the link process that sent it waited forever, or until an unrelated timer happened to fire. The stack now processes the held packet even when the poll it was waiting for has been cancelled.

Added

  • Raw-IP links now receive bounded egress batches. Each native output envelope is delivered as one {:smol_stack, link_ref, :egress, packets} message, allowing links to amortize mailbox handling and transport writes while preserving packet order.

  • SmolNet.Loopback, a link process that feeds every packet its stack emits back into that same stack. One stack then reaches its own addresses with no peer, no external transport, and no privileges, which makes a runnable example or test out of what previously needed two stacks and a relay. The link takes the SmolNet.start_stack/1 options apart from :egress, which it supplies, owns the stack it loops, and is stopped by SmolNet.stop_stack/1. examples/loopback.exs runs a complete gen_tcp request and response over one stack.

Changed

  • SmolNet.Loopback.start_link/1 now returns {:ok, link, stack}, exposing the newly created stack without a follow-up SmolNet.Loopback.stack/1 call. The stack is also returned as child information when a loopback link is started under a supervisor.

  • The :gen_tcp and :gen_udp callback modules now follow their OTP address family names. Use SmolNet.Inet.Tcp and SmolNet.Inet.Udp with :inet, or SmolNet.Inet6.Tcp and SmolNet.Inet6.Udp with :inet6. These replace the former SmolNet.InetBackend.Tcp4, SmolNet.InetBackend.Udp4, SmolNet.InetBackend.Tcp, and SmolNet.InetBackend.Udp names, respectively.

  • The README is now a concise introduction to SmolNet's motivation, raw-IP link boundary, and available interfaces. Detailed :gen_tcp, :gen_udp, and low-level socket usage now lives in dedicated ExDoc guides, while development and native-build material has moved to the repository-only maintainer guide.

  • TCP receive and send buffers now default to 64 KiB and can be sized independently with socket-style rcvbuf/sndbuf at low-level open or inet recbuf/sndbuf when connecting and listening. Sizes from 1 KiB through 1 MiB are supported, accepted sockets inherit listener sizes, and stack diagnostics report each socket's values.

  • The NIF now charges enif_consume_timeslice incrementally at work-loop chunk boundaries instead of once at the end of a call, and stops early when a charge reports the caller's reduction slice as spent. A stack owner that has already used most of its slice before re-entering the NIF receives a shorter native slice, so other processes on the scheduler are not starved. The 750 microsecond work deadline remains a hard ceiling, at least one chunk always runs per call, and retained work still continues through the existing more protocol.

0.1.2 - 2026-09-17

Fixed

  • Corrected release validation for production precompiled NIFs and their platform system-library dependencies.

0.1.1 - 2026-09-17

Initial release

SmolNet embeds the Rust smoltcp network stack in an OTP-friendly Elixir library with explicitly bounded native work.

Added

  • Independent, supervised IPv4 and IPv6 raw-IP stacks with configurable addresses, routes, MTUs, link behavior, and runtime telemetry.
  • Low-level TCP and UDP sockets plus :gen_tcp and :gen_udp compatible adapters for clients, listeners, datagrams, active and passive delivery, and controlling-process ownership.
  • Scheduler-aware NIF execution with bounded queues, buffers, readiness, maintenance, shutdown, and packet-processing work.
  • Checksum-pinned precompiled NIFs for Linux x86_64, Linux AArch64, and Apple Silicon macOS, while repository checkouts continue to build from Rust source.