Files
Sergio Ammirata d8d9034777 fix(test): build NAT/multipath rebind tests under MSVC
The issue #188 NAT/socket-rebind and multipath cname integration
tests included <pthread.h> and initialised their tracker mutex with
PTHREAD_MUTEX_INITIALIZER.  MSVC has no system pthread.h, and the
Windows pthread-shim maps pthread_mutex_t to a CRITICAL_SECTION,
which cannot be initialised statically, so the whole librist VS
solution failed to build (only these tests; the library and tools
were fine).

Follow the convention already used by test_send_receive.c and
test_reflector.c: include "pthread-shim.h" (directly, or via
rist-private.h), initialise the tracker mutex at runtime with
pthread_mutex_init(), and declare the feeder threads with the
PTHREAD_START_FUNC() macro so they match the shim's __stdcall/LPVOID
signature.  POSIX builds are unchanged; the suite now also compiles
under MSVC.
2026-06-21 21:37:52 -04:00

1408 lines
68 KiB
Plaintext

librist - Reliable Internet Stream Transport
============================================
A library implementing the Video Services Forum (VSF) Technical
Recommendations TR-06-1 (Simple Profile), TR-06-2 (Main Profile),
and TR-06-3 (Advanced Profile).
Changes for 0.2.18:
---------------------------------
ABI/API:
- ABI version 15:0:11 (soversion 4, binary-compatible with 0.2.15,
0.2.16, and 0.2.17)
- API version 4.12.0 (new features, backwards-compatible)
- RIST_PEER_CONFIG_VERSION bumped from 2 to 3 (one new field:
srp_compat_legacy). Existing zero-initialised peer configs
continue to work; the new field defaults to 0 (spec-compliant).
- RIST_PEER_CONFIG_VERSION bumped from 3 to 4 (two new fields:
profile + profile_set, populated by the ?profile= URL parameter).
rist_peer_config_defaults_set() initialises both to safe defaults
(profile = RIST_DEFAULT_PROFILE, profile_set = 0). Existing
zero-initialised configs keep their pre-bump behaviour.
- New rist_peer_config_defaults_set_versioned(cfg, version) initialises
only the fields defined at or below the given RIST_PEER_CONFIG_VERSION.
rist_peer_config_defaults_set() is now a static inline wrapper (peer.h)
that forwards the caller's compiled RIST_PEER_CONFIG_VERSION, so a
recompiled caller initialises exactly the fields its struct contains.
The exported rist_peer_config_defaults_set() symbol is retained for
binaries linked against an earlier library; it assumes version 0 and
sets only the baseline fields present in every struct revision. Such
binaries must zero-initialise their config before the call: fields
added in version 1+ (split_mode, merge_mode, profile, profile_set)
are left untouched.
- RIST_PEER_CONFIG_VERSION bumped from 4 to 5 (two new fields:
recovery_priority, populated by the ?recovery-priority= URL
parameter, and recovery_depth, populated by ?recovery-depth=).
rist_peer_config_defaults_set_versioned() initialises them for
callers compiled against version 5 or later (recovery_priority = 0,
recovery_depth = RIST_RECOVERY_DEPTH_DEFAULT); zero-initialised
configs and callers built against an earlier version keep the legacy
NACK-routing behaviour and the default recovery ring.
- New public API rist_recovery_depth_set(ctx, depth) and the
RIST_RECOVERY_DEPTH_MIN/DEFAULT/MAX macros (peer.h) size the
Advanced-profile retransmission ring. depth is a base-2 exponent:
the ring holds 65536 << depth packets (default 3 = 8x = the prior
fixed size, range 0..16). Must be called before rist_start().
- New public macro RIST_PEER_WEIGHT_DUPLICATE (0) names the
rist_peer_config.weight sentinel that selects packet duplication
instead of weighted load-balancing. Value unchanged.
- RIST_STATS_VERSION bumped from 2 to 3 and the JSON stats
schema_version from 3 to 4 (additive): rist_stats_sender_peer gains
profile + advanced_active, rist_stats_receiver_flow gains profile +
seq_bits + advanced_active. Consumers reading only earlier fields are
unaffected.
Features:
- Per-peer retransmission routing: the new ?recovery-priority=<n>
peer URL parameter (rist_peer_config.recovery_priority) lets a
receiver direct NACKs to a chosen peer when a flow is carried by
more than one RTCP-capable peer. Each NACK group is sent to the
eligible peer with the highest recovery_priority, ties broken by
lowest RTT. The default of 0 on every peer preserves the prior
lowest-RTT selection. Useful when the lowest-RTT peer cannot
answer NACKs (e.g. a duplicate/relay feed with no retransmit
buffer) while a higher-RTT peer holds the retransmission buffer.
- Tunable Advanced-profile recovery depth: the recovery rings are now
heap-allocated and profile-sized, so Simple/Main flows allocate only
their 16-bit ring (65536 packets) instead of the Advanced footprint.
The Advanced ring size is selectable via ?recovery-depth=N (or
rist_recovery_depth_set()), where N is a base-2 exponent and the ring
holds 65536 << N packets. The default of 3 (524288 packets) matches
the previous fixed size; higher values trade RAM for a deeper NACK
window. Simple/Main ignore the knob. A config-time warning is
emitted when a peer's bitrate and buffer settings would exceed the
recovery ring's addressable window.
- Advanced/Main profile interoperability (TR-06-3 Section 9): an
Advanced sender starts in Main framing and upgrades a peer to
Advanced framing only after that peer advertises Advanced capability,
so a Main-only receiver can still decode the stream. An Advanced
receiver accepts a Main-framed source and follows a mid-stream
Main->Advanced upgrade. Matched Advanced and matched Main pairs are
unchanged.
- The default RIST profile is now Advanced (RIST_DEFAULT_PROFILE, and
the ristsender/ristreceiver/YAML defaults that follow it), changed
from Main. Thanks to the interop above, a default Advanced endpoint
negotiates Advanced framing with an Advanced peer and falls back to
Main framing with a Main-only peer, so existing Main deployments keep
working. Pass -p 1 (or profile: 1 in YAML) to force Main as before.
- risttunnel now defaults to RIST_DEFAULT_PROFILE like the other tools,
instead of remaining hardcoded to Main, completing the default-profile
sweep.
- Advanced-profile visibility in statistics: the binary stats API and the
JSON stats now report each flow's configured profile and, on the
receiver, whether Advanced framing is actually active and the sequence
width in use. The Prometheus exporter adds rist_client_flow_info /
rist_sender_peer_info series carrying these labels, so a dashboard can
tell at a glance when it is monitoring an Advanced stream.
Bug Fixes:
- Advanced profile: a sequence gap larger than 65535 is no longer
truncated to 16 bits, so a real >64k loss on a 32-bit flow is recovered
instead of being mis-detected as a tiny gap.
- Advanced profile: receiver_mark_missing() now masks the sequence into the
retransmission ring index. A 32-bit sequence eventually exceeds the ring
size; the old raw index ran off the array and could crash the receiver on
a long-running Advanced flow.
- The expected-next-sequence wrap mask no longer forces the value even (it
masked with UINT16_MAX-1), which mis-computed every odd successor on
16-bit flows.
- Advanced byte accounting now counts delivered payload only, matching the
Simple/Main convention, so received-byte and bitrate stats are consistent
across profiles.
- Duplicate detection compares the sequence number rather than the source
timestamp, fixing false duplicate decisions on Advanced arrival-stamped
packets.
- Merge-mode pairing computes the successor sequence in a 32-bit-safe way,
so paired packets are matched correctly on Advanced flows.
- Advanced Profile (TR-06-3) RTT Echo Responses inflated the measured
round-trip time by a constant factor. The sender will not retransmit
a packet again within one RTT of the previous attempt, so the inflated
value made it ignore the repeat NACKs needed when a retransmission is
itself lost, leaving packets unrecovered under sustained loss. The
Advanced echo now uses calculate_rtt_delay(), as the Simple and Main
profiles do.
- Fixed a receiver abort when a retransmitted or gap-filled packet's
arrival time is interpolated. With peers whose clocks are not
synchronised the estimate could fall outside its neighbouring queue
slots, tripping an assert in debug builds or misordering the queue
otherwise. Interpolation is now bounds-checked, rounded without
overshoot, wrap/reorder-aware, and clamped to the neighbour interval.
- NAT source-port rebind recovery (issue #188): when a calling
receiver's external source port changes (NAT mapping rebind, DHCP
renewal), the listening sender no longer treats it as a brand-new
caller that leaves the old peer record orphaned until session
timeout. Listener-side re-association requires an authenticated
per-peer SRP session: once a new child completes SRP authentication
and learns its cname from an RTCP SDES, its source tuple is migrated
into the matching silent sibling. This path is SRP-only because the
cname alone is not a per-peer secret under a shared PSK. Plaintext
and shared-PSK callers recover on the caller side instead: a
caller-mode receiver on a MAIN+ profile with no pinned local port
rebinds its UDP socket to a fresh ephemeral port on session timeout
and restarts the handshake instead of giving up.
- ?profile=0|1|2 in a peer URL (0=Simple, 1=Main, 2=Advanced) now
overrides the wire profile of the context that owns the peer.
rist_parse_address2 parses the value into rist_peer_config; on the
first matching peer added before rist_start(), rist_peer_create
mutates the context profile and lazily initialises the Advanced
profile state when upgrading to Advanced. Subsequent peers must
request the same profile (or omit the parameter); rist_peer_create
returns -1 with a WARN once the context profile is locked (after
rist_start() or a prior peer fixed it). Any consumer that passes
URLs through rist_parse_address2 + rist_peer_create now gets the
URL-driven profile selection with no further integration work.
- Fixed a use-after-free in the global logger when an application frees
its rist_logging_settings, either directly via
rist_logging_settings_free2() or by destroying a context that embeds
them (as FFmpeg does), while the global logger still references that
callback and argument. The global logging settings are now cleared
when the matching settings are freed, so logs emitted afterwards (for
example from background threads on a rapid close/reopen) no longer
call into freed memory.
- rist_peer_config_defaults_set() no longer writes past the end of a
peer config supplied by a binary compiled against an older
RIST_PEER_CONFIG_VERSION, and parse_url_options() / store_peer_settings()
no longer touch version-specific fields unless peer_config->version is
high enough: reflector, multicast_ttl, multicast_source and local_port
require version >= 2; srp_compat_legacy requires version >= 3.
- Windows XP support restored: rist_transport_poll() now uses
select() via the existing contrib/poll_win.c shim instead of
WSAPoll() (Vista+ only). Regression introduced in 0.2.17.
- rist_peer_recv() now accepts both canonical numeric EAGAIN errno
values (11 and 35) when a custom transport_ops backend signals
"no data ready", protecting cross-compile environments where the
EAGAIN macro can resolve differently across translation units.
- librist/transport.h now defines ssize_t on Windows (#214), so
downstream consumers no longer have to provide their own typedef.
- Prometheus exporter: counter rows in multiple_metric_datapoints
mode now have a space between the value and the timestamp.
Affected every existing _total row.
- Prometheus exporter: in single_stat_point mode, the displayed
value no longer freezes at the first callback and silently
drops every subsequent sample. Affected all receiver-flow,
sender-peer, and per-peer-receiver gauges.
- Receiver flow stats: bitrate, bitrate_retries, bitrate_rejected
and bitrate_ts_nulls now decay to zero when the corresponding
traffic stops, instead of remaining frozen at the last value
computed while traffic was still flowing (#217).
SRP interoperability:
- 0.2.16 made TLS-SRP RFC 5054 / TR-06-2 compliant by adding the
PAD operation to the u and k hash inputs. As a side effect,
SRP-authenticated peers running 0.2.15 or earlier (which omitted
the PAD) will not handshake with 0.2.16+. Upgrading both ends
to 0.2.16+ remains the recommended fix.
- New URL parameter for transitional interop:
?srp-compat=1
On both ends of the SRP exchange, this opts the SRP wire format
back into the pre-0.2.16 unpadded form. Must be set on BOTH
sides; mixed configuration will still fail. An explicit WARN
is logged on startup when the option is active. Intended only
as an escape hatch while older deployments are upgraded.
- When an SRP handshake fails, the authenticator (and the client
on M2 mismatch) now emits a single INFO hint pointing at the
srp-compat=1 escape hatch. The hint is advisory only -
the SRP-6a transcript binds M1 to the authenticator's wire-mode
B, so there is no purely-local way to distinguish "wrong
password" from "peer is on the other wire format" once the
handshake has reached M1. No action required for non-SRP
deployments.
CI / Test Coverage:
- srp_unit_test now runs in test-ubuntu. cmocka is sourced via a
new subprojects/cmocka.wrap (WrapDB 1.1.8-1) so the test builds
even on runner images that do not ship libcmocka-dev, closing the
silent-skip gap that hid the SRP fixture drift reported in #215.
Test fixtures regenerated for the PAD-compliant SRP exchange
against the 2048-bit RFC 5054 group. Added four end-to-end tests
covering both PAD and legacy modes plus the two cross-mode failure
cases.
- The issue #188 NAT/socket-rebind and multipath cname integration
tests now compile under MSVC: they use the Windows pthread-shim
(runtime mutex initialisation and PTHREAD_START_FUNC thread entry
points) instead of a direct <pthread.h> dependency.
Tools:
- ristsender: new --blind-send option. Decouples the sender from
RIST peer state - multicast input groups are joined immediately
and incoming UDP data is written to the RIST context regardless
of whether any output peer has connected. Intended for
fire-and-forget broadcast topologies (multicast output in
simple/main/advanced profile, or ristsender used as a relay /
fan-out in front of non-RIST consumers).
- Prometheus exporter: new per-peer receiver metric block
rist_receiver_peer_* with labels {peer_id, peer_url, flow_id,
receiver_id}, mirroring rist_sender_peer_*. Auto-registered
from rist_stats_receiver_flow.peers[].
- Prometheus exporter: new client-flow gauge
rist_client_flow_avg_buffer_time_seconds reporting the dynamic
receiver buffer fill level (sourced from avg_buffer_time in
rist_stats_receiver_flow, converted from ms to seconds).
- Prometheus exporter: now also exports receiver counters that were
previously JSON-only: retries, dropped-late, dropped-full, duplicates,
and the NACK-depth recovery buckets (recovered after two / three / four
/ more NACKs).
Changes for 0.2.17:
---------------------------------
ABI/API:
- ABI version 12:0:8 (soversion 4, binary-compatible with 0.2.15)
- API version 4.9.0 (new features, backwards-compatible)
- RIST_STATS_VERSION bumped from 1 to 2 (new byte-counter fields
appended to rist_stats_sender_peer, rist_stats_receiver_peer,
and rist_stats_receiver_flow).
- RIST_STATS_JSON_SCHEMA_VERSION bumped from 2 to 3 (new fields:
sent_bytes, retransmitted_bytes, received_bytes).
- RIST_PEER_CONFIG_VERSION bumped from 1 to 2 (new fields:
multicast_ttl, multicast_source, local_port).
New Features:
Pluggable Transport Abstraction (transport.h):
- New rist_transport_ops vtable replaces direct socket syscalls with
user-supplied callbacks (sendto, recvfrom, socket, bind, close,
setsockopt, poll). Enables librist on non-POSIX platforms such as
WebAssembly/WebTransport, userspace network stacks, or unit-test
harnesses.
- New rist_transport_set() installs custom transport ops on a context.
- Default ops delegate to real syscalls so existing behaviour is
unchanged.
Packet Split / Merge (peer.h):
- Sender: split each application write into an even/odd sequence pair
when the payload exceeds half the path MTU. Controlled by
split_mode in rist_peer_config (off, auto, half).
- Receiver: transparently merge split pairs before delivery.
Controlled by merge_mode in rist_peer_config (off, pairs, auto).
- Keepalive advertises split capability via the L bit; receiver
auto-detects when the sender supports it.
- URL parameters: ?split=off|auto|half, ?merge=off|pairs|auto.
- Split/merge counters surfaced in JSON statistics.
- risttunnel auto-enables split+merge when MTU < 1400.
Advanced Profile (VSF TR-06-3):
- Wire format and protocol integration for the Advanced Profile as
defined by VSF TR-06-3.
- Native control plane: Keep-Alive with I bit (CI=0x8000), RTT Echo
Request/Response (CI=0x0010/0x0011), NACK Bitmask (CI=0x0000),
NACK Range (CI=0x0001), Unsupported Response (CI=0x8020).
- Full 32-bit sequence number indexing (expanded seq_index array).
- Type 8 GRE-over-AP encapsulation for Section 9 interoperability.
- PSK encryption mode 1 (AES-CTR, Main Profile compatible) with
per-packet Nonce/IV in the AP header.
- Future Nonce Announcement (CI=0x8011) for zero-latency key rotation.
- SRP (Secure Remote Password) passphrase exchange, reusing the EAP-SRP
state machine from Main Profile over Type 8 encapsulated EAPOL.
- Flow ID field (I flag): virt_dst_port and virt_src_port map to/from
the AP Outer/Inner Flow ID hierarchy on the wire.
- LZ4 payload compression (LPC=1): sender compresses before encryption,
receiver decompresses after decryption. Controlled by the existing
?compression=1 URL parameter; receiver auto-detects via the LPC field.
Bundled LZ4 library in contrib/lz4/ with system-library fallback.
- Flow Attribute messages (CI=0x8001): sender periodically transmits
session/flow metadata as JSON. New receiver callback API
(rist_receiver_flow_attr_callback_set) delivers parsed attributes
to the application.
- Baseline.Direct conformance for single-flow encrypted point-to-point
RIST transport with ARQ.
- 22 integration tests (basic, packet loss, encryption, mismatch,
flow-id, lz4, lz4+encryption, flow-attr, SRP auth).
- 187 unit tests (wire format, timestamp, seq_index, Type 8, PSK modes,
Flow ID mapping, LZ4 roundtrip, flow-attr control roundtrip).
Per-byte statistics (#174):
- Sender stats now include sent_bytes and retransmitted_bytes counters
in both the C struct (rist_stats_sender_peer) and JSON output.
- Receiver flow stats now include received_bytes in the C struct
(rist_stats_receiver_flow) and JSON output.
- Per-peer receiver stats include received_bytes.
Multicast enhancements (#112):
- Configurable multicast TTL via ?ttl=N URL parameter (1-255).
New udpsocket_set_mcast_ttl() public API.
- IGMPv3 Source-Specific Multicast (SSM) support via ?source=IP
URL parameter. Uses IP_ADD_SOURCE_MEMBERSHIP / MCAST_JOIN_SOURCE_GROUP.
- IPv6 multicast join support (previously only IPv4 was implemented).
- New udpsocket_open_bind_mcast() with TTL and SSM source parameters.
- udpsocket_join_mcast_group() now public API, supports IPv6 and SSM.
Configurable local port for caller peers (#158):
- New ?local-port=N URL parameter to bind caller (non-listening) peers
to a specific local UDP port instead of using an ephemeral port.
- New local_port field in rist_peer_config.
Deferred multicast join in ristsender (#85):
- When ristsender's input URL is a multicast address, the IGMP join
is now deferred until the first RIST peer handshake completes.
Prevents receiving multicast traffic before the output path is ready.
WebAssembly / Emscripten:
- TUN device stubs for Emscripten targets.
- Meson cross-file (wasm32-emscripten.txt) for WASM builds.
- C99 strict-aliasing fix for merge memcpy.
Build:
- Bundled mbedTLS upgraded from 2.28.10 (EOL) to 3.6.6 LTS.
The bundled tree now compiles as a separate static library with
targeted warning suppression, isolating mbedTLS internals from
librist's strict compiler flags. Source compatibility fixes
applied for the 2.x -> 3.x API migration (removed _ret suffixes,
updated header paths). System mbedTLS 3.6.x is also fully
supported.
- Removed unused #include <mbedtls/entropy_poll.h> which broke
builds against system mbedTLS >= 3.6 where the header was removed.
Test:
- Loss simulation tests no longer abort on transient error log
messages. Under heavy bilateral loss, error-level messages are
expected and do not indicate bugs.
- Minimum receive threshold for loss tests now scales with the
configured loss percentage, giving headroom for ARQ recovery in
CI environments with timing jitter.
New features:
- Add transparent one-to-many reflector mode (Main Profile only).
A receiver listener configured with ?reflector=1 transparently
forwards data packets from the publisher (sender) to all subscriber
(receiver) peers, and routes NACKs/RTCP from subscribers back to
the publisher. Enables a single-listener fan-out topology without
requiring a full sender context on the reflector.
Trade-offs vs rist2rist (per-subscriber ARQ relay):
* No per-subscriber retry buffer — recovery relies on the
publisher's buffer, which may have aged out the packet.
* Retransmissions fan out to ALL subscribers, not just the one
that NACKed — bandwidth scales with subscriber count.
* Recovery RTT is roughly 2x a direct connection.
* No per-subscriber congestion control or statistics.
Best suited for low subscriber counts with clean last-mile links.
For high fan-out or lossy last-mile, use rist2rist instead.
(contributed by RossWang)
RTCP/NACK:
- Fix OOB read in NACK_FMT_SEQEXT handler: the RTCP feedback loop
validated only 4 bytes per record, but the seqext struct reads
seq_msb at offset 12. A crafted datagram with a short trailing
record could read up to 12 bytes past the allocation. Add a
per-handler minimum-length check matching the adjacent ECHO branches.
- Fix OOB read and log-disclosure in rist_sender_recv_nack: the NACK
range handler read the 4-byte name field at offset 8 with only an
8-byte minimum guarantee, and logged it with %s (unbounded read to
next NUL). Add a sizeof guard and use %.4s.
Log:
- Lower log level for harmless cross-role NACK/data packets from
ERROR/WARN to DEBUG to reduce log noise in reflector topologies.
EAP/SRP:
- Fix EAPOL-Start body length field: the packet declared a 4-byte body
(sizeof(eapol_hdr)) when the correct value is 0 (EAPOL-Start carries
no body). This bug was dormant until the bounds-check tightening in
0.2.15 started correctly rejecting oversized body declarations,
which broke every SRP handshake. SRP authentication has been
non-functional since 0.2.15.
- Fix off-by-one in eap_srp_send_password bounds check: the limit did
not account for the 1-byte flags field before the ciphertext, so a
maximum-length password would write 1 byte past the stack buffer.
Not network-reachable (all callers cap at 128 bytes), but worth
hardening.
Changes for 0.2.16 (pre-release):
---------------------------------
ABI/API:
- ABI version 10:4:6 (soversion 4, binary-compatible with 0.2.15)
- API version 4.7.0 (unchanged)
Maintenance and security-hardening release for 0.2.15. No source or
binary changes are required of downstream consumers.
Bug Fixes:
Windows:
- Windows tools fail to start when remote logging is enabled.
Running `ristreceiver -r 127.0.0.1:port ...` on Windows 11 would
exit immediately with "Failed to setup logging!" because winsock
had not been initialised before the public logging API tried to
open a UDP socket. Fixes #208 (reported by Roman Dissertori).
- EAP-SRP authentication fails on Windows with multiple peers.
A 0.2.15 regression: an outbound packet that drew an ICMP
port-unreachable reply caused Windows to corrupt the next
incoming UDP datagram on the same socket, which then broke the
EAP handshake. Most visible with bonded ethernet+wifi peers,
Linux ristsender -> Windows ristreceiver. Fixes #209 (reported
and confirmed fixed by Roman Dissertori).
- Windows socket failure log lines now include the actual WSA
error code, so user bug reports from Windows are diagnosable.
- Cryptographic random number generator now seeds reliably on
Windows runtimes where the legacy CryptoAPI (CryptAcquireContext
/ CryptGenRandom) is missing or unconfigured - notably wine and
some sandboxed container builds. The seeding path now uses the
modern BCryptGenRandom (CNG) API, supported on every shipping
Windows version. Without this fix, the 0.2.15 CSPRNG hardening
refused to start the SRP/AES code paths in those environments
rather than silently downgrade. Fixes #210.
- MSVC builds no longer crash due to stack corruption in atomic
operations. The stdatomic compatibility shim hardcoded all
operations to a single width, so a 64-bit compare-exchange on a
32-bit atomic variable overwrote adjacent stack memory.
Tools:
- ristsender now preserves incoming RTP sequence numbers and
timestamps when the --rtp-sequence and --rtp-timestamp options
are used. Previously these options had no effect.
Hardening / follow-ups to 0.2.15:
- EAP-SRP authentication retry exhaustion did not drop the failed
peer. A latent sign-comparison bug meant a peer that failed
every authentication retry was kept around in a half-
authenticated state instead of being dropped. Not externally
reported.
- Crypto random number generator could silently downgrade to a
weak fallback. If the CSPRNG seed call failed at startup (no
kernel entropy source - sandboxed container, embedded image
without /dev/urandom, broken build), the failure was dropped
and the wall-clock-derived stopgap was used for the lifetime of
the process. Now logs loudly and refuses to hand out random
bytes. Not externally reported.
- Internal EAP error code documentation: clarify a guard comment
that read as a removable paranoia check but is actually
protecting against a NULL function pointer.
Post-0.2.15 audit follow-ups (reported by Thomas Guillem):
Receiver flow / peer accounting:
- Receivers no longer crash or corrupt their peer list when a
peer is added to a flow at the same instant another thread is
removing one. The list of peers per flow was being walked by
one thread under one lock and modified by another thread under
a different lock; the two paths now agree. A 256-peer-per-flow
cap is also enforced so a misconfigured (or hostile) sender
cannot grow the list without bound.
- Receivers stop creating flows once 256 active flows already
exist. Previously the count was checked and the new flow
allocated in separate steps, so a burst of new flow IDs could
race past the limit. Now the count check and the slot insert
happen under the same lock.
EAP-SRP authenticator:
- Authenticated peers can no longer be deauthenticated by a
spoofed EAPOL-LOGOFF. A successfully authenticated session
used to reset to start-over state on receipt of any LOGOFF
packet, which made the auth state machine trivially DoSable
from off-path. LOGOFF is now only honoured before the session
reaches authenticated state, and a legitimate LOGOFF before
that point also resets the retry counter so a counter
wrap-around can't lock the peer out for the lifetime of the
process.
- A spoofed EAP FAILURE no longer pushes the local state machine
into permanent failure. FAILURE packets are now matched
against the identifier of the in-flight EAP exchange and
dropped if they don't match, the same way Request/Response are
matched. A genuine FAILURE still latches the peer out, but
after a 30-second quiet period the peer is allowed to retry
from the start so a transient burst of bad responses doesn't
require restarting the process.
- EAP authenticators no longer answer EAP Identity requests
themselves. The handler used to run on both roles, so a
misbehaving (or attacker-controlled) authenticatee could
persuade the authenticator to discard its own session state
and start over. The handler is now restricted to the
authenticatee role, and even there it is rate-limited so the
pre-auth reset cannot be triggered repeatedly to wipe
in-progress sessions.
- EAP SRP setup now caps the advertised modulus (N) and
generator (g) lengths at 1024 bytes (8192-bit groups, the
largest standard RFC 5054 group). A peer that advertised
multi-megabyte values used to allocate that much memory and
burn that much CPU on bignum imports before deciding the
handshake was bogus.
PSK encryption:
- The PSK nonce generator no longer falls back to a
wall-clock-derived value when the CSPRNG misbehaves. Nonces
are required to come from the system CSPRNG; if it fails or
returns suspect output the key is marked broken, encryption
refuses to produce ciphertext for that key (sends zeroes
instead so a downstream consumer cannot mistake it for a
successful encrypt), and the existing audible logging fires.
Decryption was already locked out in the same condition; this
closes the encrypt-side gap. The non-security-critical
`prand_u32` helper used elsewhere in the tree keeps its
wall-clock fallback because its callers don't need
unpredictability.
- PSK decryption now skips the PBKDF2 key derivation when the
key is already locked out. A bursty stream of malformed
encrypted packets at a peer whose key state was already
compromised used to spin tens of thousands of PBKDF2 rounds
per packet before rejecting it. The lockout check now happens
first, so the per-packet cost drops to a few comparisons.
- PSK AES-CTR dispatch no longer silently falls back to AES-128
on an unknown key size. The earlier switch-case used a
fallthrough that meant a corrupted or unsupported key length
would still be processed (with the wrong cipher), which
masked configuration errors. Unknown key sizes now return
without touching the buffer.
SRP / cryptographic primitives:
- The Nettle build of SRP no longer feeds uninitialized stack
into the bignum library when the CSPRNG returns no entropy.
The Nettle random callback's signature returns void, so a
CSPRNG failure was previously dropped on the floor and the
caller computed against whatever was on the stack. The
callback now zeroes its output buffer and signals the caller
via a side-channel context; the bignum operation then fails
cleanly. The mbedTLS build was already correct.
- The SRP NG (N/g) group identifier enum now keeps its
pre-0.2.15 integer values. The 0.2.15 release deleted the two
smallest groups (NG_512, NG_768 - both below current
minimum-strength thresholds) but shifted every remaining
enum entry down by two as a side effect. The integer "2",
which any external caller might reasonably have hardcoded
for NG_1024, silently started meaning NG_4096. The deleted
values 0 and 1 are now explicit reserved slots that return
"no such group" - code that uses the named constants
(`LIBRIST_SRP_NG_1024` etc.) is unaffected, code that used
literal integers either gets the group it asked for or a
clean error.
Statistics JSON:
- The stats JSON payloads (sender flow, sender per-peer wrapper,
receiver flow) now carry an explicit top-level
`schema_version` field. The current value is 2 - version 1 was
the duplicate-`peer`-key sender shape that v0.2.15 replaced
with a `peers` array (NEWS noted the change, but downstream
parsers had no way to detect which shape they were getting).
Any future incompatible shape change must bump this number.
- The sender `cname` field copy is now explicitly
NUL-terminated. The previous strncpy with a destination-sized
length depended on a separate bounds check elsewhere in the
receive path to keep the result safe; the contract is now
self-contained.
Build / portability:
- librist now requires mbedTLS >= 2.7 (March 2018), and emits a
clean #error if built against anything older. The compatibility
branch for pre-2.7 mbedTLS releases (which used the
void-returning SHA-256 API) had no CI coverage and was the
same untestable corner that produced a 0.2.14 build break -
dropping it removes the surface area entirely. Every still-
supported distro is well past 2.7; the bundled tree is 2.28.x.
- On Windows, the mbedTLS CSPRNG seed no longer reaches into
private mbedtls_entropy_context fields to keep its source
count happy. It now seeds the CTR_DRBG directly from
BCryptGenRandom, skipping the entropy module entirely on
Windows. Non-Windows builds still use the normal entropy
initialisation path.
Second audit follow-ups (reported by Thomas Guillem):
EAP-SRP authentication:
- EAP passphrase response handler now gates on a matching
outstanding request identifier when a solicited passphrase
exchange is active. Previously any post-auth spoofed
RESPONSE/PASSWORD_REQUEST_RESPONSE was accepted, allowing a
man-on-the-side to hijack a key-rotation exchange. Unsolicited
passphrase pushes (rist_eap_send_passphrase) remain allowed as
the payload is encrypted under the SRP session key.
- EAP last_identifier is no longer primed in the request prologue
before subtype validation. A spoofed REQUEST with a crafted
identifier could prime the FAILURE-identifier gate, allowing a
subsequent spoofed FAILURE to latch the peer out.
- EAP authenticator initial identifier now comes from the CSPRNG
instead of prand_u32, closing an off-path prediction window.
- The use_default_2048 flag in EAP SRP challenge processing was
hardcoded true, ignoring wire-supplied N/g groups. Now derived
from the actual generator length on the wire.
- eap_set_ip_string replaced an unbounded memcpy with strncpy and
explicit NUL-termination.
EAP-SRP / cryptographic primitives:
- SRP u and k hash computations now use RFC 5054 PAD: both inputs
are zero-extended to the byte length of N before hashing. The
previous unpadded hash produced different values for leading-
zero-byte operands, deviating from the specification. This is a
wire-incompatible change with pre-0.2.16 peers using SRP.
- SRP M1/M2 verification and client M2 verification now use a
constant-time comparison instead of memcmp.
- SRP authenticator aborts if the verifier v is zero (session key
would not depend on the password).
- SRP authenticator aborts if B mod N == 0 (RFC 5054).
- SRP authenticator aborts if u == 0 mod N after hashing A||B.
- SRP hash return values are now checked; unchecked failures
previously left uninitialised buffers in the session transcript.
- SRP BIGNUM_EXP_MOD return after computing S is now checked.
- SRP client context creation now validates modulus length
[128, 1024], generator length [1, N_len], and salt length
[1, 64] to cap resource consumption from malformed handshakes.
- SRP private exponent size now matches the modulus size instead
of a hardcoded 32 bytes, providing full-strength keying for
large groups (e.g. NG_8192).
- SRP salt generation now uses fixed-size 32-byte raw CSPRNG
output copied directly, instead of going through bignum export
which strips leading zero bytes and shrinks the effective salt
domain.
Receive path / flow safety:
- Use-after-free on flow deletion during rist_receiver_data_read2
is fixed. The flow pointer returned by the lookup function is
now validated under flows_lock; the flow's own mutex is acquired
before flows_lock is released. rist_delete_flow unlinks the
flow under the same lock before freeing.
- rist_receiver_set_output_fifo_size now rejects size == 0 and
size > 65536, preventing a zero-modulus OOB on the FIFO ring.
- rist_peer_get_cname uses strnlen instead of strlen, bounded by
RIST_MAX_STRING_SHORT.
MPEG-TS null packet handling:
- expand_null_packets now bounds-checks each source-packet copy
against the original payload length, preventing an information
leak of adjacent buffer data when the NPD bit pattern implies
more real packets than the payload actually contains.
- suppress_null_packets now checks the sync byte of every packet
in the block, not just the first.
SDES / cname / miface:
- RTCP SDES writer uses strnlen instead of strlen and zero-fills
the padding region explicitly, instead of copying adjacent
memory from the source buffer.
- All strncpy sites for peer cname and miface (peer_initialize,
rist_peer_clone) now have explicit NUL-termination.
PSK encryption:
- PSK key init now uses strnlen and explicit NUL-termination
instead of unbounded strlen + memcpy.
- PSK key clone now explicitly NUL-terminates the copied password.
- PSK decrypt no longer unconditionally clears bad_decryption
after rekeying; if _librist_crypto_aes_key set the flag (PBKDF2
failure), it stays set so the lockout counter is not reset.
Sender:
- sender_buffer_size growth is now capped at sender_queue_max - 1,
preventing unbounded memory growth when packets are too young to
delete from the retry queue.
- rist_sender_peer_create now calls rist_peer_remove instead of
bare free on RTCP-peer creation failure, preventing a dangling
pointer in the peer list.
Miscellaneous:
- rist_peer_remove dead store fixed: *next = NULL now executes
before the early return on NULL peer.
- NACK debug log guarded against zero-length array dereference
and uses %zu for size_t.
- is_ip_address uses a union of in_addr/in6_addr for the
inet_pton destination buffer, fixing an AF_INET6 stack overflow
that wrote 16 bytes into a 4-byte sockaddr_in.sin_addr.
- rist_new_connection log format strings replaced char[] locals
passed via &array with const char * pointers, eliminating
strict-aliasing undefined behaviour in the varargs call.
- rist_tun_write overflow guard changed from
len + UTUN_HEADER_SIZE > sizeof(tmp) to
len > sizeof(tmp) - UTUN_HEADER_SIZE, preventing unsigned
wraparound when len is near SIZE_MAX.
Community bug reports (gitlab #211, RossWang):
Receiver flow timing:
- Initialize last_packet_ts, time_offset_changed_ts, and
time_offset_old when a flow (re)starts. After a flow reset the
stale values from the previous epoch could cause the old-clock
fallback to fire, corrupting packet timing and dropping the
first few packets.
OOB dequeue:
- rist_oob_dequeue now holds peerlist_lock while walking a
listener peer's child list. A concurrent peer add/remove could
free a sibling_next pointer during the walk.
Note: _librist_peer_match_peer_addr (#211 item 3) also walks the
child list without a lock, but cannot use peerlist_lock because the
receive callback runs on the same thread that holds it in the
protocol loop. Fixing this requires a per-listener child rwlock or
reference counting, planned for a future release.
Test / CI:
- New unit test (logging_init_order) that exercises the
rist_logging_set remote-address path before any rist_*_create
call. Would have caught #208 on the first Windows-CI run.
- The full meson test suite now runs against the cross-compiled
Windows binaries under wine, not just on Linux. Every protocol
test that gates Linux releases also gates Windows releases,
including the SRP / AES encryption suites which are now built
with mbedTLS on Windows (see the #210 fix above). The wine
runtime was already provisioned in the CI image; only the test
invocation was missing. Catches Windows-runtime regressions
without requiring a native Windows runner.
- Per-test results from the Linux job now show up directly in the
GitLab MR UI via meson's JUnit output, so reviewers don't have
to read job logs to see which test failed.
Changes for 0.2.15 (2026-05-14):
--------------------------------
ABI/API:
- ABI version 10:3:6 (soversion 4, binary-compatible with 0.2.14)
- API version 4.7.0 (unchanged)
Security release. Existing 0.2.14 installations remain
binary-compatible; downstream consumers do not need to recompile.
Security Fixes:
Receive-side parsers and key handling were audited end to end. Each
fix below is independent.
Receive path / RTCP:
- rist-common: bounds-check the RTP and RTP-extension headers.
The earlier check only covered the 4-byte reduced port subheader;
a truncated packet underflowed payload.size to ~SIZE_MAX which
then drove a malloc(len + 32) on the receiver enqueue path.
- rist-common: harden the RTCP dispatcher. Off-by-one in
bytes_left, integer overflow in 4*(1+records) (CPU spin on a
single packet), and an SDES handler that let 255 bytes be
memcpy'd into a 128-byte struct member, overrunning the adjacent
peer config/key state.
- rist-common: bound the XR block parser, clamp the on-the-wire
length to what we received, and guard the DLRR delay subtraction
so it can no longer pin last_rtt near UINT64_MAX (which would
permanently disable NACK timing on the peer).
- rist-common: validate the sender NACK record count before
iterating. ntohs(rtcp->len) - 2 underflowed to ~65K on a
malformed feedback packet, enqueueing OOB bytes as retransmit
requests and leaking them back to the peer.
- rist-common: add the missing return after the VSF length check,
and derive odd_nonce from the actual nonce byte rather than
GRE flags1 (the latter silently disabled the OTF passphrase
rotation feature on the receive side).
- rtcp: guard the RR and echo-response RTT subtractions and
size-check the SR / echo-request / echo-response casts (same
RTT-underflow primitive as above on the RR/echo paths).
- rtcp: correct the XR echoreq RTCP length field per RFC 3550;
receivers were reading 4 bytes past the actual XR block.
- mpegts: bound NPD expansion to the 7-packet wire encoding.
expand_null_packets used attacker-controlled payload_len and
npd_bits to drive ts_count and write up to 11 KB into a 10 KB
recv_npd buffer (overrunning into the adjacent RTCP TX buffer),
and shifted by a negative amount when ts_count > 7.
- gre: zero-init keepalive info on short packet and check return,
otherwise a truncated keepalive disclosed stack memory through
the user logging callback.
- udpsocket: NUL-terminate the URL hostname after strncpy so
udpsocket_resolve_host cannot over-read.
PSK / AES-CTR:
- crypto: reject zero-nonce PSK packets instead of skipping
decryption. The previous early-return left the recv buffer
untouched and the caller treated the attacker's plaintext as a
freshly-decrypted Main Profile packet, bypassing the configured
PSK per packet. Pre-auth, single packet.
- crypto: correct nettle AES-CTR dispatch for 128/192-bit keys.
AES-128 was using nettle_aes192_encrypt against an aes128
context; the AES-192 case was missing entirely, leaving an
uninitialised function pointer hot on the EAP password-rotation
path.
EAP / SRP:
- eap: validate EAPOL/EAP/SRP-TLV lengths end to end. The body
length check was inverted and inner dispatchers walked past the
recv buffer with attacker-supplied lengths, exfiltrating heap
bytes through username copies and AES-CTR rewrites. Adjacent
fixes cover EAP header / dispatch / passphrase-flags reads.
- eap: NULL-check auth_ctx and propagate handle_A failure (notably
A == 0 mod N) so a rejected SRP exchange actually fails auth
instead of sending an uninitialised B back to the client. NULL
check the SRP client ctx and propagate the int-typed return of
the A/B writers (a -1 wrapped to ~SIZE_MAX and crashed on the
last_pkt malloc cache path).
- eap: bound rist_eap_send_passphrase to the destination buffer;
an over-long passphrase from the API or YAML config overran
ctx->unsollicited_passphrase into adjacent heap fields used by
the periodic retransmit.
- eap: rate-limit the EAP_CODE_FAILURE restart loop. The handler
unconditionally called eap_reset_data + log + EAPOL_START with
no retry counting, so a spoofed FAILURE per round-trip kept us
bouncing through reset+restart forever.
- eap: write the empty server-name placeholder into outpkt rather
than pkt (pure copy/paste typo that corrupted the recv buffer
on every successful identity response).
- srp: propagate failure when the client A, server B, or u is
congruent to 0 mod N. The checks existed but the failure path
left ret==0 so the caller continued the exchange. Auth bypass
primitive on both sides.
PRNG:
- crypto: route prand_u32 through the CSPRNG. The previous
implementation called rand() seeded with a wall-clock NTP
timestamp; the result is used for the PSK GRE nonce (input to
PBKDF2), the EAP identifier and the peer SSRC. Reuse the mbedTLS
CTR-DRBG / GnuTLS RNG that already backs SRP key generation.
- crypto: fix operator-precedence bug in random_get_string and
add an upper bound on the local rand buffer. Affected callers
that ask for a runtime-derived password.
Hygiene (not security-impacting on their own but shipped in the same
release):
- flow: handle realloc/calloc failure on the peer-list growth path
and rist_receiver_missing; correct two log format strings
(uint32_t printed with PRIu64; pointer where a PRIu32 was due).
- libevsocket: preserve the global context list when removing the
head; ctx_del previously cleared CTX_LIST instead of advancing
it to c->next.
- eap, logging, udpsocket: NULL-check the malloc/calloc results
that were dereferenced unconditionally; free the calloc'd ctx
on pthread_mutex_init failure in eap_clone_ctx; close the bound
socket on the multicast-join failure leg.
- network: on the GNU/Hurd MAC-address path, check socket() and
close it on the getifaddrs() failure leg.
Additional fixes from the VideoLAN security audit:
Independent receive-side / EAP / SRP audit shared by VideoLAN.
The findings already covered above are not re-listed; the items
below close the residual gaps.
EAP / SRP:
- eap: ignore EAP_REQUEST_IDENTITY once we're past
EAP_AUTH_STATE_SUCCESS so a single forged identity request can
no longer tear an established session down and re-emit the
configured username on the wire.
- eap: refuse RESPONSE/IDENTITY on the authenticatee side
(lookup_func is NULL there per the calloc setup path; the
handler used to deref it unconditionally).
- eap: free verifier_data on every exit from
process_eap_response_identity. Three error paths returned -1
before reaching the cleanup block, leaking the four heap
allocations the verifier lookup callback handed back.
- srp: jump to failed: instead of returning out of
BIGNUM_WRITE_BYTES when MPI write fails inside calc_x; the
return -1 path leaked hash_data.
- srp: check librist_crypto_srp_hash_bignum return in
calculate_m. The helper returns -1 (and leaves the output
buffer untouched) when the bignum exceeds its 1024-byte
staging buffer; the unchecked path XOR'd uninitialised stack
into the SRP transcript.
- srp: BIGNUM_INIT k and B in the authenticator constructor.
Relying on calloc-zeroed mbedtls_mpi state happened to work
today but the public mbedTLS contract requires explicit init.
- srp: bound A and B operand sizes against N before exp_mod.
handle_A and client_handle_B accepted any length from the
wire; mbedtls_mpi_exp_mod runtime grows with operand size.
- srp: NULL the caller's *bytes_s/*bytes_v in _create_verifier
failure paths so eap_reset_data does not double-free them.
- srp: capture mbedtls_mpi_fill_random return when generating
the salt; the previous `if (ret != 0)` re-checked the prior
BIGNUM_FROM_STRING.
- srp: repair the !USE_SHA_RET branch of librist_crypto_srp_hash
(referenced symbols from a different function and was missing
its trailing semicolon).
- srp: drop the sub-1024-bit RFC 5054 groups (NG_512, NG_768).
The enum was internal; the unit-test fixture that wanted the
deterministic 512-bit exchange now inlines the constants.
PSK / AES-CTR:
- psk: fail closed when PBKDF2 setup fails on the mbedTLS path.
mbedtls_md_setup / mbedtls_pkcs5_pbkdf2_hmac errors used to
fall through into mbedtls_aes_setkey_enc with the
stack-allocated aes_key buffer in undefined state.
- psk: don't let an attacker-chosen nonce reset the bad-packet
lockout. _librist_crypto_psk_decrypt now keeps bad_decryption
set across nonce rolls so the five-bad-packets cap is no
longer trivially bypassed by sending a different nonce on
every shot.
- psk: use memcpy for the GRE nonce load instead of a punned
uint32_t cast (strict-aliasing UB; faulted on architectures
that require aligned uint32_t loads).
Receive path:
- rist-common: lift the rist_rtp_hdr size check above the
Simple/REDUCED conditional so Main-profile FULL/EAPOL paths
can no longer read past recv_bufsize.
- udp: size the NPD scratch buffer for the real worst case
(7 * 204 + sizeof(rist_rtp_hdr_ext)). The previous 6 * 204 + 4
was four bytes short for the (count == 7, suppressed == 1)
output and let suppress_null_packets smash four bytes of
stack past tmp_buf.
- rist-common: cast keepalive json_len to int for %.*s. size_t
via varargs is undefined per the C standard.
Flow / threading:
- flow: take f->mutex when growing peer_lst; the realloc could
move storage and concurrent walkers (rist_best_rtt_index, the
output thread, stats) read peer_lst without synchronization.
- flow: cap receiver flows at RIST_MAX_FLOWS (256) and walk
FLOWS under flows_lock. Without a cap an attacker that can
land traffic on a receiver can exhaust memory by walking the
32-bit flow_id space.
- flow: NULL-check the dataout_fifo_queue calloc; the
data-output thread used to write into it unconditionally.
TUN / RTCP TX / utility:
- tun_linux, tun_darwin: bounds-check prefix_len before computing
the netmask. prefix_len < 0 hit undefined behaviour in C and
prefix_len == 32 produced a 32-bit shift on a uint32_t (also
UB).
- tun_darwin: parse the utun unit suffix with strtoul, not atoi,
with full validation; falls back to "let the kernel pick" on
parse failure.
- rtp: clamp SDES name_len to 255 before computing sdes_size so
the on-the-wire length byte and the actual bytes copied stay
in agreement.
- rist-common: make compare() return a real three-way result
(qsort's contract is < 0 / 0 / > 0; the previous boolean cast
was undefined per POSIX).
- rist-common: pick the actual median in recalculate_clock_offset
(the index was biased high by one).
Bug Fixes:
- rist-common: initialize peer->sd to -1 in peer_initialize so a
DNS resolution failure no longer leaves a zero socket
descriptor wired up to the event loop, which on Windows turned
into an infinite POLLERR loop and on Unix could attach the RIST
state to fd 0 (stdin). (!312, Yannick Le Roux)
- logging: check log_socket >= 0 instead of just non-zero in the
rist_log_impl UDP path, matching the LOGGING_SETTINGS_INITIALIZER
sentinel (the early-exit was tightened in f7ace4d but the send
branch still treated 0 as a valid fd). (!313, Daisuke Matsunami)
- stats: emit sender peers under a "peers" array. The previous
shape used "peer" as the key for every peer object, producing
invalid JSON and silently dropping all but one peer in strict
parsers. (#206, Manuel Alejandro)
Tools:
- prometheus-exporter: implement rist_prometheus_parse_sender_stats.
The hook for ristsender's JSON stats callback was a TODO stub, so
sender-side rist_sender_peer_* gauges/counters never showed up at
the scrape endpoint. The parser now walks the new "peers" array
and feeds rist_prometheus_handle_sender_peer_stats; metric names
and shape are unchanged from the receiver-callback path.
(closes the second half of #206)
- ristsender: drop a duplicate rist_sender_stats_callback_set call
in setup_rist_peer that overwrote the per-instance arg with NULL,
breaking sender attribution when more than one sender ran in the
same process.
- udp2udp: log a one-shot warning when --metrics is enabled noting
that the relay produces no rist_* series (it uses librist only
for URL parsing and the httpd shell).
Windows / MinGW Build:
- rist.c: return integer (not NULL) from the PTHREAD_START_FUNC
bodies. The macro expands to a DWORD __stdcall function on
Windows without HAVE_PTHREADS, which gcc14 rejected.
- meson: always probe clock_gettime via has_header_symbol on
Windows, not only when have_mingw_pthreads=true. Modern
mingw-w64 ships clock_gettime as a static inline in <time.h>
regardless of winpthreads, so contrib/time-shim.c collided
with the toolchain definition on the default build.
- rist-common: drain WSAECONNRESET with a real recvfrom buffer
on the rist_peer_sockerr POLLERR path. The previous NULL-buffer
call returned WSAEFAULT and left the indication in the socket
queue, causing WSAPoll to fire POLLERR in a tight loop and
flood the log with "Socket error!" whenever a peer went away.
(#205, Manuel Alejandro)
- tools: centralize the strtok_r/strtok_s shim in a header so
rist2rist (and any future tool that needs strtok_r) builds on
MinGW / MSVC. ristsender and ristreceiver had inline shims;
rist2rist's recent multi-peer bonding work added strtok_r
without one and failed to link. (#207, Thierry Lelegard)
Changes for 0.2.14 (2026-04-25):
--------------------------------
ABI/API:
- ABI version 10:2:6 (soversion 4, binary-compatible with 0.2.13)
- API version 4.7.0 (unchanged)
Maintenance release. No public API or ABI signature changes.
Existing 0.2.13 installations remain binary-compatible; downstream
consumers do not need to recompile and relink.
Bug Fixes:
- fix(sender): serialize retry-queue dequeue under peerlist_lock
to avoid a use-after-free. The retry-queue cleanup path could
race with peer teardown and dereference a peer that had already
been freed on a sibling thread, occasionally producing a sender
crash under sustained NACK pressure with multi-peer
configurations. The dequeue is now performed with the peerlist
lock held for the full critical section.
- fix(macos): honor the -1/errno contract in the time-shim and
zero-initialize the timespec before calling clock_gettime_osx.
rist__get_system_boottime() previously returned the errno value
as its result instead of -1, which silently produced corrupted
NTP timestamps on macOS in restricted-syscall environments
(containers, sandboxes that block sysctl(KERN_BOOTTIME)). The
function now returns sysctl()'s actual return value, and
timestampNTP_u64() / timestampNTP_RTC_u64() defensively
zero-init their timespec so a clock_gettime failure can no
longer leak uninitialized stack into the time arithmetic and
falsely trip the "stream is dead" timeout.
Windows / MSVC / MinGW:
- contrib/poll_win: fix the Windows poll() emulation busy-spinning
at 100% CPU when the watched fd set contains only sockets
(the common case for librist). The previous implementation fell
through to a 0 ms select() loop whenever no pipe/handle fds were
present; the timeout argument is now correctly honoured in the
sockets-only path.
- msvc: round out the contrib stdatomic shim and select native
C11 atomics where the toolchain supports them. Restores
correct atomic_uint_fast64_t behaviour on MSVC builds where
earlier shim coverage left some operations as plain reads /
writes, which (under heavy contention) could let the receiver
flow's last_pkt_received drift and produce false stream-dead
events on Windows.
Tools:
- tools/rist2rist: add multi-peer bonding (multiple input or
output URLs on the command line), MPEG-TS Null Packet Deletion
(NPD), and Prometheus metrics export. rist2rist can now serve
as a load-balancing relay sitting close to the source of truth,
where small recovery buffers and large retry queues let
downstream receivers absorb network impairments without
inflating end-to-end latency.
Contrib (bundled mbedtls):
- contrib/mbedtls: refresh bundled source from 2.26.0 to 2.28.10
(last LTS release of the 2.x series, 2025-03-24). 2.28.x is
strictly ABI-compatible with 2.26.x, so no librist code change
was required. Notable mbedtls fixes pulled in:
* CVE-2025-27809 (TLS server impersonation via missing
mbedtls_ssl_set_hostname)
* CVE-2025-27810 (TLS 1.2 Finished message corruption on
allocator/HW failure)
* CVE-2024-45157 (PSA HMAC_DRBG selection regression)
* Multi-year accumulation of fixes in AES, ECP, MPI, RSA,
PK, PKCS5, bignum and the new constant_time.c
side-channel-hardened primitives.
librist itself only consumes the classic mbedtls crypto API
(AES-CTR, CTR-DRBG, entropy, SHA-256, PBKDF2, MPI), so the
upgrade is a drop-in replacement for the bundled path. The
system-mbedtls build path (-Dbuiltin_mbedtls=false, the
default when a system libmbedcrypto is detected) is not
touched by this commit.
- contrib/mbedtls: widen the hardclock gate from defined(_MSC_VER)
to defined(_WIN32) in the bundled timing.c, so MinGW and
Clang-Windows builds now also pick up the
QueryPerformanceCounter implementation of
mbedtls_timing_hardclock().
Contributors:
- Sergio Ammirata
Changes for 0.2.13 (2026-04-18):
--------------------------------
ABI/API:
- ABI version 10:1:6 (soversion 4, binary-compatible with 0.2.12)
- API version 4.7.0 (unchanged)
Build Fixes:
Platform build-fix release. No changes to the RIST protocol, public
API, or ABI signatures. All existing 0.2.12 installations remain
binary-compatible; downstream consumers do not need to recompile.
This release is only relevant if you build librist targeting one of
the platforms listed below. Native Linux, native macOS, FreeBSD,
and classic Win32 builds with MSVC are unaffected.
Windows desktop built with mingw-w64 (gcc or llvm-mingw):
- meson: detect clock_gettime with has_header_symbol
v0.2.12's probe used cc.has_function('clock_gettime'), which
does not resolve the static inline definition shipped by
mingw-w64's <pthread_time.h>. The resulting HAVE_CLOCK_GETTIME=0
caused contrib/time-shim.h to emit a conflicting extern
declaration and broke any downstream build with strict warnings
("redefinition of 'clock_gettime'"). Switching to
cc.has_header_symbol detects the inline correctly.
Windows UWP (i686/x86_64/aarch64-w64-mingw32 UWP toolchains):
- Also fixed by the has_header_symbol probe above.
- network.c: replace GetAdaptersInfo with GetAdaptersAddresses
_librist_network_get_macaddr() called GetAdaptersInfo, which is
not part of the UWP (WINAPI_FAMILY_APP) API surface;
llvm-mingw's UWP iphlpapi import library does not export it,
so UWP consumers of librist.a failed to link with "undefined
symbol: _GetAdaptersInfo". Replaced with GetAdaptersAddresses,
supported on classic Win32 desktop and on UWP since Windows 8.
Embedded Apple SDKs (iOS, tvOS, watchOS, visionOS, all archs):
- tun_darwin.c: build as stubs on non-macOS Apple platforms
The v0.2.12 TUN support was guarded by #ifdef __APPLE__, but
<sys/kern_control.h> and <net/if_utun.h> only ship with the
macOS SDK. Every non-macOS Apple cross-build failed with
"fatal error: 'sys/kern_control.h' file not found", and simply
gating off the file produced a later link error because
src/rist.c and src/rist-common.c call rist_tun_read /
rist_tun_write unconditionally. Pull in <TargetConditionals.h>,
restrict the utun implementation to
defined(__APPLE__) && TARGET_OS_OSX, and add a second
#elif defined(__APPLE__) branch with stub implementations of
the seven public rist_tun_* entry points returning -1, matching
the pattern src/tun_win.c already uses on Windows. Real utun
support on macOS is unchanged.
macOS used as a cross-build HOST:
- meson: skip Homebrew prefix lookup for cross-builds
The Homebrew fallback that adds /opt/homebrew or /usr/local
include paths was running unconditionally on any Darwin host.
Macs cross-compiling librist for iOS, tvOS, visionOS, Android,
or mingw could hang or fail at configure. Gated the lookup on
a native macOS build. Native-macOS-for-macOS builds are
unaffected.
Contributors:
- Sergio Ammirata
No source-file changes outside of meson.build, src/network.c, and
src/tun_darwin.c.
Changes for 0.2.12 (2026-04-17):
--------------------------------
ABI/API:
- ABI version 10:0:6 (soversion 4, binary-compatible with 0.2.11)
- API version 4.7.0
New Features:
- Cross-platform TUN support moved into the library
(rist_tun_open/close/read/write/set_ip/set_mtu/bring_up)
macOS (utun), Linux (/dev/net/tun), Windows (stub)
- data_fd forwarding API: rist_sender_data_fd_set,
rist_receiver_data_fd_set, rist_data_fd_stats_get
librist reads/writes directly to the fd, no application
code in the data path (works with sockets, FIFOs, TUN fds)
- New risttunnel tool: point-to-point IP tunnel over RIST
- YAML config file support for ristsender/ristreceiver with native parser
- Receiver session timeout API and --session-timeout CLI option
- Peer statistics in receiver output
- Multicast interface (miface) in sender peer statistics JSON
- avg_buffer_time field exposed in receiver_flow stats struct
- New udp2udp tool for UDP relay
- Profile selection (--profile) in rist2rist
- Sender input queue statistics
- MPEG-TS null packet suppression byte tracking
- Differentiated bitrate tracking and sender stats API
Bug Fixes:
- Fix sender queue full detection: replace wrap-unsafe index
arithmetic (which silently misfired on ring-buffer wraparound)
with a size-vs-max comparison. Prevents in-flight retransmit
buffers from being silently overwritten under sustained load.
- Fix OOB main-profile data delivery: rist_send_seq_rtcp no
longer strips the first 4 bytes of zero-header OOB packets,
and rist_oob_dequeue now iterates child peers when the OOB
buffer references a listener peer (matching main-channel
behaviour instead of failing silently).
- Fix receiver crash on configurable RTT multiplier path
- Fix bogus lost-packet counters
- Fix premature stream stop on ECHO timeout for single-path (closes #196)
- Fix multi-sender queue issues (static buffer_size moved to context)
- Thread-safe FIFO access in rist_receiver_data_read2
- Fix memory leaks (authentication failures, sender queue)
- Fix OpenMetrics response: add missing trailing EOF line
- Fix rist_client_flow_quality metric unit
- Fix OpenMetrics Content-Type header for Prometheus compatibility
- Fix RTT min/max invalid values
- Fix const correctness in URL parameter parsing
- Fix signed/unsigned comparison statements
- Fix missing strings.h include in yamlparse.c
- Fix clock used when processing RR with lsr older than last SR
- Fix -Wmisleading-indentation warning in receiver FIFO overflow path
- Fix tools build on macOS/Windows/FreeBSD: include endian-shim.h
for be16toh in ristsender.c and ristreceiver.c
- Fix tools build: always compile tunnel_interface/tun_mode into
rist_tools_config_object (consumers reference them unconditionally)
- Fix missing_counter bookkeeping: track queue length instead of
NACK-sent count. Previously the counter stayed at 0 on paths that
never emit NACKs (e.g. simple-profile multicast), causing the
missing_queue JSON stat to under-report and the congestion guard in
receiver_mark_missing to never trip.
- Fix build-only-platform-matching TUN source file; previously the
non-matching src/tun_*.c were compiled into an empty translation
unit and tripped -Werror=pedantic on Ubuntu/MinGW.
- Fix risttunnel build on MinGW: include <windows.h> for Sleep().
Improvements:
- Refactored sender queue management for better memory efficiency
- Improved UDP send reliability with EAGAIN retry
- Improved cname buffer safety
- Refactored flow timeout semantics
Build System:
- TUN support is now always compiled; removed the use_tun meson option
- CI: drop -Duse_tun=true flag from ubuntu build job
- Fix false positive mbedtls detection in meson build
- Use MbedTLS cmake config module
- Check brew exists before calling (macOS)
- Fix compiler warnings in aes.h (Steve Lhomme)
Acknowledgements:
- Dave Evans for RR clock fix
- Steve Lhomme for aes.h warning fixes
- Laur for the receiver FIFO misleading-indentation report
Changes for 0.2.11 (2024-11-15):
--------------------------------
New Features:
- Ephemeral listening ports support (add/remove ports after initialization)
- New function rist_sender_npd_get for null packet deletion status
- New function rist_peer_get_cname to extract peer's private cname
- Enhanced CI test application with packet integrity checks
Bug Fixes:
- Fixed potential socket deadlock condition under MS Windows
- Fixed ristsender confusion with multicast addresses sharing destination port
- Fixed compile warnings under different C++ versions
- Fixed theoretical IP checksum calculation issue
- Fixed ristreceiver closing when tun://@ used in output URL
- Fixed deadlock condition on FIFO overflow
- Fixed invalid verbose-level parameter handling
- Fixed binding to raw UDP socket
- Fixed memory allocation order in stats structures
- Fixed Mac homebrew arm64 build
- Fixed null packet deletion
- Fixed units on sender Prometheus stats
- Fixed keepalive packet data corruption bug
- Fixed improper RTP/RTCP classification (affected ST2110)
- Fixed flow timeout to follow session timeout
- Updated peer config timing_mode field handling
Build:
- Updated meson version to avoid deprecated items
Acknowledgements:
- Special thanks to Thierry Lelegard for null packet deletion bug fix
Changes for 0.2.10 (2023-10-30):
--------------------------------
- Development branch merge with accumulated fixes and improvements
Changes for 0.2.9 (2023-10-11):
-------------------------------
Bug Fixes:
- Fix compilation on 32-bit systems
- Fix regression in stats obj RTT value
- Remove unneeded locking in buffer scaling
- Fix "too old packages" error due to buffer scaling
- Fix empty buffer time check
- Disable buffer negotiation when sender max buffer < receiver buffer
- Fix deadlock caused by wrong lock order when removing peers
- Fix building Prometheus code against older libmicrohttpd
- Fix compilation on Hurd
Changes for 0.2.8 (2023-08-22):
-------------------------------
New Features:
- SRP (Secure Remote Password) passphrase exchange
- Passphrase changing support via U1 bit on M1/M2 packets
- Fully implemented passphrase changing on multicast
Bug Fixes:
- Add GRE check for bit 1 and GRE Ver set to 0
- Fix SIGSEGV when peer_rtcp pointer is null
Improvements:
- Refactored peer matching
- Refactored cryptographic randomness from SRP
- Added peer lock for thread safety
- Default PSK keysize to 256 if not set
Changes for 0.2.7 (2022-04-07):
-------------------------------
New Features:
- Multiplexing support
- TUN (tunnel) interface support
Changes for 0.2.6 (2021-07-22):
-------------------------------
Improvements:
- Move per-packet messages to debug level to prevent log flooding
- Release data earlier when real-time in buffer exceeds 1.1x buffer duration
- Drop stale packets older than 2x buffer duration
Changes for 0.2.5 (2021-07-20):
-------------------------------
- Maintenance release with minor fixes
Changes for 0.2.4 (2021-06-25):
-------------------------------
- Maintenance release with stability improvements
Changes for 0.2.3 (2021-06-22):
-------------------------------
- Maintenance release with bug fixes
Changes for 0.2.2 (2021-06-16):
-------------------------------
- Maintenance release
Changes for 0.2.1 (2021-06-10):
-------------------------------
- Stability fixes for initial public release
Changes for 0.2.0 (2021-05-12):
-------------------------------
Initial official public release of librist.
Core Features:
- Full support for VSF TR-06-1 (Simple Profile)
- VSF TR-06-2 (Main Profile):
* PSK Encryption (AES 128 and 256)
* Bidirectional connection initiation
* Multipath support (load balanced or redundant link bonding)
* One-to-many distribution (media server mode)
* Tested packet recovery up to 50% continuous loss
* Jitter-tolerant output timing
Tools:
- ristsender - RIST sender application
- ristreceiver - RIST receiver application
- rist2rist - RIST relay/proxy application
Platform Support:
- Linux, macOS, Windows, FreeBSD
- Cross-compilation support via meson
Project Information:
--------------------
Website: https://code.videolan.org/rist/librist
License: BSD 2-Clause "Simplified" License