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
:sslover SmolNet: passSmolNet.Inet.TcporSmolNet.Inet6.Tcpas thecb_infotransport, as a client or a server, including:ssl.listen/2. Connect to an address and pass the host name asserver_name_indication;:ssl.connect/4with a host name is not supported yet. SmolNet sockets also work with:inet.monitor/1. See the new:sslguide,ssl.md.:gen_tcp'snodelayoption, to turn off Nagle's algorithm, andSmolNet.setopt(socket, {:tcp, :nodelay}, true)in the low-level API. Before, SmolNet rejected it with:einval.:gen_tcp'skeepaliveoption, with Linux's default timing: a probe after 2 hours with nothing received, then one every 75 s, and:etimedoutafter 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:mtuand 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'sbufferstays 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
:etimedoutafter 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
:mtuno 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:mtuor MSS clamping; seepath_mtu.md.stack_inforeports newicmp_too_big_*andpath_mtu_reductionscounters. - 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_tcpand:gen_udpcalls pending when a stack fails, or when its link dies underlink_down: :stop, now return{:error, :enetdown}, and an active socket's owner getstcp_errororudp_errorwith:enetdown. Before, they could get:closed, as afterSmolNet.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; seeintegration/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/2call 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/2of queued data is about three times faster, and bulk transfers that read large chunks are up to about 20% faster. SmolNet.ingress/2no 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_copiedlimit, 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_copiednow limits what a native call copies in and what it sends out separately, instead of sharing one budget between them.max_bytes_copiedinSmolNet.stack_info/1reports 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_tcpsocket 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/2had 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 withSmolNet.grant_egress/3as 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, andSmolNet.Loopbackgrants 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- 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 / 10new 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/1reports the total assocket_buffer_bytes.
- 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
Changed
- The
ready_eventslimit 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:nowaitsends, receives, accepts and connects, fail with:system_limit. It now bounds only how many readiness events one native call delivers, andsocketsgoverns 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
socketslimit 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 lowerrecbufto get it back.
0.4.0 - 2026-09-24
Added
SmolNet.monitor/1returns an ordinary monitor on a stack, so a link receives a standard:DOWNmessage when its stack stops throughSmolNet.stop_stack/1or 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/2accepts 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_tcpstreams are no longer bounded bypacket_sizeand the receive buffer. A passiverecv/3that names its length now accumulates to that length however large, andsend/2of any size is accepted and written in the bounded pieces the adapter already used. Both previously returned:emsgsizeand closed the socket, which:gen_tcpnever does for a raw stream. Chunk reads (recv(socket, 0)), active delivery, and framed packet modes keep their bound.Fixed a
:gen_tcpreceive that could fail with:busyagainst 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/2could 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 theSmolNet.start_stack/1options apart from:egress, which it supplies, owns the stack it loops, and is stopped bySmolNet.stop_stack/1.examples/loopback.exsruns a completegen_tcprequest and response over one stack.
Changed
SmolNet.Loopback.start_link/1now returns{:ok, link, stack}, exposing the newly created stack without a follow-upSmolNet.Loopback.stack/1call. The stack is also returned as child information when a loopback link is started under a supervisor.The
:gen_tcpand:gen_udpcallback modules now follow their OTP address family names. UseSmolNet.Inet.TcpandSmolNet.Inet.Udpwith:inet, orSmolNet.Inet6.TcpandSmolNet.Inet6.Udpwith:inet6. These replace the formerSmolNet.InetBackend.Tcp4,SmolNet.InetBackend.Udp4,SmolNet.InetBackend.Tcp, andSmolNet.InetBackend.Udpnames, 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/sndbufat low-level open or inetrecbuf/sndbufwhen 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_timesliceincrementally 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 existingmoreprotocol.
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_tcpand:gen_udpcompatible 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.