{"base_revision":"git-v1:8d6525127f7f6c60dc833d919d2379f9d4727876","content":"diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml\nindex 018b1c0..5a79819 100644\n--- a/.gitlab-ci.yml\n+++ b/.gitlab-ci.yml\n@@ -37,11 +37,13 @@ build-ubuntu:\n build-win64:\n     extends: .debian-amd64-common\n     script:\n+        - wineserver -p && wine wineboot\n         - meson build --buildtype release\n                       --werror\n                       --libdir lib\n                       --cross-file common/ci/x86_64-w64-mingw32.meson\n                       -Ddefault_library=both\n+                      -Duse_mbedtls=false\n                       -Dhave_mingw_pthreads=true\n         - ninja -C build\n     artifacts:\n@@ -64,52 +66,11 @@ test-ubuntu:\n     extends:\n         - .ubuntu-amd64-jammy\n     needs: [\"build-ubuntu\"]\n-    # simple+multicast cases are environmentally flaky on the shared\n-    # runner (UDP/IGMP timing); retry covers transient failures.\n-    retry: 2\n     script:\n-        # cmocka comes from subprojects/cmocka.wrap when the runner image\n-        # does not ship libcmocka-dev; #215 used to slip silently because\n-        # the meson check skipped srp_unit_test entirely.\n         - meson build --buildtype release -Db_sanitize=address,undefined\n         - cd build && meson test --print-errorlogs\n     dependencies:\n         - build-ubuntu\n-    artifacts:\n-        when: always\n-        reports:\n-            junit: build/meson-logs/testlog.junit.xml\n-        paths:\n-            - build/meson-logs/testlog.txt\n-            - build/meson-logs/testlog.junit.xml\n-\n-# Run the cross-compiled Windows binaries (with mbedTLS, matching the\n-# release configuration) under wine so the same meson test suite that\n-# gates Linux also gates Windows. Catches Windows-runtime regressions\n-# like #208 (winsock not initialised before rist_logging_set) and the\n-# SRP/AES code paths under #210 that pure cross-compile cannot detect.\n-# Does not catch real-Windows kernel behaviours that wine does not\n-# replicate (e.g. the #209 WSAECONNRESET-from-ICMP-unreach interaction);\n-# a native Windows runner would be needed for that and is not available\n-# in VideoLAN's shared CI.\n-#\n-# TODO: allow_failure is set because wine on the shared CI runner is too\n-# slow to complete the send/receive tests within meson's 30s per-test\n-# budget — the same suite passes natively on Linux (test-ubuntu) and the\n-# logs show no crashes/asserts, only TIMEOUTs.  Either bump the per-test\n-# timeout multiplier here or move test-win64 to a faster runner, then\n-# drop allow_failure.\n-test-win64:\n-    stage: test\n-    extends: .debian-amd64-common\n-    needs: [\"build-win64\"]\n-    allow_failure: true\n-    retry: 2\n-    dependencies:\n-        - build-win64\n-    script:\n-        - wineserver -p && wine wineboot\n-        - cd build && meson test --print-errorlogs --no-rebuild\n     artifacts:\n         when: on_failure\n         paths:\ndiff --git a/NEWS b/NEWS\nindex ae91f14..84e74ce 100644\n--- a/NEWS\n+++ b/NEWS\n@@ -2,1465 +2,9 @@ librist - Reliable Internet Stream Transport\n ============================================\n \n A library implementing the Video Services Forum (VSF) Technical\n-Recommendations TR-06-1 (Simple Profile), TR-06-2 (Main Profile),\n-and TR-06-3 (Advanced Profile).\n+Recommendations TR-06-1 (Simple Profile) and TR-06-2 (Main Profile).\n \n \n-Changes for 0.2.20:\n----------------------------------\n-\n-ABI/API:\n-  - ABI version 16:1:12 (soversion 4, binary-compatible with 0.2.15,\n-    0.2.16, 0.2.17, 0.2.18, and 0.2.19)\n-  - API version 4.13.0 (unchanged)\n-\n-Bug Fixes:\n-  - ?multiplex-mode= and ?rtp-ptype= are accepted again on udp urls\n-    (regression in 0.2.19).  ristsender refused any url carrying either one,\n-    and ristsender and ristreceiver stopped applying the multiplex mode they\n-    were given.\n-\n-\n-Changes for 0.2.19:\n----------------------------------\n-\n-ABI/API:\n-  - ABI version 16:0:12 (soversion 4, binary-compatible with 0.2.15,\n-    0.2.16, 0.2.17, and 0.2.18)\n-  - API version 4.13.0 (new features, backwards-compatible)\n-\n-Features:\n-  - Receiver output can now be spaced at the stream's own rate instead of\n-    released in whatever batch the recovery buffer has made due. Intended for\n-    constant-bitrate streams feeding equipment that recovers a clock from\n-    arrival timing. Off by default. Set it with\n-    rist_receiver_set_cbr_output(), with ristreceiver's --cbr-output 0|1, or\n-    with cbr-output=1 on a udp/rtp output URL (rist_udp_config version 2).\n-    Pacing never releases a packet early and never holds one past its\n-    deadline, so enabling it cannot cost packets. The setting applies to the\n-    whole context rather than per output target, because pacing happens in the\n-    flow's output loop ahead of the fan-out and all outputs share one release\n-    schedule; a request for a different value is refused. Spacing is coarser\n-    on Windows builds without pthreads, whose condition wait cannot express\n-    sub-millisecond timeouts.\n-  - Advanced Profile receivers now dejitter off the sender's clock. The\n-    receive path reconstructs a monotonic source timeline from the 1 MHz\n-    RTP timestamp (TR-06-3 Section 5.2.1) and stamps packets with it instead\n-    of local arrival, so output spacing follows the source cadence like the\n-    Main profile. Reconstruction is wrap-safe (the 32-bit 1 MHz field rolls\n-    over about every 72 minutes). Because librist releases before this one\n-    emitted this timestamp on a broken clock, the receiver verifies the\n-    rebuilt clock over the first second and corrects to arrival timing for\n-    the rest of the session if it is not advancing at real time (an older,\n-    non-conformant sender). Set timing-mode=arrival to force arrival.\n-  - Bonded senders can auto-mute a failing leg by RTT. When a leg's smoothed\n-    RTT stays above ?rtt-drop=<ms> for ?rtt-drop-settle=<ms>, it is pulled out\n-    of the weighted payload rotation and its retransmissions are rerouted to a\n-    healthy leg, so the healthy leg(s) carry 100% of the stream and no traffic\n-    rides the slow path. It rejoins after RTT stays below ?rtt-restore=<ms> for\n-    a longer, conservative dwell (asymmetric hysteresis: quick to drop, slow to\n-    rejoin, so a still-marginal link cannot flap back in). The connection stays\n-    up and keeps probing RTT while muted; a low-rate redundant trickle\n-    (?rtt-drop-trickle=<N>, 1-in-N duplicate packets, on by default) keeps the\n-    muted leg measured so recovery is warm rather than a cold cut-over (set 0\n-    for a hard mute). The\n-    last healthy leg is never muted. Per-leg mute state (rtt_muted) and a\n-    cumulative mute-event counter (rtt_mute_events) are exposed in the sender\n-    stats JSON and the Prometheus exporter. Disabled by default (rtt-drop=0);\n-    no wire change.\n-  - EAP SHA256-SRP6a gains Version 4: the passphrase pushed over an\n-    authenticated SRP session is now carried under AES-256-GCM (authenticated\n-    encryption) instead of the v3 AES-CTR (confidentiality-only) form. Keys are\n-    per-direction (HKDF-Expand-SHA256 off the SRP session key), the 96-bit\n-    nonce is monotonic and in-packet, the EAP/SRP header is bound as AAD, and\n-    the receiver verifies the tag and accepts only strictly-increasing nonces\n-    before installing. v4 engages only when both peers advertise EAP version 4\n-    (a GCM backend is present); a v3/v2 peer falls back to AES-CTR unchanged.\n-  - TUN tools (risttunnel -l) and rist_tun_set_ip() now accept IPv6 local\n-    addresses with prefix lengths up to /128 (e.g. fd00::1/64). Linux uses\n-    rtnetlink RTM_NEWADDR; macOS uses in6_aliasreq. IPv4 behaviour is\n-    unchanged.\n-  - rist_oob_read() is now implemented.  When out-of-band data is enabled\n-    without a callback (rist_oob_callback_set with a NULL callback), incoming\n-    OOB packets are queued and can be drained by polling rist_oob_read(), as\n-    the public API already documented.  The previous stub returned success\n-    without writing the output pointer, which could crash a polling caller.\n-  - risttunnel gains a single-port mode (-1/--single-port): a bidirectional\n-    IP tunnel carried over one UDP port instead of two.  A single RIST\n-    connection runs the forward direction over the RTP data_fd path\n-    (ARQ-protected) and the reverse direction over the OOB channel on the\n-    same socket (best-effort, no ARQ).  The caller passes only -o, the\n-    listener only -b.  Because librist learns the OOB return peer only from\n-    received OOB, the listener captures the connecting peer through the\n-    auth/connect callback and hands it explicitly to rist_oob_write(), while\n-    the caller injects inbound OOB into its TUN through an OOB receive\n-    callback.  Intended for low-bandwidth, NAT-friendly control tunnels\n-    where the heavy direction is forward and the reverse is light.\n-\n-Security audit:\n-  - Tool and API memory-safety and hygiene. The YAML and SRP-verifier\n-    parsers no longer over-read or over-write on malformed input; the\n-    ipv4-mux receiver and the OOB API message handler reject short datagrams\n-    (were a SIZE_MAX send and an unbounded heap read), and the OOB API\n-    message is written at the correct byte offset; Prometheus label values\n-    taken from a peer's SDES cname are escaped (label injection); passphrases\n-    are no longer logged in clear; derived key material is wiped on every\n-    path and on key destroy (via a single shared helper); plus several\n-    NULL-guard, format-string, getopt-width and zero-init fixes.\n-  - Receiver and Advanced-profile hardening. A zero recovery buffer is\n-    rejected (was a config-time divide-by-zero SIGFPE); a failed null-packet\n-    expansion delivers the packet unexpanded instead of stale scratch (and no\n-    longer feeds SIZE_MAX into the ts-null bitrate stats); Main-profile NACK\n-    range records are capped per record and per packet (retry-queue DoS\n-    amplifier); the GRE H bit no longer rewrites a configured key size from\n-    unauthenticated wire data; state-changing Advanced control messages (NACK,\n-    RTT echo, flow attributes, PSK future nonce) are honored only after the\n-    handshake, and the keepalive capability flip is set-only; and the Advanced\n-    source-clock rate verdict is re-run each window instead of latching arrival\n-    timing for the whole session.\n-  - EAP/SRP pre-authentication hardening. Spoofed SERVER_KEY/VALIDATOR\n-    requests no longer crash an SRP caller on a NULL client context; EAPOL\n-    START/LOGOFF can no longer reset the brute-force try counter or revive a\n-    failed context; the passphrase dispatcher keys off the negotiated session\n-    version, so a v4 (AES-256-GCM) session refuses downgraded v3/derived-key\n-    frames; the retransmit cache is invalidated only by a matching identifier;\n-    a reset drops the armed passphrase retransmit to avoid GCM nonce reuse; a\n-    failed passphrase install is answered with FAILURE instead of a false\n-    SUCCESS; passphrase push is refused before authentication; wire usernames\n-    are sanitized before logging (CR/LF injection); and SRP modexp requests\n-    are rate-limited.\n-  - PSK key-state hardening. The Advanced future-nonce control message\n-    carried an unvalidated 16-bit key size that could drive PBKDF2 to write\n-    past the derived-key stack buffer; it is now rejected at every layer. The\n-    bad-decryption lockout no longer becomes a one-packet permanent key kill:\n-    a fresh nonce or passphrase clears it, a valid decrypt decays the strike\n-    counter, and zero-nonce packets are dropped without latching. The\n-    PBKDF2-flood protection is preserved as an abuse-triggered per-key rekey\n-    rate limit, so legitimate nonce churn (including a reflector's RTCP/media\n-    nonce interleave) is never throttled.\n-  - URL parsing: a malformed UDP URL no longer triggers an out-of-bounds\n-    write while extracting the scheme prefix.  A URL beginning with '/'\n-    underflowed the copy length and NUL-padded far past the 16-byte prefix\n-    buffer; a scheme longer than 15 characters wrote the terminator out of\n-    bounds.  Both the copy length and terminator index are now clamped.\n-    Reachable via rist_parse_udp_address()/rist_parse_udp_address2() and the\n-    ristsender/ristreceiver/udp2udp -i/-o URLs.  Covered by\n-    test/rist/unit/test_url_udp_prefix_parse.\n-  - URL parsing: building the Simple-profile RTCP peer address now uses a\n-    bounded formatter, so a long input URL cannot overflow the peer-config\n-    address buffer.\n-\n-Bug Fixes:\n-  - librist builds under MSVC again. Three independent breaks, two of which\n-    stopped the Visual Studio solution outright, so a Windows packaging build\n-    produced no library, no tools and no installer. The new constant-bitrate\n-    pacing counter is an atomic_uint_least64_t, a type the bundled MSVC\n-    stdatomic shim did not declare, which broke rist-private.h and every\n-    target including it. Separately the EAP authenticator-restart regression\n-    test pulled in <pthread.h> directly, which does not exist there; it now\n-    takes the mutex API from the Windows pthread-shim as the other tests do.\n-    A third, narrower break stopped only the socket-rebind test, which typed\n-    two sleep durations as useconds_t, a POSIX type MSVC does not provide.\n-    None of this affects compilers with a native stdatomic.h and real\n-    pthreads, which is every other supported toolchain.\n-  - librist now builds against nettle 4.0. The AES-CTR, SRP and EAP v4 code\n-    used three interfaces that 4.0 removed: the unified struct aes_ctx, and\n-    the length argument on the sha256, hmac-sha256 and gcm digest calls. A\n-    build configured for nettle (?use_nettle=true, which is what distributions\n-    and VLC's contrib select over the bundled mbedtls) failed to compile\n-    outright. nettle 3.x is still supported; the digest calls are selected on\n-    NETTLE_VERSION_MAJOR. No behaviour change on any backend.\n-  - CBR output pacing now spreads an arrival burst instead of passing most of\n-    it through. Enabling it used to buy almost nothing: on a 71 Mbps feed the\n-    output measured 92% of datagrams leaving back to back with pacing on,\n-    against 94% with it off. The output loop releases on a deadline that every\n-    packet of an arrival burst shares, so pacing has to hold the rest of the\n-    burst back, and three things stopped it. The hold that keeps pacing from\n-    aging a packet out of the recovery buffer was a fixed 1 ms, shorter than\n-    the time a burst takes to drain at the paced cadence (twelve datagrams at\n-    that rate need 1.8 ms), so packets past it re-anchored the schedule and\n-    left together; it now scales with the output jitter bound. A pass with\n-    nothing to send left the schedule running, banking due times in the past\n-    that the next arrivals spent as one back-to-back run; such a pass now\n-    re-anchors. The wake floor was a fixed 250 us against a 148 us datagram\n-    interval, so a wake found two datagrams due and released the pair; it now\n-    tracks half the measured interval, clamped to between 20 and 250 us, so a\n-    stream slow enough that the floor already sits under its interval keeps\n-    the old wake rate and the old cost. The standard deviation of the output\n-    spacing goes from 608 us unpaced to 20.4 us, and back-to-back from 94% to\n-    0.09%, for about two points more of a core on the receiver from the higher\n-    wake rate.\n-  - Advanced Profile senders no longer double-count the peer bitrate. The\n-    Advanced media path accounts p->bw when it builds and sends a packet,\n-    and the shared send path accounted it a second time, so the measured\n-    peer bitrate read about 2x the real wire rate. That inflated estimate\n-    fed the recovery_maxbitrate retransmit throttle, which could refuse\n-    reactive retransmits while the link was not actually saturated. The\n-    shared path now skips accounting for Advanced media it did not send.\n-  - A (re)connecting sender leg now sends its SDES the moment it authenticates,\n-    instead of waiting for the next periodic RTCP tick. The weighted balancer\n-    starts forwarding data on a leg as soon as EAP/SRP auth completes, but the\n-    receiver drops data on a leg until it has seen the SDES that binds the leg\n-    to the flow. On every reconnect the first data raced ahead of the SDES, so\n-    the receiver discarded roughly a full RTCP interval of packets (\"handshake\n-    still pending\"); this is most visible when a bonded leg flaps repeatedly.\n-    Emitting the SDES up front closes that window.\n-  - Bonded legs survive a brief stall without a full re-handshake, and the\n-    peer liveness timeout is now principled. A leg that went silent was declared\n-    dead after a hardcoded 250 ms and had to re-run EAP/SRP from cold on return,\n-    so on a flappy link every RTT spike past 250 ms self-inflicted a re-auth and\n-    the receiver dropped data on the leg until the handshake rebuilt. Two\n-    changes fix this. First, routing and session lifetime are split: the sender\n-    pulls a briefly-silent leg from the payload rotation within a few RTCP\n-    intervals (rerouting its share and its retransmissions to a healthy sibling),\n-    while the authenticated session is kept far longer. Second, the teardown is\n-    the larger of the configured session_timeout and twice the receiver buffer,\n-    and session_timeout now defaults to the documented RIST_DEFAULT_SESSION_TIMEOUT\n-    (2000 ms) with a floor of a few RTCP intervals, instead of the old 250 ms; a\n-    leg that returns within that window resumes on the existing session with no\n-    re-auth. The last leg with a live return path is never pulled, so a\n-    single-path stream is never cut. Because a leg can be pulled either for\n-    high RTT or for silence, the two are reconciled so they never pull every\n-    leg at once: a leg is muted only while another stays a carrier, and the\n-    balancer never drops data while a usable leg remains.\n-  - Senders now really do hold a silent peer for one recovery buffer window\n-    before dropping it, which is what the fix for #196 was meant to do. A peer\n-    is declared dead when its return path goes quiet, which says nothing about\n-    the forward path, so cutting a single-path stream on a late RTCP reply\n-    strands a receiver that is still fine. Two defects made the grace period\n-    inert. The window is the peer's recovery buffer, and that was only ever\n-    computed for receive-side peers, leaving it at zero on every sender. The\n-    four places that egress payload then tested it backwards, sending to a dead\n-    peer only once the window had already run out -- and the gate above them had\n-    already dropped the peer by then, so the window was the one interval in\n-    which they stayed silent. With a 4 s buffer a single-path sender went quiet\n-    3 s after the return path died; it now keeps sending for 13 s (twice the\n-    buffer before the peer is declared dead, then the buffer again as grace).\n-    Bonded legs are unaffected: a silent leg is pulled from the rotation by the\n-    stall check long before it is ever declared dead. Populating the buffer on\n-    send-side peers also makes the sender's liveness timeout match the\n-    receiver's, instead of always falling back to session_timeout and tearing\n-    the session down first.\n-  - RTT auto-mute no longer ping-pongs the stream between two failing legs.\n-    When every bonded leg was over the ceiling, the leg elected to keep\n-    carrying was re-picked from the instantaneous RTT on every tick, so with\n-    both legs oscillating the payload path swapped about once a second --\n-    worse for the stream than committing to either leg, and the visible\n-    remaining glitch on bonded cellular. The role is now sticky: the elected\n-    leg holds it until it recovers, goes silent, or a sibling measures several\n-    times better after the incumbent has served at least one drop dwell. The\n-    mute log also no longer claims the ceiling was crossed when a leg was\n-    muted while serving out its rejoin dwell.\n-  - A leg rejoining after an RTT mute ramps its share back up instead of\n-    resuming at full weight. A muted leg carries almost nothing, so its queue\n-    drains and it measures well right until it is loaded again; handing back\n-    the full share at once refilled the queue and re-muted it seconds later,\n-    which self-oscillated for as long as the link stayed marginal. The share\n-    now rises linearly over the rejoin dwell, so the leg reveals congestion\n-    under a growing load and settles at whatever it can actually carry.\n-  - Senders now report a bandwidth ceiling that leaves no room for\n-    retransmissions. ?bandwidth= (recovery_maxbitrate) caps payload and\n-    retransmissions together rather than payload alone, so a ceiling at or\n-    below the payload rate refuses every NACK for the life of the stream.\n-    The refusal was logged at debug level only, so what an operator saw was\n-    a link that never recovered a packet, which reads as a network fault\n-    rather than a configuration one. A warning now repeats every 30 seconds\n-    while the condition holds, giving the payload rate, the ceiling and the\n-    number of refusals in the interval.\n-  - A recovery_maxbitrate of 0 set through the C API no longer turns recovery\n-    off. The value looks like \"no ceiling\" and is what an application reaches\n-    for when it does not want retransmissions throttled, but the sender read it\n-    as a ceiling of zero and refused every retransmission for the life of the\n-    stream, saying so at debug level only. A 0 is now read as unset: it is\n-    replaced with the 100000 kbps default and warned about, which is how\n-    ?bandwidth=0 on a URL already behaved. Applications that passed 0 meaning\n-    \"unlimited\" get the default rather than a stream with no recovery.\n-  - Receivers no longer rebuild a flow's baseline on a stale packet. When the\n-    recovery buffer empties, the next packet in sets the flow's clock offset and\n-    sequence baseline, and any packet would do. A bonded leg queued seconds deep\n-    keeps delivering packets that old, so on a reset it could win the race and\n-    rewind the baseline by its whole lag: the next packet from the healthy leg\n-    then looked like a multi-second jump, which emptied the buffer again. Bonded\n-    runs showed this repeat until the flow was declared dead, rewinding 12000\n-    sequence numbers and 15 seconds of clock in one step. A packet further behind\n-    the flow's high-water mark than the buffer is deep cannot be output anyway,\n-    so it is no longer taken as a baseline while a current one may still arrive.\n-    The wait is capped at one buffer window, so a flow left with nothing but a\n-    lagged leg, or a source that restarted on a lower clock, still recovers.\n-  - The trickle on an RTT-muted bonded leg now stops once the leg is queued\n-    deeper than the receiver's buffer. Past that point nothing sent on it can\n-    arrive in time to be output, and the arrivals are the stale baseline\n-    candidates described above. RTCP keeps measuring the leg's RTT, and with no\n-    payload queued behind them those measurements become honest, so the leg is\n-    still retried; legs within the buffer window trickle as before.\n-  - Receiver loss statistics no longer report impossible spikes. The output\n-    path counted a lost packet for every empty slot it stepped over in the\n-    recovery ring, which only matches the real loss while the reader sits\n-    right behind the data; after an idle source or a buffer re-anchor the\n-    walk spans far more slots than there are missing packets. A clean\n-    29-minute bonded run reported 40600 packets lost in a one-second window\n-    whose sequence numbers showed no gap at all, and 12% cumulative loss on\n-    a stream that never visibly glitched. Loss is now taken from the\n-    sequence gap, the same measure the nack path already used, so the\n-    reported and recovered counts agree.\n-  - Advanced Profile RTT echo replies now feed the smoothed RTT. The reply\n-    handler recorded the measurement as the peer's last RTT but never folded it\n-    into the 8-tap average, nor mirrored it onto the data leg where the balancer\n-    and the stats read it. The value was therefore discarded, and any setup that\n-    probes RTT only over the Advanced echo would leave the smoothed RTT stuck at\n-    its seed -- blinding RTT auto-mute and the retransmission timers. In\n-    practice the Main RTCP receiver reports still refreshed it, so this was\n-    latent rather than user-visible. The smoothing rule now lives in one place\n-    shared by every RTT source.\n-  - Advanced Profile senders now emit a spec-conformant RTP timestamp. The\n-    1 MHz timestamp (TR-06-3 Section 5.2.1) was built with a >> 16 shift that\n-    ran the clock 65536x too fast and wrapped every ~65 ms; the sender now\n-    writes the correct microsecond value on both media data and control\n-    messages. No librist receiver uses this field (the receive path clocks\n-    off packet arrival), so only the on-wire value changes; it matters for\n-    third-party TR-06-3 receivers that read it.\n-  - Public peer getters no longer crash on NULL arguments (issue #226).\n-    rist_peer_get_cname() returns 0 when the output pointer is NULL (it\n-    already guarded the peer), rist_peer_get_id() returns 0 on a NULL peer,\n-    and rist_peer_get_socket() checks the peer before dereferencing it.\n-    rist_peer_get_cname() also bounds the returned length with the actual\n-    cname field size rather than a separate, coincidentally-equal constant.\n-  - EAP-SRP: a bonded caller-sender leg that goes silent and then resumes on\n-    the same source tuple (an interface flap or upstream outage that comes\n-    back without a reconnect) no longer stays wedged (issue #224).  While the\n-    leg was silent the far-end authenticator timed out and purged its session,\n-    but the caller kept its still-valid miface-bound socket and stayed in EAP\n-    SUCCESS, so it streamed data the authenticator now dropped while waiting\n-    for an EAPOL START the caller never sent; the leg never rejoined the bond\n-    without an operator restart.  A caller-sender leg silent past\n-    session_timeout now resets EAP and re-runs the SRP handshake on its\n-    existing socket; once it re-authenticates it is folded back into the\n-    weighted balancing rotation, so it resumes carrying its full share of the\n-    bond on its own instead of lingering as an authenticated-but-idle leg.\n-  - EAP-SRP: a caller now re-authenticates on its own after the authenticator\n-    (listener/hub) restarts.  A restarted authenticator loses all session state\n-    and re-challenges previously-authenticated callers with EAP-Request/Identity,\n-    but a caller parked in EAP SUCCESS refused that request and a freshly\n-    restarted authenticator ignored the caller's recovery START, so the peer sat\n-    in \"Waiting for EAP authentication\" and carried no data until an operator\n-    restarted the caller.  A persistent identity request while authenticated now\n-    drives one bounded re-run of the SRP handshake, and the authenticator\n-    re-challenges on every START, so the session recovers without intervention.\n-  - Receiver: the \"Too many old packets, resetting buffer\" error no longer\n-    repeats on every output cycle on degraded or bonded links.  The flow reset\n-    that emits it now clears its internal trigger, so a sustained run of late\n-    packets logs at most one message per reset instead of flooding the log.\n-  - recovery-depth: requesting the maximum depth no longer fails on 32-bit\n-    platforms.  The effective maximum is capped to what the platform can\n-    address (still 16 on 64-bit), with a log message when a request is capped;\n-    this also fixes the recovery_depth unit test on 32-bit builds.\n-  - Advanced<->Main retransmissions are now resolved correctly.  An\n-    Advanced-profile sender indexes its retransmit buffer by the 32-bit\n-    advanced sequence, but a peer that negotiated down to Main (TR-06-3: an\n-    Advanced device starts each peer in Main and only frames Advanced once\n-    the peer advertises I=1) requests retransmits with a 16-bit RTP sequence\n-    and nack_seq_msb = 0.  Those two sequence spaces are independent, so the\n-    Main peer's NACK never matched the 32-bit index and nothing was\n-    retransmitted.  The Advanced sender now keeps a parallel 16-bit RTP\n-    retransmit index and resolves, validates, and frames a downgraded-Main\n-    peer's retransmits in the RTP domain.  Advanced<->Advanced behaviour is\n-    unchanged.\n-  - risttunnel: fix an intermittent SIGSEGV at startup caused by an\n-    uninitialised logging-settings pointer.  risttunnel now points log_ptr\n-    at the static logging settings like the other tools, so rist_logging_set()\n-    configures them in place instead of writing through a garbage pointer.\n-    As a side effect the -v loglevel now also applies to risttunnel's own\n-    messages.\n-  - risttunnel now presents SRP credentials on its sender/client legs.\n-    Only the listener (-F verifier file) enabled EAP-SRP before, so a\n-    risttunnel pointed at an SRP-protected listener failed with\n-    \"credentials have not been configured\" while the listener waited\n-    forever for authentication.  When the URL carries both an SRP\n-    username and password, risttunnel now calls rist_enable_eap_srp_2()\n-    on the peer like ristsender/ristreceiver/rist2rist; the receiver leg\n-    falls back to the verifier file when no credentials are supplied.\n-  - Fixed a use-after-free in rist_oob_dequeue.  rist_oob_write() stashes\n-    the destination peer from a caller thread and the protocol thread\n-    sends it later; a peer torn down by a NAT rebind or session timeout\n-    in between (e.g. the child peer the risttunnel --single-port reverse\n-    path captures via the auth/connect callback) left the send path\n-    dereferencing a freed peer - an intermittent SIGSEGV.  The dequeue\n-    now takes peerlist_lock, verifies the stashed peer is still on the\n-    live peer list, drops the packet if it is gone, and holds the lock\n-    across the whole send so a concurrent add/remove cannot free the\n-    peer underneath it.\n-  - rist_receiver_data_block_free2() is now a no-op on a NULL block,\n-    like free(NULL).  The receiver data_fd/tun delivery path can\n-    legitimately hold a NULL block by the time the fifo-overflow branch\n-    frees it: free_data_block() nulls the pointer after a successful\n-    free, and merged FEC pairs never allocate one.  The previous\n-    unconditional (*block)->ref dereference therefore read offset 0x40\n-    of a NULL pointer and crashed (observed as \"segfault at 40\") on\n-    listeners running risttunnel under abrupt link changes.  The guard\n-    protects every caller, not just the data_fd path.\n-  - Advanced receiver: fix a spurious \"Rist data out fifo queue overflow\"\n-    (issue #218) when an Advanced flow upgrades from Main to Advanced wire\n-    framing mid-stream.  The two framings carry source_time in different\n-    timestamp domains, so the framing switch resets the flow timing\n-    baseline; that reset re-derived time_offset but left the clock-drift\n-    sample buffer populated with stale Main-domain samples.  The next\n-    median recalculation then jumped the offset by several seconds and\n-    released the whole receiver buffer at once, overflowing the data-out\n-    fifo.  The baseline reset now also clears the drift samples, matching\n-    the existing clock-wrap reset.  This was most visible as reproducible\n-    failures of the advanced+unicast+client tests on slower build hosts.\n-  - Build: tools/ristsender.c no longer trips -Woverlength-strings\n-    (issue #219).  Adding --blind-send to the usage text pushed the help\n-    string past the 4095-character C99 string-literal limit; it is now\n-    split into two literals printed back to back (output unchanged).\n-  - Build: fix compilation with SRP support disabled (issue #220).  The\n-    listener cname re-association added an unguarded eap_is_authenticated()\n-    call whose declaration lives under HAVE_SRP_SUPPORT; the call is now\n-    guarded, and without SRP the re-association safely refuses (the cname\n-    is not a per-peer secret, so it must not migrate a peer unauthenticated).\n-  - Sender stats reported tiny \"packets_sent\"/\"packets_received\" deltas\n-    (issue #221) when the structured callback was registered with\n-    rist_stats_callback_set().  That setter updated the common stats interval\n-    but not the sender's own copy, which the sender loop actually reads, so it\n-    stayed 0 and the callback fired every loop iteration instead of once per\n-    interval; each report then covered only one pass.  The setter now also\n-    sets the sender interval (matching rist_sender_stats_callback_set), and\n-    the sender loop reads the interval fresh each pass and skips reporting\n-    entirely when it is 0, so a stats-disabled sender no longer builds a\n-    stats object every iteration.  Affected risttunnel's sender stats; the\n-    bundled Prometheus path (ristsender) uses the legacy setter and was not.\n-  - udpsocket: size the socket buffers toward the OS maximum instead of a\n-    fixed 1 MB.  The helpers request up to 8 MB, which the kernel clamps to\n-    net.core.{r,w}mem_max, so a listener absorbs a burst of simultaneous\n-    connections without the kernel dropping the overflowing handshake\n-    datagrams (seen as Udp: RcvbufErrors) while the single protocol thread\n-    drains the socket.  Hosts with a small rmem_max fall back to the prior\n-    1 MB / 200 KB behaviour and are unaffected.\n-  - EAP-SRP: a single-port/NAT'd caller whose listener restarts now recovers on\n-    its own.  Previously SRP callers were excluded from the silent caller socket\n-    rebind, so once the listener lost all session state the caller sat idle\n-    until an operator restarted it.  The caller now resets its EAP context and\n-    re-authenticates on a fresh socket; the supplicant also retransmits its\n-    EAPOL START while the far end is silent, so a restarted listener that missed\n-    the first START still challenges it.  The rebind backoff resets once a peer\n-    re-authenticates, so a later outage retries promptly instead of after an\n-    ever-growing gap.  Covered by the srp mode of\n-    test/rist/test_caller_socket_rebind.\n-\n-CI / Test Coverage:\n-  - The reflector test no longer fails intermittently on slow or heavily\n-    loaded build machines.  It asserted RTT and jitter-buffer convergence\n-    behind fixed settling and timeout deadlines that a loaded builder could\n-    miss, causing spurious FTBFS that cleared on retry.  It now polls for the\n-    actual convergence conditions with generous caps and a longer test\n-    timeout; the assertions are unchanged.\n-\n-\n-Changes for 0.2.18:\n----------------------------------\n-\n-ABI/API:\n-  - ABI version 15:0:11 (soversion 4, binary-compatible with 0.2.15,\n-    0.2.16, and 0.2.17)\n-  - API version 4.12.0 (new features, backwards-compatible)\n-  - RIST_PEER_CONFIG_VERSION bumped from 2 to 3 (one new field:\n-    srp_compat_legacy).  Existing zero-initialised peer configs\n-    continue to work; the new field defaults to 0 (spec-compliant).\n-  - RIST_PEER_CONFIG_VERSION bumped from 3 to 4 (two new fields:\n-    profile + profile_set, populated by the ?profile= URL parameter).\n-    rist_peer_config_defaults_set() initialises both to safe defaults\n-    (profile = RIST_DEFAULT_PROFILE, profile_set = 0).  Existing\n-    zero-initialised configs keep their pre-bump behaviour.\n-  - New rist_peer_config_defaults_set_versioned(cfg, version) initialises\n-    only the fields defined at or below the given RIST_PEER_CONFIG_VERSION.\n-    rist_peer_config_defaults_set() is now a static inline wrapper (peer.h)\n-    that forwards the caller's compiled RIST_PEER_CONFIG_VERSION, so a\n-    recompiled caller initialises exactly the fields its struct contains.\n-    The exported rist_peer_config_defaults_set() symbol is retained for\n-    binaries linked against an earlier library; it assumes version 0 and\n-    sets only the baseline fields present in every struct revision.  Such\n-    binaries must zero-initialise their config before the call: fields\n-    added in version 1+ (split_mode, merge_mode, profile, profile_set)\n-    are left untouched.\n-  - RIST_PEER_CONFIG_VERSION bumped from 4 to 5 (two new fields:\n-    recovery_priority, populated by the ?recovery-priority= URL\n-    parameter, and recovery_depth, populated by ?recovery-depth=).\n-    rist_peer_config_defaults_set_versioned() initialises them for\n-    callers compiled against version 5 or later (recovery_priority = 0,\n-    recovery_depth = RIST_RECOVERY_DEPTH_DEFAULT); zero-initialised\n-    configs and callers built against an earlier version keep the legacy\n-    NACK-routing behaviour and the default recovery ring.\n-  - New public API rist_recovery_depth_set(ctx, depth) and the\n-    RIST_RECOVERY_DEPTH_MIN/DEFAULT/MAX macros (peer.h) size the\n-    Advanced-profile retransmission ring.  depth is a base-2 exponent:\n-    the ring holds 65536 << depth packets (default 3 = 8x = the prior\n-    fixed size, range 0..16).  Must be called before rist_start().\n-  - New public macro RIST_PEER_WEIGHT_DUPLICATE (0) names the\n-    rist_peer_config.weight sentinel that selects packet duplication\n-    instead of weighted load-balancing.  Value unchanged.\n-  - RIST_STATS_VERSION bumped from 2 to 3 and the JSON stats\n-    schema_version from 3 to 4 (additive): rist_stats_sender_peer gains\n-    profile + advanced_active, rist_stats_receiver_flow gains profile +\n-    seq_bits + advanced_active.  Consumers reading only earlier fields are\n-    unaffected.\n-\n-Features:\n-  - Per-peer retransmission routing: the new ?recovery-priority=<n>\n-    peer URL parameter (rist_peer_config.recovery_priority) lets a\n-    receiver direct NACKs to a chosen peer when a flow is carried by\n-    more than one RTCP-capable peer.  Each NACK group is sent to the\n-    eligible peer with the highest recovery_priority, ties broken by\n-    lowest RTT.  The default of 0 on every peer preserves the prior\n-    lowest-RTT selection.  Useful when the lowest-RTT peer cannot\n-    answer NACKs (e.g. a duplicate/relay feed with no retransmit\n-    buffer) while a higher-RTT peer holds the retransmission buffer.\n-  - Tunable Advanced-profile recovery depth: the recovery rings are now\n-    heap-allocated and profile-sized, so Simple/Main flows allocate only\n-    their 16-bit ring (65536 packets) instead of the Advanced footprint.\n-    The Advanced ring size is selectable via ?recovery-depth=N (or\n-    rist_recovery_depth_set()), where N is a base-2 exponent and the ring\n-    holds 65536 << N packets.  The default of 3 (524288 packets) matches\n-    the previous fixed size; higher values trade RAM for a deeper NACK\n-    window.  Simple/Main ignore the knob.  A config-time warning is\n-    emitted when a peer's bitrate and buffer settings would exceed the\n-    recovery ring's addressable window.\n-  - Advanced/Main profile interoperability (TR-06-3 Section 9): an\n-    Advanced sender starts in Main framing and upgrades a peer to\n-    Advanced framing only after that peer advertises Advanced capability,\n-    so a Main-only receiver can still decode the stream.  An Advanced\n-    receiver accepts a Main-framed source and follows a mid-stream\n-    Main->Advanced upgrade.  Matched Advanced and matched Main pairs are\n-    unchanged.\n-  - The default RIST profile is now Advanced (RIST_DEFAULT_PROFILE, and\n-    the ristsender/ristreceiver/YAML defaults that follow it), changed\n-    from Main.  Thanks to the interop above, a default Advanced endpoint\n-    negotiates Advanced framing with an Advanced peer and falls back to\n-    Main framing with a Main-only peer, so existing Main deployments keep\n-    working.  Pass -p 1 (or profile: 1 in YAML) to force Main as before.\n-  - risttunnel now defaults to RIST_DEFAULT_PROFILE like the other tools,\n-    instead of remaining hardcoded to Main, completing the default-profile\n-    sweep.\n-  - Advanced-profile visibility in statistics: the binary stats API and the\n-    JSON stats now report each flow's configured profile and, on the\n-    receiver, whether Advanced framing is actually active and the sequence\n-    width in use.  The Prometheus exporter adds rist_client_flow_info /\n-    rist_sender_peer_info series carrying these labels, so a dashboard can\n-    tell at a glance when it is monitoring an Advanced stream.\n-\n-Bug Fixes:\n-  - Advanced profile: a sequence gap larger than 65535 is no longer\n-    truncated to 16 bits, so a real >64k loss on a 32-bit flow is recovered\n-    instead of being mis-detected as a tiny gap.\n-  - Advanced profile: receiver_mark_missing() now masks the sequence into the\n-    retransmission ring index.  A 32-bit sequence eventually exceeds the ring\n-    size; the old raw index ran off the array and could crash the receiver on\n-    a long-running Advanced flow.\n-  - The expected-next-sequence wrap mask no longer forces the value even (it\n-    masked with UINT16_MAX-1), which mis-computed every odd successor on\n-    16-bit flows.\n-  - Advanced byte accounting now counts delivered payload only, matching the\n-    Simple/Main convention, so received-byte and bitrate stats are consistent\n-    across profiles.\n-  - Duplicate detection compares the sequence number rather than the source\n-    timestamp, fixing false duplicate decisions on Advanced arrival-stamped\n-    packets.\n-  - Merge-mode pairing computes the successor sequence in a 32-bit-safe way,\n-    so paired packets are matched correctly on Advanced flows.\n-  - Advanced Profile (TR-06-3) RTT Echo Responses inflated the measured\n-    round-trip time by a constant factor.  The sender will not retransmit\n-    a packet again within one RTT of the previous attempt, so the inflated\n-    value made it ignore the repeat NACKs needed when a retransmission is\n-    itself lost, leaving packets unrecovered under sustained loss.  The\n-    Advanced echo now uses calculate_rtt_delay(), as the Simple and Main\n-    profiles do.\n-  - Fixed a receiver abort when a retransmitted or gap-filled packet's\n-    arrival time is interpolated.  With peers whose clocks are not\n-    synchronised the estimate could fall outside its neighbouring queue\n-    slots, tripping an assert in debug builds or misordering the queue\n-    otherwise.  Interpolation is now bounds-checked, rounded without\n-    overshoot, wrap/reorder-aware, and clamped to the neighbour interval.\n-  - NAT source-port rebind recovery (issue #188): when a calling\n-    receiver's external source port changes (NAT mapping rebind, DHCP\n-    renewal), the listening sender no longer treats it as a brand-new\n-    caller that leaves the old peer record orphaned until session\n-    timeout.  Listener-side re-association requires an authenticated\n-    per-peer SRP session: once a new child completes SRP authentication\n-    and learns its cname from an RTCP SDES, its source tuple is migrated\n-    into the matching silent sibling.  This path is SRP-only because the\n-    cname alone is not a per-peer secret under a shared PSK.  Plaintext\n-    and shared-PSK callers recover on the caller side instead: a\n-    caller-mode receiver on a MAIN+ profile with no pinned local port\n-    rebinds its UDP socket to a fresh ephemeral port on session timeout\n-    and restarts the handshake instead of giving up.\n-  - ?profile=0|1|2 in a peer URL (0=Simple, 1=Main, 2=Advanced) now\n-    overrides the wire profile of the context that owns the peer.\n-    rist_parse_address2 parses the value into rist_peer_config; on the\n-    first matching peer added before rist_start(), rist_peer_create\n-    mutates the context profile and lazily initialises the Advanced\n-    profile state when upgrading to Advanced.  Subsequent peers must\n-    request the same profile (or omit the parameter); rist_peer_create\n-    returns -1 with a WARN once the context profile is locked (after\n-    rist_start() or a prior peer fixed it).  Any consumer that passes\n-    URLs through rist_parse_address2 + rist_peer_create now gets the\n-    URL-driven profile selection with no further integration work.\n-  - Fixed a use-after-free in the global logger when an application frees\n-    its rist_logging_settings, either directly via\n-    rist_logging_settings_free2() or by destroying a context that embeds\n-    them (as FFmpeg does), while the global logger still references that\n-    callback and argument.  The global logging settings are now cleared\n-    when the matching settings are freed, so logs emitted afterwards (for\n-    example from background threads on a rapid close/reopen) no longer\n-    call into freed memory.\n-  - rist_peer_config_defaults_set() no longer writes past the end of a\n-    peer config supplied by a binary compiled against an older\n-    RIST_PEER_CONFIG_VERSION, and parse_url_options() / store_peer_settings()\n-    no longer touch version-specific fields unless peer_config->version is\n-    high enough: reflector, multicast_ttl, multicast_source and local_port\n-    require version >= 2; srp_compat_legacy requires version >= 3.\n-  - Windows XP support restored: rist_transport_poll() now uses\n-    select() via the existing contrib/poll_win.c shim instead of\n-    WSAPoll() (Vista+ only).  Regression introduced in 0.2.17.\n-  - rist_peer_recv() now accepts both canonical numeric EAGAIN errno\n-    values (11 and 35) when a custom transport_ops backend signals\n-    \"no data ready\", protecting cross-compile environments where the\n-    EAGAIN macro can resolve differently across translation units.\n-  - librist/transport.h now defines ssize_t on Windows (#214), so\n-    downstream consumers no longer have to provide their own typedef.\n-  - Prometheus exporter: counter rows in multiple_metric_datapoints\n-    mode now have a space between the value and the timestamp.\n-    Affected every existing _total row.\n-  - Prometheus exporter: in single_stat_point mode, the displayed\n-    value no longer freezes at the first callback and silently\n-    drops every subsequent sample.  Affected all receiver-flow,\n-    sender-peer, and per-peer-receiver gauges.\n-  - Receiver flow stats: bitrate, bitrate_retries, bitrate_rejected\n-    and bitrate_ts_nulls now decay to zero when the corresponding\n-    traffic stops, instead of remaining frozen at the last value\n-    computed while traffic was still flowing (#217).\n-\n-SRP interoperability:\n-  - 0.2.16 made TLS-SRP RFC 5054 / TR-06-2 compliant by adding the\n-    PAD operation to the u and k hash inputs.  As a side effect,\n-    SRP-authenticated peers running 0.2.15 or earlier (which omitted\n-    the PAD) will not handshake with 0.2.16+.  Upgrading both ends\n-    to 0.2.16+ remains the recommended fix.\n-  - New URL parameter for transitional interop:\n-        ?srp-compat=1\n-    On both ends of the SRP exchange, this opts the SRP wire format\n-    back into the pre-0.2.16 unpadded form.  Must be set on BOTH\n-    sides; mixed configuration will still fail.  An explicit WARN\n-    is logged on startup when the option is active.  Intended only\n-    as an escape hatch while older deployments are upgraded.\n-  - When an SRP handshake fails, the authenticator (and the client\n-    on M2 mismatch) now emits a single INFO hint pointing at the\n-    srp-compat=1 escape hatch.  The hint is advisory only -\n-    the SRP-6a transcript binds M1 to the authenticator's wire-mode\n-    B, so there is no purely-local way to distinguish \"wrong\n-    password\" from \"peer is on the other wire format\" once the\n-    handshake has reached M1.  No action required for non-SRP\n-    deployments.\n-\n-CI / Test Coverage:\n-  - srp_unit_test now runs in test-ubuntu.  cmocka is sourced via a\n-    new subprojects/cmocka.wrap (WrapDB 1.1.8-1) so the test builds\n-    even on runner images that do not ship libcmocka-dev, closing the\n-    silent-skip gap that hid the SRP fixture drift reported in #215.\n-    Test fixtures regenerated for the PAD-compliant SRP exchange\n-    against the 2048-bit RFC 5054 group.  Added four end-to-end tests\n-    covering both PAD and legacy modes plus the two cross-mode failure\n-    cases.\n-  - The issue #188 NAT/socket-rebind and multipath cname integration\n-    tests now compile under MSVC: they use the Windows pthread-shim\n-    (runtime mutex initialisation and PTHREAD_START_FUNC thread entry\n-    points) instead of a direct <pthread.h> dependency.\n-\n-Tools:\n-  - ristsender: new --blind-send option.  Decouples the sender from\n-    RIST peer state - multicast input groups are joined immediately\n-    and incoming UDP data is written to the RIST context regardless\n-    of whether any output peer has connected.  Intended for\n-    fire-and-forget broadcast topologies (multicast output in\n-    simple/main/advanced profile, or ristsender used as a relay /\n-    fan-out in front of non-RIST consumers).\n-  - Prometheus exporter: new per-peer receiver metric block\n-    rist_receiver_peer_* with labels {peer_id, peer_url, flow_id,\n-    receiver_id}, mirroring rist_sender_peer_*.  Auto-registered\n-    from rist_stats_receiver_flow.peers[].\n-  - Prometheus exporter: new client-flow gauge\n-    rist_client_flow_avg_buffer_time_seconds reporting the dynamic\n-    receiver buffer fill level (sourced from avg_buffer_time in\n-    rist_stats_receiver_flow, converted from ms to seconds).\n-  - Prometheus exporter: now also exports receiver counters that were\n-    previously JSON-only: retries, dropped-late, dropped-full, duplicates,\n-    and the NACK-depth recovery buckets (recovered after two / three / four\n-    / more NACKs).\n-\n-\n-Changes for 0.2.17:\n----------------------------------\n-\n-ABI/API:\n-  - ABI version 12:0:8 (soversion 4, binary-compatible with 0.2.15)\n-  - API version 4.9.0 (new features, backwards-compatible)\n-  - RIST_STATS_VERSION bumped from 1 to 2 (new byte-counter fields\n-    appended to rist_stats_sender_peer, rist_stats_receiver_peer,\n-    and rist_stats_receiver_flow).\n-  - RIST_STATS_JSON_SCHEMA_VERSION bumped from 2 to 3 (new fields:\n-    sent_bytes, retransmitted_bytes, received_bytes).\n-  - RIST_PEER_CONFIG_VERSION bumped from 1 to 2 (new fields:\n-    multicast_ttl, multicast_source, local_port).\n-\n-New Features:\n-\n-  Pluggable Transport Abstraction (transport.h):\n-  - New rist_transport_ops vtable replaces direct socket syscalls with\n-    user-supplied callbacks (sendto, recvfrom, socket, bind, close,\n-    setsockopt, poll).  Enables librist on non-POSIX platforms such as\n-    WebAssembly/WebTransport, userspace network stacks, or unit-test\n-    harnesses.\n-  - New rist_transport_set() installs custom transport ops on a context.\n-  - Default ops delegate to real syscalls so existing behaviour is\n-    unchanged.\n-\n-  Packet Split / Merge (peer.h):\n-  - Sender: split each application write into an even/odd sequence pair\n-    when the payload exceeds half the path MTU.  Controlled by\n-    split_mode in rist_peer_config (off, auto, half).\n-  - Receiver: transparently merge split pairs before delivery.\n-    Controlled by merge_mode in rist_peer_config (off, pairs, auto).\n-  - Keepalive advertises split capability via the L bit; receiver\n-    auto-detects when the sender supports it.\n-  - URL parameters: ?split=off|auto|half, ?merge=off|pairs|auto.\n-  - Split/merge counters surfaced in JSON statistics.\n-  - risttunnel auto-enables split+merge when MTU < 1400.\n-\n-  Advanced Profile (VSF TR-06-3):\n-  - Wire format and protocol integration for the Advanced Profile as\n-    defined by VSF TR-06-3.\n-  - Native control plane: Keep-Alive with I bit (CI=0x8000), RTT Echo\n-    Request/Response (CI=0x0010/0x0011), NACK Bitmask (CI=0x0000),\n-    NACK Range (CI=0x0001), Unsupported Response (CI=0x8020).\n-  - Full 32-bit sequence number indexing (expanded seq_index array).\n-  - Type 8 GRE-over-AP encapsulation for Section 9 interoperability.\n-  - PSK encryption mode 1 (AES-CTR, Main Profile compatible) with\n-    per-packet Nonce/IV in the AP header.\n-  - Future Nonce Announcement (CI=0x8011) for zero-latency key rotation.\n-  - SRP (Secure Remote Password) passphrase exchange, reusing the EAP-SRP\n-    state machine from Main Profile over Type 8 encapsulated EAPOL.\n-  - Flow ID field (I flag): virt_dst_port and virt_src_port map to/from\n-    the AP Outer/Inner Flow ID hierarchy on the wire.\n-  - LZ4 payload compression (LPC=1): sender compresses before encryption,\n-    receiver decompresses after decryption.  Controlled by the existing\n-    ?compression=1 URL parameter; receiver auto-detects via the LPC field.\n-    Bundled LZ4 library in contrib/lz4/ with system-library fallback.\n-  - Flow Attribute messages (CI=0x8001): sender periodically transmits\n-    session/flow metadata as JSON.  New receiver callback API\n-    (rist_receiver_flow_attr_callback_set) delivers parsed attributes\n-    to the application.\n-  - Baseline.Direct conformance for single-flow encrypted point-to-point\n-    RIST transport with ARQ.\n-  - 22 integration tests (basic, packet loss, encryption, mismatch,\n-    flow-id, lz4, lz4+encryption, flow-attr, SRP auth).\n-  - 187 unit tests (wire format, timestamp, seq_index, Type 8, PSK modes,\n-    Flow ID mapping, LZ4 roundtrip, flow-attr control roundtrip).\n-\n-  Per-byte statistics (#174):\n-  - Sender stats now include sent_bytes and retransmitted_bytes counters\n-    in both the C struct (rist_stats_sender_peer) and JSON output.\n-  - Receiver flow stats now include received_bytes in the C struct\n-    (rist_stats_receiver_flow) and JSON output.\n-  - Per-peer receiver stats include received_bytes.\n-\n-  Multicast enhancements (#112):\n-  - Configurable multicast TTL via ?ttl=N URL parameter (1-255).\n-    New udpsocket_set_mcast_ttl() public API.\n-  - IGMPv3 Source-Specific Multicast (SSM) support via ?source=IP\n-    URL parameter.  Uses IP_ADD_SOURCE_MEMBERSHIP / MCAST_JOIN_SOURCE_GROUP.\n-  - IPv6 multicast join support (previously only IPv4 was implemented).\n-  - New udpsocket_open_bind_mcast() with TTL and SSM source parameters.\n-  - udpsocket_join_mcast_group() now public API, supports IPv6 and SSM.\n-\n-  Configurable local port for caller peers (#158):\n-  - New ?local-port=N URL parameter to bind caller (non-listening) peers\n-    to a specific local UDP port instead of using an ephemeral port.\n-  - New local_port field in rist_peer_config.\n-\n-  Deferred multicast join in ristsender (#85):\n-  - When ristsender's input URL is a multicast address, the IGMP join\n-    is now deferred until the first RIST peer handshake completes.\n-    Prevents receiving multicast traffic before the output path is ready.\n-\n-  WebAssembly / Emscripten:\n-  - TUN device stubs for Emscripten targets.\n-  - Meson cross-file (wasm32-emscripten.txt) for WASM builds.\n-  - C99 strict-aliasing fix for merge memcpy.\n-\n-Build:\n-  - Bundled mbedTLS upgraded from 2.28.10 (EOL) to 3.6.6 LTS.\n-    The bundled tree now compiles as a separate static library with\n-    targeted warning suppression, isolating mbedTLS internals from\n-    librist's strict compiler flags.  Source compatibility fixes\n-    applied for the 2.x -> 3.x API migration (removed _ret suffixes,\n-    updated header paths).  System mbedTLS 3.6.x is also fully\n-    supported.\n-  - Removed unused #include <mbedtls/entropy_poll.h> which broke\n-    builds against system mbedTLS >= 3.6 where the header was removed.\n-\n-Test:\n-  - Loss simulation tests no longer abort on transient error log\n-    messages.  Under heavy bilateral loss, error-level messages are\n-    expected and do not indicate bugs.\n-  - Minimum receive threshold for loss tests now scales with the\n-    configured loss percentage, giving headroom for ARQ recovery in\n-    CI environments with timing jitter.\n-\n-New features:\n- - Add transparent one-to-many reflector mode (Main Profile only).\n-   A receiver listener configured with ?reflector=1 transparently\n-   forwards data packets from the publisher (sender) to all subscriber\n-   (receiver) peers, and routes NACKs/RTCP from subscribers back to\n-   the publisher.  Enables a single-listener fan-out topology without\n-   requiring a full sender context on the reflector.\n-   Trade-offs vs rist2rist (per-subscriber ARQ relay):\n-     * No per-subscriber retry buffer — recovery relies on the\n-       publisher's buffer, which may have aged out the packet.\n-     * Retransmissions fan out to ALL subscribers, not just the one\n-       that NACKed — bandwidth scales with subscriber count.\n-     * Recovery RTT is roughly 2x a direct connection.\n-     * No per-subscriber congestion control or statistics.\n-   Best suited for low subscriber counts with clean last-mile links.\n-   For high fan-out or lossy last-mile, use rist2rist instead.\n-   (contributed by RossWang)\n-\n-RTCP/NACK:\n- - Fix OOB read in NACK_FMT_SEQEXT handler: the RTCP feedback loop\n-   validated only 4 bytes per record, but the seqext struct reads\n-   seq_msb at offset 12.  A crafted datagram with a short trailing\n-   record could read up to 12 bytes past the allocation.  Add a\n-   per-handler minimum-length check matching the adjacent ECHO branches.\n- - Fix OOB read and log-disclosure in rist_sender_recv_nack: the NACK\n-   range handler read the 4-byte name field at offset 8 with only an\n-   8-byte minimum guarantee, and logged it with %s (unbounded read to\n-   next NUL).  Add a sizeof guard and use %.4s.\n-\n-Log:\n- - Lower log level for harmless cross-role NACK/data packets from\n-   ERROR/WARN to DEBUG to reduce log noise in reflector topologies.\n-\n-EAP/SRP:\n- - Fix EAPOL-Start body length field: the packet declared a 4-byte body\n-   (sizeof(eapol_hdr)) when the correct value is 0 (EAPOL-Start carries\n-   no body).  This bug was dormant until the bounds-check tightening in\n-   0.2.15 started correctly rejecting oversized body declarations,\n-   which broke every SRP handshake.  SRP authentication has been\n-   non-functional since 0.2.15.\n- - Fix off-by-one in eap_srp_send_password bounds check: the limit did\n-   not account for the 1-byte flags field before the ciphertext, so a\n-   maximum-length password would write 1 byte past the stack buffer.\n-   Not network-reachable (all callers cap at 128 bytes), but worth\n-   hardening.\n-\n-\n-Changes for 0.2.16 (pre-release):\n----------------------------------\n-\n-ABI/API:\n-  - ABI version 10:4:6 (soversion 4, binary-compatible with 0.2.15)\n-  - API version 4.7.0 (unchanged)\n-\n-  Maintenance and security-hardening release for 0.2.15. No source or\n-  binary changes are required of downstream consumers.\n-\n-Bug Fixes:\n-\n-  Windows:\n-  - Windows tools fail to start when remote logging is enabled.\n-    Running `ristreceiver -r 127.0.0.1:port ...` on Windows 11 would\n-    exit immediately with \"Failed to setup logging!\" because winsock\n-    had not been initialised before the public logging API tried to\n-    open a UDP socket. Fixes #208 (reported by Roman Dissertori).\n-  - EAP-SRP authentication fails on Windows with multiple peers.\n-    A 0.2.15 regression: an outbound packet that drew an ICMP\n-    port-unreachable reply caused Windows to corrupt the next\n-    incoming UDP datagram on the same socket, which then broke the\n-    EAP handshake. Most visible with bonded ethernet+wifi peers,\n-    Linux ristsender -> Windows ristreceiver. Fixes #209 (reported\n-    and confirmed fixed by Roman Dissertori).\n-  - Windows socket failure log lines now include the actual WSA\n-    error code, so user bug reports from Windows are diagnosable.\n-  - Cryptographic random number generator now seeds reliably on\n-    Windows runtimes where the legacy CryptoAPI (CryptAcquireContext\n-    / CryptGenRandom) is missing or unconfigured - notably wine and\n-    some sandboxed container builds. The seeding path now uses the\n-    modern BCryptGenRandom (CNG) API, supported on every shipping\n-    Windows version. Without this fix, the 0.2.15 CSPRNG hardening\n-    refused to start the SRP/AES code paths in those environments\n-    rather than silently downgrade. Fixes #210.\n-  - MSVC builds no longer crash due to stack corruption in atomic\n-    operations. The stdatomic compatibility shim hardcoded all\n-    operations to a single width, so a 64-bit compare-exchange on a\n-    32-bit atomic variable overwrote adjacent stack memory.\n-\n-  Tools:\n-  - ristsender now preserves incoming RTP sequence numbers and\n-    timestamps when the --rtp-sequence and --rtp-timestamp options\n-    are used. Previously these options had no effect.\n-\n-Hardening / follow-ups to 0.2.15:\n-\n-  - EAP-SRP authentication retry exhaustion did not drop the failed\n-    peer. A latent sign-comparison bug meant a peer that failed\n-    every authentication retry was kept around in a half-\n-    authenticated state instead of being dropped. Not externally\n-    reported.\n-  - Crypto random number generator could silently downgrade to a\n-    weak fallback. If the CSPRNG seed call failed at startup (no\n-    kernel entropy source - sandboxed container, embedded image\n-    without /dev/urandom, broken build), the failure was dropped\n-    and the wall-clock-derived stopgap was used for the lifetime of\n-    the process. Now logs loudly and refuses to hand out random\n-    bytes. Not externally reported.\n-  - Internal EAP error code documentation: clarify a guard comment\n-    that read as a removable paranoia check but is actually\n-    protecting against a NULL function pointer.\n-\n-  Post-0.2.15 audit follow-ups (reported by Thomas Guillem):\n-\n-  Receiver flow / peer accounting:\n-  - Receivers no longer crash or corrupt their peer list when a\n-    peer is added to a flow at the same instant another thread is\n-    removing one. The list of peers per flow was being walked by\n-    one thread under one lock and modified by another thread under\n-    a different lock; the two paths now agree. A 256-peer-per-flow\n-    cap is also enforced so a misconfigured (or hostile) sender\n-    cannot grow the list without bound.\n-  - Receivers stop creating flows once 256 active flows already\n-    exist. Previously the count was checked and the new flow\n-    allocated in separate steps, so a burst of new flow IDs could\n-    race past the limit. Now the count check and the slot insert\n-    happen under the same lock.\n-\n-  EAP-SRP authenticator:\n-  - Authenticated peers can no longer be deauthenticated by a\n-    spoofed EAPOL-LOGOFF. A successfully authenticated session\n-    used to reset to start-over state on receipt of any LOGOFF\n-    packet, which made the auth state machine trivially DoSable\n-    from off-path. LOGOFF is now only honoured before the session\n-    reaches authenticated state, and a legitimate LOGOFF before\n-    that point also resets the retry counter so a counter\n-    wrap-around can't lock the peer out for the lifetime of the\n-    process.\n-  - A spoofed EAP FAILURE no longer pushes the local state machine\n-    into permanent failure. FAILURE packets are now matched\n-    against the identifier of the in-flight EAP exchange and\n-    dropped if they don't match, the same way Request/Response are\n-    matched. A genuine FAILURE still latches the peer out, but\n-    after a 30-second quiet period the peer is allowed to retry\n-    from the start so a transient burst of bad responses doesn't\n-    require restarting the process.\n-  - EAP authenticators no longer answer EAP Identity requests\n-    themselves. The handler used to run on both roles, so a\n-    misbehaving (or attacker-controlled) authenticatee could\n-    persuade the authenticator to discard its own session state\n-    and start over. The handler is now restricted to the\n-    authenticatee role, and even there it is rate-limited so the\n-    pre-auth reset cannot be triggered repeatedly to wipe\n-    in-progress sessions.\n-  - EAP SRP setup now caps the advertised modulus (N) and\n-    generator (g) lengths at 1024 bytes (8192-bit groups, the\n-    largest standard RFC 5054 group). A peer that advertised\n-    multi-megabyte values used to allocate that much memory and\n-    burn that much CPU on bignum imports before deciding the\n-    handshake was bogus.\n-\n-  PSK encryption:\n-  - The PSK nonce generator no longer falls back to a\n-    wall-clock-derived value when the CSPRNG misbehaves. Nonces\n-    are required to come from the system CSPRNG; if it fails or\n-    returns suspect output the key is marked broken, encryption\n-    refuses to produce ciphertext for that key (sends zeroes\n-    instead so a downstream consumer cannot mistake it for a\n-    successful encrypt), and the existing audible logging fires.\n-    Decryption was already locked out in the same condition; this\n-    closes the encrypt-side gap. The non-security-critical\n-    `prand_u32` helper used elsewhere in the tree keeps its\n-    wall-clock fallback because its callers don't need\n-    unpredictability.\n-  - PSK decryption now skips the PBKDF2 key derivation when the\n-    key is already locked out. A bursty stream of malformed\n-    encrypted packets at a peer whose key state was already\n-    compromised used to spin tens of thousands of PBKDF2 rounds\n-    per packet before rejecting it. The lockout check now happens\n-    first, so the per-packet cost drops to a few comparisons.\n-  - PSK AES-CTR dispatch no longer silently falls back to AES-128\n-    on an unknown key size. The earlier switch-case used a\n-    fallthrough that meant a corrupted or unsupported key length\n-    would still be processed (with the wrong cipher), which\n-    masked configuration errors. Unknown key sizes now return\n-    without touching the buffer.\n-\n-  SRP / cryptographic primitives:\n-  - The Nettle build of SRP no longer feeds uninitialized stack\n-    into the bignum library when the CSPRNG returns no entropy.\n-    The Nettle random callback's signature returns void, so a\n-    CSPRNG failure was previously dropped on the floor and the\n-    caller computed against whatever was on the stack. The\n-    callback now zeroes its output buffer and signals the caller\n-    via a side-channel context; the bignum operation then fails\n-    cleanly. The mbedTLS build was already correct.\n-  - The SRP NG (N/g) group identifier enum now keeps its\n-    pre-0.2.15 integer values. The 0.2.15 release deleted the two\n-    smallest groups (NG_512, NG_768 - both below current\n-    minimum-strength thresholds) but shifted every remaining\n-    enum entry down by two as a side effect. The integer \"2\",\n-    which any external caller might reasonably have hardcoded\n-    for NG_1024, silently started meaning NG_4096. The deleted\n-    values 0 and 1 are now explicit reserved slots that return\n-    \"no such group\" - code that uses the named constants\n-    (`LIBRIST_SRP_NG_1024` etc.) is unaffected, code that used\n-    literal integers either gets the group it asked for or a\n-    clean error.\n-\n-  Statistics JSON:\n-  - The stats JSON payloads (sender flow, sender per-peer wrapper,\n-    receiver flow) now carry an explicit top-level\n-    `schema_version` field. The current value is 2 - version 1 was\n-    the duplicate-`peer`-key sender shape that v0.2.15 replaced\n-    with a `peers` array (NEWS noted the change, but downstream\n-    parsers had no way to detect which shape they were getting).\n-    Any future incompatible shape change must bump this number.\n-  - The sender `cname` field copy is now explicitly\n-    NUL-terminated. The previous strncpy with a destination-sized\n-    length depended on a separate bounds check elsewhere in the\n-    receive path to keep the result safe; the contract is now\n-    self-contained.\n-\n-  Build / portability:\n-  - librist now requires mbedTLS >= 2.7 (March 2018), and emits a\n-    clean #error if built against anything older. The compatibility\n-    branch for pre-2.7 mbedTLS releases (which used the\n-    void-returning SHA-256 API) had no CI coverage and was the\n-    same untestable corner that produced a 0.2.14 build break -\n-    dropping it removes the surface area entirely. Every still-\n-    supported distro is well past 2.7; the bundled tree is 2.28.x.\n-  - On Windows, the mbedTLS CSPRNG seed no longer reaches into\n-    private mbedtls_entropy_context fields to keep its source\n-    count happy. It now seeds the CTR_DRBG directly from\n-    BCryptGenRandom, skipping the entropy module entirely on\n-    Windows. Non-Windows builds still use the normal entropy\n-    initialisation path.\n-\n-  Second audit follow-ups (reported by Thomas Guillem):\n-\n-  EAP-SRP authentication:\n-  - EAP passphrase response handler now gates on a matching\n-    outstanding request identifier when a solicited passphrase\n-    exchange is active.  Previously any post-auth spoofed\n-    RESPONSE/PASSWORD_REQUEST_RESPONSE was accepted, allowing a\n-    man-on-the-side to hijack a key-rotation exchange.  Unsolicited\n-    passphrase pushes (rist_eap_send_passphrase) remain allowed as\n-    the payload is encrypted under the SRP session key.\n-  - EAP last_identifier is no longer primed in the request prologue\n-    before subtype validation.  A spoofed REQUEST with a crafted\n-    identifier could prime the FAILURE-identifier gate, allowing a\n-    subsequent spoofed FAILURE to latch the peer out.\n-  - EAP authenticator initial identifier now comes from the CSPRNG\n-    instead of prand_u32, closing an off-path prediction window.\n-  - The use_default_2048 flag in EAP SRP challenge processing was\n-    hardcoded true, ignoring wire-supplied N/g groups.  Now derived\n-    from the actual generator length on the wire.\n-  - eap_set_ip_string replaced an unbounded memcpy with strncpy and\n-    explicit NUL-termination.\n-\n-  EAP-SRP / cryptographic primitives:\n-  - SRP u and k hash computations now use RFC 5054 PAD: both inputs\n-    are zero-extended to the byte length of N before hashing.  The\n-    previous unpadded hash produced different values for leading-\n-    zero-byte operands, deviating from the specification.  This is a\n-    wire-incompatible change with pre-0.2.16 peers using SRP.\n-  - SRP M1/M2 verification and client M2 verification now use a\n-    constant-time comparison instead of memcmp.\n-  - SRP authenticator aborts if the verifier v is zero (session key\n-    would not depend on the password).\n-  - SRP authenticator aborts if B mod N == 0 (RFC 5054).\n-  - SRP authenticator aborts if u == 0 mod N after hashing A||B.\n-  - SRP hash return values are now checked; unchecked failures\n-    previously left uninitialised buffers in the session transcript.\n-  - SRP BIGNUM_EXP_MOD return after computing S is now checked.\n-  - SRP client context creation now validates modulus length\n-    [128, 1024], generator length [1, N_len], and salt length\n-    [1, 64] to cap resource consumption from malformed handshakes.\n-  - SRP private exponent size now matches the modulus size instead\n-    of a hardcoded 32 bytes, providing full-strength keying for\n-    large groups (e.g. NG_8192).\n-  - SRP salt generation now uses fixed-size 32-byte raw CSPRNG\n-    output copied directly, instead of going through bignum export\n-    which strips leading zero bytes and shrinks the effective salt\n-    domain.\n-\n-  Receive path / flow safety:\n-  - Use-after-free on flow deletion during rist_receiver_data_read2\n-    is fixed.  The flow pointer returned by the lookup function is\n-    now validated under flows_lock; the flow's own mutex is acquired\n-    before flows_lock is released.  rist_delete_flow unlinks the\n-    flow under the same lock before freeing.\n-  - rist_receiver_set_output_fifo_size now rejects size == 0 and\n-    size > 65536, preventing a zero-modulus OOB on the FIFO ring.\n-  - rist_peer_get_cname uses strnlen instead of strlen, bounded by\n-    RIST_MAX_STRING_SHORT.\n-\n-  MPEG-TS null packet handling:\n-  - expand_null_packets now bounds-checks each source-packet copy\n-    against the original payload length, preventing an information\n-    leak of adjacent buffer data when the NPD bit pattern implies\n-    more real packets than the payload actually contains.\n-  - suppress_null_packets now checks the sync byte of every packet\n-    in the block, not just the first.\n-\n-  SDES / cname / miface:\n-  - RTCP SDES writer uses strnlen instead of strlen and zero-fills\n-    the padding region explicitly, instead of copying adjacent\n-    memory from the source buffer.\n-  - All strncpy sites for peer cname and miface (peer_initialize,\n-    rist_peer_clone) now have explicit NUL-termination.\n-\n-  PSK encryption:\n-  - PSK key init now uses strnlen and explicit NUL-termination\n-    instead of unbounded strlen + memcpy.\n-  - PSK key clone now explicitly NUL-terminates the copied password.\n-  - PSK decrypt no longer unconditionally clears bad_decryption\n-    after rekeying; if _librist_crypto_aes_key set the flag (PBKDF2\n-    failure), it stays set so the lockout counter is not reset.\n-\n-  Sender:\n-  - sender_buffer_size growth is now capped at sender_queue_max - 1,\n-    preventing unbounded memory growth when packets are too young to\n-    delete from the retry queue.\n-  - rist_sender_peer_create now calls rist_peer_remove instead of\n-    bare free on RTCP-peer creation failure, preventing a dangling\n-    pointer in the peer list.\n-\n-  Miscellaneous:\n-  - rist_peer_remove dead store fixed: *next = NULL now executes\n-    before the early return on NULL peer.\n-  - NACK debug log guarded against zero-length array dereference\n-    and uses %zu for size_t.\n-  - is_ip_address uses a union of in_addr/in6_addr for the\n-    inet_pton destination buffer, fixing an AF_INET6 stack overflow\n-    that wrote 16 bytes into a 4-byte sockaddr_in.sin_addr.\n-  - rist_new_connection log format strings replaced char[] locals\n-    passed via &array with const char * pointers, eliminating\n-    strict-aliasing undefined behaviour in the varargs call.\n-  - rist_tun_write overflow guard changed from\n-    len + UTUN_HEADER_SIZE > sizeof(tmp) to\n-    len > sizeof(tmp) - UTUN_HEADER_SIZE, preventing unsigned\n-    wraparound when len is near SIZE_MAX.\n-\n-Community bug reports (gitlab #211, RossWang):\n-\n-  Receiver flow timing:\n-  - Initialize last_packet_ts, time_offset_changed_ts, and\n-    time_offset_old when a flow (re)starts.  After a flow reset the\n-    stale values from the previous epoch could cause the old-clock\n-    fallback to fire, corrupting packet timing and dropping the\n-    first few packets.\n-\n-  OOB dequeue:\n-  - rist_oob_dequeue now holds peerlist_lock while walking a\n-    listener peer's child list.  A concurrent peer add/remove could\n-    free a sibling_next pointer during the walk.\n-\n-  Note: _librist_peer_match_peer_addr (#211 item 3) also walks the\n-  child list without a lock, but cannot use peerlist_lock because the\n-  receive callback runs on the same thread that holds it in the\n-  protocol loop.  Fixing this requires a per-listener child rwlock or\n-  reference counting, planned for a future release.\n-\n-Test / CI:\n-  - New unit test (logging_init_order) that exercises the\n-    rist_logging_set remote-address path before any rist_*_create\n-    call. Would have caught #208 on the first Windows-CI run.\n-  - The full meson test suite now runs against the cross-compiled\n-    Windows binaries under wine, not just on Linux. Every protocol\n-    test that gates Linux releases also gates Windows releases,\n-    including the SRP / AES encryption suites which are now built\n-    with mbedTLS on Windows (see the #210 fix above). The wine\n-    runtime was already provisioned in the CI image; only the test\n-    invocation was missing. Catches Windows-runtime regressions\n-    without requiring a native Windows runner.\n-  - Per-test results from the Linux job now show up directly in the\n-    GitLab MR UI via meson's JUnit output, so reviewers don't have\n-    to read job logs to see which test failed.\n-\n-\n-Changes for 0.2.15 (2026-05-14):\n---------------------------------\n-\n-ABI/API:\n-  - ABI version 10:3:6 (soversion 4, binary-compatible with 0.2.14)\n-  - API version 4.7.0 (unchanged)\n-\n-  Security release. Existing 0.2.14 installations remain\n-  binary-compatible; downstream consumers do not need to recompile.\n-\n-Security Fixes:\n-\n-  Receive-side parsers and key handling were audited end to end. Each\n-  fix below is independent.\n-\n-  Receive path / RTCP:\n-  - rist-common: bounds-check the RTP and RTP-extension headers.\n-    The earlier check only covered the 4-byte reduced port subheader;\n-    a truncated packet underflowed payload.size to ~SIZE_MAX which\n-    then drove a malloc(len + 32) on the receiver enqueue path.\n-  - rist-common: harden the RTCP dispatcher. Off-by-one in\n-    bytes_left, integer overflow in 4*(1+records) (CPU spin on a\n-    single packet), and an SDES handler that let 255 bytes be\n-    memcpy'd into a 128-byte struct member, overrunning the adjacent\n-    peer config/key state.\n-  - rist-common: bound the XR block parser, clamp the on-the-wire\n-    length to what we received, and guard the DLRR delay subtraction\n-    so it can no longer pin last_rtt near UINT64_MAX (which would\n-    permanently disable NACK timing on the peer).\n-  - rist-common: validate the sender NACK record count before\n-    iterating. ntohs(rtcp->len) - 2 underflowed to ~65K on a\n-    malformed feedback packet, enqueueing OOB bytes as retransmit\n-    requests and leaking them back to the peer.\n-  - rist-common: add the missing return after the VSF length check,\n-    and derive odd_nonce from the actual nonce byte rather than\n-    GRE flags1 (the latter silently disabled the OTF passphrase\n-    rotation feature on the receive side).\n-  - rtcp: guard the RR and echo-response RTT subtractions and\n-    size-check the SR / echo-request / echo-response casts (same\n-    RTT-underflow primitive as above on the RR/echo paths).\n-  - rtcp: correct the XR echoreq RTCP length field per RFC 3550;\n-    receivers were reading 4 bytes past the actual XR block.\n-  - mpegts: bound NPD expansion to the 7-packet wire encoding.\n-    expand_null_packets used attacker-controlled payload_len and\n-    npd_bits to drive ts_count and write up to 11 KB into a 10 KB\n-    recv_npd buffer (overrunning into the adjacent RTCP TX buffer),\n-    and shifted by a negative amount when ts_count > 7.\n-  - gre: zero-init keepalive info on short packet and check return,\n-    otherwise a truncated keepalive disclosed stack memory through\n-    the user logging callback.\n-  - udpsocket: NUL-terminate the URL hostname after strncpy so\n-    udpsocket_resolve_host cannot over-read.\n-\n-  PSK / AES-CTR:\n-  - crypto: reject zero-nonce PSK packets instead of skipping\n-    decryption. The previous early-return left the recv buffer\n-    untouched and the caller treated the attacker's plaintext as a\n-    freshly-decrypted Main Profile packet, bypassing the configured\n-    PSK per packet. Pre-auth, single packet.\n-  - crypto: correct nettle AES-CTR dispatch for 128/192-bit keys.\n-    AES-128 was using nettle_aes192_encrypt against an aes128\n-    context; the AES-192 case was missing entirely, leaving an\n-    uninitialised function pointer hot on the EAP password-rotation\n-    path.\n-\n-  EAP / SRP:\n-  - eap: validate EAPOL/EAP/SRP-TLV lengths end to end. The body\n-    length check was inverted and inner dispatchers walked past the\n-    recv buffer with attacker-supplied lengths, exfiltrating heap\n-    bytes through username copies and AES-CTR rewrites. Adjacent\n-    fixes cover EAP header / dispatch / passphrase-flags reads.\n-  - eap: NULL-check auth_ctx and propagate handle_A failure (notably\n-    A == 0 mod N) so a rejected SRP exchange actually fails auth\n-    instead of sending an uninitialised B back to the client. NULL\n-    check the SRP client ctx and propagate the int-typed return of\n-    the A/B writers (a -1 wrapped to ~SIZE_MAX and crashed on the\n-    last_pkt malloc cache path).\n-  - eap: bound rist_eap_send_passphrase to the destination buffer;\n-    an over-long passphrase from the API or YAML config overran\n-    ctx->unsollicited_passphrase into adjacent heap fields used by\n-    the periodic retransmit.\n-  - eap: rate-limit the EAP_CODE_FAILURE restart loop. The handler\n-    unconditionally called eap_reset_data + log + EAPOL_START with\n-    no retry counting, so a spoofed FAILURE per round-trip kept us\n-    bouncing through reset+restart forever.\n-  - eap: write the empty server-name placeholder into outpkt rather\n-    than pkt (pure copy/paste typo that corrupted the recv buffer\n-    on every successful identity response).\n-  - srp: propagate failure when the client A, server B, or u is\n-    congruent to 0 mod N. The checks existed but the failure path\n-    left ret==0 so the caller continued the exchange. Auth bypass\n-    primitive on both sides.\n-\n-  PRNG:\n-  - crypto: route prand_u32 through the CSPRNG. The previous\n-    implementation called rand() seeded with a wall-clock NTP\n-    timestamp; the result is used for the PSK GRE nonce (input to\n-    PBKDF2), the EAP identifier and the peer SSRC. Reuse the mbedTLS\n-    CTR-DRBG / GnuTLS RNG that already backs SRP key generation.\n-  - crypto: fix operator-precedence bug in random_get_string and\n-    add an upper bound on the local rand buffer. Affected callers\n-    that ask for a runtime-derived password.\n-\n-Hygiene (not security-impacting on their own but shipped in the same\n-release):\n-  - flow: handle realloc/calloc failure on the peer-list growth path\n-    and rist_receiver_missing; correct two log format strings\n-    (uint32_t printed with PRIu64; pointer where a PRIu32 was due).\n-  - libevsocket: preserve the global context list when removing the\n-    head; ctx_del previously cleared CTX_LIST instead of advancing\n-    it to c->next.\n-  - eap, logging, udpsocket: NULL-check the malloc/calloc results\n-    that were dereferenced unconditionally; free the calloc'd ctx\n-    on pthread_mutex_init failure in eap_clone_ctx; close the bound\n-    socket on the multicast-join failure leg.\n-  - network: on the GNU/Hurd MAC-address path, check socket() and\n-    close it on the getifaddrs() failure leg.\n-\n-Additional fixes from the VideoLAN security audit:\n-\n-  Independent receive-side / EAP / SRP audit shared by VideoLAN.\n-  The findings already covered above are not re-listed; the items\n-  below close the residual gaps.\n-\n-  EAP / SRP:\n-  - eap: ignore EAP_REQUEST_IDENTITY once we're past\n-    EAP_AUTH_STATE_SUCCESS so a single forged identity request can\n-    no longer tear an established session down and re-emit the\n-    configured username on the wire.\n-  - eap: refuse RESPONSE/IDENTITY on the authenticatee side\n-    (lookup_func is NULL there per the calloc setup path; the\n-    handler used to deref it unconditionally).\n-  - eap: free verifier_data on every exit from\n-    process_eap_response_identity. Three error paths returned -1\n-    before reaching the cleanup block, leaking the four heap\n-    allocations the verifier lookup callback handed back.\n-  - srp: jump to failed: instead of returning out of\n-    BIGNUM_WRITE_BYTES when MPI write fails inside calc_x; the\n-    return -1 path leaked hash_data.\n-  - srp: check librist_crypto_srp_hash_bignum return in\n-    calculate_m. The helper returns -1 (and leaves the output\n-    buffer untouched) when the bignum exceeds its 1024-byte\n-    staging buffer; the unchecked path XOR'd uninitialised stack\n-    into the SRP transcript.\n-  - srp: BIGNUM_INIT k and B in the authenticator constructor.\n-    Relying on calloc-zeroed mbedtls_mpi state happened to work\n-    today but the public mbedTLS contract requires explicit init.\n-  - srp: bound A and B operand sizes against N before exp_mod.\n-    handle_A and client_handle_B accepted any length from the\n-    wire; mbedtls_mpi_exp_mod runtime grows with operand size.\n-  - srp: NULL the caller's *bytes_s/*bytes_v in _create_verifier\n-    failure paths so eap_reset_data does not double-free them.\n-  - srp: capture mbedtls_mpi_fill_random return when generating\n-    the salt; the previous `if (ret != 0)` re-checked the prior\n-    BIGNUM_FROM_STRING.\n-  - srp: repair the !USE_SHA_RET branch of librist_crypto_srp_hash\n-    (referenced symbols from a different function and was missing\n-    its trailing semicolon).\n-  - srp: drop the sub-1024-bit RFC 5054 groups (NG_512, NG_768).\n-    The enum was internal; the unit-test fixture that wanted the\n-    deterministic 512-bit exchange now inlines the constants.\n-\n-  PSK / AES-CTR:\n-  - psk: fail closed when PBKDF2 setup fails on the mbedTLS path.\n-    mbedtls_md_setup / mbedtls_pkcs5_pbkdf2_hmac errors used to\n-    fall through into mbedtls_aes_setkey_enc with the\n-    stack-allocated aes_key buffer in undefined state.\n-  - psk: don't let an attacker-chosen nonce reset the bad-packet\n-    lockout. _librist_crypto_psk_decrypt now keeps bad_decryption\n-    set across nonce rolls so the five-bad-packets cap is no\n-    longer trivially bypassed by sending a different nonce on\n-    every shot.\n-  - psk: use memcpy for the GRE nonce load instead of a punned\n-    uint32_t cast (strict-aliasing UB; faulted on architectures\n-    that require aligned uint32_t loads).\n-\n-  Receive path:\n-  - rist-common: lift the rist_rtp_hdr size check above the\n-    Simple/REDUCED conditional so Main-profile FULL/EAPOL paths\n-    can no longer read past recv_bufsize.\n-  - udp: size the NPD scratch buffer for the real worst case\n-    (7 * 204 + sizeof(rist_rtp_hdr_ext)). The previous 6 * 204 + 4\n-    was four bytes short for the (count == 7, suppressed == 1)\n-    output and let suppress_null_packets smash four bytes of\n-    stack past tmp_buf.\n-  - rist-common: cast keepalive json_len to int for %.*s. size_t\n-    via varargs is undefined per the C standard.\n-\n-  Flow / threading:\n-  - flow: take f->mutex when growing peer_lst; the realloc could\n-    move storage and concurrent walkers (rist_best_rtt_index, the\n-    output thread, stats) read peer_lst without synchronization.\n-  - flow: cap receiver flows at RIST_MAX_FLOWS (256) and walk\n-    FLOWS under flows_lock. Without a cap an attacker that can\n-    land traffic on a receiver can exhaust memory by walking the\n-    32-bit flow_id space.\n-  - flow: NULL-check the dataout_fifo_queue calloc; the\n-    data-output thread used to write into it unconditionally.\n-\n-  TUN / RTCP TX / utility:\n-  - tun_linux, tun_darwin: bounds-check prefix_len before computing\n-    the netmask. prefix_len < 0 hit undefined behaviour in C and\n-    prefix_len == 32 produced a 32-bit shift on a uint32_t (also\n-    UB).\n-  - tun_darwin: parse the utun unit suffix with strtoul, not atoi,\n-    with full validation; falls back to \"let the kernel pick\" on\n-    parse failure.\n-  - rtp: clamp SDES name_len to 255 before computing sdes_size so\n-    the on-the-wire length byte and the actual bytes copied stay\n-    in agreement.\n-  - rist-common: make compare() return a real three-way result\n-    (qsort's contract is < 0 / 0 / > 0; the previous boolean cast\n-    was undefined per POSIX).\n-  - rist-common: pick the actual median in recalculate_clock_offset\n-    (the index was biased high by one).\n-\n-Bug Fixes:\n-  - rist-common: initialize peer->sd to -1 in peer_initialize so a\n-    DNS resolution failure no longer leaves a zero socket\n-    descriptor wired up to the event loop, which on Windows turned\n-    into an infinite POLLERR loop and on Unix could attach the RIST\n-    state to fd 0 (stdin). (!312, Yannick Le Roux)\n-  - logging: check log_socket >= 0 instead of just non-zero in the\n-    rist_log_impl UDP path, matching the LOGGING_SETTINGS_INITIALIZER\n-    sentinel (the early-exit was tightened in f7ace4d but the send\n-    branch still treated 0 as a valid fd). (!313, Daisuke Matsunami)\n-  - stats: emit sender peers under a \"peers\" array. The previous\n-    shape used \"peer\" as the key for every peer object, producing\n-    invalid JSON and silently dropping all but one peer in strict\n-    parsers. (#206, Manuel Alejandro)\n-\n-Tools:\n-  - prometheus-exporter: implement rist_prometheus_parse_sender_stats.\n-    The hook for ristsender's JSON stats callback was a TODO stub, so\n-    sender-side rist_sender_peer_* gauges/counters never showed up at\n-    the scrape endpoint. The parser now walks the new \"peers\" array\n-    and feeds rist_prometheus_handle_sender_peer_stats; metric names\n-    and shape are unchanged from the receiver-callback path.\n-    (closes the second half of #206)\n-  - ristsender: drop a duplicate rist_sender_stats_callback_set call\n-    in setup_rist_peer that overwrote the per-instance arg with NULL,\n-    breaking sender attribution when more than one sender ran in the\n-    same process.\n-  - udp2udp: log a one-shot warning when --metrics is enabled noting\n-    that the relay produces no rist_* series (it uses librist only\n-    for URL parsing and the httpd shell).\n-\n-Windows / MinGW Build:\n-  - rist.c: return integer (not NULL) from the PTHREAD_START_FUNC\n-    bodies. The macro expands to a DWORD __stdcall function on\n-    Windows without HAVE_PTHREADS, which gcc14 rejected.\n-  - meson: always probe clock_gettime via has_header_symbol on\n-    Windows, not only when have_mingw_pthreads=true. Modern\n-    mingw-w64 ships clock_gettime as a static inline in <time.h>\n-    regardless of winpthreads, so contrib/time-shim.c collided\n-    with the toolchain definition on the default build.\n-  - rist-common: drain WSAECONNRESET with a real recvfrom buffer\n-    on the rist_peer_sockerr POLLERR path. The previous NULL-buffer\n-    call returned WSAEFAULT and left the indication in the socket\n-    queue, causing WSAPoll to fire POLLERR in a tight loop and\n-    flood the log with \"Socket error!\" whenever a peer went away.\n-    (#205, Manuel Alejandro)\n-  - tools: centralize the strtok_r/strtok_s shim in a header so\n-    rist2rist (and any future tool that needs strtok_r) builds on\n-    MinGW / MSVC. ristsender and ristreceiver had inline shims;\n-    rist2rist's recent multi-peer bonding work added strtok_r\n-    without one and failed to link. (#207, Thierry Lelegard)\n-\n Changes for 0.2.14 (2026-04-25):\n --------------------------------\n \n@@ -1832,7 +376,7 @@ Initial official public release of librist.\n \n Core Features:\n   - Full support for VSF TR-06-1 (Simple Profile)\n-  - VSF TR-06-2 (Main Profile):\n+  - Partial support for VSF TR-06-2 (Main/Advanced Profile):\n     * PSK Encryption (AES 128 and 256)\n     * Bidirectional connection initiation\n     * Multipath support (load balanced or redundant link bonding)\ndiff --git a/README.md b/README.md\nindex 2cdbe7e..d71f043 100644\n--- a/README.md\n+++ b/README.md\n@@ -4,7 +4,7 @@\n \n A library that can be used to easily add the RIST protocol to your application.\n \n-This code was written to comply with the Video Services Forum (VSF) Technical Recommendations TR-06-1, TR-06-2, and TR-06-3. The protocol goes by the codename \"RIST\"\n+This code was written to comply with the Video Services Forum (VSF) Technical Recommendations TR-06-1 and TR-06-2. The protocol goes by the codename \"RIST\"\n \n The canonical repository URL for this repo is https://code.videolan.org/rist/librist\n \n@@ -25,21 +25,9 @@ The goal of this project is to provide a RIST library for **most platforms**.\n   - One-to-many distribution (media server mode)\n   - Multiplexing and TUN interface support\n   - Null packet deletion/suppression\n-- **VSF TR-06-3** (Advanced Profile) - Baseline.Direct conformance including:\n-  - Native control plane (Keep-Alive, RTT Echo, NACK Bitmask/Range)\n-  - Full 32-bit sequence numbering\n-  - PSK encryption (AES-CTR mode 1, per-packet Nonce/IV)\n-  - SRP (Secure Remote Password) passphrase exchange\n-  - Future Nonce Announcement for zero-latency key rotation\n-  - Flow ID hierarchy (Outer/Inner/Sub) mapped to stream-id\n-  - LZ4 payload compression with automatic receiver detection\n-  - Flow Attribute messages (periodic JSON session metadata)\n-  - Type 8 GRE-over-AP encapsulation for Section 9 interoperability\n \n ### Additional Features\n \n-- Pluggable transport abstraction for non-POSIX platforms (WebAssembly, userspace stacks)\n-- Packet Split/Merge for payloads exceeding path MTU\n - YAML configuration file support for ristsender/ristreceiver\n - Prometheus metrics exporter with OpenMetrics format\n - MPEG-TS null packet suppression byte tracking\n@@ -64,10 +52,10 @@ The reasoning behind this decision is the same as for libvorbis, see [RMS on vor\n - Buffer sizes from 50 ms to 30 seconds\n - Networks with round trip times from 0ms to 5000ms\n - Bitrates from 0 to 1 Gbps\n-- Packet size should be kept under the path's MTU (typically 1500). Payloads exceeding the MTU can be handled with the built-in split/merge feature (`?split=auto`).\n+- Packet size should be kept under the path's MTU (typically 1500). The library does not support packet fragmentation.\n - Bi-directional communication available (not one-way systems like satellite)\n \n-If you have an application that needs to operate outside the sweet spot described above, you will need to modify some constants in the rist-private.h header and/or use some of the more obscure API calls to fine tune the library for your use case.\n+If you have an application that needs to operate outside the sweet spot described above, you will need to modify some constants in the rist-private.h header and/or use some of the more obscure API calls to fine tune the library for your use case. The library can overcome all the limitations above by fine-tuning with the exception of packet fragmentation which will be addressed as a feature enhancement in the future.\n \n # Roadmap\n \n@@ -77,13 +65,9 @@ If you have an application that needs to operate outside the sweet spot describe\n - Stable and documented public API\n - VSF TR-06-1 (Simple Profile) full compliance\n - VSF TR-06-2 (Main Profile) full compliance\n-- VSF TR-06-3 (Advanced Profile) Baseline.Direct conformance\n-- Cross-platform support (Linux, macOS, Windows, FreeBSD, WebAssembly)\n+- Cross-platform support (Linux, macOS, Windows, FreeBSD)\n - PSK encryption and SRP passphrase exchange\n - Multipath and multiplexing support\n-- Pluggable transport abstraction\n-- Packet Split/Merge\n-- LZ4 payload compression\n - Prometheus metrics integration\n \n ### In Progress\n@@ -94,7 +78,8 @@ If you have an application that needs to operate outside the sweet spot describe\n \n ### Planned\n \n-- VSF TR-06-3 (Advanced Profile) additional conformance levels (DTLS, Fragmentation)\n+- VSF TR-06-4 Part 4 - Decoder Synchronization API\n+- VSF TR-06-2 (Advanced Profile)\n \n # Tools\n \n@@ -105,7 +90,6 @@ The library includes several command-line utilities:\n - **rist2rist** - RIST relay/proxy application\n - **udp2udp** - UDP relay application\n - **risttunnel** - Point-to-point IP tunnel over RIST (bidirectional, ARQ, crypto)\n-- **ristsrppasswd** - Generate SRP verifier lines for authentication\n - **prometheus-exporter** - Prometheus metrics endpoint (when built with libmicrohttpd)\n \n All tools support YAML configuration files for easier deployment.\n@@ -194,7 +178,7 @@ Here is a table of comparison of the two protocols:\n \n ## What about the packet recovery patents?\n \n-- This code was written to comply with the Video Services Forum (VSF) Technical Recommendations TR-06-1, TR-06-2, and TR-06-3 and as such is free of any patent royalty payments\n+- This code was written to comply with the Video Services Forum (VSF) Technical Recommendations TR-06-1 and TR-06-2 and as such is free of any patent royalty payments\n \n ## Will you care about <my_arch>? <my_os>?\n \ndiff --git a/THANKS.md b/THANKS.md\nindex fd176af..8f1f9dc 100644\n--- a/THANKS.md\n+++ b/THANKS.md\n@@ -16,13 +16,5 @@ SipRadius LLC for funding this project.\n \n And all the librist Authors (git shortlog -sn), including:\n \n-Sergio Ammirata, James Hurley, Daniele Lacamera,\n-Yannick Le Roux, Thierry Lelegard, Carson Riker, Anders Cedronius,\n-Zhao Zhili, Matthew Edge, Peter Kovář, Steve Lhomme, Heiko Nickerl,\n-David Holroyd, Tristan Matthews, RossWang, Christopher Degawa,\n-Dave Evans, Manuel, Samuel Thibault, Reno Dakota, Olivier Crête,\n-Kevin Wang, Kevin Pan, Johan Lescure, Gyan Doshi, Florian Ernst,\n-Derek Prestegard, Denis Slobodskoy, Daisuke Matsunami, Brian Keane,\n-Bill Roberts, Azamat H. Hackimov, Alyssa Ross, Alexandre Janniaux,\n-Alexander Podrezenko, Alex Converse, Kuldeep Singh Dhaka,\n-Antonio Cardace, Marvin Scholz, Stephan Mueller\n+Sergio Ammirata, Kuldeep Singh Dhaka, Daniele Lacamera, Antonio Cardace, Gijs Peskens, James Hurley,\n+Marvin Scholz, Stephan Mueller\ndiff --git a/compat/msvc/stdatomic.h b/compat/msvc/stdatomic.h\nindex 7c6e522..28e1bcf 100644\n--- a/compat/msvc/stdatomic.h\n+++ b/compat/msvc/stdatomic.h\n@@ -31,7 +31,6 @@ typedef volatile ULONG __declspec(align(32)) atomic_uint;\n typedef volatile ULONG __declspec(align(32)) atomic_ulong;\n typedef volatile USHORT __declspec(align(16)) atomic_uint_fast16_t;\n typedef volatile ULONG64 __declspec(align(32)) atomic_uint_fast64_t;\n-typedef volatile ULONG64 __declspec(align(32)) atomic_uint_least64_t;\n \n typedef enum {\n     memory_order_relaxed,\n@@ -40,95 +39,26 @@ typedef enum {\n } msvc_atomic_memory_order;\n \n #define atomic_init(p_a, v)           atomic_store(p_a, v)\n-\n-static inline ULONG atomic_load_32(volatile ULONG *p_a)\n-{\n-    return (ULONG)InterlockedCompareExchange((LONG volatile *)p_a, 0, 0);\n-}\n-\n-static inline ULONG64 atomic_load_64(volatile ULONG64 *p_a)\n-{\n-    return (ULONG64)InterlockedCompareExchange64((LONG64 volatile *)p_a, 0, 0);\n-}\n-\n-#define atomic_load(p_a) \\\n-    (sizeof(*(p_a)) == 8 ? \\\n-        (uint64_t)atomic_load_64((volatile ULONG64 *)(p_a)) : \\\n-        (uint64_t)atomic_load_32((volatile ULONG *)(p_a)))\n-\n-static inline void atomic_store_32(volatile ULONG *p_a, ULONG v)\n-{\n-    InterlockedExchange((LONG volatile *)p_a, (LONG)v);\n-}\n-\n-static inline void atomic_store_64(volatile ULONG64 *p_a, ULONG64 v)\n-{\n-    InterlockedExchange64((LONG64 volatile *)p_a, (LONG64)v);\n-}\n-\n-#define atomic_store(p_a, v) \\\n-    do { \\\n-        if (sizeof(*(p_a)) == 8) \\\n-            atomic_store_64((volatile ULONG64 *)(p_a), (ULONG64)(v)); \\\n-        else \\\n-            atomic_store_32((volatile ULONG *)(p_a), (ULONG)(v)); \\\n-    } while (0)\n-\n+#define atomic_store(p_a, v)          InterlockedExchange((LONG*)p_a, v)\n+#define atomic_load(p_a)              InterlockedCompareExchange((LONG*)p_a, 0, 0)\n #define atomic_load_explicit(p_a, mo) atomic_load(p_a)\n #define atomic_store_explicit(p_a, v, mo) atomic_store(p_a, v)\n \n-static inline ULONG atomic_fetch_add_32(volatile ULONG *p_a, ULONG inc)\n-{\n-    return (ULONG)InterlockedExchangeAdd((LONG volatile *)p_a, (LONG)inc);\n-}\n-\n-static inline ULONG64 atomic_fetch_add_64(volatile ULONG64 *p_a, ULONG64 inc)\n-{\n-    return (ULONG64)InterlockedExchangeAdd64((LONG64 volatile *)p_a, (LONG64)inc);\n-}\n-\n-#define atomic_fetch_add(p_a, inc) \\\n-    (sizeof(*(p_a)) == 8 ? \\\n-        (uint64_t)atomic_fetch_add_64((volatile ULONG64 *)(p_a), (ULONG64)(inc)) : \\\n-        (uint64_t)atomic_fetch_add_32((volatile ULONG *)(p_a), (ULONG)(inc)))\n-\n-static inline ULONG atomic_fetch_sub_32(volatile ULONG *p_a, ULONG dec)\n-{\n-    return (ULONG)InterlockedExchangeAdd((LONG volatile *)p_a, -(LONG)dec);\n-}\n-\n-static inline ULONG64 atomic_fetch_sub_64(volatile ULONG64 *p_a, ULONG64 dec)\n-{\n-    return (ULONG64)InterlockedExchangeAdd64((LONG64 volatile *)p_a, -(LONG64)dec);\n-}\n-\n-#define atomic_fetch_sub(p_a, dec) \\\n-    (sizeof(*(p_a)) == 8 ? \\\n-        (uint64_t)atomic_fetch_sub_64((volatile ULONG64 *)(p_a), (ULONG64)(dec)) : \\\n-        (uint64_t)atomic_fetch_sub_32((volatile ULONG *)(p_a), (ULONG)(dec)))\n-\n+/*\n+ * TODO use a special call to increment/decrement\n+ * using InterlockedIncrement/InterlockedDecrement\n+ */\n+#define atomic_fetch_add(p_a, inc)    InterlockedExchangeAdd((LPLONG)p_a, inc)\n+#define atomic_fetch_sub(p_a, dec)    InterlockedExchangeAdd((LPLONG)p_a, -(int)(dec))\n #define atomic_fetch_add_explicit(p_a, inc, mo)   atomic_fetch_add(p_a, inc)\n #define atomic_fetch_sub_explicit(p_a, inc, mo)   atomic_fetch_sub(p_a, inc)\n-\n-static inline int atomic_compare_exchange_weak_32(volatile ULONG *obj, ULONG *expected, ULONG desired)\n+static inline int atomic_compare_exchange_weak(intptr_t *obj, intptr_t *expected, intptr_t desired)\n {\n-    ULONG old = *expected;\n-    *expected = (ULONG)InterlockedCompareExchange((LONG volatile *)obj, (LONG)desired, (LONG)old);\n-    return old == *expected;\n+    intptr_t old = *expected;\n+\t*expected = (intptr_t)InterlockedCompareExchange64((PVOID *)obj,(PVOID) desired, (PVOID)*expected);\n+\treturn old == *expected;\n }\n \n-static inline int atomic_compare_exchange_weak_64(volatile ULONG64 *obj, ULONG64 *expected, ULONG64 desired)\n-{\n-    ULONG64 old = *expected;\n-    *expected = (ULONG64)InterlockedCompareExchange64((LONG64 volatile *)obj, (LONG64)desired, (LONG64)old);\n-    return old == *expected;\n-}\n-\n-#define atomic_compare_exchange_weak(obj, expected, desired) \\\n-    (sizeof(*(obj)) == 8 ? \\\n-        atomic_compare_exchange_weak_64((volatile ULONG64 *)(obj), (ULONG64 *)(expected), (ULONG64)(desired)) : \\\n-        atomic_compare_exchange_weak_32((volatile ULONG *)(obj), (ULONG *)(expected), (ULONG)(desired)))\n-\n #endif /* ! stdatomic.h */\n \n #pragma warning(pop)\ndiff --git a/contrib/endian-shim.h b/contrib/endian-shim.h\nindex 177bc37..6af1b1a 100755\n--- a/contrib/endian-shim.h\n+++ b/contrib/endian-shim.h\n@@ -53,7 +53,7 @@\n #define be64toh(x) OSSwapBigToHostInt64(x)\n #define le64toh(x) OSSwapLittleToHostInt64(x)\n \n-#elif defined(__linux__) || defined(__GNU__) || defined(__EMSCRIPTEN__)\n+#elif defined(__linux__) || defined(__GNU__)\n # include <endian.h>\n # if !defined(htobe64)\n #  if __BYTE_ORDER == __LITTLE_ENDIAN\ndiff --git a/contrib/mbedtls/meson.build b/contrib/mbedtls/meson.build\nnew file mode 100644\nindex 0000000..e5aafae\n--- /dev/null\n+++ b/contrib/mbedtls/meson.build\n@@ -0,0 +1,111 @@\n+# librist. Copyright (c) 2020 SipRadius LLC. All right reserved.\n+# Author: Sergio Ammirata, Ph.D. <sergio@ammirata.net>\n+# SPDX-License-Identifier: BSD-2-Clause\n+\n+if not builtin_mbedtls\n+    mbedcrypto_lib = dependency('MbedTLS', required: false, method: 'cmake', modules: ['MbedTLS::mbedcrypto'])\n+    if not mbedcrypto_lib.found()\n+        mbedcrypto_lib = cc.find_library('mbedcrypto', required: required_library, has_headers: ['mbedtls/aes.h'])\n+        if not mbedcrypto_lib.found()\n+            builtin_mbedtls = true\n+        endif\n+    endif\n+    if mbedcrypto_lib.found() and mbedcrypto_lib.type_name() != 'internal'\n+        mbedcrypto_lib_found = true\n+\t\tmpi_random_test = '''\n+#include <mbedtls/bignum.h>\n+void test_has_func() { mbedtls_mpi_random(NULL, 0, NULL, NULL, NULL); }\n+'''\n+\t\tcdata.set10('MBEDTLS_HAS_MPI_RANDOM', cc.compiles(mpi_random_test, name: 'MbedTLS has mbedtls_mpi_random()', dependencies: mbedcrypto_lib))\n+    endif\n+endif\n+if builtin_mbedtls and not mbedcrypto_lib_found\n+    message('Using builtin mbedtls code')\n+    mbedcrypto_lib_includes = include_directories('library','include')\n+    add_project_arguments(['-D_FILE_OFFSET_BITS=64'], language: 'c')\n+    mbedcrypto_lib = declare_dependency(\n+            sources: [\n+                    'library/aes.c',\n+                    'library/aesni.c',\n+                    'library/arc4.c',\n+                    'library/aria.c',\n+                    'library/asn1parse.c',\n+                    'library/asn1write.c',\n+                    'library/base64.c',\n+                    'library/bignum.c',\n+                    'library/blowfish.c',\n+                    'library/camellia.c',\n+                    'library/ccm.c',\n+                    'library/chacha20.c',\n+                    'library/chachapoly.c',\n+                    'library/cipher.c',\n+                    'library/cipher_wrap.c',\n+                    'library/cmac.c',\n+                    'library/constant_time.c',\n+                    'library/ctr_drbg.c',\n+                    'library/des.c',\n+                    'library/dhm.c',\n+                    'library/ecdh.c',\n+                    'library/ecdsa.c',\n+                    'library/ecjpake.c',\n+                    'library/ecp.c',\n+                    'library/ecp_curves.c',\n+                    'library/entropy.c',\n+                    'library/entropy_poll.c',\n+                    'library/error.c',\n+                    'library/gcm.c',\n+                    'library/havege.c',\n+                    'library/hkdf.c',\n+                    'library/hmac_drbg.c',\n+                    'library/md.c',\n+                    'library/md2.c',\n+                    'library/md4.c',\n+                    'library/md5.c',\n+                    'library/memory_buffer_alloc.c',\n+                    'library/nist_kw.c',\n+                    'library/oid.c',\n+                    'library/padlock.c',\n+                    'library/pem.c',\n+                    'library/pk.c',\n+                    'library/pk_wrap.c',\n+                    'library/pkcs12.c',\n+                    'library/pkcs5.c',\n+                    'library/pkparse.c',\n+                    'library/pkwrite.c',\n+                    'library/platform.c',\n+                    'library/platform_util.c',\n+                    'library/poly1305.c',\n+                    'library/psa_crypto.c',\n+                    'library/psa_crypto_aead.c',\n+                    'library/psa_crypto_cipher.c',\n+                    'library/psa_crypto_client.c',\n+                    'library/psa_crypto_driver_wrappers.c',\n+                    'library/psa_crypto_ecp.c',\n+                    'library/psa_crypto_hash.c',\n+                    'library/psa_crypto_mac.c',\n+                    'library/psa_crypto_rsa.c',\n+                    'library/psa_crypto_se.c',\n+                    'library/psa_crypto_slot_management.c',\n+                    'library/psa_crypto_storage.c',\n+                    'library/psa_its_file.c',\n+                    'library/ripemd160.c',\n+                    'library/rsa.c',\n+                    'library/rsa_internal.c',\n+                    'library/sha1.c',\n+                    'library/sha256.c',\n+                    'library/sha512.c',\n+                    'library/threading.c',\n+                    'library/timing.c',\n+                    'library/version.c',\n+                    'library/version_features.c',\n+                    'library/xtea.c',],\n+            include_directories: mbedcrypto_lib_includes\n+        )\n+    inc += mbedcrypto_lib_includes\n+    mbedcrypto_lib_found = true\n+\tcdata.set10('MBEDTLS_HAS_MPI_RANDOM', false)\n+    # We could compile libmbedcrypto.a using make on contrib/mbedtls and copy to the build folder instead\n+    #my_mbedcrypto_inc = mbedcrypto_lib_includes\n+    #my_mbedcrypto_lib = static_library('mbedcrypto')\n+    #mbedcrypto_lib = declare_dependency(link_with : my_mbedcrypto_lib, include_directories : my_mbedcrypto_inc)\n+endif\ndiff --git a/contrib/pthread-shim.c b/contrib/pthread-shim.c\nindex 164a523..05bd5b2 100755\n--- a/contrib/pthread-shim.c\n+++ b/contrib/pthread-shim.c\n@@ -10,21 +10,16 @@\n #include <stdio.h>\n #ifdef _WIN32\n #if HAVE_PTHREADS\n-int pthread_cond_timedwait_us(pthread_cond_t *cond, pthread_mutex_t *mutex, uint64_t us)\n+int pthread_cond_timedwait_ms(pthread_cond_t *cond, pthread_mutex_t *mutex, uint32_t ms)\n {\n \ttimespec_t ts;\n \tstruct timeval tv;\n \tgettimeofday(&tv, NULL);\n-\tuint64_t odd = ((uint64_t)tv.tv_usec + us) * 1000;\n-\tts.tv_sec = tv.tv_sec + (long)(odd / 1000000000ULL);\n-\tts.tv_nsec = (long)(odd % 1000000000ULL);\n+\tuint64_t odd = (tv.tv_usec + (ms * 1000)) * 1000;\n+\tts.tv_sec = tv.tv_sec + odd / 1000000000ULL;\n+\tts.tv_nsec = odd % 1000000000ULL;\n \treturn pthread_cond_timedwait(cond, mutex, (const struct timespec *)&ts);\n }\n-\n-int pthread_cond_timedwait_ms(pthread_cond_t *cond, pthread_mutex_t *mutex, uint32_t ms)\n-{\n-\treturn pthread_cond_timedwait_us(cond, mutex, (uint64_t)ms * 1000);\n-}\n #else\n #include <errno.h>\n \n@@ -192,13 +187,6 @@ int pthread_cond_timedwait_ms(pthread_cond_t *cond, pthread_mutex_t *mutex, uint\n \treturn 0;\n }\n \n-/* SleepConditionVariableCS cannot express sub-millisecond waits, so round up:\n- * coarser than asked for, never shorter, and never a zero wait. */\n-int pthread_cond_timedwait_us(pthread_cond_t *cond, pthread_mutex_t *mutex, uint64_t us)\n-{\n-\treturn pthread_cond_timedwait_ms(cond, mutex, (uint32_t)((us + 999) / 1000));\n-}\n-\n int pthread_cond_signal(pthread_cond_t *cond)\n {\n \tif (cond == NULL) {\n@@ -358,20 +346,15 @@ int sem_post(sem_t *sem)\n \n // This is not part of the POSIX API\n // Convience function around pthread\n-int pthread_cond_timedwait_us(pthread_cond_t *cond, pthread_mutex_t *mutex, uint64_t us)\n+int pthread_cond_timedwait_ms(pthread_cond_t *cond, pthread_mutex_t *mutex, uint32_t ms)\n {\n \ttimespec_t ts;\n \tstruct timeval tv;\n \tgettimeofday(&tv, NULL);\n-\tuint64_t odd = ((uint64_t)tv.tv_usec + us) * 1000;\n-\tts.tv_sec = tv.tv_sec + (time_t)(odd / 1000000000ULL);\n-\tts.tv_nsec = (long)(odd % 1000000000ULL);\n+\tuint64_t odd =  (tv.tv_usec + (ms * 1000)) * 1000;\n+\tts.tv_sec = tv.tv_sec + odd / 1000000000ULL;\n+\tts.tv_nsec = odd % 1000000000ULL;\n \treturn pthread_cond_timedwait(cond, mutex, (const struct timespec*)&ts);\n }\n \n-int pthread_cond_timedwait_ms(pthread_cond_t *cond, pthread_mutex_t *mutex, uint32_t ms)\n-{\n-\treturn pthread_cond_timedwait_us(cond, mutex, (uint64_t)ms * 1000);\n-}\n-\n #endif\ndiff --git a/contrib/pthread-shim.h b/contrib/pthread-shim.h\nindex a5e0909..4ae0083 100755\n--- a/contrib/pthread-shim.h\n+++ b/contrib/pthread-shim.h\n@@ -30,9 +30,6 @@\n # define PTHREAD_START_FUNC(fname,aname) void *fname(void *aname)\n typedef void *(*pthread_start_func_t)(void *aname);\n RIST_PRIV int pthread_cond_timedwait_ms(pthread_cond_t *cond, pthread_mutex_t *mutex, uint32_t ms);\n-/* Sub-millisecond variant. Rounded up to a millisecond on Windows builds\n- * without pthreads, where the underlying wait cannot express less. */\n-RIST_PRIV int pthread_cond_timedwait_us(pthread_cond_t *cond, pthread_mutex_t *mutex, uint64_t us);\n #else\n typedef CRITICAL_SECTION pthread_mutex_t;\n typedef void pthread_mutexattr_t;\n@@ -66,8 +63,6 @@ RIST_PRIV int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr);\n RIST_PRIV int pthread_cond_destroy(pthread_cond_t *cond);\n RIST_PRIV int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);\n RIST_PRIV int pthread_cond_timedwait_ms(pthread_cond_t *cond, pthread_mutex_t *mutex, uint32_t reltime_ms);\n-/* Sub-millisecond variant; see the note on the implementation. */\n-RIST_PRIV int pthread_cond_timedwait_us(pthread_cond_t *cond, pthread_mutex_t *mutex, uint64_t us);\n \n RIST_PRIV int pthread_cond_signal(pthread_cond_t *cond);\n RIST_PRIV int pthread_cond_broadcast(pthread_cond_t *cond);\n@@ -94,9 +89,6 @@ RIST_PRIV int sem_wait(sem_t *sem);\n # define PTHREAD_START_FUNC(fname,aname) void *fname(void *aname)\n typedef void *(*pthread_start_func_t)(void *aname);\n RIST_PRIV int pthread_cond_timedwait_ms(pthread_cond_t *cond, pthread_mutex_t *mutex, uint32_t ms);\n-/* Sub-millisecond variant. Rounded up to a millisecond on Windows builds\n- * without pthreads, where the underlying wait cannot express less. */\n-RIST_PRIV int pthread_cond_timedwait_us(pthread_cond_t *cond, pthread_mutex_t *mutex, uint64_t us);\n #endif\n \n #endif /* __PTHREAD_SHIM_H */\ndiff --git a/debian/changelog b/debian/changelog\nindex dcb451e..07ab476 100644\n--- a/debian/changelog\n+++ b/debian/changelog\n@@ -1,68 +1,3 @@\n-librist (0.2.20+dfsg-1) unstable; urgency=medium\n-\n-  * [611440c] New upstream version 0.2.20+dfsg\n-\n- -- Florian Ernst <florian@debian.org>  Thu, 30 Jul 2026 17:12:20 +0200\n-\n-librist (0.2.19+dfsg-1) unstable; urgency=medium\n-\n-  * [0eceadc] New upstream version 0.2.19+dfsg\n-  * [20b08ac] d/copyright: update for new upstream release\n-  * [bbc035a] d/manpages/: update for new upstream release\n-  * [9082f7f] d/patches/: drop those now part of the upstream release,\n-    refresh remaining\n-  * [5537391] d/librist4.symbols: add new upstream symbols\n-\n- -- Florian Ernst <florian@debian.org>  Wed, 29 Jul 2026 15:26:48 +0200\n-\n-librist (0.2.18+dfsg-1) unstable; urgency=medium\n-\n-  * [01ce0e8] d/rules: actually export workaround for -latomic on\n-    m68k/powerpc/sh4\n-  * [412769d] New upstream version 0.2.18+dfsg\n-  * [deb88e9] d/copyright: update for new upstream release\n-  * [35fe5df] d/manpages/: update to match new upstream release\n-  * [480f0ec] d/patches/: remove those now applied upstream and refresh\n-    remaining\n-  * [c4e8a41] d/rules: new upstream release introduces overlength strings\n-  * [7e3b0ad] d/rules: don't default to DH_VERBOSE anymore\n-  * [7cf122d] d/patches/: add upstream patch fixing test issues in 0.2.18\n-  * [0373639] d/patches/: add upstream patch obsoleting the need for\n-    -Wno-overlength-strings\n-  * [0478466] d/librist4.symbols: add new upstream symbols\n-  * [fda9a05] d/patches/: add upstream patch fixing test issue on 32bit\n-\n- -- Florian Ernst <florian@debian.org>  Mon, 29 Jun 2026 21:09:24 +0200\n-\n-librist (0.2.17+dfsg-1) unstable; urgency=medium\n-\n-  * [187556e] New upstream version 0.2.16+dfsg\n-  * [406aba7] d/rules: add workaround for -latomic on m68k/powerpc/sh4\n-  * [11ee90d] New upstream version 0.2.17+dfsg\n-  * [ebf5cf5] d/copyright: update for new upstream release\n-  * [32c7af7] d/control: new upstream release supports LZ4 payload compression\n-  * [17a4fc3] d/patches/: refresh for new upstream release\n-  * [3068e42] d/patches/: add upstream patch for fixing srp_unit_test FAIL\n-    (Closes: #1138637)\n-  * [f108a2d] d/librist4.symbols: add new upstream symbols\n-\n- -- Florian Ernst <florian@debian.org>  Thu, 04 Jun 2026 12:05:50 +0200\n-\n-librist (0.2.15+dfsg-1) unstable; urgency=medium\n-\n-  * [794eecf] d/copyright: fully exclude contrib/mbedtls\n-  * [086e941] New upstream version 0.2.15+dfsg\n-  * [e4a8921] d/copyright: update for new upstream release\n-\n- -- Florian Ernst <florian@debian.org>  Mon, 18 May 2026 15:29:27 +0200\n-\n-librist (0.2.14+dfsg-1.1) unstable; urgency=medium\n-\n-  * Non-maintainer upload\n-  * Build with gnutls (Closes: #1135609)\n-\n- -- Bastian Germann <bage@debian.org>  Mon, 04 May 2026 19:50:19 +0200\n-\n librist (0.2.14+dfsg-1) unstable; urgency=medium\n \n   * [d4de6e3] New upstream version 0.2.14+dfsg\ndiff --git a/debian/control b/debian/control\nindex db75dad..42cf9b8 100644\n--- a/debian/control\n+++ b/debian/control\n@@ -10,8 +10,7 @@ Build-Depends:\n  pkgconf,\n  libmicrohttpd-dev,\n  libcjson-dev,\n- libgnutls28-dev,\n- liblz4-dev,\n+ libmbedtls-dev,\n  libcmocka-dev,\n Vcs-Browser: https://salsa.debian.org/debian/librist\n Vcs-Git: https://salsa.debian.org/debian/librist.git\n@@ -21,8 +20,7 @@ Section: libdevel\n Depends:\n  librist4 (= ${binary:Version}),\n  libcjson-dev,\n- libgnutls28-dev,\n- liblz4-dev,\n+ libmbedtls-dev,\n  ${misc:Depends},\n Architecture: any\n Multi-Arch: same\ndiff --git a/debian/copyright b/debian/copyright\nindex eb3e452..03520ec 100644\n--- a/debian/copyright\n+++ b/debian/copyright\n@@ -4,7 +4,11 @@ Source: https://code.videolan.org/rist/librist\n Files-Excluded: librist-*/docs\n   librist-*/contrib/contrib_cJSON\n   librist-*/contrib/lz4\n-  librist-*/contrib/mbedtls\n+  librist-*/contrib/mbedtls/include\n+  librist-*/contrib/mbedtls/library\n+  librist-*/contrib/mbedtls/LICENSE\n+  librist-*/contrib/mbedtls/Makefile\n+  librist-*/contrib/mbedtls/refresh_mbedtls_source.sh\n Comment: Binary documentation files with no source excluded via debian/watch and uscan invocation;\n   in addition embedded copies of code that are not used anyways excluded as well.\n \n@@ -14,6 +18,7 @@ License: BSD-2-clause\n \n Files: contrib/fastpbkdf2.c\n  contrib/fastpbkdf2.h\n+ contrib/mbedtls/meson.build\n  contrib/poll_win.c\n  contrib/pthread-shim.c\n  contrib/pthread-shim.h\n@@ -23,17 +28,12 @@ Files: contrib/fastpbkdf2.c\n  contrib/time-shim.c\n  contrib/time-shim.h\n  include/librist/meson.build\n- include/librist/transport.h\n  include/librist/tun.h\n  include/librist/tunnel.h\n  include/meson.build\n  meson.build\n- src/adv.c\n- src/adv_ctrl.c\n  src/crypto/crypto.c\n  src/crypto/crypto-private.h\n- src/crypto/eap_v4_crypto.c\n- src/crypto/eap_v4_crypto.h\n  src/crypto/psk.c\n  src/crypto/psk.h\n  src/flow.c\n@@ -43,22 +43,15 @@ Files: contrib/fastpbkdf2.c\n  src/log-private.h\n  src/mpegts.c\n  src/mpegts.h\n- src/proto/adv.h\n  src/proto/eap.c\n  src/proto/eap.h\n  src/rist.c\n  src/rist-common.c\n- src/rist_pacer.c\n- src/rist_pacer.h\n  src/rist-private.h\n  src/rist_ref.c\n  src/rist_ref.h\n  src/stats.c\n- src/transport.c\n- src/transport-private.h\n- src/tun_cidr.c\n  src/tun_darwin.c\n- src/tun_emscripten.c\n  src/tun_linux.c\n  src/tun_win.c\n  src/udp.c\n@@ -66,23 +59,8 @@ Files: contrib/fastpbkdf2.c\n  src/udpsocket.c\n  test/meson.build\n  test/rist/meson.build\n- test/rist/test_bonded_carrier_never_starved_netns.sh\n- test/rist/test_bonded_leg_flap_netns.sh\n- test/rist/test_bonded_leg_stall_grace_netns.sh\n- test/rist/test_caller_socket_rebind.c\n- test/rist/test_eap_authenticator_restart.c\n- test/rist/test_multipath_cname.c\n- test/rist/test_multipath_leg_rebind.c\n- test/rist/test_nat_port_rebind.c\n- test/rist/test_oob_read.c\n- test/rist/test_psk_cname_hijack.c\n- test/rist/test_reflector.c\n- test/rist/test_rtt_mute_bonding.c\n  test/rist/test_sender_restart.c\n  test/rist/test_send_receive.c\n- test/rist/unit/rist_pacer_test.c\n- test/rist/unit/test_adv_roundtrip.c\n- test/rist/unit/test_eap_v4_crypto.c\n  tools/meson.build\n  tools/oob_shared.c\n  tools/oob_shared.h\n@@ -94,7 +72,6 @@ Files: contrib/fastpbkdf2.c\n  tools/risturlhelp.h\n  tools/srp_shared.c\n  tools/srp_shared.h\n- tools/string-shim.h\n  tools/udp2udp.c\n  tools/yamlparse.c\n  tools/yamlparse.h\ndiff --git a/debian/librist4.symbols b/debian/librist4.symbols\nindex 6593c20..1140dee 100644\n--- a/debian/librist4.symbols\n+++ b/debian/librist4.symbols\n@@ -34,7 +34,6 @@ librist.so.4 librist4 #MINVER#\n  rist_parse_udp_address2@Base 0.2.7+dfsg\n  rist_parse_udp_address@Base 0.2.7+dfsg\n  rist_peer_config_defaults_set@Base 0.2.6+dfsg\n- rist_peer_config_defaults_set_versioned@Base 0.2.18+dfsg\n  rist_peer_config_free2@Base 0.2.6+dfsg\n  rist_peer_config_free@Base 0.2.6+dfsg\n  rist_peer_create@Base 0.2.6+dfsg\n@@ -53,12 +52,9 @@ librist.so.4 librist4 #MINVER#\n  rist_receiver_data_notify_fd_set@Base 0.2.6+dfsg\n  rist_receiver_data_read2@Base 0.2.6+dfsg\n  rist_receiver_data_read@Base 0.2.6+dfsg\n- rist_receiver_flow_attr_callback_set@Base 0.2.17+dfsg\n  rist_receiver_nack_type_set@Base 0.2.6+dfsg\n  rist_receiver_session_timeout_callback_set@Base 0.2.13+dfsg\n- rist_receiver_set_cbr_output@Base 0.2.19+dfsg\n  rist_receiver_set_output_fifo_size@Base 0.2.6+dfsg\n- rist_recovery_depth_set@Base 0.2.18+dfsg\n  rist_recovery_rtt_multiplier_set@Base 0.2.13+dfsg\n  rist_sender_create@Base 0.2.6+dfsg\n  rist_sender_data_fd_set@Base 0.2.13+dfsg\n@@ -73,11 +69,9 @@ librist.so.4 librist4 #MINVER#\n  rist_start@Base 0.2.6+dfsg\n  rist_stats_callback_set@Base 0.2.6+dfsg\n  rist_stats_free@Base 0.2.6+dfsg\n- rist_transport_set@Base 0.2.17+dfsg\n  rist_tun_bring_up@Base 0.2.13+dfsg\n  rist_tun_close@Base 0.2.13+dfsg\n  rist_tun_open@Base 0.2.13+dfsg\n- rist_tun_parse_cidr@Base 0.2.19+dfsg\n  rist_tun_read@Base 0.2.13+dfsg\n  rist_tun_set_ip@Base 0.2.13+dfsg\n  rist_tun_set_mtu@Base 0.2.13+dfsg\n@@ -87,10 +81,8 @@ librist.so.4 librist4 #MINVER#\n  udpsocket_close@Base 0.2.6+dfsg\n  udpsocket_get_buffer_send_size@Base 0.2.6+dfsg\n  udpsocket_get_buffer_size@Base 0.2.6+dfsg\n- udpsocket_join_mcast_group@Base 0.2.17+dfsg\n  udpsocket_open@Base 0.2.6+dfsg\n  udpsocket_open_bind@Base 0.2.6+dfsg\n- udpsocket_open_bind_mcast@Base 0.2.17+dfsg\n  udpsocket_open_connect@Base 0.2.6+dfsg\n  udpsocket_parse_url@Base 0.2.6+dfsg\n  udpsocket_parse_url_parameters@Base 0.2.6+dfsg\n@@ -102,9 +94,7 @@ librist.so.4 librist4 #MINVER#\n  udpsocket_sendto@Base 0.2.6+dfsg\n  udpsocket_set_buffer_send_size@Base 0.2.6+dfsg\n  udpsocket_set_buffer_size@Base 0.2.6+dfsg\n- udpsocket_set_dontfragment@Base 0.2.17+dfsg\n  udpsocket_set_mcast_iface@Base 0.2.6+dfsg\n- udpsocket_set_mcast_ttl@Base 0.2.17+dfsg\n  udpsocket_set_nonblocking@Base 0.2.6+dfsg\n  udpsocket_set_optimal_buffer_send_size@Base 0.2.6+dfsg\n  udpsocket_set_optimal_buffer_size@Base 0.2.6+dfsg\ndiff --git a/debian/manpages/ristreceiver.1 b/debian/manpages/ristreceiver.1\nindex a339c10..4c7f072 100644\n--- a/debian/manpages/ristreceiver.1\n+++ b/debian/manpages/ristreceiver.1\n@@ -21,9 +21,6 @@ UDP or RTP stream.\n Use tun://@ to write udp data to a tun device defined\n using the \\fB\\-t\\fR option.\n .TP\n-\\fB\\-\\-cbr\\-output\\fR 0|1\n-Space the output at the stream rate (CBR consumers)\n-.TP\n \\fB\\-b\\fR | \\fB\\-\\-buffer\\fR value\n Default buffer size for packet retransmissions\n .TP\ndiff --git a/debian/manpages/ristsender.1 b/debian/manpages/ristsender.1\nindex 4f71057..7b6dcb3 100644\n--- a/debian/manpages/ristsender.1\n+++ b/debian/manpages/ristsender.1\n@@ -92,9 +92,6 @@ IP for metrics HTTP server to listen on\n \\fB\\-\\-metrics\\-unix\\fR\n Unix socket to expose metrics on\n .TP\n-\\fB\\-\\-blind\\-send\\fR\n-Stream regardless of peer state (fire-and-forget mode)\n-.TP\n \\fB\\-h\\fR | \\fB\\-\\-help\\fR\n Show this help\n .TP\ndiff --git a/debian/manpages/risttunnel.1 b/debian/manpages/risttunnel.1\nindex 4287c9a..9d77b29 100644\n--- a/debian/manpages/risttunnel.1\n+++ b/debian/manpages/risttunnel.1\n@@ -13,7 +13,7 @@ risttunnel allows one to tunnel over RIST with ARQ packet recovery\n .SS \"Where OPTIONS are:\"\n .TP\n \\fB\\-l\\fR, \\fB\\-\\-local\\-ip\\fR IP/PREFIX\n-\\fBMandatory Option:\\fR Local tunnel IP (e.g. 10.0.0.1/24 or fd00::1/64)\n+\\fBMandatory Option:\\fR Local tunnel IP (e.g. 10.0.0.1/24)\n .TP\n \\fB\\-o\\fR, \\fB\\-\\-output\\-url\\fR URL\n \\fBMandatory Option:\\fR RIST URL for sending\n@@ -42,12 +42,8 @@ Stats interval in ms (default: 1000, 0=off)\n \\fB\\-v\\fR, \\fB\\-\\-verbose\\-level\\fR INT\n Log level 0\\-6 (default: 3)\n .TP\n-\\fB\\-1\\fR, \\fB\\-\\-single\\-port\\fR\n-One UDP port (forward=RTP/ARQ, reverse=OOB).\n-Give exactly one of -o (caller) or -b (listener).\n-.TP\n \\fB\\-p\\fR, \\fB\\-\\-profile\\fR INT\n-RIST profile: 0=simple, 1=main, 2=advanced (default: 2)\n+RIST profile: 0=simple, 1=main (default: 1)\n .TP\n \\fB\\-F\\fR, \\fB\\-\\-srpfile\\fR PATH\n EAP\\-SRP verifier file (server side)\ndiff --git a/debian/patches/expect_multicast_tests_to_fail.patch b/debian/patches/expect_multicast_tests_to_fail.patch\nindex e3431cf..db0f678 100644\n--- a/debian/patches/expect_multicast_tests_to_fail.patch\n+++ b/debian/patches/expect_multicast_tests_to_fail.patch\n@@ -1,18 +1,12 @@\n From: Florian Ernst <florian@debian.org>\n-Date: Wed, 29 Jul 2026 15:10:35 +0200\n-Subject: We cannot assume to have multicast traffic allowed in the build\n- hosts\n-\n+Subject: We cannot assume to have multicast traffic allowed in the build hosts\n Forwarded: not-needed\n----\n- test/rist/meson.build | 6 +++---\n- 1 file changed, 3 insertions(+), 3 deletions(-)\n \n-diff --git a/test/rist/meson.build b/test/rist/meson.build\n-index bf2c260..3a8ea4a 100644\n---- a/test/rist/meson.build\n-+++ b/test/rist/meson.build\n-@@ -287,9 +287,9 @@ test('Simple profile unicast packet loss 10%', test_send_receive, args: ['0', 'r\n+Index: git/test/rist/meson.build\n+===================================================================\n+--- git.orig/test/rist/meson.build\n++++ git/test/rist/meson.build\n+@@ -43,9 +43,9 @@ test('Simple profile unicast packet loss\n  test('Simple profile unicast packet loss 25%', test_send_receive, args: ['0', 'rist://@127.0.0.1:3234', 'rist://127.0.0.1:3234', '25'],suite: ['simple', 'unicast'])\n  if (host_machine.system() != 'darwin')\n      #Multicast\ndiff --git a/debian/patches/system_cJSON.patch b/debian/patches/system_cJSON.patch\nindex af611b5..7aed8a9 100644\n--- a/debian/patches/system_cJSON.patch\n+++ b/debian/patches/system_cJSON.patch\n@@ -1,16 +1,11 @@\n From: Florian Ernst <florian@debian.org>\n-Date: Wed, 29 Jul 2026 15:10:35 +0200\n Subject: Ensure to only reference system cJSON during build\n-\n Forwarded: not-needed\n----\n- tools/meson.build | 2 +-\n- 1 file changed, 1 insertion(+), 1 deletion(-)\n \n-diff --git a/tools/meson.build b/tools/meson.build\n-index 0387627..7013bdb 100644\n---- a/tools/meson.build\n-+++ b/tools/meson.build\n+Index: git/tools/meson.build\n+===================================================================\n+--- git.orig/tools/meson.build\n++++ git/tools/meson.build\n @@ -40,7 +40,7 @@ if builtin_cjson\n  endif\n  \ndiff --git a/debian/rules b/debian/rules\nindex 1f1b8aa..43edd0c 100755\n--- a/debian/rules\n+++ b/debian/rules\n@@ -1,24 +1,10 @@\n #!/usr/bin/make -f\n-#export DH_VERBOSE = 1\n+export DH_VERBOSE = 1\n export DEB_BUILD_MAINT_OPTIONS = hardening=+all\n \n-ifneq (,$(filter m68k powerpc sh4,${DEB_HOST_ARCH}))\n-# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=81358\n-# on these platforms gcc does not link with -latomic, resulting in\n-#   /usr/bin/m68k-linux-gnu-ld.bfd: ./obj-m68k-linux-gnu/../src/rist.c:1189:(.text+0xff8): undefined reference to `__atomic_fetch_add_8'\n-#   /usr/bin/m68k-linux-gnu-ld.bfd: ./obj-m68k-linux-gnu/../src/rist.c:1374:(.text+0x2592): undefined reference to `__atomic_load_8'\n-#   /usr/bin/m68k-linux-gnu-ld.bfd: ./obj-m68k-linux-gnu/../src/rist-common.c:1011:(.text+0x2310): undefined reference to `__atomic_fetch_add_8'\n-#   collect2: error: ld returned 1 exit status\n-# include a workaround for now\n-export DEB_LDFLAGS_MAINT_APPEND += -latomic\n-endif\n-\n %:\n \tdh $@\n \n-override_dh_auto_configure:\n-\tdh_auto_configure -- -Duse_mbedtls=false -Duse_gnutls=true -Duse_nettle=true\n-\n override_dh_auto_test-arch:\n \t# must be executed consecutively lest they interfer with each other!\n \t# and grant the tests more time which is sorely needed with noisy neighbors\ndiff --git a/include/common/attributes.h b/include/common/attributes.h\nindex c3514a2..349236f 100755\n--- a/include/common/attributes.h\n+++ b/include/common/attributes.h\n@@ -56,10 +56,7 @@ struct __attribute__((packed)) sname sbody;\n #ifdef _WIN64\n #ifdef __GNUC__\n #ifndef __MINGW32__\n-#ifndef _SSIZE_T_DEFINED\n typedef long long ssize_t;\n-#define _SSIZE_T_DEFINED\n-#endif\n enum\n {\n     POLLERR=0x1,\n@@ -83,16 +80,10 @@ struct pollfd;\n #endif\n #else\n #include <stdint.h>\n-#ifndef _SSIZE_T_DEFINED\n typedef int64_t ssize_t;\n-#define _SSIZE_T_DEFINED\n-#endif\n #endif\n #else\n-#ifndef _SSIZE_T_DEFINED\n typedef signed int ssize_t;\n-#define _SSIZE_T_DEFINED\n-#endif\n #endif\n #endif\n \ndiff --git a/include/librist/headers.h b/include/librist/headers.h\nindex 4c89c24..4278eab 100644\n--- a/include/librist/headers.h\n+++ b/include/librist/headers.h\n@@ -28,7 +28,7 @@ extern \"C\" {\n \n /* Track PROTOCOL and API changes */\n #define RIST_PEER_UDPSOCKET_VERSION (0)\n-#define RIST_UDP_CONFIG_VERSION (2)\n+#define RIST_UDP_CONFIG_VERSION (1)\n \n /* Error Codes */\n #define RIST_ERR_MALLOC -1\n@@ -108,14 +108,6 @@ struct rist_udp_config\n \tuint16_t stream_id;\n \tenum librist_multiplex_mode multiplex_mode;\n \tchar multiplex_filter[RIST_MAX_STRING_SHORT];//Future usage\n-\t/* Multicast TTL / hop limit (0 = platform default) */\n-\tuint32_t multicast_ttl;\n-\t/* SSM source address for IGMPv3 source-specific multicast (empty = ASM) */\n-\tchar multicast_source[RIST_MAX_STRING_LONG];\n-\t/* version 2: ?cbr-output=, context-wide, so a caller reading it off several\n-\t * URLs must reject disagreement. _set separates \"0\" from \"not asked\". */\n-\tint cbr_output;\n-\tint cbr_output_set;\n };\n \n #ifdef __cplusplus\ndiff --git a/include/librist/librist.h b/include/librist/librist.h\nindex 160dd25..4dc589f 100755\n--- a/include/librist/librist.h\n+++ b/include/librist/librist.h\n@@ -21,7 +21,6 @@\n #include \"headers.h\"\n #include \"tun.h\"\n #include \"tunnel.h\"\n-#include \"transport.h\"\n \n #ifdef __cplusplus\n extern \"C\" {\n@@ -60,39 +59,6 @@ RIST_API int rist_jitter_max_set(struct rist_ctx *ctx, int t);\n  */\n RIST_API int rist_recovery_rtt_multiplier_set(struct rist_ctx *ctx, int multiplier);\n \n-/**\n- * @brief Set the Advanced-profile recovery (retransmit) buffer depth\n- *\n- * Sizes the retransmission ring used by the Advanced profile. `depth` is the\n- * base-2 exponent of the ring size: the ring holds (65536 << depth) packets,\n- * i.e. 2^depth times the 16-bit base buffer. The addressable NACK window is\n- * roughly half the ring. Each step up doubles the buffer (and the RAM). This\n- * is equivalent to the ?recovery-depth= URL parameter (same numeric value).\n- *\n- * Depth sizing (ring capacity / approx NACK window):\n- *   0   (1x)      65536 / 32768 packets\n- *   3   (8x)     524288 / 262144 packets   (default, historical behavior)\n- *   6  (64x)    4194304 / 2097152 packets\n- *  16 (65536x)  4294967296 / 2147483648 packets  (full 32-bit seq space)\n- *\n- * Valid range is RIST_RECOVERY_DEPTH_MIN..RIST_RECOVERY_DEPTH_MAX (0..16);\n- * out-of-range values are clamped. Large depths are limited by available RAM\n- * and will fail to allocate.\n- *\n- * This ONLY affects the Advanced profile. Simple and Main are inherently\n- * 16-bit and remain capped at 65536 packets (32768-packet window) regardless\n- * of this setting.\n- *\n- * Must be called after rist_sender_create() / rist_receiver_create() and\n- * BEFORE rist_start().\n- *\n- * @param ctx   RIST context\n- * @param depth desired recovery-depth exponent (0..16)\n- * @return 0 on success, -1 on bad context or if called after rist_start(),\n- *         -2 on allocation failure\n- */\n-RIST_API int rist_recovery_depth_set(struct rist_ctx *ctx, uint8_t depth);\n-\n /**\n  * @brief Starts the RIST sender or receiver\n  *\ndiff --git a/include/librist/meson.build b/include/librist/meson.build\nindex 45c5392..22235f5 100644\n--- a/include/librist/meson.build\n+++ b/include/librist/meson.build\n@@ -34,7 +34,6 @@ if should_install\n \t\t\t\t\t'sender.h',\n \t\t\t\t\t'stats.h',\n \t\t\t\t\t'tun.h',\n-\t\t\t\t\t'transport.h',\n \t\t\t\t\t'tunnel.h',\n \t\t\t\t\t'udpsocket.h',\n \t\t\t\t\t'urlparam.h',\ndiff --git a/include/librist/peer.h b/include/librist/peer.h\nindex 7a8c9b6..62c4566 100644\n--- a/include/librist/peer.h\n+++ b/include/librist/peer.h\n@@ -37,21 +37,10 @@ struct rist_peer;\n #define RIST_DEFAULT_MIN_RETRIES (6)\n #define RIST_DEFAULT_MAX_RETRIES (20)\n #define RIST_DEFAULT_VERBOSE_LEVEL RIST_LOG_INFO\n-#define RIST_DEFAULT_PROFILE RIST_PROFILE_ADVANCED\n+#define RIST_DEFAULT_PROFILE RIST_PROFILE_MAIN\n #define RIST_DEFAULT_SESSION_TIMEOUT (2000)\n #define RIST_DEFAULT_KEEPALIVE_INTERVAL (1000)\n #define RIST_DEFAULT_TIMING_MODE RIST_TIMING_MODE_SOURCE\n-#define RIST_DEFAULT_RECOVERY_PRIORITY (0)\n-/* Dynamic RTT-based bonded-leg muting (sender). Disabled by default. */\n-#define RIST_DEFAULT_RTT_DROP (0)           /* smoothed-RTT ceiling ms; 0 = disabled */\n-#define RIST_DEFAULT_RTT_RESTORE (0)        /* restore low-water ms; 0 = derive (80% of drop) */\n-#define RIST_DEFAULT_RTT_DROP_SETTLE (2000) /* dwell ms before a drop/restore transition */\n-#define RIST_DEFAULT_RTT_DROP_TRICKLE (100) /* 1-in-N duplicate rate while muted (~1%, warm restore); 0 = hard mute */\n-\n-/* Special value for rist_peer_config.weight: a peer configured with this\n- * weight receives a duplicate of every packet instead of taking part in\n- * the weighted load-balancing rotation. */\n-#define RIST_PEER_WEIGHT_DUPLICATE (0)\n \n enum rist_timing_mode\n {\n@@ -74,37 +63,7 @@ enum rist_congestion_control_mode\n \tRIST_CONGESTION_CONTROL_MODE_AGGRESSIVE = 2\n };\n \n-enum librist_split_mode\n-{\n-\tLIBRIST_SPLIT_MODE_OFF  = 0,\n-\tLIBRIST_SPLIT_MODE_AUTO = 1,\n-\tLIBRIST_SPLIT_MODE_HALF = 2,\n-};\n-\n-enum librist_merge_mode\n-{\n-\tLIBRIST_MERGE_MODE_OFF   = 0,\n-\tLIBRIST_MERGE_MODE_PAIRS = 1,\n-\tLIBRIST_MERGE_MODE_AUTO  = 2,\n-};\n-\n-#define RIST_PEER_CONFIG_VERSION (6)\n-\n-/* Advanced-profile recovery depth: the base-2 exponent of the retransmission\n- * ring size. The ring holds (65536 << depth) packets, i.e. 2^depth times the\n- * 16-bit base buffer (65536), and the addressable NACK window is roughly half\n- * the ring. Each step doubles the buffer:\n- *\n- *   depth  multiplier   ring packets   approx NACK window\n- *     0       1x            65536            32768\n- *     3       8x           524288           262144   (default, legacy behavior)\n- *     6      64x          4194304          2097152\n- *    16   65536x      4294967296       2147483648   (full 32-bit seq space)\n- *\n- * Simple/Main are inherently 16-bit and ignore this setting. */\n-#define RIST_RECOVERY_DEPTH_MIN     (0)\n-#define RIST_RECOVERY_DEPTH_DEFAULT (3)   /* 8x the 16-bit base = legacy default */\n-#define RIST_RECOVERY_DEPTH_MAX     (16)  /* full 32-bit sequence space */\n+#define RIST_PEER_CONFIG_VERSION (0)\n \n struct rist_peer_config\n {\n@@ -126,8 +85,6 @@ struct rist_peer_config\n \n \t/* Recovery options */\n \tenum rist_recovery_mode recovery_mode;\n-\t/* Ceiling on payload plus retransmissions, not on payload alone. 0 means\n-\t * unset, not unlimited, and is replaced with the default. */\n \tuint32_t recovery_maxbitrate; /* kbps */\n \tuint32_t recovery_maxbitrate_return; /* kbps */\n \tuint32_t recovery_length_min; /* ms */\n@@ -136,7 +93,7 @@ struct rist_peer_config\n \tuint32_t recovery_rtt_min; /* ms */\n \tuint32_t recovery_rtt_max; /* ms */\n \n-\t/* Load balancing weight (use RIST_PEER_WEIGHT_DUPLICATE for duplication) */\n+\t/* Load balancing weight (use 0 for duplication) */\n \tuint32_t weight;\n \n \t/* Encryption */\n@@ -161,101 +118,14 @@ struct rist_peer_config\n \tenum rist_timing_mode timing_mode;\n \tchar srp_username[RIST_MAX_STRING_LONG];\n \tchar srp_password[RIST_MAX_STRING_LONG];\n-\n-\tuint32_t split_mode;      /* enum librist_split_mode (sender) */\n-\tuint32_t merge_mode;      /* enum librist_merge_mode (receiver) */\n-\n-\t/* Reflector mode: when enabled on a receiver listener, incoming data\n-\t * packets are transparently forwarded to all other connected peers\n-\t * (one-to-many fan-out).  Disabled by default; enable via URL parameter\n-\t * ?reflector=1.  Main Profile only.\n-\t *\n-\t * Trade-offs vs rist2rist (per-subscriber ARQ relay):\n-\t * - No per-subscriber retry buffer: the reflector does not cache\n-\t *   data; retransmissions rely entirely on the publisher's buffer.\n-\t * - Retransmissions fan out to ALL subscribers, not just the one\n-\t *   that NACKed — bandwidth cost scales with subscriber count.\n-\t * - Recovery RTT ~ RTT(sub<->reflector) + RTT(reflector<->pub),\n-\t *   roughly 2x a direct connection.\n-\t * - No per-subscriber congestion control or stats: all NACKs\n-\t *   appear to the publisher as coming from a single peer.\n-\t *\n-\t * Best suited for low subscriber counts with clean last-mile links.\n-\t * For high fan-out, lossy last-mile, or per-subscriber buffer\n-\t * tuning, use rist2rist instead. */\n-\tint reflector;\n-\n-\t/* Multicast TTL / hop limit (0 = platform default, typically 1) */\n-\tuint32_t multicast_ttl;\n-\n-\t/* SSM source address for IGMPv3 source-specific multicast (empty = ASM) */\n-\tchar multicast_source[RIST_MAX_STRING_LONG];\n-\n-\t/* Local UDP port for caller (non-listening) peers.\n-\t * 0 = ephemeral (OS-assigned), non-zero = bind to this port.\n-\t * Not all platforms support binding caller sockets to a fixed port. */\n-\tuint16_t local_port;\n-\n-\tint srp_compat_legacy;    /* 0 = RFC 5054 PAD (default), 1 = pre-0.2.16 unpadded */\n-\n-\t/* Wire profile parsed from ?profile=.  Test profile_set first:\n-\t * profile == RIST_PROFILE_SIMPLE on a zero-initialised config is\n-\t * indistinguishable from \"value not provided\". */\n-\tenum rist_profile profile;\n-\tint profile_set;\n-\n-\t/* Retransmission (NACK) routing preference for the receiver.\n-\t * When a flow is carried by more than one RTCP-capable peer, the\n-\t * receiver sends each NACK to the eligible peer with the highest\n-\t * recovery_priority (ties broken by lowest measured RTT).  0\n-\t * (default) preserves the legacy behaviour of selecting the\n-\t * lowest-RTT eligible peer regardless of priority.  Set this >0 on\n-\t * the peer that holds the retransmission buffer when a lower-RTT\n-\t * peer carrying the same flow cannot answer NACKs (e.g. a\n-\t * duplicate/relay feed with no retransmit cache). */\n-\tuint32_t recovery_priority;\n-\n-\t/* Advanced-profile recovery depth (?recovery-depth= URL knob): base-2\n-\t * exponent of the retransmission ring size, RIST_RECOVERY_DEPTH_MIN..MAX.\n-\t * Defaults to RIST_RECOVERY_DEPTH_DEFAULT. Only meaningful on the Advanced\n-\t * profile and only before rist_start(). Version 5+. */\n-\tuint8_t recovery_depth;\n-\n-\t/* Dynamic RTT-based bonded-leg muting (sender, Version 6+). A leg whose\n-\t * smoothed RTT holds above rtt_drop for rtt_drop_settle is pulled from the\n-\t * payload rotation (retransmits reroute to a healthy leg) and rejoins once\n-\t * it holds below rtt_restore for a longer dwell (quick to drop, slow to\n-\t * rejoin). The last healthy leg is never muted. All ms; 0 disables. */\n-\tuint32_t rtt_drop;         /* smoothed-RTT ceiling; 0 = disabled */\n-\tuint32_t rtt_restore;      /* restore low-water; 0 = derive as 80% of rtt_drop */\n-\tuint32_t rtt_drop_settle;  /* dwell before muting (rejoin waits longer) */\n-\tuint32_t rtt_drop_trickle; /* 1-in-N redundant duplicate on a muted leg; 0 = hard mute */\n };\n \n-/**\n- * @brief Populate a preallocated peer_config structure with library default values (versioned)\n- *\n- * @return 0 on success or non-zero on error.\n- */\n-RIST_API int rist_peer_config_defaults_set_versioned(struct rist_peer_config *peer_config, int version);\n-\n /**\n  * @brief Populate a preallocated peer_config structure with library default values\n  *\n  * @return 0 on success or non-zero on error.\n  */\n-#ifdef LIBRIST_INTERNAL\n RIST_API int rist_peer_config_defaults_set(struct rist_peer_config *peer_config);\n-#else\n-#if defined(__cplusplus) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L)\n-static inline int rist_peer_config_defaults_set(struct rist_peer_config *peer_config) {\n-\treturn rist_peer_config_defaults_set_versioned(peer_config, RIST_PEER_CONFIG_VERSION);\n-}\n-#else\n-#define rist_peer_config_defaults_set(peer_config) \\\n-\trist_peer_config_defaults_set_versioned((peer_config), RIST_PEER_CONFIG_VERSION)\n-#endif\n-#endif\n \n /**\n  * @brief Parses rist url for peer config data (encryption, compression, etc)\n@@ -285,11 +155,6 @@ RIST_API int rist_peer_config_free2(struct rist_peer_config **peer_config);\n  *\n  * One sender can send data to multiple peers.\n  *\n- * If config->profile_set is non-zero (version >= 4), the call may\n- * change the context wire profile to config->profile when invoked\n- * before rist_start() and before any other peer has fixed it.  Once\n- * fixed, any later call whose ?profile= disagrees returns -1.\n- *\n  * @param ctx RIST context\n  * @param[out] peer Store the new peer pointer\n  * @param config a pointer to the struct rist_peer_config, which contains\ndiff --git a/include/librist/receiver.h b/include/librist/receiver.h\nindex 8d63ce1..4315bf3 100644\n--- a/include/librist/receiver.h\n+++ b/include/librist/receiver.h\n@@ -153,60 +153,6 @@ RIST_API void rist_receiver_data_block_free2(struct rist_data_block **block);\n  */\n RIST_API int rist_receiver_data_notify_fd_set(struct rist_ctx *ctx, int fd);\n \n-/**\n- * @brief Flow Attribute callback function (Advanced Profile, TR-06-3 Section 5.3.7)\n- *\n- * Called when a Flow Attribute message (CI=0x8001) is received from a sender.\n- * The JSON string contains session/flow metadata as defined by TR-06-3 Section 5.4.\n- * The json pointer is valid only for the duration of the callback.\n- *\n- * @param arg optional user data set via rist_receiver_flow_attr_callback_set\n- * @param peer the peer that sent the flow attribute\n- * @param json NUL-terminated JSON string containing flow attributes\n- * @param json_len length of the JSON string (excluding NUL terminator)\n- * @return int, ignored.\n- */\n-typedef int (*receiver_flow_attr_callback_t)(void *arg, struct rist_peer *peer,\n-                                             const char *json, size_t json_len);\n-\n-/**\n- * @brief Enable flow attribute callback channel\n- *\n- * Call to enable reception of Advanced Profile Flow Attribute messages.\n- * When set, the callback is invoked each time a CI=0x8001 control message\n- * arrives from a sender.\n- *\n- * @param ctx RIST receiver context\n- * @param cb The function that will be called when a flow attribute is received\n- * @param arg the extra argument passed to the callback\n- * @return 0 on success, -1 on error\n- */\n-RIST_API int rist_receiver_flow_attr_callback_set(struct rist_ctx *ctx,\n-                                                   receiver_flow_attr_callback_t cb,\n-                                                   void *arg);\n-\n-/**\n- * @brief Enable or disable CBR output pacing\n- *\n- * The output loop releases every packet whose deadline has passed, so packets\n- * that arrived together leave together. With pacing on, they are spread at the\n- * stream's measured rate instead, which matters wherever the consumer recovers\n- * a clock from output timing.\n- *\n- * The setting is context-wide, not per output target: pacing happens in the\n- * flow's output loop, before any fan-out to individual destinations, so all of\n- * them share one release schedule. Only flows created after this call are\n- * affected, so call it before rist_start().\n- *\n- * Equivalent to ?cbr-output= on an output URL, and the two must agree;\n- * whichever is applied first wins and a later disagreement is refused.\n- *\n- * @param ctx RIST receiver context\n- * @param enable true to pace the output, false to release on arrival\n- * @return 0 on success, -1 on error or on conflict with an earlier setting\n- */\n-RIST_API int rist_receiver_set_cbr_output(struct rist_ctx *ctx, bool enable);\n-\n #ifdef __cplusplus\n }\n #endif\ndiff --git a/include/librist/stats.h b/include/librist/stats.h\nindex 1d2dc1d..2c58dce 100644\n--- a/include/librist/stats.h\n+++ b/include/librist/stats.h\n@@ -32,21 +32,6 @@ struct rist_stats_sender_peer\n \tdouble quality;\n \t/* current RTT */\n \tuint32_t rtt;\n-\t/* total bytes sent (payload + overhead) */\n-\tuint64_t sent_bytes;\n-\t/* total bytes retransmitted */\n-\tuint64_t retransmitted_bytes;\n-\t/* context profile (enum rist_profile: 0 simple, 1 main, 2 advanced) */\n-\tuint8_t profile;\n-\t/* Non-zero when Advanced framing is active on the wire. An Advanced\n-\t * context uses Main framing until the peer advertises Advanced support\n-\t * (TR-06-3 Section 9), so profile alone does not imply Advanced framing. */\n-\tuint8_t advanced_active;\n-\t/* Non-zero while this bonded leg is currently muted by RTT auto-mute\n-\t * (?rtt-drop=): its unique payload is diverted to the healthier legs. */\n-\tuint8_t rtt_muted;\n-\t/* Cumulative number of RTT-triggered mute events on this leg. */\n-\tuint32_t rtt_mute_events;\n };\n \n struct rist_stats_receiver_peer\n@@ -67,8 +52,6 @@ struct rist_stats_receiver_peer\n \tsize_t bandwidth;\n \t/* average bandwidth */\n \tsize_t avg_bandwidth;\n-\t/* total data bytes received from this peer */\n-\tuint64_t received_bytes;\n };\n \n struct rist_stats_receiver_flow\n@@ -109,19 +92,8 @@ struct rist_stats_receiver_flow\n \tuint32_t rtt;\n \t/* peers */\n \tstruct rist_stats_receiver_peer *peers;\n-\t/* average buffer duration in milliseconds (dynamic RIST receiver buffer fill level) */\n+\t/* average buffer duration in microseconds (dynamic RIST receiver buffer fill level) */\n \tuint64_t avg_buffer_time;\n-\t/* total data bytes received (payload only, excluding headers) */\n-\tuint64_t received_bytes;\n-\t/* context profile (enum rist_profile: 0 simple, 1 main, 2 advanced) */\n-\tuint8_t profile;\n-\t/* on-wire sequence-number width: 16 (Simple/Main framing) or 32\n-\t * (Advanced framing). A flow reads 16 until the source upgrades framing. */\n-\tuint8_t seq_bits;\n-\t/* non-zero when Advanced framing is active on this flow (Advanced context\n-\t * on 32-bit framing); reads 0 when an Advanced context stays on Main\n-\t * framing and for Main/Simple contexts. */\n-\tuint8_t advanced_active;\n };\n \n enum rist_stats_type\n@@ -130,7 +102,7 @@ enum rist_stats_type\n \tRIST_STATS_RECEIVER_FLOW\n };\n \n-#define RIST_STATS_VERSION (3)\n+#define RIST_STATS_VERSION (1)\n #define RIST_SENDER_STATS_VERSION (0)\n \n struct rist_stats\ndiff --git a/include/librist/transport.h b/include/librist/transport.h\ndeleted file mode 100644\nindex 9387d85..0000000\n--- a/include/librist/transport.h\n+++ /dev/null\n@@ -1,105 +0,0 @@\n-/*\n- * Copyright © 2024 SipRadius LLC\n- * All rights reserved.\n- *\n- * SPDX-License-Identifier: BSD-2-Clause\n- */\n-\n-#ifndef LIBRIST_TRANSPORT_H\n-#define LIBRIST_TRANSPORT_H\n-\n-#include \"common.h\"\n-#include <stddef.h>\n-#include <stdint.h>\n-\n-#ifdef _WIN32\n-#include <winsock2.h>\n-#include <ws2tcpip.h>\n-#include <basetsd.h>     /* SSIZE_T; lowercase spelling for mingw cross-builds */\n-#ifndef _SSIZE_T_DEFINED\n-typedef SSIZE_T ssize_t;\n-#define _SSIZE_T_DEFINED\n-#endif\n-#else\n-#include <sys/types.h>\n-#include <sys/socket.h>\n-#include <poll.h>\n-#endif\n-\n-#ifdef __cplusplus\n-extern \"C\" {\n-#endif\n-\n-struct rist_ctx;\n-\n-/**\n- * @brief Transport operations vtable for abstracting network I/O.\n- *\n- * All function pointers follow POSIX socket semantics.  The default\n- * implementation (used when no custom ops are installed) delegates\n- * directly to the real syscalls, so existing behaviour is unchanged.\n- *\n- * For non-POSIX backends (e.g. WebAssembly/WebTransport, unit-test\n- * harnesses, userspace network stacks) the application provides its\n- * own implementations via rist_transport_set().\n- *\n- * @a opaque is passed as the first argument to every callback and\n- * can point to arbitrary per-context state owned by the backend.\n- */\n-struct rist_transport_ops {\n-\tvoid *opaque;\n-\n-\t/**\n-\t * Send a datagram to a specific destination.\n-\t * Semantics match POSIX sendto(2) — returns bytes sent or -1.\n-\t */\n-\tssize_t (*sendto)(void *opaque, int fd, const void *buf, size_t len,\n-\t                  int flags, const struct sockaddr *addr, socklen_t addrlen);\n-\n-\t/**\n-\t * Receive a datagram, capturing the source address.\n-\t * Semantics match POSIX recvfrom(2) — returns bytes received or -1.\n-\t */\n-\tssize_t (*recvfrom)(void *opaque, int fd, void *buf, size_t len,\n-\t                    int flags, struct sockaddr *addr, socklen_t *addrlen);\n-\n-\t/**\n-\t * Wait for I/O readiness on a set of descriptors.\n-\t * Semantics match POSIX poll(2) — returns the number of ready\n-\t * descriptors, 0 on timeout, or -1 on error.\n-\t */\n-\tint (*poll)(void *opaque, struct pollfd *fds, int nfds, int timeout_ms);\n-\n-\t/**\n-\t * Scatter-gather send (optional).\n-\t * Semantics match POSIX sendmsg(2).  If NULL, the library\n-\t * linearises the iov and falls back to the sendto callback.\n-\t */\n-#ifndef _WIN32\n-\tssize_t (*sendmsg)(void *opaque, int fd, const struct msghdr *msg,\n-\t                   int flags);\n-#endif\n-};\n-\n-/**\n- * @brief Install custom transport operations on a RIST context.\n- *\n- * Must be called after rist_sender_create() / rist_receiver_create()\n- * but BEFORE rist_start().  The library takes a shallow copy of @a ops;\n- * the caller must keep the pointees (callbacks, opaque) alive for the\n- * lifetime of @a ctx.\n- *\n- * Passing NULL restores the default (POSIX socket) transport.\n- *\n- * @param ctx   RIST sender or receiver context\n- * @param ops   Transport ops to install, or NULL for defaults\n- * @return 0 on success, -1 on error (e.g. context already started)\n- */\n-RIST_API int rist_transport_set(struct rist_ctx *ctx,\n-                                const struct rist_transport_ops *ops);\n-\n-#ifdef __cplusplus\n-}\n-#endif\n-\n-#endif /* LIBRIST_TRANSPORT_H */\ndiff --git a/include/librist/tun.h b/include/librist/tun.h\nindex fae01ae..7a00141 100644\n--- a/include/librist/tun.h\n+++ b/include/librist/tun.h\n@@ -52,22 +52,12 @@ RIST_API int rist_tun_read(int fd, uint8_t *buf, size_t len);\n  */\n RIST_API int rist_tun_write(int fd, const uint8_t *buf, size_t len);\n \n-/**\n- * @brief Parse an IPv4 or IPv6 address with optional CIDR prefix.\n- *\n- * Accepts \"ADDR\" (defaults to /32 or /128) or \"ADDR/PREFIX\". The address\n- * family is inferred from the presence of ':' in ADDR.\n- *\n- * @return  0 on success, -1 on invalid input.\n- */\n-RIST_API int rist_tun_parse_cidr(const char *cidr, char *ip_out, size_t ip_len, int *prefix_out);\n-\n /**\n  * @brief Assign an IP address and prefix length to the TUN interface.\n  *\n  * @param dev         Interface name (e.g. \"utun3\").\n- * @param ip          IPv4 or IPv6 address string (e.g. \"10.0.0.1\", \"fd00::1\").\n- * @param prefix_len  Prefix length (0-32 for IPv4, 0-128 for IPv6).\n+ * @param ip          IP address string (e.g. \"10.0.0.1\").\n+ * @param prefix_len  Prefix length (e.g. 24).\n  * @return            0 on success, -1 on failure.\n  */\n RIST_API int rist_tun_set_ip(const char *dev, const char *ip, int prefix_len);\ndiff --git a/include/librist/udpsocket.h b/include/librist/udpsocket.h\nindex 5e64621..9ea9d7f 100644\n--- a/include/librist/udpsocket.h\n+++ b/include/librist/udpsocket.h\n@@ -56,10 +56,7 @@ extern \"C\" {\n #endif\n \n /*** Public API ***/\n-/* Historical fixed target / floor for the UDP socket buffers (1 MB). */\n #define UDPSOCKET_SOCK_BUFSIZE (1048576)\n-/* Upper target requested before the kernel clamps to net.core.{r,w}mem_max. */\n-#define UDPSOCKET_SOCK_BUFSIZE_MAX (8388608)\n \n typedef struct udpsocket_url_param {\n \tchar *key;\n@@ -70,36 +67,9 @@ typedef struct udpsocket_url_param {\n  *\n  * Returns: socket descriptor, -1 for error (errno is set)\n  *\n- * Note: callers that bind() or connect() the socket should call\n- *       udpsocket_set_dontfragment() afterwards; on Windows,\n- *       IPV6_DONTFRAG setsockopt on an unbound IPv6 socket can\n- *       cause a subsequent bind() to fail with WSAEADDRNOTAVAIL.\n  */\n RIST_API int udpsocket_open(uint16_t af);\n \n-/* Set IP-level \"don't fragment\" on a UDP socket.\n- *\n- * Best-effort: platforms that do not support the option keep the\n- * default (fragmenting) behaviour; the failure is logged as a warning.\n- *\n- * On Windows, this must be called AFTER bind()/connect() for IPv6\n- * sockets (see rist/librist#212).\n- */\n-RIST_API void udpsocket_set_dontfragment(int sd, uint16_t af);\n-\n-/* Set multicast TTL (IPv4) or hop limit (IPv6) on a UDP socket.\n- * A value of 0 means \"use platform default\".\n- * Returns 0 on success, -1 on error. */\n-RIST_API int udpsocket_set_mcast_ttl(int sd, uint16_t af, uint32_t ttl);\n-\n-/* Join a multicast group.  [sd] must already be bound.\n- * [miface]: multicast interface name or IP (NULL or empty = default route).\n- * [sa]: multicast group sockaddr.\n- * [family]: AF_INET or AF_INET6.\n- * [ssm_source]: SSM source IP for IGMPv3 source filtering (NULL = ASM).\n- * Returns 0 on success, -1 on error. */\n-RIST_API int udpsocket_join_mcast_group(int sd, const char *miface, struct sockaddr *sa, uint16_t family, const char *ssm_source);\n-\n /* Open a udp socket and binds it to local [host] + [port].\n  *\n  * binds to multicast interface [mciface], (if not NULL).\n@@ -110,13 +80,6 @@ RIST_API int udpsocket_join_mcast_group(int sd, const char *miface, struct socka\n  */\n RIST_API int udpsocket_open_bind(const char *host, uint16_t port, const char *mciface);\n \n-/* Extended open+bind with multicast TTL and SSM source address support.\n- * ttl: multicast TTL (0 = platform default).\n- * ssm_source: SSM source IP address for IGMPv3 source filtering (NULL = ASM).\n- */\n-RIST_API int udpsocket_open_bind_mcast(const char *host, uint16_t port, const char *mciface,\n-                                        uint32_t ttl, const char *ssm_source);\n-\n /*\n  * Try to set RX buffer to 1Mbyte and fallback to 256Kbytes if that fails\n  * Returns -1 on error, 0 on success.\ndiff --git a/include/librist/urlparam.h b/include/librist/urlparam.h\nindex a2a0083..7e14ceb 100644\n--- a/include/librist/urlparam.h\n+++ b/include/librist/urlparam.h\n@@ -21,15 +21,11 @@\n #define RIST_URL_PARAM_CNAME \"cname\"\n #define RIST_URL_PARAM_VIRT_DST_PORT \"virt-dst-port\"\n #define RIST_URL_PARAM_WEIGHT \"weight\"\n-#define RIST_URL_PARAM_RECOVERY_PRIORITY \"recovery-priority\"\n #define RIST_URL_PARAM_MIFACE \"miface\"\n-#define RIST_URL_PARAM_MCAST_TTL \"ttl\"\n-#define RIST_URL_PARAM_MCAST_SOURCE \"source\"\n #define RIST_URL_PARAM_SESSION_TIMEOUT \"session-timeout\"\n #define RIST_URL_PARAM_KEEPALIVE_INT \"keepalive-interval\"\n #define RIST_URL_PARAM_SRP_USERNAME \"username\"\n #define RIST_URL_PARAM_SRP_PASSWORD \"password\"\n-#define RIST_URL_PARAM_SRP_COMPAT \"srp-compat\"  //0|1 (1 => pre-0.2.16 SRP wire format)\n /* Less common URL parameters */\n #define RIST_URL_PARAM_BUFFER_SIZE_MIN \"buffer-min\"\n #define RIST_URL_PARAM_BUFFER_SIZE_MAX \"buffer-max\"\n@@ -50,18 +46,7 @@\n /* Rist additional parameter names */\n #define RIST_URL_PARAM_VIRT_SRC_PORT \"virt-src-port\"\n #define RIST_URL_PARAM_PROFILE \"profile\"\n-#define RIST_URL_PARAM_SPLIT_MODE \"split\"\n-#define RIST_URL_PARAM_MERGE_MODE \"merge\"\n #define RIST_URL_PARAM_VERBOSE_LEVEL \"verbose-level\"\n #define RIST_URL_PARAM_RTT_MULTIPLIER \"rtt-multiplier\"\n-#define RIST_URL_PARAM_REFLECTOR \"reflector\"\n-#define RIST_URL_PARAM_LOCAL_PORT \"local-port\"\n-#define RIST_URL_PARAM_RECOVERY_DEPTH \"recovery-depth\"  //0..16, ring = 65536<<depth packets, default 3 (Advanced profile)\n-#define RIST_URL_PARAM_CBR_OUTPUT \"cbr-output\"  //0|1, on a udp/rtp output url: space receiver output at the stream's measured rate (assumes CBR)\n-/* Dynamic RTT-based bonded-leg muting (sender) */\n-#define RIST_URL_PARAM_RTT_DROP \"rtt-drop\"            //smoothed-RTT ceiling in ms; 0 = disabled\n-#define RIST_URL_PARAM_RTT_RESTORE \"rtt-restore\"      //restore low-water in ms; 0 = derive from rtt-drop\n-#define RIST_URL_PARAM_RTT_DROP_SETTLE \"rtt-drop-settle\" //dwell in ms before drop/restore\n-#define RIST_URL_PARAM_RTT_DROP_TRICKLE \"rtt-drop-trickle\" //1-in-N duplicate rate on a muted leg; 0 = hard mute\n \n #endif /* LIBRIST_URL_PARAM_H */\ndiff --git a/meson.build b/meson.build\nindex 01c5d5e..77b60b3 100755\n--- a/meson.build\n+++ b/meson.build\n@@ -4,7 +4,7 @@\n # SPDX-License-Identifier: BSD-2-Clause\n \n project('libRIST', 'c',\n-\tversion: '0.2.20',\n+\tversion: '0.2.14',\n \tdefault_options: ['c_std=c99', 'warning_level=3', 'libdir=lib'],\n         meson_version: '>= 0.51.0')\n \n@@ -17,9 +17,9 @@ cdata = configuration_data()\n #If any interfaces have been added, removed, or changed since the last update, increment current, and set revision to 0.\n #If any interfaces have been added since the last public release, then increment age.\n #If any interfaces have been removed or changed since the last public release, then set age to 0.\n-librist_abi_current = 16\n-librist_abi_revision = 1\n-librist_abi_age = 12\n+librist_abi_current = 10\n+librist_abi_revision = 2\n+librist_abi_age = 6\n librist_soversion = librist_abi_current - librist_abi_age\n librist_version = '@0@.@1@.@2@'.format(librist_abi_current - librist_abi_age, librist_abi_age, librist_abi_revision)\n \n@@ -30,7 +30,7 @@ librist_version = '@0@.@1@.@2@'.format(librist_abi_current - librist_abi_age, li\n #PATCH not used (doesn't make sense for API version, remains here for backwards compat)\n \n librist_api_version_major = 4\n-librist_api_version_minor = 13\n+librist_api_version_minor = 7\n librist_api_version_patch = 0\n \n librist_src_root = meson.current_source_dir()\n@@ -40,7 +40,7 @@ platform_files = []\n inc = []\n inc += include_directories('.', 'src', 'include/librist', 'include', 'contrib')\n \n-builtin_lz4 = get_option('builtin_lz4')\n+#builtin_lz4 = get_option('builtin_lz4')\n builtin_cjson = get_option('builtin_cjson')\n builtin_mbedtls = get_option('builtin_mbedtls')\n \n@@ -84,8 +84,7 @@ if host_machine.system() == 'windows'\n \tplatform_files += 'src/tun_win.c'\n \tdeps += [\n \t\tcc.find_library('ws2_32'),\n-\t\tcc.find_library('iphlpapi'),\n-\t\tcc.find_library('bcrypt')\n+\t\tcc.find_library('iphlpapi')\n \t]\n \n windows_version_test = '''\n@@ -106,16 +105,15 @@ windows_version_test = '''\n \t\t\tthreads = dependency('threads')\n \t\t\tdeps += threads\n \t\t\tcdata.set('HAVE_PTHREADS', 1)\n+\t\t\t# Use has_header_symbol so mingw-w64's static inline clock_gettime\n+\t\t\t# (declared in <pthread_time.h> pulled in by <time.h>) is detected.\n+\t\t\t# has_function does a linker test and would miss it because\n+\t\t\t# winpthreads only exports clock_gettime32/clock_gettime64.\n+\t\t\thave_clock_gettime = cc.has_header_symbol('time.h', 'clock_gettime', args : test_args)\n \t\tendif\n \telse\n \t\thave_pthreads = false\n \tendif\n-\t# Use has_header_symbol so mingw-w64's static inline clock_gettime\n-\t# (in <time.h>, regardless of winpthreads) is detected.\n-\t# Must stay outside the have_mingw_pthreads block: modern mingw-w64\n-\t# ships the inline unconditionally and contrib/time-shim.c must\n-\t# not redefine it.\n-\thave_clock_gettime = cc.has_header_symbol('time.h', 'clock_gettime', args : test_args)\n \tif cc.get_id() == 'msvc'\n \t\t# librist uses C11 features (stdatomic.h, mixed declarations).\n \t\t# MSVC only exposes them under /std:c11 or /std:c17.\n@@ -161,23 +159,16 @@ void test(void) {\n \telif host_machine.system() == 'gnu'\n \t\ttest_args += '-D_GNU_SOURCE'\n \t\tadd_project_arguments('-D_GNU_SOURCE', language: 'c')\n-\telif host_machine.system() == 'emscripten'\n-\t\tplatform_files += 'src/tun_emscripten.c'\n-\t\tadd_project_arguments('-D_GNU_SOURCE', language: 'c')\n \tendif\n \tlibrist_soversion = librist_soversion\n-\tif host_machine.system() == 'emscripten'\n-\t\thave_clock_gettime = true\n-\telse\n-\t\thave_clock_gettime = cc.has_function('clock_gettime', prefix : '#include <time.h>', args : test_args)\n-\t\tif not have_clock_gettime and host_machine.system() != 'darwin'\n-\t\t\tlib_rt = cc.find_library('rt', required: false)\n-\t\t\thave_clock_gettime = cc.has_function('clock_gettime', prefix : '#include <time.h>', args : test_args, dependencies : lib_rt)\n-\t\t\tif not have_clock_gettime\n-\t\t\t\terror('clock_gettime not found')\n-\t\t\tendif\n-\t\t\tdeps += [ lib_rt ]\n+\thave_clock_gettime = cc.has_function('clock_gettime', prefix : '#include <time.h>', args : test_args)\n+\tif not have_clock_gettime and host_machine.system() != 'darwin'\n+\t\tlib_rt = cc.find_library('rt', required: false)\n+\t\thave_clock_gettime = cc.has_function('clock_gettime', prefix : '#include <time.h>', args : test_args, dependencies : lib_rt)\n+\t\tif not have_clock_gettime\n+\t\t\terror('clock_gettime not found')\n \t\tendif\n+\t\tdeps += [ lib_rt ]\n \tendif\n \tadd_project_arguments(['-Wshadow', '-pedantic-errors'], language: 'c')\n \tadd_project_arguments(cc.get_supported_arguments([\n@@ -191,10 +182,8 @@ void test(void) {\n \t\t'-Wmaybe-uninitialized',\n \t\t'-Wno-error=deprecated-declarations'\n \t\t]), language : 'c')\n-\tif host_machine.system() != 'emscripten'\n-\t\tthreads = [ dependency('threads') ]\n-\tendif\n-\tif host_machine.system() != 'freebsd' and host_machine.system() != 'emscripten'\n+\tthreads = [ dependency('threads') ]\n+\tif host_machine.system() != 'freebsd'\n \t  add_project_arguments(cc.get_supported_arguments([\n             '-Watomic-implicit-seq-cst']), language: 'c')\n \tendif\n@@ -266,22 +255,6 @@ if builtin_cjson\n \t\t\t\t\t\t\t\t\tinclude_directories : include_directories('contrib/contrib_cJSON'))\n endif\n \n-if not builtin_lz4\n-\tlz4_lib = dependency('liblz4', required: false)\n-\tif not lz4_lib.found()\n-\t\tlz4_lib = cc.find_library('lz4', required: required_library, has_headers: ['lz4.h'])\n-\t\tif not lz4_lib.found()\n-\t\t\tbuiltin_lz4 = true\n-\t\tendif\n-\tendif\n-endif\n-if builtin_lz4\n-\tmessage('Using builtin LZ4 library')\n-\tlz4_lib = declare_dependency(\n-\t\tsources: 'contrib/lz4/lz4.c',\n-\t\tinclude_directories : include_directories('contrib/lz4'))\n-endif\n-\n crypto_deps = []\n \n mbedcrypto_lib_found = false\n@@ -333,13 +306,11 @@ endif\n \n have_srp = mbedcrypto_lib_found or use_gnutls\n \n-platform_files += 'src/crypto/random.c'\n-\n if have_srp\n     platform_files += 'src/proto/eap.c'\n \tplatform_files += 'src/crypto/srp_constants.c'\n \tplatform_files += 'src/crypto/srp.c'\n-\tplatform_files += 'src/crypto/eap_v4_crypto.c'\n+\tplatform_files += 'src/crypto/random.c'\n endif\n \n cdata.set10('HAVE_MBEDTLS', mbedcrypto_lib_found)\n@@ -354,11 +325,8 @@ librist = library('librist',\n \t'src/proto/rtp.c',\n \t'src/proto/rist_time.c',\n \t'src/flow.c',\n-\t'src/rist_pacer.c',\n \t'src/logging.c',\n \t'src/network.c',\n-\t'src/adv.c',\n-\t'src/adv_ctrl.c',\n \t'src/rist.c',\n \t'src/rist-common.c',\n \t'src/rist_ref.c',\n@@ -366,11 +334,9 @@ librist = library('librist',\n \t'src/mpegts.c',\n \t'src/peer.c',\n \t'src/udp.c',\n-\t'src/transport.c',\n \t'src/stats.c',\n \t'src/udpsocket.c',\n \t'src/libevsocket.c',\n-\t'src/tun_cidr.c',\n \t'contrib/stdio-shim.c',\n \t'contrib/time-shim.c',\n \t'contrib/pthread-shim.c',\n@@ -382,7 +348,6 @@ librist = library('librist',\n \t\tdeps,\n \t\tstdatomic_dependency,\n \t\tcjson_lib,\n-\t\tlz4_lib,\n \t],\n \tname_prefix : '',\n \tversion: librist_version,\ndiff --git a/meson_options.txt b/meson_options.txt\nindex a63bb72..70afbe1 100644\n--- a/meson_options.txt\n+++ b/meson_options.txt\n@@ -1,6 +1,6 @@\n option('static_analyze', type : 'boolean', value : false)\n option('test', type : 'boolean', value : true)\n-option('builtin_lz4', type: 'boolean', value: false)\n+#option('builtin_lz4', type: 'boolean', value: false)\n option('builtin_cjson', type: 'boolean', value: false)\n option('builtin_mbedtls', type: 'boolean', value: false)\n option('built_tools', type: 'boolean', value: true)\ndiff --git a/src/adv.c b/src/adv.c\ndeleted file mode 100644\nindex 339794c..0000000\n--- a/src/adv.c\n+++ /dev/null\n@@ -1,260 +0,0 @@\n-/*\n- * Copyright © 2019-2020 SipRadius LLC\n- * Copyright © 2024-2026 SipRadius LLC\n- * All rights reserved.\n- *\n- * SPDX-License-Identifier: BSD-2-Clause\n- *\n- * VSF TR-06-3:2024 — RIST Advanced Profile packet parser and builder.\n- */\n-\n-#include \"proto/adv.h\"\n-#include <string.h>\n-\n-size_t rist_adv_header_size(const struct rist_adv_params *params)\n-{\n-\tsize_t sz = RIST_ADV_HEADER_MIN; /* RTP (12) + ext (4) */\n-\n-\tif (params->flow_id)\n-\t\tsz += RIST_ADV_FLOW_ID_SIZE;\n-\n-\tsz += rist_adv_psk_hdr_size(params->psk_mode);\n-\n-\tif (params->lpc_mode == RIST_ADV_LPC_FIELD_PRESENT && params->compression)\n-\t\tsz += RIST_ADV_COMPRESSION_SIZE;\n-\n-\tif (params->pfd)\n-\t\tsz += RIST_ADV_PFD_SIZE;\n-\n-\tif (params->hdr_ext && params->hdr_ext_len > 0)\n-\t\tsz += params->hdr_ext_len;\n-\n-\treturn sz;\n-}\n-\n-int rist_adv_parse(const uint8_t *buf, size_t len,\n-                   struct rist_adv_parsed *out)\n-{\n-\tif (!buf || !out || len < RIST_ADV_HEADER_MIN)\n-\t\treturn -1;\n-\n-\tmemset(out, 0, sizeof(*out));\n-\n-\tconst struct rist_rtp_hdr *rtp = (const struct rist_rtp_hdr *)buf;\n-\n-\t/* Validate RTP: V=2 */\n-\tif ((rtp->flags & 0xC0) != 0x80)\n-\t\treturn -1;\n-\n-\t/* PT must be 127 (or a dynamic PT >= 96 if SDP is in use) */\n-\tuint8_t pt = rtp->payload_type & 0x7F;\n-\tif (pt != RIST_ADV_PT && pt < 96)\n-\t\treturn -1;\n-\n-\tout->rtp_padding = !!(rtp->flags & 0x20);\n-\tout->rtp_ext_present = !!(rtp->flags & 0x10);\n-\n-\t/* CC must be 0 per spec */\n-\tuint8_t cc = rtp->flags & 0x0F;\n-\n-\tout->timestamp = ntohl(rtp->ts);\n-\tout->ssrc = ntohl(rtp->ssrc);\n-\n-\tsize_t offset = sizeof(struct rist_rtp_hdr); /* 12 */\n-\n-\t/* Skip CSRC entries (should be 0, but be safe) */\n-\toffset += (size_t)cc * 4;\n-\tif (offset > len)\n-\t\treturn -1;\n-\n-\t/* Skip RFC 3550 RTP Header Extension if X=1 (line 4 of Figure 1) */\n-\tif (out->rtp_ext_present) {\n-\t\tif (offset + 4 > len)\n-\t\t\treturn -1;\n-\t\tuint16_t ext_len_words = ntohs(*(const uint16_t *)(buf + offset + 2));\n-\t\toffset += 4 + (size_t)ext_len_words * 4;\n-\t\tif (offset > len)\n-\t\t\treturn -1;\n-\t}\n-\n-\t/* Profile-defined extension (line 5 of Figure 1) */\n-\tif (offset + RIST_ADV_EXT_SIZE > len)\n-\t\treturn -1;\n-\n-\tconst struct rist_adv_ext *ext =\n-\t\t(const struct rist_adv_ext *)(buf + offset);\n-\toffset += RIST_ADV_EXT_SIZE;\n-\n-\tout->seq = rist_adv_seq32(rtp, ext);\n-\tout->first_frag  = !!(ext->flags & RIST_ADV_FLAG_F);\n-\tout->last_frag   = !!(ext->flags & RIST_ADV_FLAG_L);\n-\tout->expedite    = !!(ext->flags & RIST_ADV_FLAG_E);\n-\tout->retransmit  = !!(ext->flags & RIST_ADV_FLAG_R);\n-\tout->has_flow_id = !!(ext->flags & RIST_ADV_FLAG_I);\n-\tout->has_pfd     = !!(ext->flags & RIST_ADV_FLAG_P);\n-\tout->has_hdr_ext = !!(ext->flags & RIST_ADV_FLAG_H);\n-\n-\tout->psk_mode = rist_adv_get_psk(ext);\n-\tout->lpc_mode = rist_adv_get_lpc(ext);\n-\tout->enc_type = rist_adv_get_type(ext);\n-\n-\tout->has_psk = (out->psk_mode > 0);\n-\tout->has_compression = (out->lpc_mode == RIST_ADV_LPC_FIELD_PRESENT);\n-\n-\t/* Optional fields must be consumed in order per spec:\n-\t *   Flow ID, PSK Hash, PSK Nonce, PSK IV,\n-\t *   Compression, PFD, RIST Header Extension */\n-\n-\t/* Flow ID (4 bytes, if I=1) */\n-\tif (out->has_flow_id) {\n-\t\tif (offset + RIST_ADV_FLOW_ID_SIZE > len)\n-\t\t\treturn -1;\n-\t\tout->flow_id = (const struct rist_adv_flow_id *)(buf + offset);\n-\t\toffset += RIST_ADV_FLOW_ID_SIZE;\n-\t}\n-\n-\t/* PSK fields (variable, per Table 1) */\n-\tif (rist_adv_psk_has_hash(out->psk_mode)) {\n-\t\tif (offset + RIST_ADV_PSK_HASH_SIZE > len)\n-\t\t\treturn -1;\n-\t\tout->psk_hash = buf + offset;\n-\t\toffset += RIST_ADV_PSK_HASH_SIZE;\n-\t}\n-\tif (rist_adv_psk_has_nonce(out->psk_mode)) {\n-\t\tif (offset + RIST_ADV_PSK_NONCE_SIZE > len)\n-\t\t\treturn -1;\n-\t\tout->psk_nonce = buf + offset;\n-\t\toffset += RIST_ADV_PSK_NONCE_SIZE;\n-\t}\n-\tif (rist_adv_psk_has_iv(out->psk_mode)) {\n-\t\tif (offset + RIST_ADV_PSK_IV_SIZE > len)\n-\t\t\treturn -1;\n-\t\tout->psk_iv = buf + offset;\n-\t\toffset += RIST_ADV_PSK_IV_SIZE;\n-\t}\n-\n-\t/* Payload Compression field (4 bytes, if LPC==3) */\n-\tif (out->has_compression) {\n-\t\tif (offset + RIST_ADV_COMPRESSION_SIZE > len)\n-\t\t\treturn -1;\n-\t\tout->compression = buf + offset;\n-\t\toffset += RIST_ADV_COMPRESSION_SIZE;\n-\t}\n-\n-\t/* Payload Format Descriptor (4 bytes, if P=1) */\n-\tif (out->has_pfd) {\n-\t\tif (offset + RIST_ADV_PFD_SIZE > len)\n-\t\t\treturn -1;\n-\t\tout->pfd = (const struct rist_adv_pfd *)(buf + offset);\n-\t\toffset += RIST_ADV_PFD_SIZE;\n-\t}\n-\n-\t/* RIST Header Extension (if H=1) — same format as RFC 3550 extension */\n-\tif (out->has_hdr_ext) {\n-\t\tif (offset + 4 > len)\n-\t\t\treturn -1;\n-\t\tuint16_t hdr_ext_words = ntohs(*(const uint16_t *)(buf + offset + 2));\n-\t\tsize_t hdr_ext_total = 4 + (size_t)hdr_ext_words * 4;\n-\t\tif (offset + hdr_ext_total > len)\n-\t\t\treturn -1;\n-\t\tout->hdr_ext = buf + offset;\n-\t\tout->hdr_ext_len = hdr_ext_total;\n-\t\toffset += hdr_ext_total;\n-\t}\n-\n-\t/* Remaining bytes are payload */\n-\tout->payload = buf + offset;\n-\tout->payload_len = len - offset;\n-\n-\treturn 0;\n-}\n-\n-int rist_adv_build(uint8_t *buf, const struct rist_adv_params *params,\n-                   const uint8_t *payload, size_t payload_len)\n-{\n-\tif (!buf || !params)\n-\t\treturn -1;\n-\n-\tsize_t offset = 0;\n-\n-\t/* RTP header (12 bytes) */\n-\tstruct rist_rtp_hdr *rtp = (struct rist_rtp_hdr *)buf;\n-\trtp->flags = RIST_ADV_RTP_FLAGS;\n-\trtp->payload_type = RIST_ADV_PT;\n-\trtp->ts = htonl(params->timestamp);\n-\trtp->ssrc = htonl(params->ssrc);\n-\toffset += sizeof(struct rist_rtp_hdr);\n-\n-\t/* Profile-defined extension (4 bytes) */\n-\tstruct rist_adv_ext *ext = (struct rist_adv_ext *)(buf + offset);\n-\tmemset(ext, 0, sizeof(*ext));\n-\toffset += RIST_ADV_EXT_SIZE;\n-\n-\trist_adv_set_seq32(rtp, ext, params->seq);\n-\n-\t/* Build flags byte */\n-\text->flags = 0;\n-\tif (params->first_frag)  ext->flags |= RIST_ADV_FLAG_F;\n-\tif (params->last_frag)   ext->flags |= RIST_ADV_FLAG_L;\n-\tif (params->expedite)    ext->flags |= RIST_ADV_FLAG_E;\n-\tif (params->retransmit)  ext->flags |= RIST_ADV_FLAG_R;\n-\tif (params->flow_id)     ext->flags |= RIST_ADV_FLAG_I;\n-\tif (params->pfd)         ext->flags |= RIST_ADV_FLAG_P;\n-\tif (params->hdr_ext && params->hdr_ext_len > 0)\n-\t\text->flags |= RIST_ADV_FLAG_H;\n-\n-\t/* Build params byte + PSK bits that span both bytes */\n-\trist_adv_set_psk(ext, params->psk_mode);\n-\trist_adv_set_lpc(ext, params->lpc_mode);\n-\trist_adv_set_type(ext, params->enc_type);\n-\n-\t/* Optional fields in spec order */\n-\n-\t/* Flow ID */\n-\tif (params->flow_id) {\n-\t\tmemcpy(buf + offset, params->flow_id, RIST_ADV_FLOW_ID_SIZE);\n-\t\toffset += RIST_ADV_FLOW_ID_SIZE;\n-\t}\n-\n-\t/* PSK Hash */\n-\tif (rist_adv_psk_has_hash(params->psk_mode) && params->psk_hash) {\n-\t\tmemcpy(buf + offset, params->psk_hash, RIST_ADV_PSK_HASH_SIZE);\n-\t\toffset += RIST_ADV_PSK_HASH_SIZE;\n-\t}\n-\n-\t/* PSK Nonce */\n-\tif (rist_adv_psk_has_nonce(params->psk_mode) && params->psk_nonce) {\n-\t\tmemcpy(buf + offset, params->psk_nonce, RIST_ADV_PSK_NONCE_SIZE);\n-\t\toffset += RIST_ADV_PSK_NONCE_SIZE;\n-\t}\n-\n-\t/* PSK IV */\n-\tif (rist_adv_psk_has_iv(params->psk_mode) && params->psk_iv) {\n-\t\tmemcpy(buf + offset, params->psk_iv, RIST_ADV_PSK_IV_SIZE);\n-\t\toffset += RIST_ADV_PSK_IV_SIZE;\n-\t}\n-\n-\t/* Payload Compression field */\n-\tif (params->lpc_mode == RIST_ADV_LPC_FIELD_PRESENT && params->compression) {\n-\t\tmemcpy(buf + offset, params->compression, RIST_ADV_COMPRESSION_SIZE);\n-\t\toffset += RIST_ADV_COMPRESSION_SIZE;\n-\t}\n-\n-\t/* Payload Format Descriptor */\n-\tif (params->pfd) {\n-\t\tmemcpy(buf + offset, params->pfd, RIST_ADV_PFD_SIZE);\n-\t\toffset += RIST_ADV_PFD_SIZE;\n-\t}\n-\n-\t/* RIST Header Extension */\n-\tif (params->hdr_ext && params->hdr_ext_len > 0) {\n-\t\tmemcpy(buf + offset, params->hdr_ext, params->hdr_ext_len);\n-\t\toffset += params->hdr_ext_len;\n-\t}\n-\n-\t/* Payload */\n-\tif (payload && payload_len > 0)\n-\t\tmemcpy(buf + offset, payload, payload_len);\n-\n-\treturn (int)(offset + payload_len);\n-}\ndiff --git a/src/adv_ctrl.c b/src/adv_ctrl.c\ndeleted file mode 100644\nindex 74961e3..0000000\n--- a/src/adv_ctrl.c\n+++ /dev/null\n@@ -1,762 +0,0 @@\n-/*\n- * Copyright © 2024-2026 SipRadius LLC\n- * All rights reserved.\n- *\n- * SPDX-License-Identifier: BSD-2-Clause\n- *\n- * VSF TR-06-3:2024 — RIST Advanced Profile control message\n- * serialization and deserialization.\n- */\n-\n-#include \"proto/adv.h\"\n-#include \"rist-private.h\"\n-#include \"udp-private.h\"\n-#include \"transport-private.h\"\n-#include \"log-private.h\"\n-#include \"proto/rist_time.h\"\n-#include \"crypto/psk.h\"\n-#include \"cjson/cJSON.h\"\n-#include <string.h>\n-#include <errno.h>\n-\n-/* --------------------------------------------------------------------------\n- * All Advanced Profile control messages are sent as:\n- *   Type = 4 (Control), E = 1 (Expedite), F = L = 1 (unfragmented),\n- *   PSK = 0 (no encryption for now), LPC = 0 (no compression).\n- *\n- * The payload of a Type 4 packet is:\n- *   | Control Index (16 bits) | Length (16 bits) |\n- *   |       ... body per CI ...                  |\n- * -------------------------------------------------------------------------- */\n-\n-/* Build the outer Advanced Profile packet for a control message.\n- * ctrl_payload: serialized control body (CI + Length + data)\n- * ctrl_len: length of ctrl_payload\n- * Returns total packet length written into buf, or -1 on error. */\n-static int rist_adv_build_control(uint8_t *buf, uint32_t seq, uint32_t timestamp,\n-                                  uint32_t ssrc, const uint8_t *ctrl_payload,\n-                                  size_t ctrl_len)\n-{\n-\tstruct rist_adv_params params;\n-\tmemset(&params, 0, sizeof(params));\n-\tparams.seq = seq;\n-\tparams.timestamp = timestamp;\n-\tparams.ssrc = ssrc;\n-\tparams.enc_type = RIST_ADV_TYPE_CONTROL;\n-\tparams.psk_mode = RIST_ADV_PSK_NONE;\n-\tparams.lpc_mode = RIST_ADV_LPC_NONE;\n-\tparams.first_frag = true;\n-\tparams.last_frag = true;\n-\tparams.expedite = true;\n-\tparams.retransmit = false;\n-\n-\treturn rist_adv_build(buf, &params, ctrl_payload, ctrl_len);\n-}\n-\n-/* --------------------------------------------------------------------------\n- * NACK Bitmask (CI=0x0000, Section 5.3.2)\n- *\n- * Body (repeatable 12-byte entries):\n- *   | SSRC of Media Source (32) |\n- *   | PSS — Packet Start Seq (32) |\n- *   | BLP — Bitmask of Lost Packets (32) |\n- * -------------------------------------------------------------------------- */\n-\n-int rist_adv_send_nack_bitmask(struct rist_peer *peer,\n-                               uint32_t media_ssrc,\n-                               uint32_t pss, uint32_t blp)\n-{\n-\tstruct rist_common_ctx *ctx = get_cctx(peer);\n-\tuint8_t pkt[RIST_MAX_PACKET_SIZE];\n-\tuint8_t ctrl[4 + 12]; /* CI(2) + Len(2) + one 12-byte entry */\n-\tsize_t off = 0;\n-\n-\t/* Control header */\n-\tctrl[off++] = (RIST_ADV_CI_NACK_BITMASK >> 8) & 0xFF;\n-\tctrl[off++] = RIST_ADV_CI_NACK_BITMASK & 0xFF;\n-\tuint16_t body_len = 12;\n-\tctrl[off++] = (body_len >> 8) & 0xFF;\n-\tctrl[off++] = body_len & 0xFF;\n-\n-\t/* SSRC of media source */\n-\tctrl[off++] = (media_ssrc >> 24) & 0xFF;\n-\tctrl[off++] = (media_ssrc >> 16) & 0xFF;\n-\tctrl[off++] = (media_ssrc >> 8) & 0xFF;\n-\tctrl[off++] = media_ssrc & 0xFF;\n-\n-\t/* PSS */\n-\tctrl[off++] = (pss >> 24) & 0xFF;\n-\tctrl[off++] = (pss >> 16) & 0xFF;\n-\tctrl[off++] = (pss >> 8) & 0xFF;\n-\tctrl[off++] = pss & 0xFF;\n-\n-\t/* BLP */\n-\tctrl[off++] = (blp >> 24) & 0xFF;\n-\tctrl[off++] = (blp >> 16) & 0xFF;\n-\tctrl[off++] = (blp >> 8) & 0xFF;\n-\tctrl[off++] = blp & 0xFF;\n-\n-\tuint32_t seq = ctx->adv_seq_unprotected++;\n-\tuint32_t ts = timestampRTP_u32(1, timestampNTP_u64());\n-\tuint32_t ssrc = rist_adv_ssrc_unprotected(ctx->adv_ssrc_base);\n-\n-\tint total = rist_adv_build_control(pkt, seq, ts, ssrc, ctrl, off);\n-\tif (total < 0)\n-\t\treturn -1;\n-\n-\tssize_t ret = rist_transport_sendto(peer, pkt, (size_t)total, 0);\n-\tif (ret < 0)\n-\t\t_librist_log_send_error(peer, errno, (size_t)total, \"adv-nack-bitmask sendto\");\n-\n-\treturn (ret > 0) ? 0 : -1;\n-}\n-\n-/* --------------------------------------------------------------------------\n- * NACK Range (CI=0x0001, Section 5.3.3)\n- *\n- * Body (repeatable 12-byte entries):\n- *   | SSRC of Media Source (32) |\n- *   | PSS — Packet Start Seq (32) |\n- *   | NALP — Number of Additional Lost Packets (32) |\n- * -------------------------------------------------------------------------- */\n-\n-int rist_adv_send_nack_range(struct rist_peer *peer,\n-                             uint32_t media_ssrc,\n-                             uint32_t pss, uint32_t nalp)\n-{\n-\tstruct rist_common_ctx *ctx = get_cctx(peer);\n-\tuint8_t pkt[RIST_MAX_PACKET_SIZE];\n-\tuint8_t ctrl[4 + 12];\n-\tsize_t off = 0;\n-\n-\tctrl[off++] = (RIST_ADV_CI_NACK_RANGE >> 8) & 0xFF;\n-\tctrl[off++] = RIST_ADV_CI_NACK_RANGE & 0xFF;\n-\tuint16_t body_len = 12;\n-\tctrl[off++] = (body_len >> 8) & 0xFF;\n-\tctrl[off++] = body_len & 0xFF;\n-\n-\tctrl[off++] = (media_ssrc >> 24) & 0xFF;\n-\tctrl[off++] = (media_ssrc >> 16) & 0xFF;\n-\tctrl[off++] = (media_ssrc >> 8) & 0xFF;\n-\tctrl[off++] = media_ssrc & 0xFF;\n-\n-\tctrl[off++] = (pss >> 24) & 0xFF;\n-\tctrl[off++] = (pss >> 16) & 0xFF;\n-\tctrl[off++] = (pss >> 8) & 0xFF;\n-\tctrl[off++] = pss & 0xFF;\n-\n-\tctrl[off++] = (nalp >> 24) & 0xFF;\n-\tctrl[off++] = (nalp >> 16) & 0xFF;\n-\tctrl[off++] = (nalp >> 8) & 0xFF;\n-\tctrl[off++] = nalp & 0xFF;\n-\n-\tuint32_t seq = ctx->adv_seq_unprotected++;\n-\tuint32_t ts = timestampRTP_u32(1, timestampNTP_u64());\n-\tuint32_t ssrc = rist_adv_ssrc_unprotected(ctx->adv_ssrc_base);\n-\n-\tint total = rist_adv_build_control(pkt, seq, ts, ssrc, ctrl, off);\n-\tif (total < 0)\n-\t\treturn -1;\n-\n-\tssize_t ret = rist_transport_sendto(peer, pkt, (size_t)total, 0);\n-\tif (ret < 0)\n-\t\t_librist_log_send_error(peer, errno, (size_t)total, \"adv-nack-range sendto\");\n-\n-\treturn (ret > 0) ? 0 : -1;\n-}\n-\n-/* --------------------------------------------------------------------------\n- * RTT Echo Request (CI=0x0010, Section 5.3.4)\n- *\n- * Body:\n- *   | Requester SSRC (32) |\n- *   | Timestamp MSW (32)  |\n- *   | Timestamp LSW (32)  |\n- *   | Processing Delay (32, microseconds) |\n- *   | Padding (n * 32 bits) |\n- * -------------------------------------------------------------------------- */\n-\n-int rist_adv_send_rtt_echo_request(struct rist_peer *peer)\n-{\n-\tstruct rist_common_ctx *ctx = get_cctx(peer);\n-\tuint8_t pkt[RIST_MAX_PACKET_SIZE];\n-\tuint8_t ctrl[4 + 16]; /* CI(2) + Len(2) + 4 words */\n-\tsize_t off = 0;\n-\n-\tctrl[off++] = (RIST_ADV_CI_RTT_ECHO_REQ >> 8) & 0xFF;\n-\tctrl[off++] = RIST_ADV_CI_RTT_ECHO_REQ & 0xFF;\n-\tuint16_t body_len = 16;\n-\tctrl[off++] = (body_len >> 8) & 0xFF;\n-\tctrl[off++] = body_len & 0xFF;\n-\n-\tuint32_t req_ssrc = rist_adv_ssrc_unprotected(ctx->adv_ssrc_base);\n-\tctrl[off++] = (req_ssrc >> 24) & 0xFF;\n-\tctrl[off++] = (req_ssrc >> 16) & 0xFF;\n-\tctrl[off++] = (req_ssrc >> 8) & 0xFF;\n-\tctrl[off++] = req_ssrc & 0xFF;\n-\n-\tuint64_t ntp = timestampNTP_u64();\n-\tuint32_t msw = (uint32_t)(ntp >> 32);\n-\tuint32_t lsw = (uint32_t)(ntp & 0xFFFFFFFF);\n-\n-\tctrl[off++] = (msw >> 24) & 0xFF;\n-\tctrl[off++] = (msw >> 16) & 0xFF;\n-\tctrl[off++] = (msw >> 8) & 0xFF;\n-\tctrl[off++] = msw & 0xFF;\n-\n-\tctrl[off++] = (lsw >> 24) & 0xFF;\n-\tctrl[off++] = (lsw >> 16) & 0xFF;\n-\tctrl[off++] = (lsw >> 8) & 0xFF;\n-\tctrl[off++] = lsw & 0xFF;\n-\n-\t/* Processing delay = 0 for request */\n-\tctrl[off++] = 0; ctrl[off++] = 0; ctrl[off++] = 0; ctrl[off++] = 0;\n-\n-\tuint32_t seq = ctx->adv_seq_unprotected++;\n-\tuint32_t ts = timestampRTP_u32(1, ntp);\n-\n-\tint total = rist_adv_build_control(pkt, seq, ts, req_ssrc, ctrl, off);\n-\tif (total < 0)\n-\t\treturn -1;\n-\n-\tssize_t ret = rist_transport_sendto(peer, pkt, (size_t)total, 0);\n-\tif (ret < 0)\n-\t\t_librist_log_send_error(peer, errno, (size_t)total, \"adv-rtt-echo-req sendto\");\n-\n-\treturn (ret > 0) ? 0 : -1;\n-}\n-\n-/* --------------------------------------------------------------------------\n- * RTT Echo Response (CI=0x0011, Section 5.3.4)\n- *\n- * Mirror the request body, fill in processing delay.\n- * -------------------------------------------------------------------------- */\n-\n-int rist_adv_send_rtt_echo_response(struct rist_peer *peer,\n-                                    uint32_t requester_ssrc,\n-                                    uint32_t orig_msw, uint32_t orig_lsw,\n-                                    uint32_t processing_delay_us)\n-{\n-\tstruct rist_common_ctx *ctx = get_cctx(peer);\n-\tuint8_t pkt[RIST_MAX_PACKET_SIZE];\n-\tuint8_t ctrl[4 + 16];\n-\tsize_t off = 0;\n-\n-\tctrl[off++] = (RIST_ADV_CI_RTT_ECHO_RESP >> 8) & 0xFF;\n-\tctrl[off++] = RIST_ADV_CI_RTT_ECHO_RESP & 0xFF;\n-\tuint16_t body_len = 16;\n-\tctrl[off++] = (body_len >> 8) & 0xFF;\n-\tctrl[off++] = body_len & 0xFF;\n-\n-\tctrl[off++] = (requester_ssrc >> 24) & 0xFF;\n-\tctrl[off++] = (requester_ssrc >> 16) & 0xFF;\n-\tctrl[off++] = (requester_ssrc >> 8) & 0xFF;\n-\tctrl[off++] = requester_ssrc & 0xFF;\n-\n-\tctrl[off++] = (orig_msw >> 24) & 0xFF;\n-\tctrl[off++] = (orig_msw >> 16) & 0xFF;\n-\tctrl[off++] = (orig_msw >> 8) & 0xFF;\n-\tctrl[off++] = orig_msw & 0xFF;\n-\n-\tctrl[off++] = (orig_lsw >> 24) & 0xFF;\n-\tctrl[off++] = (orig_lsw >> 16) & 0xFF;\n-\tctrl[off++] = (orig_lsw >> 8) & 0xFF;\n-\tctrl[off++] = orig_lsw & 0xFF;\n-\n-\tctrl[off++] = (processing_delay_us >> 24) & 0xFF;\n-\tctrl[off++] = (processing_delay_us >> 16) & 0xFF;\n-\tctrl[off++] = (processing_delay_us >> 8) & 0xFF;\n-\tctrl[off++] = processing_delay_us & 0xFF;\n-\n-\tuint32_t seq = ctx->adv_seq_unprotected++;\n-\tuint64_t ntp = timestampNTP_u64();\n-\tuint32_t ts = timestampRTP_u32(1, ntp);\n-\tuint32_t ssrc = rist_adv_ssrc_unprotected(ctx->adv_ssrc_base);\n-\n-\tint total = rist_adv_build_control(pkt, seq, ts, ssrc, ctrl, off);\n-\tif (total < 0)\n-\t\treturn -1;\n-\n-\tssize_t ret = rist_transport_sendto(peer, pkt, (size_t)total, 0);\n-\tif (ret < 0)\n-\t\t_librist_log_send_error(peer, errno, (size_t)total, \"adv-rtt-echo-resp sendto\");\n-\n-\treturn (ret > 0) ? 0 : -1;\n-}\n-\n-/* --------------------------------------------------------------------------\n- * Keep-Alive (CI=0x8000, Section 5.3.6)\n- *\n- * Wraps Main Profile keep-alive format with additional capability bits:\n- *   C (bit 29): Compression capable\n- *   G (bit 30): GRE key rotation capable\n- *   I (bit 31): Advanced Profile capable\n- *\n- * For now we send a minimal keep-alive with the I bit set.\n- * -------------------------------------------------------------------------- */\n-\n-#define RIST_ADV_KEEPALIVE_CAP_I  (1u << 31)\n-#define RIST_ADV_KEEPALIVE_CAP_G  (1u << 30)\n-#define RIST_ADV_KEEPALIVE_CAP_C  (1u << 29)\n-\n-int rist_adv_send_keepalive(struct rist_peer *peer)\n-{\n-\tstruct rist_common_ctx *ctx = get_cctx(peer);\n-\tuint8_t pkt[RIST_MAX_PACKET_SIZE];\n-\n-\t/*\n-\t * Keep-alive body layout (Section 5.3.6):\n-\t *   | MAC Address (6 bytes) |\n-\t *   | Capabilities (4 bytes, with C/G/I bits) |\n-\t *   | JSON (optional, variable) |\n-\t *\n-\t * We encode it as CI(2) + Len(2) + MAC(6) + Capabilities(4) = 14 body bytes\n-\t */\n-\tuint8_t ctrl[4 + 6 + 4];\n-\tsize_t off = 0;\n-\n-\tctrl[off++] = (RIST_ADV_CI_KEEPALIVE >> 8) & 0xFF;\n-\tctrl[off++] = RIST_ADV_CI_KEEPALIVE & 0xFF;\n-\tuint16_t body_len = 10; /* MAC(6) + Capabilities(4) */\n-\tctrl[off++] = (body_len >> 8) & 0xFF;\n-\tctrl[off++] = body_len & 0xFF;\n-\n-\t/* MAC address from the peer struct */\n-\tmemcpy(&ctrl[off], peer->mac_addr, 6);\n-\toff += 6;\n-\n-\t/* Capabilities with I bit set to signal Advanced Profile support */\n-\tuint32_t caps = RIST_ADV_KEEPALIVE_CAP_I;\n-\tctrl[off++] = (caps >> 24) & 0xFF;\n-\tctrl[off++] = (caps >> 16) & 0xFF;\n-\tctrl[off++] = (caps >> 8) & 0xFF;\n-\tctrl[off++] = caps & 0xFF;\n-\n-\tuint32_t seq = ctx->adv_seq_unprotected++;\n-\tuint64_t ntp = timestampNTP_u64();\n-\tuint32_t ts = timestampRTP_u32(1, ntp);\n-\tuint32_t ssrc = rist_adv_ssrc_unprotected(ctx->adv_ssrc_base);\n-\n-\tint total = rist_adv_build_control(pkt, seq, ts, ssrc, ctrl, off);\n-\tif (total < 0)\n-\t\treturn -1;\n-\n-\tssize_t ret = rist_transport_sendto(peer, pkt, (size_t)total, 0);\n-\tif (ret < 0)\n-\t\t_librist_log_send_error(peer, errno, (size_t)total, \"adv-keepalive sendto\");\n-\n-\treturn (ret > 0) ? 0 : -1;\n-}\n-\n-/* --------------------------------------------------------------------------\n- * Control Message Unsupported Response (CI=0x8020, Section 5.3.10)\n- *\n- * Body:\n- *   | Responder SSRC (32) |\n- *   | Incoming CI (16) | Reserved (16) |\n- *   | First 48 bits of incoming payload (48) | Padding (16) |\n- * -------------------------------------------------------------------------- */\n-\n-int rist_adv_send_unsupported(struct rist_peer *peer,\n-                              uint16_t incoming_ci,\n-                              const uint8_t *incoming_payload,\n-                              size_t incoming_len)\n-{\n-\tstruct rist_common_ctx *ctx = get_cctx(peer);\n-\tuint8_t pkt[RIST_MAX_PACKET_SIZE];\n-\tuint8_t ctrl[4 + 16];\n-\tsize_t off = 0;\n-\n-\tctrl[off++] = (RIST_ADV_CI_UNSUPPORTED >> 8) & 0xFF;\n-\tctrl[off++] = RIST_ADV_CI_UNSUPPORTED & 0xFF;\n-\tuint16_t body_len = 12;\n-\tctrl[off++] = (body_len >> 8) & 0xFF;\n-\tctrl[off++] = body_len & 0xFF;\n-\n-\tuint32_t resp_ssrc = rist_adv_ssrc_unprotected(ctx->adv_ssrc_base);\n-\tctrl[off++] = (resp_ssrc >> 24) & 0xFF;\n-\tctrl[off++] = (resp_ssrc >> 16) & 0xFF;\n-\tctrl[off++] = (resp_ssrc >> 8) & 0xFF;\n-\tctrl[off++] = resp_ssrc & 0xFF;\n-\n-\tctrl[off++] = (incoming_ci >> 8) & 0xFF;\n-\tctrl[off++] = incoming_ci & 0xFF;\n-\tctrl[off++] = 0; /* Reserved */\n-\tctrl[off++] = 0;\n-\n-\t/* Copy first 6 bytes (48 bits) of incoming payload, or pad with zeros */\n-\tsize_t copy_len = incoming_len < 6 ? incoming_len : 6;\n-\tif (incoming_payload && copy_len > 0)\n-\t\tmemcpy(&ctrl[off], incoming_payload, copy_len);\n-\tif (copy_len < 6)\n-\t\tmemset(&ctrl[off + copy_len], 0, 6 - copy_len);\n-\toff += 6;\n-\t/* Padding to 32-bit boundary */\n-\tctrl[off++] = 0;\n-\tctrl[off++] = 0;\n-\n-\tuint32_t seq = ctx->adv_seq_unprotected++;\n-\tuint64_t ntp = timestampNTP_u64();\n-\tuint32_t ts = timestampRTP_u32(1, ntp);\n-\n-\tint total = rist_adv_build_control(pkt, seq, ts, resp_ssrc, ctrl, off);\n-\tif (total < 0)\n-\t\treturn -1;\n-\n-\tssize_t ret = rist_transport_sendto(peer, pkt, (size_t)total, 0);\n-\tif (ret < 0)\n-\t\t_librist_log_send_error(peer, errno, (size_t)total, \"adv-unsupported sendto\");\n-\n-\treturn (ret > 0) ? 0 : -1;\n-}\n-\n-/* --------------------------------------------------------------------------\n- * Type 8 — GRE-over-Advanced Profile (Section 5.2, Type=8)\n- *\n- * Wraps a complete Main Profile GRE packet inside an Advanced Profile\n- * envelope.  The receiver strips the AP header and falls through to\n- * normal GRE processing.  This enables rist2rist relay/bridge devices\n- * and is used during Section 9 transition.\n- * -------------------------------------------------------------------------- */\n-\n-int rist_adv_send_type8(struct rist_peer *peer,\n-                        const uint8_t *gre_packet, size_t gre_len)\n-{\n-\tstruct rist_common_ctx *ctx = get_cctx(peer);\n-\tuint8_t pkt[RIST_MAX_PACKET_SIZE + RIST_ADV_MAX_FIXED_HEADER];\n-\n-\tif (gre_len > RIST_MAX_PACKET_SIZE)\n-\t\treturn -1;\n-\n-\tstruct rist_adv_params params;\n-\tmemset(&params, 0, sizeof(params));\n-\tparams.seq = ctx->adv_seq_unprotected++;\n-\tparams.timestamp = timestampRTP_u32(1, timestampNTP_u64());\n-\tparams.ssrc = rist_adv_ssrc_unprotected(ctx->adv_ssrc_base);\n-\tparams.enc_type = RIST_ADV_TYPE_GRE_MAIN;\n-\tparams.psk_mode = RIST_ADV_PSK_NONE;\n-\tparams.lpc_mode = RIST_ADV_LPC_NONE;\n-\tparams.first_frag = true;\n-\tparams.last_frag = true;\n-\tparams.expedite = true;\n-\tparams.retransmit = false;\n-\n-\tint total = rist_adv_build(pkt, &params, gre_packet, gre_len);\n-\tif (total < 0)\n-\t\treturn -1;\n-\n-\tssize_t ret = rist_transport_sendto(peer, pkt, (size_t)total, 0);\n-\tif (ret < 0)\n-\t\t_librist_log_send_error(peer, errno, (size_t)total, \"adv-type8 sendto\");\n-\n-\treturn (ret > 0) ? 0 : -1;\n-}\n-\n-/* --------------------------------------------------------------------------\n- * PSK Future Nonce Announcement (CI=0x8011, Section 5.3.9)\n- *\n- * Body:\n- *   | Future Nonce (4 bytes) |\n- *   | Key Size (2 bytes) | Reserved (2 bytes) |\n- *\n- * Sent by the sender before rotating to a new nonce so the receiver\n- * can pre-derive the AES key (PBKDF2 is expensive) before the first\n- * data packet with the new nonce arrives.\n- * -------------------------------------------------------------------------- */\n-\n-int rist_adv_send_psk_nonce(struct rist_peer *peer,\n-                            const uint8_t nonce[4], uint16_t key_size_bits)\n-{\n-\tstruct rist_common_ctx *ctx = get_cctx(peer);\n-\tuint8_t pkt[RIST_MAX_PACKET_SIZE];\n-\tuint8_t ctrl[4 + 8]; /* CI(2) + Len(2) + Nonce(4) + KeySize(2) + Rsvd(2) */\n-\tsize_t off = 0;\n-\n-\tctrl[off++] = (RIST_ADV_CI_PSK_NONCE >> 8) & 0xFF;\n-\tctrl[off++] = RIST_ADV_CI_PSK_NONCE & 0xFF;\n-\tuint16_t body_len = 8;\n-\tctrl[off++] = (body_len >> 8) & 0xFF;\n-\tctrl[off++] = body_len & 0xFF;\n-\n-\tmemcpy(&ctrl[off], nonce, 4);\n-\toff += 4;\n-\n-\tctrl[off++] = (key_size_bits >> 8) & 0xFF;\n-\tctrl[off++] = key_size_bits & 0xFF;\n-\tctrl[off++] = 0; /* Reserved */\n-\tctrl[off++] = 0;\n-\n-\tuint32_t seq = ctx->adv_seq_unprotected++;\n-\tuint64_t ntp = timestampNTP_u64();\n-\tuint32_t ts = timestampRTP_u32(1, ntp);\n-\tuint32_t ssrc = rist_adv_ssrc_unprotected(ctx->adv_ssrc_base);\n-\n-\tint total = rist_adv_build_control(pkt, seq, ts, ssrc, ctrl, off);\n-\tif (total < 0)\n-\t\treturn -1;\n-\n-\tssize_t ret = rist_transport_sendto(peer, pkt, (size_t)total, 0);\n-\tif (ret < 0)\n-\t\t_librist_log_send_error(peer, errno, (size_t)total, \"adv-psk-nonce sendto\");\n-\n-\treturn (ret > 0) ? 0 : -1;\n-}\n-\n-/* --------------------------------------------------------------------------\n- * Flow Attribute (CI=0x8001, Section 5.3.7)\n- *\n- * Body: JSON object describing session and flow metadata.\n- * Sent periodically by the sender (~1 Hz).\n- *\n- * Minimal JSON schema:\n- *   { \"session\": \"<cname>\",\n- *     \"flow_id\": <outer>, \"flow_inner\": <inner>,\n- *     \"virt_dst_port\": <port>, \"profile\": \"advanced\" }\n- * -------------------------------------------------------------------------- */\n-\n-int rist_adv_send_flow_attr(struct rist_peer *peer)\n-{\n-\tstruct rist_common_ctx *ctx = get_cctx(peer);\n-\tuint8_t pkt[RIST_MAX_PACKET_SIZE];\n-\n-\tcJSON *root = cJSON_CreateObject();\n-\tif (!root)\n-\t\treturn -1;\n-\n-\tcJSON_AddStringToObject(root, \"session\", peer->cname);\n-\tcJSON_AddNumberToObject(root, \"virt_dst_port\", peer->config.virt_dst_port);\n-\tcJSON_AddStringToObject(root, \"profile\", \"advanced\");\n-\n-\tuint16_t outer = peer->config.virt_dst_port;\n-\tuint16_t inner = 0;\n-\tcJSON_AddNumberToObject(root, \"flow_id\", outer);\n-\tcJSON_AddNumberToObject(root, \"flow_inner\", inner);\n-\n-\tchar *json = cJSON_PrintUnformatted(root);\n-\tcJSON_Delete(root);\n-\tif (!json)\n-\t\treturn -1;\n-\n-\tsize_t json_len = strlen(json);\n-\tif (json_len > RIST_MAX_PACKET_SIZE - RIST_ADV_MAX_FIXED_HEADER - 4) {\n-\t\tfree(json);\n-\t\treturn -1;\n-\t}\n-\n-\tuint8_t ctrl[4 + RIST_MAX_PACKET_SIZE];\n-\tsize_t off = 0;\n-\n-\tctrl[off++] = (RIST_ADV_CI_FLOW_ATTR >> 8) & 0xFF;\n-\tctrl[off++] = RIST_ADV_CI_FLOW_ATTR & 0xFF;\n-\tuint16_t body_len = (uint16_t)json_len;\n-\tctrl[off++] = (body_len >> 8) & 0xFF;\n-\tctrl[off++] = body_len & 0xFF;\n-\n-\tmemcpy(&ctrl[off], json, json_len);\n-\toff += json_len;\n-\tfree(json);\n-\n-\tuint32_t seq = ctx->adv_seq_unprotected++;\n-\tuint64_t ntp = timestampNTP_u64();\n-\tuint32_t ts = timestampRTP_u32(1, ntp);\n-\tuint32_t ssrc = rist_adv_ssrc_unprotected(ctx->adv_ssrc_base);\n-\n-\tint total = rist_adv_build_control(pkt, seq, ts, ssrc, ctrl, off);\n-\tif (total < 0)\n-\t\treturn -1;\n-\n-\tssize_t ret = rist_transport_sendto(peer, pkt, (size_t)total, 0);\n-\tif (ret < 0)\n-\t\t_librist_log_send_error(peer, errno, (size_t)total, \"adv-flow-attr sendto\");\n-\n-\treturn (ret > 0) ? 0 : -1;\n-}\n-\n-/* --------------------------------------------------------------------------\n- * Control message receive dispatcher\n- *\n- * Called from the Advanced Profile receive path when enc_type == CONTROL.\n- * Returns 0 on success, -1 on error/unknown CI.\n- * -------------------------------------------------------------------------- */\n-\n-int rist_adv_recv_control(struct rist_peer *peer,\n-                          const uint8_t *ctrl_payload,\n-                          size_t ctrl_len)\n-{\n-\tstruct rist_common_ctx *ctx = get_cctx(peer);\n-\n-\tif (ctrl_len < RIST_ADV_CTRL_HDR_SIZE)\n-\t\treturn -1;\n-\n-\tuint16_t ci = (uint16_t)((ctrl_payload[0] << 8) | ctrl_payload[1]);\n-\tuint16_t body_len = (uint16_t)((ctrl_payload[2] << 8) | ctrl_payload[3]);\n-\n-\tif ((size_t)4 + body_len > ctrl_len)\n-\t\treturn -1;\n-\n-\tconst uint8_t *body = ctrl_payload + 4;\n-\n-\t/* The Advanced control channel is cleartext by design (PSK=0 on all\n-\t * control sends), so every handler here is pre-auth attack surface.\n-\t * Mirror the Main-profile rule (rist_sender_recv_nack): state-changing\n-\t * messages are honored only from peers that completed the (SDES or\n-\t * EAP) handshake. Discovery/diagnostic traffic stays pre-auth. */\n-\tswitch (ci) {\n-\tcase RIST_ADV_CI_NACK_BITMASK:\n-\tcase RIST_ADV_CI_NACK_RANGE:\n-\tcase RIST_ADV_CI_RTT_ECHO_RESP:\n-\tcase RIST_ADV_CI_FLOW_ATTR:\n-\tcase RIST_ADV_CI_PSK_NONCE:\n-\t\tif (!peer->authenticated)\n-\t\t\treturn 0;\n-\t\tbreak;\n-\tdefault:\n-\t\tbreak;\n-\t}\n-\n-\tswitch (ci) {\n-\tcase RIST_ADV_CI_NACK_BITMASK: {\n-\t\tif (body_len < 12)\n-\t\t\treturn -1;\n-\t\tuint32_t media_ssrc = (uint32_t)body[0] << 24 | body[1] << 16 | body[2] << 8 | body[3];\n-\t\tuint32_t pss = (uint32_t)body[4] << 24 | body[5] << 16 | body[6] << 8 | body[7];\n-\t\tuint32_t blp = (uint32_t)body[8] << 24 | body[9] << 16 | body[10] << 8 | body[11];\n-\t\t(void)media_ssrc;\n-\t\trist_log_priv(ctx, RIST_LOG_DEBUG,\n-\t\t\t\"Advanced NACK Bitmask received: PSS=%u BLP=0x%08x\\n\", pss, blp);\n-\n-\t\t/* Enqueue retransmissions for pss and each bit set in blp */\n-\t\tif (peer->sender_ctx) {\n-\t\t\tstruct rist_sender *sender = peer->sender_ctx;\n-\t\t\trist_retry_enqueue(sender, pss, peer);\n-\t\t\tfor (int i = 0; i < 32; i++) {\n-\t\t\t\tif (blp & (1u << i))\n-\t\t\t\t\trist_retry_enqueue(sender, pss + 1 + (uint32_t)i, peer);\n-\t\t\t}\n-\t\t}\n-\t\treturn 0;\n-\t}\n-\n-\tcase RIST_ADV_CI_NACK_RANGE: {\n-\t\tif (body_len < 12)\n-\t\t\treturn -1;\n-\t\tuint32_t media_ssrc = (uint32_t)body[0] << 24 | body[1] << 16 | body[2] << 8 | body[3];\n-\t\tuint32_t pss = (uint32_t)body[4] << 24 | body[5] << 16 | body[6] << 8 | body[7];\n-\t\tuint32_t nalp = (uint32_t)body[8] << 24 | body[9] << 16 | body[10] << 8 | body[11];\n-\t\t(void)media_ssrc;\n-\t\trist_log_priv(ctx, RIST_LOG_DEBUG,\n-\t\t\t\"Advanced NACK Range received: PSS=%u NALP=%u\\n\", pss, nalp);\n-\n-\t\tif (peer->sender_ctx) {\n-\t\t\tstruct rist_sender *sender = peer->sender_ctx;\n-\t\t\tfor (uint32_t i = 0; i <= nalp && i < 10000; i++)\n-\t\t\t\trist_retry_enqueue(sender, pss + i, peer);\n-\t\t}\n-\t\treturn 0;\n-\t}\n-\n-\tcase RIST_ADV_CI_RTT_ECHO_REQ: {\n-\t\tif (body_len < 16)\n-\t\t\treturn -1;\n-\t\tuint32_t req_ssrc = (uint32_t)body[0] << 24 | body[1] << 16 | body[2] << 8 | body[3];\n-\t\tuint32_t orig_msw = (uint32_t)body[4] << 24 | body[5] << 16 | body[6] << 8 | body[7];\n-\t\tuint32_t orig_lsw = (uint32_t)body[8] << 24 | body[9] << 16 | body[10] << 8 | body[11];\n-\t\t/* processing_delay in the request is always 0 */\n-\t\trist_log_priv(ctx, RIST_LOG_DEBUG, \"Advanced RTT Echo Request from SSRC %u\\n\", req_ssrc);\n-\t\treturn rist_adv_send_rtt_echo_response(peer, req_ssrc, orig_msw, orig_lsw, 0);\n-\t}\n-\n-\tcase RIST_ADV_CI_RTT_ECHO_RESP: {\n-\t\tif (body_len < 16)\n-\t\t\treturn -1;\n-\t\tuint32_t req_ssrc = (uint32_t)body[0] << 24 | body[1] << 16 | body[2] << 8 | body[3];\n-\t\tuint32_t orig_msw = (uint32_t)body[4] << 24 | body[5] << 16 | body[6] << 8 | body[7];\n-\t\tuint32_t orig_lsw = (uint32_t)body[8] << 24 | body[9] << 16 | body[10] << 8 | body[11];\n-\t\tuint32_t proc_delay = (uint32_t)body[12] << 24 | body[13] << 16 | body[14] << 8 | body[15];\n-\t\t(void)req_ssrc;\n-\n-\t\tuint64_t orig_ntp = ((uint64_t)orig_msw << 32) | orig_lsw;\n-\t\trist_peer_rtt_update(peer,\n-\t\t\tcalculate_rtt_delay(orig_ntp, timestampNTP_u64(), proc_delay));\n-\t\trist_log_priv(ctx, RIST_LOG_DEBUG,\n-\t\t\t\"Advanced RTT Echo Response: RTT=%\"PRIu64\" us\\n\",\n-\t\t\t(peer->last_rtt * 1000000ULL) >> 32);\n-\t\treturn 0;\n-\t}\n-\n-\tcase RIST_ADV_CI_KEEPALIVE: {\n-\t\tif (body_len < 10)\n-\t\t\treturn -1;\n-\t\t/* Parse capabilities to detect I bit; set-only, so a crafted\n-\t\t * keepalive can't downgrade a peer back to Main framing\n-\t\t * mid-session (the Main keepalive path is set-only too). */\n-\t\tuint32_t caps = (uint32_t)body[6] << 24 | body[7] << 16 | body[8] << 8 | body[9];\n-\t\tif (caps & RIST_ADV_KEEPALIVE_CAP_I)\n-\t\t\tpeer->remote_supports_advanced = true;\n-\t\tpeer->last_pkt_received = timestampNTP_u64();\n-\t\trist_log_priv(ctx, RIST_LOG_DEBUG,\n-\t\t\t\"Advanced Keep-Alive: caps=0x%08x (I=%d)\\n\",\n-\t\t\tcaps, peer->remote_supports_advanced);\n-\t\treturn 0;\n-\t}\n-\n-\tcase RIST_ADV_CI_FLOW_ATTR: {\n-\t\tif (body_len == 0)\n-\t\t\treturn 0;\n-\t\trist_log_priv(ctx, RIST_LOG_DEBUG,\n-\t\t\t\"Advanced Flow Attribute received (%u bytes)\\n\", body_len);\n-\t\tif (peer->receiver_ctx) {\n-\t\t\tstruct rist_receiver *rcv = peer->receiver_ctx;\n-\t\t\tif (rcv->receiver_flow_attr_callback) {\n-\t\t\t\tchar json_buf[RIST_MAX_PACKET_SIZE];\n-\t\t\t\tsize_t copy_len = body_len < sizeof(json_buf) - 1 ? body_len : sizeof(json_buf) - 1;\n-\t\t\t\tmemcpy(json_buf, body, copy_len);\n-\t\t\t\tjson_buf[copy_len] = '\\0';\n-\t\t\t\trcv->receiver_flow_attr_callback(\n-\t\t\t\t\trcv->receiver_flow_attr_callback_argument,\n-\t\t\t\t\tpeer, json_buf, copy_len);\n-\t\t\t}\n-\t\t}\n-\t\treturn 0;\n-\t}\n-\n-\tcase RIST_ADV_CI_PSK_NONCE: {\n-\t\tif (body_len < 8)\n-\t\t\treturn -1;\n-\t\tuint8_t future_nonce[4];\n-\t\tmemcpy(future_nonce, body, 4);\n-\t\tuint16_t key_bits = (uint16_t)((body[4] << 8) | body[5]);\n-\t\tif (key_bits != 0 && key_bits != 128 && key_bits != 192 && key_bits != 256)\n-\t\t\treturn -1;\n-\n-\t\tbool odd = CHECK_BIT(future_nonce[0], 7);\n-\t\tstruct rist_key *ak = odd ? &peer->key_rx_odd : &peer->key_rx;\n-\t\tif (ak->password_len > 0) {\n-\t\t\tpthread_mutex_lock(&peer->peer_lock);\n-\t\t\t_librist_crypto_psk_preannounce_nonce(ak, future_nonce, key_bits);\n-\t\t\tpthread_mutex_unlock(&peer->peer_lock);\n-\t\t}\n-\n-\t\trist_log_priv(ctx, RIST_LOG_DEBUG,\n-\t\t\t\"Advanced PSK Future Nonce: nonce=0x%02x%02x%02x%02x key_bits=%u\\n\",\n-\t\t\tfuture_nonce[0], future_nonce[1], future_nonce[2], future_nonce[3],\n-\t\t\tkey_bits);\n-\t\treturn 0;\n-\t}\n-\n-\tcase RIST_ADV_CI_UNSUPPORTED: {\n-\t\tif (body_len < 8)\n-\t\t\treturn -1;\n-\t\tuint16_t unsup_ci = (uint16_t)((body[4] << 8) | body[5]);\n-\t\trist_log_priv(ctx, RIST_LOG_WARN,\n-\t\t\t\"Remote does not support control message CI=0x%04x\\n\", unsup_ci);\n-\t\treturn 0;\n-\t}\n-\n-\tdefault:\n-\t\trist_log_priv(ctx, RIST_LOG_DEBUG,\n-\t\t\t\"Unknown Advanced Profile control CI=0x%04x, sending unsupported response\\n\", ci);\n-\t\trist_adv_send_unsupported(peer, ci, body, body_len);\n-\t\treturn -1;\n-\t}\n-}\ndiff --git a/src/crypto/crypto-private.h b/src/crypto/crypto-private.h\nindex a39617f..e2bcd21 100755\n--- a/src/crypto/crypto-private.h\n+++ b/src/crypto/crypto-private.h\n@@ -10,23 +10,8 @@\n \n #include \"common/attributes.h\"\n #include <stdint.h>\n-#include <stddef.h>\n-\n-/* Best-effort wipe of transient key material that a compiler will not elide.\n- * Shared by the PSK and EAP v4 crypto paths. */\n-static inline void _librist_crypto_secure_zero(void *p, size_t n)\n-{\n-\tvolatile unsigned char *v = (volatile unsigned char *)p;\n-\twhile (n--)\n-\t\t*v++ = 0;\n-}\n \n RIST_PRIV uint64_t rist_siphash(uint64_t birthtime, uint32_t seq, const char *phrase);\n-\n-/* prand_u32: wall-clock fallback on CSPRNG failure. Non-security only (SSRC, flow-id, peer-id). */\n RIST_PRIV uint32_t prand_u32(void);\n \n-/* Fail-closed CSPRNG. Returns 0 on success with *out populated, non-zero error otherwise. */\n-RIST_PRIV int _librist_crypto_random_u32(uint32_t *out);\n-\n #endif\ndiff --git a/src/crypto/crypto.c b/src/crypto/crypto.c\nindex b8024d5..ad4290e 100755\n--- a/src/crypto/crypto.c\n+++ b/src/crypto/crypto.c\n@@ -9,7 +9,6 @@\n #include \"proto/rist_time.h\"\n #include \"udp-private.h\"\n #include \"sha256.h\"\n-#include \"crypto/random.h\"\n #include <string.h>\n #include <stdlib.h>\n /*\n@@ -43,22 +42,29 @@ uint64_t rist_siphash(uint64_t birthtime, uint32_t seq, const char *phrase)\n }\n */\n \n-/* Non-security caller of the CSPRNG with a wall-clock fallback on failure.\n- * Used by SSRC / flow-id / peer-id generation. Security-critical sites must\n- * use _librist_crypto_random_u32 instead. */\n+static bool seeded = false;\n+//Generate pseudo-random 32 bit\n uint32_t prand_u32(void) {\n-\tuint32_t u32 = 0;\n-\tif (_librist_crypto_ramdom_get_bytes((uint8_t *)&u32, sizeof(u32)) == 0)\n-\t\treturn u32;\n-\tuint32_t fallback = (uint32_t)timestampNTP_u64();\n-\treturn fallback ? fallback : 0xa5a5a5a5u;\n+\tif (!seeded) {\n+\t\tsrand((unsigned int)timestampNTP_u64());\n+\t\tseeded = true;\n+\t}\n+\tuint32_t u32;\n+\tuint8_t *u8 = (void *) &u32;\n+\tfor (size_t i = 0; i < sizeof(u32); i++) {\n+\t\tu8[i] = rand() % 256;//Use the lowest byte of rand()\n+\t}\n+\treturn u32;\n }\n \n-int _librist_crypto_random_u32(uint32_t *out) {\n-\tuint32_t u32 = 0;\n-\tint ret = _librist_crypto_ramdom_get_bytes((uint8_t *)&u32, sizeof(u32));\n-\tif (ret != 0)\n-\t\treturn ret;\n-\t*out = u32;\n-\treturn 0;\n+uint32_t rand_u32(void)\n+{\n+\tuint32_t u32;\n+\tuint8_t *u8 = (void *) &u32;\n+\n+\tfor (size_t i = 0; i < sizeof(u32); i++) {\n+\t\tu8[i] = rand() % 256;\n+\t}\n+\n+\treturn u32;\n }\ndiff --git a/src/crypto/eap_v4_crypto.c b/src/crypto/eap_v4_crypto.c\ndeleted file mode 100644\nindex 822f6fc..0000000\n--- a/src/crypto/eap_v4_crypto.c\n+++ /dev/null\n@@ -1,198 +0,0 @@\n-/* librist. Copyright © 2026 SipRadius LLC. All right reserved.\n- * Author: Sergio Ammirata, Ph.D. <sergio@ammirata.net>\n- *\n- * SPDX-License-Identifier: BSD-2-Clause\n- *\n- * EAP SHA256-SRP6a Version 4 authenticated passphrase channel primitives.\n- * Self-contained: depends only on the crypto backend (mbedTLS or Nettle) so it\n- * can be unit-tested without the rest of librist.\n- */\n-\n-#include \"config.h\"\n-#include \"eap_v4_crypto.h\"\n-#include <string.h>\n-\n-#if HAVE_MBEDTLS\n-#include \"mbedtls/md.h\"\n-#include \"mbedtls/gcm.h\"\n-#elif HAVE_NETTLE\n-#include <nettle/hmac.h>\n-#include <nettle/gcm.h>\n-#include <nettle/memops.h>\n-#include <nettle/version.h>\n-\n-/* nettle 4.0 dropped the length argument from the digest calls, and with it\n- * their ability to truncate. Take the whole digest and cut it here so the\n- * call sites read the same against either nettle. */\n-#if NETTLE_VERSION_MAJOR >= 4\n-static void rist_hmac_sha256_digest(struct hmac_sha256_ctx *ctx, size_t len, uint8_t *out)\n-{\n-\tuint8_t full[SHA256_DIGEST_SIZE];\n-\thmac_sha256_digest(ctx, full);\n-\tmemcpy(out, full, len < sizeof(full) ? len : sizeof(full));\n-\t_librist_crypto_secure_zero(full, sizeof(full));\n-}\n-\n-static void rist_gcm_aes256_digest(struct gcm_aes256_ctx *ctx, size_t len, uint8_t *out)\n-{\n-\tuint8_t full[GCM_DIGEST_SIZE];\n-\tgcm_aes256_digest(ctx, full);\n-\tmemcpy(out, full, len < sizeof(full) ? len : sizeof(full));\n-}\n-#else\n-#define rist_hmac_sha256_digest hmac_sha256_digest\n-#define rist_gcm_aes256_digest gcm_aes256_digest\n-#endif\n-#endif\n-\n-int _librist_crypto_hkdf_expand_sha256(const uint8_t *prk, size_t prk_len,\n-\t\t\t\t\t\t\t\t\t   const uint8_t *info, size_t info_len,\n-\t\t\t\t\t\t\t\t\t   uint8_t *okm, size_t okm_len)\n-{\n-\tconst size_t hash_len = 32;\n-\tif (okm_len == 0 || okm_len > 255 * hash_len)\n-\t\treturn -1;\n-#if HAVE_MBEDTLS\n-\tconst mbedtls_md_info_t *md = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);\n-\tif (md == NULL)\n-\t\treturn -1;\n-\tmbedtls_md_context_t ctx;\n-\tmbedtls_md_init(&ctx);\n-\tint rc = -1;\n-\tif (mbedtls_md_setup(&ctx, md, 1) != 0)\n-\t\tgoto done;\n-\tuint8_t t[32];\n-\tsize_t t_len = 0;\n-\tsize_t done_len = 0;\n-\tuint8_t counter = 0;\n-\twhile (done_len < okm_len) {\n-\t\tcounter++;\n-\t\tif (mbedtls_md_hmac_starts(&ctx, prk, prk_len) != 0) goto done;\n-\t\tif (t_len && mbedtls_md_hmac_update(&ctx, t, t_len) != 0) goto done;\n-\t\tif (info_len && mbedtls_md_hmac_update(&ctx, info, info_len) != 0) goto done;\n-\t\tif (mbedtls_md_hmac_update(&ctx, &counter, 1) != 0) goto done;\n-\t\tif (mbedtls_md_hmac_finish(&ctx, t) != 0) goto done;\n-\t\tt_len = hash_len;\n-\t\tsize_t n = okm_len - done_len < hash_len ? okm_len - done_len : hash_len;\n-\t\tmemcpy(okm + done_len, t, n);\n-\t\tdone_len += n;\n-\t}\n-\trc = 0;\n-done:\n-\t_librist_crypto_secure_zero(t, sizeof(t));\n-\tmbedtls_md_free(&ctx);\n-\tif (rc != 0)\n-\t\tmemset(okm, 0, okm_len);\n-\treturn rc;\n-#elif HAVE_NETTLE\n-\tstruct hmac_sha256_ctx ctx;\n-\thmac_sha256_set_key(&ctx, prk_len, prk);\n-\tuint8_t t[32];\n-\tsize_t t_len = 0;\n-\tsize_t done_len = 0;\n-\tuint8_t counter = 0;\n-\twhile (done_len < okm_len) {\n-\t\tcounter++;\n-\t\tif (t_len) hmac_sha256_update(&ctx, t_len, t);\n-\t\tif (info_len) hmac_sha256_update(&ctx, info_len, info);\n-\t\thmac_sha256_update(&ctx, 1, &counter);\n-\t\trist_hmac_sha256_digest(&ctx, hash_len, t); /* resets to post-set-key state */\n-\t\tt_len = hash_len;\n-\t\tsize_t n = okm_len - done_len < hash_len ? okm_len - done_len : hash_len;\n-\t\tmemcpy(okm + done_len, t, n);\n-\t\tdone_len += n;\n-\t}\n-\t_librist_crypto_secure_zero(t, sizeof(t));\n-\t_librist_crypto_secure_zero(&ctx, sizeof(ctx));\n-\treturn 0;\n-#else\n-\t(void)prk; (void)prk_len; (void)info; (void)info_len;\n-\tmemset(okm, 0, okm_len);\n-\treturn -1;\n-#endif\n-}\n-\n-int _librist_crypto_aes_gcm_encrypt(const uint8_t *key, const uint8_t *iv, size_t iv_len,\n-\t\t\t\t\t\t\t\t\tconst uint8_t *aad, size_t aad_len,\n-\t\t\t\t\t\t\t\t\tconst uint8_t *pt, size_t pt_len,\n-\t\t\t\t\t\t\t\t\tuint8_t *ct, uint8_t *tag, size_t tag_len)\n-{\n-#if HAVE_MBEDTLS\n-\tmbedtls_gcm_context ctx;\n-\tmbedtls_gcm_init(&ctx);\n-\tint rc = -1;\n-\tif (mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key, 256) != 0)\n-\t\tgoto done;\n-\tif (mbedtls_gcm_crypt_and_tag(&ctx, MBEDTLS_GCM_ENCRYPT, pt_len, iv, iv_len,\n-\t\t\t\t\t\t\t\t  aad, aad_len, pt, ct, tag_len, tag) != 0)\n-\t\tgoto done;\n-\trc = 0;\n-done:\n-\tmbedtls_gcm_free(&ctx);\n-\treturn rc;\n-#elif HAVE_NETTLE\n-\tstruct gcm_aes256_ctx ctx;\n-\tgcm_aes256_set_key(&ctx, key);\n-\tgcm_aes256_set_iv(&ctx, iv_len, iv);\n-\tif (aad_len)\n-\t\tgcm_aes256_update(&ctx, aad_len, aad);\n-\tgcm_aes256_encrypt(&ctx, pt_len, ct, pt);\n-\trist_gcm_aes256_digest(&ctx, tag_len, tag);\n-\t_librist_crypto_secure_zero(&ctx, sizeof(ctx));\n-\treturn 0;\n-#else\n-\t(void)key; (void)iv; (void)iv_len; (void)aad; (void)aad_len;\n-\t(void)pt; (void)pt_len; (void)ct; (void)tag; (void)tag_len;\n-\treturn -1;\n-#endif\n-}\n-\n-int _librist_crypto_aes_gcm_decrypt(const uint8_t *key, const uint8_t *iv, size_t iv_len,\n-\t\t\t\t\t\t\t\t\tconst uint8_t *aad, size_t aad_len,\n-\t\t\t\t\t\t\t\t\tconst uint8_t *ct, size_t ct_len,\n-\t\t\t\t\t\t\t\t\tconst uint8_t *tag, size_t tag_len,\n-\t\t\t\t\t\t\t\t\tuint8_t *pt)\n-{\n-#if HAVE_MBEDTLS\n-\tmbedtls_gcm_context ctx;\n-\tmbedtls_gcm_init(&ctx);\n-\tint rc = -1;\n-\tif (mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key, 256) != 0)\n-\t\tgoto done;\n-\tif (mbedtls_gcm_auth_decrypt(&ctx, ct_len, iv, iv_len, aad, aad_len,\n-\t\t\t\t\t\t\t\t tag, tag_len, ct, pt) != 0)\n-\t\tgoto done;\n-\trc = 0;\n-done:\n-\tmbedtls_gcm_free(&ctx);\n-\tif (rc != 0 && ct_len)\n-\t\tmemset(pt, 0, ct_len);\n-\treturn rc;\n-#elif HAVE_NETTLE\n-\tstruct gcm_aes256_ctx ctx;\n-\tuint8_t local_tag[16];\n-\tif (tag_len > sizeof(local_tag))\n-\t\treturn -1;\n-\tgcm_aes256_set_key(&ctx, key);\n-\tgcm_aes256_set_iv(&ctx, iv_len, iv);\n-\tif (aad_len)\n-\t\tgcm_aes256_update(&ctx, aad_len, aad);\n-\tgcm_aes256_decrypt(&ctx, ct_len, pt, ct);\n-\trist_gcm_aes256_digest(&ctx, tag_len, local_tag);\n-\t/* nettle memeql_sec is constant-time; returns 1 when equal. */\n-\tif (memeql_sec(local_tag, tag, tag_len) != 1) {\n-\t\tif (ct_len)\n-\t\t\tmemset(pt, 0, ct_len);\n-\t\t_librist_crypto_secure_zero(&ctx, sizeof(ctx));\n-\t\treturn -1;\n-\t}\n-\t_librist_crypto_secure_zero(&ctx, sizeof(ctx));\n-\treturn 0;\n-#else\n-\t(void)key; (void)iv; (void)iv_len; (void)aad; (void)aad_len;\n-\t(void)ct; (void)tag; (void)tag_len;\n-\tif (ct_len)\n-\t\tmemset(pt, 0, ct_len);\n-\treturn -1;\n-#endif\n-}\ndiff --git a/src/crypto/eap_v4_crypto.h b/src/crypto/eap_v4_crypto.h\ndeleted file mode 100644\nindex b67ae65..0000000\n--- a/src/crypto/eap_v4_crypto.h\n+++ /dev/null\n@@ -1,48 +0,0 @@\n-/* librist. Copyright © 2026 SipRadius LLC. All right reserved.\n- * Author: Sergio Ammirata, Ph.D. <sergio@ammirata.net>\n- *\n- * SPDX-License-Identifier: BSD-2-Clause\n- */\n-\n-#ifndef RIST_CRYPTO_EAP_V4_CRYPTO_H\n-#define RIST_CRYPTO_EAP_V4_CRYPTO_H\n-\n-#include \"common/attributes.h\"\n-#include \"crypto-private.h\"\n-#include <stdint.h>\n-#include <stddef.h>\n-\n-/* EAP SHA256-SRP6a Version 4 authenticated passphrase channel primitives:\n- * HKDF-Expand-SHA256 (per-direction key derivation) and AES-256-GCM. AEAD/HMAC\n- * need a real crypto backend (mbedTLS or Nettle); the built-in fallback returns\n- * -1, which the caller treats as \"v4 unavailable, negotiate down to v3\".\n- *\n- * All functions return 0 on success and a negative value on error / backend\n- * unavailable. */\n-\n-/* _librist_crypto_secure_zero (best-effort key-material wipe) is provided by\n- * crypto-private.h. */\n-\n-/* RFC 5869 §2.3 HKDF-Expand with SHA-256. PRK is used directly as the HMAC key\n- * (K is already a uniformly random 256-bit value, so Extract is unnecessary). */\n-RIST_PRIV int _librist_crypto_hkdf_expand_sha256(const uint8_t *prk, size_t prk_len,\n-                                                 const uint8_t *info, size_t info_len,\n-                                                 uint8_t *okm, size_t okm_len);\n-\n-/* AES-256-GCM encrypt. key is 32 octets; iv is iv_len octets (12 for v4);\n- * tag_len is 16. ct must have room for pt_len octets. */\n-RIST_PRIV int _librist_crypto_aes_gcm_encrypt(const uint8_t *key, const uint8_t *iv, size_t iv_len,\n-                                              const uint8_t *aad, size_t aad_len,\n-                                              const uint8_t *pt, size_t pt_len,\n-                                              uint8_t *ct, uint8_t *tag, size_t tag_len);\n-\n-/* AES-256-GCM decrypt with tag verification. Returns 0 only if the tag is valid;\n- * on any failure the plaintext buffer is zeroed so a caller can never act on\n- * unverified output. */\n-RIST_PRIV int _librist_crypto_aes_gcm_decrypt(const uint8_t *key, const uint8_t *iv, size_t iv_len,\n-                                              const uint8_t *aad, size_t aad_len,\n-                                              const uint8_t *ct, size_t ct_len,\n-                                              const uint8_t *tag, size_t tag_len,\n-                                              uint8_t *pt);\n-\n-#endif\ndiff --git a/src/crypto/psk.c b/src/crypto/psk.c\nindex 08ef8d8..72dbace 100644\n--- a/src/crypto/psk.c\n+++ b/src/crypto/psk.c\n@@ -9,22 +9,8 @@\n #include \"psk.h\"\n #include \"log-private.h\"\n #include \"crypto-private.h\"\n-#include \"proto/rist_time.h\"\n #include <string.h>\n \n-/* Minimum wall-time between nonce-adoption rekeys (PBKDF2) on one key.\n- * Bounds the CPU a packet flood with randomized nonces can burn. The floor\n- * is dictated by the reflector topology: a subscriber's single key sees\n- * interleaved nonces from the reflector's own RTCP and the forwarded\n- * publisher media, so legitimate rekeys arrive at the RTCP cadence\n- * (~2 per 100 ms interval); the limit must stay well above that or valid\n- * media is dropped. 10 ms still caps a flood at ~100 PBKDF2/s per key. */\n-#define RIST_REKEY_MIN_INTERVAL (ONE_SECOND / 100)\n-\n-/* bad_count strikes marking decryption-failure abuse before the decrypt-path\n- * rekey rate limit engages; legitimate nonce churn keeps bad_count at 0. */\n-#define RIST_REKEY_ABUSE_STRIKES 2\n-\n #if HAVE_MBEDTLS\n #include \"mbedtls/aes.h\"\n #include \"mbedtls/md.h\"\n@@ -49,9 +35,8 @@\n //TODO: handle failures?\n int _librist_crypto_psk_rist_key_init(struct rist_key *key, uint32_t key_size, uint32_t rotation, const char *password, bool odd)\n {\n-\tkey->password_len = strnlen(password, sizeof(key->password) - 1);\n+\tkey->password_len = strlen(password);\n \tmemcpy(key->password, password, key->password_len);\n-\tkey->password[key->password_len] = '\\0';\n \tkey->key_size = key_size;\n \tkey->key_rotation = rotation;\n #if HAVE_MBEDTLS\n@@ -76,19 +61,13 @@ int _librist_crypto_psk_rist_key_destroy(struct rist_key *key)\n \t    linux_crypto_free(&key->linux_crypto_ctx);\n #endif\n     }\n-\t/* Wipe key material: the peer struct is freed without zeroing. */\n-\t_librist_crypto_secure_zero(key->password, sizeof(key->password));\n-\t_librist_crypto_secure_zero(key->gre_nonce, sizeof(key->gre_nonce));\n-\t_librist_crypto_secure_zero(key->iv, sizeof(key->iv));\n-\t_librist_crypto_secure_zero(key->aes_key_sched, sizeof(key->aes_key_sched));\n \treturn 0;\n }\n \n int _librist_crypto_psk_rist_key_clone(struct rist_key *key_in, struct rist_key *key_out)\n {\n \tkey_out->password_len = key_in->password_len;\n-\tmemcpy(key_out->password, key_in->password, key_in->password_len);\n-\tkey_out->password[key_out->password_len] = '\\0';\n+    memcpy(key_out->password, key_in->password, key_in->password_len);\n     key_out->key_size = key_in->key_size;\n     key_out->key_rotation = key_in->key_rotation;\n #if HAVE_MBEDTLS\n@@ -105,33 +84,32 @@ int _librist_crypto_psk_rist_key_clone(struct rist_key *key_in, struct rist_key\n static void _librist_crypto_aes_key(struct rist_key *key)\n {\n     uint8_t aes_key[256 / 8];\n-    /* Hard invariant: key_size drives the PBKDF2 output length into this\n-     * 32-byte stack buffer. Anything but a valid AES size here means the\n-     * value arrived unvalidated from the wire or config; refuse rather\n-     * than overflow. */\n-    if (key->key_size != 128 && key->key_size != 192 && key->key_size != 256) {\n-        key->bad_decryption = true;\n-        return;\n-    }\n #if HAVE_MBEDTLS\n     mbedtls_md_context_t sha_ctx;\n     const mbedtls_md_info_t *info_sha;\n     int ret = -1;\n+    /* Setup the hash/HMAC function, for the PBKDF2 function. */\n     mbedtls_md_init(&sha_ctx);\n     info_sha = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);\n-    if (info_sha == NULL)\n-        goto fail;\n+    if (info_sha == NULL) {\n+            // rist_log_priv(cctx, RIST_LOG_ERROR, \"Failed to setup Mbed TLS\n+            // hash info\\n\");\n+    }\n \n     ret = mbedtls_md_setup(&sha_ctx, info_sha, 1);\n-    if (ret != 0)\n-        goto fail;\n+    if (ret != 0) {\n+            // rist_log_priv(cctx, RIST_LOG_ERROR, \"Failed to setup Mbed TLS MD\n+            // ctx\");\n+    }\n \n     ret = mbedtls_pkcs5_pbkdf2_hmac(\n         &sha_ctx, (const unsigned char *)key->password, key->password_len,\n         key->gre_nonce, sizeof(key->gre_nonce),\n         RIST_PBKDF2_HMAC_SHA256_ITERATIONS, key->key_size / 8, aes_key);\n-    if (ret != 0)\n-        goto fail;\n+    if (ret != 0) {\n+            // rist_log_priv(cctx, RIST_LOG_ERROR, \"Mbed TLS pbkdf2 function\n+            // failed\\n\");\n+    }\n     mbedtls_md_free(&sha_ctx);\n #elif HAVE_NETTLE\n     nettle_pbkdf2_hmac_sha256(key->password_len,(const uint8_t*)key->password,\n@@ -152,15 +130,15 @@ static void _librist_crypto_aes_key(struct rist_key *key)\n #elif HAVE_NETTLE\n \tswitch(key->key_size) {\n \tcase 256:\n-        nettle_aes256_set_encrypt_key(&key->nettle_ctx.ctx256, aes_key);\n+        nettle_aes256_set_encrypt_key(&key->nettle_ctx.u.ctx256, aes_key);\n         break;\n \tcase 192:\n-        nettle_aes192_set_encrypt_key(&key->nettle_ctx.ctx192, aes_key);\n+        nettle_aes192_set_encrypt_key(&key->nettle_ctx.u.ctx192, aes_key);\n         break;\n \tcase 128:\n \t\tRIST_FALLTHROUGH;\n \tdefault:\n-\t\tnettle_aes128_set_encrypt_key(&key->nettle_ctx.ctx128, aes_key);\n+\t\tnettle_aes128_set_encrypt_key(&key->nettle_ctx.u.ctx128, aes_key);\n     }\n #elif defined(LINUX_CRYPTO)\n     if (key->linux_crypto_ctx)\n@@ -171,17 +149,6 @@ static void _librist_crypto_aes_key(struct rist_key *key)\n     aes_key_setup(aes_key, key->aes_key_sched, key->key_size);\n #endif\n     key->used_times = 0;\n-    _librist_crypto_secure_zero(aes_key, sizeof(aes_key));\n-    return;\n-#if HAVE_MBEDTLS\n-fail:\n-    mbedtls_md_free(&sha_ctx);\n-    _librist_crypto_secure_zero(aes_key, sizeof(aes_key));\n-    /* Leave any prior key install in place but force the lockout flag so we\n-     * don't run AES-CTR with whatever happened to be on the stack. */\n-    key->bad_decryption = true;\n-    return;\n-#endif\n }\n \n //This doesn't really belong here (not PSK related), but since all other crypto interop stuff is here it goes in here..\n@@ -195,26 +162,20 @@ void _librist_crypto_aes_ctr(const uint8_t key[], int key_size, uint8_t iv[], co\n \tmbedtls_aes_crypt_ctr(&ctx, payload_len, &nc_off, iv, stream_block, inbuf, outbuf);\n \tmbedtls_aes_free(&ctx);\n #elif HAVE_NETTLE\n-    union rist_nettle_aes_ctx aes_ctx;\n+    struct aes_ctx aes_ctx;\n     memset(&aes_ctx, 0, sizeof(aes_ctx));\n     nettle_cipher_func *f;\n     switch (key_size) {\n     case 256:\n-\t\tnettle_aes256_set_encrypt_key(&aes_ctx.ctx256, key);\n+\t\tnettle_aes256_set_encrypt_key(&aes_ctx.u.ctx256, key);\n \t\tf = (nettle_cipher_func *)nettle_aes256_encrypt;\n \t\tbreak;\n-\tcase 192:\n-\t\tnettle_aes192_set_encrypt_key(&aes_ctx.ctx192, key);\n-\t\tf = (nettle_cipher_func *)nettle_aes192_encrypt;\n-\t\tbreak;\n \tcase 128:\n-\t\tnettle_aes128_set_encrypt_key(&aes_ctx.ctx128, key);\n-\t\tf = (nettle_cipher_func *)nettle_aes128_encrypt;\n+\t\tnettle_aes128_set_encrypt_key(&aes_ctx.u.ctx128, key);\n+\t\tf = (nettle_cipher_func *)nettle_aes192_encrypt;\n \t\tbreak;\n-\tdefault:\n-\t\treturn;\n \t}\n-\tnettle_ctr_crypt(&aes_ctx, f, AES_BLOCK_SIZE, iv, payload_len, outbuf, inbuf);\n+\tnettle_ctr_crypt(&aes_ctx.u, f, AES_BLOCK_SIZE, iv, payload_len, outbuf, inbuf);\n #else\n     uint32_t aes_key_sched[60];\n     aes_key_setup(key, aes_key_sched, key_size);\n@@ -229,19 +190,18 @@ static void _librist_crypto_psk_aes_ctr(struct rist_key *key, const uint8_t inbu\n #elif HAVE_NETTLE\n \tnettle_cipher_func *f;\n \tswitch(key->key_size) {\n-\tcase 128:\n-\t\tf = (nettle_cipher_func *)nettle_aes128_encrypt;\n+\tcase 256:\n+\t\tf = (nettle_cipher_func *)nettle_aes256_encrypt;\n \t\tbreak;\n \tcase 192:\n \t\tf = (nettle_cipher_func *)nettle_aes192_encrypt;\n \t\tbreak;\n-\tcase 256:\n-\t\tf = (nettle_cipher_func *)nettle_aes256_encrypt;\n-\t\tbreak;\n+\tcase 128:\n+\t\tRIST_FALLTHROUGH;\n \tdefault:\n-\t\treturn;\n+\t\tf = (nettle_cipher_func *)nettle_aes128_encrypt;\n \t}\n-\tnettle_ctr_crypt(&key->nettle_ctx, f, AES_BLOCK_SIZE, key->iv,payload_len, outbuf, inbuf);\n+\tnettle_ctr_crypt(&key->nettle_ctx.u, f, AES_BLOCK_SIZE, key->iv,payload_len, outbuf, inbuf);\n #elif defined(LINUX_CRYPTO)\n \tif (key->linux_crypto_ctx)\n \t\tlinux_crypto_decrypt(inbuf, outbuf, payload_len, key->iv, key->linux_crypto_ctx);\n@@ -262,29 +222,10 @@ static void _librist_crypto_psk_prepare_iv(struct rist_key *key, uint8_t gre_ver\n }\n \n static void _librist_crypto_psk_generate_nonce(struct rist_key *key) {\n-\t/* Fail-closed CSPRNG: on failure, mark the key locked so encrypt/decrypt\n-\t * short-circuit instead of running AES under a predictable nonce. */\n-\tuint32_t nonce_val = 0;\n-\tfor (int attempts = 0; attempts < 8; attempts++) {\n-\t\tif (_librist_crypto_random_u32(&nonce_val) != 0) {\n-\t\t\trist_log_priv3(RIST_LOG_ERROR,\n-\t\t\t\t\"PSK nonce generation: CSPRNG unavailable, \"\n-\t\t\t\t\"PSK encrypt/decrypt locked out until passphrase rotation\\n\");\n-\t\t\tkey->csprng_failed = true;\n-\t\t\tkey->bad_decryption = true;\n-\t\t\treturn;\n-\t\t}\n-\t\tif (nonce_val != 0)\n-\t\t\tbreak;\n-\t}\n-\tif (nonce_val == 0) {\n-\t\t/* 8 zeros in a row from a working CSPRNG is 2^-256; treat as malfunction. */\n-\t\trist_log_priv3(RIST_LOG_ERROR,\n-\t\t\t\"PSK nonce generation: CSPRNG returned only zeros, locking out\\n\");\n-\t\tkey->csprng_failed = true;\n-\t\tkey->bad_decryption = true;\n-\t\treturn;\n-\t}\n+\tuint32_t nonce_val;\n+\tdo {\n+\t\tnonce_val = prand_u32();\n+\t} while (!nonce_val);\n \n \tmemcpy(key->gre_nonce, &nonce_val, sizeof(key->gre_nonce));\n \n@@ -295,42 +236,19 @@ static void _librist_crypto_psk_generate_nonce(struct rist_key *key) {\n \n void _librist_crypto_psk_decrypt(struct rist_key *key, uint8_t nonce[4], uint32_t seq_nbe, uint8_t gre_version, const uint8_t inbuf[], uint8_t outbuf[], size_t payload_len)\n {\n-\tuint32_t nonce_val;\n-\tmemcpy(&nonce_val, nonce, sizeof(nonce_val));\n-    /* A zero nonce never comes from a legitimate sender; drop the packet.\n-     * Do NOT latch bad_decryption here: a zero nonce triggers no PBKDF2,\n-     * so the latch only served as a one-packet permanent session kill. */\n+\tuint32_t nonce_val = *((uint32_t *)nonce);\n     if (!nonce_val)\n         return;\n \n     if (memcmp(nonce, key->gre_nonce, sizeof(key->gre_nonce)) != 0) {\n-        /* Adopt the new nonce even while locked out: a latch no fresh nonce\n-         * can clear turns one spoofed packet (or six garbage decryptions)\n-         * into a permanent session kill and blocks legitimate passphrase\n-         * rotations. The PBKDF2-CPU-DoS protection the lockout provided\n-         * becomes a rekey rate limit that engages ONLY under abuse:\n-         * legitimate nonce churn (rotations, reflector RTCP/media nonce\n-         * interleave) decrypts cleanly and keeps bad_count at 0, so it is\n-         * never throttled; a garbage flood pushes bad_count past\n-         * RIST_REKEY_ABUSE_STRIKES and is throttled to RIST_REKEY_MIN_INTERVAL. */\n-        uint64_t now = timestampNTP_u64();\n-        if (key->bad_count > RIST_REKEY_ABUSE_STRIKES && key->last_rekey &&\n-            (now - key->last_rekey) < RIST_REKEY_MIN_INTERVAL)\n-            return;\n         memcpy(key->gre_nonce, nonce, sizeof(key->gre_nonce));\n         _librist_crypto_aes_key(key);\n-        /* Only clear the flag if _librist_crypto_aes_key succeeded;\n-         * it sets bad_decryption=true on PBKDF2/setup failure. */\n-        if (key->bad_decryption)\n-            return;\n+        key->bad_decryption = false;\n         key->bad_count = 0;\n-        key->last_rekey = now;\n     }\n \n-    if (key->used_times > RIST_AES_KEY_REUSE_TIMES) {\n-        key->bad_decryption = true;\n+    if (key->used_times > RIST_AES_KEY_REUSE_TIMES)\n         return;\n-    }\n \n     _librist_crypto_psk_prepare_iv(key, gre_version, seq_nbe);\n #if HAVE_MBEDTLS\n@@ -342,20 +260,11 @@ void _librist_crypto_psk_decrypt(struct rist_key *key, uint8_t nonce[4], uint32_\n \n void _librist_crypto_psk_encrypt(struct rist_key *key, uint32_t seq_nbe, uint8_t gre_version,const uint8_t inbuf[], uint8_t outbuf[], size_t payload_len)\n {\n-    uint32_t nonce_val;\n-    memcpy(&nonce_val, key->gre_nonce, sizeof(nonce_val));\n+    uint32_t nonce_val = *((uint32_t *)key->gre_nonce);\n     if (!nonce_val || (key->used_times +1) > RIST_AES_KEY_REUSE_TIMES || (key->key_rotation > 0 && key->used_times >= key->key_rotation)) {\n         _librist_crypto_psk_generate_nonce(key);\n-        if (key->csprng_failed) {\n-            memset(outbuf, 0, payload_len);\n-            return;\n-        }\n         _librist_crypto_aes_key(key);\n     }\n-    if (key->csprng_failed) {\n-        memset(outbuf, 0, payload_len);\n-        return;\n-    }\n     _librist_crypto_psk_prepare_iv(key, gre_version, seq_nbe);\n #if HAVE_MBEDTLS\n     key->aes_offset = 0;\n@@ -373,10 +282,6 @@ int _librist_crypto_psk_set_passphrase(struct rist_key *key, const uint8_t *pass\n \tmemcpy(key->password, passsphrase, passphrase_len);\n \tkey->password_len = passphrase_len;\n \tkey->used_times = 0;\n-\tkey->csprng_failed = false; /* fresh passphrase, retry CSPRNG */\n-\tkey->bad_decryption = false; /* fresh passphrase: clear any lockout */\n-\tkey->bad_count = 0;\n-\tkey->last_rekey = 0;\n \t_librist_crypto_psk_generate_nonce(key);\n \t_librist_crypto_aes_key(key);\n \treturn 0;\n@@ -390,33 +295,3 @@ void _librist_crypto_psk_get_passphrase(struct rist_key *key, const uint8_t **pa\n void _librist_crypto_psk_encrypt_continue(struct rist_key *key, const uint8_t inbuf[], uint8_t outbuf[], size_t payload_len) {\n \t_librist_crypto_psk_aes_ctr(key, inbuf, outbuf, payload_len);\n }\n-\n-void _librist_crypto_psk_preannounce_nonce(struct rist_key *key, const uint8_t nonce[4], uint32_t key_size_bits) {\n-\tuint32_t nonce_val;\n-\tmemcpy(&nonce_val, nonce, sizeof(nonce_val));\n-\tif (!nonce_val)\n-\t\treturn;\n-\tif (memcmp(nonce, key->gre_nonce, sizeof(key->gre_nonce)) == 0)\n-\t\treturn;\n-\t/* key_size_bits comes straight off the wire (Advanced PSK nonce\n-\t * control message); accept only valid AES sizes. */\n-\tif (key_size_bits) {\n-\t\tif (key_size_bits != 128 && key_size_bits != 192 && key_size_bits != 256)\n-\t\t\treturn;\n-\t\tkey->key_size = key_size_bits;\n-\t}\n-\t/* This control message is reachable before authentication and carries no\n-\t * decrypt outcome, so there is no bad_count abuse signal to gate on (unlike\n-\t * the decrypt path); keep the flat RIST_REKEY_MIN_INTERVAL rate limit\n-\t * unconditionally. Preannounce is not in the media hot path, so the flat\n-\t * floor costs nothing legitimate. */\n-\tuint64_t now = timestampNTP_u64();\n-\tif (key->last_rekey && (now - key->last_rekey) < RIST_REKEY_MIN_INTERVAL)\n-\t\treturn;\n-\tmemcpy(key->gre_nonce, nonce, sizeof(key->gre_nonce));\n-\t_librist_crypto_aes_key(key);\n-\tif (!key->bad_decryption) {\n-\t\tkey->bad_count = 0;\n-\t\tkey->last_rekey = now;\n-\t}\n-}\ndiff --git a/src/crypto/psk.h b/src/crypto/psk.h\nindex 04e9404..dd9e924 100644\n--- a/src/crypto/psk.h\n+++ b/src/crypto/psk.h\n@@ -15,14 +15,6 @@\n #include \"mbedtls/aes.h\"\n #elif HAVE_NETTLE\n #include <nettle/aes.h>\n-/* nettle's unified struct aes_ctx was deprecated for years and removed in\n- * 4.0. All we ever wanted from it was somewhere to hold one of the three\n- * per-size key schedules, so hold it ourselves. */\n-union rist_nettle_aes_ctx {\n-\tstruct aes128_ctx ctx128;\n-\tstruct aes192_ctx ctx192;\n-\tstruct aes256_ctx ctx256;\n-};\n #else\n #ifdef LINUX_CRYPTO\n #include \"linux-crypto.h\"\n@@ -46,7 +38,7 @@ struct rist_key {\n \tunsigned char strean_block[16];\n \tmbedtls_aes_context mbedtls_aes_ctx;\n #elif HAVE_NETTLE\n-\tunion rist_nettle_aes_ctx nettle_ctx;\n+\tstruct aes_ctx nettle_ctx;\n #elif defined(LINUX_CRYPTO)\n \tstruct linux_crypto *linux_crypto_ctx;\n #endif\n@@ -58,8 +50,6 @@ struct rist_key {\n     bool bad_decryption;\n     int bad_count;\n \tbool odd;\n-    bool csprng_failed; /* set when nonce generation can't draw randomness; encrypt/decrypt short-circuit */\n-    uint64_t last_rekey; /* timestampNTP_u64() of the last nonce-adoption rekey; rate-limits PBKDF2 */\n };\n \n RIST_PRIV int _librist_crypto_psk_rist_key_init(struct rist_key *key, uint32_t key_size, uint32_t rotation, const char *password, bool odd);\n@@ -71,5 +61,4 @@ RIST_PRIV void _librist_crypto_psk_encrypt_continue(struct rist_key *key, const\n RIST_PRIV int _librist_crypto_psk_set_passphrase(struct rist_key *key, const uint8_t *passsphrase, size_t passphrase_len);\n RIST_PRIV void _librist_crypto_psk_get_passphrase(struct rist_key *key, const uint8_t **passphrase, size_t *passphrase_len);\n RIST_PRIV void _librist_crypto_aes_ctr(const uint8_t key[], int key_size, uint8_t iv[], const uint8_t inbuf[], uint8_t outbuf[], size_t payload_len);\n-RIST_PRIV void _librist_crypto_psk_preannounce_nonce(struct rist_key *key, const uint8_t nonce[4], uint32_t key_size_bits);\n #endif\ndiff --git a/src/crypto/random.c b/src/crypto/random.c\nindex e964fbb..10b6772 100644\n--- a/src/crypto/random.c\n+++ b/src/crypto/random.c\n@@ -1,47 +1,15 @@\n #include \"random.h\"\n #include \"config.h\"\n-#include \"log-private.h\"\n \n #if HAVE_MBEDTLS\n-#include <string.h>\n-\n #include <mbedtls/ctr_drbg.h>\n #include <mbedtls/entropy.h>\n \n #include \"pthread-shim.h\"\n #include \"vcs_version.h\"\n \n-#ifdef _WIN32\n-/* Seed the DRBG from BCryptGenRandom (CNG) directly. The mbedTLS-provided\n- * Windows entropy source uses the legacy CryptoAPI which is missing or\n- * unconfigured under wine and stripped-down container images (#210); going\n- * through f_rng instead of the entropy module also avoids touching any\n- * MBEDTLS_PRIVATE() internals on mbedTLS 3.x. */\n-#include <windows.h>\n-#include <bcrypt.h>\n-#ifndef BCRYPT_USE_SYSTEM_PREFERRED_RNG\n-#define BCRYPT_USE_SYSTEM_PREFERRED_RNG 0x00000002\n-#endif\n-\n-static int _librist_bcrypt_f_rng(void *data, unsigned char *output, size_t len)\n-{\n-\t(void) data;\n-\tNTSTATUS s = BCryptGenRandom(NULL, output, (ULONG) len,\n-\t                             BCRYPT_USE_SYSTEM_PREFERRED_RNG);\n-\tif (s != 0)\n-\t\treturn MBEDTLS_ERR_CTR_DRBG_ENTROPY_SOURCE_FAILED;\n-\treturn 0;\n-}\n-#endif /* _WIN32 */\n-\n-#ifndef _WIN32\n static mbedtls_entropy_context entropy_ctx;\n-#endif\n static mbedtls_ctr_drbg_context ctr_drbg_ctx;\n-/* 0 once mbedtls_ctr_drbg_seed succeeds; non-zero (the mbedTLS error)\n- * if seeding failed and the DRBG context is unusable. Read-only after\n- * pthread_once / InitOnce returns. */\n-static int ctr_drbg_seed_ret = MBEDTLS_ERR_CTR_DRBG_ENTROPY_SOURCE_FAILED;\n \n #if !defined(_WIN32) || HAVE_PTHREADS\n //For some reason GNU Hurd complains that PTHREAD_ONCE_INIT isn't a constant\n@@ -63,27 +31,11 @@ static BOOL WINAPI librist_crypto_srp_init_random_func(PINIT_ONCE InitOnce, PVOI\n #endif\n {\n #if HAVE_MBEDTLS\n+\tmbedtls_entropy_init(&entropy_ctx);\n \tmbedtls_ctr_drbg_init(&ctr_drbg_ctx);\n \t//ctr_drbg_ctx is threadsafe, so can be used by multiple threads freely, seeding isn't though.\n \tconst char user_custom[] = \"libRIST librist_crypto_random_init_func \"LIBRIST_VERSION;\n-#ifdef _WIN32\n-\tctr_drbg_seed_ret = mbedtls_ctr_drbg_seed(&ctr_drbg_ctx, _librist_bcrypt_f_rng, NULL,\n-\t\t(const unsigned char *)user_custom, sizeof(user_custom));\n-#else\n-\tmbedtls_entropy_init(&entropy_ctx);\n-\tctr_drbg_seed_ret = mbedtls_ctr_drbg_seed(&ctr_drbg_ctx, mbedtls_entropy_func, &entropy_ctx,\n-\t\t(const unsigned char *)user_custom, sizeof(user_custom));\n-#endif\n-\tif (ctr_drbg_seed_ret != 0) {\n-\t\t/* Sandbox / container with no entropy source, missing /dev/urandom,\n-\t\t * or a broken mbedTLS build. The DRBG is now unusable; every\n-\t\t * subsequent _librist_crypto_ramdom_get_bytes() call will return\n-\t\t * the same error rather than silently emitting zeros. */\n-\t\trist_log_priv3(RIST_LOG_ERROR,\n-\t\t\t\"CSPRNG seeding failed (mbedtls_ctr_drbg_seed = -0x%04x); \"\n-\t\t\t\"all crypto-random calls will fail until the entropy source is fixed\\n\",\n-\t\t\t(unsigned)-ctr_drbg_seed_ret);\n-\t}\n+\tmbedtls_ctr_drbg_seed(&ctr_drbg_ctx, mbedtls_entropy_func, &entropy_ctx, (const unsigned char  *)user_custom, sizeof(user_custom));\n #endif\n #if !HAVE_PTHREADS\n \treturn 1;\n@@ -106,12 +58,9 @@ static void _librist_crypto_random_init(void) {\n #endif\n \n int _librist_crypto_ramdom_get_bytes(uint8_t buf[], size_t buflen) {\n-#if HAVE_MBEDTLS || HAVE_NETTLE\n \t_librist_crypto_random_init();\n \tint ret;\n #if HAVE_MBEDTLS\n-\tif (ctr_drbg_seed_ret != 0)\n-\t\treturn ctr_drbg_seed_ret;\n \tret = mbedtls_ctr_drbg_random(&ctr_drbg_ctx, buf, buflen);\n #elif HAVE_NETTLE\n \tint i=0;\n@@ -119,29 +68,18 @@ int _librist_crypto_ramdom_get_bytes(uint8_t buf[], size_t buflen) {\n \t\tret = gnutls_rnd(GNUTLS_RND_NONCE, buf, buflen);//This call is thread-safe\n \t\ti++;\n \t} while (ret != 0 && i < 10);\n-\tif (ret != 0)\n-\t\trist_log_priv3(RIST_LOG_ERROR,\n-\t\t\t\"CSPRNG (gnutls_rnd) failed %d times in a row, returning %d\\n\",\n-\t\t\ti, ret);\n #endif\n \treturn ret;\n-#else\n-\t(void)buf; (void)buflen;\n-\treturn -1;\n-#endif\n }\n \n int _librist_crypto_random_get_string(char buf[], size_t len) {\n-\t// 64 chars to keep the modulo bias-free (power of two)\n-\tstatic const char charset[] = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!?\";\n-\tconst size_t charset_n = sizeof(charset) - 1;\n+\tchar charset[] = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!?\";//Cut to 64 characters to deal with modulo bias\n \tuint8_t rand_buf[128];\n-\tif (len > sizeof(rand_buf))\n-\t\treturn -1;\n \tint ret = _librist_crypto_ramdom_get_bytes(rand_buf, len);\n \tif (ret != 0)\n \t\treturn ret;\n-\tfor (size_t i = 0; i < len; i++)\n-\t\tbuf[i] = charset[rand_buf[i] % charset_n];\n+\tfor (size_t i=0; i< len; i++) {\n+\t\tbuf[i] = charset[rand_buf[i] % sizeof(charset) -1];\n+\t}\n \treturn 0;\n }\ndiff --git a/src/crypto/srp.c b/src/crypto/srp.c\nindex 0ddb51c..baf3049 100644\n--- a/src/crypto/srp.c\n+++ b/src/crypto/srp.c\n@@ -11,13 +11,6 @@\n #include <stdlib.h>\n #include <stdbool.h>\n \n-static int constant_time_memcmp(const uint8_t *a, const uint8_t *b, size_t n) {\n-\tuint8_t diff = 0;\n-\tfor (size_t i = 0; i < n; i++)\n-\t\tdiff |= a[i] ^ b[i];\n-\treturn diff != 0;\n-}\n-\n #ifndef WINAPI\n #define WINAPI\n #endif\n@@ -47,22 +40,19 @@ static void print_hash(const uint8_t *buf, char *specifier) {\n \n #if HAVE_MBEDTLS\n #include <mbedtls/bignum.h>\n+#include <mbedtls/entropy.h>\n+#include <mbedtls/ctr_drbg.h>\n #include <mbedtls/sha256.h>\n #include <mbedtls/version.h>\n \n-#if MBEDTLS_VERSION_NUMBER < 0x02070000\n-#error \"librist requires mbedTLS >= 2.7 (the *_ret SHA-256 API). Upgrade your mbedTLS or build with the bundled copy in contrib/mbedtls.\"\n+#if MBEDTLS_VERSION_NUMBER > 0x02070000\n+#define USE_SHA_RET 1\n+#else\n+#define USE_SHA_RET 0\n #endif\n \n #if MBEDTLS_VERSION_NUMBER >= 0x03000000\n-/* mbedTLS 3.x dropped the _ret suffix; provide our own compat macros\n- * instead of pulling in the deprecated compat-2.x.h header (which\n- * triggers -Werror on distro builds with MBEDTLS_DEPRECATED_WARNING\n- * and is removed entirely in mbedTLS 4.x). */\n-#define mbedtls_sha256_starts_ret mbedtls_sha256_starts\n-#define mbedtls_sha256_update_ret mbedtls_sha256_update\n-#define mbedtls_sha256_finish_ret mbedtls_sha256_finish\n-#define mbedtls_sha256_ret        mbedtls_sha256\n+#include <mbedtls/compat-2.x.h>\n #endif\n \n int _librist_srp_mbedtls_wrap_random(void *unused, unsigned char * buf, size_t size) {\n@@ -79,13 +69,7 @@ int _librist_srp_mbedtls_wrap_random(void *unused, unsigned char * buf, size_t s\n #if MBEDTLS_HAS_MPI_RANDOM\n #define BIGNUM_RANDOM(num, max) ret = mbedtls_mpi_random(num, 0, max, _librist_srp_mbedtls_wrap_random, NULL);\n #else\n-#define BIGNUM_RANDOM(num, max) do { \\\n-\tsize_t _rlen = mbedtls_mpi_size(max); \\\n-\tdo { \\\n-\t\tret = mbedtls_mpi_fill_random(num, _rlen, _librist_srp_mbedtls_wrap_random, NULL); \\\n-\t\tif (ret != 0) break; \\\n-\t} while (mbedtls_mpi_cmp_mpi(num, max) >= 0 || mbedtls_mpi_bitlen(num) == 0); \\\n-} while (0)\n+#define BIGNUM_RANDOM(num, max) ret = mbedtls_mpi_fill_random(num, 32, _librist_srp_mbedtls_wrap_random, NULL);\n #endif\n #define BIGNUM_MOD_RED(out, a, b) ret = mbedtls_mpi_mod_mpi(out, a, b)\n #define BIGNUM_EXP_MOD(out, base, exp, mod) ret = mbedtls_mpi_exp_mod(out, base, exp, mod, NULL)\n@@ -94,11 +78,6 @@ int _librist_srp_mbedtls_wrap_random(void *unused, unsigned char * buf, size_t s\n #define BIGNUM_SUB_BIG(prod, a, b) ret = mbedtls_mpi_sub_mpi(prod, a, b)\n #define BIGNUM_EQUALS(num, comp) (mbedtls_mpi_cmp_int(num, comp) == 0)\n #define BIGNUM_WRITE_BYTES(num, bytes, bytes_size) if (mbedtls_mpi_write_binary(num, bytes, bytes_size) != 0) {return -1; }\n-/* Variant that jumps to a cleanup label instead of returning, for use inside\n- * functions that own heap that needs freeing on failure. */\n-#define BIGNUM_WRITE_BYTES_OR_GOTO(num, bytes, bytes_size, lbl) do { \\\n-\tif (mbedtls_mpi_write_binary(num, bytes, bytes_size) != 0) goto lbl; \\\n-} while (0)\n #define BIGNUM_WRITE_BYTES_ALLOC(num, bytes_pp, len_p, lbl) do {\\\n \t*len_p = mbedtls_mpi_size(num); \\\n \t*bytes_pp = malloc(*len_p); \\\n@@ -111,7 +90,11 @@ int _librist_srp_mbedtls_wrap_random(void *unused, unsigned char * buf, size_t s\n void librist_crypto_srp_mbedtls_hash_init(HASH_CONTEXT *ctx, bool correct_init) {\n     mbedtls_sha256_init(ctx);\n \tif (correct_init) {\n+#if USE_SHA_RET\n \t\tmbedtls_sha256_starts_ret(ctx, 0);\n+#else\n+\t\tmbedtls_sha256_starts(ctx, 0);\n+#endif\n \t}\n }\n \n@@ -121,18 +104,13 @@ void librist_crypto_srp_mbedtls_hash_init(HASH_CONTEXT *ctx, bool correct_init)\n #elif HAVE_NETTLE\n #include <nettle/sha2.h>\n #include <nettle/bignum.h>\n-#include <nettle/version.h>\n #define BIGNUM MP_INT\n #define BIGNUM_INIT(num) mpz_init(num)\n #define BIGNUM_FREE(num) mpz_clear(num)\n #define BIGNUM_GET_BINARY_SIZE(num) ((mpz_sizeinbase(num, 2) +7) /8)\n #define BIGNUM_FROM_ARRAY(num, array, size) mpz_import(num, size, 1, 1, 0, 0, array)\n #define BIGNUM_FROM_STRING(num, str) ret = mpz_set_str(num, str, 16)\n-/* Pass &ret as the void* context so the wrapper can signal CSPRNG failure. */\n-#define BIGNUM_RANDOM(num, max) do { \\\n-    ret = 0; \\\n-    nettle_mpz_random(num, &ret, _librist_srp_nettle_wrap_random, max); \\\n-} while (0)\n+#define BIGNUM_RANDOM(num, max) nettle_mpz_random(num, NULL, _librist_srp_nettle_wrap_random, max);\n #define BIGNUM_MOD_RED(out, a, b) mpz_mod(out, a, b)\n #define BIGNUM_EXP_MOD(out, base, exp, mod) mpz_powm(out, base, exp, mod)\n #define BIGNUM_MULT_BIG(prod, a, b) mpz_mul(prod, a, b)\n@@ -140,11 +118,6 @@ void librist_crypto_srp_mbedtls_hash_init(HASH_CONTEXT *ctx, bool correct_init)\n #define BIGNUM_SUB_BIG(prod, a, b) mpz_sub(prod, a, b)\n #define BIGNUM_EQUALS(num, comp) (mpz_cmp_ui(num, comp) == 0)\n #define BIGNUM_WRITE_BYTES(num, bytes, bytes_size) mpz_export(bytes, NULL, 1, 1, 0, 0, num)\n-/* Nettle's mpz_export into a caller-provided buffer cannot fail; mirror the\n- * mbedTLS-side macro so the source compiles unchanged. The lbl argument is a\n- * goto target (not an expression), so we cannot reference it here. */\n-#define BIGNUM_WRITE_BYTES_OR_GOTO(num, bytes, bytes_size, lbl) \\\n-\tmpz_export(bytes, NULL, 1, 1, 0, 0, num)\n #define BIGNUM_WRITE_BYTES_ALLOC(num, bytes_pp, len_p, lbl) do {\\\n \t*bytes_pp = mpz_export(NULL, len_p, 1, 1, 0, 0, num); \\\n \tif (!*bytes_pp) { goto lbl; } \\\n@@ -158,17 +131,9 @@ void librist_crypto_srp_mbedtls_hash_init(HASH_CONTEXT *ctx, bool correct_init)\n #define HASH_CONTEXT_INIT(ctx, correct) (void)(correct); nettle_sha256_init(ctx)\n #define HASH_CONTEXT_FREE(ctx)\n \n-void _librist_srp_nettle_wrap_random(void *err_out, size_t size, uint8_t* buf) {\n-\t/* Nettle's callback returns void; we route CSPRNG failure through\n-\t * the caller's int* (passed via the otherwise-unused ctx arg) and\n-\t * zero the buffer so a missed return check feeds SRP a value that\n-\t * its mod-N checks will reject. */\n-\tint *err = (int *)err_out;\n-\tif (_librist_crypto_ramdom_get_bytes(buf, size) != 0) {\n-\t\tmemset(buf, 0, size);\n-\t\tif (err)\n-\t\t\t*err = -1;\n-\t}\n+void _librist_srp_nettle_wrap_random(void *unused, size_t size, uint8_t* buf) {\n+\tRIST_MARK_UNUSED(unused);\n+\t_librist_crypto_ramdom_get_bytes(buf, size);\n }\n \n #endif\n@@ -176,7 +141,11 @@ void _librist_srp_nettle_wrap_random(void *err_out, size_t size, uint8_t* buf) {\n static int librist_crypto_srp_hash_update(HASH_CONTEXT *hash_ctx, const void *data, size_t len)\n {\n #if HAVE_MBEDTLS\n+#if !USE_SHA_RET\n+\tmbedtls_sha256_update( hash_ctx, data, len );\n+#else\n \treturn mbedtls_sha256_update_ret( hash_ctx, data, len );\n+#endif\n #else\n \tnettle_sha256_update( hash_ctx, len, data);\n \treturn 0;\n@@ -197,29 +166,32 @@ static int librist_crypto_srp_hash_update_bignum(HASH_CONTEXT *hash_ctx, const B\n static int librist_crypto_srp_hash_final(HASH_CONTEXT *hash_ctx, uint8_t *data)\n {\n #if HAVE_MBEDTLS\n-\treturn mbedtls_sha256_finish_ret( hash_ctx, data);\n+#if !USE_SHA_RET\n+\tmbedtls_sha256_finish( hash_ctx, data);\n #else\n-\t/* nettle 4.0 dropped the length argument; this digest is never truncated. */\n-#if NETTLE_VERSION_MAJOR >= 4\n-\tnettle_sha256_digest( hash_ctx, data);\n+\treturn mbedtls_sha256_finish_ret( hash_ctx, data);\n+#endif\n #else\n \tnettle_sha256_digest( hash_ctx, SHA256_DIGEST_LENGTH, data);\n #endif\n \treturn 0;\n-#endif\n }\n \n static int librist_crypto_srp_hash(const uint8_t *indata, size_t inlen, uint8_t outdata[SHA256_DIGEST_LENGTH])\n {\n #if HAVE_MBEDTLS\n+#if !USE_SHA_RET\n+\tmbedtls_sha256(hash_data, salt_len + SHA256_DIGEST_LENGTH, x_hash, 0)\n+#else\n \treturn mbedtls_sha256_ret(indata, inlen, outdata, 0);\n+#endif\n #else\n \tHASH_CONTEXT hash_ctx;\n \tHASH_CONTEXT_INIT(&hash_ctx, true);\n \tlibrist_crypto_srp_hash_update(&hash_ctx, indata, inlen);\n \tlibrist_crypto_srp_hash_final(&hash_ctx, outdata);\n-\treturn 0;\n #endif\n+\treturn 0;\n }\n \n //Calculates the value of x as follows: x = SHA256(s, SHA256(I | “:” | P))\n@@ -244,7 +216,9 @@ static int librist_crypto_srp_calc_x(BIGNUM *salt, const char * username, const\n \tif (librist_crypto_srp_hash_final(&hash_ctx, &hash_data[salt_len]) != 0)\n \t\tgoto failed;\n \n-\tBIGNUM_WRITE_BYTES_OR_GOTO(salt, hash_data, salt_len, failed);\n+\tBIGNUM_WRITE_BYTES(salt, hash_data, salt_len);\n+\tif (ret != 0)\n+\t\tgoto failed;\n \n \tHASH_CONTEXT_FREE(&hash_ctx);\n \n@@ -265,30 +239,7 @@ failed:\n \treturn -1;\n }\n \n-/* RFC 5054 PAD variant: zero-extends both inputs to pad_len bytes before\n- * hashing.  Required for u = SHA(PAD(A) | PAD(B)) and k = SHA(N | PAD(g))\n- * where PAD(x) is x left-zero-padded to the byte length of N. */\n-static int librist_crypto_srp_hash_2_bignum_padded(size_t pad_len, BIGNUM *A, BIGNUM *B, uint8_t hash_out[SHA256_DIGEST_LENGTH])\n-{\n-\tuint8_t AB[2048];\n-\n-\tif (pad_len * 2 > sizeof(AB))\n-\t\treturn -1;\n-\n-\tsize_t A_size = BIGNUM_GET_BINARY_SIZE(A);\n-\tsize_t B_size = BIGNUM_GET_BINARY_SIZE(B);\n-\tif (A_size > pad_len || B_size > pad_len)\n-\t\treturn -1;\n-\n-\tmemset(AB, 0, pad_len * 2);\n-\tBIGNUM_WRITE_BYTES(A, AB + (pad_len - A_size), A_size);\n-\tBIGNUM_WRITE_BYTES(B, AB + pad_len + (pad_len - B_size), B_size);\n-\n-\treturn librist_crypto_srp_hash(AB, pad_len * 2, hash_out);\n-}\n-\n-/* Pre-0.2.16 unpadded variant; only reachable when ctx->legacy_pad is set. */\n-static int librist_crypto_srp_hash_2_bignum_unpadded(BIGNUM *A, BIGNUM *B, uint8_t hash_out[SHA256_DIGEST_LENGTH])\n+static int librist_crypto_srp_hash_2_bignum(BIGNUM *A, BIGNUM *B, uint8_t hash_out[SHA256_DIGEST_LENGTH])\n {\n \tsize_t A_size = BIGNUM_GET_BINARY_SIZE(A);\n \tsize_t B_size = BIGNUM_GET_BINARY_SIZE(B);\n@@ -296,18 +247,10 @@ static int librist_crypto_srp_hash_2_bignum_unpadded(BIGNUM *A, BIGNUM *B, uint8\n \n \tif (A_size + B_size > sizeof(AB))\n \t\treturn -1;\n+\tBIGNUM_WRITE_BYTES(A, AB, A_size);\n+\tBIGNUM_WRITE_BYTES(B, &AB[A_size], B_size);\n \n-\tBIGNUM_WRITE_BYTES(A, AB,          A_size);\n-\tBIGNUM_WRITE_BYTES(B, AB + A_size, B_size);\n-\n-\treturn librist_crypto_srp_hash(AB, A_size + B_size, hash_out);\n-}\n-\n-static int srp_hash_uk(bool legacy_pad, size_t pad_len, BIGNUM *X, BIGNUM *Y, uint8_t hash_out[SHA256_DIGEST_LENGTH])\n-{\n-\tif (legacy_pad)\n-\t\treturn librist_crypto_srp_hash_2_bignum_unpadded(X, Y, hash_out);\n-\treturn librist_crypto_srp_hash_2_bignum_padded(pad_len, X, Y, hash_out);\n+\treturn librist_crypto_srp_hash(AB, A_size+B_size, hash_out);\n }\n \n static int librist_crypto_srp_hash_bignum(BIGNUM *in, uint8_t hash_out[SHA256_DIGEST_LENGTH])\n@@ -333,18 +276,12 @@ static int librist_crypto_srp_calculate_m(BIGNUM *N, BIGNUM *g, const char *I, B\n \tprint_hash(K, \"K: \");\n #endif\n \tuint8_t hash_tmp[SHA256_DIGEST_LENGTH];\n-\tint ret = -1;\n-\tHASH_CONTEXT hash_ctx;\n \t{\n \t\tuint8_t hash_N[SHA256_DIGEST_LENGTH];\n \t\tuint8_t hash_g[SHA256_DIGEST_LENGTH];\n \n-\t\t/* hash_bignum returns -1 (without writing the output) when the\n-\t\t * bignum exceeds its 1024-byte staging buffer. Bail out instead\n-\t\t * of XOR-ing uninitialised stack into the SRP transcript. */\n-\t\tif (librist_crypto_srp_hash_bignum(N, hash_N) != 0 ||\n-\t\t    librist_crypto_srp_hash_bignum(g, hash_g) != 0)\n-\t\t\treturn -1;\n+\t\tlibrist_crypto_srp_hash_bignum(N, hash_N);\n+\t\tlibrist_crypto_srp_hash_bignum(g, hash_g);\n \n \t\tfor (size_t i=0; i < sizeof(hash_tmp); i++)\n \t\t\thash_tmp[i] = hash_N[i] ^ hash_g[i];\n@@ -354,6 +291,9 @@ static int librist_crypto_srp_calculate_m(BIGNUM *N, BIGNUM *g, const char *I, B\n \tprint_hash(hash_tmp, \"XOR: \");\n #endif\n \n+\n+\tint ret = -1;\n+\tHASH_CONTEXT hash_ctx;\n \tHASH_CONTEXT_INIT(&hash_ctx, correct);\n \n \tif (librist_crypto_srp_hash_update(&hash_ctx, hash_tmp, sizeof(hash_tmp)) != 0)\n@@ -438,10 +378,9 @@ struct librist_crypto_srp_authenticator_ctx {\n \tuint8_t m2[SHA256_DIGEST_LENGTH];\n \n \tbool correct_hashing_init;\n-\tbool legacy_pad;             //pre-0.2.16 unpadded u/k (srp-compat=1)\n };\n \n-struct librist_crypto_srp_authenticator_ctx *librist_crypto_srp_authenticator_ctx_create(const char* n_hex, const char *g_hex, const uint8_t *v_bytes, size_t v_len, const uint8_t *s_bytes, size_t s_len, bool correct, bool legacy_pad) {\n+struct librist_crypto_srp_authenticator_ctx *librist_crypto_srp_authenticator_ctx_create(const char* n_hex, const char *g_hex, const uint8_t *v_bytes, size_t v_len, const uint8_t *s_bytes, size_t s_len, bool correct) {\n \tif (!v_bytes || !s_bytes || v_len == 0 || s_len == 0)\n \t\treturn NULL;\n \tstruct librist_crypto_srp_authenticator_ctx *ctx = calloc(1, sizeof(*ctx));\n@@ -449,7 +388,6 @@ struct librist_crypto_srp_authenticator_ctx *librist_crypto_srp_authenticator_ct\n \t\treturn NULL;\n \n \tctx->correct_hashing_init = correct;\n-\tctx->legacy_pad = legacy_pad;\n \tBIGNUM_INIT(&ctx->N);\n \tBIGNUM_INIT(&ctx->g);\n \tBIGNUM_INIT(&ctx->v);\n@@ -468,10 +406,6 @@ struct librist_crypto_srp_authenticator_ctx *librist_crypto_srp_authenticator_ct\n \tif (ret != 0)\n \t\tgoto fail;\n \n-\t/* RFC 5054: reject v == 0 (session key would not depend on verifier) */\n-\tif (BIGNUM_EQUALS(&ctx->v, 0))\n-\t\tgoto fail;\n-\n \tBIGNUM_FROM_ARRAY(&ctx->s, s_bytes, s_len);\n \tif (ret != 0)\n \t\tgoto fail;\n@@ -479,8 +413,6 @@ struct librist_crypto_srp_authenticator_ctx *librist_crypto_srp_authenticator_ct\n \n \tBIGNUM_INIT(&ctx->A);\n \tBIGNUM_INIT(&ctx->b);\n-\tBIGNUM_INIT(&ctx->k);\n-\tBIGNUM_INIT(&ctx->B);\n \treturn ctx;\n \n fail:\n@@ -548,11 +480,6 @@ const uint8_t *librist_crypto_srp_authenticator_get_key(struct librist_crypto_sr\n //B=(kv + g^b) % N\n int librist_crypto_srp_authenticator_handle_A(struct librist_crypto_srp_authenticator_ctx *ctx, uint8_t *A_buf, size_t A_buf_len) {\n \tint ret = 0;\n-\t/* Bound the attacker-supplied operand against N. Without this,\n-\t * mbedtls_mpi_exp_mod() runs in time proportional to A_buf_len,\n-\t * giving a peer-pre-auth CPU DoS handle. */\n-\tif (A_buf_len == 0 || A_buf_len > BIGNUM_GET_BINARY_SIZE(&ctx->N))\n-\t\treturn -1;\n \tBIGNUM_FROM_ARRAY(&ctx->A, A_buf, A_buf_len);\n \tif (ret != 0)\n \t\treturn -1;\n@@ -563,11 +490,8 @@ int librist_crypto_srp_authenticator_handle_A(struct librist_crypto_srp_authenti\n \tif (ret != 0)\n \t\tgoto out;\n \n-\t// RFC 5054 / SRP-6a: server aborts if A mod N == 0\n-\tif (BIGNUM_EQUALS(&tmp, 0)) {\n-\t\tret = -1;\n+\tif (BIGNUM_EQUALS(&tmp, 0))\n \t\tgoto out;\n-\t}\n \n \n \n@@ -582,13 +506,9 @@ int librist_crypto_srp_authenticator_handle_A(struct librist_crypto_srp_authenti\n #endif\n \n \n-\t//calc k = SHA256(PAD(N) | PAD(g))  (RFC 5054); legacy_pad strips the PAD.\n+\t//calc k\n \tuint8_t k_hash[SHA256_DIGEST_LENGTH];\n-\tsize_t N_size = BIGNUM_GET_BINARY_SIZE(&ctx->N);\n-\tif (srp_hash_uk(ctx->legacy_pad, N_size, &ctx->N, &ctx->g, k_hash) != 0) {\n-\t\tret = -1;\n-\t\tgoto out;\n-\t}\n+\tlibrist_crypto_srp_hash_2_bignum(&ctx->N, &ctx->g, k_hash);\n \tBIGNUM_FROM_ARRAY(&ctx->k, k_hash, sizeof(k_hash));\n \tif (ret != 0)\n \t\tgoto out;\n@@ -613,12 +533,6 @@ int librist_crypto_srp_authenticator_handle_A(struct librist_crypto_srp_authenti\n \tif (ret != 0)\n \t\tgoto b_out;\n \n-\t/* RFC 5054: authenticator aborts if B mod N == 0 */\n-\tif (BIGNUM_EQUALS(&ctx->B, 0)) {\n-\t\tret = -1;\n-\t\tgoto b_out;\n-\t}\n-\n #if DEBUG_EXTRACT_SRP_EXCHANGE\n \tfprintf(stderr, \"%s\\n\", __func__);\n \tBIGNUM_PRINT(\"b: \", &ctx->b);\n@@ -645,8 +559,7 @@ out:\n //M2 = SHA256(A, M1, K)\n int librist_crypto_srp_authenticator_verify_m1(struct librist_crypto_srp_authenticator_ctx *ctx, const char *username,  uint8_t *client_m1_buf) {\n \tuint8_t u_hash[SHA256_DIGEST_LENGTH];\n-\tsize_t pad = BIGNUM_GET_BINARY_SIZE(&ctx->N);\n-\tif (srp_hash_uk(ctx->legacy_pad, pad, &ctx->A, &ctx->B, u_hash) != 0) {\n+\tif (librist_crypto_srp_hash_2_bignum(&ctx->A, &ctx->B, u_hash) != 0) {\n \t\treturn -1;\n \t}\n \n@@ -662,19 +575,6 @@ int librist_crypto_srp_authenticator_verify_m1(struct librist_crypto_srp_authent\n \tif (ret != 0)\n \t\tgoto out;\n \n-\t/* RFC 5054: authenticator aborts if u == 0 */\n-\t{\n-\t\tBIGNUM u_mod;\n-\t\tBIGNUM_INIT(&u_mod);\n-\t\tBIGNUM_MOD_RED(&u_mod, &u, &ctx->N);\n-\t\tbool u_zero = BIGNUM_EQUALS(&u_mod, 0);\n-\t\tBIGNUM_FREE(&u_mod);\n-\t\tif (ret != 0 || u_zero) {\n-\t\t\tret = -1;\n-\t\t\tgoto out;\n-\t\t}\n-\t}\n-\n \tBIGNUM_EXP_MOD(&tmp1, &ctx->v, &u, &ctx->N);\n \tif (ret != 0)\n \t\tgoto out;\n@@ -685,13 +585,8 @@ int librist_crypto_srp_authenticator_verify_m1(struct librist_crypto_srp_authent\n \n \t//tmp1 -> S\n \tBIGNUM_EXP_MOD(&tmp1, &tmp2, &ctx->b, &ctx->N);\n-\tif (ret != 0)\n-\t\tgoto out;\n \n-\tif (librist_crypto_srp_hash_bignum(&tmp1, ctx->key) != 0) {\n-\t\tret = -1;\n-\t\tgoto out;\n-\t}\n+    librist_crypto_srp_hash_bignum(&tmp1, ctx->key);\n \n #if DEBUG_EXTRACT_SRP_EXCHANGE\n     fprintf(stderr, \"%s\\n\", __func__);\n@@ -709,14 +604,14 @@ int librist_crypto_srp_authenticator_verify_m1(struct librist_crypto_srp_authent\n \tprint_hash(m1_buf, \"M1: \");\n #endif\n \n-\tret = constant_time_memcmp(m1_buf, client_m1_buf, sizeof(m1_buf));\n+\tret = memcmp(m1_buf, client_m1_buf, sizeof(m1_buf));\n \tif (ret != 0)\n \t\tgoto out;\n \n \tret = librist_crypto_srp_calculate_m2(&ctx->A, m1_buf, ctx->key, ctx->m2, ctx->correct_hashing_init);\n \n #if DEBUG_EXTRACT_SRP_EXCHANGE\n-\tprint_hash(ctx->m2, \"M2: \");\n+\tprint_hash(ctx->key, \"M2: \");\n #endif\n out:\n \tBIGNUM_FREE(&u);\n@@ -745,7 +640,6 @@ struct librist_crypto_srp_client_ctx {\n \tuint8_t m1[SHA256_DIGEST_LENGTH];\n \n \tbool correct_hashing_init;\n-\tbool legacy_pad;             //pre-0.2.16 unpadded u/k (srp-compat=1)\n };\n \n int librist_crypto_srp_client_write_A_bytes(struct librist_crypto_srp_client_ctx *ctx, uint8_t *A_buf, size_t A_buf_len) {\n@@ -772,10 +666,6 @@ void librist_crypto_srp_client_write_M1_bytes(struct librist_crypto_srp_client_c\n int librist_crypto_srp_client_handle_B(struct librist_crypto_srp_client_ctx *ctx, uint8_t *B_bytes, size_t B_len, const char *username, const char *password) {\n \tint ret = 0;\n \n-\t/* Same operand-size bound as the authenticator side; a malicious\n-\t * server otherwise drives mbedtls_mpi_exp_mod runtime via B. */\n-\tif (B_len == 0 || B_len > BIGNUM_GET_BINARY_SIZE(&ctx->N))\n-\t\treturn -1;\n \tBIGNUM_FROM_ARRAY(&ctx->B, B_bytes, B_len);\n \tif (ret != 0)\n \t\treturn -1;\n@@ -794,42 +684,36 @@ int librist_crypto_srp_client_handle_B(struct librist_crypto_srp_client_ctx *ctx\n \tBIGNUM_INIT(&tmp3);\n \tBIGNUM_INIT(&tmp4);\n \n-\t// RFC 5054 / SRP-6a: client aborts if B mod N == 0\n+\t//Safety check: exit early if B mod N equals 0\n \tBIGNUM_MOD_RED(&tmp1, &ctx->B, &ctx->N);\n \tif (ret != 0)\n \t\tgoto out;\n \n-\tif (BIGNUM_EQUALS(&tmp1, 0)) {\n-\t\tret = -1;\n+\tif (BIGNUM_EQUALS(&tmp1, 0))\n \t\tgoto out;\n-\t}\n \n-\t//Calculate u = SHA256(PAD(A) | PAD(B))  (RFC 5054); legacy_pad strips PAD.\n+\t//Calculate u & execute safety check\n \t{\n \t\tuint8_t u_hash[SHA256_DIGEST_LENGTH];\n-\t\tsize_t u_pad = BIGNUM_GET_BINARY_SIZE(&ctx->N);\n-\t\tret = srp_hash_uk(ctx->legacy_pad, u_pad, &ctx->A, &ctx->B, u_hash);\n+\t\tret =librist_crypto_srp_hash_2_bignum(&ctx->A, &ctx->B, u_hash);\n \t\tif (ret != 0)\n \t\t\tgoto out;\n \n \n \t\tBIGNUM_FROM_ARRAY(&u, u_hash, sizeof(u_hash));\n-\t\t// And aborts if u mod N == 0\n+\t\t//Safety check: exit early if u mod N equals 0\n \t\tBIGNUM_MOD_RED(&tmp1, &u, &ctx->N);\n \t\tif (ret != 0)\n \t\t\tgoto out;\n \n-\t\tif (BIGNUM_EQUALS(&tmp1, 0)) {\n-\t\t\tret = -1;\n+\t\tif (BIGNUM_EQUALS(&tmp1, 0))\n \t\t\tgoto out;\n-\t\t}\n \t}\n \n-\t//Calculate k = SHA256(PAD(N) | PAD(g))  (RFC 5054); legacy_pad strips PAD.\n+\t//Calculate k\n \t{\n \t\tuint8_t k_hash[SHA256_DIGEST_LENGTH];\n-\t\tsize_t k_pad = BIGNUM_GET_BINARY_SIZE(&ctx->N);\n-\t\tret = srp_hash_uk(ctx->legacy_pad, k_pad, &ctx->N, &ctx->g, k_hash);\n+\t\tret = librist_crypto_srp_hash_2_bignum(&ctx->N, &ctx->g, k_hash);\n \t\tif (ret != 0)\n \t\t\tgoto out;\n \n@@ -904,27 +788,22 @@ int librist_crypto_srp_client_verify_m2(struct librist_crypto_srp_client_ctx *ct\n \tif (ret != 0)\n \t\treturn ret;\n \n-\treturn constant_time_memcmp(m2, calc_m2, sizeof(calc_m2));\n+\treturn memcmp(m2, calc_m2, sizeof(calc_m2));\n }\n \n const uint8_t *librist_crypto_srp_client_get_key(struct librist_crypto_srp_client_ctx *ctx) {\n \treturn ctx->key;\n }\n \n-struct librist_crypto_srp_client_ctx *librist_crypto_srp_client_ctx_create(bool default_ng, uint8_t *N_bytes, size_t N_len, uint8_t *g_bytes, size_t g_len, uint8_t *s_bytes, size_t s_len, bool correct, bool legacy_pad) {\n-\tif (!s_bytes || s_len == 0 || s_len > 64)\n+struct librist_crypto_srp_client_ctx *librist_crypto_srp_client_ctx_create(bool default_ng, uint8_t *N_bytes, size_t N_len, uint8_t *g_bytes, size_t g_len, uint8_t *s_bytes, size_t s_len, bool correct) {\n+\tif (!s_bytes || s_len == 0 || (!default_ng && (!N_bytes || N_len == 0 || !g_bytes || g_len == 0)))\n \t\treturn NULL;\n-\tif (!default_ng) {\n-\t\tif (!N_bytes || N_len < 128 || N_len > 1024 || !g_bytes || g_len == 0 || g_len > N_len)\n-\t\t\treturn NULL;\n-\t}\n \n \tstruct librist_crypto_srp_client_ctx *ctx = calloc(1, sizeof(*ctx));\n \tif (!ctx)\n \t\treturn NULL;\n \n \tctx->correct_hashing_init = correct;\n-\tctx->legacy_pad = legacy_pad;\n \tBIGNUM_INIT(&ctx->N);\n \tBIGNUM_INIT(&ctx->g);\n \tBIGNUM_INIT(&ctx->s);\n@@ -1036,24 +915,16 @@ int librist_crypto_srp_create_verifier(\n \tif (ret != 0)\n \t\tgoto failed;\n \n-\t/* Generate salt as raw bytes so leading zeros are preserved.\n-\t * Bignum export strips them, shrinking the effective salt domain. */\n-\tuint8_t salt_raw[32];\n+\t//Fill the salt\n #if DEBUG_USE_EXAMPLE_CONSTANTS\n \tconst char salt_hex[] = \"72F9D5383B7EB7599FB63028F47475B60A55F313D40E0BE023E026C97C0A2C32\";\n \tBIGNUM_FROM_STRING(&s, salt_hex);\n-\tif (ret != 0)\n-\t\tgoto failed;\n-\tmemset(salt_raw, 0, sizeof(salt_raw));\n-\t{\n-\t\tsize_t ss = BIGNUM_GET_BINARY_SIZE(&s);\n-\t\tif (ss <= sizeof(salt_raw))\n-\t\t\tBIGNUM_WRITE_BYTES(&s, salt_raw + (sizeof(salt_raw) - ss), ss);\n-\t}\n #else\n-\tif (_librist_crypto_ramdom_get_bytes(salt_raw, sizeof(salt_raw)) != 0)\n-\t\tgoto failed;\n-\tBIGNUM_FROM_ARRAY(&s, salt_raw, sizeof(salt_raw));\n+#if HAVE_MBEDTLS\n+\tmbedtls_mpi_fill_random(&s, 32, _librist_srp_mbedtls_wrap_random, NULL);\n+#elif HAVE_NETTLE\n+\tnettle_mpz_random_size(&s, NULL, _librist_srp_nettle_wrap_random, 8 * 32);\n+#endif\n \tif (ret != 0)\n \t\tgoto failed;\n #endif\n@@ -1066,11 +937,7 @@ int librist_crypto_srp_create_verifier(\n \tif (ret != 0)\n \t\tgoto failed;\n \n-\t*len_s = sizeof(salt_raw);\n-\t*bytes_s = malloc(sizeof(salt_raw));\n-\tif (!*bytes_s) goto failed;\n-\tmemcpy(*bytes_s, salt_raw, sizeof(salt_raw));\n-\n+\tBIGNUM_WRITE_BYTES_ALLOC(&s, bytes_s, len_s, failed);\n \tBIGNUM_WRITE_BYTES_ALLOC(&v, bytes_v, len_v, failed);\n \n #if DEBUG_EXTRACT_SRP_EXCHANGE\n@@ -1098,10 +965,6 @@ failed:\n \n \tfree(*bytes_s);\n \tfree(*bytes_v);\n-\t*bytes_s = NULL;\n-\t*bytes_v = NULL;\n-\t*len_s = 0;\n-\t*len_v = 0;\n \n \treturn -1;\n }\ndiff --git a/src/crypto/srp.h b/src/crypto/srp.h\nindex 8555b29..042c480 100644\n--- a/src/crypto/srp.h\n+++ b/src/crypto/srp.h\n@@ -20,7 +20,7 @@ RIST_API int librist_crypto_srp_create_verifier(const char *n_hex, const char *g\n                                        unsigned char **bytes_s, size_t *len_s,\n                                        unsigned char **bytes_v, size_t *len_v, bool correct);\n \n-RIST_PRIV struct librist_crypto_srp_authenticator_ctx *librist_crypto_srp_authenticator_ctx_create(const char* n_hex, const char *g_hex, const uint8_t *v_bytes, size_t v_len, const uint8_t *s_bytes, size_t s_len, bool correct, bool legacy_pad);\n+RIST_PRIV struct librist_crypto_srp_authenticator_ctx *librist_crypto_srp_authenticator_ctx_create(const char* n_hex, const char *g_hex, const uint8_t *v_bytes, size_t v_len, const uint8_t *s_bytes, size_t s_len, bool correct);\n RIST_PRIV void librist_crypto_srp_authenticator_ctx_free(struct librist_crypto_srp_authenticator_ctx *ctx);\n RIST_PRIV int librist_crypto_srp_authenticator_write_g_bytes(struct librist_crypto_srp_authenticator_ctx *ctx, uint8_t *g_buf, size_t g_buf_len);\n RIST_PRIV int librist_crypto_srp_authenticator_write_n_bytes(struct librist_crypto_srp_authenticator_ctx *ctx, uint8_t *n_buf, size_t n_buf_len);\n@@ -30,7 +30,7 @@ RIST_PRIV int librist_crypto_srp_authenticator_verify_m1(struct librist_crypto_s\n RIST_PRIV void librist_crypto_srp_authenticator_write_M2_bytes(struct librist_crypto_srp_authenticator_ctx *ctx, uint8_t* m2_buf);\n RIST_PRIV const uint8_t *librist_crypto_srp_authenticator_get_key(struct librist_crypto_srp_authenticator_ctx *ctx);\n \n-RIST_PRIV struct librist_crypto_srp_client_ctx *librist_crypto_srp_client_ctx_create(bool default_ng, uint8_t *N_bytes, size_t N_len, uint8_t *g_bytes, size_t g_len, uint8_t *s_bytes, size_t s_len, bool correct, bool legacy_pad);\n+RIST_PRIV struct librist_crypto_srp_client_ctx *librist_crypto_srp_client_ctx_create(bool default_ng, uint8_t *N_bytes, size_t N_len, uint8_t *g_bytes, size_t g_len, uint8_t *s_bytes, size_t s_len, bool correct);\n RIST_PRIV void librist_crypto_srp_client_ctx_free(struct librist_crypto_srp_client_ctx *ctx);\n RIST_PRIV int librist_crypto_srp_client_write_A_bytes(struct librist_crypto_srp_client_ctx *ctx, uint8_t *A_buf, size_t A_buf_len);\n RIST_PRIV int librist_crypto_srp_client_handle_B(struct librist_crypto_srp_client_ctx *ctx, uint8_t *B_bytes, size_t B_len, const char *username, const char *password);\ndiff --git a/src/crypto/srp_constants.c b/src/crypto/srp_constants.c\nindex 31dcd5a..0331f5f 100644\n--- a/src/crypto/srp_constants.c\n+++ b/src/crypto/srp_constants.c\n@@ -7,10 +7,19 @@ struct NGHex {\n   const char *g_hex;\n };\n \n-/* All constants here were pulled from Appendix A of RFC 5054.\n- * The 512- and 768-bit groups from RFC 5054 are intentionally not\n- * included; they are below current minimum-strength thresholds. */\n+/* All constants here were pulled from Appendix A of RFC 5054 */\n static const struct NGHex global_Ng_constants[] = {\n+\t{/* 512 */\n+\t\t\"D66AAFE8E245F9AC245A199F62CE61AB8FA90A4D80C71CD2ADFD0B9DA163B29F2A34AFBDB3B\"\n+\t\t\"1B5D0102559CE63D8B6E86B0AA59C14E79D4AA62D1748E4249DF3\",\n+\t\t\"2\"\n+\t},\n+\t{/* 768 */\n+\t\t\"B344C7C4F8C495031BB4E04FF8F84EE95008163940B9558276744D91F7CC9F402653BE7147F\"\n+\t\t\"00F576B93754BCDDF71B636F2099E6FFF90E79575F3D0DE694AFF737D9BE9713CEF8D837ADA\"\n+\t\t\"6380B1093E94B6A529A8C6C2BE33E0867C60C3262B\",\n+\t\t\"2\"\n+\t},\n \t{/* 1024 */\n \t\t\"EEAF0AB9ADB38DD69C33F80AFA8FC5E86072618775FF3C0B9EA2314C9C256576D674DF7496\"\n \t\t\"EA81D3383B4813D692C6E0E0D5D8E250B98BE48E495C1D6089DAD15DC7D7B46154D6B6CE8E\"\n@@ -98,15 +107,12 @@ int librist_get_ng_constants(librist_srp_ng_e ng_pair, const char **n, const cha\n \tif (*n != NULL || *g != NULL) {\n \t\treturn -1;\n \t}\n-\tif ((int)ng_pair < (int)LIBRIST_SRP_NG_1024)\n-\t\treturn -1;\n-\tint idx = (int)ng_pair - (int)LIBRIST_SRP_NG_1024;\n \tint i = 0;\n \twhile (true) {\n \t\tif (global_Ng_constants[i].n_hex == NULL) {\n \t\t\treturn -1;\n \t\t}\n-\t\tif (i == idx) {\n+\t\tif (i == (int)ng_pair) {\n \t\t\t*n = global_Ng_constants[i].n_hex;\n \t\t\t*g = global_Ng_constants[i].g_hex;\n \t\t\treturn 0;\ndiff --git a/src/crypto/srp_constants.h b/src/crypto/srp_constants.h\nindex 7ddff46..ec1e530 100644\n--- a/src/crypto/srp_constants.h\n+++ b/src/crypto/srp_constants.h\n@@ -5,14 +5,12 @@\n #include <librist/common.h>\n typedef enum\n {\n-    /* Slots 0 and 1 (formerly NG_512, NG_768) are reserved to keep the\n-     * post-removal integer values of the surviving groups stable. */\n-    LIBRIST_SRP_NG_RESERVED_0 = 0,\n-    LIBRIST_SRP_NG_RESERVED_1 = 1,\n-    LIBRIST_SRP_NG_1024       = 2,\n-    LIBRIST_SRP_NG_2048       = 3,\n-    LIBRIST_SRP_NG_4096       = 4,\n-    LIBRIST_SRP_NG_8192       = 5\n+\tLIBRIST_SRP_NG_512,\n+\tLIBRIST_SRP_NG_768,\n+    LIBRIST_SRP_NG_1024,\n+    LIBRIST_SRP_NG_2048,\n+    LIBRIST_SRP_NG_4096,\n+    LIBRIST_SRP_NG_8192\n } librist_srp_ng_e;\n \n #define LIBRIST_SRP_NG_DEFAULT LIBRIST_SRP_NG_2048\ndiff --git a/src/flow.c b/src/flow.c\nindex e4fb47a..00cc588 100755\n--- a/src/flow.c\n+++ b/src/flow.c\n@@ -15,8 +15,6 @@\n void rist_receiver_missing(struct rist_flow *f, struct rist_peer *peer,uint64_t nack_time, uint32_t seq, uint64_t rtt)\n {\n \tstruct rist_missing_buffer *m = calloc(1, sizeof(*m));\n-\tif (!m)\n-\t\treturn;\n \tuint64_t now = timestampNTP_u64();\n \tif (nack_time > now)\n \t\tnack_time = now;\n@@ -150,25 +148,22 @@ void rist_delete_flow(struct rist_receiver *ctx, struct rist_flow *f)\n \t\t}\n \t}\n \tfree(f->dataout_fifo_queue);\n-\t/* Unlink from the flow list under flows_lock so that concurrent\n-\t * readers (rist_receiver_data_read2) cannot observe a dangling\n-\t * pointer.  The free happens after the lock is released. */\n+\t// Delete flow\n \trist_log_priv(&ctx->common, RIST_LOG_INFO, \"Deleting flow\\n\");\n-\tpthread_mutex_lock(&ctx->common.flows_lock);\n \tstruct rist_flow **prev_flow = &ctx->common.FLOWS;\n \tstruct rist_flow *current_flow = *prev_flow;\n \twhile (current_flow)\n \t{\n \t\tif (current_flow == f) {\n \t\t\t*prev_flow = current_flow->next;\n+\t\t\tfree(current_flow);\n+\t\t\tcurrent_flow = NULL;\n \t\t\tbreak;\n \t\t}\n \t\tprev_flow = &current_flow->next;\n \t\tcurrent_flow = current_flow->next;\n \t}\n-\tpthread_mutex_unlock(&ctx->common.flows_lock);\n-\tfree(f->receiver_queue);\n-\tfree(f);\n+\n }\n \n static void rist_flow_append(struct rist_flow **FLOWS, struct rist_flow *f)\n@@ -188,15 +183,6 @@ static void rist_flow_append(struct rist_flow **FLOWS, struct rist_flow *f)\n \tlast->next = f;\n }\n \n-/* Soft cap on simultaneous receiver flows. A peer can advertise an\n- * arbitrary 32-bit flow_id and we always create_flow() on a previously\n- * unseen one, so without a cap an attacker that can land authenticated\n- * traffic (or any traffic on a non-encrypted receiver) can exhaust\n- * memory by walking flow_id space. 256 is well past any legitimate\n- * deployment we've seen. */\n-#define RIST_MAX_FLOWS 256\n-#define RIST_MAX_PEERS_PER_FLOW 256\n-\n static struct rist_flow *create_flow(struct rist_receiver *ctx, uint32_t flow_id)\n {\n \tstruct rist_flow *f = calloc(1, sizeof(*f));\n@@ -206,54 +192,13 @@ static struct rist_flow *create_flow(struct rist_receiver *ctx, uint32_t flow_id\n \t\treturn NULL;\n \t}\n \n-\t/* Recovery ring: Simple/Main are hard-capped at 16 bits; Advanced uses\n-\t * the configured capacity. Heap-allocated so the 16-bit profiles don't\n-\t * pay the Advanced footprint. Advanced flows default to 32-bit framing\n-\t * but recv_data refines short_seq from the wire so a Main-framed source\n-\t * still works; the ring size stays fixed per context regardless. */\n-\tif (ctx->common.profile < RIST_PROFILE_ADVANCED) {\n-\t\tf->short_seq = true;\n-\t\tf->receiver_queue_max = UINT16_SIZE;\n-\t} else {\n-\t\tf->short_seq = false;\n-\t\tf->receiver_queue_max = ctx->common.recovery_queue_max;\n-\t}\n-\tf->receiver_queue = calloc(f->receiver_queue_max, sizeof(*f->receiver_queue));\n-\tif (!f->receiver_queue) {\n-\t\trist_log_priv(&ctx->common, RIST_LOG_ERROR,\n-\t\t\t\"OOM allocating receiver recovery ring (%zu entries)\\n\", f->receiver_queue_max);\n-\t\tfree(f);\n-\t\treturn NULL;\n-\t}\n-\n \tf->flow_id = flow_id;\n \tf->receiver_id = ctx->id;\n \tf->stats_next_time = timestampNTP_u64();\n \tf->max_output_jitter = ctx->common.rist_max_jitter;\n-\n-\t/* Output pacing; max_output_jitter becomes the wake ceiling. The occupancy\n-\t * trim stays off (target 0): target_output_time already governs latency. */\n-\tf->cbr_output = ctx->common.cbr_output;\n-\tf->cbr_output_min_us = ctx->common.cbr_output_min_us;\n-\trist_pacer_rate_init(&f->cbr_rate, 0);\n-\trist_pacer_init(&f->cbr_pacer, 0.0,\n-\t                (uint64_t)f->cbr_output_min_us * 1000,\n-\t                (uint64_t)(f->max_output_jitter / RIST_CLOCK) * 1000000ULL);\n-\tf->cbr_max_hold_us = rist_cbr_hold_us((uint32_t)(f->max_output_jitter / RIST_CLOCK));\n-\tatomic_init(&f->cbr_arrived_bytes, 0);\n-\n \tf->dataout_fifo_queue = calloc(ctx->fifo_queue_size, sizeof(*f->dataout_fifo_queue));\n-\tif (!f->dataout_fifo_queue) {\n-\t\trist_log_priv(&ctx->common, RIST_LOG_ERROR,\n-\t\t\t\"OOM allocating dataout fifo queue (%zu entries)\\n\", ctx->fifo_queue_size);\n-\t\tfree(f->receiver_queue);\n-\t\tfree(f);\n-\t\treturn NULL;\n-\t}\n \tint ret = pthread_cond_init(&f->condition, NULL);\n \tif (ret) {\n-\t\tfree(f->dataout_fifo_queue);\n-\t\tfree(f->receiver_queue);\n \t\tfree(f);\n \t\trist_log_priv(&ctx->common, RIST_LOG_ERROR, \"Error %d calling pthread_cond_init\\n\", ret);\n \t\treturn NULL;\n@@ -262,8 +207,6 @@ static struct rist_flow *create_flow(struct rist_receiver *ctx, uint32_t flow_id\n \tret = pthread_mutex_init(&f->mutex, NULL);\n \tif (ret){\n \t\tpthread_cond_destroy(&f->condition);\n-\t\tfree(f->dataout_fifo_queue);\n-\t\tfree(f->receiver_queue);\n \t\tfree(f);\n \t\trist_log_priv(&ctx->common, RIST_LOG_ERROR, \"Error %d calling pthread_mutex_init\\n\", ret);\n \t\treturn NULL;\n@@ -278,25 +221,8 @@ static struct rist_flow *create_flow(struct rist_receiver *ctx, uint32_t flow_id\n \tf->session_timeout = RIST_DEFAULT_SESSION_TIMEOUT * RIST_CLOCK;\n \tf->flow_timeout = 250 * RIST_CLOCK;\n \n-\t/* Count + cap-check + append under one critical section so concurrent\n-\t * callers cannot each observe a sub-cap count and each insert. */\n+\t/* Append flow to list */\n \tpthread_mutex_lock(&ctx->common.flows_lock);\n-\tsize_t flow_count = 0;\n-\tfor (struct rist_flow *cur = ctx->common.FLOWS; cur != NULL; cur = cur->next)\n-\t\tflow_count++;\n-\tif (flow_count >= RIST_MAX_FLOWS) {\n-\t\tpthread_mutex_unlock(&ctx->common.flows_lock);\n-\t\trist_log_priv(&ctx->common, RIST_LOG_ERROR,\n-\t\t\t\"Refusing to create FLOW #%\"PRIu32\" inside create_flow: \"\n-\t\t\t\"cap of %d flows reached (race with concurrent caller)\\n\",\n-\t\t\tflow_id, RIST_MAX_FLOWS);\n-\t\tpthread_mutex_destroy(&f->mutex);\n-\t\tpthread_cond_destroy(&f->condition);\n-\t\tfree(f->dataout_fifo_queue);\n-\t\tfree(f->receiver_queue);\n-\t\tfree(f);\n-\t\treturn NULL;\n-\t}\n \trist_flow_append(&ctx->common.FLOWS, f);\n \tpthread_mutex_unlock(&ctx->common.flows_lock);\n \tf->logging_settings = ctx->common.logging_settings;\n@@ -322,24 +248,14 @@ int rist_receiver_associate_flow(struct rist_peer *p, uint32_t flow_id)\n \tint ret = 0;\n \n \t// Find the flow based on the flow_id\n-\tstruct rist_flow *f = NULL;\n+\tstruct rist_flow *f;\n \tif (ctx->common.profile > RIST_PROFILE_SIMPLE)\n \t{\n-\t\tpthread_mutex_lock(&ctx->common.flows_lock);\n-\t\tsize_t flow_count = 0;\n-\t\tfor (struct rist_flow *cur = ctx->common.FLOWS; cur != NULL; cur = cur->next) {\n-\t\t\tflow_count++;\n-\t\t\tif (cur->flow_id == flow_id) {\n-\t\t\t\tf = cur;\n+\t\tfor (f = ctx->common.FLOWS; f != NULL; f = f->next) {\n+\t\t\tif (f->flow_id == flow_id) {\n+\t\t\t\tbreak;\n \t\t\t}\n \t\t}\n-\t\tpthread_mutex_unlock(&ctx->common.flows_lock);\n-\t\tif (!f && flow_count >= RIST_MAX_FLOWS) {\n-\t\t\trist_log_priv(&ctx->common, RIST_LOG_ERROR,\n-\t\t\t\t\"Refusing to create FLOW #%\"PRIu32\": cap of %d flows reached\\n\",\n-\t\t\t\tflow_id, RIST_MAX_FLOWS);\n-\t\t\treturn -1;\n-\t\t}\n \t} else\n \t{\n \t\tif (!p->parent) {\n@@ -359,15 +275,20 @@ int rist_receiver_associate_flow(struct rist_peer *p, uint32_t flow_id)\n \t\tif (p->config.timing_mode == RIST_TIMING_MODE_RTC)\n \t\t\tf->rtc_timing_mode = true;\n \n-\t\t/* short_seq + receiver_queue_max are set (and the ring allocated)\n-\t\t * inside create_flow() now. */\n+\t\tif (ctx->common.profile < RIST_PROFILE_ADVANCED) {\n+\t\t\tf->short_seq = true;\n+\t\t\tf->receiver_queue_max = UINT16_SIZE;\n+\t\t}\n+\t\telse\n+\t\t\tf->receiver_queue_max = RIST_SERVER_QUEUE_BUFFERS;\n+\n \t\tf->recovery_buffer_ticks = p->recovery_buffer_ticks;\n \t\trist_log_priv(&ctx->common, RIST_LOG_INFO, \"FLOW #%\"PRIu32\" created (short=%d)\\n\", flow_id, f->short_seq);\n \t} else {\n \t\t/* double check that this peer is not a member of this flow already */\n \t\tif (flow_has_peer(f, flow_id, p->adv_peer_id)) {\n \t\t\trist_log_priv(&ctx->common, RIST_LOG_INFO, \"FLOW #%\"PRIu32\", Existing peer (id=%\"PRIu32\") re-joining existing flow ...\\n\",\n-\t\t\t\tflow_id, p->adv_peer_id);\n+\t\t\t\tflow_id, p);\n \t\t\tret = 2;\n \t\t} else {\n \t\t\trist_log_priv(&ctx->common, RIST_LOG_INFO, \"FLOW #%\"PRIu32\": New peer (id=%u) joining existing flow ...\\n\",\n@@ -402,36 +323,17 @@ int rist_receiver_associate_flow(struct rist_peer *p, uint32_t flow_id)\n \tp->flow = f;\n \tp->adv_flow_id = flow_id;\n \tif (ret == 1) {\n-\t\t/* f->mutex protects peer_lst against all concurrent walkers/writers. */\n-\t\tpthread_mutex_lock(&f->mutex);\n-\t\tif (f->peer_lst_len >= RIST_MAX_PEERS_PER_FLOW) {\n-\t\t\tpthread_mutex_unlock(&f->mutex);\n-\t\t\trist_log_priv(&ctx->common, RIST_LOG_ERROR,\n-\t\t\t\t\"Refusing to attach peer (id=%u) to flow #%\"PRIu32\": \"\n-\t\t\t\t\"cap of %d peers/flow reached\\n\",\n-\t\t\t\tp->adv_peer_id, flow_id, RIST_MAX_PEERS_PER_FLOW);\n-\t\t\tp->flow = NULL;\n-\t\t\treturn -1;\n-\t\t}\n-\t\tstruct rist_peer **new_lst = realloc(f->peer_lst, (f->peer_lst_len + 1) * sizeof(*f->peer_lst));\n-\t\tif (!new_lst) {\n-\t\t\tpthread_mutex_unlock(&f->mutex);\n-\t\t\trist_log_priv(&ctx->common, RIST_LOG_ERROR,\n-\t\t\t\t\"OOM growing flow peer list, dropping new peer\\n\");\n-\t\t\tp->flow = NULL;\n-\t\t\treturn -1;\n-\t\t}\n-\t\tf->peer_lst = new_lst;\n+\t\t// TODO: lock the list?\n+\t\tf->peer_lst = realloc(f->peer_lst, (f->peer_lst_len + 1) * sizeof(*f->peer_lst));\n \t\tf->peer_lst[f->peer_lst_len] = p;\n \t\tf->peer_lst_len++;\n-\t\tpthread_mutex_unlock(&f->mutex);\n \t}\n \n \trist_log_priv(&ctx->common, RIST_LOG_INFO,\n-\t\t\"Peer with id #%u associated with flow #%\" PRIu32 \"\\n\", p->adv_peer_id, flow_id);\n+\t\t\"Peer with id #%u associated with flow #%\" PRIu64 \"\\n\", p->adv_peer_id, flow_id);\n \n \trist_log_priv(&ctx->common, RIST_LOG_INFO,\n-\t\t\"Flow #%\" PRIu32 \" has now %zu peers.\\n\", flow_id, f->peer_lst_len);\n+\t\t\"Flow #%\" PRIu64 \" has now %d peers.\\n\", flow_id, f->peer_lst_len);\n \n \treturn ret;\n }\ndiff --git a/src/libevsocket.c b/src/libevsocket.c\nindex 55b66e6..8405032 100755\n--- a/src/libevsocket.c\n+++ b/src/libevsocket.c\n@@ -64,8 +64,6 @@ struct evsocket_ctx {\n \tstruct evsocket_event *_array;\n \tint giveup;\n \tstruct evsocket_ctx *next;\n-\tevsocket_poll_func poll_override;\n-\tvoid *poll_opaque;\n };\n #if !defined(_WIN32) || HAVE_PTHREADS\n static pthread_mutex_t ctx_list_mutex = PTHREAD_MUTEX_INITIALIZER;\n@@ -96,8 +94,7 @@ static void ctx_del(struct evsocket_ctx *delme)\n \t\t\tif (p) {\n \t\t\t\tp->next = c->next;\n \t\t\t} else {\n-\t\t\t\t// removing the head: promote c->next, do not clear CTX_LIST\n-\t\t\t\tCTX_LIST = c->next;\n+\t\t\t\tCTX_LIST = NULL;\n \t\t\t}\n \t\t\tgoto out;\n \t\t}\n@@ -308,10 +305,7 @@ int evsocket_loop_single(struct evsocket_ctx *ctx, int timeout, int max_events)\n \t\tgoto loop_error;\n \t}\n \n-\tif (ctx->poll_override)\n-\t\tpollret = ctx->poll_override(ctx->poll_opaque, ctx->pfd, ctx->n_events, timeout);\n-\telse\n-\t\tpollret = poll(ctx->pfd, ctx->n_events, timeout);\n+\tpollret = poll(ctx->pfd, ctx->n_events, timeout);\n \tif (pollret <= 0) {\n \t\tif (pollret < 0) {\n \t\t\trist_log_priv3( RIST_LOG_ERROR, \"libevsocket, evsocket_loop: poll returned %d, n_events = %d, error = %d\\n\",\n@@ -364,16 +358,6 @@ void evsocket_loop_stop(struct evsocket_ctx *ctx)\n \t\tctx->giveup = 1;\n }\n \n-void evsocket_set_poll_override(struct evsocket_ctx *ctx,\n-                                evsocket_poll_func func,\n-                                void *opaque)\n-{\n-\tif (ctx) {\n-\t\tctx->poll_override = func;\n-\t\tctx->poll_opaque = opaque;\n-\t}\n-}\n-\n int evsocket_geteventcount(struct evsocket_ctx *ctx)\n {\n \tif (ctx)\ndiff --git a/src/libevsocket.h b/src/libevsocket.h\nindex 4869ed6..dc12468 100755\n--- a/src/libevsocket.h\n+++ b/src/libevsocket.h\n@@ -10,21 +10,8 @@\n \n #include \"common/attributes.h\"\n \n-#ifdef _WIN32\n-#include <winsock2.h>\n-#else\n-#include <poll.h>\n-#endif\n-\n struct evsocket_event;\n struct evsocket_ctx;\n \n-typedef int (*evsocket_poll_func)(void *opaque, struct pollfd *fds,\n-                                  int nfds, int timeout_ms);\n-\n-RIST_PRIV void evsocket_set_poll_override(struct evsocket_ctx *ctx,\n-                                          evsocket_poll_func func,\n-                                          void *opaque);\n-\n #endif\n \ndiff --git a/src/log-private.h b/src/log-private.h\nindex e04077b..90fa50c 100755\n--- a/src/log-private.h\n+++ b/src/log-private.h\n@@ -14,5 +14,4 @@\n RIST_PRIV void rist_log_priv(struct rist_common_ctx *cctx, enum rist_log_level level, const char *format, ...);\n RIST_PRIV void rist_log_priv2(struct rist_logging_settings *logging_settings, enum rist_log_level level, const char *format, ...);\n RIST_PRIV void rist_log_priv3(enum rist_log_level level, const char *format, ...);\n-RIST_PRIV void rist_logging_unset_global_if_matches(const struct rist_logging_settings *logging_settings);\n #endif\ndiff --git a/src/logging.c b/src/logging.c\nindex 5762e0e..52b64da 100755\n--- a/src/logging.c\n+++ b/src/logging.c\n@@ -87,7 +87,7 @@ static inline void rist_log_impl(struct rist_logging_settings *log_settings,\n \t\tfprintf(stderr, \"[ERROR] Failed to format log message\\n\");\n \t\tgoto out;\n \t}\n-\tif (log_settings->log_socket >= 0)\n+\tif (log_settings->log_socket)\n \t\tudpsocket_send_nonblocking(log_settings->log_socket, logmsg, msglen);\n \tif (log_settings->log_stream) {\n \t\tfputs(logmsg, log_settings->log_stream);\n@@ -229,8 +229,6 @@ int rist_logging_set(struct rist_logging_settings **logging_settings, enum rist_\n \tbool alloc = false;\n \tif (!settings) {\n \t\tsettings = malloc(sizeof(*settings));\n-\t\tif (!settings)\n-\t\t\treturn -1;\n \t\t*settings = (struct rist_logging_settings)\n \t\t\tLOGGING_SETTINGS_INITIALIZER;\n \t\t*logging_settings = settings;\n@@ -275,7 +273,7 @@ int rist_logging_set(struct rist_logging_settings **logging_settings, enum rist_\n \t\t\tsettings->log_socket = -1;\n \t\t}\n \t\tchar host[200];\n-\t\tuint16_t port = 0;\n+\t\tuint16_t port;\n \t\tint local;\n \t\tif (udpsocket_parse_url(address, host, sizeof(host), &port, &local) != 0 || local == 1) {\n \t\t\trist_log_priv3(RIST_LOG_ERROR, \"Failed to parse logsocket address\\n\");\n@@ -313,38 +311,3 @@ err:\n \t}\n \treturn -1;\n }\n-\n-void rist_logging_unset_global_if_matches(const struct rist_logging_settings *logging_settings)\n-{\n-\tif (!logging_settings)\n-\t\treturn;\n-\tif (init_once_global() != 0)\n-\t{\n-\t\treturn;\n-\t}\n-\tpthread_mutex_lock(&global_logging_settings.global_logs_lock);\n-\tif (global_logging_settings.logs_set &&\n-\t\tglobal_logging_settings.settings.log_cb == logging_settings->log_cb &&\n-\t\tglobal_logging_settings.settings.log_cb_arg == logging_settings->log_cb_arg &&\n-\t\tglobal_logging_settings.settings.log_stream == logging_settings->log_stream)\n-\t{\n-\t\tif (global_logging_settings.settings.log_socket >= 0 &&\n-#ifndef _WIN32\n-\t\t\tglobal_logging_settings.settings.log_socket != STDIN_FILENO &&\n-\t\t\tglobal_logging_settings.settings.log_socket != STDOUT_FILENO &&\n-\t\t\tglobal_logging_settings.settings.log_socket != STDERR_FILENO)\n-#else\n-\t\t\tglobal_logging_settings.settings.log_socket != _fileno( stdin ) &&\n-\t\t\tglobal_logging_settings.settings.log_socket != _fileno( stdout ) &&\n-\t\t\tglobal_logging_settings.settings.log_socket != _fileno( stderr ))\n-#endif\n-\t\t{\n-\t\t\tudpsocket_close(global_logging_settings.settings.log_socket);\n-\t\t}\n-\t\tglobal_logging_settings.settings =\n-\t\t\t(struct rist_logging_settings)LOGGING_SETTINGS_INITIALIZER;\n-\t\tglobal_logging_settings.logs_set = false;\n-\t}\n-\tpthread_mutex_unlock(&global_logging_settings.global_logs_lock);\n-}\n-\ndiff --git a/src/mpegts.c b/src/mpegts.c\nindex 5e4a0e5..17b39a9 100644\n--- a/src/mpegts.c\n+++ b/src/mpegts.c\n@@ -24,16 +24,18 @@ int suppress_null_packets(const uint8_t payload_in[],uint8_t payload_out[], size\n \tSET_BIT(header_ext->flags, 7);\n \tsize_t offset = 0;\n \tint suppressed = 0;\n+\tstruct mpegts_header *hdr = (struct mpegts_header *)&payload_in[offset];\n+\tif (RIST_UNLIKELY(hdr->syncbyte != 0x47))\n+\t\tgoto fail;\n+\n \tfor (int i = 0; i <= (int)count-1; i++) {\n-\t\tstruct mpegts_header *hdr = (struct mpegts_header *)&payload_in[offset];\n-\t\tif (RIST_UNLIKELY(hdr->syncbyte != 0x47))\n-\t\t\tgoto fail;\n \t\tif (be16toh(hdr->flags1) == 0x1FFF) {\n \t\t\t*payload_len -= packet_size;\n \t\t\tSET_BIT(header_ext->npd_bits, (6 - i));\n \t\t\tsuppressed++;\n \t\t}\n \t\toffset += packet_size;\n+\t\thdr = (struct mpegts_header *)&payload_in[offset];\n \t}\n \n \tif (suppressed == 0)\n@@ -58,7 +60,7 @@ fail:\n int expand_null_packets(uint8_t payload_in[], uint8_t payload_out[], size_t *payload_len, uint8_t npd_bits) {\n \tsize_t packet_size = CHECK_BIT(npd_bits, 7) == 0? 188: 204;\n \n-\t// Non-null data\n+\t// Non-null data \n \tint ts_count = (int)(*payload_len / packet_size);\n \t// Null packets defined in header\n \tint null_count = CHECK_BIT(npd_bits, 6) + CHECK_BIT(npd_bits, 5) + CHECK_BIT(npd_bits, 4) + CHECK_BIT(npd_bits, 3) + CHECK_BIT(npd_bits, 2) + CHECK_BIT(npd_bits, 1) + CHECK_BIT(npd_bits, 0);\n@@ -66,24 +68,12 @@ int expand_null_packets(uint8_t payload_in[], uint8_t payload_out[], size_t *pay\n \tif (null_count == 0)\n \t\treturn 0;\n \n-\t// npd_bits only encodes 7 positions; mirror suppress_null_packets()'s upper bound\n-\tif ((ts_count + null_count) > 7)\n-\t\treturn 0;\n-\n-\tsize_t orig_payload_len = *payload_len;\n \tsize_t offset = 0;\n \tts_count += null_count;\n \t*payload_len = ts_count * packet_size;\n \tsize_t input_offset = 0;\n \tfor (int i = 0; i <= (int)ts_count-1; i++) {\n \t\tif (CHECK_BIT(npd_bits, (6 - i)) == 0) {\n-\t\t\tif (input_offset + packet_size > orig_payload_len) {\n-\t\t\t\t/* Failure mid-loop: restore the caller's length so the\n-\t\t\t\t * error return can't be mistaken for a successful\n-\t\t\t\t * expansion delivering unwritten scratch content. */\n-\t\t\t\t*payload_len = orig_payload_len;\n-\t\t\t\treturn -1;\n-\t\t\t}\n \t\t\tmemcpy(&payload_out[offset], &payload_in[input_offset], packet_size);\n \t\t\tinput_offset += packet_size;\n \t\t}\ndiff --git a/src/network.c b/src/network.c\nindex f46e48a..9cc0cd7 100644\n--- a/src/network.c\n+++ b/src/network.c\n@@ -4,15 +4,6 @@\n #include <stdint.h>\n #include <stdlib.h>\n #include <memory.h>\n-\n-#ifdef __EMSCRIPTEN__\n-/* WASM has no network interfaces — fill with zeros */\n-int _librist_network_get_macaddr(uint8_t mac[]) {\n-  memset(mac, 0, 6);\n-  return 0;\n-}\n-#else /* !__EMSCRIPTEN__ */\n-\n #ifndef _WIN32\n #include <ifaddrs.h>\n #ifdef __linux__\n@@ -33,15 +24,10 @@ int _librist_network_get_macaddr(uint8_t mac[]) {\n #ifndef _WIN32\n #if defined(__GNU__)\n   int sock = socket(PF_INET, SOCK_DGRAM, 0);\n-  if (sock < 0)\n-    return -1;\n #endif\n   struct ifaddrs *ifaddr = NULL;\n   struct ifaddrs *ifa = NULL;\n   if (getifaddrs(&ifaddr) == -1) {\n-#if defined(__GNU__)\n-    close(sock);\n-#endif\n     return -1;\n   } else {\n     for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) {\n@@ -114,4 +100,3 @@ int _librist_network_get_macaddr(uint8_t mac[]) {\n #endif\n   return 0;\n }\n-#endif /* !__EMSCRIPTEN__ */\ndiff --git a/src/peer.c b/src/peer.c\nindex 2b9f589..15738e6 100644\n--- a/src/peer.c\n+++ b/src/peer.c\n@@ -6,20 +6,16 @@\n #endif\n \n #if HAVE_SRP_SUPPORT\n-int librist_peer_update_rx_passphrase(struct rist_peer *peer, const uint8_t *passphrase, size_t passphrase_len, bool immediate) {\n-\tint ret = 0;\n+void librist_peer_update_rx_passphrase(struct rist_peer *peer, const uint8_t *passphrase, size_t passphrase_len, bool immediate) {\n \tif (immediate || !peer->supports_otf_passphrase_change) {\n-\t\tif (_librist_crypto_psk_set_passphrase(&peer->key_rx, passphrase, passphrase_len) != 0)\n-\t\t\tret = -1;\n-\t\tif (_librist_crypto_psk_set_passphrase(&peer->key_rx_odd, passphrase, passphrase_len) != 0)\n-\t\t\tret = -1;\n+\t\t_librist_crypto_psk_set_passphrase(&peer->key_rx, passphrase, passphrase_len);\n+\t\t_librist_crypto_psk_set_passphrase(&peer->key_rx_odd, passphrase, passphrase_len);\n \t} else if (peer->supports_otf_passphrase_change) {\n \t\tif (!peer->key_rx_odd_active)\n-\t\t\tret = _librist_crypto_psk_set_passphrase(&peer->key_rx, passphrase, passphrase_len);\n+\t\t\t_librist_crypto_psk_set_passphrase(&peer->key_rx, passphrase, passphrase_len);\n \t\telse\n-\t\t\tret = _librist_crypto_psk_set_passphrase(&peer->key_rx_odd, passphrase, passphrase_len);\n+\t\t\t_librist_crypto_psk_set_passphrase(&peer->key_rx_odd, passphrase, passphrase_len);\n \t}\n-\treturn ret;\n }\n \n void librist_peer_get_current_tx_passphrase(struct rist_peer *peer, const uint8_t **passphrase, size_t *passphrase_len) {\n@@ -105,9 +101,7 @@ struct rist_peer * _librist_peer_match_peer_addr(struct rist_peer *p, uint16_t f\n \t\t\treturn p;\n \t\tp = p->child;\n \t\twhile (p) {\n-\t\t\t/* Skip dead children so an absorbed peer cannot shadow\n-\t\t\t * the live record that now owns the tuple. */\n-\t\t\tif (!p->dead && _librist_peer_equal_address(family, addr, p))\n+\t\t\tif (_librist_peer_equal_address(family, addr, p))\n \t\t\t\treturn p;\n \t\t\tp = p->sibling_next;\n \t\t}\ndiff --git a/src/peer.h b/src/peer.h\nindex 8b5ecd6..b41869d 100644\n--- a/src/peer.h\n+++ b/src/peer.h\n@@ -10,7 +10,7 @@\n struct rist_peer;\n \n #if HAVE_SRP_SUPPORT\n-RIST_PRIV int librist_peer_update_rx_passphrase(struct rist_peer *peer, const uint8_t *passphrase, size_t passphrase_len, bool immediate);\n+RIST_PRIV void librist_peer_update_rx_passphrase(struct rist_peer *peer, const uint8_t *passphrase, size_t passphrase_len, bool immediate);\n RIST_PRIV void librist_peer_update_tx_passphrase(struct rist_peer *peer, const uint8_t *passphrase, size_t passphrase_len, bool immediate);\n RIST_PRIV void librist_peer_get_current_tx_passphrase(struct rist_peer *peer, const uint8_t **passphrase, size_t *passphrase_len);\n RIST_PRIV bool librist_peer_should_rollover_passphrase(struct rist_peer *peer);\ndiff --git a/src/proto/adv.h b/src/proto/adv.h\ndeleted file mode 100644\nindex 3c05dc1..0000000\n--- a/src/proto/adv.h\n+++ /dev/null\n@@ -1,492 +0,0 @@\n-/*\n- * Copyright © 2019-2020 SipRadius LLC\n- * Copyright © 2024-2026 SipRadius LLC\n- * All rights reserved.\n- *\n- * SPDX-License-Identifier: BSD-2-Clause\n- *\n- * VSF TR-06-3:2024 — RIST Advanced Profile wire format definitions.\n- */\n-\n-#ifndef RIST_PROTO_ADV_H\n-#define RIST_PROTO_ADV_H\n-\n-#include \"common/attributes.h\"\n-#include \"proto/rtp.h\"\n-#include <stdint.h>\n-#include <stdbool.h>\n-#include <stddef.h>\n-\n-#ifdef _WIN32\n-#include <winsock2.h>\n-#else\n-#include <arpa/inet.h>\n-#endif\n-\n-/*\n- * VSF TR-06-3 Advanced Profile top-level tunnel packet format (Figure 1).\n- *\n- * 0                   1                   2                   3\n- * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1\n- * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n- * |V=2|P|X| CC  |M|   PT=127     |     Sequence Number (low 16)  |  RTP\n- * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  (12 B)\n- * |                      Timestamp (1 MHz)                        |\n- * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n- * |                         SSRC identifier                       |\n- * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n- * :             RTP Header Extension (if X=1)                     :\n- * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n- * |   Seq Number Extension (16)   |F|L|E|R|I|P|H|PSK |LPC| Type |  Profile-\n- * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  defined\n- * |              Flow ID (if I=1, 4 bytes)                        |  header\n- * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n- * |              PSK Hash (if present, 16 bytes)                  |\n- * |              PSK Nonce (if present, 4 bytes)                  |\n- * |              PSK IV (if present, 4 bytes)                     |\n- * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n- * |              Payload Compression (if LPC=11, 4 bytes)         |\n- * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n- * |              Payload Format Descriptor (if P=1, 4 bytes)      |\n- * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n- * :              RIST Header Extension (if H=1, variable)         :\n- * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n- * |                        Payload ...                            |\n- * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n- */\n-\n-/* --------------------------------------------------------------------------\n- * RTP constants for Advanced Profile\n- * -------------------------------------------------------------------------- */\n-\n-/* PT=127 when SDP is not in use (Section 5.2.1) */\n-#define RIST_ADV_PT             127\n-\n-/* Default timestamp frequency: 1 MHz (Section 5.2.1) */\n-#define RIST_ADV_CLOCK_HZ       1000000\n-\n-/*\n- * Default RTP flags byte: V=2, P=0, X=0, CC=0 = 0x80.\n- * The profile-defined header (seq_ext + flags) is NOT an RFC 3550 header\n- * extension — it's part of the RTP \"payload\" interpreted by the Advanced\n- * Profile.  X is only set when an actual RFC 3550 header extension is used.\n- */\n-#define RIST_ADV_RTP_FLAGS      0x80\n-\n-/* --------------------------------------------------------------------------\n- * Profile-defined header: fixed extension (always present, 4 bytes)\n- * -------------------------------------------------------------------------- */\n-\n-RIST_PACKED_STRUCT(rist_adv_ext, {\n-\tuint16_t seq_ext;   /* Sequence Number Extension (high 16 bits) */\n-\tuint8_t  flags;     /* F|L|E|R|I|P|H|PSK[2] */\n-\tuint8_t  params;    /* PSK[1:0]|LPC[1:0]|Type[3:0] */\n-})\n-\n-/* flags byte (byte 2 of the extension, bits 16-23 of line 5) */\n-#define RIST_ADV_FLAG_F     0x80    /* First fragment */\n-#define RIST_ADV_FLAG_L     0x40    /* Last fragment */\n-#define RIST_ADV_FLAG_E     0x20    /* Expedite */\n-#define RIST_ADV_FLAG_R     0x10    /* Retransmit */\n-#define RIST_ADV_FLAG_I     0x08    /* Flow ID present */\n-#define RIST_ADV_FLAG_P     0x04    /* Payload Format Descriptor present */\n-#define RIST_ADV_FLAG_H     0x02    /* RIST Header Extension present */\n-#define RIST_ADV_PSK2_MASK  0x01    /* PSK field bit 2 (MSB of 3-bit PSK) */\n-\n-/* params byte (byte 3 of the extension, bits 24-31 of line 5) */\n-#define RIST_ADV_PSK10_SHIFT    6   /* PSK[1:0] in bits 7-6 */\n-#define RIST_ADV_PSK10_MASK     0xC0\n-#define RIST_ADV_LPC_SHIFT      4   /* LPC[1:0] in bits 5-4 */\n-#define RIST_ADV_LPC_MASK       0x30\n-#define RIST_ADV_TYPE_MASK      0x0F /* Type[3:0] in bits 3-0 */\n-\n-/* --------------------------------------------------------------------------\n- * PSK mode extraction / insertion\n- *\n- *  The 3-bit PSK field spans two bytes:\n- *    PSK[2]   = ext->flags  bit 0\n- *    PSK[1:0] = ext->params bits 7-6\n- * -------------------------------------------------------------------------- */\n-\n-static inline uint8_t rist_adv_get_psk(const struct rist_adv_ext *ext)\n-{\n-\treturn (uint8_t)(((ext->flags & RIST_ADV_PSK2_MASK) << 2) |\n-\t                 ((ext->params & RIST_ADV_PSK10_MASK) >> RIST_ADV_PSK10_SHIFT));\n-}\n-\n-static inline void rist_adv_set_psk(struct rist_adv_ext *ext, uint8_t psk)\n-{\n-\text->flags  = (ext->flags & ~RIST_ADV_PSK2_MASK) | ((psk >> 2) & 0x01);\n-\text->params = (ext->params & ~RIST_ADV_PSK10_MASK) |\n-\t              (uint8_t)((psk & 0x03) << RIST_ADV_PSK10_SHIFT);\n-}\n-\n-/* LPC extraction / insertion */\n-static inline uint8_t rist_adv_get_lpc(const struct rist_adv_ext *ext)\n-{\n-\treturn (uint8_t)((ext->params & RIST_ADV_LPC_MASK) >> RIST_ADV_LPC_SHIFT);\n-}\n-\n-static inline void rist_adv_set_lpc(struct rist_adv_ext *ext, uint8_t lpc)\n-{\n-\text->params = (ext->params & ~RIST_ADV_LPC_MASK) |\n-\t              (uint8_t)((lpc & 0x03) << RIST_ADV_LPC_SHIFT);\n-}\n-\n-/* Type extraction / insertion */\n-static inline uint8_t rist_adv_get_type(const struct rist_adv_ext *ext)\n-{\n-\treturn ext->params & RIST_ADV_TYPE_MASK;\n-}\n-\n-static inline void rist_adv_set_type(struct rist_adv_ext *ext, uint8_t type)\n-{\n-\text->params = (ext->params & ~RIST_ADV_TYPE_MASK) | (type & 0x0F);\n-}\n-\n-/* --------------------------------------------------------------------------\n- * Encapsulation type constants (Type[3:0], Section 5.2.3)\n- * -------------------------------------------------------------------------- */\n-#define RIST_ADV_TYPE_RESERVED      0\n-#define RIST_ADV_TYPE_IPV4          1\n-#define RIST_ADV_TYPE_IPV6          2\n-#define RIST_ADV_TYPE_REDUCED_UDP   3\n-#define RIST_ADV_TYPE_CONTROL       4\n-#define RIST_ADV_TYPE_DIRECT        5\n-#define RIST_ADV_TYPE_LAYER2        6\n-#define RIST_ADV_TYPE_GRE_RFC2784   7\n-#define RIST_ADV_TYPE_GRE_MAIN      8\n-\n-/* --------------------------------------------------------------------------\n- * PSK mode constants (PSK[2:0], Section 5.2.3)\n- * -------------------------------------------------------------------------- */\n-#define RIST_ADV_PSK_NONE               0   /* No Encryption */\n-#define RIST_ADV_PSK_AES_CTR            1   /* AES-CTR (Main Profile compat) */\n-#define RIST_ADV_PSK_HMAC_SHA256        2   /* HMAC-SHA256 (no encryption) */\n-#define RIST_ADV_PSK_AES_CTR_HMAC       3   /* AES-CTR-HMAC-SHA256 */\n-#define RIST_ADV_PSK_AES_GCM            4   /* AES-GCM */\n-#define RIST_ADV_PSK_CHACHA20_POLY1305  5   /* CHACHA20-POLY1305 */\n-#define RIST_ADV_PSK_USER_NOHASH        6   /* User-defined, no hash */\n-#define RIST_ADV_PSK_USER_HASH          7   /* User-defined, with hash */\n-\n-/* --------------------------------------------------------------------------\n- * PSK header sizes per Table 1 (Section 5.2.3)\n- *\n- *   Hash (16 B) + Nonce (4 B) + IV (4 B) — presence per mode.\n- * -------------------------------------------------------------------------- */\n-\n-#define RIST_ADV_PSK_HASH_SIZE   16\n-#define RIST_ADV_PSK_NONCE_SIZE   4\n-#define RIST_ADV_PSK_IV_SIZE      4\n-\n-static inline bool rist_adv_psk_has_hash(uint8_t psk)\n-{\n-\treturn psk == 2 || psk == 3 || psk == 4 || psk == 5 || psk == 7;\n-}\n-\n-static inline bool rist_adv_psk_has_nonce(uint8_t psk)\n-{\n-\treturn psk >= 1;\n-}\n-\n-static inline bool rist_adv_psk_has_iv(uint8_t psk)\n-{\n-\treturn psk == 1 || psk >= 3;\n-}\n-\n-static inline size_t rist_adv_psk_hdr_size(uint8_t psk)\n-{\n-\tsize_t sz = 0;\n-\tif (rist_adv_psk_has_hash(psk))  sz += RIST_ADV_PSK_HASH_SIZE;\n-\tif (rist_adv_psk_has_nonce(psk)) sz += RIST_ADV_PSK_NONCE_SIZE;\n-\tif (rist_adv_psk_has_iv(psk))    sz += RIST_ADV_PSK_IV_SIZE;\n-\treturn sz;\n-}\n-\n-/* --------------------------------------------------------------------------\n- * LPC mode constants (LPC[1:0], Section 5.2.3)\n- * -------------------------------------------------------------------------- */\n-#define RIST_ADV_LPC_NONE           0   /* No Compression */\n-#define RIST_ADV_LPC_LZ4            1   /* LZ4 Compression */\n-#define RIST_ADV_LPC_RESERVED       2   /* Reserved */\n-#define RIST_ADV_LPC_FIELD_PRESENT  3   /* Payload Compression field present */\n-\n-/* --------------------------------------------------------------------------\n- * Control Index values (Table 2, Section 5.3)\n- * -------------------------------------------------------------------------- */\n-#define RIST_ADV_CI_NACK_BITMASK        0x0000\n-#define RIST_ADV_CI_NACK_RANGE          0x0001\n-#define RIST_ADV_CI_RTT_ECHO_REQ        0x0010\n-#define RIST_ADV_CI_RTT_ECHO_RESP       0x0011\n-#define RIST_ADV_CI_FEC_2022_5_ROW      0x0020\n-#define RIST_ADV_CI_FEC_2022_5_COL      0x0021\n-#define RIST_ADV_CI_FEC_2022_1_ROW      0x0022\n-#define RIST_ADV_CI_FEC_2022_1_COL      0x0023\n-#define RIST_ADV_CI_KEEPALIVE           0x8000\n-#define RIST_ADV_CI_FLOW_ATTR           0x8001\n-#define RIST_ADV_CI_SRP_AUTH            0x8010\n-#define RIST_ADV_CI_PSK_NONCE           0x8011\n-#define RIST_ADV_CI_UNSUPPORTED         0x8020\n-\n-/* --------------------------------------------------------------------------\n- * Flow ID field (4 bytes, Section 5.2.4, Figure 4)\n- * -------------------------------------------------------------------------- */\n-\n-RIST_PACKED_STRUCT(rist_adv_flow_id, {\n-\tuint16_t outer;             /* Outer Flow ID */\n-\tuint8_t  inner_hi;          /* Inner Flow ID bits [11:4] */\n-\tuint8_t  inner_lo_sub;      /* Inner Flow ID bits [3:0] | IFSID [3:0] */\n-})\n-\n-static inline uint16_t rist_adv_flow_outer(const struct rist_adv_flow_id *f)\n-{\n-\treturn ntohs(f->outer);\n-}\n-\n-static inline uint16_t rist_adv_flow_inner(const struct rist_adv_flow_id *f)\n-{\n-\treturn (uint16_t)((f->inner_hi << 4) | ((f->inner_lo_sub >> 4) & 0x0F));\n-}\n-\n-static inline uint8_t rist_adv_flow_sub(const struct rist_adv_flow_id *f)\n-{\n-\treturn f->inner_lo_sub & 0x0F;\n-}\n-\n-/* --------------------------------------------------------------------------\n- * Payload Format Descriptor (4 bytes, Section 5.2.7, Figure 6)\n- * -------------------------------------------------------------------------- */\n-\n-RIST_PACKED_STRUCT(rist_adv_pfd, {\n-\tuint32_t value;     /* ID Type (4 bits) | ID Value (28 bits) */\n-})\n-\n-static inline uint8_t rist_adv_pfd_id_type(const struct rist_adv_pfd *p)\n-{\n-\treturn (uint8_t)(ntohl(p->value) >> 28);\n-}\n-\n-static inline uint32_t rist_adv_pfd_id_value(const struct rist_adv_pfd *p)\n-{\n-\treturn ntohl(p->value) & 0x0FFFFFFF;\n-}\n-\n-/* --------------------------------------------------------------------------\n- * Header size constants\n- * -------------------------------------------------------------------------- */\n-#define RIST_ADV_RTP_SIZE           12  /* Standard RTP header */\n-#define RIST_ADV_EXT_SIZE            4  /* Profile-defined extension */\n-#define RIST_ADV_HEADER_MIN         16  /* RTP (12) + ext (4) */\n-#define RIST_ADV_FLOW_ID_SIZE        4\n-#define RIST_ADV_COMPRESSION_SIZE    4\n-#define RIST_ADV_PFD_SIZE            4\n-\n-/* Maximum header: RTP(12) + ext(4) + flow_id(4) + hash(16) + nonce(4) + iv(4) + comp(4) + pfd(4) = 52\n- * (RIST Header Extension is variable-length on top of this) */\n-#define RIST_ADV_MAX_FIXED_HEADER   52\n-\n-/* Control message sub-header */\n-#define RIST_ADV_CTRL_HDR_SIZE       4  /* Control Index (2) + Length (2) */\n-\n-/* --------------------------------------------------------------------------\n- * 32-bit sequence number helpers\n- * -------------------------------------------------------------------------- */\n-\n-static inline uint32_t rist_adv_seq32(const struct rist_rtp_hdr *rtp,\n-                                       const struct rist_adv_ext *ext)\n-{\n-\treturn ((uint32_t)ntohs(ext->seq_ext) << 16) | ntohs(rtp->seq);\n-}\n-\n-static inline void rist_adv_set_seq32(struct rist_rtp_hdr *rtp,\n-                                       struct rist_adv_ext *ext,\n-                                       uint32_t seq)\n-{\n-\trtp->seq = htons((uint16_t)(seq & 0xFFFF));\n-\text->seq_ext = htons((uint16_t)(seq >> 16));\n-}\n-\n-/* --------------------------------------------------------------------------\n- * Fragment flag helpers\n- *\n- *   F=1, L=1 -> unfragmented\n- *   F=1, L=0 -> first fragment\n- *   F=0, L=0 -> middle fragment\n- *   F=0, L=1 -> last fragment\n- * -------------------------------------------------------------------------- */\n-\n-static inline bool rist_adv_is_unfragmented(uint8_t flags)\n-{\n-\treturn (flags & (RIST_ADV_FLAG_F | RIST_ADV_FLAG_L)) ==\n-\t       (RIST_ADV_FLAG_F | RIST_ADV_FLAG_L);\n-}\n-\n-static inline bool rist_adv_is_fragmented(uint8_t flags)\n-{\n-\treturn !rist_adv_is_unfragmented(flags);\n-}\n-\n-static inline bool rist_adv_is_first_fragment(uint8_t flags)\n-{\n-\treturn (flags & RIST_ADV_FLAG_F) && !(flags & RIST_ADV_FLAG_L);\n-}\n-\n-static inline bool rist_adv_is_last_fragment(uint8_t flags)\n-{\n-\treturn !(flags & RIST_ADV_FLAG_F) && (flags & RIST_ADV_FLAG_L);\n-}\n-\n-/* Sequence difference for wrap-around comparison */\n-static inline int32_t rist_adv_seq_diff(uint32_t a, uint32_t b)\n-{\n-\treturn (int32_t)(a - b);\n-}\n-\n-/* SSRC parity helpers (Section 5.2.1) */\n-static inline bool rist_adv_ssrc_is_protected(uint32_t ssrc)\n-{\n-\treturn (ssrc & 1) == 0;\n-}\n-\n-static inline uint32_t rist_adv_ssrc_protected(uint32_t ssrc_base)\n-{\n-\treturn ssrc_base & ~(uint32_t)1;\n-}\n-\n-static inline uint32_t rist_adv_ssrc_unprotected(uint32_t ssrc_base)\n-{\n-\treturn ssrc_base | 1;\n-}\n-\n-/* --------------------------------------------------------------------------\n- * Parse output structure\n- * -------------------------------------------------------------------------- */\n-\n-struct rist_adv_parsed {\n-\t/* From RTP header */\n-\tuint32_t seq;               /* Full 32-bit sequence number */\n-\tuint32_t timestamp;         /* RTP timestamp (1 MHz clock) */\n-\tuint32_t ssrc;\n-\tbool     rtp_ext_present;   /* X bit in RTP header */\n-\tbool     rtp_padding;       /* P bit in RTP header */\n-\n-\t/* From profile-defined extension */\n-\tbool     first_frag;        /* F bit */\n-\tbool     last_frag;         /* L bit */\n-\tbool     expedite;          /* E bit */\n-\tbool     retransmit;        /* R bit */\n-\tuint8_t  psk_mode;          /* PSK[2:0] (0-7) */\n-\tuint8_t  lpc_mode;          /* LPC[1:0] (0-3) */\n-\tuint8_t  enc_type;          /* Type[3:0] (0-15) */\n-\n-\t/* Optional fields — pointers into original buffer (NULL if absent) */\n-\tbool     has_flow_id;       /* I flag */\n-\tconst struct rist_adv_flow_id *flow_id;\n-\n-\tbool     has_psk;           /* PSK mode > 0 */\n-\tconst uint8_t *psk_hash;    /* NULL if no hash for this mode */\n-\tconst uint8_t *psk_nonce;   /* NULL if no nonce */\n-\tconst uint8_t *psk_iv;      /* NULL if no IV */\n-\n-\tbool     has_compression;   /* LPC == 3 */\n-\tconst uint8_t *compression; /* 4-byte compression field */\n-\n-\tbool     has_pfd;           /* P flag */\n-\tconst struct rist_adv_pfd *pfd;\n-\n-\tbool     has_hdr_ext;       /* H flag */\n-\tconst uint8_t *hdr_ext;     /* Points to RFC 3550-style extension header */\n-\tsize_t   hdr_ext_len;       /* Total bytes of RIST header extension */\n-\n-\t/* Payload */\n-\tconst uint8_t *payload;\n-\tsize_t   payload_len;\n-};\n-\n-/* --------------------------------------------------------------------------\n- * Build input structure\n- * -------------------------------------------------------------------------- */\n-\n-struct rist_adv_params {\n-\tuint32_t seq;\n-\tuint32_t timestamp;\n-\tuint32_t ssrc;\n-\tuint8_t  enc_type;          /* Type[3:0] */\n-\tuint8_t  psk_mode;          /* PSK[2:0] */\n-\tuint8_t  lpc_mode;          /* LPC[1:0] */\n-\tbool     first_frag;\n-\tbool     last_frag;\n-\tbool     expedite;\n-\tbool     retransmit;\n-\n-\t/* Optional fields — NULL/zero to omit */\n-\tconst struct rist_adv_flow_id *flow_id;\n-\tconst uint8_t *psk_hash;\n-\tconst uint8_t *psk_nonce;\n-\tconst uint8_t *psk_iv;\n-\tconst uint8_t *compression;     /* 4 bytes if LPC==3 */\n-\tconst struct rist_adv_pfd *pfd;\n-\tconst uint8_t *hdr_ext;\n-\tsize_t   hdr_ext_len;\n-};\n-\n-/* --------------------------------------------------------------------------\n- * Function declarations\n- * -------------------------------------------------------------------------- */\n-\n-/* Parse an Advanced Profile packet.\n- * buf: pointer to the start of the RTP header\n- * len: total packet length\n- * out: populated on success\n- * Returns 0 on success, -1 on malformed packet. */\n-RIST_PRIV int rist_adv_parse(const uint8_t *buf, size_t len,\n-                             struct rist_adv_parsed *out);\n-\n-/* Build an Advanced Profile packet.\n- * buf: output buffer (caller ensures sufficient space)\n- * params: packet parameters\n- * payload: inner payload data (may be NULL if payload_len == 0)\n- * payload_len: inner payload length\n- * Returns total packet length on success, or -1 on error. */\n-RIST_PRIV int rist_adv_build(uint8_t *buf, const struct rist_adv_params *params,\n-                             const uint8_t *payload, size_t payload_len);\n-\n-/* Compute the header size for given parameters (without payload). */\n-RIST_PRIV size_t rist_adv_header_size(const struct rist_adv_params *params);\n-\n-/* --------------------------------------------------------------------------\n- * Control message API (adv_ctrl.c)\n- * -------------------------------------------------------------------------- */\n-\n-/* Forward declaration for control message API */\n-struct rist_peer;\n-\n-RIST_PRIV int rist_adv_send_nack_bitmask(struct rist_peer *peer,\n-                                          uint32_t media_ssrc,\n-                                          uint32_t pss, uint32_t blp);\n-RIST_PRIV int rist_adv_send_nack_range(struct rist_peer *peer,\n-                                        uint32_t media_ssrc,\n-                                        uint32_t pss, uint32_t nalp);\n-RIST_PRIV int rist_adv_send_rtt_echo_request(struct rist_peer *peer);\n-RIST_PRIV int rist_adv_send_rtt_echo_response(struct rist_peer *peer,\n-                                               uint32_t requester_ssrc,\n-                                               uint32_t orig_msw,\n-                                               uint32_t orig_lsw,\n-                                               uint32_t processing_delay_us);\n-RIST_PRIV int rist_adv_send_keepalive(struct rist_peer *peer);\n-RIST_PRIV int rist_adv_send_unsupported(struct rist_peer *peer,\n-                                         uint16_t incoming_ci,\n-                                         const uint8_t *incoming_payload,\n-                                         size_t incoming_len);\n-RIST_PRIV int rist_adv_send_type8(struct rist_peer *peer,\n-                                   const uint8_t *gre_packet,\n-                                   size_t gre_len);\n-RIST_PRIV int rist_adv_send_psk_nonce(struct rist_peer *peer,\n-                                       const uint8_t nonce[4],\n-                                       uint16_t key_size_bits);\n-RIST_PRIV int rist_adv_send_flow_attr(struct rist_peer *peer);\n-RIST_PRIV int rist_adv_recv_control(struct rist_peer *peer,\n-                                     const uint8_t *ctrl_payload,\n-                                     size_t ctrl_len);\n-\n-#endif /* RIST_PROTO_ADV_H */\ndiff --git a/src/proto/eap.c b/src/proto/eap.c\nindex 8cb417d..70d8806 100644\n--- a/src/proto/eap.c\n+++ b/src/proto/eap.c\n@@ -9,7 +9,6 @@\n #include \"common/attributes.h\"\n #include \"config.h\"\n #include \"crypto/psk.h\"\n-#include \"crypto/eap_v4_crypto.h\"\n #include \"endian-shim.h\"\n #include \"crypto/crypto-private.h\"\n #include \"crypto/srp.h\"\n@@ -17,7 +16,6 @@\n #include \"librist_srp.h\"\n #include \"rist-private.h\"\n #include \"udp-private.h\"\n-#include \"transport-private.h\"\n #include \"log-private.h\"\n #include \"proto/rist_time.h\"\n #include \"peer.h\"\n@@ -28,8 +26,6 @@\n #include <stdlib.h>\n #include <stdbool.h>\n #include <assert.h>\n-#include <limits.h>\n-#include <inttypes.h>\n \n #define HASH_ALGO SRP_SHA256\n #define DIGEST_LENGTH SHA256_DIGEST_LENGTH\n@@ -38,30 +34,6 @@\n #define EAP_AUTH_TIMEOUT_RETRY_MAX 5\n #define EAP_AUTH_TIMEOUT 500//ms\n #define EAP_REAUTH_PERIOD 60000 // ms\n-#define EAP_AUTH_FAILED_RECOVERY 30000 // ms, soft-FAILED -> UNAUTH after this quiet\n-#define EAP_IDENTITY_REPLY_INTERVAL 200 // ms, rate-limit pre-auth IDENTITY replies\n-#define EAP_SRP_OP_INTERVAL 200 // ms, rate-limit pre-auth SRP modexp operations\n-#define EAP_REAUTH_PROBE_MAX 2000 // ms, forget a stale authenticated-side identity-request probe after this gap\n-#define EAP_MAX_MODULUS_BYTES 1024 // largest RFC 5054 group (NG_8192) is 1024 bytes\n-\n-/* Permanent-failure sentinel for ctx->tries; fixed point under eap_tries_inc(). */\n-#define EAP_AUTH_TRIES_PERMANENT UINT_MAX\n-\n-/* v4 (AES-256-GCM passphrase channel) needs a real crypto backend; else stay v3. */\n-#if HAVE_MBEDTLS || HAVE_NETTLE\n-#define EAP_V4_GCM_AVAILABLE 1\n-#else\n-#define EAP_V4_GCM_AVAILABLE 0\n-#endif\n-/* Highest EAP version we originate; -DEAP_VERSION_MAX=3 forces a v3 peer for interop. */\n-#ifndef EAP_VERSION_MAX\n-#define EAP_VERSION_MAX (EAP_V4_GCM_AVAILABLE ? 4 : 3)\n-#endif\n-#define EAP_V4_NONCE_LEN 12\n-#define EAP_V4_TAG_LEN 16\n-#define EAP_V4_AAD_LEN (5 + EAP_V4_NONCE_LEN)\n-/* Max v4 plaintext in one EAPOL frame; the send bound and receive buffer share it. */\n-#define EAP_V4_MAX_PLAINTEXT (1500 - (EAPOL_EAP_HDRS_OFFSET + sizeof(struct eap_srp_hdr) + 1 + EAP_V4_NONCE_LEN + EAP_V4_TAG_LEN))\n \n static int eap_request_passphrase(struct eapsrp_ctx *ctx, bool start);\n \n@@ -82,23 +54,10 @@ struct eapsrp_ctx\n \n     int authentication_state;\n     uint8_t last_identifier;\n-    unsigned int tries;\n+    uint8_t tries;\n     bool may_rollover_passphrase;\n     bool did_first_auth;\n \n-    uint64_t failed_state_timestamp; /* 0 = not in soft-FAILED */\n-    uint64_t last_identity_reply_timestamp; /* rate-limit pre-auth IDENTITY replies */\n-    uint64_t last_srp_op_timestamp; /* rate-limit pre-auth SRP modexp operations */\n-\n-    /* Authenticatee-only bounded EAPOL START retransmit; 0 = idle. */\n-    uint64_t authee_start_timer;\n-    int authee_start_tries;\n-\n-    /* Authenticatee-only: start of the current run of identity requests seen\n-     * while already authenticated; 0 = none in progress. Gates re-auth after\n-     * an authenticator restart without honoring a lone forged reset packet. */\n-    uint64_t reauth_probe_timer;\n-\n     uint64_t passphrase_request_timer;\n     int passphrase_request_times;\n     uint8_t passphrase_request_identifier;\n@@ -140,97 +99,8 @@ struct eapsrp_ctx\n \tuint8_t *authenticator_bytes_salt;\n \n \tbool eapversion3;//EAPv3 signalled. old libRIST used v2, so use this to ensure compat with broken hashing\n-\n-\tbool srp_legacy_pad;         //srp-compat=1 URL opt-in\n-\tbool srp_legacy_peer_warned; //one-shot latch for the M1/M2 hint\n-\n-\t/* EAP v4 (AEAD passphrase channel) state. */\n-\tuint8_t peer_eap_version;                 //highest EAP version the peer has advertised\n-\tuint64_t tx_nonce_counter;                //next 96-bit GCM nonce to assign for our send direction\n-\tuint64_t unsollicited_passphrase_nonce;   //cached nonce for the unsolicited push (stable across retransmits)\n-\tuint64_t rx_last_nonce;                   //highest v4 nonce accepted from the peer (this direction)\n-\tbool rx_nonce_seen;                       //whether rx_last_nonce is valid yet\n };\n \n-static inline void eap_tries_inc(struct eapsrp_ctx *ctx)\n-{\n-\t/* Saturating: stays parked at EAP_AUTH_TRIES_PERMANENT instead of wrapping. */\n-\tif (ctx->tries < EAP_AUTH_TRIES_PERMANENT)\n-\t\tctx->tries++;\n-}\n-\n-/* v2 for a legacy session, else the highest we support. A v3 peer reads a\n- * v4-tagged packet as v3 (checks only >= 3), so advertising 4 is safe. */\n-static inline uint8_t eap_tx_version(struct eapsrp_ctx *ctx)\n-{\n-\tif (!ctx->eapversion3)\n-\t\treturn 2;\n-\treturn (uint8_t)EAP_VERSION_MAX;\n-}\n-\n-/* Print a wire-supplied username safely: replace anything outside\n- * printable ASCII so CR/LF can't forge log lines. */\n-static const char *eap_sanitize_log(const char *in, char *buf, size_t buflen)\n-{\n-\tsize_t i;\n-\tfor (i = 0; i + 1 < buflen && in[i]; i++) {\n-\t\tchar c = in[i];\n-\t\tbuf[i] = (c >= 32 && c < 127) ? c : '?';\n-\t}\n-\tbuf[i] = '\\0';\n-\treturn buf;\n-}\n-\n-/* Both ends can run v4: we originate it, the session is v3-hashed, and the peer\n- * advertised >= 4. Version is negotiated in the clear (outside the SRP proof),\n- * so a MITM can force a v3 downgrade -- integrity, not confidentiality. */\n-static inline bool eap_use_v4(struct eapsrp_ctx *ctx)\n-{\n-\treturn EAP_VERSION_MAX >= 4 && ctx->eapversion3 && ctx->peer_eap_version >= 4;\n-}\n-\n-/* Per-direction v4 key from K via HKDF-Expand-SHA256; the label separates the\n- * two directions so they never share a (key, nonce) pair. */\n-static int eap_v4_dir_key(struct eapsrp_ctx *ctx, bool client_to_server, uint8_t out[32])\n-{\n-\tconst uint8_t *K = (ctx->config.role == EAP_ROLE_AUTHENTICATOR)\n-\t\t? librist_crypto_srp_authenticator_get_key(ctx->auth_ctx)\n-\t\t: librist_crypto_srp_client_get_key(ctx->client_ctx);\n-\tif (K == NULL)\n-\t\treturn -1;\n-\tconst char *label = client_to_server ? \"RIST-EAP-v4 pass c2s\" : \"RIST-EAP-v4 pass s2c\";\n-\treturn _librist_crypto_hkdf_expand_sha256(K, SHA256_DIGEST_LENGTH,\n-\t\t(const uint8_t *)label, strlen(label), out, 32);\n-}\n-\n-/* AAD binds the wire-carried fields (identifier, flags, nonce). code/type/subtype\n- * are fixed constants pinned by both ends, so they frame the tag, not detect edits. */\n-static void eap_v4_build_aad(uint8_t aad[EAP_V4_AAD_LEN], uint8_t identifier,\n-                             uint8_t flags, const uint8_t nonce[EAP_V4_NONCE_LEN])\n-{\n-\taad[0] = EAP_CODE_RESPONSE;\n-\taad[1] = identifier;\n-\taad[2] = EAP_TYPE_SRP_SHA1;\n-\taad[3] = EAP_SRP_SUBTYPE_PASSWORD_REQUEST_RESPONSE;\n-\taad[4] = flags;\n-\tmemcpy(&aad[5], nonce, EAP_V4_NONCE_LEN);\n-}\n-\n-static void eap_v4_encode_nonce(uint64_t counter, uint8_t nonce[EAP_V4_NONCE_LEN])\n-{\n-\tmemset(nonce, 0, EAP_V4_NONCE_LEN); //top 4 octets stay zero\n-\tfor (int i = 0; i < 8; i++)\n-\t\tnonce[EAP_V4_NONCE_LEN - 1 - i] = (uint8_t)(counter >> (8 * i));\n-}\n-\n-static uint64_t eap_v4_decode_nonce(const uint8_t nonce[EAP_V4_NONCE_LEN])\n-{\n-\tuint64_t v = 0;\n-\tfor (int i = 0; i < 8; i++)\n-\t\tv = (v << 8) | nonce[EAP_V4_NONCE_LEN - 8 + i];\n-\treturn v;\n-}\n-\n void eap_reset_data(struct eapsrp_ctx *ctx)\n {\n \tif (ctx->config.role == EAP_ROLE_AUTHENTICATOR)\n@@ -255,21 +125,6 @@ void eap_reset_data(struct eapsrp_ctx *ctx)\n \tctx->last_pkt = NULL;\n \n \tctx->authenticated = false;\n-\n-\t/* Re-auth derives a fresh K, so restart the v4 nonce space; the new epoch\n-\t * re-baselines rx_last_nonce (else a re-authing ctx rejects the peer's\n-\t * restarted nonces despite a valid tag). */\n-\tctx->tx_nonce_counter = 0;\n-\tctx->rx_last_nonce = 0;\n-\tctx->rx_nonce_seen = false;\n-\n-\t/* Drop any armed unsolicited-passphrase retransmit: it caches the old\n-\t * epoch's plaintext and nonce, and resending it under the new K would\n-\t * reuse that nonce value once the restarted counter catches up -- the\n-\t * GCM forbidden state. The app layer can re-push after re-auth. */\n-\tctx->unsollicited_passphrase_response_timer = 0;\n-\tctx->unsollicited_passphrase_response_times = 0;\n-\tctx->unsollicited_passphrase_state = 0;\n }\n \n static int send_eapol_pkt(struct eapsrp_ctx *ctx, uint8_t eapoltype, uint8_t eapcode, uint8_t identifier, size_t payload_len, uint8_t buf[], uint8_t eap_version)\n@@ -290,15 +145,10 @@ static int send_eapol_pkt(struct eapsrp_ctx *ctx, uint8_t eapoltype, uint8_t eap\n \t{\n \t\tfree(ctx->last_pkt);\n \t\tctx->last_pkt = malloc((payload_len + EAPOL_EAP_HDRS_OFFSET));\n-\t\tif (ctx->last_pkt == NULL) {\n-\t\t\t// OOM: skip caching the retransmit copy\n-\t\t\tctx->last_pkt_size = 0;\n-\t\t} else {\n-\t\t\tmemcpy(ctx->last_pkt, buf, (payload_len + EAPOL_EAP_HDRS_OFFSET));\n-\t\t\tctx->last_pkt_size = (payload_len + EAPOL_EAP_HDRS_OFFSET);\n-\t\t\tctx->last_timestamp = timestampNTP_u64();\n-\t\t\tctx->timeout_retries = 0;\n-\t\t}\n+\t\tmemcpy(ctx->last_pkt, buf,(payload_len + EAPOL_EAP_HDRS_OFFSET));\n+\t\tctx->last_pkt_size = (payload_len + EAPOL_EAP_HDRS_OFFSET);\n+\t\tctx->last_timestamp = timestampNTP_u64();\n+\t\tctx->timeout_retries = 0;\n \t}\n \tif (_librist_proto_gre_send_data(ctx->peer, 0, RIST_GRE_PROTOCOL_TYPE_EAPOL, buf, (EAPOL_EAP_HDRS_OFFSET + payload_len), 0, 0, ctx->peer->rist_gre_version) < 0)\n \t\treturn -1;\n@@ -310,29 +160,6 @@ static int send_eapol_pkt(struct eapsrp_ctx *ctx, uint8_t eapoltype, uint8_t eap\n //EAP REQUEST HANDLING\n static int process_eap_request_identity(struct eapsrp_ctx *ctx, uint8_t identifier)\n {\n-\tif (ctx->config.role == EAP_ROLE_AUTHENTICATOR)\n-\t\treturn EAP_UNEXPECTEDREQUEST;\n-\tuint64_t now = timestampNTP_u64();\n-\t/* An identity request while authenticated is either a forged reset or a sign\n-\t * the authenticator lost our session; we can't tell on the wire. Don't tear\n-\t * down inline (a lone forged packet must not): arm reauth_probe_timer and\n-\t * keep refusing, letting eap_periodic_impl drive one bounded re-auth if it\n-\t * persists. */\n-\tif (ctx->authentication_state >= EAP_AUTH_STATE_SUCCESS) {\n-\t\tif (ctx->reauth_probe_timer == 0 ||\n-\t\t    now - ctx->reauth_probe_timer > (uint64_t)EAP_REAUTH_PROBE_MAX * RIST_CLOCK)\n-\t\t\tctx->reauth_probe_timer = now;\n-\t\treturn EAP_UNEXPECTEDREQUEST;\n-\t}\n-\t/* Rate-limit pre-auth replies (caps username echo + eap_reset_data work). */\n-\tif (ctx->last_identity_reply_timestamp != 0 &&\n-\t    now < ctx->last_identity_reply_timestamp + (uint64_t)EAP_IDENTITY_REPLY_INTERVAL * RIST_CLOCK) {\n-\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_DEBUG,\n-\t\t\tEAP_LOG_PREFIX\"Rate-limiting EAP IDENTITY response (last sent %u ms ago)\\n\",\n-\t\t\t(unsigned)((now - ctx->last_identity_reply_timestamp) / RIST_CLOCK));\n-\t\treturn 0;\n-\t}\n-\tctx->last_identity_reply_timestamp = now;\n \teap_reset_data(ctx);\n \tuint8_t eapolpkt[512];\n \tsize_t offset = EAPOL_EAP_HDRS_OFFSET;\n@@ -342,7 +169,7 @@ static int process_eap_request_identity(struct eapsrp_ctx *ctx, uint8_t identifi\n \toffset += strlen(ctx->config.username);\n \tsize_t len = offset;\n \tlen -= EAPOL_EAP_HDRS_OFFSET;\n-\treturn send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_RESPONSE, identifier, len, eapolpkt, eap_tx_version(ctx));\n+\treturn send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_RESPONSE, identifier, len, eapolpkt, ctx->eapversion3? 3 :2);\n }\n \n static int process_eap_request_srp_challenge(struct eapsrp_ctx *ctx, uint8_t identifier, size_t len, uint8_t pkt[], uint8_t eap_version)\n@@ -350,93 +177,64 @@ static int process_eap_request_srp_challenge(struct eapsrp_ctx *ctx, uint8_t ide\n \tif (len < 6)\n \t\treturn EAP_LENERR;\n \n-\t/* Rate-limit: each challenge costs a full client-side modexp (g^a) and\n-\t * the packet is forgeable pre-auth. */\n-\tuint64_t now = timestampNTP_u64();\n-\tif (ctx->last_srp_op_timestamp != 0 &&\n-\t    now < ctx->last_srp_op_timestamp + (uint64_t)EAP_SRP_OP_INTERVAL * RIST_CLOCK)\n-\t\treturn 0;\n-\tctx->last_srp_op_timestamp = now;\n-\n #if HAVE_MBEDTLS\n \tctx->eapversion3 = (eap_version >= 3);\n #elif HAVE_NETTLE\n \t(void)(eap_version);\n #endif\n-\t// each TLV is prefixed with a 2-byte big-endian length, account for it\n \tsize_t offset = 0;\n-\tif (offset + 2 > len)\n-\t\treturn EAP_LENERR;\n \tuint16_t *tmp_swap = (uint16_t *)&pkt[offset];\n \tsize_t name_len = be16toh(*tmp_swap);\n \toffset += 2;\n \t//name can be ignored\n-\tif (name_len > len - offset)\n-\t\treturn EAP_LENERR;\n \toffset += name_len;\n-\tif (offset + 2 > len)\n+\tif (offset > len)\n \t\treturn EAP_LENERR;\n \ttmp_swap = (uint16_t *)&pkt[offset];\n \tsize_t salt_len = be16toh(*tmp_swap);\n \toffset += 2;\n-\tif (salt_len > len - offset)\n+\tif (len < (offset + salt_len))\n \t\treturn EAP_LENERR;\n \n+\tbool use_default_2048 = true;\n \tuint8_t *salt = &pkt[offset];\n \tuint8_t *g = NULL;\n \tuint8_t *N = NULL;\n \tsize_t N_len = 0;\n \toffset += salt_len;\n-\tif (offset + 2 > len)\n-\t\treturn EAP_LENERR;\n \ttmp_swap = (uint16_t *)&pkt[offset];\n \tsize_t generator_len = be16toh(*tmp_swap);\n \toffset += 2;\n \tif (generator_len != 0)\n \t{\n-\t\tif (generator_len > len - offset)\n-\t\t\treturn EAP_LENERR;\n-\t\tif (generator_len > EAP_MAX_MODULUS_BYTES)\n+\t\tif (len < (offset + generator_len))\n \t\t\treturn EAP_LENERR;\n \n \t\tg = &pkt[offset];\n \t\toffset += generator_len;\n \t\tN = &pkt[offset];\n \t\tN_len = len - offset;\n-\t\tif (N_len > EAP_MAX_MODULUS_BYTES)\n-\t\t\treturn EAP_LENERR;\n \t}\n-\tbool use_default_ng = (generator_len == 0);\n \tlibrist_crypto_srp_client_ctx_free(ctx->client_ctx);\n-\tctx->client_ctx = librist_crypto_srp_client_ctx_create(use_default_ng, N, N_len, g, generator_len, salt, salt_len, ctx->eapversion3, ctx->srp_legacy_pad);\n-\tif (ctx->client_ctx == NULL)\n-\t\treturn EAP_INTERNALERR;\n+\tctx->client_ctx = librist_crypto_srp_client_ctx_create(use_default_2048, N, N_len, g, generator_len, salt, salt_len, ctx->eapversion3);\n+\tsize_t len_A;\n \tuint8_t response[1500] = {0};\n \tstruct eap_srp_hdr *hdr = (struct eap_srp_hdr *)&response[EAPOL_EAP_HDRS_OFFSET];\n \thdr->type = EAP_TYPE_SRP_SHA1;\n \thdr->subtype = EAP_SRP_SUBTYPE_CHALLENGE;\n-\tint len_A = librist_crypto_srp_client_write_A_bytes(ctx->client_ctx, &response[EAPOL_EAP_HDRS_OFFSET + sizeof(*hdr)], sizeof(response) -(EAPOL_EAP_HDRS_OFFSET + sizeof(*hdr)));\n-\tif (len_A < 0)\n-\t\treturn -1;\n-\treturn send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_RESPONSE, identifier, ((size_t)len_A + sizeof(*hdr)), response, eap_tx_version(ctx));\n+\tlen_A = librist_crypto_srp_client_write_A_bytes(ctx->client_ctx, &response[EAPOL_EAP_HDRS_OFFSET + sizeof(*hdr)], sizeof(response) -(EAPOL_EAP_HDRS_OFFSET + sizeof(*hdr)));\n+\tint ret = send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_RESPONSE, identifier, (len_A + sizeof(*hdr)), response, ctx->eapversion3? 3 :2);\n+\treturn ret;\n }\n \n static int process_eap_request_srp_server_key(struct eapsrp_ctx *ctx, uint8_t identifier, size_t len, uint8_t pkt[])\n {\n-\tif (!ctx->client_ctx)\n-\t{\n-\t\t/* No SRP client state: either no CHALLENGE was processed or the\n-\t\t * state was reset. A spoofed SERVER_KEY request must not reach\n-\t\t * the bignum layer with a NULL ctx. */\n-\t\tctx->authentication_state = EAP_AUTH_STATE_FAILED;\n-\t\treturn EAP_INTERNALERR;\n-\t}\n \tif (librist_crypto_srp_client_handle_B(ctx->client_ctx, pkt, len, ctx->config.username, ctx->config.password) != 0)\n \t{\n \t\tctx->authentication_state = EAP_AUTH_STATE_FAILED;\n \t\t//\"must disconnect immediately, set tries past limit\"\n-\t\tctx->tries = EAP_AUTH_TRIES_PERMANENT;\n-\t\treturn EAP_AUTH_TERMINATED;\n+\t\tctx->tries = 255;\n+\t\treturn -255;\n \t}\n \tsize_t out_len = sizeof(struct eap_srp_hdr) + 4 + DIGEST_LENGTH;\n \tuint8_t response[(EAPOL_EAP_HDRS_OFFSET + sizeof(struct eap_srp_hdr) + 4 + DIGEST_LENGTH)];\n@@ -453,7 +251,7 @@ static int process_eap_request_srp_server_key(struct eapsrp_ctx *ctx, uint8_t id\n \t\tctx->did_first_auth = true;\n \t}\n \tlibrist_crypto_srp_client_write_M1_bytes(ctx->client_ctx, &response[offset]);\n-\tint ret = send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_RESPONSE, identifier, out_len, response, eap_tx_version(ctx));\n+\tint ret = send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_RESPONSE, identifier, out_len, response, ctx->eapversion3? 3 :2);\n \treturn ret;\n }\n \n@@ -461,11 +259,6 @@ static int process_eap_request_srp_server_validator(struct eapsrp_ctx *ctx, uint\n {\n \tif (len < (4 + DIGEST_LENGTH))\n \t\treturn EAP_LENERR;\n-\tif (!ctx->client_ctx)\n-\t{\n-\t\tctx->authentication_state = EAP_AUTH_STATE_FAILED;\n-\t\treturn EAP_INTERNALERR;\n-\t}\n \tif (librist_crypto_srp_client_verify_m2(ctx->client_ctx, &pkt[4]) == 0)\n \t{\n \t\tif (ctx->authentication_state < EAP_AUTH_STATE_SUCCESS)\n@@ -482,133 +275,74 @@ static int process_eap_request_srp_server_validator(struct eapsrp_ctx *ctx, uint\n \t\tctx->authentication_state = EAP_AUTH_STATE_SUCCESS;\n \t\tctx->last_auth_timestamp = timestampNTP_u64();\n \t\tctx->tries = 0;\n-\t\tctx->failed_state_timestamp = 0;\n \t\tuint8_t outpkt[(EAPOL_EAP_HDRS_OFFSET + sizeof(struct eap_srp_hdr))] = {0};\n \t\tstruct eap_srp_hdr *hdr = (struct eap_srp_hdr *)&outpkt[EAPOL_EAP_HDRS_OFFSET];\n \t\tif (ctx->eapversion3) {\n-\t\t\tint ret = send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_SUCCESS, identifier, sizeof(*hdr), outpkt, eap_tx_version(ctx));\n+\t\t\tint ret = send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_SUCCESS, identifier, sizeof(*hdr), outpkt, 3);\n \t\t\teap_request_passphrase(ctx, true);\n \t\t\treturn ret;\n \t\t}\n \t\thdr->type = EAP_TYPE_SRP_SHA1;\n \t\thdr->subtype = EAP_SRP_SUBTYPE_SERVER_VALIDATOR;\n-\t\treturn send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_RESPONSE, identifier, sizeof(*hdr), outpkt, eap_tx_version(ctx));\n+\t\treturn send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_RESPONSE, identifier, sizeof(*hdr), outpkt, ctx->eapversion3? 3 :2);\n \t}\n \t//perm failure\n-\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_WARN,\n-\t\tEAP_LOG_PREFIX\"Server M2 verification failed for server@%s\\n\", ctx->ip_string);\n-\t/* Advisory hint, latched: see srp-compat note in NEWS for v0.2.18. */\n-\tif (!ctx->srp_legacy_peer_warned) {\n-\t\tctx->srp_legacy_peer_warned = true;\n-\t\tif (ctx->srp_legacy_pad) {\n-\t\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_INFO,\n-\t\t\t\tEAP_LOG_PREFIX\"  Hint: this side is configured with srp-compat=1. If the server is running librist 0.2.16+ (PAD-compliant, the default), drop ?srp-compat=1 on both sides.\\n\");\n-\t\t} else {\n-\t\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_INFO,\n-\t\t\t\tEAP_LOG_PREFIX\"  Hint: if the server is running librist 0.2.15 or earlier, the SRP wire format changed in 0.2.16 for RFC 5054 / TR-06-2 compliance. To interoperate with an older server, add ?srp-compat=1 on BOTH URLs. Otherwise check the password.\\n\");\n-\t\t}\n-\t}\n \tctx->authentication_state = EAP_AUTH_STATE_FAILED;\n-\tctx->tries = EAP_AUTH_TRIES_PERMANENT;\n+\tctx->tries = 255;\n \n \treturn -1;\n }\n \n-/* Send an EAP SRP Passphrase Response: AES-256-GCM for v4 (per-direction key,\n- * caller-supplied nonce, header as AAD), else the v3 AES-CTR form. The nonce is\n- * used only on v4; reuse it verbatim when retransmitting identical plaintext. */\n-static int eap_srp_send_password(struct eapsrp_ctx *ctx, uint8_t identifier, const uint8_t *password, size_t password_len, uint64_t nonce) {\n-\tbool v4 = (password_len > 0) && eap_use_v4(ctx);\n-\tsize_t overhead = EAPOL_EAP_HDRS_OFFSET + sizeof(struct eap_srp_hdr) + 1 +\n-\t\t(v4 ? (EAP_V4_NONCE_LEN + EAP_V4_TAG_LEN) : 0);\n-\tif (password_len > (1500 - overhead))\n+static int eap_srp_send_password(struct eapsrp_ctx *ctx, uint8_t identifier, const uint8_t *password, size_t password_len) {\n+\tif (password_len > (1500 - (EAPOL_EAP_HDRS_OFFSET + sizeof(struct eap_srp_hdr))))\n \t\treturn -1;\n \tuint8_t outpkt[1500] = {0};\n \tstruct eap_srp_hdr *hdr = (struct eap_srp_hdr *)&outpkt[EAPOL_EAP_HDRS_OFFSET];\n \thdr->type = EAP_TYPE_SRP_SHA1;\n \thdr->subtype = EAP_SRP_SUBTYPE_PASSWORD_REQUEST_RESPONSE;\n-\tuint8_t *flags = &outpkt[EAPOL_EAP_HDRS_OFFSET + sizeof(*hdr)];\n-\tsize_t payload_len;\n-\tuint8_t send_version = eap_tx_version(ctx);\n-\tif (password_len == 0) { //Use session key (U bit); no ciphertext either version\n-\t\tSET_BIT(*flags, 7);\n-\t\tpayload_len = sizeof(*hdr) + 1;\n-\t} else if (v4) {\n-\t\tSET_BIT(*flags, 6); //H = 256-bit (reserved-1 in v4)\n-\t\tuint8_t *noncep = flags + 1;\n-\t\tuint8_t *ct = noncep + EAP_V4_NONCE_LEN;\n-\t\tuint8_t *tag = ct + password_len;\n-\t\teap_v4_encode_nonce(nonce, noncep);\n-\t\tuint8_t key[32];\n-\t\tbool c2s = (ctx->config.role == EAP_ROLE_AUTHENTICATEE); //we send client->server\n-\t\tif (eap_v4_dir_key(ctx, c2s, key) != 0)\n-\t\t\treturn -1;\n-\t\tuint8_t aad[EAP_V4_AAD_LEN];\n-\t\teap_v4_build_aad(aad, identifier, *flags, noncep);\n-\t\tint rc = _librist_crypto_aes_gcm_encrypt(key, noncep, EAP_V4_NONCE_LEN,\n-\t\t\taad, sizeof(aad), password, password_len, ct, tag, EAP_V4_TAG_LEN);\n-\t\t_librist_crypto_secure_zero(key, sizeof(key));\n-\t\tif (rc != 0) {\n-\t\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_ERROR,\n-\t\t\t\tEAP_LOG_PREFIX\"v4 passphrase GCM encrypt failed\\n\");\n-\t\t\treturn -1;\n-\t\t}\n-\t\tpayload_len = sizeof(*hdr) + 1 + EAP_V4_NONCE_LEN + password_len + EAP_V4_TAG_LEN;\n-\t\tsend_version = 4; //payload format must match the version byte\n-\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_DEBUG,\n-\t\t\tEAP_LOG_PREFIX\"v4 passphrase sent under AES-256-GCM (nonce=%\" PRIu64 \", %zu bytes)\\n\",\n-\t\t\tnonce, password_len);\n-\t} else { //v3 AES-CTR\n-\t\tSET_BIT(*flags, 6); //aes_256\n-\t\tconst uint8_t *key = (ctx->config.role == EAP_ROLE_AUTHENTICATOR)\n-\t\t\t? librist_crypto_srp_authenticator_get_key(ctx->auth_ctx)\n-\t\t\t: librist_crypto_srp_client_get_key(ctx->client_ctx);\n+\tif (password_len == 0) { //Use session key\n+\t\tSET_BIT(outpkt[EAPOL_EAP_HDRS_OFFSET + sizeof(*hdr)], 7);\n+\t} else {\n+\t\tSET_BIT(outpkt[EAPOL_EAP_HDRS_OFFSET + sizeof(*hdr)], 6);\n+\t\tconst uint8_t *key;\n+\t\tif (ctx->config.role == EAP_ROLE_AUTHENTICATOR)\n+\t\t\tkey = librist_crypto_srp_authenticator_get_key(ctx->auth_ctx);\n+\t\telse\n+\t\t\tkey = librist_crypto_srp_client_get_key(ctx->client_ctx);\n+\n \t\tuint8_t iv[AES_BLOCK_SIZE] = {0};\n \t\tiv[AES_BLOCK_SIZE-1] = identifier;\n-\t\t_librist_crypto_aes_ctr(key, 256, iv, password, flags + 1, password_len);\n-\t\tpayload_len = sizeof(*hdr) + 1 + password_len;\n-\t\tsend_version = ctx->eapversion3 ? 3 : 2; //never tag a v3 payload as v4\n+\t\t_librist_crypto_aes_ctr(key, 256, iv, password, &outpkt[EAPOL_EAP_HDRS_OFFSET + sizeof(*hdr) +1], password_len);\n \t}\n-\treturn send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_RESPONSE, identifier, payload_len, outpkt, send_version);\n+\treturn send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_RESPONSE, identifier, sizeof(*hdr) +1 + password_len, outpkt, ctx->eapversion3? 3 :2);\n }\n \n static int process_eap_request_srp_passphrase(struct eapsrp_ctx *ctx, uint8_t identifier) {\n \tif (ctx->authentication_state == EAP_AUTH_STATE_SUCCESS) {\n \t\tif (ctx->config.use_key_as_passphrase)\n-\t\t\treturn eap_srp_send_password(ctx, identifier, NULL, 0, 0);\n+\t\t\treturn eap_srp_send_password(ctx, identifier, NULL, 0);\n \n \t\tconst uint8_t *passphrase = NULL;\n \t\tsize_t passphrase_len = 0;\n \t\tlibrist_peer_get_current_tx_passphrase(ctx->peer, &passphrase, &passphrase_len);\n-\t\t/* Solicited response: fresh nonce per send. Same-plaintext-different-nonce\n-\t\t * is safe under GCM; this path is not self-retransmitted (the peer re-requests). */\n-\t\treturn eap_srp_send_password(ctx, identifier, passphrase, passphrase_len, ctx->tx_nonce_counter++);\n+\t\treturn eap_srp_send_password(ctx, identifier, passphrase,  passphrase_len);\n \t}\n \n \tuint8_t buf[EAPOL_EAP_HDRS_OFFSET];\n-\treturn send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_FAILURE, identifier, 0, buf, eap_tx_version(ctx));\n+\treturn send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_FAILURE, identifier, 0, buf, ctx->eapversion3? 3 :2);\n }\n \n static int process_eap_request(struct eapsrp_ctx *ctx, uint8_t pkt[], size_t len, uint8_t identifier, uint8_t eap_version)\n {\n-\tif (len < 1)\n-\t\treturn EAP_LENERR;\n \tuint8_t type = pkt[0];\n \tif (type == EAP_TYPE_IDENTITY)\n \t\treturn process_eap_request_identity(ctx, identifier);\n \tif (type == EAP_TYPE_SRP_SHA1)\n \t{\n-\t\tif (len < 2)\n-\t\t\treturn EAP_LENERR;\n \t\tuint8_t subtype = pkt[1];\n \t\tif (subtype != EAP_SRP_SUBTYPE_PASSWORD_REQUEST_RESPONSE && ctx->config.role == EAP_ROLE_AUTHENTICATOR)\n \t\t\treturn EAP_UNEXPECTEDREQUEST;\n \n-\t\t/* Record identifier for FAILURE matching only after validation\n-\t\t * passes.  Moving this out of the prologue prevents an\n-\t\t * unauthenticated spoofed REQUEST from priming last_identifier\n-\t\t * and defeating the FAILURE-identifier gate. */\n-\t\tctx->last_identifier = identifier;\n \t\tswitch (subtype)\n \t\t{\n \t\t\tcase EAP_SRP_SUBTYPE_CHALLENGE:\n@@ -639,11 +373,6 @@ static int process_eap_response_identity(struct eapsrp_ctx *ctx, size_t len, uin\n {\n \tif (len > 255)\n \t\treturn -1;\n-\t/* On the authenticatee side ctx->config.lookup_func is NULL (calloc'd\n-\t * in rist_enable_eap_srp_2 and never assigned for that role); refuse the\n-\t * IDENTITY response here so we don't deref it further down. */\n-\tif (ctx->config.role != EAP_ROLE_AUTHENTICATOR || !ctx->config.lookup_func)\n-\t\treturn -1;\n \tmemcpy(ctx->config.username, pkt, len);\n \tctx->config.username[len] = '\\0';\n #if HAVE_MBEDTLS\n@@ -672,19 +401,19 @@ static int process_eap_response_identity(struct eapsrp_ctx *ctx, size_t len, uin\n \toffset += sizeof(*hdr);\n \thdr->type = EAP_TYPE_SRP_SHA1;\n \thdr->subtype = EAP_SRP_SUBTYPE_CHALLENGE;\n-\tint rc = -1;\n \tif (found)\n \t{\n \t\tstruct librist_crypto_srp_authenticator_ctx *auth_ctx = NULL;\n \t\tif (verifier_data.default_ng)\n \t\t\tlibrist_get_ng_constants(LIBRIST_SRP_NG_DEFAULT, &n_hex, &g_hex);\n \n-\t\tauth_ctx = librist_crypto_srp_authenticator_ctx_create(n_hex, g_hex, verifier_data.verifier, verifier_data.verifier_len, verifier_data.salt, verifier_data.salt_len, ctx->eapversion3, ctx->srp_legacy_pad);\n-\t\tif (!auth_ctx)\n-\t\t\tgoto out;\n+\t\tauth_ctx = librist_crypto_srp_authenticator_ctx_create(n_hex, g_hex, verifier_data.verifier, verifier_data.verifier_len, verifier_data.salt, verifier_data.salt_len, ctx->eapversion3);\n+\t\tif (!auth_ctx) {\n+\t\t\treturn -1;//Log some error?\n+\t\t}\n \t\tlibrist_crypto_srp_authenticator_ctx_free(ctx->auth_ctx);\n \t\tctx->auth_ctx = auth_ctx;\n-\t\tmemset(&outpkt[offset], 0, 2);\n+\t\tmemset(&pkt[offset], 0, 2);\n \t\toffset += 2;//we dont send the server name\n \t\tuint16_t *tmp_swap = (uint16_t *)&outpkt[offset];\n \t\t*tmp_swap = htobe16(verifier_data.salt_len);\n@@ -700,57 +429,40 @@ static int process_eap_response_identity(struct eapsrp_ctx *ctx, size_t len, uin\n \t\t\toffset += 2;\n \t\t\tint g_size = librist_crypto_srp_authenticator_write_g_bytes(auth_ctx, &outpkt[offset], sizeof(outpkt) -offset);\n \t\t\tif (g_size < 0)\n-\t\t\t\tgoto out;\n+\t\t\t\treturn -1;\n \t\t\t*tmp_swap = htobe16(g_size);\n \t\t\toffset += g_size;\n \n \t\t\tint n_len = librist_crypto_srp_authenticator_write_n_bytes(auth_ctx, &outpkt[offset], sizeof(outpkt) -offset);\n \t\t\tif (n_len < 0)\n-\t\t\t\tgoto out;\n+\t\t\t\treturn -1;\n \t\t\toffset += n_len;\n \t\t}\n-\t\tctx->last_identifier++;\n-\t\tsize_t out_len = offset - EAPOL_EAP_HDRS_OFFSET;\n-\t\trc = send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_REQUEST, ctx->last_identifier, out_len, outpkt, eap_tx_version(ctx));\n \t}\n-out:\n \tfree(verifier_data.verifier);\n \tfree(verifier_data.salt);\n \tfree(verifier_data.generator_ascii);\n \tfree(verifier_data.n_modulus_ascii);\n-\treturn rc;\n+\tif (!found)\n+\t\treturn -1;\n+\tctx->last_identifier++;\n+\tsize_t out_len = offset;\n+\tout_len -= EAPOL_EAP_HDRS_OFFSET;\n+\treturn send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_REQUEST, ctx->last_identifier, out_len, outpkt, ctx->eapversion3? 3 :2);\n }\n \n static int process_eap_response_client_key(struct eapsrp_ctx *ctx, size_t len, uint8_t pkt[])\n {\n-\tif (!ctx->auth_ctx) {\n-\t\tctx->authentication_state = EAP_AUTH_STATE_FAILED;\n-\t\treturn EAP_INTERNALERR;\n-\t}\n-\n-\t/* Rate-limit: each client key costs a full server-side modexp\n-\t * (B = kv + g^b) and the packet is forgeable pre-auth. */\n-\tuint64_t now = timestampNTP_u64();\n-\tif (ctx->last_srp_op_timestamp != 0 &&\n-\t    now < ctx->last_srp_op_timestamp + (uint64_t)EAP_SRP_OP_INTERVAL * RIST_CLOCK)\n-\t\treturn 0;\n-\tctx->last_srp_op_timestamp = now;\n-\n-\tif (librist_crypto_srp_authenticator_handle_A(ctx->auth_ctx, pkt, len) != 0) {\n-\t\tctx->authentication_state = EAP_AUTH_STATE_FAILED;\n-\t\tctx->tries = EAP_AUTH_TRIES_PERMANENT;\n-\t\treturn EAP_AUTH_TERMINATED;\n-\t}\n+\tlibrist_crypto_srp_authenticator_handle_A(ctx->auth_ctx, pkt, len);\n \n \tuint8_t outpkt[1500];\n \tstruct eap_srp_hdr *hdr = (struct eap_srp_hdr *)&outpkt[EAPOL_EAP_HDRS_OFFSET];\n \thdr->type = EAP_TYPE_SRP_SHA1;\n \thdr->subtype = EAP_SRP_SUBTYPE_SERVER_KEY;\n-\tint len_B = librist_crypto_srp_authenticator_write_B_bytes(ctx->auth_ctx, &outpkt[(EAPOL_EAP_HDRS_OFFSET + sizeof(*hdr))], sizeof(outpkt) - (EAPOL_EAP_HDRS_OFFSET + sizeof(*hdr)));\n-\tif (len_B < 0)\n-\t\treturn -1;\n+\tsize_t len_B = librist_crypto_srp_authenticator_write_B_bytes(ctx->auth_ctx, &outpkt[(EAPOL_EAP_HDRS_OFFSET + sizeof(*hdr))], sizeof(outpkt) - (EAPOL_EAP_HDRS_OFFSET + sizeof(*hdr)));\n \tctx->last_identifier++;\n-\treturn send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_REQUEST, ctx->last_identifier, (sizeof(struct eap_srp_hdr) + (size_t)len_B), outpkt, eap_tx_version(ctx));\n+\tint ret = send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_REQUEST, ctx->last_identifier, (sizeof(struct eap_srp_hdr) + len_B), outpkt, ctx->eapversion3? 3 :2);\n+\treturn ret;\n }\n \n static int process_eap_response_client_validator(struct eapsrp_ctx *ctx, size_t len, uint8_t pkt[])\n@@ -760,34 +472,20 @@ static int process_eap_response_client_validator(struct eapsrp_ctx *ctx, size_t\n \n \tif (!ctx->auth_ctx) {\n \t\tctx->authentication_state = EAP_AUTH_STATE_FAILED;\n-\t\treturn EAP_INTERNALERR;\n+\t\treturn -254;\n \t}\n \n \tif (librist_crypto_srp_authenticator_verify_m1(ctx->auth_ctx, ctx->config.username, &pkt[4]) != 0) {\n-\t\tchar ubuf[256];\n-\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_WARN, EAP_LOG_PREFIX\"Authentication failed for %s@%s\\n\", eap_sanitize_log(ctx->config.username, ubuf, sizeof(ubuf)), ctx->ip_string);\n-\t\t/* Advisory hint, latched: see srp-compat note in NEWS for v0.2.18. */\n-\t\tif (!ctx->srp_legacy_peer_warned) {\n-\t\t\tctx->srp_legacy_peer_warned = true;\n-\t\t\tif (ctx->srp_legacy_pad) {\n-\t\t\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_INFO,\n-\t\t\t\t\tEAP_LOG_PREFIX\"  Hint: this side is configured with srp-compat=1. If %s is running librist 0.2.16+ (PAD-compliant, the default), drop ?srp-compat=1 on both sides.\\n\",\n-\t\t\t\t\tctx->ip_string);\n-\t\t\t} else {\n-\t\t\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_INFO,\n-\t\t\t\t\tEAP_LOG_PREFIX\"  Hint: if %s is running librist 0.2.15 or earlier, the SRP wire format changed in 0.2.16 for RFC 5054 / TR-06-2 compliance. To interoperate with an older peer, add ?srp-compat=1 on BOTH URLs. Otherwise check the password.\\n\",\n-\t\t\t\t\tctx->ip_string);\n-\t\t\t}\n-\t\t}\n+\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_WARN, EAP_LOG_PREFIX\"Authentication failed for %s@%s\\n\", ctx->config.username, ctx->ip_string);\n \t\tctx->authentication_state = EAP_AUTH_STATE_FAILED;\n-\t\teap_tries_inc(ctx);\n-\t\tint ret = EAP_AUTH_FAILED;\n+\t\tctx->tries++;\n+\t\tint ret = -254;\n \t\tif (ctx->tries > EAP_AUTH_RETRY_MAX) {\n \t\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_ERROR, EAP_LOG_PREFIX\"Authentication retry count exceeded\\n\");\n-\t\t\tret = EAP_AUTH_TERMINATED;\n+\t\t\tret = -255;\n \t\t}\n \t\tuint8_t buf[EAPOL_EAP_HDRS_OFFSET];\n-\t\tsend_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_FAILURE, ctx->last_identifier, 0, buf, eap_tx_version(ctx));\n+\t\tsend_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_FAILURE, ctx->last_identifier, 0, buf, ctx->eapversion3? 3 :2);\n \t\teap_reset_data(ctx);\n \t\treturn ret;\n \t}\n@@ -809,17 +507,15 @@ static int process_eap_response_client_validator(struct eapsrp_ctx *ctx, size_t\n \t}\n \tlibrist_crypto_srp_authenticator_write_M2_bytes(ctx->auth_ctx, &outpkt[(EAPOL_EAP_HDRS_OFFSET + sizeof(*hdr) + 4)]);\n \tctx->last_identifier++;\n-\treturn send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_REQUEST, ctx->last_identifier, (sizeof(*hdr) + 4 + DIGEST_LENGTH), outpkt, eap_tx_version(ctx));\n+\treturn send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_REQUEST, ctx->last_identifier, (sizeof(*hdr) + 4 + DIGEST_LENGTH), outpkt, ctx->eapversion3? 3 :2);\n }\n \n static int process_eap_response_srp_server_validator(struct eapsrp_ctx *ctx)\n {\n \tif (ctx->authenticated)\n \t{\n-\t\tif (ctx->authentication_state < EAP_AUTH_STATE_SUCCESS) {\n-\t\t\tchar ubuf[256];\n-\t\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_INFO, EAP_LOG_PREFIX\"Successfully authenticated %s@%s\\n\", eap_sanitize_log(ctx->config.username, ubuf, sizeof(ubuf)), ctx->ip_string);\n-\t\t}\n+\t\tif (ctx->authentication_state < EAP_AUTH_STATE_SUCCESS)\n+\t\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_INFO, EAP_LOG_PREFIX\"Successfully authenticated %s@%s\\n\", ctx->config.username, ctx->ip_string);\n \n \t\tif (ctx->config.use_key_as_passphrase && ctx->eapversion3)\n \t\t\tctx->may_rollover_passphrase = true;\n@@ -827,108 +523,34 @@ static int process_eap_response_srp_server_validator(struct eapsrp_ctx *ctx)\n \t\tctx->authentication_state = EAP_AUTH_STATE_SUCCESS;\n \t\tctx->last_auth_timestamp = timestampNTP_u64();\n \t\tctx->tries = 0;\n-\t\tctx->failed_state_timestamp = 0;\n \t\tctx->last_identifier++;\n \t}\n \treturn 0;\n }\n \n-static int process_eap_response_passphrase(struct eapsrp_ctx *ctx, uint8_t identifier, size_t len, uint8_t pkt[], uint8_t eap_version) {\n-\tif (ctx->authentication_state != EAP_AUTH_STATE_SUCCESS)\n+static int process_eap_response_passphrase(struct eapsrp_ctx *ctx, uint8_t identifier, size_t len, uint8_t pkt[]) {\n+\tif (ctx->authentication_state != EAP_AUTH_STATE_SUCCESS)//We cannot process it now,\n \t\treturn 0;\n \n-\t/* If a solicited passphrase request is outstanding, only accept a\n-\t * response whose identifier matches the request.  This prevents\n-\t * replay or spoofed RESPONSEs from hijacking an active exchange.\n-\t *\n-\t * When no request is active the RESPONSE is an unsolicited\n-\t * passphrase push (the protocol explicitly allows this — see\n-\t * rist_eap_send_passphrase).  Its payload is encrypted under the SRP\n-\t * session key: v3 AES-CTR (confidentiality only) or, when both peers\n-\t * are v4, AES-256-GCM (integrity + authenticity + replay protection). */\n-\tbool matches_request = (ctx->passphrase_request_timer != 0 &&\n-\t                        identifier == ctx->passphrase_request_identifier);\n-\tif (ctx->passphrase_request_timer != 0 && !matches_request)\n-\t\treturn 0;\n-\n-\tif (len < 1)\n-\t\treturn EAP_LENERR;\n \tbool use_derived_key = CHECK_BIT(pkt[0], 7);\n-\tconst uint8_t *skey = (ctx->config.role == EAP_ROLE_AUTHENTICATOR)\n-\t\t? librist_crypto_srp_authenticator_get_key(ctx->auth_ctx)\n-\t\t: librist_crypto_srp_client_get_key(ctx->client_ctx);\n-\t/* Dispatch on the NEGOTIATED session version, never on the per-packet\n-\t * EAPOL version byte (cleartext, attacker-controlled): once v4 is\n-\t * negotiated, downgraded frames (v3 CTR, no integrity) and the U-bit\n-\t * derived-key install (no verification at all) are refused. */\n-\tbool session_v4 = eap_use_v4(ctx);\n-\tbool installed = false;\n+\tconst uint8_t *key = NULL;\n+\tif (ctx->config.role == EAP_ROLE_AUTHENTICATOR)\n+\t\tkey = librist_crypto_srp_authenticator_get_key(ctx->auth_ctx);\n+\telse\n+\t\tkey = librist_crypto_srp_client_get_key(ctx->client_ctx);\n \tif (use_derived_key) {\n-\t\tif (session_v4)\n-\t\t\treturn 0;\n-\t\tinstalled = (librist_peer_update_rx_passphrase(ctx->peer, skey, SHA256_DIGEST_LENGTH, matches_request) == 0);\n-\t} else if (session_v4) {\n-\t\tif (eap_version < 4)\n-\t\t\treturn 0;\n-\t\t/* v4 layout after the SRP header: [flags][nonce 12][ciphertext..][tag 16] */\n-\t\tif (len < (size_t)(1 + EAP_V4_NONCE_LEN + EAP_V4_TAG_LEN))\n-\t\t\treturn EAP_LENERR;\n-\t\tsize_t ct_len = len - 1 - EAP_V4_NONCE_LEN - EAP_V4_TAG_LEN;\n-\t\tconst uint8_t *noncep = &pkt[1];\n-\t\tconst uint8_t *ct = noncep + EAP_V4_NONCE_LEN;\n-\t\tconst uint8_t *tag = ct + ct_len;\n-\t\tuint8_t plain[EAP_V4_MAX_PLAINTEXT];\n-\t\tif (ct_len == 0 || ct_len > sizeof(plain))\n-\t\t\treturn EAP_LENERR;\n-\t\tuint8_t key[32];\n-\t\tbool c2s = (ctx->config.role == EAP_ROLE_AUTHENTICATOR); //we receive client->server\n-\t\tif (eap_v4_dir_key(ctx, c2s, key) != 0)\n-\t\t\treturn EAP_INTERNALERR;\n-\t\tuint8_t aad[EAP_V4_AAD_LEN];\n-\t\teap_v4_build_aad(aad, identifier, pkt[0], noncep);\n-\t\tif (_librist_crypto_aes_gcm_decrypt(key, noncep, EAP_V4_NONCE_LEN,\n-\t\t\taad, sizeof(aad), ct, ct_len, tag, EAP_V4_TAG_LEN, plain) != 0) {\n-\t\t\t/* Tag failed: drop without ACK so the sender retransmits; never\n-\t\t\t * install unverified material (plain already zeroed by decrypt). */\n-\t\t\t_librist_crypto_secure_zero(key, sizeof(key));\n-\t\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_WARN,\n-\t\t\t\tEAP_LOG_PREFIX\"v4 passphrase authentication tag failed; dropping\\n\");\n-\t\t\treturn 0;\n-\t\t}\n-\t\tuint64_t rx_nonce = eap_v4_decode_nonce(noncep);\n-\t\t/* Strictly-increasing nonces only; a byte-identical retransmit is\n-\t\t * re-ACKed without reinstalling. */\n-\t\tif (!ctx->rx_nonce_seen || rx_nonce > ctx->rx_last_nonce) {\n-\t\t\tctx->rx_last_nonce = rx_nonce;\n-\t\t\tctx->rx_nonce_seen = true;\n-\t\t\tinstalled = (librist_peer_update_rx_passphrase(ctx->peer, plain, ct_len, matches_request) == 0);\n-\t\t\tif (installed)\n-\t\t\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_DEBUG,\n-\t\t\t\t\tEAP_LOG_PREFIX\"v4 passphrase verified and installed (AES-256-GCM, nonce=%\" PRIu64 \")\\n\",\n-\t\t\t\t\trx_nonce);\n-\t\t} else {\n-\t\t\tinstalled = true;\n-\t\t}\n-\t\t_librist_crypto_secure_zero(key, sizeof(key));\n-\t\t_librist_crypto_secure_zero(plain, sizeof(plain));\n+\t\tlibrist_peer_update_rx_passphrase(ctx->peer, key, SHA256_DIGEST_LENGTH, ctx->passphrase_request_timer != 0 && identifier == ctx->passphrase_request_identifier);\n \t} else {\n \t\tbool aes_256 = CHECK_BIT(pkt[0], 6);\n \t\tuint8_t iv[16] = {0};\n \t\tiv[15] = identifier;\n-\t\t_librist_crypto_aes_ctr(skey, aes_256? 256: 128, iv, &pkt[1], &pkt[1], len -1);\n-\t\tinstalled = (librist_peer_update_rx_passphrase(ctx->peer, &pkt[1], len-1, matches_request) == 0);\n+\t\t_librist_crypto_aes_ctr(key, aes_256? 256: 128, iv, &pkt[1], &pkt[1], len -1);\n+\t\tlibrist_peer_update_rx_passphrase(ctx->peer, &pkt[1], len-1, ctx->passphrase_request_timer != 0 && identifier == ctx->passphrase_request_identifier);\n \t}\n \tuint8_t buf[EAPOL_EAP_HDRS_OFFSET];\n \tif (ctx->passphrase_request_timer)\n \t\tctx->passphrase_request_timer = 0;\n-\tif (!installed) {\n-\t\t/* The key slot refused the passphrase (e.g. oversize): answer\n-\t\t * FAILURE, never confirm a rotation that did not happen. */\n-\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_WARN,\n-\t\t\tEAP_LOG_PREFIX\"passphrase install failed; answering FAILURE\\n\");\n-\t\treturn send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_FAILURE, identifier, 0, buf, eap_tx_version(ctx));\n-\t}\n-\treturn send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_SUCCESS, identifier, 0, buf, eap_tx_version(ctx));\n+\treturn send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_SUCCESS, identifier, 0, buf, ctx->eapversion3? 3 :2);\n }\n \n static int eap_request_passphrase(struct eapsrp_ctx *ctx, bool start) {\n@@ -945,40 +567,23 @@ static int eap_request_passphrase(struct eapsrp_ctx *ctx, bool start) {\n \thdr->subtype = EAP_SRP_SUBTYPE_PASSWORD_REQUEST_RESPONSE;\n \tctx->passphrase_request_times++;\n \tctx->passphrase_request_timer = timestampNTP_u64();\n-\treturn send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_REQUEST, ctx->passphrase_request_identifier, sizeof(*hdr), outpkt, eap_tx_version(ctx));\n+\treturn send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_REQUEST, ctx->passphrase_request_identifier, sizeof(*hdr), outpkt, ctx->eapversion3? 3 :2);\n }\n \n static int process_eap_response(struct eapsrp_ctx *ctx, uint8_t pkt[], size_t len, uint8_t identifier, uint8_t eap_version)\n {\n-\tif (len < 1)\n-\t\treturn EAP_LENERR;\n \tuint8_t type = pkt[0];\n-\t/* Invalidate the retransmit cache only for a response matching the\n-\t * in-flight exchange identifier; junk responses (spoofed or stale)\n-\t * must not flush it, otherwise a spoofer blasting one response per\n-\t * retransmit period starves the cache and stalls the handshake\n-\t * indefinitely. */\n-\tif (identifier == ctx->last_identifier) {\n-\t\tctx->timeout_retries = 0;\n-\t\tfree(ctx->last_pkt);\n-\t\tctx->last_pkt_size = 0;\n-\t\tctx->last_pkt = NULL;\n-\t}\n+\tctx->timeout_retries = 0;\n+\tfree(ctx->last_pkt);\n+\tctx->last_pkt_size = 0;\n+\tctx->last_pkt = NULL;\n \tif (type == EAP_TYPE_IDENTITY) {\n-\t\t/* Only an authenticator should ever process IDENTITY RESPONSE; on the\n-\t\t * authenticatee side ctx->config.lookup_func is NULL (set up via\n-\t\t * calloc + role/username/password assignment in rist_enable_eap_srp_2)\n-\t\t * and process_eap_response_identity would dereference it. */\n-\t\tif (ctx->config.role != EAP_ROLE_AUTHENTICATOR || !ctx->config.lookup_func)\n-\t\t\treturn EAP_UNEXPECTEDRESPONSE;\n \t\tif (identifier != ctx->last_identifier)\n \t\t\treturn EAP_WRONGIDENTIFIER;\n \t\treturn process_eap_response_identity(ctx, (len -1), &pkt[1], eap_version);\n \t}\n \tif (type == EAP_TYPE_SRP_SHA1)\n \t{\n-\t\tif (len < 2)\n-\t\t\treturn EAP_LENERR;\n \t\tuint8_t subtype = pkt[1];\n \n \t\tif (subtype != EAP_SRP_SUBTYPE_PASSWORD_REQUEST_RESPONSE && ctx->config.role == EAP_ROLE_AUTHENTICATEE)\n@@ -1003,7 +608,7 @@ static int process_eap_response(struct eapsrp_ctx *ctx, uint8_t pkt[], size_t le\n \t\t\t\t//handle SRP lw rechallenge\n \t\t\t\tbreak;\n \t\t\tcase EAP_SRP_SUBTYPE_PASSWORD_REQUEST_RESPONSE:\n-\t\t\t\treturn process_eap_response_passphrase(ctx, identifier, (len -2), &pkt[2], eap_version);\n+\t\t\t\treturn process_eap_response_passphrase(ctx, identifier, (len -2), &pkt[2]);\n \t\t\t\tbreak;\n \t\t\tdefault:\n \t\t\t\treturn EAP_SRP_WRONGSUBTYPE;\n@@ -1030,12 +635,7 @@ static int process_eap_pkt(struct eapsrp_ctx *ctx, uint8_t pkt[], size_t len, ui\n \tif (ctx == NULL)\n \t\treturn -1;\n \tif (ctx->authentication_state == EAP_AUTH_STATE_FAILED && ctx->tries >EAP_AUTH_RETRY_MAX)\n-\t\treturn EAP_AUTH_TERMINATED;\n-\tif (len < sizeof(struct eap_hdr))\n-\t\treturn EAP_LENERR;\n-\t/* Record the peer's advertised EAP version for v4 negotiation. */\n-\tif (eap_version > ctx->peer_eap_version)\n-\t\tctx->peer_eap_version = eap_version;\n+\t\treturn -255;\n \tstruct eap_hdr *hdr = (struct eap_hdr *)pkt;\n \tuint8_t code = hdr->code;\n \tuint8_t identifier = hdr->identifier;\n@@ -1055,22 +655,8 @@ static int process_eap_pkt(struct eapsrp_ctx *ctx, uint8_t pkt[], size_t len, ui\n \t\t\treturn process_eap_succes(ctx, identifier);\n \t\t\tbreak;\n \t\tcase EAP_CODE_FAILURE:\n-\t\t\t/* Drop FAILUREs whose identifier doesn't match the in-flight exchange. */\n-\t\t\tif (identifier != ctx->last_identifier) {\n-\t\t\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_DEBUG,\n-\t\t\t\t\tEAP_LOG_PREFIX\"Dropping FAILURE with stale identifier %u (expected %u)\\n\",\n-\t\t\t\t\tidentifier, ctx->last_identifier);\n-\t\t\t\treturn 0;\n-\t\t\t}\n-\t\t\t// rate-limit, otherwise a spoofed FAILURE can loop us forever\n-\t\t\teap_tries_inc(ctx);\n \t\t\teap_reset_data(ctx);\n \t\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_ERROR, EAP_LOG_PREFIX\"Authentication failed\\n\");\n-\t\t\tif (ctx->tries > EAP_AUTH_RETRY_MAX) {\n-\t\t\t\tctx->authentication_state = EAP_AUTH_STATE_FAILED;\n-\t\t\t\tctx->failed_state_timestamp = timestampNTP_u64();\n-\t\t\t\treturn EAP_AUTH_TERMINATED;\n-\t\t\t}\n \t\t\treturn _librist_proto_eap_start(ctx);//try to restart the process\n \t\tdefault:\n \t\t\treturn -1;\n@@ -1082,83 +668,27 @@ int eap_request_identity(struct eapsrp_ctx *ctx)\n {\n \tuint8_t outpkt[EAPOL_EAP_HDRS_OFFSET +1];\n \toutpkt[EAPOL_EAP_HDRS_OFFSET] = EAP_TYPE_IDENTITY;\n-\tuint32_t id_rand;\n-\tif (_librist_crypto_random_u32(&id_rand) != 0)\n-\t\treturn -1;\n-\tctx->last_identifier = (uint8_t)(id_rand >> 24);\n-\treturn send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_REQUEST, ctx->last_identifier, 1, outpkt, eap_tx_version(ctx));\n+\tctx->last_identifier = (uint8_t)(prand_u32() >> 24);\n+\treturn send_eapol_pkt(ctx, EAPOL_TYPE_EAP, EAP_CODE_REQUEST, ctx->last_identifier, 1, outpkt, ctx->eapversion3? 3 :2);\n }\n \n-/* EAPOL is exempt from encryption, so START still reaches a peer that has\n- * lost our session key (e.g. a restarted authenticator). */\n-static int eap_send_start_pkt(struct eapsrp_ctx *ctx)\n+int _librist_proto_eap_start(struct eapsrp_ctx *ctx)\n {\n+\tif (ctx->authentication_state == EAP_AUTH_STATE_SUCCESS)\n+\t\tctx->authentication_state = EAP_AUTH_STATE_REAUTH;\n \tstruct eapol_hdr eapol;\n \teapol.eapversion = 3;\n \teapol.eaptype = EAPOL_TYPE_START;\n-\teapol.length = 0;\n+\teapol.length = htobe16(sizeof(eapol));\n \tif (_librist_proto_gre_send_data(ctx->peer, 0, RIST_GRE_PROTOCOL_TYPE_EAPOL, (uint8_t*)&eapol, sizeof(eapol), 0, 0, ctx->peer->rist_gre_version) < 0)\n \t\treturn -1;\n \treturn 0;\n }\n \n-int _librist_proto_eap_start(struct eapsrp_ctx *ctx)\n-{\n-\tif (ctx->authentication_state == EAP_AUTH_STATE_SUCCESS)\n-\t\tctx->authentication_state = EAP_AUTH_STATE_REAUTH;\n-\tint ret = eap_send_start_pkt(ctx);\n-\t/* Authenticatees have no other periodic START driver; arm the bounded\n-\t * retransmit (eap_periodic_impl). Inbound EAP re-arms it. */\n-\tif (ctx->config.role == EAP_ROLE_AUTHENTICATEE) {\n-\t\tctx->authee_start_timer = timestampNTP_u64();\n-\t\tctx->authee_start_tries = 0;\n-\t}\n-\treturn ret;\n-}\n-\n-/* Force an authenticatee (caller/supplicant) to re-run the full SRP\n- * handshake from scratch and re-initiate it right now.\n- *\n- * Used after a caller socket rebind (NAT source-port change or a peer\n- * restart): the previously authenticated session is bound to the old\n- * source tuple and to per-session crypto the far end no longer has (a\n- * restarted listener has forgotten everything). Keeping the stale\n- * SUCCESS state would leave us sending data the authenticator drops as\n- * unauthenticated, and the authenticator-driven re-auth never fires for\n- * a peer it does not know about. So drop all per-session state back to\n- * UNAUTH and send a fresh EAPOL START, exactly as on a cold connect, so\n- * the authenticator challenges us immediately on the new socket.\n- *\n- * Authenticator contexts are left untouched (re-auth there is driven by\n- * eap_periodic and the listener-side reassociation path). */\n-void eap_reset_authenticatee(struct eapsrp_ctx *ctx)\n-{\n-\tif (ctx == NULL)\n-\t\treturn;\n-\tpthread_mutex_lock(&ctx->eap_lock);\n-\tif (ctx->config.role == EAP_ROLE_AUTHENTICATEE) {\n-\t\teap_reset_data(ctx);        /* frees client crypto, authenticated=false */\n-\t\tctx->authentication_state = EAP_AUTH_STATE_UNAUTH;\n-\t\tctx->tries = 0;             /* clear any parked failure count */\n-\t\tctx->timeout_retries = 0;\n-\t\tctx->failed_state_timestamp = 0;\n-\t\tctx->last_identity_reply_timestamp = 0; /* don't rate-limit the fresh handshake */\n-\t\tctx->last_identifier = 0;\n-\t\tctx->did_first_auth = false;/* install the new session key as a first auth */\n-\t\tif (_librist_proto_eap_start(ctx) < 0)\n-\t\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_WARN,\n-\t\t\t\tEAP_LOG_PREFIX\"Failed to send EAPOL START after socket rebind; \"\n-\t\t\t\t\"periodic retransmit will retry\\n\");\n-\t}\n-\tpthread_mutex_unlock(&ctx->eap_lock);\n-}\n-\n void eap_set_ip_string(struct eapsrp_ctx *ctx, char ip_string[])\n {\n-\tif (ctx != NULL) {\n-\t\tstrncpy(ctx->ip_string, ip_string, sizeof(ctx->ip_string) - 1);\n-\t\tctx->ip_string[sizeof(ctx->ip_string) - 1] = '\\0';\n-\t}\n+\tif (ctx != NULL)\n+\t\tmemcpy(ctx->ip_string, ip_string, 46);\n }\n \n int eap_clone_ctx(struct eapsrp_ctx *in, struct rist_peer *peer)\n@@ -1171,12 +701,9 @@ int eap_clone_ctx(struct eapsrp_ctx *in, struct rist_peer *peer)\n \tif (!ctx)\n \t\treturn -1;\n \n-\tif (pthread_mutex_init(&ctx->eap_lock, NULL) != 0) {\n-\t\tfree(ctx);\n+\tif (pthread_mutex_init(&ctx->eap_lock, NULL) != 0)\n \t\treturn -1;\n-\t}\n     memcpy(&ctx->config, &in->config, sizeof(in->config));\n-\tctx->srp_legacy_pad = in->srp_legacy_pad;\n \tpeer->eap_ctx = ctx;\n \tctx->peer = peer;\n \tctx->eapversion3 = true;\n@@ -1198,13 +725,10 @@ void eap_delete_ctx(struct eapsrp_ctx **in)\n int eap_process_eapol(struct eapsrp_ctx* ctx, uint8_t pkt[], size_t len)\n {\n \tassert(ctx != NULL);\n-\tif (len < sizeof(struct eapol_hdr))\n-\t\treturn EAP_LENERR;\n \tstruct eapol_hdr *hdr = (struct eapol_hdr *)pkt;\n \tuint8_t eap_version = hdr->eapversion;\n \tsize_t body_len = be16toh(hdr->length);\n-\t// the on-the-wire body_len must fit inside what we actually received\n-\tif (body_len + sizeof(struct eapol_hdr) > len)\n+\tif ((body_len +4) < (len))\n \t\treturn EAP_LENERR;\n \n \tpthread_mutex_lock(&ctx->eap_lock);\n@@ -1215,50 +739,21 @@ int eap_process_eapol(struct eapsrp_ctx* ctx, uint8_t pkt[], size_t len)\n \t\t\tret = process_eap_pkt(ctx, &pkt[sizeof(*hdr)], body_len, eap_version);\n \t\t\tbreak;\n \t\tcase EAPOL_TYPE_START:\n-\t\t\t/* START (re)drives the handshake; only an authenticatee sends it.\n-\t\t\t * Always re-challenge with IDENTITY and reset the retransmit\n-\t\t\t * bookkeeping so a caller recovering from a restart or rebind is\n-\t\t\t * driven anew rather than ignored for a stale last_pkt. Do NOT\n-\t\t\t * reset tries: it is the brute-force cap, and START is reachable\n-\t\t\t * pre-auth, so clearing it here would hand an attacker unlimited\n-\t\t\t * online password guesses (and unlimited server-side modexps). */\n-\t\t\tif (ctx->config.role == EAP_ROLE_AUTHENTICATOR) {\n-\t\t\t\tif (ctx->authentication_state == EAP_AUTH_STATE_SUCCESS)\n-\t\t\t\t\tctx->authentication_state = EAP_AUTH_STATE_REAUTH;\n-\t\t\t\tctx->timeout_retries = 0;\n-\t\t\t\tret = eap_request_identity(ctx);\n-\t\t\t} else {\n+\t\t\tif (ctx->config.role == EAP_ROLE_AUTHENTICATOR && !ctx->last_pkt)\n+\t\t\t\tret =  eap_request_identity(ctx);\n+\t\t\tif (ctx->config.role == EAP_ROLE_AUTHENTICATOR && ctx->authentication_state == EAP_AUTH_STATE_SUCCESS) {\n+\t\t\t\tctx->authentication_state = EAP_AUTH_STATE_REAUTH;\n+                ret = eap_request_identity(ctx);\n+            } else\n \t\t\t\tret = 0;\n-\t\t\t}\n \t\t\tbreak;\n \t\tcase EAPOL_TYPE_LOGOFF:\n-\t\t\t/* Refuse LOGOFF once authenticated; re-auth runs from eap_periodic.\n-\t\t\t * A soft-FAILED ctx stays FAILED: walking it back to UNAUTH from\n-\t\t\t * the wire bypasses the brute-force kill path, and clearing tries\n-\t\t\t * would hand out unlimited online guesses. Recovery stays with\n-\t\t\t * the quiet-period path in eap_periodic. */\n-\t\t\tif (ctx->authentication_state >= EAP_AUTH_STATE_SUCCESS) {\n-\t\t\t\tret = EAP_UNEXPECTEDREQUEST;\n-\t\t\t\tbreak;\n-\t\t\t}\n-\t\t\tif (ctx->authentication_state != EAP_AUTH_STATE_FAILED)\n-\t\t\t\tctx->authentication_state = EAP_AUTH_STATE_UNAUTH;\n+\t\t\tctx->authentication_state = EAP_AUTH_STATE_UNAUTH;\n \t\t\tret = 0;\n \t\t\tbreak;\n \t\tdefault:\n \t\t\tbreak;\n \t}\n-\t/* Far end is engaged: while still handshaking (UNAUTH) push the START\n-\t * retransmit timer out so it only fires on true silence; once\n-\t * authenticated, disarm it. */\n-\tif (ctx->config.role == EAP_ROLE_AUTHENTICATEE) {\n-\t\tif (ctx->authentication_state == EAP_AUTH_STATE_UNAUTH) {\n-\t\t\tctx->authee_start_timer = timestampNTP_u64();\n-\t\t\tctx->authee_start_tries = 0;\n-\t\t} else {\n-\t\t\tctx->authee_start_timer = 0;\n-\t\t}\n-\t}\n \tpthread_mutex_unlock(&ctx->eap_lock);\n \treturn ret;\n }\n@@ -1279,47 +774,6 @@ static void eap_periodic_impl(struct eapsrp_ctx *ctx)\n \tuint64_t now = timestampNTP_u64();\n \tuint64_t retry_period = EAP_AUTH_TIMEOUT * RIST_CLOCK;\n \tuint64_t reauth_period = EAP_REAUTH_PERIOD * RIST_CLOCK;//3 seconds\n-\n-\t/* Authenticator-restart recovery: once the identity-request probe armed by\n-\t * process_eap_request_identity has persisted one retry period, drop to UNAUTH\n-\t * and re-run the SRP handshake (bounds a lone forged packet to one re-auth). */\n-\tif (ctx->config.role == EAP_ROLE_AUTHENTICATEE &&\n-\t    ctx->authentication_state >= EAP_AUTH_STATE_SUCCESS &&\n-\t    ctx->reauth_probe_timer != 0 &&\n-\t    now > ctx->reauth_probe_timer + retry_period) {\n-\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_INFO,\n-\t\t\tEAP_LOG_PREFIX\"Authenticator re-requested identity while authenticated \"\n-\t\t\t\"(it likely restarted); re-authenticating\\n\");\n-\t\tctx->reauth_probe_timer = 0;\n-\t\teap_reset_data(ctx);\n-\t\tctx->authentication_state = EAP_AUTH_STATE_UNAUTH;\n-\t\tctx->tries = 0;\n-\t\tctx->timeout_retries = 0;\n-\t\tctx->failed_state_timestamp = 0;\n-\t\tctx->last_identity_reply_timestamp = 0;\n-\t\tctx->did_first_auth = false;\n-\t\tif (_librist_proto_eap_start(ctx) < 0)\n-\t\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_WARN,\n-\t\t\t\tEAP_LOG_PREFIX\"Failed to send EAPOL START for re-auth; \"\n-\t\t\t\t\"periodic retransmit will retry\\n\");\n-\t}\n-\n-\t/* Retransmit START while stuck UNAUTH and the authenticator is silent\n-\t * (e.g. a restarted listener that missed our first START). Bounded; when\n-\t * exhausted the caller-side socket rebind re-arms us on its next cycle. */\n-\tif (ctx->config.role == EAP_ROLE_AUTHENTICATEE &&\n-\t    ctx->authentication_state == EAP_AUTH_STATE_UNAUTH &&\n-\t    ctx->authee_start_timer != 0 &&\n-\t    now > ctx->authee_start_timer + retry_period) {\n-\t\tif (ctx->authee_start_tries >= EAP_AUTH_TIMEOUT_RETRY_MAX) {\n-\t\t\tctx->authee_start_timer = 0;\n-\t\t} else {\n-\t\t\teap_send_start_pkt(ctx);\n-\t\t\tctx->authee_start_timer = now;\n-\t\t\tctx->authee_start_tries++;\n-\t\t}\n-\t}\n-\n \tif (ctx->authentication_state == EAP_AUTH_STATE_SUCCESS && ctx->passphrase_request_timer != 0 && ctx->passphrase_request_timer + retry_period < now) {\n \t\tif (ctx->passphrase_request_times > EAP_AUTH_TIMEOUT_RETRY_MAX) {\n \t\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_WARN, EAP_LOG_PREFIX\"Failed to receive requested passphrase in a timely manner\\n\");\n@@ -1335,8 +789,7 @@ static void eap_periodic_impl(struct eapsrp_ctx *ctx)\n \t\t\tctx->unsollicited_passphrase_response_timer = 0;\n \t\t\tctx->unsollicited_passphrase_state = EAP_PASSPHRASE_STATE_FAILED;\n \t\t} else {\n-\t\t\t/* Retransmit reuses the cached nonce so a v4 payload is byte-identical. */\n-\t\t\teap_srp_send_password(ctx, ctx->unsollicited_passphrase_response_identifier, ctx->unsollicited_passphrase, ctx->unsollicited_passphrase_len, ctx->unsollicited_passphrase_nonce);\n+\t\t\teap_srp_send_password(ctx, ctx->unsollicited_passphrase_response_identifier, ctx->unsollicited_passphrase, ctx->unsollicited_passphrase_len);\n \t\t\tctx->unsollicited_passphrase_response_timer = timestampNTP_u64();\n \t\t\tctx->unsollicited_passphrase_response_times++;\n \t\t}\n@@ -1356,7 +809,7 @@ static void eap_periodic_impl(struct eapsrp_ctx *ctx)\n \t{\n \t\tif (ctx->last_pkt)\n \t\t{\n-\t\t\trist_transport_sendto(ctx->peer, ctx->last_pkt, ctx->last_pkt_size, 0);\n+\t\t\tsendto(ctx->peer->sd, (const char *)ctx->last_pkt, ctx->last_pkt_size, 0, &ctx->peer->u.address, ctx->peer->address_len);\n \t\t\t//check\n \t\t\tctx->timeout_retries++;\n \t\t\tctx->last_timestamp = now;\n@@ -1386,19 +839,6 @@ static void eap_periodic_impl(struct eapsrp_ctx *ctx)\n \t\tctx->authentication_state = EAP_AUTH_STATE_UNAUTH;\n \t\treturn;\n \t}\n-\t/* Soft-FAILED (tries bumped past max but not parked at PERMANENT) -> UNAUTH after quiet. */\n-\tif (ctx->authentication_state == EAP_AUTH_STATE_FAILED &&\n-\t    ctx->tries > EAP_AUTH_RETRY_MAX &&\n-\t    ctx->tries < EAP_AUTH_TRIES_PERMANENT &&\n-\t    ctx->failed_state_timestamp != 0 &&\n-\t    now > ctx->failed_state_timestamp + (uint64_t)EAP_AUTH_FAILED_RECOVERY * RIST_CLOCK) {\n-\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_INFO,\n-\t\t\tEAP_LOG_PREFIX\"Recovered from soft FAILED state after %d ms of quiet\\n\",\n-\t\t\tEAP_AUTH_FAILED_RECOVERY);\n-\t\tctx->tries = 0;\n-\t\tctx->failed_state_timestamp = 0;\n-\t\tctx->authentication_state = EAP_AUTH_STATE_UNAUTH;\n-\t}\n }\n void eap_periodic(struct eapsrp_ctx *ctx) {\n \tif (ctx == NULL)\n@@ -1434,8 +874,6 @@ static void internal_user_verifier_lookup(char * username,\n #if HAVE_MBEDTLS\n \t\tdecoded_verifier = malloc(ctx->authenticator_len_verifier_old);\n \t\tdecoded_salt = malloc(ctx->authenticator_len_salt_old);\n-\t\tif (!decoded_verifier || !decoded_salt)\n-\t\t\tgoto fail_decode;\n \t\tmemcpy(decoded_verifier, ctx->authenticator_bytes_verifier_old, ctx->authenticator_len_verifier_old);\n \t\tmemcpy(decoded_salt, ctx->authenticator_bytes_salt_old, ctx->authenticator_len_salt_old);\n \t\tlookup_data->verifier_len = ctx->authenticator_len_verifier_old;\n@@ -1444,8 +882,6 @@ static void internal_user_verifier_lookup(char * username,\n \t} else {\n \t\tdecoded_verifier = malloc(ctx->authenticator_len_verifier);\n \t\tdecoded_salt = malloc(ctx->authenticator_len_salt);\n-\t\tif (!decoded_verifier || !decoded_salt)\n-\t\t\tgoto fail_decode;\n \t\tmemcpy(decoded_verifier, ctx->authenticator_bytes_verifier, ctx->authenticator_len_verifier);\n \t\tmemcpy(decoded_salt, ctx->authenticator_bytes_salt, ctx->authenticator_len_salt);\n \t\tlookup_data->verifier_len = ctx->authenticator_len_verifier;\n@@ -1488,9 +924,9 @@ int rist_enable_eap_srp_2(struct rist_peer *peer, const char *username, const ch\n \tif ((peer->listening && !peer->multicast_receiver) || peer->multicast_sender)\n \t{\n \t\tstruct eapsrp_ctx *ctx = calloc(1, sizeof(*ctx));\n+\t\tctx->config.logging_settings = get_cctx(peer)->logging_settings;\n \t\tif (ctx == NULL)\n \t\t\treturn RIST_ERR_MALLOC;\n-\t\tctx->config.logging_settings = get_cctx(peer)->logging_settings;\n \t\tif (pthread_mutex_init(&ctx->eap_lock, NULL) != 0) {\n \t\t\tfree(ctx);\n \t\t\treturn RIST_ERR_MALLOC;\n@@ -1530,10 +966,6 @@ int rist_enable_eap_srp_2(struct rist_peer *peer, const char *username, const ch\n \t\tctx->config.role = EAP_ROLE_AUTHENTICATOR;\n \t\tctx->config.use_key_as_passphrase = peer->key_tx.password_len == 0;\n \t\tctx->eapversion3 = true;\n-\t\tctx->srp_legacy_pad = (peer->config.srp_compat_legacy != 0);\n-\t\tif (ctx->srp_legacy_pad)\n-\t\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_WARN,\n-\t\t\t\tEAP_LOG_PREFIX\"SRP legacy compat mode ACTIVE on this authenticator (srp-compat=1). Wire format is the pre-0.2.16 unpadded form — NOT TR-06-2 / RFC 5054 compliant. For transitional interop only.\\n\");\n \t\tpeer->eap_ctx = ctx;\n \t\tstruct rist_peer *child = peer->child;\n \t\tpeer->eap_authentication_state = 1;\n@@ -1563,12 +995,8 @@ int rist_enable_eap_srp_2(struct rist_peer *peer, const char *username, const ch\n \tctx->config.role = EAP_ROLE_AUTHENTICATEE;\n \tstrcpy(ctx->config.username, username);\n \tstrcpy(ctx->config.password, password);\n-\tctx->srp_legacy_pad = (peer->config.srp_compat_legacy != 0);\n \tpeer->eap_ctx = ctx;\n \trist_log_priv2(ctx->config.logging_settings, RIST_LOG_INFO, EAP_LOG_PREFIX\"EAP Authentication enabled, role = authenticatee\\n\");\n-\tif (ctx->srp_legacy_pad)\n-\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_WARN,\n-\t\t\tEAP_LOG_PREFIX\"SRP legacy compat mode ACTIVE on this client (srp-compat=1). Wire format is the pre-0.2.16 unpadded form — NOT TR-06-2 / RFC 5054 compliant. For transitional interop only.\\n\");\n \tctx->eapversion3 = true;\n \tif (!peer->multicast_receiver)\n \t\t_librist_proto_eap_start(ctx);\n@@ -1608,26 +1036,8 @@ bool rist_eap_may_rollover_tx(struct eapsrp_ctx *ctx) {\n \n void rist_eap_send_passphrase(struct eapsrp_ctx *ctx, const char *passphrase) {\n \tpthread_mutex_lock(&ctx->eap_lock);\n-\t/* Sending pre-auth would derive direction keys from a crypto ctx that\n-\t * does not exist yet (NULL) and install nothing meaningful; the peer\n-\t * version byte can even be pre-armed by a spoofed packet. Require a\n-\t * completed authentication. */\n-\tif (ctx->authentication_state != EAP_AUTH_STATE_SUCCESS) {\n-\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_ERROR,\n-\t\t\tEAP_LOG_PREFIX\"Passphrase push refused: not authenticated\\n\");\n-\t\tpthread_mutex_unlock(&ctx->eap_lock);\n-\t\treturn;\n-\t}\n-\tsize_t plen = strlen(passphrase);\n-\tif (plen > sizeof(ctx->unsollicited_passphrase)) {\n-\t\trist_log_priv2(ctx->config.logging_settings, RIST_LOG_ERROR,\n-\t\t\tEAP_LOG_PREFIX\"Passphrase too long (%zu > %zu), dropping\\n\",\n-\t\t\tplen, sizeof(ctx->unsollicited_passphrase));\n-\t\tpthread_mutex_unlock(&ctx->eap_lock);\n-\t\treturn;\n-\t}\n-\tctx->unsollicited_passphrase_len = plen;\n-\tmemcpy(ctx->unsollicited_passphrase, passphrase, plen);\n+\tctx->unsollicited_passphrase_len = strlen(passphrase);\n+\tmemcpy(ctx->unsollicited_passphrase, passphrase, ctx->unsollicited_passphrase_len);\n \tctx->unsollicited_passphrase_response_timer = timestampNTP_u64();\n \tctx->unsollicited_passphrase_response_times = 1;\n \tctx->unsollicited_passphrase_response_identifier++;\n@@ -1635,8 +1045,6 @@ void rist_eap_send_passphrase(struct eapsrp_ctx *ctx, const char *passphrase) {\n \tif (ctx->config.role == EAP_ROLE_AUTHENTICATOR)\n \t\tSET_BIT(ctx->unsollicited_passphrase_response_identifier, 7);\n \tUNSET_BIT(ctx->unsollicited_passphrase_response_identifier, 6);\n-\t/* Assign a fresh nonce for this new passphrase; retransmits reuse it. */\n-\tctx->unsollicited_passphrase_nonce = ctx->tx_nonce_counter++;\n-    eap_srp_send_password(ctx, ctx->unsollicited_passphrase_response_identifier, ctx->unsollicited_passphrase, ctx->unsollicited_passphrase_len, ctx->unsollicited_passphrase_nonce);\n+    eap_srp_send_password(ctx, ctx->unsollicited_passphrase_response_identifier, ctx->unsollicited_passphrase, ctx->unsollicited_passphrase_len);\n     pthread_mutex_unlock(&ctx->eap_lock);\n }\ndiff --git a/src/proto/eap.h b/src/proto/eap.h\nindex 45421d5..bd8e9a3 100644\n--- a/src/proto/eap.h\n+++ b/src/proto/eap.h\n@@ -79,14 +79,11 @@ RIST_PACKED_STRUCT(eap_srp_hdr, {\n #define EAP_PASSPHRASE_STATE_SUCCESS 1\n #define EAP_PASSPHRASE_STATE_FAILED 2\n \n-#define EAP_LENERR              -1\n-#define EAP_WRONGIDENTIFIER     -2\n-#define EAP_UNEXPECTEDRESPONSE  -3\n-#define EAP_UNEXPECTEDREQUEST   -4\n-#define EAP_SRP_WRONGSUBTYPE    -5  /* was -4: collided with EAP_UNEXPECTEDREQUEST */\n-#define EAP_INTERNALERR         -6  /* crypto context / state precondition missing */\n-#define EAP_AUTH_FAILED         -7  /* one authentication attempt failed */\n-#define EAP_AUTH_TERMINATED     -8  /* permanent: retries exhausted, drop the peer */\n+#define EAP_LENERR -1\n+#define EAP_WRONGIDENTIFIER -2\n+#define EAP_UNEXPECTEDRESPONSE -3\n+#define EAP_UNEXPECTEDREQUEST -4\n+#define EAP_SRP_WRONGSUBTYPE -4\n \n struct eapsrp_ctx;\n RIST_PRIV int eap_process_eapol(struct eapsrp_ctx* ctx, uint8_t pkt[], size_t len);\n@@ -101,5 +98,4 @@ RIST_PRIV bool rist_eap_password_sending_done(struct eapsrp_ctx *ctx);\n RIST_PRIV bool rist_eap_may_rollover_tx(struct eapsrp_ctx *ctx);\n RIST_PRIV void rist_eap_send_passphrase(struct eapsrp_ctx *ctx, const char *passphrase);\n RIST_PRIV int _librist_proto_eap_start(struct eapsrp_ctx *ctx);\n-RIST_PRIV void eap_reset_authenticatee(struct eapsrp_ctx *ctx);\n #endif\ndiff --git a/src/proto/gre.c b/src/proto/gre.c\nindex e2b81e0..c2c1a3e 100644\n--- a/src/proto/gre.c\n+++ b/src/proto/gre.c\n@@ -9,7 +9,6 @@\n #include \"rist-private.h\"\n #include \"endian-shim.h\"\n #include \"udp-private.h\"\n-#include \"transport-private.h\"\n #include \"eap.h\"\n #include \"peer.h\"\n \n@@ -138,25 +137,9 @@ ssize_t _librist_proto_gre_send_data(struct rist_peer *p, uint8_t payload_type,\n \tssize_t ret;\n \tint errorcode = 0;\n \n-\t/* Type 8 wrapping: when both sides have negotiated Advanced Profile,\n-\t * wrap outbound GRE packets in an AP Type 8 envelope so the remote\n-\t * can accept them on a unified AP port. */\n-\tif (get_cctx(p)->profile == RIST_PROFILE_ADVANCED &&\n-\t    p->is_advanced && p->remote_supports_advanced) {\n-\t\tuint8_t gre_flat[RIST_MAX_PACKET_SIZE];\n-\t\tsize_t gre_total = hdr_len + payload_len;\n-\t\tif (gre_total <= sizeof(gre_flat)) {\n-\t\t\tmemcpy(gre_flat, hdr_buf, hdr_len);\n-\t\t\tmemcpy(gre_flat + hdr_len, payload_wr, payload_len);\n-\t\t\tret = (rist_adv_send_type8(p, gre_flat, gre_total) == 0)\n-\t\t\t      ? (ssize_t)gre_total : -1;\n-\t\t\tif (modifying_payload)\n-\t\t\t\tfree(payload_wr);\n-\t\t\treturn ret;\n-\t\t}\n-\t}\n-\n+\t//TODO: abstract this away\n #ifndef _WIN32\n+\t//TODO: this is POSIX only: add windows equivalent\n \tstruct msghdr msghdr;\n \tstruct iovec iov[2];\n \tiov[0].iov_base = hdr_buf;\n@@ -170,9 +153,10 @@ ssize_t _librist_proto_gre_send_data(struct rist_peer *p, uint8_t payload_type,\n \tmsghdr.msg_control = NULL;\n \tmsghdr.msg_controllen = 0;\n \tmsghdr.msg_flags = 0;\n+\t// retry when kernel buffer is full instead of dropping packet (EAGAIN)\n \tint retries = 0;\n \tdo {\n-\t\tret = rist_transport_sendmsg(p, &msghdr, MSG_DONTWAIT);\n+\t\tret = sendmsg(p->sd, &msghdr, MSG_DONTWAIT);\n \t\tif (RIST_UNLIKELY(ret < 0)) {\n \t\t\terrorcode = errno;\n \t\t\tretries++;\n@@ -214,8 +198,8 @@ ssize_t _librist_proto_gre_send_data(struct rist_peer *p, uint8_t payload_type,\n \t}\n \n \tif (RIST_UNLIKELY(errorcode)) {\n-\t\t_librist_log_send_error(p, errorcode, (size_t)(hdr_len + payload_len),\n-\t\t                        \"main-profile sendmsg\");\n+\t\tstruct rist_common_ctx *ctx = get_cctx(p);\n+        rist_log_priv(ctx, RIST_LOG_ERROR, \"Send failed: errno=%d, reason=%s, ret=%d, socket=%d, retries=%d\\n\", errorcode, strerror(errorcode), ret, p->sd, retries);\n \t} else if (RIST_UNLIKELY(retries > RIST_MAX_SEND_RETRIES / 5)) {\n         struct rist_common_ctx *ctx = get_cctx(p);\n         rist_log_priv(ctx, RIST_LOG_WARN, \"Send Succeded after retries=%d, ret=%d, socket=%d\\n\", retries, ret, p->sd);\n@@ -225,31 +209,17 @@ ssize_t _librist_proto_gre_send_data(struct rist_peer *p, uint8_t payload_type,\n }\n \n void _librist_proto_gre_send_keepalive(struct rist_peer *p, uint8_t gre_version) {\n-\tuint8_t ka_buf[12] = {0};\n-\tstruct rist_gre_keepalive *ka = (struct rist_gre_keepalive *)ka_buf;\n-\tmemcpy(ka->mac_array, p->mac_addr, sizeof(ka->mac_array));\n-\tSET_BIT(ka->capabilities1, 0); // Null packet deletion\n-\tif (p->sender_ctx && p->sender_ctx->split_mode != LIBRIST_SPLIT_MODE_OFF)\n-\t\tSET_BIT(ka->capabilities1, 1); // Pair-split active\n-\tSET_BIT(ka->capabilities1, 2); // SMPTE-2022-7\n-\tSET_BIT(ka->capabilities1, 5); // Bonding\n-\tSET_BIT(ka->capabilities2, 5); // Reduced overhead\n-\n-\tstruct rist_common_ctx *cctx = get_cctx(p);\n-\tsize_t ka_len = sizeof(struct rist_gre_keepalive);\n-\tif (cctx && cctx->profile == RIST_PROFILE_ADVANCED) {\n-\t\t/* TR-06-3 Section 5.3.6: I bit (bit 31 of 32-bit capabilities) */\n-\t\tka_buf[8]  = 0x80; /* I bit = bit 7 of capabilities3 (= bit 31 overall) */\n-\t\tka_buf[9]  = 0;\n-\t\tka_buf[10] = 0;\n-\t\tka_buf[11] = 0;\n-\t\tka_len = 12;\n-\t}\n-\t_librist_proto_gre_send_data(p, 0, RIST_GRE_PROTOCOL_TYPE_KEEPALIVE, ka_buf, ka_len, 0, 0, gre_version);\n+\tstruct rist_gre_keepalive ka = {0};\n+\tmemcpy(ka.mac_array, p->mac_addr, sizeof(ka.mac_array));\n+\tSET_BIT(ka.capabilities1, 0); // Null packet deletion\n+\tSET_BIT(ka.capabilities1, 2); // SMPTE-2022-7\n+\tSET_BIT(ka.capabilities1, 5); // Bonding\n+\t//SET_BIT(ka.capabilities2, 3);//OTF Passphrase change\n+\tSET_BIT(ka.capabilities2, 5);//Reduced overhead\n+\t_librist_proto_gre_send_data(p, 0, RIST_GRE_PROTOCOL_TYPE_KEEPALIVE, (uint8_t *)&ka, sizeof(ka), 0, 0, gre_version);\n }\n \n int _librist_proto_gre_parse_keepalive(const uint8_t buf[], size_t buflen, struct rist_keepalive_info  *info) {\n-\tmemset(info, 0, sizeof(*info));\n \tif (buflen < sizeof(struct rist_gre_keepalive)) {\n \t\treturn -1;\n \t}\n@@ -269,18 +239,9 @@ int _librist_proto_gre_parse_keepalive(const uint8_t buf[], size_t buflen, struc\n \tinfo->v = CHECK_BIT(ka->capabilities2, 5);\n \tinfo->j = CHECK_BIT(ka->capabilities2, 4);\n \tinfo->f = CHECK_BIT(ka->capabilities2, 3);\n-\t/* TR-06-3 extended capabilities (4 bytes at offset 8, if present) */\n-\tif (buflen >= sizeof(*ka) + 4) {\n-\t\tinfo->adv_i = !!(buf[sizeof(*ka)]     & 0x80); /* bit 31 → I */\n-\t\tinfo->adv_g = !!(buf[sizeof(*ka)]     & 0x40); /* bit 30 → G */\n-\t\tinfo->adv_c = !!(buf[sizeof(*ka)]     & 0x20); /* bit 29 → C */\n-\t\tinfo->json_len = buflen - sizeof(*ka) - 4;\n-\t\tif (info->json_len > 0)\n-\t\t\tinfo->json = (const char *)&buf[sizeof(*ka) + 4];\n-\t} else {\n-\t\tinfo->json_len = buflen - sizeof(*ka);\n-\t\tif (info->json_len > 0)\n-\t\t\tinfo->json = (const char *)&buf[sizeof(*ka)];\n+\tinfo->json_len = buflen - sizeof(*ka);\n+\tif (info->json_len > 0) {\n+\t\tinfo->json = (const char *)&buf[sizeof(*ka)];\n \t}\n \treturn 0;\n }\ndiff --git a/src/proto/protocol_gre.h b/src/proto/protocol_gre.h\nindex 5ee71c0..26763ab 100644\n--- a/src/proto/protocol_gre.h\n+++ b/src/proto/protocol_gre.h\n@@ -23,10 +23,6 @@ struct rist_keepalive_info {\n \tbool v : 1;\n \tbool j : 1;\n \tbool f : 1;\n-\t/* TR-06-3 extended capability bits (bytes 8-11 of keep-alive) */\n-\tbool adv_i : 1;  /* I bit: Advanced Profile capable */\n-\tbool adv_g : 1;  /* G bit: GRE key rotation capable */\n-\tbool adv_c : 1;  /* C bit: Compression capable */\n \tsize_t json_len;\n \tconst char *json;\n };\ndiff --git a/src/proto/rist_time.c b/src/proto/rist_time.c\nindex 34aaa3c..d9c9666 100644\n--- a/src/proto/rist_time.c\n+++ b/src/proto/rist_time.c\n@@ -53,6 +53,18 @@ uint64_t timestampNTP_RTC_u64(void) {\n   return t;\n }\n \n+uint32_t timestampRTP_u32(int advanced, uint64_t i_ntp) {\n+  if (!advanced) {\n+    i_ntp *= RTP_PTYPE_MPEGTS_CLOCKHZ;\n+    i_ntp = i_ntp >> 32;\n+    return (uint32_t)i_ntp;\n+  } else {\n+    // We just need the middle 32 bits, i.e. 65536Hz clock\n+    i_ntp = i_ntp >> 16;\n+    return (uint32_t)i_ntp;\n+  }\n+}\n+\n uint64_t convertRTPtoNTP(uint8_t ptype, uint32_t time_extension,\n                          uint32_t i_rtp) {\n   uint64_t i_ntp;\n@@ -82,14 +94,8 @@ uint64_t convertRTPtoNTP(uint8_t ptype, uint32_t time_extension,\n uint64_t calculate_rtt_delay(uint64_t request, uint64_t response,\n                              uint32_t delay) {\n   /* both request and response are NTP timestamps, delay is in microseconds */\n-  if (request > response)\n-    return 0;\n   uint64_t rtt = response - request;\n-  if (RIST_UNLIKELY(delay)) {\n-    uint64_t delay_ticks = (((uint64_t)delay) << 32) / 1000000;\n-    if (delay_ticks > rtt)\n-      return 0;\n-    rtt -= delay_ticks;\n-  }\n+  if (RIST_UNLIKELY(delay))\n+    rtt -= (((uint64_t)delay) << 32) / 1000000;\n   return rtt;\n }\ndiff --git a/src/proto/rist_time.h b/src/proto/rist_time.h\nindex e3a3877..e243e52 100644\n--- a/src/proto/rist_time.h\n+++ b/src/proto/rist_time.h\n@@ -2,7 +2,6 @@\n #define RIST_TIME_H\n \n #include \"common/attributes.h\"\n-#include \"proto/rtp.h\"\n \n #include <stdint.h>\n /* Time conversion */\n@@ -14,34 +13,8 @@\n \n RIST_PRIV uint64_t timestampNTP_u64(void);\n RIST_PRIV uint64_t timestampNTP_RTC_u64(void);\n+RIST_PRIV uint32_t timestampRTP_u32(int advanced, uint64_t i_ntp);\n RIST_PRIV uint64_t convertRTPtoNTP(uint8_t ptype, uint32_t time_extension, uint32_t i_rtp);\n RIST_PRIV uint64_t calculate_rtt_delay(uint64_t request, uint64_t response, uint32_t delay);\n \n-/* Convert a 64-bit NTP timestamp (1 s = 2^32) to a 32-bit RTP timestamp.\n- * advanced picks the clock: the Advanced Profile 1 MHz clock (TR-06-3\n- * Section 5.2.1; RIST_ADV_CLOCK_HZ) or the MPEG-TS 90 kHz clock. Scaling by\n- * the clock rate and shifting down 32 bits gives the tick count; the uint32\n- * cast wraps as RTP timestamps do. Header-inline so it can be unit-tested\n- * without dragging in the clock_gettime plumbing of this module. */\n-static inline uint32_t timestampRTP_u32(int advanced, uint64_t i_ntp)\n-{\n-  uint64_t hz = advanced ? 1000000ULL : (uint64_t)RTP_PTYPE_MPEGTS_CLOCKHZ;\n-  return (uint32_t)((i_ntp * hz) >> 32);\n-}\n-\n-/* Convert the 64-bit NTP form (1 s = 2^32) to nanoseconds or microseconds. The\n- * halves are converted separately because the 1900 epoch offset in the upper 32\n- * bits makes (i_ntp * 1000000000) >> 32 overflow a uint64. */\n-static inline uint64_t timestampNTP_to_ns(uint64_t i_ntp)\n-{\n-  return (i_ntp >> 32) * 1000000000ULL +\n-         (((i_ntp & 0xFFFFFFFFULL) * 1000000000ULL) >> 32);\n-}\n-\n-static inline uint64_t timestampNTP_to_us(uint64_t i_ntp)\n-{\n-  return (i_ntp >> 32) * 1000000ULL +\n-         (((i_ntp & 0xFFFFFFFFULL) * 1000000ULL) >> 32);\n-}\n-\n #endif /* RIST_TIME_H */\ndiff --git a/src/proto/rtp.c b/src/proto/rtp.c\nindex cb99195..61c3658 100644\n--- a/src/proto/rtp.c\n+++ b/src/proto/rtp.c\n@@ -67,22 +67,22 @@ void rist_rtcp_write_sr(uint8_t *buf, int *offset,\n void rist_rtcp_write_sdes(uint8_t *buf, int *offset,\n                                         const char *name,\n                                         const uint32_t flow_id) {\n-  size_t namelen = strnlen(name, RIST_MAX_STRING_SHORT - 1);\n-  if (namelen > 255)\n-    namelen = 255;\n+  size_t namelen = strlen(name);\n   size_t sdes_size = ((10 + namelen + 1) + 3) & ~3;\n   size_t padding = sdes_size - namelen - 10;\n   struct rist_rtcp_sdes_pkt *sdes =\n       (struct rist_rtcp_sdes_pkt *)(buf + RIST_MAX_PAYLOAD_OFFSET + *offset);\n   *offset += sdes_size;\n+  /* Populate SDES for sender description */\n   sdes->rtcp.flags = RTCP_SDES_FLAGS;\n   sdes->rtcp.ptype = PTYPE_SDES;\n   sdes->rtcp.len = htons((uint16_t)((sdes_size - 1) >> 2));\n   sdes->rtcp.ssrc = htobe32(flow_id);\n   sdes->cname = 1;\n   sdes->name_len = (uint8_t)namelen;\n-  memcpy(sdes->udn, name, namelen);\n-  memset(sdes->udn + namelen, 0, padding);\n+  // We copy the extra padding bytes from the source because it is a\n+  // preallocated buffer of size 128 with all zeroes\n+  memcpy(sdes->udn, name, namelen + padding);\n }\n \n void rist_rtcp_write_echoreq(uint8_t *buf, int *offset,\n@@ -134,6 +134,5 @@ void rist_rtcp_write_xr_echoreq(uint8_t *buf, int *offset,\n   peer->last_sender_report_ts = now;\n   block->ntp_msw = htobe32((uint32_t)(now >> 32));\n   block->ntp_lsw = htobe32((uint32_t)(now & 0x000000000FFFFFFFF));\n-  // RFC 3550: length is (32-bit words in header+payload) - 1\n-  xr_hdr->len = htobe16(((sizeof(*xr_hdr) + sizeof(*block)) / 4) - 1);\n+  xr_hdr->len = htobe16(1 + sizeof(*block) / 4);\n }\ndiff --git a/src/proto/rtp.h b/src/proto/rtp.h\nindex 91cf3aa..c63fef9 100644\n--- a/src/proto/rtp.h\n+++ b/src/proto/rtp.h\n@@ -99,6 +99,8 @@ RTCP Control Channel:\n \n #define NACK_FMT_BITMASK 1\n #define NACK_FMT_RANGE 0\n+#define NACK_FMT_SEQEXT 1\n+\n #define ECHO_REQUEST 2\n #define ECHO_RESPONSE 3\n \n@@ -109,6 +111,7 @@ RTCP Control Channel:\n #define RTCP_SDES_FLAGS 0x81\n #define RTCP_NACK_RANGE_FLAGS 0x80\n #define RTCP_NACK_BITMASK_FLAGS 0x81\n+#define RTCP_NACK_SEQEXT_FLAGS 0x81\n #define RTCP_ECHOEXT_REQ_FLAGS 0x82\n #define RTCP_ECHOEXT_RESP_FLAGS 0x83\n \n@@ -170,6 +173,16 @@ RIST_PACKED_STRUCT(rist_rtcp_nack_bitmask,{\n \tuint32_t ssrc;\n })\n \n+RIST_PACKED_STRUCT(rist_rtcp_seqext,{\n+\tuint8_t flags;\n+\tuint8_t ptype;\n+\tuint16_t len;\n+\tuint32_t ssrc;\n+\tuint8_t  name[4];\n+\tuint16_t seq_msb;\n+\tuint16_t reserved0;\n+})\n+\n RIST_PACKED_STRUCT(rist_rtcp_echoext, {\n \tuint8_t flags;\n \tuint8_t ptype;\ndiff --git a/src/rist-adv-ts.h b/src/rist-adv-ts.h\ndeleted file mode 100644\nindex d6e4a4f..0000000\n--- a/src/rist-adv-ts.h\n+++ /dev/null\n@@ -1,137 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Advanced Profile (TR-06-3) receive-side source-clock reconstruction.\n- *\n- * The Advanced Profile carries a 32-bit RTP timestamp on a 1 MHz clock\n- * (Section 5.2.1), i.e. microseconds that wrap every ~71.6 min. To dejitter\n- * off the sender's timeline (instead of local arrival), the receiver rebuilds\n- * a monotonic 64-bit NTP source clock from that field: accumulate wrap-safe\n- * signed deltas (good for reorders/gaps up to +-35 min) scaled us -> NTP\n- * ticks (x 2^32/1e6, exact as x2^26/15625, overflow-safe).\n- *\n- * By default the receiver assumes a conformant clock and dejitters off it\n- * immediately, exactly like the Main profile. The catch is our own history:\n- * librist releases before this one emitted this timestamp on a broken clock\n- * (65536x too fast; see the sender fix), so a receiver may be fed by a sender\n- * still running that code. There is no negotiated signal for it, so over the\n- * first window the receiver also checks that the rebuilt clock advances at\n- * real-time rate. A conformant clock matches arrival elapsed within jitter; a\n- * broken one is off by orders of magnitude, and the peer then falls back to\n- * arrival timing. The fallback is re-tried every window, not latched for the\n- * session: a sender that recovers (or an attack burst that stops) costs one\n- * window of dejitter, not the rest of the session. */\n-\n-#ifndef RIST_ADV_TS_H\n-#define RIST_ADV_TS_H\n-\n-#include <stdbool.h>\n-#include <stdint.h>\n-\n-struct rist_adv_ts_state {\n-\tbool seeded;         /* first live packet has anchored the clock */\n-\tbool calibrating;    /* still verifying the source clock rate */\n-\tbool use_source;     /* post-verdict: source clock kept, else arrival */\n-\tuint32_t last;       /* last wire 1 MHz timestamp accumulated */\n-\tuint64_t accum;      /* reconstructed source time, NTP ticks (1 s = 2^32) */\n-\tuint64_t seed_now;   /* arrival at the window anchor */\n-\tuint64_t deadline;   /* seed_now + window: when the verdict is (re)taken */\n-\tuint64_t recon_anchor; /* accum at the window anchor (fallback re-verdict) */\n-};\n-\n-/* Reconstruct the source time (NTP ticks) for one Advanced data packet.\n- *   wire_ts   : the packet's 32-bit 1 MHz RTP timestamp\n- *   now       : local arrival time (NTP ticks), also the fallback value\n- *   window    : the source clock is used immediately; over this span its rate\n- *               is verified and, if it fails, the peer falls back to arrival\n- *               (re-verified every window thereafter)\n- *   advance   : true for an in-order live packet (advances the clock);\n- *               false for a retransmit (position only, no state change)\n- * Returns the source time to stamp the packet with. */\n-static inline uint64_t\n-rist_adv_ts_reconstruct(struct rist_adv_ts_state *st, uint32_t wire_ts,\n-\t\t\tuint64_t now, uint64_t window, bool advance)\n-{\n-\t/* Only a live packet can anchor the clock; until then there is no\n-\t * reference, so stamp arrival. */\n-\tif (!st->seeded) {\n-\t\tif (!advance)\n-\t\t\treturn now;\n-\t\tst->seeded = true;\n-\t\tst->calibrating = true;\n-\t\tst->last = wire_ts;\n-\t\tst->accum = now;\n-\t\tst->seed_now = now;\n-\t\tst->deadline = now + window;\n-\t\tst->recon_anchor = st->accum;\n-\t\treturn now;\n-\t}\n-\n-\t/* Advance the rebuilt clock by the wire delta (wrap-safe signed 32-bit),\n-\t * scaled us -> NTP ticks. */\n-\tint64_t d = (int32_t)(wire_ts - st->last);\n-\tuint64_t recon = st->accum + (uint64_t)((d * 67108864LL) / 15625LL);\n-\n-\tif (st->calibrating) {\n-\t\t/* Assume a conformant clock and dejitter off it from the start,\n-\t\t * like the Main profile. A retransmit only positions. Do not\n-\t\t * re-anchor here: a broken clock must be allowed to diverge so the\n-\t\t * verdict can see it. */\n-\t\tif (!advance)\n-\t\t\treturn recon;\n-\t\tst->accum = recon;\n-\t\tst->last = wire_ts;\n-\t\tif (now < st->deadline)\n-\t\t\treturn recon;\n-\t\t/* Verdict: over a full window a conformant clock has advanced at\n-\t\t * real-time rate, so reconstructed elapsed matches arrival elapsed\n-\t\t * within jitter. A broken clock (the 65536x-fast one an older\n-\t\t * release emitted) misses by far more; fall back to arrival and\n-\t\t * keep re-trying the verdict every window. */\n-\t\tst->calibrating = false;\n-\t\tuint64_t src_elapsed = st->accum - st->seed_now;\n-\t\tuint64_t arr_elapsed = now - st->seed_now;\n-\t\tuint64_t drift = src_elapsed > arr_elapsed\n-\t\t\t\t ? src_elapsed - arr_elapsed\n-\t\t\t\t : arr_elapsed - src_elapsed;\n-\t\tst->use_source = drift <= (arr_elapsed >> 1);\n-\t\tst->recon_anchor = st->accum;\n-\t\tst->seed_now = now;\n-\t\tst->deadline = now + window;\n-\t\treturn st->use_source ? recon : now;\n-\t}\n-\n-\tif (!st->use_source) {\n-\t\t/* Arrival fallback. Keep tracking the wire clock and re-run the\n-\t\t * verdict each window; if the source clock advances at real-time\n-\t\t * rate again, resume dejittering off it. A retransmit only\n-\t\t * positions (stamped arrival anyway). */\n-\t\tif (!advance)\n-\t\t\treturn now;\n-\t\tst->accum = recon;\n-\t\tst->last = wire_ts;\n-\t\tif (now >= st->deadline) {\n-\t\t\tuint64_t src_elapsed = st->accum - st->recon_anchor;\n-\t\t\tuint64_t arr_elapsed = now - st->seed_now;\n-\t\t\tuint64_t drift = src_elapsed > arr_elapsed\n-\t\t\t\t\t ? src_elapsed - arr_elapsed\n-\t\t\t\t\t : arr_elapsed - src_elapsed;\n-\t\t\tst->use_source = drift <= (arr_elapsed >> 1);\n-\t\t\tst->recon_anchor = st->accum;\n-\t\t\tst->seed_now = now;\n-\t\t\tst->deadline = now + window;\n-\t\t}\n-\t\treturn now;\n-\t}\n-\n-\tif (!advance)\n-\t\treturn recon; /* retransmit: position only, no state change */\n-\n-\t/* Verified: trust the source clock like the Main profile. A sender clock\n-\t * discontinuity is followed, not snapped to arrival (snapping would inject\n-\t * worse jitter than the jump itself). */\n-\tst->accum = recon;\n-\tst->last = wire_ts;\n-\treturn recon;\n-}\n-\n-#endif /* RIST_ADV_TS_H */\ndiff --git a/src/rist-bandwidth-guard.h b/src/rist-bandwidth-guard.h\ndeleted file mode 100644\nindex fc83ac8..0000000\n--- a/src/rist-bandwidth-guard.h\n+++ /dev/null\n@@ -1,53 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Whether a sender peer has any retransmission budget left to work with, and\n- * whether it is time to say so again.  Kept in a small standalone header so\n- * both can be unit-tested without a peer or a running sender.\n- */\n-\n-#ifndef RIST_BANDWIDTH_GUARD_H\n-#define RIST_BANDWIDTH_GUARD_H\n-\n-#include <stdbool.h>\n-#include <stddef.h>\n-#include <stdint.h>\n-\n-/* Is the retransmission budget structurally exhausted?\n- *\n- * The bandwidth ceiling (?bandwidth=, recovery_maxbitrate) is not a limit on\n- * payload: rist_retry_dequeue() refuses a retransmission whenever payload plus\n- * retransmit bitrate would cross it, so what the ceiling really bounds is the\n- * two together. Set it at or below the payload rate and the budget is nil --\n- * every NACK is refused for as long as the stream runs, and the only trace is a\n- * debug-level line nobody has enabled. What the operator sees is a link that\n- * never recovers a lost packet, which reads as a bad network rather than a bad\n- * ceiling, so it is worth saying out loud.\n- *\n- * payload and ceiling share one unit (bits per second). A ceiling of 0 means\n- * \"unset\" everywhere it can still be seen -- init_peer_settings() replaces it\n- * with the default before a peer runs -- so there is nothing to have exceeded. */\n-static inline bool\n-rist_retransmit_budget_starved(size_t payload_bps, size_t ceiling_bps)\n-{\n-\tif (!ceiling_bps)\n-\t\treturn false;\n-\treturn payload_bps >= ceiling_bps;\n-}\n-\n-/* Rate-limit for the warning above, which is evaluated once per stats interval\n- * and would otherwise repeat every second for the life of a misconfigured\n- * stream. Still repeats, because a single line at startup scrolls away long\n- * before anyone goes looking for why nothing recovers.\n- *\n- * last is when it was last emitted (0 if never) and shares one unit with now.\n- * A now behind last emits, which is the harmless reading of a clock that moved\n- * backwards. */\n-static inline bool\n-rist_bandwidth_warn_due(uint64_t last, uint64_t interval, uint64_t now)\n-{\n-\tif (!last || now < last)\n-\t\treturn true;\n-\treturn (now - last) >= interval;\n-}\n-\n-#endif /* RIST_BANDWIDTH_GUARD_H */\ndiff --git a/src/rist-common.c b/src/rist-common.c\nindex 8866fee..3afb51e 100755\n--- a/src/rist-common.c\n+++ b/src/rist-common.c\n@@ -9,13 +9,10 @@\n \n #include \"proto/gre.h\"\n #include \"rist-private.h\"\n-#include <limits.h>\n #include \"log-private.h\"\n #include \"crypto/psk.h\"\n-#include <lz4.h>\n #include \"crypto/random.h\"\n #include \"udp-private.h\"\n-#include \"transport-private.h\"\n #include \"udpsocket.h\"\n #include \"endian-shim.h\"\n #include \"time-shim.h\"\n@@ -32,7 +29,6 @@\n #include \"rist_ref.h\"\n #include \"config.h\"\n #include \"rist-thread.h\"\n-#include \"rist-nack-select.h\"\n #include \"peer.h\"\n #include <stdbool.h>\n #include \"stdio-shim.h\"\n@@ -68,12 +64,8 @@ int parse_url_udp_options(const char* url, struct rist_udp_config *output_udp_co\n \tquery = strchr( tmp_url, '/' );\n \tif (query != NULL) {\n \t\tprefix_len = (uint32_t)(query - tmp_url);\n-\t\t/* clamp copy length and terminator to prefix[]; prefix_len may be 0 */\n-\t\tsize_t prefix_copy = prefix_len > 0 ? (size_t)(prefix_len - 1) : 0;\n-\t\tif (prefix_copy > sizeof(output_udp_config->prefix) - 1)\n-\t\t\tprefix_copy = sizeof(output_udp_config->prefix) - 1;\n-\t\tmemcpy((void *)output_udp_config->prefix, tmp_url, prefix_copy);\n-\t\toutput_udp_config->prefix[prefix_copy] = '\\0';\n+\t\tstrncpy((void *)output_udp_config->prefix, tmp_url, prefix_len >= 16 ? 15 : prefix_len - 1);\n+\t\toutput_udp_config->prefix[prefix_len] = '\\0';\n \t\t// Convert to lower\n \t\tchar *p =(char *)output_udp_config->prefix;\n \t\tfor(i = 0; i < 16; i++)\n@@ -100,20 +92,6 @@ int parse_url_udp_options(const char* url, struct rist_udp_config *output_udp_co\n \n \t\t\tif (strcmp( url_params[i].key, RIST_URL_PARAM_MIFACE ) == 0) {\n \t\t\t\tstrncpy((void *)output_udp_config->miface, val, RIST_MAX_STRING_SHORT - 1);\n-\t\t\t} else if (strcmp(url_params[i].key, RIST_URL_PARAM_MCAST_TTL) == 0) {\n-\t\t\t\tint temp = atoi(val);\n-\t\t\t\tif (temp > 0 && temp <= 255)\n-\t\t\t\t\toutput_udp_config->multicast_ttl = (uint32_t)temp;\n-\t\t\t} else if (output_udp_config->version >= 2 &&\n-\t\t\t           strcmp(url_params[i].key, RIST_URL_PARAM_CBR_OUTPUT) == 0) {\n-\t\t\t\tif (strcmp(val, \"0\") && strcmp(val, \"1\")) {\n-\t\t\t\t\tret = -1;\n-\t\t\t\t} else {\n-\t\t\t\t\toutput_udp_config->cbr_output = atoi(val);\n-\t\t\t\t\toutput_udp_config->cbr_output_set = 1;\n-\t\t\t\t}\n-\t\t\t} else if (strcmp(url_params[i].key, RIST_URL_PARAM_MCAST_SOURCE) == 0) {\n-\t\t\t\tstrncpy((void *)output_udp_config->multicast_source, val, RIST_MAX_STRING_LONG - 1);\n \t\t\t} else if (strcmp( url_params[i].key, RIST_URL_PARAM_STREAM_ID ) == 0) {\n \t\t\t\tint temp = atoi( val );\n \t\t\t\tif (temp > 0)\n@@ -126,15 +104,15 @@ int parse_url_udp_options(const char* url, struct rist_udp_config *output_udp_co\n \t\t\t\tint temp = atoi( val );\n \t\t\t\tif (temp >= 0)\n \t\t\t\t\toutput_udp_config->rtp_sequence = (uint16_t)temp;\n-\t\t\t} else if (output_udp_config->version >= 1 && strcmp( url_params[i].key, RIST_URL_PARAM_RTP_OUTPUT_PTYPE) == 0) {\n+\t\t\t} else if (output_udp_config->version == 1 && strcmp( url_params[i].key, RIST_URL_PARAM_RTP_OUTPUT_PTYPE) == 0) {\n \t\t\t\tint temp = atoi( val );\n \t\t\t\tif (temp >= 0)\n \t\t\t\t\toutput_udp_config->rtp_ptype = (uint8_t)temp;\n-\t\t\t} else if (output_udp_config->version >= 1 && strcmp( url_params[i].key, RIST_URL_PARAM_MULTIPLEX_MODE) == 0) {\n+\t\t\t} else if (output_udp_config->version == 1 && strcmp( url_params[i].key, RIST_URL_PARAM_MULTIPLEX_MODE) == 0) {\n \t\t\t\tint temp = atoi( val );\n \t\t\t\tif (temp >= 0)\n \t\t\t\t\toutput_udp_config->multiplex_mode = (uint8_t)temp;\n-/*UNUSED TODO} else if (output_udp_config->version >= 1 && strcmp( url_params[i].key, RIST_URL_PARAM_MULTIPLEX_FILTER) == 0) {\n+/*UNUSED TODO} else if (output_udp_config->version == 1 && strcmp( url_params[i].key, RIST_URL_PARAM_MULTIPLEX_FILTER) == 0) {\n \t\t\t\tstrncpy((void *)output_udp_config->multiplex_filter, val, RIST_MAX_STRING_SHORT -1); */\n \t\t\t} else {\n \t\t\t\tret = -1;\n@@ -178,9 +156,7 @@ int parse_url_options(const char* url, struct rist_peer_config *output_peer_conf\n \n \t\t\tif (strcmp( url_params[i].key, RIST_URL_PARAM_BUFFER_SIZE ) == 0) {\n \t\t\t\tint temp = atoi( val );\n-\t\t\t\t/* 0 is meaningless as a recovery buffer and divides by zero\n-\t\t\t\t * in init_peer_settings (SIGFPE at peer creation). */\n-\t\t\t\tif (temp > 0) {\n+\t\t\t\tif (temp >= 0) {\n \t\t\t\t\toutput_peer_config->recovery_length_min = temp;\n \t\t\t\t\toutput_peer_config->recovery_length_max = temp;\n \t\t\t\t}\n@@ -190,31 +166,16 @@ int parse_url_options(const char* url, struct rist_peer_config *output_peer_conf\n \t\t\t\t\toutput_peer_config->recovery_length_min = temp;\n \t\t\t} else if (strcmp( url_params[i].key, RIST_URL_PARAM_BUFFER_SIZE_MAX ) == 0) {\n \t\t\t\tint temp = atoi( val );\n-\t\t\t\tif (temp > 0)\n+\t\t\t\tif (temp >= 0)\n \t\t\t\t\toutput_peer_config->recovery_length_max = temp;\n \t\t\t} else if (strcmp( url_params[i].key, RIST_URL_PARAM_MIFACE ) == 0) {\n \t\t\t\tstrncpy((void *)output_peer_config->miface, val, 128-1);\n-\t\t\t} else if (output_peer_config->version >= 2 && strcmp(url_params[i].key, RIST_URL_PARAM_MCAST_TTL) == 0) {\n-\t\t\t\tint temp = atoi(val);\n-\t\t\t\tif (temp > 0 && temp <= 255)\n-\t\t\t\t\toutput_peer_config->multicast_ttl = (uint32_t)temp;\n-\t\t\t} else if (output_peer_config->version >= 2 && strcmp(url_params[i].key, RIST_URL_PARAM_MCAST_SOURCE) == 0) {\n-\t\t\t\tstrncpy((void *)output_peer_config->multicast_source, val, RIST_MAX_STRING_LONG - 1);\n \t\t\t} else if (strcmp( url_params[i].key, RIST_URL_PARAM_SECRET ) == 0) {\n \t\t\t\tstrncpy((void *)output_peer_config->secret, val, 128-1);\n \t\t\t} else if (strcmp( url_params[i].key, RIST_URL_PARAM_SRP_USERNAME) == 0) {\n \t\t\t\tstrncpy((void *)output_peer_config->srp_username, val, 256 -1);\n \t\t\t} else if (strcmp( url_params[i].key, RIST_URL_PARAM_SRP_PASSWORD) == 0) {\n \t\t\t\tstrncpy((void *)output_peer_config->srp_password, val, 256 -1);\n-\t\t\t} else if (output_peer_config->version >= 3 && strcmp( url_params[i].key, RIST_URL_PARAM_SRP_COMPAT) == 0) {\n-\t\t\t\tchar *endp = NULL;\n-\t\t\t\tlong temp = strtol(val, &endp, 10);\n-\t\t\t\tif (endp == val || *endp != '\\0' || (temp != 0 && temp != 1)) {\n-\t\t\t\t\tret = -1;\n-\t\t\t\t\tfprintf(stderr, \"Invalid srp-compat '%s'; expected 0|1\\n\", val);\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\t\t\t\toutput_peer_config->srp_compat_legacy = (int)temp;\n \t\t\t} else if (strcmp( url_params[i].key, RIST_URL_PARAM_CNAME ) == 0) {\n \t\t\t\tstrncpy((void *)output_peer_config->cname, val, 128-1);\n \t\t\t} else if (strcmp( url_params[i].key, RIST_URL_PARAM_AES_TYPE ) == 0) {\n@@ -264,26 +225,6 @@ int parse_url_options(const char* url, struct rist_peer_config *output_peer_conf\n \t\t\t\tint temp = atoi( val );\n \t\t\t\tif (temp >= 0)\n \t\t\t\t\toutput_peer_config->weight = temp;\n-\t\t\t} else if (strcmp( url_params[i].key, RIST_URL_PARAM_RECOVERY_PRIORITY ) == 0) {\n-\t\t\t\tint temp = atoi( val );\n-\t\t\t\tif (temp >= 0)\n-\t\t\t\t\toutput_peer_config->recovery_priority = (uint32_t)temp;\n-\t\t\t} else if (strcmp( url_params[i].key, RIST_URL_PARAM_RTT_DROP ) == 0) {\n-\t\t\t\tint temp = atoi( val );\n-\t\t\t\tif (temp >= 0)\n-\t\t\t\t\toutput_peer_config->rtt_drop = (uint32_t)temp;\n-\t\t\t} else if (strcmp( url_params[i].key, RIST_URL_PARAM_RTT_RESTORE ) == 0) {\n-\t\t\t\tint temp = atoi( val );\n-\t\t\t\tif (temp >= 0)\n-\t\t\t\t\toutput_peer_config->rtt_restore = (uint32_t)temp;\n-\t\t\t} else if (strcmp( url_params[i].key, RIST_URL_PARAM_RTT_DROP_SETTLE ) == 0) {\n-\t\t\t\tint temp = atoi( val );\n-\t\t\t\tif (temp >= 0)\n-\t\t\t\t\toutput_peer_config->rtt_drop_settle = (uint32_t)temp;\n-\t\t\t} else if (strcmp( url_params[i].key, RIST_URL_PARAM_RTT_DROP_TRICKLE ) == 0) {\n-\t\t\t\tint temp = atoi( val );\n-\t\t\t\tif (temp >= 0)\n-\t\t\t\t\toutput_peer_config->rtt_drop_trickle = (uint32_t)temp;\n \t\t\t} else if (strcmp( url_params[i].key, RIST_URL_PARAM_SESSION_TIMEOUT ) == 0) {\n \t\t\t\tint temp = atoi( val );\n \t\t\t\tif (temp > 0)\n@@ -308,67 +249,6 @@ int parse_url_options(const char* url, struct rist_peer_config *output_peer_conf\n \t\t\t\tint temp = atoi( val );\n \t\t\t\tif (temp > 0)\n \t\t\t\t\toutput_peer_config->max_retries = temp;\n-\t\t\t} else if (output_peer_config->version >= 1 &&\n-\t\t\t           strcmp( url_params[i].key, RIST_URL_PARAM_SPLIT_MODE ) == 0) {\n-\t\t\t\tif (strcmp(val, \"off\") == 0)\n-\t\t\t\t\toutput_peer_config->split_mode = LIBRIST_SPLIT_MODE_OFF;\n-\t\t\t\telse if (strcmp(val, \"auto\") == 0 || strcmp(val, \"ts\") == 0)\n-\t\t\t\t\toutput_peer_config->split_mode = LIBRIST_SPLIT_MODE_AUTO;\n-\t\t\t\telse if (strcmp(val, \"half\") == 0)\n-\t\t\t\t\toutput_peer_config->split_mode = LIBRIST_SPLIT_MODE_HALF;\n-\t\t\t\telse {\n-\t\t\t\t\tret = -1;\n-\t\t\t\t\tfprintf(stderr, \"Unknown split mode '%s'; expected off|auto|half\\n\", val);\n-\t\t\t\t}\n-\t\t\t} else if (output_peer_config->version >= 1 &&\n-\t\t\t           strcmp( url_params[i].key, RIST_URL_PARAM_MERGE_MODE ) == 0) {\n-\t\t\t\tif (strcmp(val, \"off\") == 0)\n-\t\t\t\t\toutput_peer_config->merge_mode = LIBRIST_MERGE_MODE_OFF;\n-\t\t\t\telse if (strcmp(val, \"pairs\") == 0)\n-\t\t\t\t\toutput_peer_config->merge_mode = LIBRIST_MERGE_MODE_PAIRS;\n-\t\t\t\telse if (strcmp(val, \"auto\") == 0)\n-\t\t\t\t\toutput_peer_config->merge_mode = LIBRIST_MERGE_MODE_AUTO;\n-\t\t\t\telse {\n-\t\t\t\t\tret = -1;\n-\t\t\t\t\tfprintf(stderr, \"Unknown merge mode '%s'; expected off|auto|pairs\\n\", val);\n-\t\t\t\t}\n-\t\t\t} else if (output_peer_config->version >= 2 && strcmp( url_params[i].key, RIST_URL_PARAM_REFLECTOR ) == 0) {\n-\t\t\t\tint temp = atoi( val );\n-\t\t\t\tif (temp >= 0 && temp <= 1)\n-\t\t\t\t\toutput_peer_config->reflector = temp;\n-\t\t\t} else if (output_peer_config->version >= 2 && strcmp( url_params[i].key, RIST_URL_PARAM_LOCAL_PORT ) == 0) {\n-\t\t\t\tint temp = atoi( val );\n-\t\t\t\tif (temp > 0 && temp <= 65535)\n-\t\t\t\t\toutput_peer_config->local_port = (uint16_t)temp;\n-\t\t\t} else if (output_peer_config->version >= 5 &&\n-\t\t\t           strcmp( url_params[i].key, RIST_URL_PARAM_RECOVERY_DEPTH ) == 0) {\n-\t\t\t\tchar *endp = NULL;\n-\t\t\t\tlong temp = strtol(val, &endp, 10);\n-\t\t\t\tif (endp == val || *endp != '\\0' ||\n-\t\t\t\t    temp < RIST_RECOVERY_DEPTH_MIN || temp > RIST_RECOVERY_DEPTH_MAX) {\n-\t\t\t\t\tret = -1;\n-\t\t\t\t\tfprintf(stderr, \"Invalid recovery-depth '%s'; expected %d..%d \"\n-\t\t\t\t\t\t\"(ring = 65536 << depth packets; default %d)\\n\", val,\n-\t\t\t\t\t\tRIST_RECOVERY_DEPTH_MIN, RIST_RECOVERY_DEPTH_MAX,\n-\t\t\t\t\t\tRIST_RECOVERY_DEPTH_DEFAULT);\n-\t\t\t\t} else {\n-\t\t\t\t\toutput_peer_config->recovery_depth = (uint8_t)temp;\n-\t\t\t\t}\n-\t\t\t} else if (output_peer_config->version >= 4 &&\n-\t\t\t           strcmp( url_params[i].key, RIST_URL_PARAM_PROFILE ) == 0) {\n-\t\t\t\t/* version >= 4: writing profile fields on a\n-\t\t\t\t * struct sized for an older version would\n-\t\t\t\t * overflow the caller's allocation. */\n-\t\t\t\tchar *endp = NULL;\n-\t\t\t\tlong temp = strtol(val, &endp, 10);\n-\t\t\t\tif (endp == val || *endp != '\\0' ||\n-\t\t\t\t    temp < RIST_PROFILE_SIMPLE || temp > RIST_PROFILE_ADVANCED) {\n-\t\t\t\t\tret = -1;\n-\t\t\t\t\tfprintf(stderr, \"Invalid profile '%s'; expected 0|1|2\\n\", val);\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\t\t\t\toutput_peer_config->profile = (enum rist_profile)temp;\n-\t\t\t\toutput_peer_config->profile_set = 1;\n \t\t\t} else {\n \t\t\t\tret = -1;\n \t\t\t\tfprintf(stderr, \"Unknown or invalid parameter %s\\n\", url_params[i].key);\n@@ -413,80 +293,20 @@ int rist_recovery_rtt_multiplier_set_internal(struct rist_common_ctx *ctx, int m\n \treturn -1;\n }\n \n-/* Reference packet size for the config-time recovery-window sanity check.\n- * 7x188 MPEG-TS over RTP is the common RIST framing. Smaller packets put\n- * MORE packets in the buffer and make the window tighter, so this estimate\n- * is deliberately optimistic: it only warns when the configuration clearly\n- * overruns the window even with full-size packets. */\n-#define RIST_RECOVERY_REF_PKT_BYTES 1316\n-\n-/* Warn (once, at peer-config time) if the configured recovery-maxbitrate and\n- * max buffer would queue more packets than the recovery window can address.\n- * Packets beyond the window cannot be retransmitted regardless of how many\n- * NACKs are sent. window is the addressable window in packets for this role\n- * (receiver NACK window or sender retransmit window). */\n-static void rist_warn_recovery_window(struct rist_peer *peer, size_t window, bool sender)\n-{\n-\tuint32_t maxbitrate = peer->config.recovery_maxbitrate;   /* kbps */\n-\tuint32_t length_max = peer->config.recovery_length_max;    /* ms   */\n-\tif (maxbitrate == 0 || length_max == 0 || window == 0)\n-\t\treturn;\n-\tuint64_t pkts_in_buffer =\n-\t\t(uint64_t)maxbitrate * length_max / (RIST_RECOVERY_REF_PKT_BYTES * 8);\n-\tif (pkts_in_buffer <= window)\n-\t\treturn;\n-\tstruct rist_common_ctx *cctx = get_cctx(peer);\n-\tbool advanced = (cctx->profile == RIST_PROFILE_ADVANCED);\n-\trist_log_priv(cctx, RIST_LOG_WARN,\n-\t\t\"Peer #%\"PRIu32\": recovery config exceeds the %s recovery window. \"\n-\t\t\"At %u kbps a %u ms buffer holds ~%\"PRIu64\" packets (assuming ~%d-byte \"\n-\t\t\"packets; smaller packets are worse), but the %s-profile %s window \"\n-\t\t\"addresses only %zu packets - packets beyond it cannot be retransmitted. %s\\n\",\n-\t\tpeer->adv_peer_id,\n-\t\tsender ? \"sender\" : \"receiver\",\n-\t\tmaxbitrate, length_max, pkts_in_buffer, RIST_RECOVERY_REF_PKT_BYTES,\n-\t\tadvanced ? \"Advanced\" : (cctx->profile == RIST_PROFILE_MAIN ? \"Main\" : \"Simple\"),\n-\t\tsender ? \"retransmit\" : \"NACK\",\n-\t\twindow,\n-\t\tadvanced\n-\t\t\t? \"Lower recovery-maxbitrate/buffer, or enlarge the ring via ?recovery-depth= (or rist_recovery_depth_set()) before rist_start().\"\n-\t\t\t: \"Lower recovery-maxbitrate/buffer, or use the Advanced profile for a 32-bit sequence space.\");\n-}\n-\n static void init_peer_settings(struct rist_peer *peer)\n {\n-\t/* 0 means unset, not unlimited: every reader below takes it as a literal\n-\t * ceiling of zero. ?bandwidth=0 is ignored the same way. */\n-\tif (peer->config.recovery_maxbitrate == 0) {\n-\t\trist_log_priv(get_cctx(peer), RIST_LOG_WARN,\n-\t\t\t\"Peer #%\"PRIu32\": a recovery-maxbitrate of 0 disables retransmission \"\n-\t\t\t\"instead of lifting the ceiling, so using the %d kbps default. Set it \"\n-\t\t\t\"to the ceiling you want for payload plus retransmissions.\\n\",\n-\t\t\tpeer->adv_peer_id, RIST_DEFAULT_RECOVERY_MAXBITRATE);\n-\t\tpeer->config.recovery_maxbitrate = RIST_DEFAULT_RECOVERY_MAXBITRATE;\n-\t}\n-\n \tpeer->eight_times_rtt = peer->config.recovery_rtt_min * 8;\n-\t/* Midpoint of the configured buffer range, and the same on both ends so the\n-\t * two sides agree on how long a silent peer has to come back. The receiver\n-\t * scales its reorder buffer from here; the sender holds a dead peer's place\n-\t * in the send rotation for this long, and floors its liveness timeout on it. */\n-\tpeer->recovery_buffer_ticks =\n-\t\t((uint64_t)(peer->config.recovery_length_max - peer->config.recovery_length_min) / 2 +\n-\t\t peer->config.recovery_length_min) * RIST_CLOCK;\n \tif (peer->receiver_mode) {\n \t\tassert(peer->receiver_ctx != NULL);\n \t\tuint32_t recovery_maxbitrate_mbps = peer->config.recovery_maxbitrate < 1000 ? 1 : peer->config.recovery_maxbitrate / 1000;\n+\t\t// Initial value for some variables\n+\t\tpeer->recovery_buffer_ticks =\n+\t\t\t(peer->config.recovery_length_max - peer->config.recovery_length_min) / 2 + peer->config.recovery_length_min;\n+\t\tpeer->recovery_buffer_ticks = peer->recovery_buffer_ticks * RIST_CLOCK;\n \t\tpeer->missing_counter_max =\n \t\t\t(uint32_t)(peer->recovery_buffer_ticks / RIST_CLOCK) * recovery_maxbitrate_mbps /\n \t\t\t(sizeof(struct rist_gre_seq) + sizeof(struct rist_rtp_hdr) + sizeof(uint32_t));\n \n-\t\t{\n-\t\t\tstruct rist_common_ctx *cctx = get_cctx(peer);\n-\t\t\tsize_t rwin = ((cctx->profile == RIST_PROFILE_ADVANCED)\n-\t\t\t\t? cctx->recovery_queue_max : UINT16_SIZE) / 2;\n-\t\t\trist_warn_recovery_window(peer, rwin, false);\n-\t\t}\n \n \t\trist_log_priv(get_cctx(peer), RIST_LOG_INFO,\n \t\t\t\t\"New peer with id #%\"PRIu32\" was configured with maxrate=%d/%d bufmin=%d bufmax=%d reorder=%d rttmin=%d rttmax=%d congestion_control=%d min_retries=%d max_retries=%d\\n\",\n@@ -498,21 +318,13 @@ static void init_peer_settings(struct rist_peer *peer)\n \telse {\n \t\tassert(peer->sender_ctx != NULL);\n \t\tstruct rist_sender *ctx = peer->sender_ctx;\n-\t\t{\n-\t\t\tsize_t swin = ((ctx->common.profile == RIST_PROFILE_ADVANCED)\n-\t\t\t\t? ctx->sender_queue_max : UINT16_SIZE) / 2;\n-\t\t\trist_warn_recovery_window(peer, swin, true);\n-\t\t}\n \t\t/* Global context settings */\n \t\tif (peer->config.recovery_maxbitrate > ctx->recovery_maxbitrate_max) {\n \t\t\tctx->recovery_maxbitrate_max = peer->config.recovery_maxbitrate;\n \t\t\tint max_jitter_ms = ctx->common.rist_max_jitter / RIST_CLOCK;\n \t\t\t// Asume MTU of 1400 for now\n \t\t\tuint32_t max_nacksperloop = ctx->recovery_maxbitrate_max * max_jitter_ms / (8*1400);\n-\t\t\t// Normalize against the total buffer size; guard the divisor in\n-\t\t\t// case a 0 buffer ever gets past URL/API validation (SIGFPE).\n-\t\t\tif (peer->config.recovery_length_max == 0)\n-\t\t\t\tpeer->config.recovery_length_max = 1;\n+\t\t\t// Normalize against the total buffer size\n \t\t\tmax_nacksperloop = max_nacksperloop * 1000 / peer->config.recovery_length_max;\n \t\t\t// Anything less that 2240Kbps at 5ms will round down to zero (100Mbps is 44)\n \t\t\tif (max_nacksperloop == 0)\n@@ -526,7 +338,7 @@ static void init_peer_settings(struct rist_peer *peer)\n \t\t\t}\n \t\t}\n \n-\t\tif (!peer->listening && peer->config.weight != RIST_PEER_WEIGHT_DUPLICATE && !peer->parent) {\n+\t\tif (!peer->listening && peer->config.weight > 0 && !peer->parent) {\n \t\t\tctx->total_weight += peer->config.weight;\n \t\t\trist_log_priv(&ctx->common, RIST_LOG_INFO, \"Peer weight: %lu\\n\", peer->config.weight);\n \t\t}\n@@ -554,11 +366,14 @@ struct rist_buffer *rist_new_buffer(struct rist_common_ctx *ctx, const void *buf\n \t\treturn NULL;\n \t}\n \n-\tb->data = malloc(len + RIST_MAX_PAYLOAD_OFFSET);\n-\tif (!b->data) {\n-\t\tfree(b);\n-\t\tfprintf(stderr, \"OOM\\n\");\n-\t\treturn NULL;\n+\tif (buf != NULL && len > 0)\n+\t{\n+\t\tb->data = malloc(len + RIST_MAX_PAYLOAD_OFFSET);\n+\t\tif (!b->data) {\n+\t\t\tfree(b);\n+\t\t\tfprintf(stderr, \"OOM\\n\");\n+\t\t\treturn NULL;\n+\t\t}\n \t}\n \tb->alloc_size = len;\n \tif (buf != NULL && len > 0)\n@@ -641,10 +456,6 @@ static int receiver_insert_queue_packet(struct rist_flow *f, struct rist_peer *p\n \tf->receiver_queue[idx]->packet_time = packet_time;\n \tf->receiver_queue[idx]->target_output_time = packet_time + f->recovery_buffer_ticks;\n \tatomic_fetch_add_explicit(&f->receiver_queue_size, len, memory_order_release);\n-\tif (f->cbr_output) {\n-\t\t/* Counted on the way in, so the estimate measures the source. */\n-\t\tatomic_fetch_add_explicit(&f->cbr_arrived_bytes, len, memory_order_relaxed);\n-\t}\n \n \treturn 0;\n }\n@@ -652,38 +463,25 @@ static int receiver_insert_queue_packet(struct rist_flow *f, struct rist_peer *p\n static inline void receiver_mark_missing(struct rist_flow *f, struct rist_peer *peer, uint32_t current_seq, uint64_t rtt) {\n \tuint32_t counter = 1;\n \tuint64_t packet_time_last = 0;\n-\t/* Index the ring exactly as receiver_enqueue() does. 16-bit flows had a\n-\t * sequence range equal to receiver_queue_max so a raw seq was always a\n-\t * valid index, but a 32-bit (Advanced) seq can exceed receiver_queue_max\n-\t * and run off the array -> OOB read of a stale/NULL slot, then a NULL\n-\t * deref. */\n-\tsize_t last_idx = f->last_seq_found & (f->receiver_queue_max - 1);\n-\tsize_t cur_idx = current_seq & (f->receiver_queue_max - 1);\n-\tif (RIST_UNLIKELY(!f->receiver_queue[last_idx]))\n+\tif (RIST_UNLIKELY(!f->receiver_queue[f->last_seq_found]))\n \t\tif (RIST_LIKELY(!f->rtc_timing_mode))\n \t\t\tpacket_time_last = timestampNTP_u64();\n \t\telse\n \t\t\tpacket_time_last = timestampNTP_RTC_u64();\n \telse\n-\t\tpacket_time_last = f->receiver_queue[last_idx]->packet_time;\n+\t\tpacket_time_last = f->receiver_queue[f->last_seq_found]->packet_time;\n \tuint64_t packet_time_now;\n-\tif (RIST_UNLIKELY(!f->receiver_queue[cur_idx])) {\n+\tif (RIST_UNLIKELY(!f->receiver_queue[current_seq])) {\n \t\tif (RIST_LIKELY(!f->rtc_timing_mode))\n \t\t\tpacket_time_now = timestampNTP_u64();\n \t\telse\n \t\t\tpacket_time_now = timestampNTP_RTC_u64();\n \t} else {\n-\t\tpacket_time_now = f->receiver_queue[cur_idx]->packet_time;\n-\t}\n-\t/* short_seq (Simple/Main) flows wrap at 16 bits; 32-bit (Advanced) flows\n-\t * use the true gap so a real >64k loss is not truncated. Cap mirrors the\n-\t * recovery-walk hole cap (UINT16_SIZE/2 short, receiver_queue_max/2 else). */\n-\tuint32_t missing_count = rist_seq_gap(current_seq, f->last_seq_found,\n-\t                                      f->short_seq);\n-\tuint32_t missing_count_cap = f->short_seq\n-\t        ? (UINT16_SIZE / 2)\n-\t        : (uint32_t)(f->receiver_queue_max / 2);\n-\tif (missing_count > missing_count_cap)\n+\t\tpacket_time_now = f->receiver_queue[current_seq]->packet_time;\n+\t}\n+\tuint32_t missing_count = (current_seq - f->last_seq_found) & UINT16_MAX;\n+\t//arbitrary large number to prevent incorrectly marking packets as missing when wrap-around occurs & we did not correctly detect as out of order\n+\tif (missing_count > 32768)\n \t\treturn;\n \tuint64_t interpacket_time = (packet_time_now - packet_time_last) / (missing_count +1);\n \tuint32_t missing_seq = (f->last_seq_found + counter);\n@@ -719,9 +517,7 @@ static inline void receiver_mark_missing(struct rist_flow *f, struct rist_peer *\n \n int compare(const void *a, const void *b)\n {\n-\tuint64_t ua = *(const uint64_t *)a;\n-\tuint64_t ub = *(const uint64_t *)b;\n-\treturn (ua > ub) - (ua < ub);\n+\treturn ( *(uint64_t*)a < *(uint64_t*)b);\n }\n \n static void recalculate_clock_offset(struct rist_flow *flow)\n@@ -734,7 +530,7 @@ static void recalculate_clock_offset(struct rist_flow *flow)\n \t   packets. Every \"correctly\" (in-order, no-discontinuities) packet's clock offset\n \t   is inserted into an array. Of which we will take the median. */\n \tqsort(flow->offset_recalc_samples, flow->offset_recalc_sample_count, sizeof(uint64_t), compare);\n-\tsize_t middle = flow->offset_recalc_sample_count / 2;\n+\tsize_t middle = (flow->offset_recalc_sample_count +1)/2 +1;\n \tuint64_t median_offset = flow->offset_recalc_samples[middle];\n \tflow->offset_recalc_sample_count = 0;\n \tuint64_t diff = 0;\n@@ -766,26 +562,6 @@ static int receiver_enqueue(struct rist_peer *peer, uint64_t source_time, uint64\n \t\treturn -1;\n \tif (RIST_UNLIKELY(!f->receiver_queue_has_items)) {\n \t\t/* we just received our first packet for this flow */\n-\t\tswitch (rist_flow_reanchor_check(source_time, f->max_source_time,\n-\t\t\t\t\t\t f->recovery_buffer_ticks,\n-\t\t\t\t\t\t f->reanchor_wait_since, now_monotonic)) {\n-\t\tcase RIST_REANCHOR_WAIT:\n-\t\t\tif (!f->reanchor_wait_since) {\n-\t\t\t\tf->reanchor_wait_since = now_monotonic;\n-\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_INFO,\n-\t\t\t\t\t\t\"Waiting for a current packet to anchor flow on, peer %\"PRIu32\" is %\" PRIu64 \" ms behind\\n\",\n-\t\t\t\t\t\tpeer->adv_peer_id, (f->max_source_time - source_time) / RIST_CLOCK);\n-\t\t\t}\n-\t\t\treturn -1;\n-\t\tcase RIST_REANCHOR_FORCED:\n-\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_WARN,\n-\t\t\t\t\t\"No current packet to anchor flow on, using seq %\" PRIu32 \" from peer %\"PRIu32\", %\" PRIu64 \" ms behind\\n\",\n-\t\t\t\t\tseq, peer->adv_peer_id, (f->max_source_time - source_time) / RIST_CLOCK);\n-\t\t\tbreak;\n-\t\tcase RIST_REANCHOR_OK:\n-\t\t\tbreak;\n-\t\t}\n-\t\tf->reanchor_wait_since = 0;\n \t\tpthread_mutex_lock(&f->mutex);\n \t\tif (atomic_load_explicit(&f->receiver_queue_size, memory_order_acquire) > 0)\n \t\t{\n@@ -815,18 +591,6 @@ static int receiver_enqueue(struct rist_peer *peer, uint64_t source_time, uint64\n \t\t\t\tseq, idx_initial, source_time, peer->flow->time_offset / RIST_CLOCK, idx_initial);\n \t\tuint64_t packet_time = source_time + f->time_offset;\n \n-\t\tf->last_packet_ts = packet_time;\n-\t\tf->time_offset_changed_ts = 0;\n-\t\tf->time_offset_old = f->time_offset;\n-\t\t/* Discard clock-drift samples gathered against the previous\n-\t\t * baseline.  A flow-id change or a Main<->Advanced wire-framing\n-\t\t * switch (the two framings carry source_time in different\n-\t\t * timestamp domains) lands here with stale samples still queued;\n-\t\t * blending them into the median yields a bogus multi-second\n-\t\t * offset correction that releases the whole buffer at once and\n-\t\t * overflows the data-out fifo.  Matches the clock-wrap reset. */\n-\t\tf->offset_recalc_sample_count = 0;\n-\n \t\treceiver_insert_queue_packet(f, peer, idx_initial, buf, len, seq, source_time, src_port, dst_port, packet_time);\n \t\tatomic_store_explicit(&f->receiver_queue_output_idx, idx_initial, memory_order_release);\n \n@@ -862,34 +626,19 @@ static int receiver_enqueue(struct rist_peer *peer, uint64_t source_time, uint64\n \t\t\tindex = (index +1)& (f->receiver_queue_max -1);\n \t\t}\n \t\t//interpolate the arrival time, assuming CBR\n-\t\t/* Neighbours can be non-monotonic under arrival-based timing;\n-\t\t * clamp the CBR estimate into the interval instead of asserting. */\n-\t\tif (next && previous && next->packet_time > previous->packet_time)\n+\t\tif (next && previous)\n \t\t{\n \t\t\tuint32_t steps = (next->seq - previous->seq);\n \t\t\tif (f->short_seq)\n \t\t\t\tsteps = (uint16_t)steps;\n+\t\t\tuint64_t time_per_step = (next->packet_time - previous->packet_time) / steps;\n \t\t\tuint32_t steps_since_previous = seq - previous->seq;\n \t\t\tif (f->short_seq)\n \t\t\t\tsteps_since_previous = (uint16_t)steps_since_previous;\n-\t\t\tif (steps > 1 && steps_since_previous > 0 && steps_since_previous < steps) {\n-\t\t\t\t/* multiply before dividing so integer rounding cannot push\n-\t\t\t\t * the estimate up to or past next->packet_time */\n-\t\t\t\tuint64_t span = next->packet_time - previous->packet_time;\n-\t\t\t\tpacket_time = previous->packet_time + (span * steps_since_previous) / steps;\n-\t\t\t} else {\n-\t\t\t\t/* seq is not strictly between the neighbours (gap, reorder\n-\t\t\t\t * or sequence-number wrap): place it next to next. */\n-\t\t\t\tpacket_time = next->packet_time - 1;\n-\t\t\t}\n-\t\t\tif (packet_time < previous->packet_time)\n-\t\t\t\tpacket_time = previous->packet_time;\n-\t\t\telse if (packet_time > next->packet_time)\n-\t\t\t\tpacket_time = next->packet_time;\n+\t\t\tpacket_time = previous->packet_time + (time_per_step * steps_since_previous);\n+\t\t\tassert(packet_time < next->packet_time);\n \t\t} else if (next)\n \t\t{\n-\t\t\t/* No usable previous neighbour, or neighbours whose times are\n-\t\t\t * non-monotonic: use next's time. */\n \t\t\tpacket_time = next->packet_time;\n \t\t}\n \t}\n@@ -902,22 +651,15 @@ static int receiver_enqueue(struct rist_peer *peer, uint64_t source_time, uint64\n \t   output time than the highest known output time) */\n \tsize_t reader_idx;\n \tbool out_of_order = false;\n-\tuint32_t expected_seq = rist_seq_next(f->last_seq_found, f->short_seq);\n+\tuint32_t expected_seq = (f->last_seq_found +1) & (UINT16_MAX -1);\n \tif (RIST_UNLIKELY(packet_time < f->last_packet_ts && seq != expected_seq)) {\n \t\tif (now > (packet_time + (f->recovery_buffer_ticks *1.1)))\n \t\t{\n \t\t\trist_log_priv(get_cctx(peer), RIST_LOG_DEBUG, \"Packet %\"PRIu32\" too late, dropping!\\n\", seq);\n \t\t\tpthread_mutex_lock(&(get_cctx(peer)->stats_lock));\n \t\t\tf->stats_instant.dropped_late++;\n-\t\t\t/* This soft reset is intentional anti-stall behavior: when a\n-\t\t\t * flow re-bases (received falls back toward 0) it lets the\n-\t\t\t * queue re-anchor onto the changed path instead of stalling.\n-\t\t\t * A crafted late-packet burst can trip it early, but the queue\n-\t\t\t * just rebuilds from the next in-window packet (no persistent\n-\t\t\t * effect), so it is deliberately left without a received floor\n-\t\t\t * here; the hard reset below keeps its received > 100 floor. */\n-\t\t\tif (f->stats_instant.dropped_late > 5 * f->stats_instant.received)\n-\t\t\t\tf->receiver_queue_has_items = false;\n+                        if (f->stats_instant.dropped_late > 5 * f->stats_instant.received)\n+                            f->receiver_queue_has_items = false;\n \t\t\tif ((f->stats_instant.dropped_late > (f->stats_instant.received * 5) && f->stats_instant.received > 100) ||\n \t\t\t\t(f->stats_instant.dropped_late > 100 && f->stats_instant.received == 0)) {\n \t\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_ERROR, \"Too many late packets received, resetting flow\");\n@@ -953,11 +695,7 @@ static int receiver_enqueue(struct rist_peer *peer, uint64_t source_time, uint64\n \tif (RIST_UNLIKELY(f->receiver_queue[idx])) {\n \t\t// TODO: record stats\n \t\tstruct rist_buffer *b = f->receiver_queue[idx];\n-\t\t/* Match on seq: the slot index is derived from the sequence number,\n-\t\t * and source_time need not be unique per packet, so it cannot\n-\t\t * identify a genuine duplicate. A different seq in this slot is a\n-\t\t * stale entry from an earlier ring cycle and is replaced below. */\n-\t\tif (b->seq == seq) {\n+\t\tif (b->source_time == source_time) {\n \t\t\trist_log_priv(get_cctx(peer), RIST_LOG_DEBUG, \"Dupe! %\"PRIu32\"/%zu\\n\", seq, idx);\n \t\t\tpthread_mutex_lock(&(get_cctx(peer)->stats_lock));\n \t\t\tf->stats_instant.dupe++;\n@@ -981,7 +719,6 @@ static int receiver_enqueue(struct rist_peer *peer, uint64_t source_time, uint64\n \tif (out_of_order)\n \t\tf->stats_instant.reordered++;\n \tf->stats_instant.received++;\n-\tf->stats_instant.received_bytes += (uint64_t)len;\n \tpthread_mutex_unlock(&(get_cctx(peer)->stats_lock));\n \t// Check for missing data and queue retries\n \tif (!retry) {\n@@ -1180,25 +917,18 @@ static void receiver_output(struct rist_receiver *ctx, struct rist_flow *f)\n \t\t\t\t\tbreak;\n \t\t\t\t}\n \t\t\t}\n-\t\t\t/* Count the sequence gap, not the ring slots we stepped over:\n-\t\t\t * the two only agree while the reader sits right behind the\n-\t\t\t * data, and after an idle source or a re-anchor the walk spans\n-\t\t\t * far more slots than there are missing packets. */\n-\t\t\tuint32_t gap = rist_seq_gap(b->seq, rist_seq_next(f->last_seq_output, f->short_seq),\n-\t\t\t\t\t\t    f->short_seq);\n \t\t\tsize_t max_holes = f->short_seq ? (UINT16_SIZE / 2) : (f->receiver_queue_max / 2);\n-\t\t\tif (gap <= max_holes) {\n+\t\t\tif (holes <= max_holes) {\n \t\t\t\tpthread_mutex_lock(&ctx->common.stats_lock);\n-\t\t\t\tf->stats_instant.lost += gap;\n+\t\t\t\tf->stats_instant.lost += holes;\n \t\t\t\tpthread_mutex_unlock(&ctx->common.stats_lock);\n \t\t\t}\n \t\t\toutput_idx = counter;\n \t\t\trist_log_priv(&ctx->common, RIST_LOG_DEBUG,\n-\t\t\t\t\t\"Empty buffer element, flushing %zu hole(s), now at index %zu, size is %zu\\n\",\n+\t\t\t\t\t\"Empty buffer element, flushing %\"PRIu32\" hole(s), now at index %zu, size is %zu\\n\",\n \t\t\t\t\tholes, counter, atomic_load_explicit(&f->receiver_queue_size, memory_order_acquire));\n \t\t}\n \t\tif (b) {\n-\t\t\tbool merged = false;\n \t\t\tif (b->type == RIST_PAYLOAD_TYPE_DATA_RAW) {\n \n \t\t\t\tnow = timestampNTP_u64();\n@@ -1214,8 +944,6 @@ static void receiver_output(struct rist_receiver *ctx, struct rist_flow *f)\n \t\t\t\t\t\tdrop = true;\n \t\t\t\t\t\tif (f->too_late_ctr > 100) {\n \t\t\t\t\t\t\trist_log_priv(&ctx->common, RIST_LOG_ERROR, \"Too many old packets, resetting buffer\\n\");\n-\t\t\t\t\t\t\t/* clear the latch, else it re-fires every output cycle */\n-\t\t\t\t\t\t\tf->too_late_ctr = 0;\n \t\t\t\t\t\t\tf->receiver_queue_has_items = false;\n \t\t\t\t\t\t\treturn;\n \t\t\t\t\t\t}\n@@ -1235,30 +963,6 @@ static void receiver_output(struct rist_receiver *ctx, struct rist_flow *f)\n \t\t\t\t\t//\tdelay_rtc / RIST_CLOCK , delay / RIST_CLOCK, recovery_buffer_ticks / RIST_CLOCK, f->receiver_queue_size);\n \t\t\t\t\tbreak;\n \t\t\t\t}\n-\t\t\t\t/* The buffer has cleared this packet; pacing decides whether it\n-\t\t\t\t * is its turn in the measured cadence yet. Delay only, and it\n-\t\t\t\t * yields below rather than hold a packet past its deadline. */\n-\t\t\t\tif (f->cbr_output) {\n-\t\t\t\t\tuint64_t iv = rist_pacer_interval_ns(&f->cbr_pacer,\n-\t\t\t\t\t\t\t\t\trist_pacer_rate_bps(&f->cbr_rate), b->size, 0);\n-\t\t\t\t\tif (iv) {\n-\t\t\t\t\t\tuint64_t now_ns = timestampNTP_to_ns(now);\n-\t\t\t\t\t\tf->cbr_interval_ns = iv;\n-\t\t\t\t\t\tif (rist_pacer_due_ns(&f->cbr_pacer, now_ns) > now_ns) {\n-\t\t\t\t\t\t\tuint64_t overdue = now > b->target_output_time ?\n-\t\t\t\t\t\t\t\t\t(now - b->target_output_time) : 0;\n-\t\t\t\t\t\t\tif (overdue < ((uint64_t)f->cbr_max_hold_us * RIST_CLOCK) / 1000) {\n-\t\t\t\t\t\t\t\tf->cbr_paced_hold = true; /* waiting its turn, not idle */\n-\t\t\t\t\t\t\t\tbreak;\n-\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\t/* Re-anchor, else every later packet lands here too. */\n-\t\t\t\t\t\t\tf->cbr_overdue_releases++;\n-\t\t\t\t\t\t\trist_pacer_reset(&f->cbr_pacer);\n-\t\t\t\t\t\t\trist_pacer_due_ns(&f->cbr_pacer, now_ns);\n-\t\t\t\t\t\t}\n-\t\t\t\t\t\trist_pacer_advance(&f->cbr_pacer, iv);\n-\t\t\t\t\t}\n-\t\t\t\t}\n \t\t\t\tif (holes > 0)\n \t\t\t\t{\n \t\t\t\t\trist_log_priv(&ctx->common, RIST_LOG_DEBUG, \"Did not find any data after %zu holes (%zu bytes in queue)\\n\",\n@@ -1287,65 +991,12 @@ static void receiver_output(struct rist_receiver *ctx, struct rist_flow *f)\n \t\t\t\t\t\tf->flag_flow_buffer_start = false;\n \t\t\t\t\t\tflags |= RIST_DATA_FLAGS_FLOW_BUFFER_START;\n \t\t\t\t\t}\n-\n-\t\t\t\t\tbool merge_active = (ctx->merge_mode == LIBRIST_MERGE_MODE_PAIRS) ||\n-\t\t\t\t\t                    (ctx->merge_mode == LIBRIST_MERGE_MODE_AUTO &&\n-\t\t\t\t\t                     f->merge_auto_enabled);\n-\n-\t\t\t\t\tstruct rist_data_block *block = NULL;\n-\n-\t\t\t\t\tif (merge_active && (b->seq & 1) == 0) {\n-\t\t\t\t\t\tsize_t partner_idx = (output_idx + 1) & (f->receiver_queue_max - 1);\n-\t\t\t\t\t\tstruct rist_buffer *b2 = f->receiver_queue[partner_idx];\n-\t\t\t\t\t\tif (b2 && b2->type == RIST_PAYLOAD_TYPE_DATA_RAW &&\n-\t\t\t\t\t\t    b2->seq == rist_seq_next(b->seq, f->short_seq) &&\n-\t\t\t\t\t\t    b2->source_time == b->source_time) {\n-\t\t\t\t\t\t\tsize_t combined_len = b->size + b2->size;\n-\t\t\t\t\t\t\tuint8_t *combined = malloc(RIST_MAX_PAYLOAD_OFFSET + combined_len);\n-\t\t\t\t\t\t\tif (combined) {\n-\t\t\t\t\t\t\t\tmemcpy(combined + RIST_MAX_PAYLOAD_OFFSET,\n-\t\t\t\t\t\t\t\t       (uint8_t *)b->data + RIST_MAX_PAYLOAD_OFFSET, b->size);\n-\t\t\t\t\t\t\t\tmemcpy(combined + RIST_MAX_PAYLOAD_OFFSET + b->size,\n-\t\t\t\t\t\t\t\t       (uint8_t *)b2->data + RIST_MAX_PAYLOAD_OFFSET, b2->size);\n-\t\t\t\t\t\t\t\tblock = calloc(1, sizeof(*block));\n-\t\t\t\t\t\t\t\tif (block) {\n-\t\t\t\t\t\t\t\t\tblock->ref = rist_ref_create(block);\n-\t\t\t\t\t\t\t\t\tblock->peer = b->peer;\n-\t\t\t\t\t\t\t\t\tblock->flow_id = f->flow_id;\n-\t\t\t\t\t\t\t\t\tblock->payload = combined + RIST_MAX_PAYLOAD_OFFSET;\n-\t\t\t\t\t\t\t\t\tblock->payload_len = combined_len;\n-\t\t\t\t\t\t\t\t\tblock->virt_src_port = b->src_port;\n-\t\t\t\t\t\t\t\t\tblock->virt_dst_port = b->dst_port;\n-\t\t\t\t\t\t\t\t\tblock->ts_ntp = b->source_time;\n-\t\t\t\t\t\t\t\t\tblock->seq = b->seq / 2;\n-\t\t\t\t\t\t\t\t\tblock->flags = flags;\n-\t\t\t\t\t\t\t\t\tmerged = true;\n-\t\t\t\t\t\t\t\t\tctx->stats_pairs_merged++;\n-\t\t\t\t\t\t\t\t} else {\n-\t\t\t\t\t\t\t\t\tfree(combined);\n-\t\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\tif (merged) {\n-\t\t\t\t\t\t\t\tb->data = NULL;\n-\t\t\t\t\t\t\t\tf->last_seq_output = b2->seq;\n-\t\t\t\t\t\t\t\tatomic_fetch_sub_explicit(&f->receiver_queue_size, b2->size, memory_order_relaxed);\n-\t\t\t\t\t\t\t\tf->receiver_queue[partner_idx] = NULL;\n-\t\t\t\t\t\t\t\tfree_rist_buffer(&ctx->common, b2);\n-\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t} else {\n-\t\t\t\t\t\t\tctx->stats_orphan_first_delivered++;\n-\t\t\t\t\t\t}\n-\t\t\t\t\t} else if (merge_active && (b->seq & 1) == 1) {\n-\t\t\t\t\t\tctx->stats_orphan_last_delivered++;\n-\t\t\t\t\t}\n-\n-\t\t\t\t\tif (!merged) {\n-\t\t\t\t\t\tuint8_t *payload = b->data;\n-\t\t\t\t\t\tblock = new_data_block(\n-\t\t\t\t\t\t\t\tNULL, b,\n-\t\t\t\t\t\t\t\t&payload[RIST_MAX_PAYLOAD_OFFSET], f->flow_id, flags);\n-\t\t\t\t\t\tb->data = NULL;\n-\t\t\t\t\t}\n+\t\t\t\t\t/* insert into fifo queue */\n+\t\t\t\t\tuint8_t *payload = b->data;\n+\t\t\t\t\tstruct rist_data_block *block = new_data_block(\n+\t\t\t\t\t\t\tNULL, b,\n+\t\t\t\t\t\t\t&payload[RIST_MAX_PAYLOAD_OFFSET], f->flow_id, flags);\n+\t\t\t\t\tb->data = NULL;\n \t\t\t\t\tif (ctx->receiver_data_fd >= 0 && block) {\n \t\t\t\t\t\tint written;\n \t\t\t\t\t\tif (ctx->receiver_data_fd_flags & RIST_DATA_FD_FLAG_TUN)\n@@ -1406,16 +1057,11 @@ static void receiver_output(struct rist_receiver *ctx, struct rist_flow *f)\n \t\t\t//\tfprintf(stderr, \"rtcp skip at %\"PRIu32\", just removing it from queue\\n\", b->seq);\n \n next:\n-\t\t\tif (!merged)\n-\t\t\t\tf->last_seq_output = b->seq;\n+\t\t\tf->last_seq_output = b->seq;\n \t\t\tatomic_fetch_sub_explicit(&f->receiver_queue_size, b->size, memory_order_relaxed);\n \t\t\tf->receiver_queue[output_idx] = NULL;\n \t\t\tfree_rist_buffer(&ctx->common, b);\n \t\t\toutput_idx = (output_idx + 1)& (f->receiver_queue_max -1);\n-\t\t\tif (merged) {\n-\t\t\t\t/* partner already freed and NULLed above; skip its slot */\n-\t\t\t\toutput_idx = (output_idx + 1) & (f->receiver_queue_max - 1);\n-\t\t\t}\n \t\t\tatomic_store_explicit(&f->receiver_queue_output_idx, output_idx, memory_order_release);\n \t\t\tif (atomic_load_explicit(&f->receiver_queue_size, memory_order_acquire) == 0) {\n \t\t\t\tif (f->last_output_time == 0)\n@@ -1444,29 +1090,18 @@ static void send_nack_group(struct rist_receiver *ctx, struct rist_flow *f)\n \t// Now actually send all the nack IP packets for this flow (the above routing will process/group them)\n \tif (f->nacks.counter == 0)\n \t\treturn;\n-\t/* Lock order: peerlist_lock outside, f->mutex inside. */\n \tpthread_mutex_lock(&ctx->common.peerlist_lock);\n-\tpthread_mutex_lock(&f->mutex);\n \tstruct rist_peer *peer = NULL;\n-\tuint64_t best_rtt = UINT64_MAX;\n-\tuint32_t best_priority = 0;\n+\tuint64_t last_rtt = UINT64_MAX;\n \tif (f->peer_lst_len == 0 || f->peer_lst == NULL)\n \t\tgoto out;\n-\t/* Route NACKs to the eligible peer with the highest recovery_priority,\n-\t * tie-broken by lowest RTT.  With the default priority of 0 on every\n-\t * peer this reduces to the historical lowest-RTT selection. */\n \tfor (size_t i = 0; i < f->peer_lst_len; i++)\n \t{\n \t\tstruct rist_peer *check = f->peer_lst[i];\n-\t\tif (!check->is_rtcp || check->dead)\n-\t\t\tcontinue;\n-\t\tuint32_t priority = check->config.recovery_priority;\n-\t\tif (rist_nack_peer_preferred(priority, check->last_rtt,\n-\t\t                             best_priority, best_rtt))\n+\t\tif (check->is_rtcp && !check->dead && check->last_rtt < last_rtt)\n \t\t{\n \t\t\tpeer = check;\n-\t\t\tbest_priority = priority;\n-\t\t\tbest_rtt = check->last_rtt;\n+\t\t\tlast_rtt = peer->last_rtt;\n \t\t}\n \t}\n \tif (peer != NULL)\n@@ -1487,7 +1122,6 @@ static void send_nack_group(struct rist_receiver *ctx, struct rist_flow *f)\n \t}\n \tf->nacks.counter = 0;\n out:\n-\tpthread_mutex_unlock(&f->mutex);\n \tpthread_mutex_unlock(&ctx->common.peerlist_lock);\n }\n \n@@ -1505,6 +1139,10 @@ void receiver_nack_output(struct rist_receiver *ctx, struct rist_flow *f)\n \tstruct rist_missing_buffer **prev = &f->missing;\n \tstruct rist_missing_buffer *previous = NULL;\n \tint empty = 0;\n+\tuint32_t seq_msb = 0;\n+\tif (mb)\n+\t\tseq_msb = mb->seq >> 16;\n+\n \twhile (mb) {\n \t\tint remove_from_queue_reason = 0;\n \t\tstruct rist_peer *peer = mb->peer;\n@@ -1580,7 +1218,17 @@ void receiver_nack_output(struct rist_receiver *ctx, struct rist_flow *f)\n \t\t\t// Packet is still missing, re-stamp the expiration time so we can re-add to queue\n \t\t\t// We reject the next retry for a number of reasons checked inside the function,\n \t\t\t// in which case the nack will never be resent and we signal a queue removal\n-\t\t\tif (f->nacks.counter == (maxcounter - 1)) {\n+\t\t\tif (seq_msb != (mb->seq >> 16))\n+\t\t\t{\n+\t\t\t\t// We do not mix/group missing sequence numbers with different upper 2 bytes\n+\t\t\t\tif (ctx->common.debug)\n+\t\t\t\t\trist_log_priv(&ctx->common, RIST_LOG_DEBUG,\n+\t\t\t\t\t\t\t\"seq-msb changed from %\"PRIu32\" to %\"PRIu32\" (%\"PRIu32\", %zu, %\"PRIu32\")\\n\",\n+\t\t\t\t\t\t\tseq_msb, mb->seq >> 16, mb->seq, f->nacks.counter,\n+\t\t\t\t\t\t\tf->missing_counter);\n+\t\t\t\tsend_nack_group(ctx, f);\n+\t\t\t}\n+\t\t\telse if (f->nacks.counter == (maxcounter - 1)) {\n \t\t\t\trist_log_priv(&ctx->common, RIST_LOG_DEBUG,\n \t\t\t\t\t\t\"nack max counter per packet (%d) exceeded. Skipping the rest\\n\",\n \t\t\t\t\t\tmaxcounter);\n@@ -1698,9 +1346,7 @@ struct rist_peer *_librist_peer_create_common(struct rist_common_ctx *cctx, stru\n \t}\n \n \tstrncpy(&p->miface[0], config->miface, RIST_MAX_STRING_SHORT);\n-\tp->miface[RIST_MAX_STRING_SHORT - 1] = '\\0';\n \tstrncpy(&p->cname[0], config->cname, RIST_MAX_STRING_SHORT);\n-\tp->cname[RIST_MAX_STRING_SHORT - 1] = '\\0';\n \tif (config->address_family && rist_set_manual_sockdata(p, config)) {\n \t\tfree(p);\n \t\treturn NULL;\n@@ -1715,18 +1361,17 @@ struct rist_peer *_librist_peer_create_common(struct rist_common_ctx *cctx, stru\n \t\tp->rtcp_keepalive_interval = config->keepalive_interval * RIST_CLOCK;\n \t}\n \n-\t/* Unset session_timeout means the documented default; floored at a few\n-\t * RTCP intervals so a peer is never declared dead before its heartbeat\n-\t * could have arrived. */\n-\tuint64_t min_liveness = RIST_LIVENESS_MIN_PINGS * p->rtcp_keepalive_interval;\n-\tif (config->session_timeout > 0)\n-\t\tp->session_timeout = (uint64_t)config->session_timeout * RIST_CLOCK;\n-\telse\n-\t\tp->session_timeout = (uint64_t)RIST_DEFAULT_SESSION_TIMEOUT * RIST_CLOCK;\n-\tif (p->session_timeout < min_liveness) {\n-\t\trist_log_priv(cctx, RIST_LOG_WARN, \"The configured (%\"PRIu64\" ms) peer session timeout is below %\"PRIu64\" ms (%d RTCP intervals), using the floor instead\\n\",\n-\t\t\tp->session_timeout / RIST_CLOCK, min_liveness / RIST_CLOCK, RIST_LIVENESS_MIN_PINGS);\n-\t\tp->session_timeout = min_liveness;\n+\tif (config->session_timeout > 0) {\n+\t\tif (config->session_timeout < 250) {\n+\t\t\trist_log_priv(cctx, RIST_LOG_WARN, \"The configured (%d ms) peer session timeout is too small, using %d ms instead\\n\",\n+\t\t\t\tconfig->session_timeout, 250);\n+\t\t\tp->session_timeout = 250 * RIST_CLOCK;\n+\t\t}\n+\t\telse\n+\t\t\tp->session_timeout = config->session_timeout * RIST_CLOCK;\n+\t}\n+\telse {\n+\t\tp->session_timeout = 250 * RIST_CLOCK;\n \t}\n \n \tif (cctx->profile > RIST_PROFILE_SIMPLE) {\n@@ -1833,19 +1478,6 @@ void rist_peer_authenticate(struct rist_peer *peer)\n \tif (peer->peer_data)\n \t\tpeer->peer_data->authenticated = true;\n \n-\t/* Re-auth means any prior caller rebind converged; clear the backoff so\n-\t * the next outage retries promptly instead of from an ever-growing gap. */\n-\tpeer->rebind_attempts = 0;\n-\tpeer->last_rebind_time = 0;\n-\tif (peer->peer_data) {\n-\t\tpeer->peer_data->rebind_attempts = 0;\n-\t\tpeer->peer_data->last_rebind_time = 0;\n-\t}\n-\tif (peer->peer_rtcp) {\n-\t\tpeer->peer_rtcp->rebind_attempts = 0;\n-\t\tpeer->peer_rtcp->last_rebind_time = 0;\n-\t}\n-\n \trist_log_priv(get_cctx(peer), RIST_LOG_INFO,\n \t\t\t\"Successfully Authenticated peer %\"PRIu32\"\\n\", peer->adv_peer_id);\n }\n@@ -1895,39 +1527,6 @@ void rist_calculate_bitrate(size_t len, struct rist_bandwidth_estimation *bw)\n \t}\n }\n \n-/* Recompute bw->bitrate using zero new bytes so the value decays to\n- * zero when the stats tick fires but no packets have arrived on this\n- * counter since the last tick. Mirrors what rist_calculate_bitrate(0,\n- * ...) does for peer-side counters, deliberately skipping the per-flow\n- * inter-packet-spacing bookkeeping that lives in\n- * rist_calculate_flow_bitrate(). */\n-void rist_refresh_flow_bitrate(struct rist_bandwidth_estimation *bw)\n-{\n-\tif (!bw->last_bitrate_calctime)\n-\t\treturn;\n-\n-\tstruct timeval tv;\n-\tgettimeofday(&tv, NULL);\n-\tuint64_t now = (uint64_t)tv.tv_sec * 1000000;\n-\tnow += tv.tv_usec;\n-\n-\tuint64_t time_fast = now - bw->last_bitrate_calctime_fast;\n-\tif (time_fast >= 100000 /* 100 ms */) {\n-\t\tbw->bitrate_fast = (size_t)((8 * bw->bytes_fast * 1000000) / time_fast);\n-\t\tbw->eight_times_bitrate_fast += bw->bitrate_fast - bw->eight_times_bitrate_fast / 8;\n-\t\tbw->last_bitrate_calctime_fast = now;\n-\t\tbw->bytes_fast = 0;\n-\t}\n-\n-\tuint64_t time = now - bw->last_bitrate_calctime;\n-\tif (time >= 1000000 /* 1 second */) {\n-\t\tbw->bitrate = (size_t)((8 * bw->bytes * 1000000) / time);\n-\t\tbw->eight_times_bitrate += bw->bitrate - bw->eight_times_bitrate / 8;\n-\t\tbw->last_bitrate_calctime = now;\n-\t\tbw->bytes = 0;\n-\t}\n-}\n-\n static void rist_calculate_flow_bitrate(struct rist_flow *flow, size_t len, struct rist_bandwidth_estimation *bw)\n {\n \tstruct timeval tv;\n@@ -2003,11 +1602,11 @@ static void rist_sender_recv_nack(struct rist_peer *peer,\n \tassert(payload_len >= sizeof(struct rist_rtcp_hdr));\n \n \tif (peer->receiver_mode) {\n-\t\trist_log_priv(get_cctx(peer), RIST_LOG_DEBUG,\n+\t\trist_log_priv(get_cctx(peer), RIST_LOG_ERROR,\n \t\t\t\t\"Received nack packet on receiver, ignoring ...\\n\");\n \t\treturn;\n \t} else if (!peer->authenticated) {\n-\t\trist_log_priv(get_cctx(peer), RIST_LOG_DEBUG,\n+\t\trist_log_priv(get_cctx(peer), RIST_LOG_ERROR,\n \t\t\t\t\"Received nack packet but handshake is still pending, ignoring ...\\n\");\n \t\treturn;\n \t}\n@@ -2022,53 +1621,29 @@ static void rist_sender_recv_nack(struct rist_peer *peer,\n \t}\n \n \tif (rtcp->ptype == PTYPE_NACK_CUSTOM) {\n-\t\tif (payload_len < sizeof(struct rist_rtcp_nack_range))\n-\t\t\treturn;\n \t\tstruct rist_rtcp_nack_range *rtcp_nack = (struct rist_rtcp_nack_range *) payload;\n \t\tif (memcmp(rtcp_nack->name, \"RIST\", 4) != 0) {\n-\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_ERROR, \"Non-Rist nack packet (%.4s).\\n\", rtcp_nack->name);\n+\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_ERROR, \"Non-Rist nack packet (%s).\\n\", rtcp_nack->name);\n \t\t\treturn; /* Ignore app-type not RIST */\n \t\t}\n-\t\tuint16_t raw_len = ntohs(rtcp->len);\n-\t\tif (raw_len < 2)\n-\t\t\treturn;\n-\t\tuint16_t nrecords = raw_len - 2;\n-\t\tsize_t needed = sizeof(struct rist_rtcp_nack_range) + (size_t)nrecords * sizeof(struct rist_rtp_nack_record);\n-\t\tif (needed > payload_len)\n-\t\t\treturn;\n+\t\tuint16_t nrecords =\tntohs(rtcp->len) - 2;\n \t\t//rist_log_priv(get_cctx(peer), RIST_LOG_ERROR, \"Nack (RbRR), %d record(s)\\n\", nrecords);\n-\t\t/* Cap the per-record range and the per-packet total: a ~10 KB NACK\n-\t\t * otherwise drives ~1.6e8 retry-queue operations from one packet.\n-\t\t * 256 covers any honest loss burst; 4096/packet ~16 full records. */\n-\t\tsize_t enqueued = 0;\n \t\tfor (i = 0; i < nrecords; i++) {\n \t\t\tuint16_t missing;\n \t\t\tuint16_t additional;\n \t\t\tstruct rist_rtp_nack_record *nr = (struct rist_rtp_nack_record *)(payload + sizeof(struct rist_rtcp_nack_range) + i * sizeof(struct rist_rtp_nack_record));\n \t\t\tmissing =  ntohs(nr->start);\n \t\t\tadditional = ntohs(nr->extra);\n-\t\t\tif (additional > 256)\n-\t\t\t\tadditional = 256;\n \t\t\trist_retry_enqueue(peer->sender_ctx, nack_seq_msb + (uint32_t)missing, peer);\n-\t\t\tif (++enqueued >= 4096)\n-\t\t\t\treturn;\n \t\t\t//rist_log_priv(get_cctx(peer), RIST_LOG_ERROR, \"Record %\"PRIu32\": base packet: %\"PRIu32\" range len: %d\\n\", i, nack_seq_msb + missing, additional);\n \t\t\tfor (j = 0; j < additional; j++) {\n \t\t\t\trist_retry_enqueue(peer->sender_ctx, nack_seq_msb + (uint32_t)missing + j + 1, peer);\n-\t\t\t\tif (++enqueued >= 4096)\n-\t\t\t\t\treturn;\n \t\t\t}\n \t\t}\n \t} else if (rtcp->ptype == PTYPE_NACK_BITMASK) {\n \t\tstruct rist_rtcp_nack_bitmask *rtcp_nack = (struct rist_rtcp_nack_bitmask *) payload;\n \t\t(void)rtcp_nack;\n-\t\tuint16_t raw_len = ntohs(rtcp->len);\n-\t\tif (raw_len < 2)\n-\t\t\treturn;\n-\t\tuint16_t nrecords = raw_len - 2;\n-\t\tsize_t needed = sizeof(struct rist_rtcp_nack_bitmask) + (size_t)nrecords * sizeof(struct rist_rtp_nack_record);\n-\t\tif (needed > payload_len)\n-\t\t\treturn;\n+\t\tuint16_t nrecords =\tntohs(rtcp->len) - 2;\n \t\t//rist_log_priv(get_cctx(peer), RIST_LOG_ERROR, \"Nack (BbRR), %d record(s)\\n\", nrecords);\n \t\tfor (i = 0; i < nrecords; i++) {\n \t\t\tuint16_t missing;\n@@ -2336,7 +1911,7 @@ static bool rist_receiver_rtcp_authenticate(struct rist_peer *peer, uint32_t seq\n }\n \n static void rist_receiver_recv_data(struct rist_peer *peer, uint32_t seq, uint32_t flow_id,\n-\t\tuint64_t source_time, uint64_t packet_recv_time, struct rist_buffer *payload, uint8_t retry, uint8_t payload_type, size_t ingest_size, size_t ts_null_bytes, bool pkt_short_seq)\n+\t\tuint64_t source_time, uint64_t packet_recv_time, struct rist_buffer *payload, uint8_t retry, uint8_t payload_type, size_t ingest_size, size_t ts_null_bytes)\n {\n \tassert(peer->receiver_ctx != NULL);\n \tstruct rist_receiver *ctx = peer->receiver_ctx;\n@@ -2346,27 +1921,6 @@ static void rist_receiver_recv_data(struct rist_peer *peer, uint32_t seq, uint32\n \t\treturn;\n \t}\n \n-\t/* Advanced contexts default to 32-bit framing but interoperate with a\n-\t * Main-framed (16-bit) source: track the actual wire framing of the data\n-\t * so the seq/wrap math matches. Simple/Main are always 16-bit.\n-\t *\n-\t * TR-06-3 Section 9 lets a flow switch framing mid-stream (Main->Advanced\n-\t * upgrade once the peer advertises I=1, or a legacy Main-only source that\n-\t * never upgrades). The two framings carry different seq widths AND\n-\t * timestamp encodings, so mixing them in one flow corrupts the timing\n-\t * baseline. Treat a framing change like a flow-id change: drop the old\n-\t * baseline so the next enqueue re-derives time_offset and the seq->idx\n-\t * mapping from the new framing instead of blending the two. */\n-\tif (ctx->common.profile >= RIST_PROFILE_ADVANCED && peer->flow &&\n-\t    peer->flow->short_seq != pkt_short_seq) {\n-\t\trist_log_priv(&ctx->common, RIST_LOG_INFO,\n-\t\t\t\"Flow %u wire framing changed to %s sequence numbers, \"\n-\t\t\t\"resetting flow timing baseline\\n\",\n-\t\t\tpeer->flow->flow_id, pkt_short_seq ? \"16-bit\" : \"32-bit\");\n-\t\tpeer->flow->short_seq = pkt_short_seq;\n-\t\tpeer->flow->receiver_queue_has_items = false;\n-\t}\n-\n \t//rist_log_priv(&ctx->common, RIST_LOG_ERROR,\n \t//\t\"rist_recv_data, seq %\"PRIu32\", retry=%d\\n\", seq, retry);\n \n@@ -2380,7 +1934,6 @@ static void rist_receiver_recv_data(struct rist_peer *peer, uint32_t seq, uint32\n \t/* * * * * * * * * * * * * * * * * * * */\n \t/**************** WIP *****************/\n \tpeer->stats_receiver_instant.received++;\n-\tpeer->stats_receiver_instant.received_bytes += (uint64_t)ingest_size;\n \n \tuint64_t rtt;\n \trtt = peer->eight_times_rtt / 8;\n@@ -2438,84 +1991,16 @@ static void rist_recv_oob_data(struct rist_peer *peer, struct rist_buffer *paylo\n \t// TODO: if the calling app locks the thread for long, the protocol management thread will suffer\n \t// either use a new thread with a fifo or write warning on documentation\n \tstruct rist_common_ctx *ctx = get_cctx(peer);\n-\tif (!ctx->oob_data_enabled)\n-\t\treturn;\n-\n-\tif (ctx->oob_current_peer == NULL || ctx->oob_current_peer->dead)\n-\t\tctx->oob_current_peer = peer;\n-\n-\tif (ctx->oob_data_callback)\n+\tif (ctx->oob_data_enabled && ctx->oob_data_callback)\n \t{\n \t\tstruct rist_oob_block oob_block;\n+\t\tif (ctx->oob_current_peer == NULL || ctx->oob_current_peer->dead)\n+\t\t\tctx->oob_current_peer = peer;\n \t\toob_block.peer = peer;\n \t\toob_block.payload = payload->data;\n \t\toob_block.payload_len = payload->size;\n-\t\toob_block.ts_ntp = payload->source_time;\n \t\tctx->oob_data_callback(ctx->oob_data_callback_argument, &oob_block);\n-\t\treturn;\n-\t}\n-\n-\t/* No callback installed: stash the packet in the receive fifo so the\n-\t * application can pull it via rist_oob_read(). */\n-\tpthread_rwlock_wrlock(&ctx->oob_queue_lock);\n-\tif ((uint16_t)(ctx->oob_rx_queue_write_index + 1) == ctx->oob_rx_queue_read_index)\n-\t{\n-\t\tpthread_rwlock_unlock(&ctx->oob_queue_lock);\n-\t\trist_log_priv(ctx, RIST_LOG_WARN,\n-\t\t\t\t\"oob receive queue is full, dropping packet of size %zu\\n\", payload->size);\n-\t\treturn;\n \t}\n-\tstruct rist_buffer *b = rist_new_buffer(ctx, payload->data, payload->size,\n-\t\t\tRIST_PAYLOAD_TYPE_DATA_OOB, 0, payload->source_time, 0, 0);\n-\tif (RIST_UNLIKELY(!b))\n-\t{\n-\t\tpthread_rwlock_unlock(&ctx->oob_queue_lock);\n-\t\trist_log_priv(ctx, RIST_LOG_ERROR, \"Could not allocate oob receive buffer, OOM\\n\");\n-\t\treturn;\n-\t}\n-\tb->peer = peer;\n-\tctx->oob_rx_queue[ctx->oob_rx_queue_write_index] = b;\n-\tctx->oob_rx_queue_write_index = (uint16_t)(ctx->oob_rx_queue_write_index + 1);\n-\tpthread_rwlock_unlock(&ctx->oob_queue_lock);\n-}\n-\n-/* Pull one packet from the oob receive fifo.  The returned block is owned by\n- * the library and stays valid until the next rist_oob_dequeue_rx() call.\n- * Returns the number of packets that were available (>=1) when data is\n- * returned, 0 when the fifo is empty. */\n-int rist_oob_dequeue_rx(struct rist_common_ctx *ctx, const struct rist_oob_block **oob_block)\n-{\n-\t*oob_block = NULL;\n-\n-\tpthread_rwlock_wrlock(&ctx->oob_queue_lock);\n-\n-\t/* release the buffer handed out by the previous call */\n-\tif (ctx->oob_rx_current)\n-\t{\n-\t\tfree_rist_buffer(ctx, ctx->oob_rx_current);\n-\t\tctx->oob_rx_current = NULL;\n-\t}\n-\n-\tif (ctx->oob_rx_queue_read_index == ctx->oob_rx_queue_write_index)\n-\t{\n-\t\tpthread_rwlock_unlock(&ctx->oob_queue_lock);\n-\t\treturn 0;\n-\t}\n-\n-\tstruct rist_buffer *b = ctx->oob_rx_queue[ctx->oob_rx_queue_read_index];\n-\tctx->oob_rx_queue[ctx->oob_rx_queue_read_index] = NULL;\n-\tctx->oob_rx_queue_read_index = (uint16_t)(ctx->oob_rx_queue_read_index + 1);\n-\tint available = (uint16_t)(ctx->oob_rx_queue_write_index - ctx->oob_rx_queue_read_index) + 1;\n-\n-\tctx->oob_rx_current = b;\n-\tctx->oob_rx_block.peer = b->peer;\n-\tctx->oob_rx_block.payload = (uint8_t *)b->data + RIST_MAX_PAYLOAD_OFFSET;\n-\tctx->oob_rx_block.payload_len = b->size;\n-\tctx->oob_rx_block.ts_ntp = b->source_time;\n-\t*oob_block = &ctx->oob_rx_block;\n-\n-\tpthread_rwlock_unlock(&ctx->oob_queue_lock);\n-\treturn available;\n }\n \n static void rist_rtcp_handle_echo_request(struct rist_peer *peer, struct rist_rtcp_echoext *echoreq) {\n@@ -2532,7 +2017,14 @@ static void rist_rtcp_handle_echo_response(struct rist_peer *peer, struct rist_r\n \t\treturn;\n \tuint64_t request_time = ((uint64_t)be32toh(echoreq->ntp_msw) << 32) | be32toh(echoreq->ntp_lsw);\n \tuint64_t rtt = calculate_rtt_delay(request_time, timestampNTP_u64(), be32toh(echoreq->delay));\n-\trist_peer_rtt_update(peer, rtt);\n+\tpeer->last_rtt = rtt;\n+\tpeer->eight_times_rtt -= peer->eight_times_rtt / 8;\n+\tpeer->eight_times_rtt += peer->last_rtt;\n+\tif (peer->peer_data && peer->peer_data != peer)\n+\t{\n+\t\tpeer->peer_data->last_rtt = peer->last_rtt;\n+\t\tpeer->peer_data->eight_times_rtt = peer->eight_times_rtt;\n+\t}\n }\n \n static void rist_handle_sr_pkt(struct rist_peer *peer, struct rist_rtcp_sr_pkt *sr) {\n@@ -2557,13 +2049,9 @@ static void rist_handle_rr_pkt(struct rist_peer *peer, struct rist_rtcp_rr_pkt *\n \tuint64_t rtt;\n \tif (!peer->last_sender_report_ts)\n \t\treturn;\n-\tuint64_t dlsr = (uint64_t)be32toh(rr->dlsr) << 16;\n \tif (lsr_ntp == lsr_tmp) {\n \t\tuint64_t now = timestampNTP_u64();\n-\t\t// guard the unsigned subtraction; dlsr comes from the wire\n-\t\tif (peer->last_sender_report_ts + dlsr > now)\n-\t\t\treturn;\n-\t\trtt = now - peer->last_sender_report_ts - dlsr;\n+\t\trtt = now - peer->last_sender_report_ts - ((uint64_t)be32toh(rr->dlsr) << 16);\n \n \t} else {\n \t\tif (!lsr_ntp)//this can happen on the first time\n@@ -2573,64 +2061,62 @@ static void rist_handle_rr_pkt(struct rist_peer *peer, struct rist_rtcp_rr_pkt *\n \t\tuint64_t now_rtc = timestampNTP_RTC_u64();\n \t\tlsr_ntp = (lsr_ntp << 16) & 0x0000FFFFFFFF0000;\n \t\tlsr_ntp |= (now_rtc & 0xFFFF000000000000);\n-\t\tif (lsr_ntp > now_rtc || lsr_ntp + dlsr > now_rtc)\n+\t\tif (lsr_ntp > now_rtc)\n \t\t\treturn;\n-\t\trtt  = now_rtc - lsr_ntp  - dlsr;\n+\t\trtt  = now_rtc - lsr_ntp  - ((uint64_t)be32toh(rr->dlsr) << 16);\n+\t}\n+\tpeer->last_rtt = rtt;\n+\tpeer->eight_times_rtt -= peer->eight_times_rtt / 8;\n+\tpeer->eight_times_rtt += peer->last_rtt;\n+\tif (peer->peer_data && peer->peer_data != peer)\n+\t{\n+\t\tpeer->peer_data->last_rtt = peer->last_rtt;\n+\t\tpeer->peer_data->eight_times_rtt = peer->eight_times_rtt;\n \t}\n-\trist_peer_rtt_update(peer, rtt);\n }\n \n-static void rist_handle_xr_pkt(struct rist_peer *peer, uint8_t xr_pkt[], size_t pkt_len)\n+static void rist_handle_xr_pkt(struct rist_peer *peer, uint8_t xr_pkt[])\n {\n \tsize_t offset = 0;\n-\tif (pkt_len < sizeof(struct rist_rtcp_hdr))\n-\t\treturn;\n \tstruct rist_rtcp_hdr *hdr = (struct rist_rtcp_hdr *)&xr_pkt[offset];\n-\t// clamp the declared len to what we actually received\n-\tsize_t payload_len = ((size_t)be16toh(hdr->len) + 1) * 4;\n-\tif (payload_len > pkt_len)\n-\t\tpayload_len = pkt_len;\n-\tif (payload_len < sizeof(struct rist_rtcp_hdr))\n-\t\treturn;\n-\tsize_t bytes_remaining = payload_len - sizeof(struct rist_rtcp_hdr);\n+\tsize_t payload_len = (be16toh(hdr->len) +1) * 4;\n+\tssize_t bytes_remaining = payload_len - sizeof(struct rist_rtcp_hdr);\n \toffset += sizeof(struct rist_rtcp_hdr);\n-\twhile (bytes_remaining >= sizeof(struct rist_rtcp_xr_block_hdr))\n+\twhile (bytes_remaining > 0)\n \t{\n \t\tstruct rist_rtcp_xr_block_hdr *block = (struct rist_rtcp_xr_block_hdr *)&xr_pkt[offset];\n \t\tuint8_t block_type = block->type;\n-\t\tsize_t block_length = ((size_t)be16toh(block->length) + 1) * 4;\n-\t\tif (block_length < sizeof(struct rist_rtcp_xr_block_hdr) ||\n-\t\t    block_length > bytes_remaining)\n-\t\t\treturn;\n+\t\tuint16_t block_length = (be16toh(block->length)+1) * 4;\n \t\tif (block_type == 5)\n \t\t{\n-\t\t\tif (block_length < sizeof(struct rist_rtcp_xr_dlrr))\n-\t\t\t\treturn;\n \t\t\tstruct rist_rtcp_xr_dlrr *dlrr = (struct rist_rtcp_xr_dlrr *)&xr_pkt[offset];\n \t\t\tuint32_t ssrc  = be32toh(dlrr->ssrc);\n \t\t\tif (ssrc != peer->peer_ssrc)\n \t\t\t\treturn;\n \t\t\tuint64_t lrr_tmp = (peer->last_sender_report_ts >> 16) & 0xFFFFFFFF;\n \t\t\tuint64_t lrr = be32toh(dlrr->lrr);\n-\t\t\tuint64_t delay = (uint64_t)be32toh(dlrr->delay) << 16;\n \t\t\tuint64_t rtt;\n \t\t\tif (lrr == lrr_tmp)\n \t\t\t{\n-\t\t\t\tuint64_t now = timestampNTP_u64();\n-\t\t\t\t// guard the unsigned subtraction; delay comes from the wire\n-\t\t\t\tif (peer->last_sender_report_ts + delay > now)\n-\t\t\t\t\treturn;\n-\t\t\t\trtt = now - peer->last_sender_report_ts - delay;\n+\t\t\t\trtt = timestampNTP_u64() - peer->last_sender_report_ts - ((uint64_t)be32toh(dlrr->delay) << 16);\n+\n \t\t\t} else {\n \t\t\t\t//Slightly less accurate, needed when RTT is bigger than our RTCP interval.\n \t\t\t\tuint64_t now = timestampNTP_u64();\n \t\t\t\tlrr = (lrr << 16) & 0x0000FFFFFFFF0000;\n \t\t\t\tlrr |= (now & 0xFFFF000000000000);\n-\t\t\t\tif (lrr > now || lrr + delay > now)\n+\t\t\t\tif (lrr > now)\n \t\t\t\t\treturn;\n-\t\t\t\trtt  = now - lrr - delay;\n+\t\t\t\trtt  = now - lrr  - ((uint64_t)be32toh(dlrr->delay) << 16);\n+\t\t\t}\n+\t\t\tpeer->last_rtt = rtt;\n+\t\t\tpeer->eight_times_rtt -= peer->eight_times_rtt /8;\n+\t\t\tpeer->eight_times_rtt += peer->last_rtt;\n+\t\t\tif (peer->peer_data && peer->peer_data != peer)\n+\t\t\t{\n+\t\t\t\tpeer->peer_data->last_rtt = peer->last_rtt;\n+\t\t\t\tpeer->peer_data->eight_times_rtt = peer->eight_times_rtt;\n \t\t\t}\n-\t\t\trist_peer_rtt_update(peer, rtt);\n \t\t}\n \t\toffset += block_length;\n \t\tbytes_remaining -= block_length;\n@@ -2657,9 +2143,6 @@ static char *get_ip_str(struct sockaddr *sa, char *s, size_t maxlen)\n \treturn s;\n }\n \n-static bool try_listener_reassociate_by_cname(struct rist_peer *new_peer, uint64_t now);\n-static bool try_caller_socket_rebind(struct rist_peer *peer, uint64_t now);\n-\n static void rist_recv_rtcp(struct rist_peer *peer, uint32_t seq,\n \t\tuint32_t flow_id, struct rist_buffer *payload)\n {\n@@ -2668,6 +2151,7 @@ static void rist_recv_rtcp(struct rist_peer *peer, uint32_t seq,\n \tuint16_t processed_bytes = 0;\n \tuint16_t records;\n \tuint8_t subtype;\n+\tuint32_t nack_seq_msb = 0;\n \tpeer->stats_receiver_instant.received_rtcp++;\n \tstruct rist_common_ctx *ctx = get_cctx(peer);\n \n@@ -2675,12 +2159,12 @@ static void rist_recv_rtcp(struct rist_peer *peer, uint32_t seq,\n \t\tpkt = (uint8_t*)payload->data + processed_bytes;\n \t\tstruct rist_rtcp_hdr *rtcp = (struct rist_rtcp_hdr *)pkt;\n \t\t/* safety checks */\n-\t\tsize_t bytes_left = payload->size - processed_bytes;\n+\t\tsize_t bytes_left = payload->size - processed_bytes + 1;\n \n \t\tif ( bytes_left < 4 )\n \t\t{\n \t\t\t/* we must have at least 4 bytes */\n-\t\t\trist_log_priv(ctx, RIST_LOG_ERROR, \"Rist rtcp packet must have at least 4 bytes, we have %zu\\n\",\n+\t\t\trist_log_priv(ctx, RIST_LOG_ERROR, \"Rist rtcp packet must have at least 4 bytes, we have %d\\n\",\n \t\t\t\t\tbytes_left);\n \t\t\treturn;\n \t\t}\n@@ -2688,29 +2172,30 @@ static void rist_recv_rtcp(struct rist_peer *peer, uint32_t seq,\n \t\tptype = rtcp->ptype;\n \t\tsubtype = rtcp->flags & 0x1f;\n \t\trecords = be16toh(rtcp->len);\n-\t\t// in size_t so 4*(1+0xFFFF) cannot wrap to 0 and spin the loop\n-\t\tsize_t bytes = (size_t)4 * ((size_t)1 + records);\n-\t\tif (bytes < 4 || bytes > bytes_left)\n+\t\tuint16_t bytes = (uint16_t)(4 * (1 + records));\n+\t\tif (bytes > bytes_left)\n \t\t{\n \t\t\t/* check for a sane number of bytes */\n-\t\t\trist_log_priv(ctx, RIST_LOG_ERROR, \"Malformed feedback packet, expecting %zu bytes in the\" \\\n-\t\t\t\t\t\" packet, got a buffer of %zu bytes. ptype = %d\\n\", bytes,\n+\t\t\trist_log_priv(ctx, RIST_LOG_ERROR, \"Malformed feedback packet, expecting %u bytes in the\" \\\n+\t\t\t\t\t\" packet, got a buffer of %u bytes. ptype = %d\\n\", bytes,\n \t\t\t\t\tbytes_left, ptype);\n \t\t\treturn;\n \t\t}\n \n \t\tswitch(ptype) {\n \t\t\tcase PTYPE_NACK_CUSTOM:\n-\t\t\t\tif (subtype == ECHO_RESPONSE) {\n-\t\t\t\t\tif (bytes < sizeof(struct rist_rtcp_echoext))\n-\t\t\t\t\t\tbreak;\n+\t\t\t\tif (subtype == NACK_FMT_SEQEXT)\n+\t\t\t\t{\n+\t\t\t\t\tstruct rist_rtcp_seqext *seq_ext = (struct rist_rtcp_seqext *) pkt;\n+\t\t\t\t\tnack_seq_msb = ((uint32_t)be16toh(seq_ext->seq_msb)) << 16;\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t\telse if (subtype == ECHO_RESPONSE) {\n \t\t\t\t\tstruct rist_rtcp_echoext *echoresponse = (struct rist_rtcp_echoext *) pkt;\n \t\t\t\t\trist_rtcp_handle_echo_response(peer, echoresponse);\n \t\t\t\t\tbreak;\n \t\t\t\t}\n \t\t\t\telse if (subtype == ECHO_REQUEST) {\n-\t\t\t\t\tif (bytes < sizeof(struct rist_rtcp_echoext))\n-\t\t\t\t\t\tbreak;\n \t\t\t\t\tstruct rist_rtcp_echoext *echorequest = (struct rist_rtcp_echoext *)pkt;\n \t\t\t\t\trist_rtcp_handle_echo_request(peer, echorequest);\n \t\t\t\t\tbreak;\n@@ -2725,7 +2210,7 @@ static void rist_recv_rtcp(struct rist_peer *peer, uint32_t seq,\n \t\t\t\t}\n \t\t\tcase PTYPE_NACK_BITMASK:\n \t\t\t\t//Also FMT Range\n-\t\t\t\trist_sender_recv_nack(peer, flow_id, payload->src_port, payload->dst_port, pkt, bytes_left, 0);\n+\t\t\t\trist_sender_recv_nack(peer, flow_id, payload->src_port, payload->dst_port, pkt, bytes_left, nack_seq_msb);\n \t\t\t\tbreak;\n \t\t\tcase PTYPE_RR:\n \t\t\t\tif (ntohs(rtcp->len) == 7) {\n@@ -2737,25 +2222,17 @@ static void rist_recv_rtcp(struct rist_peer *peer, uint32_t seq,\n \t\t\tcase PTYPE_SDES:\n \t\t\t\t{\n \t\t\t\t\tpeer->stats_sender_instant.received++;\n-\t\t\t\t\tif (bytes_left < RTCP_SDES_SIZE)\n-\t\t\t\t\t{\n-\t\t\t\t\t\trist_log_priv(ctx, RIST_LOG_ERROR, \"Malformed SDES packet, %zu bytes < SDES header size\\n\",\n-\t\t\t\t\t\t\t\tbytes_left);\n-\t\t\t\t\t\treturn;\n-\t\t\t\t\t}\n \t\t\t\t\tuint8_t name_length = pkt[9];\n-\t\t\t\t\t// must fit the remaining packet AND receiver_name (incl NUL)\n-\t\t\t\t\tif ((size_t)name_length + RTCP_SDES_SIZE > bytes_left ||\n-\t\t\t\t\t    name_length >= sizeof(peer->receiver_name))\n+\t\t\t\t\tif (name_length > bytes_left)\n \t\t\t\t\t{\n-\t\t\t\t\t\trist_log_priv(ctx, RIST_LOG_ERROR, \"Malformed SDES packet, cname len %u out of bounds (left %zu, max %zu)\\n\",\n-\t\t\t\t\t\t\t\tname_length, bytes_left, sizeof(peer->receiver_name) - 1);\n+\t\t\t\t\t\t/* check for a sane number of bytes */\n+\t\t\t\t\t\trist_log_priv(ctx, RIST_LOG_ERROR, \"Malformed SDES packet, wrong cname len %u, got a \" \\\n+\t\t\t\t\t\t\t\t\"buffer of %u bytes.\\n\", name_length, bytes_left);\n \t\t\t\t\t\treturn;\n \t\t\t\t\t}\n \t\t\t\t\tif (memcmp(pkt + RTCP_SDES_SIZE, peer->receiver_name, name_length) != 0)\n \t\t\t\t\t{\n \t\t\t\t\t\tmemcpy(peer->receiver_name, pkt + RTCP_SDES_SIZE, name_length);\n-\t\t\t\t\t\tpeer->receiver_name[name_length] = '\\0';\n \t\t\t\t\t\trist_log_priv(ctx, RIST_LOG_INFO, \"Peer %\"PRIu32\" receiver name is now: %s\\n\",\n \t\t\t\t\t\t\t\tpeer->adv_peer_id, peer->receiver_name);\n \t\t\t\t\t}\n@@ -2773,14 +2250,6 @@ static void rist_recv_rtcp(struct rist_peer *peer, uint32_t seq,\n \t\t\t\t\t}\n \t\t\t\t\telse {\n \t\t\t\t\t\tconnection_message = RIST_CONNECTION_ESTABLISHED;\n-\t\t\t\t\t\t/* Re-associate by cname before announcing a\n-\t\t\t\t\t\t * brand-new caller.  On success this kills the\n-\t\t\t\t\t\t * new peer and returns, dropping any RTCP records\n-\t\t\t\t\t\t * that followed the SDES in the same compound\n-\t\t\t\t\t\t * packet. */\n-\t\t\t\t\t\tif (!peer->send_first_connection_event &&\n-\t\t\t\t\t\t    try_listener_reassociate_by_cname(peer, timestampNTP_u64()))\n-\t\t\t\t\t\t\treturn;\n \t\t\t\t\t}\n \t\t\t\t\tif (peer->timed_out || !peer->send_first_connection_event || (!peer_authenticated && peer->authenticated)) {\n \t\t\t\t\t\tif (!peer->send_first_connection_event)\n@@ -2794,18 +2263,16 @@ static void rist_recv_rtcp(struct rist_peer *peer, uint32_t seq,\n \t\t\t\tbreak;\n \t\t\t}\n \t\t\tcase PTYPE_SR:;\n-\t\t\t\tif (bytes < sizeof(struct rist_rtcp_sr_pkt))\n-\t\t\t\t\tbreak;\n \t\t\t\tstruct rist_rtcp_sr_pkt *sr = (struct rist_rtcp_sr_pkt *)pkt;\n \t\t\t\trist_handle_sr_pkt(peer, sr);\n \t\t\t\tbreak;\n \t\t\tcase PTYPE_XR:\n-\t\t\t\trist_handle_xr_pkt(peer, pkt, bytes);\n+\t\t\t\trist_handle_xr_pkt(peer, pkt);\n \t\t\t\tbreak;\n \t\t\tdefault:\n \t\t\t\trist_log_priv(ctx, RIST_LOG_DEBUG, \"Unrecognized RTCP packet with PTYPE=%02x!!\\n\", ptype);\n \t\t}\n-\t\tprocessed_bytes += (uint16_t)bytes;\n+\t\tprocessed_bytes += bytes;\n \t}\n \n }\n@@ -2837,15 +2304,11 @@ static void rist_peer_sockerr(struct evsocket_ctx *evctx, int fd, short revents,\n \tRIST_MARK_UNUSED(revents);\n \tstruct rist_peer *peer = (struct rist_peer *) arg;\n #ifdef _WIN32\n-\t/* WSAECONNRESET indications are disabled at socket creation time via\n-\t * WSAIoctl(SIO_UDP_CONNRESET, FALSE) - see udpsocket_open(). The earlier\n-\t * recvfrom(drain[1], ...) drain has been removed because it could\n-\t * cannibalise (or truncate via WSAEMSGSIZE) a real datagram queued\n-\t * ahead of - or instead of - the synthetic error indication, leading\n-\t * to malformed EAPOL packets and other downstream failures. */\n-\tint wsa_err = WSAGetLastError();\n-\tif (wsa_err == WSAECONNRESET)\n+\tif (WSAGetLastError() == WSAECONNRESET) {\n+\t\t//recvfrom clears the WSAECONNRESET error\n+\t\trecvfrom(peer->sd, NULL, 0, 0, NULL, 0);\n \t\treturn;\n+\t}\n #endif\n \trist_log_priv(get_cctx(peer), RIST_LOG_ERROR, \"\\tSocket error!\\n\");\n \n@@ -2868,11 +2331,8 @@ static void peer_copy_settings(struct rist_peer *peer_src, struct rist_peer *pee\n \t_librist_crypto_psk_rist_key_clone(&peer_src->key_tx, &peer->key_tx_odd);\n \tpeer->key_tx_odd_active = peer_src->key_tx_odd_active;\n \tstrncpy(&peer->cname[0], &peer_src->cname[0], RIST_MAX_STRING_SHORT);\n-\tpeer->cname[RIST_MAX_STRING_SHORT - 1] = '\\0';\n \tstrncpy(&peer->miface[0], &peer_src->miface[0], RIST_MAX_STRING_SHORT);\n-\tpeer->miface[RIST_MAX_STRING_SHORT - 1] = '\\0';\n \tpeer->config.weight = peer_src->config.weight;\n-\tpeer->config.recovery_priority = peer_src->config.recovery_priority;\n \tpeer->config.virt_dst_port = peer_src->config.virt_dst_port;\n \tpeer->config.recovery_mode = peer_src->config.recovery_mode;\n \tpeer->config.recovery_maxbitrate = peer_src->config.recovery_maxbitrate;\n@@ -2886,15 +2346,6 @@ static void peer_copy_settings(struct rist_peer *peer_src, struct rist_peer *pee\n \tpeer->config.min_retries = peer_src->config.min_retries;\n \tpeer->config.max_retries = peer_src->config.max_retries;\n \tpeer->config.timing_mode = peer_src->config.timing_mode;\n-\tpeer->config.reflector = peer_src->config.reflector;\n-\tpeer->config.multicast_ttl = peer_src->config.multicast_ttl;\n-\tstrncpy(peer->config.multicast_source, peer_src->config.multicast_source, RIST_MAX_STRING_LONG - 1);\n-\tpeer->config.multicast_source[RIST_MAX_STRING_LONG - 1] = '\\0';\n-\tpeer->config.local_port = peer_src->config.local_port;\n-\tpeer->config.rtt_drop = peer_src->config.rtt_drop;\n-\tpeer->config.rtt_restore = peer_src->config.rtt_restore;\n-\tpeer->config.rtt_drop_settle = peer_src->config.rtt_drop_settle;\n-\tpeer->config.rtt_drop_trickle = peer_src->config.rtt_drop_trickle;\n \tpeer->rtcp_keepalive_interval = peer_src->rtcp_keepalive_interval;\n \tpeer->peer_ssrc = peer_src->peer_ssrc;\n \tpeer->session_timeout = peer_src->session_timeout;\n@@ -2912,217 +2363,6 @@ static void kill_peer(struct rist_peer *peer)\n \tpeer->dead_since = timestampNTP_u64();\n }\n \n-/* Listener-side cname re-association for a NAT source-port rebind.\n- * SRP only: the cname is not a per-peer secret, so an authenticated\n- * per-peer session is required before migrating an identity to a new\n- * source tuple.  Returns true after killing new_peer. */\n-static bool try_listener_reassociate_by_cname(struct rist_peer *new_peer, uint64_t now)\n-{\n-\tstruct rist_peer *parent = new_peer->parent;\n-\tif (!parent || !parent->listening || !new_peer->sender_ctx ||\n-\t    new_peer->receiver_mode || new_peer->receiver_name[0] == '\\0')\n-\t\treturn false;\n-\n-#if HAVE_SRP_SUPPORT\n-\tif (!new_peer->eap_ctx || !eap_is_authenticated(new_peer->eap_ctx))\n-\t\treturn false;\n-#else\n-\t/* No SRP: there is no authenticated per-peer session to gate on, and\n-\t * the cname is not a per-peer secret, so reassociation is unsafe. */\n-\treturn false;\n-#endif\n-\n-\tuint64_t ka = new_peer->rtcp_keepalive_interval\n-\t              ? new_peer->rtcp_keepalive_interval\n-\t              : (uint64_t)RIST_PING_INTERVAL * RIST_CLOCK;\n-\tuint64_t silent_threshold = 2 * ka;\n-\n-\tstruct rist_peer *candidate = NULL;\n-\tint alive_duplicates = 0;\n-\tstruct rist_peer *sib = parent->child;\n-\twhile (sib) {\n-\t\tif (sib != new_peer &&\n-\t\t    sib->is_rtcp == new_peer->is_rtcp &&\n-\t\t    sib->is_data == new_peer->is_data &&\n-\t\t    sib->adv_flow_id == new_peer->adv_flow_id &&\n-\t\t    sib->receiver_name[0] != '\\0' &&\n-\t\t    strncmp(sib->receiver_name, new_peer->receiver_name,\n-\t\t            RIST_MAX_HOSTNAME) == 0) {\n-\t\t\tbool silent = sib->last_pkt_received > 0 &&\n-\t\t\t              now > sib->last_pkt_received &&\n-\t\t\t              (now - sib->last_pkt_received) > silent_threshold;\n-\t\t\tif (sib->dead || silent) {\n-\t\t\t\tif (candidate == NULL)\n-\t\t\t\t\tcandidate = sib;\n-\t\t\t} else {\n-\t\t\t\talive_duplicates++;\n-\t\t\t}\n-\t\t}\n-\t\tsib = sib->sibling_next;\n-\t}\n-\n-\tif (!candidate || alive_duplicates > 0)\n-\t\treturn false;\n-\n-\tmemcpy(&candidate->u.address, &new_peer->u.address, new_peer->address_len);\n-\tcandidate->address_len = new_peer->address_len;\n-\tcandidate->address_family = new_peer->address_family;\n-\tcandidate->remote_port = new_peer->remote_port;\n-\tcandidate->dead = 0;\n-\tcandidate->timed_out = 0;\n-\tcandidate->last_pkt_received = now;\n-\trist_log_priv(get_cctx(new_peer), RIST_LOG_INFO,\n-\t    \"cname \\\"%s\\\" matched existing peer %\"PRIu32\n-\t    \"; migrated source tuple from new peer %\"PRIu32\n-\t    \" and retired it (NAT-rebind recovery, SRP).\\n\",\n-\t    new_peer->receiver_name, candidate->adv_peer_id,\n-\t    new_peer->adv_peer_id);\n-\n-#if HAVE_SRP_SUPPORT\n-\t/* Kick a fresh EAPOL START so re-auth fires now, not up to\n-\t * EAP_REAUTH_PERIOD later. */\n-\tif (candidate->eap_ctx)\n-\t\t_librist_proto_eap_start(candidate->eap_ctx);\n-#endif\n-\n-\tkill_peer(new_peer);\n-\treturn true;\n-}\n-\n-/* Caller-side recovery when a peer goes silent past session_timeout.\n- * Receiver-mode callers rebind the local socket (NAT rebind / listener\n- * restart); SRP callers also reset EAP and re-handshake on the fresh socket.\n- * Sender-mode callers only reach the SRP path: their miface-bound socket\n- * survives a flap, so they reset EAP without rebinding (see the branches).\n- * Linear backoff capped at REBIND_BACKOFF_CAP. */\n-#define REBIND_BACKOFF_CAP 10\n-static bool try_caller_socket_rebind(struct rist_peer *peer, uint64_t now)\n-{\n-\tstruct rist_common_ctx *cctx = get_cctx(peer);\n-\tif (!peer || peer->parent || peer->listening ||\n-\t    peer->multicast_sender || peer->multicast_receiver)\n-\t\treturn false;\n-\tif (cctx->profile <= RIST_PROFILE_SIMPLE)\n-\t\treturn false;\n-\tif (peer->config.local_port != 0)\n-\t\treturn false;\n-\n-\t/* Require silence beyond max(session_timeout, 4*keepalive) so a\n-\t * misconfigured short session_timeout against a slower keepalive\n-\t * cadence does not trigger a flap loop on legitimate streams. */\n-\tuint64_t ka = peer->rtcp_keepalive_interval\n-\t              ? peer->rtcp_keepalive_interval\n-\t              : (uint64_t)RIST_PING_INTERVAL * RIST_CLOCK;\n-\tuint64_t min_silence = peer->session_timeout > 4 * ka\n-\t                       ? peer->session_timeout : 4 * ka;\n-\tif (peer->last_pkt_received == 0 ||\n-\t    now <= peer->last_pkt_received ||\n-\t    (now - peer->last_pkt_received) <= min_silence)\n-\t\treturn false;\n-\n-\tuint32_t backoff_mult = peer->rebind_attempts > REBIND_BACKOFF_CAP\n-\t                        ? REBIND_BACKOFF_CAP : peer->rebind_attempts;\n-\tuint64_t min_gap = (uint64_t)backoff_mult * peer->session_timeout;\n-\tif (peer->last_rebind_time != 0 && now > peer->last_rebind_time &&\n-\t    (now - peer->last_rebind_time) < min_gap)\n-\t\treturn false;\n-\n-\tif (!peer->receiver_mode) {\n-#if HAVE_SRP_SUPPORT\n-\t\t/* Sender-mode leg: the miface-bound socket survives a flap, so\n-\t\t * don't rebind it -- just reset EAP and re-drive the handshake on\n-\t\t * the existing socket.  Only SRP deadlocks like this; plaintext/PSK\n-\t\t * recover via normal reconnect.\n-\t\t *\n-\t\t * De-authenticate the leg so it drops out of the weighted sender\n-\t\t * balancing while it is silent (the balancer keeps a leg in rotation\n-\t\t * only while authenticated) and re-drives the connection handshake.\n-\t\t * eap_authentication_state is rewound to 1 so the \"EAP Authentication\n-\t\t * succeeded\" transition fires again when re-auth completes; that path\n-\t\t * restores authenticated and folds the leg back into the bond at full\n-\t\t * weight (without this, the leg re-authenticates but never rejoins\n-\t\t * balancing, streaming only NACK retransmits). */\n-\t\tif (peer->eap_ctx == NULL)\n-\t\t\treturn false;\n-\t\tpeer->authenticated = false;\n-\t\tpeer->eap_authentication_state = 1;\n-\t\tpeer->dead = 0;\n-\t\tpeer->timed_out = 0;\n-\t\tpeer->last_pkt_received = now;\n-\t\teap_reset_authenticatee(peer->eap_ctx);\n-\t\tpeer->rebind_attempts++;\n-\t\tpeer->last_rebind_time = now;\n-\t\trist_log_priv(cctx, RIST_LOG_WARN,\n-\t\t    \"Sender caller peer %\"PRIu32\" silent past session_timeout \"\n-\t\t    \"(attempt %\"PRIu32\"); reset EAP and re-initiated the SRP \"\n-\t\t    \"handshake to recover the leg without operator intervention.\\n\",\n-\t\t    peer->adv_peer_id, peer->rebind_attempts);\n-\t\treturn true;\n-#else\n-\t\treturn false;\n-#endif\n-\t}\n-\n-\tstruct evsocket_ctx *evctx = cctx->evctx;\n-\tint old_sd = peer->sd;\n-\tuint16_t old_local_port = peer->local_port;\n-\n-\tif (peer->event_recv) {\n-\t\tevsocket_delevent(evctx, peer->event_recv);\n-\t\tpeer->event_recv = NULL;\n-\t}\n-\tif (old_sd >= 0) {\n-\t\tudpsocket_close(old_sd);\n-\t\tpeer->sd = -1;\n-\t}\n-\n-\trist_create_socket(peer);\n-\tif (peer->sd < 0) {\n-\t\trist_log_priv(cctx, RIST_LOG_ERROR,\n-\t\t    \"Caller socket rebind failed for peer %\"PRIu32\n-\t\t    \" (errno=%d), falling through to kill_peer.\\n\",\n-\t\t    peer->adv_peer_id, errno);\n-\t\treturn false;\n-\t}\n-\n-\tpeer->event_recv = evsocket_addevent(evctx, peer->sd, EVSOCKET_EV_READ,\n-\t                                     rist_peer_recv_wrap,\n-\t                                     rist_peer_sockerr, peer);\n-\n-\t/* Force a fresh handshake on the new tuple. */\n-\tpeer->authenticated = false;\n-\tpeer->dead = 0;\n-\tpeer->timed_out = 0;\n-\tpeer->last_pkt_received = now;\n-\tpeer->next_periodic_rtcp = now;\n-\tpeer->next_keepalive_packet = now;\n-\tpeer->send_keepalive = true;\n-\tpeer->send_first_connection_event = false;\n-\n-#if HAVE_SRP_SUPPORT\n-\t/* SRP callers: the authenticated session is bound to the old source\n-\t * tuple and (after a listener restart) to state the far end no longer\n-\t * has.  Reset EAP back to UNAUTH and re-send EAPOL START on the new\n-\t * socket so the authenticator re-challenges us immediately, exactly as\n-\t * on a cold connect.  Non-SRP callers just resume via the keepalives\n-\t * armed above. */\n-\tif (peer->eap_ctx != NULL)\n-\t\teap_reset_authenticatee(peer->eap_ctx);\n-#endif\n-\n-\tpeer->rebind_attempts++;\n-\tpeer->last_rebind_time = now;\n-\n-\trist_log_priv(cctx, RIST_LOG_WARN,\n-\t    \"Receiver peer %\"PRIu32\" silent past session_timeout \"\n-\t    \"(attempt %\"PRIu32\"); rebound caller socket %d->%d \"\n-\t    \"(local_port %u->%u) for NAT/dynamic-IP recovery.\\n\",\n-\t    peer->adv_peer_id, peer->rebind_attempts, old_sd, peer->sd,\n-\t    (unsigned)old_local_port, (unsigned)peer->local_port);\n-\n-\treturn true;\n-}\n-\n static void rist_peer_recv_wrap(struct evsocket_ctx *evctx, int fd, short revents, void *arg) {\n \tbool again = true;\n \twhile (true) {\n@@ -3133,27 +2373,27 @@ static void rist_peer_recv_wrap(struct evsocket_ctx *evctx, int fd, short revent\n }\n \n static void rist_new_connection(struct rist_peer *peer, struct rist_peer *p, uint32_t flow_id) {\n-\tconst char *peer_type = \"?\";\n-\tconst char *id_name = \"?\";\n+\tchar peer_type[5];\n+\tchar id_name[8];\n \tif (peer->is_rtcp) {\n-\t\tpeer_type = \"RTCP\";\n-\t\tid_name = \"flow_id\";\n+\t\tstrcpy(peer_type, \"RTCP\");\n+\t\tstrcpy(id_name, \"flow_id\");\n \t} else if (peer->is_data) {\n-\t\tpeer_type = \"RTP\";\n-\t\tid_name = \"ssrc\";\n+\t\tstrcpy(peer_type, \"RTP\");\n+\t\tstrcpy(id_name, \"ssrc\");\n \t}\n \tif (peer->receiver_mode) {\n \t\trist_log_priv(get_cctx(peer), RIST_LOG_INFO, \"New %s peer connecting, %s %\"PRIu32\", peer_id %\"PRIu32\", ports %u <- %u\\n\",\n-\t\tpeer_type, id_name, flow_id, p->adv_peer_id, p->local_port, p->remote_port);\n+\t\t&peer_type, &id_name, flow_id, p->adv_peer_id, p->local_port, p->remote_port);\n \t\tp->adv_flow_id = flow_id;\n \t}\n \telse {\n \t\tif (flow_id) {\n \t\t\trist_log_priv(get_cctx(peer), RIST_LOG_INFO, \"New reverse %s peer connecting with old flow_id %\"PRIu32\", peer_id %\"PRIu32\", ports %u <- %u\\n\",\n-\t\t\tpeer_type, flow_id, p->adv_peer_id, p->local_port, p->remote_port);\n+\t\t\t&peer_type, flow_id, p->adv_peer_id, p->local_port, p->remote_port);\n \t\t} else {\n \t\t\trist_log_priv(get_cctx(peer), RIST_LOG_INFO, \"New reverse %s peer connecting, peer_id %\"PRIu32\", ports %u <- %u\\n\",\n-\t\t\tpeer_type, p->adv_peer_id, p->local_port, p->remote_port);\n+\t\t\t&peer_type, p->adv_peer_id, p->local_port, p->remote_port);\n \t\t}\n \t\tp->peer_ssrc = p->adv_flow_id = p->sender_ctx->adv_flow_id;\n \t}\n@@ -3182,7 +2422,7 @@ static void rist_peer_recv(struct evsocket_ctx *evctx, int fd, short revents, vo\n \tuint8_t *recv_buf_npd = cctx->buf.recv_npd;\n \tuint16_t port = 0;\n \n-\tssize_t ret = rist_transport_recvfrom(peer, recv_buf, RIST_MAX_PACKET_SIZE, MSG_DONTWAIT, addr, &addrlen);\n+\tssize_t ret = recvfrom(peer->sd, (char*)recv_buf, RIST_MAX_PACKET_SIZE, MSG_DONTWAIT, (struct sockaddr *)addr, &addrlen);\n \tif (ss.ss_family == AF_INET)\n \t\tport = htons(((struct sockaddr_in *)addr)->sin_port);\n \telse\n@@ -3192,11 +2432,7 @@ static void rist_peer_recv(struct evsocket_ctx *evctx, int fd, short revents, vo\n \tif (ret <= 0) {\n \t\t*again = false;\n \t\tint errorcode = errno;\n-\t\t/* Custom transports compiled in a different TU may have EAGAIN\n-\t\t * resolve to a different numeric value than this TU; match both\n-\t\t * canonical values defensively. */\n-\t\tif (errno == EAGAIN || errno == EWOULDBLOCK ||\n-\t\t    (ret == -1 && (errorcode == 11 || errorcode == 35)))\n+\t\tif (errno == EAGAIN || errno == EWOULDBLOCK)\n \t\t\t\treturn;\n #else\n \tif (ret == SOCKET_ERROR) {\n@@ -3212,32 +2448,6 @@ static void rist_peer_recv(struct evsocket_ctx *evctx, int fd, short revents, vo\n \n \trecv_bufsize = ret;\n \n-\t/* Transparent one-to-many reflector: forward raw packets between\n-\t * publisher and subscriber children before any decryption or\n-\t * protocol processing.  Only active when ?reflector=1 is set. */\n-\tif (peer->config.reflector && peer->listening && !peer->multicast_receiver) {\n-\t\tstruct rist_peer *p_sender = _librist_peer_match_peer_addr(peer, family, addr);\n-\t\tif (p_sender && p_sender->parent == peer) {\n-\t\t\tstruct rist_peer *child = peer->child;\n-\t\t\twhile (child) {\n-\t\t\t\tif (child != p_sender) {\n-\t\t\t\t\tif (p_sender->is_reflector_publisher) {\n-\t\t\t\t\t\t/* Forward publisher's packet (RTP/RTCP) to subscriber children */\n-\t\t\t\t\t\tif (!child->is_reflector_publisher) {\n-\t\t\t\t\t\t\tsendto(peer->sd, (const char *)recv_buf, recv_bufsize, 0, &child->u.address, child->address_len);\n-\t\t\t\t\t\t}\n-\t\t\t\t\t} else {\n-\t\t\t\t\t\t/* Forward subscriber's packet (NACK/RTCP) to publisher children */\n-\t\t\t\t\t\tif (child->is_reflector_publisher) {\n-\t\t\t\t\t\t\tsendto(peer->sd, (const char *)recv_buf, recv_bufsize, 0, &child->u.address, child->address_len);\n-\t\t\t\t\t\t}\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t\tchild = child->sibling_next;\n-\t\t\t}\n-\t\t}\n-\t}\n-\n \tstruct rist_key *k = &peer->key_rx;\n \tuint32_t seq = 0;\n \tuint32_t time_extension = 0;\n@@ -3249,222 +2459,6 @@ static void rist_peer_recv(struct evsocket_ctx *evctx, int fd, short revents, vo\n \tuint8_t rist_gre_version = RIST_GRE_VERSION_MIN;\n \tif (cctx->profile > RIST_PROFILE_SIMPLE)\n \t{\n-\t\t/* Section 9 (TR-06-3): Auto-detect GRE vs RTP framing.\n-\t\t * First byte bits [7:6]:\n-\t\t *   0b0x = GRE (bit 7 = 0 → Main/Advanced GRE tunnel)\n-\t\t *   0b10 = RTP V=2 → Advanced Profile if PT=127 */\n-\t\tif (cctx->profile == RIST_PROFILE_ADVANCED &&\n-\t\t    recv_bufsize >= RIST_ADV_HEADER_MIN &&\n-\t\t    (recv_buf[0] & 0xC0) == 0x80) {\n-\t\t\t/* Looks like RTP V=2. Check PT field for Advanced Profile. */\n-\t\t\tuint8_t pt = recv_buf[1] & 0x7F;\n-\t\t\tif (pt == RIST_ADV_PT || pt >= 96) {\n-\t\t\t\tstruct rist_adv_parsed adv_parsed;\n-\t\t\t\tif (rist_adv_parse(recv_buf, recv_bufsize, &adv_parsed) == 0) {\n-\t\t\t\t\tp = _librist_peer_match_peer_addr(peer, family, addr);\n-\t\t\t\t\tif (!p) {\n-\t\t\t\t\t\tif (peer->listening || peer->multicast_sender) {\n-\t\t\t\t\t\t\tp = peer_initialize(NULL, peer->sender_ctx, peer->receiver_ctx);\n-\t\t\t\t\t\t\tp->handled_first = false;\n-\t\t\t\t\t\t\tp->adv_peer_id = ++cctx->peer_counter;\n-\t\t\t\t\t\t\tp->parent = peer;\n-\t\t\t\t\t\t\tpeer_copy_settings(peer, p);\n-\t\t\t\t\t\t\tif (peer->receiver_ctx) {\n-\t\t\t\t\t\t\t\tp->remote_port = port;\n-\t\t\t\t\t\t\t\tp->local_port = peer->local_port;\n-\t\t\t\t\t\t\t} else {\n-\t\t\t\t\t\t\t\tp->remote_port = peer->remote_port;\n-\t\t\t\t\t\t\t\tp->local_port = peer->local_port;\n-\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\tp->address_family = family;\n-\t\t\t\t\t\t\tp->address_len = addrlen;\n-\t\t\t\t\t\t\tp->listening = 0;\n-\t\t\t\t\t\t\tp->is_rtcp = peer->is_rtcp;\n-\t\t\t\t\t\t\tp->is_data = peer->is_data;\n-\t\t\t\t\t\t\tp->peer_data = p;\n-\t\t\t\t\t\t\tp->is_advanced = true;\n-\t\t\t\t\t\t\tmemcpy(&p->u.address, addr, addrlen);\n-\t\t\t\t\t\t\tp->sd = peer->sd;\n-\t\t\t\t\t\t\tp->authenticated = false;\n-\t\t\t\t\t\t\tp->event_recv = peer->event_recv;\n-\t\t\t\t\t\t\tp->send_keepalive = true;\n-\t\t\t\t\t\t\tif (cctx->auth.conn_cb) {\n-\t\t\t\t\t\t\t\tchar incoming_ip_str[INET6_ADDRSTRLEN];\n-\t\t\t\t\t\t\t\tchar *ip_str = get_ip_str(&p->u.address, incoming_ip_str, INET6_ADDRSTRLEN);\n-\t\t\t\t\t\t\t\tchar parent_ip_str[INET6_ADDRSTRLEN];\n-\t\t\t\t\t\t\t\tchar *parent_str = get_ip_str(&p->parent->u.address, parent_ip_str, INET6_ADDRSTRLEN);\n-\t\t\t\t\t\t\t\tif (!parent_str) parent_str = \"\";\n-\t\t\t\t\t\t\t\tuint16_t parent_port = 0;\n-\t\t\t\t\t\t\t\tif (p->parent->u.storage.ss_family == AF_INET)\n-\t\t\t\t\t\t\t\t\tparent_port = htons(p->parent->u.inaddr.sin_port);\n-\t\t\t\t\t\t\t\telse\n-\t\t\t\t\t\t\t\t\tparent_port = htons(p->parent->u.inaddr6.sin6_port);\n-\t\t\t\t\t\t\t\tif (ip_str && cctx->auth.conn_cb(cctx->auth.arg, ip_str, port, parent_str, parent_port, p)) {\n-\t\t\t\t\t\t\t\t\tfree(p);\n-\t\t\t\t\t\t\t\t\treturn;\n-\t\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\tpeer_append(p);\n-#if HAVE_SRP_SUPPORT\n-\t\t\t\t\t\t\teap_clone_ctx(peer->eap_ctx, p);\n-\t\t\t\t\t\t\t{\n-\t\t\t\t\t\t\t\tchar ip_buf[INET6_ADDRSTRLEN];\n-\t\t\t\t\t\t\t\tchar *ip = get_ip_str(&p->u.address, ip_buf, INET6_ADDRSTRLEN);\n-\t\t\t\t\t\t\t\tif (ip)\n-\t\t\t\t\t\t\t\t\teap_set_ip_string(p->eap_ctx, ip_buf);\n-\t\t\t\t\t\t\t}\n-#endif\n-\t\t\t\t\t\t} else {\n-\t\t\t\t\t\t\treturn;\n-\t\t\t\t\t\t}\n-\t\t\t\t\t}\n-\t\t\t\t\tp->is_advanced = true;\n-\t\t\t\t\tp->last_pkt_received = now;\n-\n-\t\t\t\t\t/* Dispatch by encapsulation type */\n-\t\t\t\t\tif (adv_parsed.enc_type == RIST_ADV_TYPE_CONTROL) {\n-\t\t\t\t\t\trist_calculate_bitrate(recv_bufsize, &p->bw);\n-\t\t\t\t\t\trist_adv_recv_control(p, adv_parsed.payload,\n-\t\t\t\t\t\t                      adv_parsed.payload_len);\n-\t\t\t\t\t\treturn;\n-\t\t\t\t\t}\n-\n-\t\t\t\t\tuint32_t adv_flow_id = adv_parsed.ssrc & ~(uint32_t)1;\n-\t\t\t\t\tretry = adv_parsed.retransmit ? 1 : 0;\n-\n-\t\t\t\t\tif (adv_parsed.enc_type == RIST_ADV_TYPE_DIRECT) {\n-\t\t\t\t\t\t/* PSK decryption for Advanced Profile */\n-\t\t\t\t\t\tuint8_t adv_dec_buf[RIST_MAX_PACKET_SIZE];\n-\t\t\t\t\t\tconst uint8_t *adv_data = adv_parsed.payload;\n-\t\t\t\t\t\tsize_t adv_data_len = adv_parsed.payload_len;\n-\n-\t\t\t\t\t\tif (adv_parsed.psk_mode == RIST_ADV_PSK_AES_CTR) {\n-\t\t\t\t\t\t\tif (!p->key_rx.key_size) {\n-\t\t\t\t\t\t\t\tif (now > (peer->log_repeat_timer + RIST_LOG_QUIESCE_TIMER)) {\n-\t\t\t\t\t\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_ERROR,\n-\t\t\t\t\t\t\t\t\t\t\"Advanced Profile: encrypted data but no PSK configured\\n\");\n-\t\t\t\t\t\t\t\t\tpeer->log_repeat_timer = now;\n-\t\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\t\treturn;\n-\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\tif (!adv_parsed.psk_nonce || !adv_parsed.psk_iv) {\n-\t\t\t\t\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_ERROR,\n-\t\t\t\t\t\t\t\t\t\"Advanced Profile: PSK AES-CTR missing nonce/IV\\n\");\n-\t\t\t\t\t\t\t\treturn;\n-\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\tbool odd_nonce_adv = CHECK_BIT(adv_parsed.psk_nonce[0], 7);\n-\t\t\t\t\t\t\tstruct rist_key *ak = &p->key_rx;\n-\t\t\t\t\t\t\tif (odd_nonce_adv)\n-\t\t\t\t\t\t\t\tak = &p->key_rx_odd;\n-\t\t\t\t\t\t\tpthread_mutex_lock(&p->peer_lock);\n-\t\t\t\t\t\t\tuint32_t seq_nbe;\n-\t\t\t\t\t\t\tmemcpy(&seq_nbe, adv_parsed.psk_iv, sizeof(seq_nbe));\n-\t\t\t\t\t\t\t_librist_crypto_psk_decrypt(ak,\n-\t\t\t\t\t\t\t\t(uint8_t *)adv_parsed.psk_nonce, seq_nbe, 1,\n-\t\t\t\t\t\t\t\tadv_parsed.payload, adv_dec_buf, adv_data_len);\n-\t\t\t\t\t\t\tpthread_mutex_unlock(&p->peer_lock);\n-\t\t\t\t\t\t\tif (ak->bad_decryption)\n-\t\t\t\t\t\t\t\treturn;\n-\t\t\t\t\t\t\tadv_data = adv_dec_buf;\n-\t\t\t\t\t\t} else if (adv_parsed.psk_mode != RIST_ADV_PSK_NONE) {\n-\t\t\t\t\t\t\tif (now > (peer->log_repeat_timer + RIST_LOG_QUIESCE_TIMER)) {\n-\t\t\t\t\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_WARN,\n-\t\t\t\t\t\t\t\t\t\"Advanced Profile: unsupported PSK mode %u\\n\",\n-\t\t\t\t\t\t\t\t\tadv_parsed.psk_mode);\n-\t\t\t\t\t\t\t\tpeer->log_repeat_timer = now;\n-\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\treturn;\n-\t\t\t\t\t\t} else if (p->key_rx.key_size) {\n-\t\t\t\t\t\t\tif (now > (peer->log_repeat_timer + RIST_LOG_QUIESCE_TIMER)) {\n-\t\t\t\t\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_ERROR,\n-\t\t\t\t\t\t\t\t\t\"Advanced Profile: expect encrypted data but received clear\\n\");\n-\t\t\t\t\t\t\t\tpeer->log_repeat_timer = now;\n-\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\treturn;\n-\t\t\t\t\t\t}\n-\n-\t\t\t\t\t\tuint8_t adv_lz4_buf[RIST_MAX_PACKET_SIZE];\n-\t\t\t\t\t\tif (adv_parsed.lpc_mode == RIST_ADV_LPC_LZ4 && adv_data_len > 0) {\n-\t\t\t\t\t\t\tint dlen = LZ4_decompress_safe(\n-\t\t\t\t\t\t\t\t(const char *)adv_data, (char *)adv_lz4_buf,\n-\t\t\t\t\t\t\t\t(int)adv_data_len, RIST_MAX_PACKET_SIZE);\n-\t\t\t\t\t\t\tif (dlen <= 0) {\n-\t\t\t\t\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_ERROR,\n-\t\t\t\t\t\t\t\t\t\"Advanced Profile: LZ4 decompress failed\\n\");\n-\t\t\t\t\t\t\t\treturn;\n-\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\tadv_data = adv_lz4_buf;\n-\t\t\t\t\t\t\tadv_data_len = (size_t)dlen;\n-\t\t\t\t\t\t} else if (adv_parsed.lpc_mode != RIST_ADV_LPC_NONE) {\n-\t\t\t\t\t\t\tif (now > (peer->log_repeat_timer + RIST_LOG_QUIESCE_TIMER)) {\n-\t\t\t\t\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_WARN,\n-\t\t\t\t\t\t\t\t\t\"Advanced Profile: unsupported LPC mode %u\\n\",\n-\t\t\t\t\t\t\t\t\tadv_parsed.lpc_mode);\n-\t\t\t\t\t\t\t\tpeer->log_repeat_timer = now;\n-\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\treturn;\n-\t\t\t\t\t\t}\n-\n-\t\t\t\t\t\t/* Stamp with the sender's timeline rebuilt from the 1 MHz\n-\t\t\t\t\t\t * RTP timestamp so the dejitter buffer smooths off the\n-\t\t\t\t\t\t * source clock like the Main path, not raw arrival. ARRIVAL\n-\t\t\t\t\t\t * mode keeps arrival; otherwise the reconstruct dejitters off\n-\t\t\t\t\t\t * the source clock immediately and, because older releases\n-\t\t\t\t\t\t * emitted a broken clock, corrects to arrival after the first\n-\t\t\t\t\t\t * second if the sender's clock is not advancing at real time. */\n-\t\t\t\t\t\tuint64_t adv_source_time;\n-\t\t\t\t\t\tif (RIST_UNLIKELY(p->config.timing_mode == RIST_TIMING_MODE_ARRIVAL))\n-\t\t\t\t\t\t\tadv_source_time = now;\n-\t\t\t\t\t\telse\n-\t\t\t\t\t\t\tadv_source_time = rist_adv_ts_reconstruct(&p->rx_adv_ts,\n-\t\t\t\t\t\t\t\tadv_parsed.timestamp, now,\n-\t\t\t\t\t\t\t\tONE_SECOND, !retry);\n-\n-\t\t\t\t\t\tif (peer->receiver_ctx) {\n-\t\t\t\t\t\t\trist_calculate_bitrate(recv_bufsize, &p->bw);\n-\t\t\t\t\t\t\tuint16_t adv_src_port = (uint16_t)(32768 + p->adv_peer_id);\n-\t\t\t\t\t\t\tuint16_t adv_dst_port = p->config.virt_dst_port;\n-\t\t\t\t\t\t\tif (adv_parsed.has_flow_id && adv_parsed.flow_id) {\n-\t\t\t\t\t\t\t\tadv_dst_port = rist_adv_flow_outer(adv_parsed.flow_id);\n-\t\t\t\t\t\t\t\tadv_src_port = rist_adv_flow_inner(adv_parsed.flow_id);\n-\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\tstruct rist_buffer adv_payload = {\n-\t\t\t\t\t\t\t\t.data = (void *)adv_data,\n-\t\t\t\t\t\t\t\t.size = adv_data_len,\n-\t\t\t\t\t\t\t\t.type = RIST_PAYLOAD_TYPE_DATA_RAW,\n-\t\t\t\t\t\t\t\t.src_port = adv_src_port,\n-\t\t\t\t\t\t\t\t.dst_port = adv_dst_port,\n-\t\t\t\t\t\t\t};\n-\t\t\t\t\t\t\t/* Pass the delivered payload size (adv_data_len), not\n-\t\t\t\t\t\t\t * the full datagram, so received_bytes and bitrate\n-\t\t\t\t\t\t\t * match the Main path's payload-only accounting.\n-\t\t\t\t\t\t\t * ts_null_bytes is 0: the Advanced receive path does\n-\t\t\t\t\t\t\t * no ts-null reinsertion. */\n-\t\t\t\t\t\t\trist_receiver_recv_data(p, adv_parsed.seq, adv_flow_id,\n-\t\t\t\t\t\t\t\tadv_source_time, now, &adv_payload, retry,\n-\t\t\t\t\t\t\t\tRIST_PAYLOAD_TYPE_DATA_RAW, adv_data_len, 0, false);\n-\t\t\t\t\t\t}\n-\t\t\t\t\t\treturn;\n-\t\t\t\t\t}\n-\n-\t\t\t\t\tif (adv_parsed.enc_type == RIST_ADV_TYPE_GRE_MAIN) {\n-\t\t\t\t\t\t/* Type 8: Strip Advanced Profile header, process inner GRE.\n-\t\t\t\t\t\t * The payload is a Main Profile GRE packet — feed it back\n-\t\t\t\t\t\t * through the existing GRE receive path by updating the\n-\t\t\t\t\t\t * buffer pointer and falling through. */\n-\t\t\t\t\t\trecv_buf = (uint8_t *)adv_parsed.payload;\n-\t\t\t\t\t\trecv_bufsize = adv_parsed.payload_len;\n-\t\t\t\t\t\t/* Fall through to GRE processing below */\n-\t\t\t\t\t} else {\n-\t\t\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_DEBUG,\n-\t\t\t\t\t\t\t\"Advanced Profile: unsupported encapsulation type %u\\n\",\n-\t\t\t\t\t\t\tadv_parsed.enc_type);\n-\t\t\t\t\t\treturn;\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n-\t\t}\n-\n \t\tstruct rist_gre_hdr *gre = NULL;\n \t\t// Make sure we have enough bytes\n \t\tif (recv_bufsize < (int)sizeof(*gre)) {\n@@ -3500,8 +2494,8 @@ static void rist_peer_recv(struct evsocket_ctx *evctx, int fd, short revents, vo\n \t\tsize_t nonce_offset = 0;\n \t\tbool odd_nonce = false;\n \t\tif (has_key) {\n-\t\t\tnonce_offset = payload_offset;\n \t\t\todd_nonce = CHECK_BIT(recv_buf[nonce_offset], 7);\n+\t\t\tnonce_offset = payload_offset;\n \t\t\tpayload_offset += 4;\n \t\t}\n \n@@ -3542,12 +2536,7 @@ static void rist_peer_recv(struct evsocket_ctx *evctx, int fd, short revents, vo\n \t\t\tif (p->rist_gre_version)\n \t\t\t{\n \t\t\t\tint bits = (CHECK_BIT(gre->flags2, 6))? 256 : 128;\n-\t\t\t\t/* Honor the H bit only when it agrees with the configured\n-\t\t\t\t * size (or none was configured): it is unauthenticated wire\n-\t\t\t\t * data, and letting it rewrite key_size (e.g. a 192 config\n-\t\t\t\t * clobbered to 128/256) desyncs every later rekey. */\n-\t\t\t\tif (k->key_size == 0 || k->key_size == (uint32_t)bits)\n-\t\t\t\t\tk->key_size = bits;\n+\t\t\t\tk->key_size = bits;\n \t\t\t}\n \t\t\t_librist_crypto_psk_decrypt(k, &recv_buf[nonce_offset], htobe32(seq), rist_gre_version,&recv_buf[payload_offset],  &recv_buf[payload_offset], (recv_bufsize - payload_offset));\n \t\t\tpthread_mutex_unlock(&p->peer_lock);\n@@ -3580,7 +2569,6 @@ static void rist_peer_recv(struct evsocket_ctx *evctx, int fd, short revents, vo\n \t\tif (gre_proto == RIST_GRE_PROTOCOL_TYPE_VSF) {\n \t\t\tif (recv_bufsize < payload_offset + 4) {\n \t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_ERROR, \"Packet too small: %d bytes, ignoring ...\\n\", recv_bufsize);\n-\t\t\t\treturn;\n \t\t\t}\n \n \t\t\tuint16_t vsf_proto = 0;\n@@ -3600,9 +2588,9 @@ static void rist_peer_recv(struct evsocket_ctx *evctx, int fd, short revents, vo\n \t\t\t} else if (vsf_subtype >= 0x8000){//Control messages\n \t\t\t\tif (vsf_subtype == 0x8000) {\n \t\t\t\t\tgre_proto = RIST_GRE_PROTOCOL_TYPE_KEEPALIVE;\n-\t\t\t} else if (vsf_subtype == 0x8001) {\n-\t\t\t\t/* Flow Attribute (Main Profile VSF extension) — not parsed here */\n-\t\t\t\treturn;\n+\t\t\t\t} else if (vsf_subtype == 0x8001) {\n+\t\t\t\t\t//nonce announcements, we don't care\n+\t\t\t\t\treturn;\n \t\t\t\t} else if (vsf_subtype == RIST_VSF_PROTOCOL_SUBTYPE_BUFFER_NEGOTIATION) {\n \t\t\t\t\tgre_proto = RIST_VSF_PROTOCOL_SUBTYPE_BUFFER_NEGOTIATION;\n \t\t\t\t} else {\n@@ -3633,19 +2621,8 @@ protocol_bypass:\n \tif (!p)\n \t\tp = _librist_peer_match_peer_addr(peer, family, addr);\n \n-\t/* The RTP header only exists on the data path (Simple profile or\n-\t * Main-profile REDUCED); leave the pointer NULL elsewhere so a\n-\t * stray deref shows up at compile/run time instead of silently\n-\t * reading past recv_bufsize on a short FULL/EAPOL/KEEPALIVE\n-\t * datagram. */\n-\tstruct rist_rtp_hdr *rtp = NULL;\n+    struct rist_rtp_hdr *rtp = (struct rist_rtp_hdr *)&recv_buf[payload_offset];\n \tif (cctx->profile == RIST_PROFILE_SIMPLE || gre_proto == RIST_GRE_PROTOCOL_TYPE_REDUCED) {\n-\t\tif (recv_bufsize < payload_offset + sizeof(struct rist_rtp_hdr))\n-\t\t{\n-\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_ERROR, \"Packet too small for RTP header: %zu bytes, ignoring ...\\n\", recv_bufsize);\n-\t\t\treturn;\n-\t\t}\n-\t\trtp = (struct rist_rtp_hdr *)&recv_buf[payload_offset];\n \t\t/* Double check for a valid rtp header */\n \t\tif ((rtp->flags & 0xc0) != 0x80)\n \t\t{\n@@ -3665,11 +2642,6 @@ protocol_bypass:\n \t\t\t}\n \t\t\treturn;\n \t\t}\n-\t\t/* A packet that decrypts to a valid RTP header proves the key is\n-\t\t * healthy; decay the strike counter so a few garbage packets mixed\n-\t\t * into good traffic can't accumulate into a lockout. */\n-\t\tif (k && k->bad_count)\n-\t\t\tk->bad_count = 0;\n \t}\n \n \n@@ -3766,25 +2738,10 @@ protocol_bypass:\n \t\t\t_librist_proto_gre_send_keepalive(p, p->rist_gre_version);\n \t\t\t_librist_proto_gre_send_keepalive(p, p->rist_gre_version);\n \t\t\t_librist_proto_gre_send_keepalive(p, p->rist_gre_version);\n-\t\t\tif (p->rist_gre_version >= 2) {\n-\t\t\t\tuint16_t sender_max_buffer = 0;\n-\t\t\t\tif (p->sender_ctx != NULL) {\n-\t\t\t\t\tsender_max_buffer = p->sender_ctx->sender_recover_min_time;\n-\t\t\t\t} else if (peer->config.reflector && peer->listening && !peer->multicast_receiver) {\n-\t\t\t\t\tstruct rist_peer *child = peer->child;\n-\t\t\t\t\twhile (child) {\n-\t\t\t\t\t\tif (child->is_reflector_publisher && child->sender_max_buffer_ticks > 0) {\n-\t\t\t\t\t\t\tsender_max_buffer = (uint16_t)(child->sender_max_buffer_ticks / RIST_CLOCK);\n-\t\t\t\t\t\t\tbreak;\n-\t\t\t\t\t\t}\n-\t\t\t\t\t\tchild = child->sibling_next;\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t\tif (sender_max_buffer > 0) {\n-\t\t\t\t\t_librist_proto_gre_send_buffer_negotiation(p, sender_max_buffer, 0);\n-\t\t\t\t\t_librist_proto_gre_send_buffer_negotiation(p, sender_max_buffer, 0);\n-\t\t\t\t\t_librist_proto_gre_send_buffer_negotiation(p, sender_max_buffer, 0);\n-\t\t\t\t}\n+\t\t\tif (p->rist_gre_version >= 2 && p->sender_ctx != NULL) {\n+\t\t\t\t_librist_proto_gre_send_buffer_negotiation(p, peer->sender_ctx->sender_recover_min_time, 0);\n+\t\t\t\t_librist_proto_gre_send_buffer_negotiation(p, peer->sender_ctx->sender_recover_min_time, 0);\n+\t\t\t\t_librist_proto_gre_send_buffer_negotiation(p, peer->sender_ctx->sender_recover_min_time, 0);\n \t\t\t}\n \t\t}\n \t\tpeer_append(p);\n@@ -3799,35 +2756,17 @@ protocol_bypass:\n #endif\n \t) {\n \t\t//Our GRE version got upgraded, try kickstarting buffer negotiation\n-\t\tif (p->rist_gre_version == RIST_GRE_VERSION_MIN) {\n-\t\t\tuint16_t sender_max_buffer = 0;\n-\t\t\tif (p->sender_ctx != NULL) {\n-\t\t\t\tsender_max_buffer = p->sender_ctx->sender_recover_min_time;\n-\t\t\t} else if (peer->config.reflector && peer->listening && !peer->multicast_receiver) {\n-\t\t\t\tstruct rist_peer *child = peer->child;\n-\t\t\t\twhile (child) {\n-\t\t\t\t\tif (child->is_reflector_publisher && child->sender_max_buffer_ticks > 0) {\n-\t\t\t\t\t\tsender_max_buffer = (uint16_t)(child->sender_max_buffer_ticks / RIST_CLOCK);\n-\t\t\t\t\t\tbreak;\n-\t\t\t\t\t}\n-\t\t\t\t\tchild = child->sibling_next;\n-\t\t\t\t}\n-\t\t\t}\n-\t\t\tif (sender_max_buffer > 0) {\n-\t\t\t\t_librist_proto_gre_send_buffer_negotiation(p, sender_max_buffer, 0);\n-\t\t\t\t_librist_proto_gre_send_buffer_negotiation(p, sender_max_buffer, 0);\n-\t\t\t\t_librist_proto_gre_send_buffer_negotiation(p, sender_max_buffer, 0);\n-\t\t\t}\n+\t\tif (p->rist_gre_version == RIST_GRE_VERSION_MIN && p->sender_ctx != NULL) {\n+\t\t\t_librist_proto_gre_send_buffer_negotiation(p, peer->sender_ctx->sender_recover_min_time, 0);\n+\t\t\t_librist_proto_gre_send_buffer_negotiation(p, peer->sender_ctx->sender_recover_min_time, 0);\n+\t\t\t_librist_proto_gre_send_buffer_negotiation(p, peer->sender_ctx->sender_recover_min_time, 0);\n \t\t}\n \t\tp->rist_gre_version = rist_gre_version;\n \t}\n \n \tif (gre_proto == RIST_GRE_PROTOCOL_TYPE_KEEPALIVE) {\n \t\tstruct rist_keepalive_info info;\n-\t\tif (_librist_proto_gre_parse_keepalive(&recv_buf[payload_offset], recv_bufsize - payload_offset, &info) != 0) {\n-\t\t\tp->last_pkt_received = now;\n-\t\t\treturn;\n-\t\t}\n+\t\t_librist_proto_gre_parse_keepalive(&recv_buf[payload_offset], recv_bufsize - payload_offset, &info);\n \t\tif (memcmp(&info.ka, &p->data, sizeof(peer->data)) != 0) {\n \t\t\trist_log_priv(get_cctx(peer), RIST_LOG_INFO,\n \t\t\t\t\"New keepalive received. MAC: %x:%x:%x:%x:%x:%x\"\n@@ -3839,38 +2778,12 @@ protocol_bypass:\n \t\t\t\tinfo.p, info.e, info.l, info.e,\n \t\t\t\tinfo.n, info.d, info.t, info.v,\n \t\t\t\tinfo.j, info.f);\n-\t\t\tif (info.json_len) {\n-\t\t\t\t/* %.*s precision must be int. Truncate the log line if some\n-\t\t\t\t * weird future caller hands us a giant blob. */\n-\t\t\t\tint json_log_len = info.json_len > (size_t)INT_MAX ? INT_MAX : (int)info.json_len;\n-\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_INFO, \"Keepalive JSON:\\n%.*s\\n\", json_log_len, info.json);\n-\t\t\t}\n+\t\t\tif (info.json_len)\n+\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_INFO, \"Keepalive JSON:\\n%.*s\\n\", info.json_len, info.json);\n \t\t\t//TODO: add callback?\n-\t\t\t/* TR-06-3 Section 9: detect Advanced Profile support via I bit */\n-\t\t\tif (info.adv_i && !p->remote_supports_advanced) {\n-\t\t\t\tp->remote_supports_advanced = true;\n-\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_INFO,\n-\t\t\t\t\t\"Peer %u advertises Advanced Profile capability (I=1)\\n\",\n-\t\t\t\t\tp->adv_peer_id);\n-\t\t\t}\n+\t\t\t//TODO: handle capabilities in some way\n \t\t\tmemcpy(&p->data, &info.ka, sizeof(peer->data));\n \t\t}\n-\t\tif (p->receiver_ctx &&\n-\t\t    p->receiver_ctx->merge_mode == LIBRIST_MERGE_MODE_AUTO) {\n-\t\t\tstruct rist_flow *fl = cctx->FLOWS;\n-\t\t\twhile (fl) {\n-\t\t\t\tif (fl->merge_auto_enabled != (bool)info.l) {\n-\t\t\t\t\tfl->merge_auto_enabled = (bool)info.l;\n-\t\t\t\t\trist_log_priv(cctx, RIST_LOG_INFO,\n-\t\t\t\t\t\t\"merge=auto: peer %s pair-split; \"\n-\t\t\t\t\t\t\"merge %s for flow %u\\n\",\n-\t\t\t\t\t\tinfo.l ? \"advertises\" : \"stopped advertising\",\n-\t\t\t\t\t\tinfo.l ? \"enabled\" : \"disabled\",\n-\t\t\t\t\t\tfl->flow_id);\n-\t\t\t\t}\n-\t\t\t\tfl = fl->next;\n-\t\t\t}\n-\t\t}\n \t\tp->last_pkt_received = now;\n \t\treturn;\n \t}\n@@ -3881,39 +2794,27 @@ protocol_bypass:\n \t\tif (_librist_proto_gre_parse_buffer_negotiation(p, &recv_buf[payload_offset], recv_bufsize - payload_offset, &sender_max_buffer, &client_current_buffer) != 0)\n \t\t\treturn;\n \n-\t\tp->is_reflector_publisher = sender_max_buffer != 0;\n-\n \t\tif (p->receiver_ctx != NULL && sender_max_buffer != 0) {\n \t\t\tif (sender_max_buffer < p->config.recovery_length_min) {\n \t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_ERROR, \"Sender max buffer %u smaller than min buffer %u, disabling buffer negotiation!\\n\", sender_max_buffer, p->config.recovery_length_min);\n \t\t\t\tp->sender_max_buffer_ticks = 0;\n-\t\t\t} else {\n+\t\t\t} else\n \t\t\t\tp->sender_max_buffer_ticks = sender_max_buffer * RIST_CLOCK;\n-\t\t\t\tif (peer->config.reflector && peer->listening && !peer->multicast_receiver) {\n-\t\t\t\t\tstruct rist_peer *child = peer->child;\n-\t\t\t\t\twhile (child) {\n-\t\t\t\t\t\tif (child != p && !child->is_reflector_publisher && child->rist_gre_version >= 2) {\n-\t\t\t\t\t\t\t_librist_proto_gre_send_buffer_negotiation(child, sender_max_buffer, 0);\n-\t\t\t\t\t\t}\n-\t\t\t\t\t\tchild = child->sibling_next;\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t}\n \t\t}\n \t\treturn;\n \t}\n \n+\t// This is for legacy compatibility (to be removed later)\n+\tif (rtp->payload_type == PTYPE_XR_LEGACY)\n+\t\trtp->payload_type = PTYPE_XR;\n+\n \tuint32_t rtp_time = 0;\n \tuint64_t source_time = 0;\n-\tuint8_t payload_type = 0;\n+\tuint8_t payload_type = rtp->payload_type;\n+\tuint8_t payload_type_nomarker_bit = rtp->payload_type & 127;\n \tint ts_null_bytes = 0;\n \n \tif (cctx->profile == RIST_PROFILE_SIMPLE || gre_proto == RIST_GRE_PROTOCOL_TYPE_REDUCED) {\n-\t\t// This is for legacy compatibility (to be removed later)\n-\t\tif (rtp->payload_type == PTYPE_XR_LEGACY)\n-\t\t\trtp->payload_type = PTYPE_XR;\n-\t\tpayload_type = rtp->payload_type;\n-\t\tuint8_t payload_type_nomarker_bit = rtp->payload_type & 127;\n \t\t// Finish defining the payload (we assume reduced header)\n \t\t// The check is for 200-205 and 72-77 (payload type minus 128)\n \t\tif(payload_type_nomarker_bit < 72 || payload_type_nomarker_bit > 77) {\n@@ -3935,13 +2836,6 @@ protocol_bypass:\n \t\t\tpayload.data = (void *)data_payload;\n \n \t\t\tif (CHECK_BIT(rtp->flags, 4)) {\n-\t\t\t\t// X bit promises an extension header, the packet may not actually have it\n-\t\t\t\tif (payload.size < sizeof(struct rist_rtp_hdr_ext)) {\n-\t\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_ERROR,\n-\t\t\t\t\t\t\"Packet too small for RTP extension header: %zu bytes, ignoring ...\\n\",\n-\t\t\t\t\t\tpayload.size);\n-\t\t\t\t\treturn;\n-\t\t\t\t}\n \t\t\t\tuint8_t *data_payload_out = &recv_buf_npd[0];\n \t\t\t\t//RTP extension header\n \t\t\t\tstruct rist_rtp_hdr_ext * hdr_ext = (struct rist_rtp_hdr_ext *)(&recv_buf[payload_offset]);\n@@ -3953,9 +2847,7 @@ protocol_bypass:\n \t\t\t\t\t// Null packet expansion (use a separate buffer and replace it when we had nulls)\n \t\t\t\t\tif (CHECK_BIT(hdr_ext->flags, 7)) {\n \t\t\t\t\t\tts_null_bytes = expand_null_packets(data_payload, data_payload_out, &payload.size, hdr_ext->npd_bits);\n-\t\t\t\t\t\tif (ts_null_bytes < 0)\n-\t\t\t\t\t\t\tts_null_bytes = 0; /* expansion failed; deliver unexpanded */\n-\t\t\t\t\t\telse\n+\t\t\t\t\t\tif (ts_null_bytes)\n \t\t\t\t\t\t\tpayload.data = (void *)data_payload_out;\n \t\t\t\t\t}\n \t\t\t\t}\n@@ -4058,12 +2950,12 @@ protocol_bypass:\n \t\t\t\tsource_time = convertRTPtoNTP(payload_type, time_extension, rtp_time);\n \t\t\tseq = (uint32_t)be16toh(rtp->seq);\n \t\t\tif (RIST_UNLIKELY(!p->receiver_mode))\n-\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_DEBUG,\n+\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_WARN,\n \t\t\t\t\t\t\"Received data packet on sender, ignoring (%d bytes)...\\n\", payload.size);\n \t\t\telse {\n \t\t\t\tsize_t received_bytes = recv_bufsize - payload_offset; //use the unexpanded size to show real BW\n \t\t\t\trist_calculate_bitrate(received_bytes, &p->bw);\n-\t\t\t\trist_receiver_recv_data(p, seq, flow_id, source_time, now, &payload, retry, payload_type, received_bytes, ts_null_bytes, true);\n+\t\t\t\trist_receiver_recv_data(p, seq, flow_id, source_time, now, &payload, retry, payload_type, received_bytes, ts_null_bytes);\n \t\t\t}\n \t\t\tbreak;\n \t\tcase RIST_PAYLOAD_TYPE_EAPOL:\n@@ -4077,23 +2969,13 @@ protocol_bypass:\n \t\t\t\t\t\t\t\t\t\t\t\t(recv_buf + payload_offset),\n \t\t\t\t\t\t\t\t\t\t\t\t(recv_bufsize - payload_offset))) < 0) {\n \t\t\t\t\trist_log_priv(get_cctx(p), RIST_LOG_ERROR, \"Failed to process EAPOL pkt, return code: %i\\n\", eapret);\n-\t\t\t\t\t/* eap_process_eapol returns a negative code; was being\n-\t\t\t\t\t * compared against +255 and never matched, so failed_eap\n-\t\t\t\t\t * was effectively dead. */\n-\t\t\t\t\tif (eapret == EAP_AUTH_TERMINATED)\n+\t\t\t\t\tif (eapret == 255)//permanent failure, we allow a few retries\n \t\t\t\t\t\tfailed_eap = true;\n \t\t\t\t}\n \t\t\t\telse if (p->eap_authentication_state != 2 && eap_is_authenticated(p->eap_ctx)) {\n \t\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_INFO,\n \t\t\t\t\t\t\"Peer %d EAP Authentication succeeded\\n\", peer->adv_peer_id);\n \t\t\t\t\tp->eap_authentication_state = 2;\n-\t\t\t\t\t/* A caller-sender leg that re-authenticated after going\n-\t\t\t\t\t * silent (see try_caller_socket_rebind) cleared its\n-\t\t\t\t\t * connection-level authenticated flag to leave the bond\n-\t\t\t\t\t * while down.  Restore it now so the weighted balancer\n-\t\t\t\t\t * folds the leg back in at full weight. */\n-\t\t\t\t\tif (!p->receiver_mode && !p->listening && !p->authenticated)\n-\t\t\t\t\t\trist_peer_authenticate(p);\n \t\t\t\t\t//First authentication, so send keepalive\n \t\t\t\t\t_librist_proto_gre_send_keepalive(p, p->rist_gre_version);\n \t\t\t\t\t_librist_proto_gre_send_keepalive(p, p->rist_gre_version);\n@@ -4103,10 +2985,6 @@ protocol_bypass:\n \t\t\t\t\t\t_librist_proto_gre_send_buffer_negotiation(p, peer->sender_ctx->sender_recover_min_time, 0);\n \t\t\t\t\t\t_librist_proto_gre_send_buffer_negotiation(p, peer->sender_ctx->sender_recover_min_time, 0);\n \t\t\t\t\t}\n-\t\t\t\t\t/* Emit the binding SDES now, not on the next periodic\n-\t\t\t\t\t * tick, so it precedes this leg's first forwarded data. */\n-\t\t\t\t\tif (!p->receiver_mode)\n-\t\t\t\t\t\trist_sender_periodic_rtcp(p);\n \t\t\t\t}\n \t\t\t}\n #else\n@@ -4195,27 +3073,8 @@ static void rist_oob_dequeue(struct rist_common_ctx *ctx, int maxcount)\n \n \t\tuint8_t *payload = oob_buffer->data;\n \t\tstruct rist_peer *p = oob_buffer->peer;\n-\n-\t\t/* The stashed peer may have been freed (NAT rebind / timeout) since\n-\t\t * rist_oob_write() queued it; verify it is still live and hold\n-\t\t * peerlist_lock across the send so it can't be freed under us. */\n-\t\tpthread_mutex_lock(&ctx->peerlist_lock);\n-\t\tbool peer_alive = false;\n-\t\tfor (struct rist_peer *pp = ctx->PEERS; pp != NULL; pp = pp->next) {\n-\t\t\tif (pp == p) {\n-\t\t\t\tpeer_alive = true;\n-\t\t\t\tbreak;\n-\t\t\t}\n-\t\t}\n-\t\tif (!peer_alive) {\n-\t\t\tpthread_mutex_unlock(&ctx->peerlist_lock);\n-\t\t\trist_log_priv(ctx, RIST_LOG_WARN, \"OOB: target peer no longer exists, dropping packet\\n\");\n-\t\t\tctx->oob_queue_bytesize -= oob_buffer->size;\n-\t\t\tctx->oob_queue_read_index++;\n-\t\t\tcontinue;\n-\t\t}\n \t\tif (p->listening) {\n-\t\t\t/* Listener peer: send OOB to all alive child peers. */\n+\t\t\t/* Listener peer: send OOB to all alive child peers */\n \t\t\tstruct rist_peer *child = p->child;\n \t\t\tbool sent = false;\n \t\t\twhile (child) {\n@@ -4232,7 +3091,6 @@ static void rist_oob_dequeue(struct rist_common_ctx *ctx, int maxcount)\n \t\t\trist_send_common_rtcp(p, RIST_PAYLOAD_TYPE_DATA_OOB, &payload[RIST_MAX_PAYLOAD_OFFSET],\n \t\t\t\t\toob_buffer->size, 0, 0, 0, 0, 0);\n \t\t}\n-\t\tpthread_mutex_unlock(&ctx->peerlist_lock);\n \t\tctx->oob_queue_bytesize -= oob_buffer->size;\n \t\tctx->oob_queue_read_index++;\n \t}\n@@ -4326,15 +3184,8 @@ static void sender_send_data(struct rist_sender *ctx, int maxcount)\n \t\t\t}\n \t\t\telse {\n \t\t\t\trist_sender_send_data_balanced(ctx, buffer);\n-\t\t\t\tif (ctx->common.profile == RIST_PROFILE_ADVANCED) {\n-\t\t\t\t\tctx->seq_index[buffer->seq & (ctx->sender_queue_max - 1)] = (uint32_t)idx;\n-\t\t\t\t\t/* Mirror into the RTP index so a Main-downgraded peer's\n-\t\t\t\t\t * 16-bit NACK can still resolve this packet. Dead data for\n-\t\t\t\t\t * Advanced-negotiated peers (never read for them). */\n-\t\t\t\t\tif (ctx->seq_rtp_index)\n-\t\t\t\t\t\tctx->seq_rtp_index[buffer->seq_rtp] = (uint32_t)idx;\n-\t\t\t\t} else\n-\t\t\t\t\tctx->seq_index[buffer->seq_rtp] = (uint32_t)idx;\n+\t\t\t\t// For non-advanced mode seq to index mapping\n+\t\t\t\tctx->seq_index[buffer->seq_rtp] = (uint32_t)idx;\n \t\t\t}\n \t\t}\n \n@@ -4348,8 +3199,8 @@ static void sender_send_data(struct rist_sender *ctx, int maxcount)\n \t\t\t\tuint64_t delay = (timestampNTP_u64() - ctx->sender_queue[delete_idx]->time) / RIST_CLOCK;\n \t\t\t\tctx->sender_queue_timelength = delay;\n \t\t\t\tif (delay < ctx->sender_recover_min_time) {\n-\t\t\t\t\tif (ctx->sender_buffer_size < ctx->sender_queue_max - 1)\n-\t\t\t\t\t\tctx->sender_buffer_size++;\n+\t\t\t\t\t// Grow the buffer size by one (packet is too young to delete)\n+\t\t\t\t\tctx->sender_buffer_size++;\n \t\t\t\t\treduce = 0;\n \t\t\t\t\tbreak;\n \t\t\t\t}\n@@ -4410,7 +3261,6 @@ static struct rist_peer *peer_initialize(const char *url, struct rist_sender *se\n \tp->receiver_ctx = receiver_ctx;\n \tp->birthtime_local = timestampNTP_u64();\n \tp->handled_first = true;\n-\tp->sd = -1;\n \n \treturn p;\n }\n@@ -4436,15 +3286,6 @@ static PTHREAD_START_FUNC(receiver_pthread_dataout, arg)\n \n \trist_log_priv(&receiver_ctx->common, RIST_LOG_INFO, \"Starting data output thread with %d ms max output jitter\\n\", max_output_jitter_ms);\n \n-\t/* Keep one source of truth for the wake ceiling. */\n-\tflow->cbr_pacer.max_sleep_ns = (uint64_t)max_output_jitter_ms * 1000000ULL;\n-\tflow->cbr_max_hold_us = rist_cbr_hold_us((uint32_t)max_output_jitter_ms);\n-\tif (flow->cbr_output)\n-\t\trist_log_priv(&receiver_ctx->common, RIST_LOG_INFO,\n-\t\t\t\"CBR output pacing enabled, wake interval %u us to %d ms, hold %u us\\n\",\n-\t\t\tflow->cbr_output_min_us, max_output_jitter_ms,\n-\t\t\tflow->cbr_max_hold_us);\n-\n \tpthread_mutex_lock(&(flow->mutex));\n \tuint64_t target_recovery_buffer_size = flow->recovery_buffer_ticks;\n \tpthread_mutex_unlock(&(flow->mutex));\n@@ -4456,47 +3297,14 @@ static PTHREAD_START_FUNC(receiver_pthread_dataout, arg)\n \n \twhile (true) {\n \t\tpthread_mutex_lock(&(flow->mutex));\n-\t\tuint64_t wait_us = (uint64_t)max_output_jitter_ms * 1000;\n-\t\tif (flow->cbr_output) {\n-\t\t\tuint64_t now_ntp = timestampNTP_u64();\n-\t\t\tuint64_t arrived = atomic_load_explicit(&flow->cbr_arrived_bytes,\n-\t\t\t\t\t\t\t\tmemory_order_relaxed);\n-\t\t\tuint64_t delta = arrived - flow->cbr_arrived_seen;\n-\t\t\tflow->cbr_arrived_seen = arrived;\n-\t\t\t/* Only on real arrivals: feeding zero would close windows during a\n-\t\t\t * signal loss and average the rate away, and the estimator's outage\n-\t\t\t * detection keys off the gap between calls. */\n-\t\t\tif (delta)\n-\t\t\t\trist_pacer_rate_add(&flow->cbr_rate, (size_t)delta,\n-\t\t\t\t\t\t    timestampNTP_to_us(now_ntp));\n-\t\t\t/* Wake at least twice per datagram interval, once one is known. */\n-\t\t\tif (flow->cbr_interval_ns) {\n-\t\t\t\tuint64_t floor_ns = flow->cbr_interval_ns / 2;\n-\t\t\t\tuint64_t ceil_ns = (uint64_t)flow->cbr_output_min_us * 1000;\n-\t\t\t\tif (floor_ns < RIST_CBR_OUTPUT_MIN_US_FLOOR * 1000ULL)\n-\t\t\t\t\tfloor_ns = RIST_CBR_OUTPUT_MIN_US_FLOOR * 1000ULL;\n-\t\t\t\tif (floor_ns > ceil_ns)\n-\t\t\t\t\tfloor_ns = ceil_ns;\n-\t\t\t\tflow->cbr_pacer.min_sleep_ns = floor_ns;\n-\t\t\t}\n-\t\t\twait_us = rist_pacer_sleep_ns(&flow->cbr_pacer,\n-\t\t\t\t\t\t      timestampNTP_to_ns(now_ntp)) / 1000;\n-\t\t\tif (!wait_us)\n-\t\t\t\twait_us = 1;\n-\t\t}\n-\t\tint ret = pthread_cond_timedwait_us(&flow->condition, &flow->mutex, wait_us);\n+\t\tint ret = pthread_cond_timedwait_ms(&flow->condition, &flow->mutex, max_output_jitter_ms);\n \t\tif (ret && ret != ETIMEDOUT)\n \t\t\trist_log_priv(&receiver_ctx->common, RIST_LOG_ERROR, \"Error %d in receiver data out loop\\n\", ret);\n \t\tif (atomic_load_explicit(&flow->shutdown,memory_order_acquire) > 0)\n \t\t\tbreak;\n-\t\tflow->cbr_paced_hold = false;\n \t\tif (atomic_load_explicit(&flow->receiver_queue_size, memory_order_acquire) > 0) {\n \t\t\treceiver_output(receiver_ctx, flow);\n \t\t}\n-\t\t/* Nothing sent and the pacer was not holding: idle, so don't let the\n-\t\t * schedule accrue due times through the gap. */\n-\t\tif (flow->cbr_output && !flow->cbr_paced_hold)\n-\t\t\trist_pacer_reset(&flow->cbr_pacer);\n \n \t\tif (flow->flow_auto_buffer_scaling) {\n \t\t\tuint64_t now = timestampNTP_u64();\n@@ -4557,27 +3365,15 @@ static PTHREAD_START_FUNC(receiver_pthread_dataout, arg)\n }\n \n static void rist_peer_periodic(struct rist_peer *p, uint64_t now) {\n-\tbool adv_negotiated = (get_cctx(p)->profile == RIST_PROFILE_ADVANCED &&\n-\t                       p->is_advanced && p->remote_supports_advanced);\n \tif (p->send_keepalive) {\n \t\tif (now > p->next_periodic_rtcp) {\n \t\t\tp->next_periodic_rtcp = now + p->rtcp_keepalive_interval;\n-\t\t\tif (p->remote_port != 0) {\n+\t\t\tif (p->remote_port != 0)\n \t\t\t\trist_peer_rtcp(NULL, p);\n-\t\t\t\tif (adv_negotiated)\n-\t\t\t\t\trist_adv_send_rtt_echo_request(p);\n-\t\t\t}\n \t\t}\n-\t\tif (get_cctx(p)->profile >= RIST_PROFILE_MAIN && p->next_keepalive_packet <= now) {\n+\t\tif (get_cctx(p)->profile == RIST_PROFILE_MAIN && p->next_keepalive_packet <= now) {\n \t\t\tp->next_keepalive_packet = now + ONE_SECOND;\n-\t\t\tif (adv_negotiated) {\n-\t\t\t\trist_adv_send_keepalive(p);\n-\t\t\t\tif (p->sender_ctx && p->next_flow_attr <= now) {\n-\t\t\t\t\tp->next_flow_attr = now + ONE_SECOND;\n-\t\t\t\t\trist_adv_send_flow_attr(p);\n-\t\t\t\t}\n-\t\t\t} else\n-\t\t\t\t_librist_proto_gre_send_keepalive(p, p->rist_gre_version);\n+\t\t\t_librist_proto_gre_send_keepalive(p, p->rist_gre_version);\n #if HAVE_SRP_SUPPORT\n \t\t\tif (!p->child && !eap_is_authenticated(p->eap_ctx) && p->eap_authentication_state == 2 && p->parent && p->parent->multicast_sender)  {\n \t\t\t\tp->eap_authentication_state = 1;\n@@ -4616,15 +3412,6 @@ static void receiver_peer_events(struct rist_receiver *ctx, uint64_t now)\n \tpthread_mutex_unlock(&ctx->common.peerlist_lock);\n }\n \n-/* Silence before an authenticated leg is torn down and re-handshaked: the\n- * configured session_timeout, or twice the receiver buffer when that is larger\n- * (a leg returning within the buffer window can still contribute packets). */\n-static inline uint64_t rist_peer_liveness_timeout(const struct rist_peer *peer)\n-{\n-\tuint64_t buf = 2 * peer->recovery_buffer_ticks;\n-\treturn buf > peer->session_timeout ? buf : peer->session_timeout;\n-}\n-\n void rist_timeout_check(struct rist_common_ctx *cctx, uint64_t now)\n {\n \tstruct rist_peer *peer = cctx->PEERS;\n@@ -4638,17 +3425,10 @@ void rist_timeout_check(struct rist_common_ctx *cctx, uint64_t now)\n \t\t\tlast_rtcp_received = peer->peer_rtcp->last_pkt_received;\n \t\tif (!peer->dead && now > last_rtcp_received && last_rtcp_received > 0)\n \t\t{\n-\t\t\tif ((now - last_rtcp_received) > rist_peer_liveness_timeout(peer))\n+\t\t\tif ((now - last_rtcp_received) > peer->session_timeout)\n \t\t\t{\n \t\t\t\trist_log_priv2(cctx->logging_settings, RIST_LOG_WARN, \"Listening peer %u timed out after %\"PRIu64\" ms\\n\", peer->adv_peer_id,\n \t\t\t\t\t(now - last_rtcp_received)/ RIST_CLOCK);\n-\t\t\t\tif (try_caller_socket_rebind(peer, now))\n-\t\t\t\t{\n-\t\t\t\t\t/* Rebind kept the peer alive with a fresh\n-\t\t\t\t\t * socket; skip kill_peer. */\n-\t\t\t\t\tpeer = next;\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n \t\t\t\tkill_peer(peer);\n \t\t\t}\n \t\t} else if (peer->dead && peer->parent)\n@@ -4667,199 +3447,6 @@ void rist_timeout_check(struct rist_common_ctx *cctx, uint64_t now)\n \t}\n }\n \n-/* True for a bonded data leg that participates in weighted balancing.\n- * Listeners, children, and duplicate (weight 0) legs are out of scope. */\n-static inline bool rist_peer_bonded_data_leg(const struct rist_peer *peer)\n-{\n-\treturn peer->is_data && !peer->parent && !peer->listening\n-\t\t&& peer->config.weight != RIST_PEER_WEIGHT_DUPLICATE;\n-}\n-\n-/* True for a bonded data leg that also has RTT auto-mute configured.\n- * Caller must hold peerlist_lock. */\n-static inline bool rist_peer_rtt_mute_eligible(const struct rist_peer *peer)\n-{\n-\treturn rist_peer_bonded_data_leg(peer) && peer->config.rtt_drop > 0;\n-}\n-\n-/* Silent for a few probe intervals: well short of the liveness timeout, since\n- * fast-mute only reroutes payload rather than tearing the session down. Scales\n- * with the configured keepalive_interval. */\n-static inline bool rist_peer_stall_silent(const struct rist_peer *peer, uint64_t now)\n-{\n-\tuint64_t thr = RIST_STALL_MUTE_PINGS * peer->rtcp_keepalive_interval;\n-\treturn peer->last_pkt_received > 0 && now > peer->last_pkt_received\n-\t\t&& (now - peer->last_pkt_received) > thr;\n-}\n-\n-/* Fast-mute briefly-silent bonded legs so their payload reroutes to a healthy\n- * sibling rather than pouring into a stalled path until the liveness timeout\n- * tears the session down. Any bonded leg qualifies (no rtt-drop needed); the\n- * flag is recomputed each tick, so a resumed leg clears at once. The last leg\n- * with a live return path is never muted. Short tick, peerlist_lock held. */\n-static void rist_sender_stall_check(struct rist_sender *ctx, uint64_t now)\n-{\n-\tstruct rist_peer *peer;\n-\tint eligible = 0, live = 0;\n-\n-\tfor (peer = ctx->common.PEERS; peer; peer = peer->next) {\n-\t\tif (!rist_peer_bonded_data_leg(peer))\n-\t\t\tcontinue;\n-\t\teligible++;\n-\t\tif (!rist_peer_stall_silent(peer, now))\n-\t\t\tlive++;\n-\t}\n-\t/* Nothing to fail over to: never stall-mute a single leg. */\n-\tif (eligible < 2) {\n-\t\tfor (peer = ctx->common.PEERS; peer; peer = peer->next)\n-\t\t\tpeer->stalled = false;\n-\t\treturn;\n-\t}\n-\tfor (peer = ctx->common.PEERS; peer; peer = peer->next) {\n-\t\tif (!rist_peer_bonded_data_leg(peer)) {\n-\t\t\tpeer->stalled = false;\n-\t\t\tcontinue;\n-\t\t}\n-\t\tbool silent = rist_peer_stall_silent(peer, now);\n-\t\t/* keep every leg sending if all are silent, rather than black out */\n-\t\tbool stall = silent && live > 0;\n-\t\tif (stall && !peer->stalled)\n-\t\t\trist_log_priv(&ctx->common, RIST_LOG_INFO,\n-\t\t\t\t\"Peer %\"PRIu32\" stalled: silent %\"PRIu64\"ms, rerouting payload to healthy leg(s)\\n\",\n-\t\t\t\tpeer->adv_peer_id, (now - peer->last_pkt_received) / RIST_CLOCK);\n-\t\telse if (!stall && peer->stalled)\n-\t\t\trist_log_priv(&ctx->common, RIST_LOG_INFO,\n-\t\t\t\t\"Peer %\"PRIu32\" resumed from stall\\n\", peer->adv_peer_id);\n-\t\tpeer->stalled = stall;\n-\t}\n-}\n-\n-/* Advance the RTT hysteresis for every eligible bonded leg, then decide which\n- * legs are actually pulled from the payload rotation. rtt_mute_state.muted is\n- * the *desired* state; peer->rtt_muted (what the balancer skips) is only set\n- * when pulling the leg still leaves a carrier -- a bonded leg that is neither\n- * muted nor stalled. Since the stall check runs first, a leg that wants mute\n- * but whose only sibling is stalled is simply kept carrying, never actually\n- * muted: that avoids blacking out the bond and, because the hysteresis state is\n- * left untouched, avoids re-muting (and re-counting/logging) it every dwell.\n- * Caller holds peerlist_lock; runs on a short tick (see the sender loop). */\n-static void rist_sender_rtt_mute_check(struct rist_sender *ctx, uint64_t now)\n-{\n-\tstruct rist_peer *peer;\n-\tint eligible = 0;\n-\n-\tfor (peer = ctx->common.PEERS; peer; peer = peer->next)\n-\t\tif (rist_peer_rtt_mute_eligible(peer))\n-\t\t\teligible++;\n-\t/* Nothing to fail over to: never mute a single leg. */\n-\tif (eligible < 2)\n-\t\treturn;\n-\n-\tfor (peer = ctx->common.PEERS; peer; peer = peer->next) {\n-\t\tif (!rist_peer_rtt_mute_eligible(peer))\n-\t\t\tcontinue;\n-\t\tif (peer->eight_times_rtt == 0)\n-\t\t\tcontinue; /* no RTT sample yet */\n-\n-\t\tuint64_t smoothed = peer->eight_times_rtt / 8;\n-\t\tuint64_t drop = (uint64_t)peer->config.rtt_drop * RIST_CLOCK;\n-\t\tuint64_t restore = peer->config.rtt_restore\n-\t\t\t? (uint64_t)peer->config.rtt_restore * RIST_CLOCK\n-\t\t\t: (drop * 4) / 5;\n-\t\tif (restore >= drop)\n-\t\t\trestore = (drop * 4) / 5;\n-\t\tuint64_t drop_settle = (uint64_t)peer->config.rtt_drop_settle * RIST_CLOCK;\n-\t\t/* Rejoin dwell is twice the drop dwell so a still-marginal link\n-\t\t * cannot immediately flap back in. */\n-\t\tuint64_t restore_settle = drop_settle * 2;\n-\t\trist_rtt_mute_step(&peer->rtt_mute_state, smoothed, drop, restore,\n-\t\t\t\t   drop_settle, restore_settle, now);\n-\t}\n-\n-\t/* Count natural carriers; among the legs that want mute but could still\n-\t * carry (not stalled), find the incumbent sole carrier and the best\n-\t * challenger. */\n-\tint carriers = 0;\n-\tstruct rist_peer *incumbent = NULL, *best = NULL;\n-\tuint64_t incumbent_rtt = UINT64_MAX, best_rtt = UINT64_MAX;\n-\tfor (peer = ctx->common.PEERS; peer; peer = peer->next) {\n-\t\tif (!rist_peer_bonded_data_leg(peer))\n-\t\t\tcontinue;\n-\t\tbool want = rist_peer_rtt_mute_eligible(peer) && peer->rtt_mute_state.muted;\n-\t\tif (!want && !peer->stalled) {\n-\t\t\tcarriers++;\n-\t\t} else if (want && !peer->stalled) {\n-\t\t\tuint64_t smoothed = peer->eight_times_rtt ? peer->eight_times_rtt / 8 : UINT64_MAX;\n-\t\t\tif (smoothed < best_rtt) { best_rtt = smoothed; best = peer; }\n-\t\t\tif (peer->rtt_sole_carrier) { incumbent = peer; incumbent_rtt = smoothed; }\n-\t\t}\n-\t}\n-\n-\t/* Pick the leg to keep when nothing else can carry. Re-running this from\n-\t * the instantaneous RTT every tick made the payload path ping-pong between\n-\t * two equally bad legs roughly once a second, which is worse for the stream\n-\t * than staying on either one, so the incumbent holds the role until it\n-\t * recovers, goes stalled, or a sibling measures several times better and\n-\t * the incumbent has served at least one drop dwell. */\n-\tstruct rist_peer *keep = NULL;\n-\tif (carriers == 0) {\n-\t\tkeep = incumbent ? incumbent : best;\n-\t\tif (incumbent && best && best != incumbent\n-\t\t    && rist_rtt_sole_carrier_handover(incumbent_rtt, best_rtt,\n-\t\t\t\tnow - incumbent->rtt_sole_since,\n-\t\t\t\t(uint64_t)incumbent->config.rtt_drop_settle * RIST_CLOCK,\n-\t\t\t\tRIST_SOLE_CARRIER_MARGIN))\n-\t\t\tkeep = best;\n-\t}\n-\tfor (peer = ctx->common.PEERS; peer; peer = peer->next) {\n-\t\tif (!rist_peer_bonded_data_leg(peer))\n-\t\t\tcontinue;\n-\t\tif (peer != keep)\n-\t\t\tpeer->rtt_sole_carrier = false;\n-\t\telse if (!peer->rtt_sole_carrier) {\n-\t\t\tpeer->rtt_sole_carrier = true;\n-\t\t\tpeer->rtt_sole_since = now;\n-\t\t}\n-\t}\n-\n-\t/* Apply, counting/logging only genuine peer->rtt_muted transitions. */\n-\tfor (peer = ctx->common.PEERS; peer; peer = peer->next) {\n-\t\tif (!rist_peer_rtt_mute_eligible(peer))\n-\t\t\tcontinue;\n-\t\tbool mute = peer->rtt_mute_state.muted && peer != keep;\n-\t\tif (mute && !peer->rtt_muted) {\n-\t\t\tuint64_t smoothed = (peer->eight_times_rtt / 8) / RIST_CLOCK;\n-\t\t\tpeer->rtt_muted = true;\n-\t\t\tpeer->rtt_mute_count++;\n-\t\t\t/* Below the ceiling means the leg spiked earlier and is still\n-\t\t\t * serving out its rejoin dwell, not that it just crossed. */\n-\t\t\tif (smoothed > peer->config.rtt_drop)\n-\t\t\t\trist_log_priv(&ctx->common, RIST_LOG_INFO,\n-\t\t\t\t\t\"Peer %\"PRIu32\" muted: smoothed RTT %\"PRIu64\"ms over %ums ceiling\\n\",\n-\t\t\t\t\tpeer->adv_peer_id, smoothed, peer->config.rtt_drop);\n-\t\t\telse\n-\t\t\t\trist_log_priv(&ctx->common, RIST_LOG_INFO,\n-\t\t\t\t\t\"Peer %\"PRIu32\" muted: smoothed RTT %\"PRIu64\"ms, rejoin dwell not met\\n\",\n-\t\t\t\t\tpeer->adv_peer_id, smoothed);\n-\t\t} else if (!mute && peer->rtt_muted) {\n-\t\t\tpeer->rtt_muted = false;\n-\t\t\tpeer->rtt_trickle_counter = 0;\n-\t\t\t/* Rejoin gradually: an idle leg measures well until it carries\n-\t\t\t * again, so ramp its share back instead of re-flooding it. */\n-\t\t\tpeer->rtt_ramp_start = now;\n-\t\t\tif (peer->rtt_mute_state.muted)\n-\t\t\t\trist_log_priv(&ctx->common, RIST_LOG_INFO,\n-\t\t\t\t\t\"Peer %\"PRIu32\" kept as sole carrier (all bonded legs muted or stalled)\\n\",\n-\t\t\t\t\tpeer->adv_peer_id);\n-\t\t\telse\n-\t\t\t\trist_log_priv(&ctx->common, RIST_LOG_INFO,\n-\t\t\t\t\t\"Peer %\"PRIu32\" restored to bond: smoothed RTT %\"PRIu64\"ms\\n\",\n-\t\t\t\t\tpeer->adv_peer_id,\n-\t\t\t\t\t(peer->eight_times_rtt / 8) / RIST_CLOCK);\n-\t\t}\n-\t}\n-}\n-\n PTHREAD_START_FUNC(sender_pthread_protocol, arg)\n {\n \tstruct rist_sender *ctx = (struct rist_sender *) arg;\n@@ -4868,6 +3455,7 @@ PTHREAD_START_FUNC(sender_pthread_protocol, arg)\n \tint max_oobperloop = 100;\n \n \tint max_jitter_ms = ctx->common.rist_max_jitter / RIST_CLOCK;\n+\tuint64_t rist_stats_interval = ctx->stats_report_time; // 1 second\n \n \trist_log_priv(&ctx->common, RIST_LOG_INFO, \"Starting master sender loop at %d ms max jitter\\n\",\n \t\t\tmax_jitter_ms);\n@@ -4876,7 +3464,6 @@ PTHREAD_START_FUNC(sender_pthread_protocol, arg)\n \tctx->stats_next_time = now;\n \tctx->checks_next_time = now;\n \tuint64_t nacks_next_time = now;\n-\tuint64_t mute_check_next_time = now;\n \twhile(!atomic_load_explicit(&ctx->common.shutdown, memory_order_acquire)) {\n \t\t// Conditional 5ms sleep that is woken by data coming in\n \t\tpthread_mutex_lock(&(ctx->mutex));\n@@ -4900,25 +3487,8 @@ PTHREAD_START_FUNC(sender_pthread_protocol, arg)\n \t\t\tpthread_mutex_unlock(&ctx->common.peerlist_lock);\n \t\t}\n \n-\t\t/* Short tick so the configured settle is honoured, not quantised\n-\t\t * to the 1 s check cadence. */\n-\t\tif (now > mute_check_next_time)\n-\t\t{\n-\t\t\tmute_check_next_time = now + (uint64_t)250 * (uint64_t)RIST_CLOCK;\n-\t\t\tpthread_mutex_lock(&ctx->common.peerlist_lock);\n-\t\t\t/* Stall first so the mute check sees this tick's stalled\n-\t\t\t * flags and can keep a carrier when a sibling is silent. */\n-\t\t\trist_sender_stall_check(ctx, now);\n-\t\t\trist_sender_rtt_mute_check(ctx, now);\n-\t\t\tpthread_mutex_unlock(&ctx->common.peerlist_lock);\n-\t\t}\n-\n-\t\t// stats timer; 0 == disabled.  Read fresh so a callback registered\n-\t\t// after loop start takes effect.\n-\t\tuint64_t rist_stats_interval = ctx->stats_report_time;\n-\t\tif (rist_stats_interval == 0) {\n-\t\t\tctx->stats_next_time = now; // keep current to avoid a catch-up burst\n-\t\t} else if (now > ctx->stats_next_time) {\n+\t\t// stats timer\n+\t\tif (now > ctx->stats_next_time) {\n \t\t\tctx->stats_next_time += rist_stats_interval;\n \t\t\trist_sender_flow_statistics(ctx);\n \t\t\t// TODO: remove dead peers after stale flow time (both sender list and peer chain)\n@@ -4967,20 +3537,6 @@ PTHREAD_START_FUNC(sender_pthread_protocol, arg)\n \treturn 0;\n }\n \n-/* Idempotent: re-init is fine if cctx->profile is upgraded to Advanced\n- * after the original init_common_ctx ran (e.g. via ?profile= URL override\n- * in rist_peer_create).  Re-rolls SSRC base and clears the seq counters. */\n-void init_advanced_state(struct rist_common_ctx *ctx)\n-{\n-\t/* Generate a random even SSRC base for the Protected flow.\n-\t * The Unprotected flow uses ssrc_base | 1 (Section 5.2.1). */\n-\tuint32_t rnd = 0;\n-\t_librist_crypto_ramdom_get_bytes((uint8_t *)&rnd, sizeof(rnd));\n-\tctx->adv_ssrc_base = rnd & ~(uint32_t)1;\n-\tctx->adv_seq_protected = 0;\n-\tctx->adv_seq_unprotected = 0;\n-}\n-\n int init_common_ctx(struct rist_common_ctx *ctx, enum rist_profile profile)\n {\n #ifdef _WIN32\n@@ -4994,9 +3550,6 @@ int init_common_ctx(struct rist_common_ctx *ctx, enum rist_profile profile)\n #endif\n \tctx->evctx = evsocket_create();\n \tctx->rist_max_jitter = RIST_MAX_JITTER * RIST_CLOCK;\n-\tctx->cbr_output = false;\n-\tctx->cbr_output_min_us = RIST_CBR_OUTPUT_MIN_US_DEFAULT;\n-\tctx->recovery_queue_max = RIST_SERVER_QUEUE_BUFFERS;\n \tif (profile > RIST_PROFILE_ADVANCED) {\n \t\trist_log_priv3( RIST_LOG_ERROR, \"Profile not supported (%d), using main profile instead\\n\", profile);\n \t\tprofile = RIST_PROFILE_MAIN;\n@@ -5009,12 +3562,8 @@ int init_common_ctx(struct rist_common_ctx *ctx, enum rist_profile profile)\n \t\trist_log_priv3( RIST_LOG_INFO, \"Starting in Advanced Profile Mode\\n\");\n \n \tctx->profile = profile;\n-\tatomic_store_explicit(&ctx->profile_locked, false, memory_order_release);\n \tctx->stats_report_time = 0;\n \n-\tif (profile == RIST_PROFILE_ADVANCED)\n-\t\tinit_advanced_state(ctx);\n-\n \tif (pthread_mutex_init(&ctx->peerlist_lock, NULL) != 0) {\n \t\trist_log_priv3( RIST_LOG_ERROR, \"Failed to init ctx->peerlist_lock\\n\");\n \t\treturn -1;\n@@ -5064,45 +3613,37 @@ static inline void peer_remove_linked_list(struct rist_peer *peer) {\n \n void remove_peer_from_flow(struct rist_peer *peer)\n {\n-\tstruct rist_flow *f = peer->flow;\n-\tif (f == NULL)\n-\t\treturn;\n-\tpthread_mutex_lock(&f->mutex);\n \tbool found = false;\n-\tfor (size_t i = 0; i < f->peer_lst_len; i++)\n+\tfor (size_t i = 0; i < peer->flow->peer_lst_len; i++)\n \t{\n-\t\tif (f->peer_lst[i] == peer)\n+\t\tif (peer->flow->peer_lst[i] == peer)\n \t\t{\n-\t\t\tf->peer_lst[i] = f->peer_lst[(f->peer_lst_len -1)];\n+\t\t\tpeer->flow->peer_lst[i] = peer->flow->peer_lst[(peer->flow->peer_lst_len -1)];\n \t\t\tfound = true;\n \t\t\tbreak;\n \t\t}\n \t}\n \tif (found)\n \t{\n-\t\tif (f->peer_lst_len > 1)\n+\t\tif (peer->flow->peer_lst_len > 1)\n \t\t{\n-\t\t\tstruct rist_peer **shrunk = realloc(f->peer_lst, sizeof(peer) * (f->peer_lst_len -1));\n-\t\t\t/* Shrink realloc may fail; keep the oversized allocation. */\n-\t\t\tif (shrunk)\n-\t\t\t\tf->peer_lst = shrunk;\n-\t\t\tf->peer_lst_len--;\n+\t\t\tpeer->flow->peer_lst = realloc(peer->flow->peer_lst, sizeof(peer) * (peer->flow->peer_lst_len -1));\n+\t\t\tpeer->flow->peer_lst_len--;\n \t\t} else\n \t\t{\n-\t\t\tfree(f->peer_lst);\n-\t\t\tf->peer_lst_len = 0;\n-\t\t\tf->peer_lst = NULL;\n+\t\t\tfree(peer->flow->peer_lst);\n+\t\t\tpeer->flow->peer_lst_len = 0;\n+\t\t\tpeer->flow->peer_lst = NULL;\n \t\t}\n \t}\n-\tpthread_mutex_unlock(&f->mutex);\n }\n \n int rist_peer_remove(struct rist_common_ctx *ctx, struct rist_peer *peer, struct rist_peer **next)\n {\n \tif (peer == NULL) {\n+\t\treturn -1;\n \t\tif (next)\n \t\t\t*next = NULL;\n-\t\treturn -1;\n \t}\n \tatomic_store_explicit(&peer->shutdown, true, memory_order_release);\n \tif (peer->send_first_connection_event  && !peer->timed_out && ctx->connection_status_callback && (ctx->profile != RIST_PROFILE_SIMPLE || peer->is_rtcp))\n@@ -5189,10 +3730,7 @@ int rist_peer_remove(struct rist_common_ctx *ctx, struct rist_peer *peer, struct\n \n \t\tif (found) {\n \t\t\tif (peer->flow->peer_lst_len > 1) {\n-\t\t\t\t/* Shrink realloc may fail; keep the oversized allocation. */\n-\t\t\t\tstruct rist_peer **shrunk = realloc(peer->flow->peer_lst, sizeof(peer) * (peer->flow->peer_lst_len -1));\n-\t\t\t\tif (shrunk)\n-\t\t\t\t\tpeer->flow->peer_lst = shrunk;\n+\t\t\t\tpeer->flow->peer_lst = realloc(peer->flow->peer_lst, sizeof(peer) * (peer->flow->peer_lst_len -1));\n \t\t\t\tpeer->flow->peer_lst_len--;\n \t\t\t} else {\n \t\t\t\tfree(peer->flow->peer_lst);\n@@ -5321,28 +3859,8 @@ static void store_peer_settings(const struct rist_peer_config *settings, struct\n \tpeer->config.min_retries = min_retries;\n \tpeer->config.max_retries = max_retries;\n \tpeer->config.weight = settings->weight;\n-\tpeer->config.recovery_priority = settings->recovery_priority;\n \tpeer->config.timing_mode = settings->timing_mode;\n \tpeer->config.virt_dst_port = settings->virt_dst_port;\n-\tif (settings->version >= 2)\n-\t\tpeer->config.reflector = settings->reflector;\n-\telse\n-\t\tpeer->config.reflector = 0;\n-\tif (settings->version >= 3)\n-\t\tpeer->config.srp_compat_legacy = settings->srp_compat_legacy; //read by rist_enable_eap_srp_2 after peer_create\n-\telse\n-\t\tpeer->config.srp_compat_legacy = 0;\n-\tif (settings->version >= 6) {\n-\t\tpeer->config.rtt_drop = settings->rtt_drop;\n-\t\tpeer->config.rtt_restore = settings->rtt_restore;\n-\t\tpeer->config.rtt_drop_settle = settings->rtt_drop_settle;\n-\t\tpeer->config.rtt_drop_trickle = settings->rtt_drop_trickle;\n-\t} else {\n-\t\tpeer->config.rtt_drop = 0;\n-\t\tpeer->config.rtt_restore = 0;\n-\t\tpeer->config.rtt_drop_settle = RIST_DEFAULT_RTT_DROP_SETTLE;\n-\t\tpeer->config.rtt_drop_trickle = 0;\n-\t}\n \n \tinit_peer_settings(peer);\n }\n@@ -5405,19 +3923,6 @@ void rist_empty_oob_queue(struct rist_common_ctx *ctx)\n \t\tindex++;\n \t}\n \tctx->oob_queue_bytesize = 0;\n-\n-\t/* drain the oob receive fifo and the last handed-out buffer */\n-\twhile (ctx->oob_rx_queue_read_index != ctx->oob_rx_queue_write_index) {\n-\t\tstruct rist_buffer *rx_buffer = ctx->oob_rx_queue[ctx->oob_rx_queue_read_index];\n-\t\tctx->oob_rx_queue[ctx->oob_rx_queue_read_index] = NULL;\n-\t\tif (rx_buffer)\n-\t\t\tfree_rist_buffer(ctx, rx_buffer);\n-\t\tctx->oob_rx_queue_read_index = (uint16_t)(ctx->oob_rx_queue_read_index + 1);\n-\t}\n-\tif (ctx->oob_rx_current) {\n-\t\tfree_rist_buffer(ctx, ctx->oob_rx_current);\n-\t\tctx->oob_rx_current = NULL;\n-\t}\n }\n \n void rist_receiver_destroy_local(struct rist_receiver *ctx)\n@@ -5472,8 +3977,6 @@ void rist_receiver_destroy_local(struct rist_receiver *ctx)\n \tpthread_cond_destroy(&ctx->condition);\n \tpthread_mutex_destroy(&ctx->mutex);\n \n-\trist_logging_unset_global_if_matches(ctx->common.logging_settings);\n-\n \tfree(ctx);\n \tctx = NULL;\n }\n@@ -5607,12 +4110,10 @@ PTHREAD_START_FUNC(receiver_pthread_protocol, arg)\n \t\t\t\t\t\t}\n \t\t\t\t\t\tpthread_mutex_unlock(&f->mutex);\n \t\t\t\t\t\tpthread_mutex_lock(&ctx->common.peerlist_lock);\n-\t\t\t\t\t\tpthread_mutex_lock(&f->mutex);\n \t\t\t\t\t\tfor (size_t i = 0; i < f->peer_lst_len; i++) {\n \t\t\t\t\t\t\tstruct rist_peer *peer = f->peer_lst[i];\n \t\t\t\t\t\t\tpeer->flow = NULL;\n \t\t\t\t\t\t}\n-\t\t\t\t\t\tpthread_mutex_unlock(&f->mutex);\n \t\t\t\t\t\trist_delete_flow(ctx, f);\n \t\t\t\t\t\tpthread_mutex_unlock(&ctx->common.peerlist_lock);\n \t\t\t\t\t\tf = next;\n@@ -5713,7 +4214,6 @@ void rist_sender_destroy_local(struct rist_sender *ctx)\n \n \trist_log_priv(&ctx->common, RIST_LOG_INFO, \"Freeing up context memory allocations\\n\");\n \tfree(ctx->sender_retry_queue);\n-\tctx->sender_retry_queue = NULL;\n \tstruct rist_buffer *b = NULL;\n \twhile(1) {\n \t\tb = ctx->sender_queue[ctx->sender_queue_delete_index];\n@@ -5734,15 +4234,6 @@ void rist_sender_destroy_local(struct rist_sender *ctx)\n \t\t}\n \t\tctx->sender_queue_delete_index = (ctx->sender_queue_delete_index + 1)& (ctx->sender_queue_max -1);\n \t}\n-\n-\trist_logging_unset_global_if_matches(ctx->common.logging_settings);\n-\n-\tfree(ctx->sender_queue);\n-\tctx->sender_queue = NULL;\n-\tfree(ctx->seq_index);\n-\tctx->seq_index = NULL;\n-\tfree(ctx->seq_rtp_index);\n-\tctx->seq_rtp_index = NULL;\n \tfree(ctx);\n \tctx = NULL;\n }\ndiff --git a/src/rist-nack-select.h b/src/rist-nack-select.h\ndeleted file mode 100644\nindex e251db1..0000000\n--- a/src/rist-nack-select.h\n+++ /dev/null\n@@ -1,32 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * NACK routing peer-selection predicate.  Kept in a small standalone\n- * header so the comparison can be unit-tested in isolation, without\n- * constructing full peer objects or starting receiver threads.\n- */\n-\n-#ifndef RIST_NACK_SELECT_H\n-#define RIST_NACK_SELECT_H\n-\n-#include <stdbool.h>\n-#include <stdint.h>\n-\n-/* Decide whether a candidate retransmission peer should replace the\n- * current best choice for receiving a NACK group.\n- *\n- * Selection key, highest precedence first:\n- *   1. higher recovery_priority wins;\n- *   2. on equal priority, lower RTT wins.\n- *\n- * With the default recovery_priority of 0 on every peer this collapses\n- * to \"lowest RTT wins\", matching the historical behaviour. */\n-static inline bool rist_nack_peer_preferred(uint32_t cand_priority,\n-                                            uint64_t cand_rtt,\n-                                            uint32_t best_priority,\n-                                            uint64_t best_rtt)\n-{\n-\treturn cand_priority > best_priority ||\n-\t       (cand_priority == best_priority && cand_rtt < best_rtt);\n-}\n-\n-#endif /* RIST_NACK_SELECT_H */\ndiff --git a/src/rist-private.h b/src/rist-private.h\nindex 75508dc..a9fb578 100755\n--- a/src/rist-private.h\n+++ b/src/rist-private.h\n@@ -23,58 +23,23 @@\n #include \"time-shim.h\"\n #include \"pthread-shim.h\"\n #include \"socket-shim.h\"\n-#include \"rist_pacer.h\"\n #include \"libevsocket.h\"\n #include \"librist.h\"\n-#include \"librist/transport.h\"\n #include \"udpsocket.h\"\n #include \"crypto/psk.h\"\n #include <errno.h>\n #include <stdatomic.h>\n #include \"librist/logging.h\"\n #include \"proto/gre.h\"\n-#include \"proto/adv.h\"\n-#include \"rist-adv-ts.h\"\n-#include \"rist-reanchor.h\"\n-#include \"rist-send-grace.h\"\n-#include \"rist-bandwidth-guard.h\"\n-#include \"rist-rtt-mute.h\"\n \n struct cJSON;\n \n #undef RIST_DEPRECATED\n \n #define UINT16_SIZE (UINT16_MAX + 1)\n-\n-/* Forward sequence-number gap. short_seq (Simple/Main) flows wrap at 16 bits;\n- * full 32-bit (Advanced) flows use the true difference so a genuine >64k gap is\n- * not truncated. Inputs are modular counters, so the subtraction is unsigned\n- * (well-defined wrap) before the optional 16-bit mask. */\n-static inline uint32_t rist_seq_gap(uint32_t current, uint32_t last,\n-                                    bool short_seq)\n-{\n-\tuint32_t gap = current - last;\n-\treturn short_seq ? (gap & UINT16_MAX) : gap;\n-}\n-\n-/* Next expected sequence number after `last`. short_seq (Simple/Main) flows\n- * wrap at 16 bits; 32-bit (Advanced) flows use the natural successor. */\n-static inline uint32_t rist_seq_next(uint32_t last, bool short_seq)\n-{\n-\tuint32_t next = last + 1;\n-\treturn short_seq ? (next & UINT16_MAX) : next;\n-}\n-\n-// These control the memory footprint and buffer capacity of the lib\n+// These 4 control the memory footprint and buffer capacity of the lib\n // They MUST be a power of two or wrap-around index calculations will break\n-// RIST_SERVER_QUEUE_BUFFERS is the DEFAULT Advanced-profile recovery-ring\n-// capacity (in packets), == UINT16_SIZE << RIST_RECOVERY_DEPTH_DEFAULT (8x).\n-// The ring is heap-allocated per flow/sender and can be resized at runtime to\n-// UINT16_SIZE << depth via rist_recovery_depth_set() / ?recovery-depth=, up to\n-// RIST_RECOVERY_QUEUE_MAX (depth 16, the full 32-bit space). Large depths are\n-// limited by available RAM. Simple/Main are always UINT16_SIZE.\n #define RIST_SERVER_QUEUE_BUFFERS ((UINT16_SIZE) * 8)\n-#define RIST_RECOVERY_QUEUE_MAX ((uint64_t)(UINT16_SIZE) << 16)\n #define RIST_RETRY_QUEUE_BUFFERS ((UINT16_SIZE) * 4)\n #define RIST_OOB_QUEUE_BUFFERS ((UINT16_SIZE) * 2)\n #define RIST_DATAOUT_QUEUE_BUFFERS (1024)\n@@ -85,32 +50,7 @@ static inline uint32_t rist_seq_next(uint32_t last, bool short_seq)\n /* nack requests are sent every time a data packet is received. */\n /* this timer will be triggered to ensure we output nacks even when there is no data coming in */\n #define RIST_MAX_JITTER (5) /* In milliseconds */\n-\n-/* Bounds on the output loop's wake interval when CBR pacing is on, trading CPU\n- * against spacing: residual burst is ceil(floor / datagram interval). The working\n- * floor tracks the measured interval and is clamped to these. */\n-#define RIST_CBR_OUTPUT_MIN_US_DEFAULT (250)\n-#define RIST_CBR_OUTPUT_MIN_US_FLOOR (20)\n-/* Pacing gives way once a packet is this far past its release deadline, so it can\n- * never age a packet out of the recovery buffer. A floor: the working value has to\n- * cover a burst's drain time, which the output jitter ceiling bounds. */\n-#define RIST_CBR_MAX_HOLD_US (1000)\n-#define RIST_CBR_HOLD_JITTER_MULT (2)\n-\n-static inline uint32_t rist_cbr_hold_us(uint32_t max_output_jitter_ms)\n-{\n-\tuint32_t hold = RIST_CBR_HOLD_JITTER_MULT * max_output_jitter_ms * 1000;\n-\treturn hold > RIST_CBR_MAX_HOLD_US ? hold : RIST_CBR_MAX_HOLD_US;\n-}\n #define RIST_PING_INTERVAL (100)  /* In milliseconds, how long to space ping requests */\n-/* Missed RTCP/ping intervals before a bonded leg is pulled from the rotation\n- * (fast, reversible); kept well under the liveness timeout, the real teardown. */\n-#define RIST_STALL_MUTE_PINGS (3)\n-/* Floor for the per-peer liveness timeout, in missed RTCP/ping intervals. */\n-#define RIST_LIVENESS_MIN_PINGS (4)\n-/* How much better a sibling must measure before it takes the sole-carrier role\n- * from the incumbent, as a divisor of the incumbent's smoothed RTT. */\n-#define RIST_SOLE_CARRIER_MARGIN (2)\n #define RIST_PBKDF2_HMAC_SHA256_ITERATIONS (1024)\n #define RIST_AES_KEY_REUSE_TIMES UINT32_MAX\n #define RIST_MAX_HOSTNAME (128)\n@@ -169,22 +109,6 @@ struct rist_buffer {\n \tbool retry_queued;\n };\n \n-/* Largest recovery depth whose ring (UINT16_SIZE << depth packets) is\n- * addressable as size_t: the two parallel index arrays cost\n- * sizeof(ptr) + sizeof(uint32_t) per slot. Depth 16 (2^32) only fits a 64-bit\n- * size_t; on a 32-bit size_t the exponent is reduced until it fits. Shared with\n- * the recovery-depth unit test. */\n-static inline int rist_recovery_depth_platform_max(void)\n-{\n-\tconst uint64_t per = (uint64_t)sizeof(struct rist_buffer *) + sizeof(uint32_t);\n-\tconst uint64_t limit = (uint64_t)SIZE_MAX / per;\n-\tint depth = RIST_RECOVERY_DEPTH_MAX;\n-\twhile (depth > RIST_RECOVERY_DEPTH_MIN &&\n-\t       ((uint64_t)UINT16_SIZE << depth) > limit)\n-\t\tdepth--;\n-\treturn depth;\n-}\n-\n struct rist_missing_buffer {\n \tuint32_t seq;\n \tuint64_t next_nack;\n@@ -235,9 +159,6 @@ struct rist_peer_flow_stats {\n \tuint64_t cur_ips;\n \tuint32_t avg_count;\n \tuint64_t total_ips;\n-\n-\t/* Per-byte counters */\n-\tuint64_t received_bytes;\n };\n \n struct rist_peer_sender_stats {\n@@ -248,8 +169,6 @@ struct rist_peer_sender_stats {\n \tuint32_t bandwidth_skip;\n \tuint32_t retrans_skip;\n \tuint64_t ts_null;\n-\tuint64_t sent_bytes;\n-\tuint64_t retransmitted_bytes;\n };\n \n struct rist_peer_receiver_stats {\n@@ -257,7 +176,6 @@ struct rist_peer_receiver_stats {\n \tuint32_t received_rtcp;\n \tuint64_t received;\n \tuint64_t ts_null;\n-\tuint64_t received_bytes;\n };\n \n struct nacks {\n@@ -269,7 +187,7 @@ struct rist_flow {\n \tatomic_int shutdown;\n \tint max_output_jitter;\n \n-\tstruct rist_buffer **receiver_queue; /* output queue, heap-allocated to receiver_queue_max */\n+\tstruct rist_buffer *receiver_queue[RIST_SERVER_QUEUE_BUFFERS]; /* output queue */\n \n \tpthread_rwlock_t queue_lock;\n \n@@ -283,21 +201,6 @@ struct rist_flow {\n \tsize_t receiver_queue_max;\n \tbool flag_flow_buffer_start;\n \n-\t/* Constant-bitrate output pacing; see rist_pacer.h. cbr_arrived_bytes is\n-\t * written by the receive threads and drained by the output thread: the\n-\t * estimate must come from arrivals, since measuring the paced output would\n-\t * make the rate an output of itself. */\n-\tbool cbr_output;\n-\tuint32_t cbr_output_min_us;\n-\tatomic_uint_least64_t cbr_arrived_bytes;\n-\tuint64_t cbr_arrived_seen;\n-\tstruct rist_pacer_rate cbr_rate;\n-\tstruct rist_pacer cbr_pacer;\n-\tuint64_t cbr_overdue_releases;   /* pacer yielded to the buffer deadline */\n-\tuint32_t cbr_max_hold_us;        /* longest a paced packet may wait */\n-\tbool cbr_paced_hold;             /* last output pass stopped on the pacer */\n-\tuint64_t cbr_interval_ns;        /* last paced interval, sizes the wake floor */\n-\n \t/* Missing incoming packets, waiting for retransmission */\n \tstruct rist_missing_buffer *missing;\n \tstruct rist_missing_buffer *missing_tail;\n@@ -330,9 +233,6 @@ struct rist_flow {\n \tuint64_t last_output_time;\n \tuint64_t max_source_time;\n \tuint64_t too_late_ctr;\n-\t/* When we started holding off a re-anchor for want of a current packet.\n-\t * Zero while not holding off. */\n-\tuint64_t reanchor_wait_since;\n \n \tsize_t offset_recalc_sample_count;\n \tuint64_t offset_recalc_samples[2048];\n@@ -355,9 +255,6 @@ struct rist_flow {\n \t/* variable used for seq number length (16bit or 32bit) */\n \tbool short_seq;\n \n-\t/* Scope-B merge auto-detection: set by keepalive L bit */\n-\tbool merge_auto_enabled;\n-\n \t/* Session timeouts variables */\n \tuint64_t session_timeout;\n \tuint64_t flow_timeout;\n@@ -401,21 +298,10 @@ struct rist_common_ctx {\n \n \t/* Timers */\n \tint rist_max_jitter;\n-\t/* CBR output pacing, per context: a flow's legs share one output, so this is\n-\t * not a per-peer property. */\n-\tbool cbr_output;\n-\tbool cbr_output_set;   /* a value was stated; later conflicts refused */\n-\tuint32_t cbr_output_min_us;\n \n \t/* Recovery buffer RTT multiplier (default 7, per RIST spec) */\n \tint recovery_rtt_multiplier;\n \n-\t/* Advanced-profile recovery-ring capacity in packets (power of two).\n-\t * Default RIST_SERVER_QUEUE_BUFFERS; tunable before rist_start() via\n-\t * rist_recovery_depth_set(). Simple/Main ignore this and use\n-\t * UINT16_SIZE. */\n-\tsize_t recovery_queue_max;\n-\n \t/* Peer list sync - RW locks */\n \tstruct rist_peer *PEERS;\n \tpthread_mutex_t peerlist_lock;\n@@ -438,21 +324,12 @@ struct rist_common_ctx {\n \tuint64_t stats_report_time;\n \n \tenum rist_profile profile;\n-\t/* Set by rist_*_start() and by rist_peer_create() once a peer with\n-\t * a definite profile has been inserted.  When set, rist_peer_create\n-\t * refuses any peer whose ?profile= disagrees with cctx->profile. */\n-\tatomic_bool profile_locked;\n \tuint8_t cname[RIST_MAX_HOSTNAME];\n \n \t/* seq variables */\n \tuint32_t seq;\n \tuint16_t seq_rtp;\n \n-\t/* Advanced Profile (TR-06-3) state */\n-\tuint32_t adv_ssrc_base;       /* Even SSRC for Protected flow */\n-\tuint32_t adv_seq_protected;   /* 32-bit seq counter for even SSRC */\n-\tuint32_t adv_seq_unprotected; /* 32-bit seq counter for odd SSRC */\n-\n \t/* Peer counter (only the ones created by the API) */\n \tuint32_t peer_counter;\n \n@@ -474,26 +351,14 @@ struct rist_common_ctx {\n \tpthread_mutex_t stats_lock;\n \n \tpthread_rwlock_t oob_queue_lock;\n-\tstruct rist_buffer *oob_queue[RIST_OOB_QUEUE_BUFFERS]; /* oob transmit queue */\n+\tstruct rist_buffer *oob_queue[RIST_OOB_QUEUE_BUFFERS]; /* oob queue */\n \tsize_t oob_queue_bytesize;\n \tuint16_t oob_queue_read_index;\n \tuint16_t oob_queue_write_index;\n \n-\t/* oob receive fifo: populated by the protocol thread when oob is\n-\t * enabled but no callback is set, drained by rist_oob_read(). */\n-\tstruct rist_buffer *oob_rx_queue[RIST_OOB_QUEUE_BUFFERS];\n-\tuint16_t oob_rx_queue_read_index;\n-\tuint16_t oob_rx_queue_write_index;\n-\tstruct rist_buffer *oob_rx_current; /* backs the block handed out by the last rist_oob_read */\n-\tstruct rist_oob_block oob_rx_block; /* borrowed view returned to the caller */\n-\n \tbool debug;\n \tuint32_t birthtime_rtp_offset;\n \n-\t/* Pluggable transport (default: POSIX sockets) */\n-\tstruct rist_transport_ops transport;\n-\tbool transport_active;\n-\n \t/* Connection status callback */\n \tconnection_status_callback_t connection_status_callback;\n \tvoid *connection_status_callback_argument;\n@@ -522,10 +387,6 @@ struct rist_receiver {\n \treceiver_session_timeout_callback_t receiver_session_timeout_callback;\n \tvoid *receiver_session_timeout_callback_argument;\n \n-\t/* Receiver flow attribute callback (Advanced Profile CI=0x8001) */\n-\treceiver_flow_attr_callback_t receiver_flow_attr_callback;\n-\tvoid *receiver_flow_attr_callback_argument;\n-\n \t/* Receiver thread variables */\n \tbool protocol_running;\n \tpthread_t receiver_thread;\n@@ -541,11 +402,6 @@ struct rist_receiver {\n \tbool simulate_loss;\n \tuint16_t loss_percentage;\n \tuint32_t fifo_queue_size;\n-\n-\tuint32_t merge_mode;\n-\tuint64_t stats_pairs_merged;\n-\tuint64_t stats_orphan_first_delivered;\n-\tuint64_t stats_orphan_last_delivered;\n };\n \n struct rist_sender {\n@@ -557,10 +413,6 @@ struct rist_sender {\n \tuint32_t max_nacksperloop;\n \tbool null_packet_suppression;\n \n-\tuint32_t split_mode;\n-\tuint64_t stats_pairs_emitted;\n-\tuint64_t stats_split_fallback_not_ts;\n-\n \t/* Sender thread variables */\n \tbool protocol_running;\n \tpthread_t sender_thread;\n@@ -570,7 +422,7 @@ struct rist_sender {\n \n \tbool sender_initialized;\n \tuint32_t total_weight;\n-\tstruct rist_buffer **sender_queue; /* input queue, heap-allocated to sender_queue_max */\n+\tstruct rist_buffer *sender_queue[RIST_SERVER_QUEUE_BUFFERS]; /* input queue */\n \tsize_t sender_queue_bytesize;\n \tsize_t sender_queue_size;\n \tsize_t sender_queue_timelength;\n@@ -595,16 +447,8 @@ struct rist_sender {\n \tuint64_t cooldown_time;\n \tint cooldown_mode;\n \n-\t/* Recovery - heap-allocated to sender_queue_max so Advanced Profile\n-\t * can index with the full 32-bit seq space (seq & (queue_max - 1)). */\n-\tuint32_t *seq_index;\n-\t/* Parallel 16-bit-RTP index (65536 entries), allocated only for an\n-\t * Advanced-context sender. A peer that negotiated down to Main NACKs in\n-\t * the 16-bit RTP sequence domain (nack_seq_msb = 0), which never matches\n-\t * the 32-bit advanced seq_index; this lets the Advanced sender still find\n-\t * and retransmit the requested packet for that peer. NULL on Main/Simple\n-\t * senders (their primary seq_index is already the 16-bit domain). */\n-\tuint32_t *seq_rtp_index;\n+\t/* Recovery */\n+\tuint32_t seq_index[UINT16_SIZE];\n \tsize_t sender_recover_min_time;\n \tsize_t sender_queue_buffer_size;\n \n@@ -706,32 +550,6 @@ struct rist_peer {\n \t/* RTT statistics */\n \tuint64_t last_rtt;\n \n-\t/* Dynamic RTT-based muting (sender bonding, ?rtt-drop=). When muted the\n-\t * leg is skipped in the weighted payload rotation but its connection and\n-\t * RTCP probing continue so RTT keeps updating for the restore decision. */\n-\tbool rtt_muted;\n-\tstruct rist_rtt_mute_state rtt_mute_state;\n-\tuint32_t rtt_trickle_counter; /* 1-in-N counter for redundant trickle sends */\n-\tuint32_t rtt_mute_count; /* cumulative count of RTT-triggered mute events */\n-\t/* Elected to keep carrying while every leg wants mute. Sticky: held until\n-\t * the leg recovers or a sibling is clearly better, so the payload path\n-\t * does not ping-pong between two equally bad legs. */\n-\tbool rtt_sole_carrier;\n-\tuint64_t rtt_sole_since;\n-\t/* Start of the post-restore weight ramp. A leg that was muted for high RTT\n-\t * has an empty queue, so it measures well until it is loaded again;\n-\t * rejoining at full share re-floods it. Zero when no ramp is in progress. */\n-\tuint64_t rtt_ramp_start;\n-\n-\t/* Last time this peer was warned that its payload rate leaves no\n-\t * retransmission budget under its bandwidth ceiling; 0 if never. */\n-\tuint64_t bandwidth_warn_ts;\n-\n-\t/* Briefly-silent bonded leg: skipped in the payload rotation but kept\n-\t * authenticated, so it resumes without re-auth if it returns before the\n-\t * liveness timeout (rist_sender_stall_check / rist_peer_liveness_timeout). */\n-\tbool stalled;\n-\n \t/* Missing queue max size */\n \tuint32_t missing_counter_max;\n \n@@ -748,11 +566,6 @@ struct rist_peer {\n \tint eap_authentication_state;\n \tuint8_t rist_gre_version;\n \n-\t/* Advanced Profile (TR-06-3) peer state */\n-\tbool is_advanced;              /* Peer operating in Advanced Profile mode */\n-\tbool remote_supports_advanced; /* Remote advertised I=1 in keep-alive */\n-\tstruct rist_adv_ts_state rx_adv_ts; /* receive-side 1 MHz source-clock rebuild */\n-\n \t/* compression flag (sender only) */\n \tbool compression;\n \n@@ -785,9 +598,6 @@ struct rist_peer {\n \tstruct rist_sender *sender_ctx;\n \tstruct rist_receiver *receiver_ctx;\n \n-\t/* Reflector role flags */\n-\tbool is_reflector_publisher;\n-\n \t/* rist buffer bloating counteract */\n \tuint64_t cooldown_time;\n \n@@ -803,9 +613,6 @@ struct rist_peer {\n \tint dead;\n \tint timed_out;\n \tuint64_t dead_since;\n-\t/* Caller-side socket rebind bookkeeping. */\n-\tuint64_t last_rebind_time;\n-\tuint32_t rebind_attempts;\n \tuint64_t birthtime_peer;\n \tuint64_t birthtime_local;\n \n@@ -821,7 +628,6 @@ struct rist_peer {\n \tuint32_t rtcp_keepalive_interval;\n \tuint64_t next_periodic_rtcp;\n \tuint64_t next_keepalive_packet;\n-\tuint64_t next_flow_attr;\n \tuint64_t session_timeout;\n \tuint64_t last_pkt_received;\n \tuint64_t last_sender_report_time;\n@@ -833,11 +639,6 @@ struct rist_peer {\n \tuint8_t mac_addr[6];\n \tbool send_first_connection_event;\n \n-\t/* Rate-limit for EMSGSIZE/PMTU-too-large send errors so a flow of\n-\t * oversized packets doesn't drown the log. Holds the last log time\n-\t * in NTP ticks. */\n-\tuint64_t last_pmtu_error_log;\n-\n \tuint64_t log_repeat_timer;\n \n \tuint8_t data[SIZEOF_GRE_KEEPALIVE];\n@@ -866,7 +667,6 @@ RIST_PRIV size_t rist_best_rtt_index(struct rist_flow *f);\n RIST_PRIV struct rist_buffer *rist_new_buffer(struct rist_common_ctx *ctx, const void *buf, size_t len, uint8_t type, uint32_t seq, uint64_t source_time, uint16_t src_port, uint16_t dst_port);\n RIST_PRIV void free_rist_buffer(struct rist_common_ctx *ctx, struct rist_buffer *b);\n RIST_PRIV void rist_calculate_bitrate(size_t len, struct rist_bandwidth_estimation *bw);\n-RIST_PRIV void rist_refresh_flow_bitrate(struct rist_bandwidth_estimation *bw);\n RIST_PRIV void empty_receiver_queue(struct rist_flow *f, struct rist_common_ctx *ctx);\n RIST_PRIV void rist_flush_missing_flow_queue(struct rist_flow *flow);\n \n@@ -893,10 +693,7 @@ RIST_PRIV struct rist_peer *rist_sender_peer_insert_local(struct rist_sender *ct\n \t\t\t\t\t\t\t\t\t\t\t\t\t\t  const struct rist_peer_config *config, bool b_rtcp);\n RIST_PRIV void rist_fsm_init_comm(struct rist_peer *peer);\n RIST_PRIV int rist_oob_enqueue(struct rist_common_ctx *ctx, struct rist_peer *peer, const void *buf, size_t len);\n-\n-RIST_PRIV int rist_oob_dequeue_rx(struct rist_common_ctx *ctx, const struct rist_oob_block **oob_block);\n RIST_PRIV int init_common_ctx(struct rist_common_ctx *ctx, enum rist_profile profile);\n-RIST_PRIV void init_advanced_state(struct rist_common_ctx *ctx);\n RIST_PRIV int rist_peer_remove(struct rist_common_ctx *ctx, struct rist_peer *peer, struct rist_peer **next);\n RIST_PRIV int rist_auth_handler(struct rist_common_ctx *ctx,\n \t\t\t\t\t\t\t\tint (*conn_cb)(void *arg, const char *connecting_ip, uint16_t connecting_port, const char *local_ip, uint16_t local_port, struct rist_peer *peer),\n@@ -907,21 +704,6 @@ RIST_PRIV void sender_peer_append(struct rist_sender *ctx, struct rist_peer *pee\n /* Get common context */\n RIST_PRIV struct rist_common_ctx *get_cctx(struct rist_peer *peer);\n \n-/* Fold a fresh RTT measurement into the peer's 8-tap smoothed average. RTT is\n- * measured on whichever peer object receives the response, so mirror it onto\n- * the data leg: that is where the balancer and the stats read it from. */\n-static inline void rist_peer_rtt_update(struct rist_peer *peer, uint64_t rtt)\n-{\n-\tpeer->last_rtt = rtt;\n-\tpeer->eight_times_rtt -= peer->eight_times_rtt / 8;\n-\tpeer->eight_times_rtt += peer->last_rtt;\n-\tif (peer->peer_data && peer->peer_data != peer)\n-\t{\n-\t\tpeer->peer_data->last_rtt = peer->last_rtt;\n-\t\tpeer->peer_data->eight_times_rtt = peer->eight_times_rtt;\n-\t}\n-}\n-\n /*static inline in header file */\n static inline void peer_append(struct rist_peer *p)\n {\ndiff --git a/src/rist-reanchor.h b/src/rist-reanchor.h\ndeleted file mode 100644\nindex a8edbcb..0000000\n--- a/src/rist-reanchor.h\n+++ /dev/null\n@@ -1,49 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Whether a packet may serve as the new baseline for a flow whose buffer has\n- * just been emptied.  Kept in a small standalone header so the decision can be\n- * unit-tested without a flow, a peer or a receiver thread.\n- */\n-\n-#ifndef RIST_REANCHOR_H\n-#define RIST_REANCHOR_H\n-\n-#include <stdint.h>\n-\n-enum rist_reanchor_action\n-{\n-\tRIST_REANCHOR_OK = 0,     /* current enough; anchor the flow on it */\n-\tRIST_REANCHOR_WAIT = 1,   /* too far behind; drop it and wait */\n-\tRIST_REANCHOR_FORCED = 2, /* too far behind, but nothing better is coming */\n-};\n-\n-/* Anchoring adopts a packet's sequence number and clock as the flow baseline,\n- * so it has to come from a leg that is current. A bonded leg queued seconds\n- * deep keeps delivering packets that old, and anchoring on one rewinds the\n- * baseline by its lag: the next healthy packet then looks like a multi-second\n- * jump, which empties the buffer again and can repeat until the stream dies.\n- *\n- * A packet more than buffer behind the highest source time the flow has seen\n- * cannot be output anyway, so it is never a good baseline. Waiting for a better\n- * one is bounded at one buffer window, after which there is no current leg left\n- * to wait for -- or the source itself restarted on a lower clock -- and the\n- * packet in hand is taken (FORCED).\n- *\n- * max_source_time or buffer of 0 means there is no baseline to compare against\n- * yet (first packet of a flow, or just after a clock wrap): anchor freely.\n- * waiting_since is when the flow first refused a candidate, 0 if it has not.\n- * All times share one unit (RIST_CLOCK ticks). */\n-static inline enum rist_reanchor_action\n-rist_flow_reanchor_check(uint64_t source_time, uint64_t max_source_time,\n-                         uint64_t buffer, uint64_t waiting_since, uint64_t now)\n-{\n-\tif (!max_source_time || !buffer)\n-\t\treturn RIST_REANCHOR_OK;\n-\tif ((source_time + buffer) >= max_source_time)\n-\t\treturn RIST_REANCHOR_OK;\n-\tif (waiting_since && (now - waiting_since) >= buffer)\n-\t\treturn RIST_REANCHOR_FORCED;\n-\treturn RIST_REANCHOR_WAIT;\n-}\n-\n-#endif /* RIST_REANCHOR_H */\ndiff --git a/src/rist-retx-domain.h b/src/rist-retx-domain.h\ndeleted file mode 100644\nindex 1f43df1..0000000\n--- a/src/rist-retx-domain.h\n+++ /dev/null\n@@ -1,35 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Retransmission sequence-domain predicate.  An Advanced-context sender\n- * indexes its retransmit buffer by the 32-bit advanced sequence, but a\n- * peer that has negotiated down to Main (TR-06-3 §9: an Advanced device\n- * starts each peer in Main and only frames Advanced once the peer\n- * advertises I=1) requests retransmits with a 16-bit RTP sequence\n- * (nack_seq_msb = 0).  Those two sequence spaces are independent\n- * counters, so a Main peer's NACK never resolves against the 32-bit\n- * index and its losses are never retransmitted.  This predicate decides,\n- * per outgoing retransmission, whether to resolve/verify/frame it in the\n- * 16-bit RTP domain instead.  Kept standalone so it can be unit-tested\n- * without constructing peers or starting threads.\n- */\n-\n-#ifndef RIST_RETX_DOMAIN_H\n-#define RIST_RETX_DOMAIN_H\n-\n-#include <stdbool.h>\n-\n-#include \"librist/headers.h\" /* enum rist_profile */\n-\n-/* True when a retransmission for a peer must be resolved in the 16-bit\n- * RTP sequence domain rather than the 32-bit advanced domain: only an\n- * Advanced-context sender serving a peer that has NOT negotiated up to\n- * Advanced.  Every other case (Advanced<->Advanced, or a Main/Simple\n- * context whose primary index is already the RTP domain) returns false,\n- * so the established paths are unchanged. */\n-static inline bool rist_retx_use_rtp_domain(enum rist_profile ctx_profile,\n-                                            bool remote_supports_advanced)\n-{\n-\treturn ctx_profile == RIST_PROFILE_ADVANCED && !remote_supports_advanced;\n-}\n-\n-#endif /* RIST_RETX_DOMAIN_H */\ndiff --git a/src/rist-rtt-mute.h b/src/rist-rtt-mute.h\ndeleted file mode 100644\nindex 7806c32..0000000\n--- a/src/rist-rtt-mute.h\n+++ /dev/null\n@@ -1,132 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * RTT-based bonded-leg muting hysteresis state machine.  Kept in a small\n- * standalone header so the transition logic can be unit-tested in isolation,\n- * without constructing full peer objects or starting sender threads.\n- */\n-\n-#ifndef RIST_RTT_MUTE_H\n-#define RIST_RTT_MUTE_H\n-\n-#include <stdbool.h>\n-#include <stdint.h>\n-\n-enum rist_rtt_mute_action\n-{\n-\tRIST_RTT_MUTE_NONE = 0,    /* no state change this step */\n-\tRIST_RTT_MUTE_DROP = 1,    /* transition active -> muted */\n-\tRIST_RTT_MUTE_RESTORE = 2, /* transition muted -> active */\n-};\n-\n-struct rist_rtt_mute_state\n-{\n-\tbool muted;             /* leg currently out of the unique-payload rotation */\n-\tbool pending;           /* a transition condition is currently held */\n-\tuint64_t pending_since; /* tick the pending condition first held */\n-};\n-\n-/* Advance the hysteresis machine for one leg by one sample.\n- *\n- * All RTT/time arguments share one unit (RIST_CLOCK ticks):\n- *   smoothed_rtt  : the leg's smoothed (EWMA) round-trip time\n- *   drop          : ceiling; staying above it for drop_settle mutes the leg\n- *   restore       : low-water; staying below it for restore_settle restores it\n- *   drop_settle   : dwell required before muting (active -> muted)\n- *   restore_settle: dwell required before rejoining (muted -> active)\n- *   now           : monotonic tick\n- *\n- * The dwell is deliberately asymmetric (drop_settle short, restore_settle\n- * longer) so a bad leg is pulled quickly but a still-marginal one does not flap\n- * back in; the threshold split (restore < drop) is the spatial half of the\n- * hysteresis. A drop of 0 disables muting. Returns the transition taken this\n- * step (NONE if unchanged) and updates *st in place. */\n-static inline enum rist_rtt_mute_action\n-rist_rtt_mute_step(struct rist_rtt_mute_state *st, uint64_t smoothed_rtt,\n-                   uint64_t drop, uint64_t restore,\n-                   uint64_t drop_settle, uint64_t restore_settle, uint64_t now)\n-{\n-\tif (drop == 0) {\n-\t\tst->pending = false;\n-\t\tif (st->muted) {\n-\t\t\tst->muted = false;\n-\t\t\treturn RIST_RTT_MUTE_RESTORE;\n-\t\t}\n-\t\treturn RIST_RTT_MUTE_NONE;\n-\t}\n-\n-\tbool condition = st->muted ? (smoothed_rtt < restore)\n-\t                           : (smoothed_rtt > drop);\n-\tif (!condition) {\n-\t\tst->pending = false;\n-\t\treturn RIST_RTT_MUTE_NONE;\n-\t}\n-\tif (!st->pending) {\n-\t\tst->pending = true;\n-\t\tst->pending_since = now;\n-\t\treturn RIST_RTT_MUTE_NONE;\n-\t}\n-\tuint64_t settle = st->muted ? restore_settle : drop_settle;\n-\tif (now - st->pending_since < settle)\n-\t\treturn RIST_RTT_MUTE_NONE;\n-\n-\tst->pending = false;\n-\tif (st->muted) {\n-\t\tst->muted = false;\n-\t\treturn RIST_RTT_MUTE_RESTORE;\n-\t}\n-\tst->muted = true;\n-\treturn RIST_RTT_MUTE_DROP;\n-}\n-\n-/* Should the sole-carrier role move from the incumbent to a challenger?\n- *\n- * When every leg wants muting, one has to keep carrying. Choosing the leg with\n- * the lowest RTT each tick sounds right but ping-pongs the payload between two\n- * equally bad legs, which is worse for the stream than committing to either, so\n- * hand over only when the challenger measures margin times better and the\n- * incumbent has served at least held_min. */\n-static inline bool\n-rist_rtt_sole_carrier_handover(uint64_t incumbent_rtt, uint64_t challenger_rtt,\n-                               uint64_t held_for, uint64_t held_min, unsigned margin)\n-{\n-\tif (held_for < held_min)\n-\t\treturn false;\n-\treturn challenger_rtt * margin <= incumbent_rtt;\n-}\n-\n-/* Is a trickle on a muted leg still worth sending?\n- *\n- * The trickle keeps a muted leg carrying a little payload so a restore starts\n- * warm. Once the leg is queued deeper than the receiver's buffer, nothing sent\n- * on it can arrive in time to be output, and the arrivals do harm: they are old\n- * enough to be the only candidate the receiver has to rebuild its baseline on\n- * after a reset, which then rewinds it. Judge by the one-way delay, half the\n- * round trip. A buffer of 0 means unknown, so keep trickling. */\n-static inline bool\n-rist_rtt_trickle_useful(uint64_t smoothed_rtt, uint64_t buffer)\n-{\n-\tif (!buffer)\n-\t\treturn true;\n-\treturn (smoothed_rtt / 2) <= buffer;\n-}\n-\n-/* Share of the configured weight a rejoining leg should carry, ramped linearly\n- * to full over ramp ticks since it was restored.\n- *\n- * A muted leg drains, so it measures well right up until it carries traffic\n- * again; handing back the full share at once refloods the queue and mutes it\n- * moments later. Ramping lets the leg reveal congestion under a rising load.\n- * ramp_start of 0 (never muted) or an elapsed ramp both yield full weight, and\n- * a ramping leg always keeps at least 1 so it never falls out entirely. */\n-static inline uint32_t\n-rist_rtt_ramped_weight(uint32_t weight, uint64_t ramp_start, uint64_t ramp, uint64_t now)\n-{\n-\tif (!ramp_start || !ramp || weight <= 1)\n-\t\treturn weight;\n-\tif (now <= ramp_start || (now - ramp_start) >= ramp)\n-\t\treturn weight;\n-\tuint32_t scaled = (uint32_t)((uint64_t)weight * (now - ramp_start) / ramp);\n-\treturn scaled ? scaled : 1;\n-}\n-\n-#endif /* RIST_RTT_MUTE_H */\ndiff --git a/src/rist-send-grace.h b/src/rist-send-grace.h\ndeleted file mode 100644\nindex acad56c..0000000\n--- a/src/rist-send-grace.h\n+++ /dev/null\n@@ -1,35 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Whether a peer that has gone silent may still be sent payload.  Kept in a\n- * small standalone header so the decision can be unit-tested without a peer,\n- * a socket or a sender thread.\n- */\n-\n-#ifndef RIST_SEND_GRACE_H\n-#define RIST_SEND_GRACE_H\n-\n-#include <stdbool.h>\n-#include <stdint.h>\n-\n-/* Declaring a peer dead means its return path went quiet, which says nothing\n- * about whether the forward path still works. Cutting a single-path stream the\n- * moment an RTCP reply is late strands a receiver that is otherwise fine, so a\n- * dead peer keeps its place in the send rotation for one recovery buffer window\n- * and is only dropped once that runs out. A bonded leg never reaches this: the\n- * stall check pulls it from the rotation far sooner.\n- *\n- * dead_since is when the peer was declared dead, grace is the window, and both\n- * share one unit with now (RIST_CLOCK ticks). A grace of 0 drops the peer as\n- * soon as it dies. A now behind dead_since keeps sending, which is the safe\n- * reading of a clock that moved backwards. */\n-static inline bool\n-rist_peer_may_send(bool dead, uint64_t dead_since, uint64_t grace, uint64_t now)\n-{\n-\tif (!dead)\n-\t\treturn true;\n-\tif (now < dead_since)\n-\t\treturn true;\n-\treturn (now - dead_since) <= grace;\n-}\n-\n-#endif /* RIST_SEND_GRACE_H */\ndiff --git a/src/rist.c b/src/rist.c\nindex 41bc4e8..0f32938 100644\n--- a/src/rist.c\n+++ b/src/rist.c\n@@ -7,7 +7,6 @@\n  * SPDX-License-Identifier: BSD-2-Clause\n  */\n \n-#define LIBRIST_INTERNAL 1\n #include \"rist-private.h\"\n #include \"log-private.h\"\n #include \"udp-private.h\"\n@@ -45,7 +44,8 @@ int rist_receiver_create(struct rist_ctx **_ctx, enum rist_profile profile,\n \t}\n \tif (profile == RIST_PROFILE_ADVANCED)\n \t{\n-\t\trist_log_priv2(logging_settings, RIST_LOG_INFO, \"Using Advanced profile (VSF TR-06-3)\\n\");\n+\t\trist_log_priv2(logging_settings, RIST_LOG_WARN, \"Advanced profile not implemented yet, using main profile instead\\n\");\n+\t\tprofile = RIST_PROFILE_MAIN;\n \t}\n \tstruct rist_receiver *ctx = calloc(1, sizeof(*ctx));\n \tif (!ctx)\n@@ -124,13 +124,12 @@ int rist_receiver_nack_type_set(struct rist_ctx *rist_ctx, enum rist_nack_type n\n \treturn 0;\n }\n \n-/* Find the flow with the most queued data.  Caller MUST hold\n- * ctx->common.flows_lock on entry; the lock remains held on return so\n- * the returned pointer stays valid until the caller releases it. */\n-static struct rist_flow *rist_get_longest_flow_locked(struct rist_receiver *ctx, ssize_t *num)\n+static struct rist_flow *rist_get_longest_flow(struct rist_receiver *ctx, ssize_t *num)\n {\n+\t// Select the flow with highest queue count\n \tssize_t num_loop = 0;\n \tstruct rist_flow *f = NULL;\n+\tpthread_mutex_lock(&ctx->common.flows_lock);\n \tstruct rist_flow *f_loop = ctx->common.FLOWS;\n \twhile (f_loop) {\n \t\tstruct rist_flow *nextflow = f_loop->next;\n@@ -145,6 +144,7 @@ static struct rist_flow *rist_get_longest_flow_locked(struct rist_receiver *ctx,\n \t\t}\n \t\tf_loop = nextflow;\n \t}\n+\tpthread_mutex_unlock(&ctx->common.flows_lock);\n \treturn f;\n }\n \n@@ -169,36 +169,30 @@ int rist_receiver_data_read2(struct rist_ctx *rist_ctx, struct rist_data_block *\n \tstruct rist_receiver *ctx = rist_ctx->receiver_ctx;\n \n \tstruct rist_data_block *data_block = NULL;\n+\t/* We could enter the lock now, to read the counter. However performance penalties apply.\n+\t   The risks for not entering the lock are either sleeping too much (a packet gets added while we read)\n+\t   or not at all when we should (i.e.: the calling application is reading from multiple threads). Both\n+\t   risks are tolerable */\n \n \tssize_t num = 0;\n-\t/* Hold flows_lock across the flow lookup and data read to prevent\n-\t * rist_delete_flow from freeing the flow underneath us. */\n-\tpthread_mutex_lock(&ctx->common.flows_lock);\n-\tstruct rist_flow *f = rist_get_longest_flow_locked(ctx, &num);\n+\t// Select the flow with highest queue count to minimize jitter for calling app\n+\tstruct rist_flow *f = rist_get_longest_flow(ctx, &num);\n \tif (!num && timeout > 0)\n \t{\n-\t\tpthread_mutex_unlock(&ctx->common.flows_lock);\n \t\tpthread_mutex_lock(&(ctx->mutex));\n \t\tpthread_cond_timedwait_ms(&(ctx->condition), &(ctx->mutex), timeout);\n \t\tpthread_mutex_unlock(&(ctx->mutex));\n-\t\tnum = 0;\n-\t\tpthread_mutex_lock(&ctx->common.flows_lock);\n-\t\tf = rist_get_longest_flow_locked(ctx, &num);\n+\t\tf = rist_get_longest_flow(ctx, &num);\n \t}\n \n \tif (RIST_UNLIKELY(!num || !f))\n \t{\n-\t\tpthread_mutex_unlock(&ctx->common.flows_lock);\n+\t\t//No need to log, these can be triggered by gaps in data or low bitrate stream with low timeout values\n+\t\t//rist_log_priv3(RIST_LOG_ERROR, \"rist_receiver_data_read call with no flow data, %d/%\"PRIu32\"\\n\", num, f);\n \t\treturn 0;\n \t}\n \n-\t/* Lock the flow's own mutex while still holding flows_lock, then\n-\t * release flows_lock.  This guarantees the flow pointer is valid\n-\t * when we dereference it: rist_delete_flow takes flows_lock before\n-\t * unlinking and freeing the flow. */\n \tpthread_mutex_lock(&f->mutex);\n-\tpthread_mutex_unlock(&ctx->common.flows_lock);\n-\n \tunsigned long dataout_read_index = atomic_load_explicit(&f->dataout_fifo_queue_read_index, memory_order_relaxed);\n \tsize_t write_index = atomic_load_explicit(&f->dataout_fifo_queue_write_index, memory_order_acquire);\n \tif (write_index != dataout_read_index)\n@@ -213,9 +207,7 @@ int rist_receiver_data_read2(struct rist_ctx *rist_ctx, struct rist_data_block *\n \t\t\t}\n \t\t} while (num > 0);\n \t}\n-\tbool overflow = atomic_load_explicit(&f->fifo_overflow, memory_order_relaxed);\n \tpthread_mutex_unlock(&f->mutex);\n-\n \tif (data_block == NULL && num > 0)\n \t{\n \t\trist_log_priv3(RIST_LOG_ERROR, \"[rist_receiver_data_read2] data_block is NULL but num > 0 ! num=%zd, dataout_read_index=%lu, write_index=%zu\\n\", num, dataout_read_index, write_index);\n@@ -224,7 +216,7 @@ int rist_receiver_data_read2(struct rist_ctx *rist_ctx, struct rist_data_block *\n \n \t*data_buffer = data_block;\n \n-\tif (RIST_UNLIKELY(overflow))\n+\tif (RIST_UNLIKELY(atomic_load_explicit(&f->fifo_overflow, memory_order_relaxed) == true))\n \t\tdata_block->flags |= RIST_DATA_FLAGS_OVERFLOW;\n \n \treturn (int)num;\n@@ -237,13 +229,7 @@ void rist_receiver_data_block_free(struct rist_data_block **const block)\n \n void rist_receiver_data_block_free2(struct rist_data_block **block)\n {\n-\t/* free(NULL)-style no-op: *block may already be NULL on the data_fd\n-\t * delivery path (free_data_block nulls it). Mirror its guard. */\n-\tif (block == NULL)\n-\t\treturn;\n \tstruct rist_data_block *b = *block;\n-\tif (b == NULL)\n-\t\treturn;\n \tif (b->ref != NULL)\n \t\tfree_data_block(block);\n }\n@@ -343,27 +329,6 @@ int rist_receiver_session_timeout_callback_set(struct rist_ctx *rist_ctx,\n   return 0;\n }\n \n-int rist_receiver_flow_attr_callback_set(struct rist_ctx *rist_ctx,\n-                                         receiver_flow_attr_callback_t cb,\n-                                         void *arg)\n-{\n-  if (RIST_UNLIKELY(!rist_ctx)) {\n-    rist_log_priv3(RIST_LOG_ERROR,\n-                   \"ctx is null on rist_receiver_flow_attr_callback_set call!\\n\");\n-    return -1;\n-  }\n-  if (RIST_UNLIKELY(rist_ctx->mode != RIST_RECEIVER_MODE ||\n-                    !rist_ctx->receiver_ctx)) {\n-    rist_log_priv3(RIST_LOG_ERROR, \"rist_receiver_flow_attr_callback_set call with \"\n-                                   \"CTX not set up for receiving\\n\");\n-    return -1;\n-  }\n-  struct rist_receiver *ctx = rist_ctx->receiver_ctx;\n-  ctx->receiver_flow_attr_callback = cb;\n-  ctx->receiver_flow_attr_callback_argument = arg;\n-  return 0;\n-}\n-\n /* Sender functions */\n int rist_sender_create(struct rist_ctx **_ctx, enum rist_profile profile,\n \t\t\t\t\t   uint32_t flow_id, struct rist_logging_settings *logging_settings)\n@@ -372,7 +337,8 @@ int rist_sender_create(struct rist_ctx **_ctx, enum rist_profile profile,\n \n \tif (profile == RIST_PROFILE_ADVANCED)\n \t{\n-\t\trist_log_priv2(logging_settings, RIST_LOG_INFO, \"Using Advanced profile (VSF TR-06-3)\\n\");\n+\t\trist_log_priv2(logging_settings, RIST_LOG_WARN, \"Advanced profile not implemented yet, using main profile instead\\n\");\n+\t\tprofile = RIST_PROFILE_MAIN;\n \t}\n \n \tif (flow_id % 2 != 0)\n@@ -419,7 +385,7 @@ int rist_sender_create(struct rist_ctx **_ctx, enum rist_profile profile,\n \t\tif (RIST_UNLIKELY(!ctx->sender_retry_queue))\n \t\t{\n \t\t\trist_log_priv(&ctx->common, RIST_LOG_ERROR, \"Could not create sender retry buffer of size %u MB, OOM\\n\",\n-\t\t\t\t\t\t  (unsigned)(RIST_RETRY_QUEUE_BUFFERS * sizeof(ctx->sender_retry_queue[0])) / 1000000);\n+\t\t\t\t\t\t  (unsigned)(RIST_SERVER_QUEUE_BUFFERS * sizeof(ctx->sender_retry_queue[0])) / 1000000);\n \t\t\tret = -1;\n \t\t\tgoto free_ctx_and_ret;\n \t\t}\n@@ -430,33 +396,7 @@ int rist_sender_create(struct rist_ctx **_ctx, enum rist_profile profile,\n \t}\n \n \tctx->sender_queue_delete_index = 1;\n-\t/* Advanced uses the (runtime-tunable) configured capacity; Simple/Main\n-\t * keep the default ring. Heap-allocated so the size can be tuned via\n-\t * rist_recovery_depth_set() before rist_start(). */\n-\tctx->sender_queue_max = (ctx->common.profile == RIST_PROFILE_ADVANCED)\n-\t\t\t? ctx->common.recovery_queue_max : RIST_SERVER_QUEUE_BUFFERS;\n-\tctx->sender_queue = calloc(ctx->sender_queue_max, sizeof(*ctx->sender_queue));\n-\tctx->seq_index = calloc(ctx->sender_queue_max, sizeof(*ctx->seq_index));\n-\tif (RIST_UNLIKELY(!ctx->sender_queue || !ctx->seq_index))\n-\t{\n-\t\trist_log_priv(&ctx->common, RIST_LOG_ERROR,\n-\t\t\t\t\t  \"Could not allocate sender recovery ring (%zu entries), OOM\\n\",\n-\t\t\t\t\t  ctx->sender_queue_max);\n-\t\tret = -1;\n-\t\tgoto free_ctx_and_ret;\n-\t}\n-\t/* Advanced sender keeps a parallel 16-bit-RTP retransmit index so a\n-\t * peer that negotiated down to Main (which NACKs in the RTP domain) can\n-\t * still be served. Main/Simple senders index by RTP directly already. */\n-\tif (ctx->common.profile == RIST_PROFILE_ADVANCED) {\n-\t\tctx->seq_rtp_index = calloc((size_t)UINT16_MAX + 1, sizeof(*ctx->seq_rtp_index));\n-\t\tif (RIST_UNLIKELY(!ctx->seq_rtp_index)) {\n-\t\t\trist_log_priv(&ctx->common, RIST_LOG_ERROR,\n-\t\t\t\t\t\t  \"Could not allocate sender RTP recovery index, OOM\\n\");\n-\t\t\tret = -1;\n-\t\t\tgoto free_ctx_and_ret;\n-\t\t}\n-\t}\n+\tctx->sender_queue_max = RIST_SERVER_QUEUE_BUFFERS;\n \tatomic_init(&ctx->sender_queue_write_index, 1);\n \tatomic_init(&ctx->sender_queue_read_index, 0);\n \n@@ -510,12 +450,6 @@ int rist_sender_create(struct rist_ctx **_ctx, enum rist_profile profile,\n \n \t// Failed!\n free_ctx_and_ret:\n-\tif (ctx) {\n-\t\tfree(ctx->sender_retry_queue);\n-\t\tfree(ctx->sender_queue);\n-\t\tfree(ctx->seq_index);\n-\t\tfree(ctx->seq_rtp_index);\n-\t}\n \tfree(ctx);\n \tfree(rist_ctx);\n \treturn ret;\n@@ -637,82 +571,16 @@ int rist_sender_data_write(struct rist_ctx *rist_ctx, const struct rist_data_blo\n \t\treturn -1;\n \t}\n \n-\tif (RIST_UNLIKELY((data_block->flags & RIST_DATA_FLAGS_USE_SEQ) &&\n-\t                  ctx->split_mode != LIBRIST_SPLIT_MODE_OFF)) {\n-\t\trist_log_priv(&ctx->common, RIST_LOG_ERROR,\n-\t\t\t\"RIST_DATA_FLAGS_USE_SEQ cannot be used with split mode; \"\n-\t\t\t\"the library must control the wire sequence for pair \"\n-\t\t\t\"splitting. Use ts_ntp to preserve source timestamps.\\n\");\n-\t\treturn -1;\n-\t}\n-\n \tuint64_t ts_ntp = data_block->ts_ntp == 0 ? timestampNTP_u64() : data_block->ts_ntp;\n+\tuint32_t seq_rtp;\n+\tif (data_block->flags & RIST_DATA_FLAGS_USE_SEQ)\n+\t\tseq_rtp = (uint32_t)data_block->seq;\n+\telse\n+\t\tseq_rtp = ctx->common.seq_rtp++;\n+\t//When we support 32bit seq this should be changed\n+\tseq_rtp = seq_rtp & (UINT16_MAX);\n \n-\tconst uint8_t *payload_to_send = data_block->payload;\n-\tsize_t payload_len = data_block->payload_len;\n-\n-\tbool actually_split = (ctx->split_mode != LIBRIST_SPLIT_MODE_OFF);\n-\tsize_t first_len = 0;\n-\tsize_t last_len = 0;\n-\tif (actually_split) {\n-\t\tif (ctx->split_mode == LIBRIST_SPLIT_MODE_AUTO) {\n-\t\t\tconst uint8_t *p = payload_to_send;\n-\t\t\tif (payload_len >= 2 * 188 && payload_len % 188 == 0 && p[0] == 0x47) {\n-\t\t\t\tsize_t ts_count = payload_len / 188;\n-\t\t\t\tsize_t first_ts = ts_count / 2;\n-\t\t\t\tif (first_ts == 0)\n-\t\t\t\t\tfirst_ts = 1;\n-\t\t\t\tfirst_len = first_ts * 188;\n-\t\t\t\tlast_len = payload_len - first_len;\n-\t\t\t} else {\n-\t\t\t\tctx->stats_split_fallback_not_ts++;\n-\t\t\t\tfirst_len = payload_len / 2;\n-\t\t\t\tlast_len = payload_len - first_len;\n-\t\t\t}\n-\t\t} else {\n-\t\t\tfirst_len = payload_len / 2;\n-\t\t\tlast_len = payload_len - first_len;\n-\t\t}\n-\t}\n-\n-\tuint32_t seq_rtp_first;\n-\tuint32_t seq_rtp_last = 0;\n-\tif (data_block->flags & RIST_DATA_FLAGS_USE_SEQ) {\n-\t\tseq_rtp_first = (uint32_t)data_block->seq;\n-\t} else if (actually_split) {\n-\t\tif (ctx->common.seq_rtp & 1)\n-\t\t\tctx->common.seq_rtp++;\n-\t\tseq_rtp_first = ctx->common.seq_rtp++;\n-\t\tseq_rtp_last = ctx->common.seq_rtp++;\n-\t} else {\n-\t\tseq_rtp_first = ctx->common.seq_rtp++;\n-\t}\n-\tseq_rtp_first &= UINT16_MAX;\n-\tseq_rtp_last &= UINT16_MAX;\n-\n-\tint ret;\n-\tif (actually_split) {\n-\t\tret = rist_sender_enqueue(ctx, payload_to_send, first_len, ts_ntp,\n-\t\t                          data_block->virt_src_port,\n-\t\t                          data_block->virt_dst_port, seq_rtp_first);\n-\t\tif (ret == 0) {\n-\t\t\tret = rist_sender_enqueue(ctx, payload_to_send + first_len, last_len,\n-\t\t\t                          ts_ntp, data_block->virt_src_port,\n-\t\t\t                          data_block->virt_dst_port, seq_rtp_last);\n-\t\t\tif (ret == 0) {\n-\t\t\t\tctx->stats_pairs_emitted++;\n-\t\t\t} else {\n-\t\t\t\trist_log_priv(&ctx->common, RIST_LOG_WARN,\n-\t\t\t\t\t\"Split write: first half (seq %u) enqueued but second \"\n-\t\t\t\t\t\"half (seq %u) failed (ret=%d); receiver will see an \"\n-\t\t\t\t\t\"orphan.\\n\", seq_rtp_first, seq_rtp_last, ret);\n-\t\t\t}\n-\t\t}\n-\t} else {\n-\t\tret = rist_sender_enqueue(ctx, payload_to_send, payload_len, ts_ntp,\n-\t\t                          data_block->virt_src_port,\n-\t\t                          data_block->virt_dst_port, seq_rtp_first);\n-\t}\n+\tint ret = rist_sender_enqueue(ctx, data_block->payload, data_block->payload_len, ts_ntp, data_block->virt_src_port, data_block->virt_dst_port, seq_rtp);\n \t// Wake up data/nack output thread when data comes in\n \tif (pthread_cond_signal(&ctx->condition))\n \t\trist_log_priv(&ctx->common, RIST_LOG_ERROR, \"Call to pthread_cond_signal failed.\\n\");\n@@ -726,36 +594,18 @@ int rist_sender_data_write(struct rist_ctx *rist_ctx, const struct rist_data_blo\n /* Shared OOB functions -> Tunneled IP packets within GRE */\n int rist_oob_read(struct rist_ctx *ctx, const struct rist_oob_block **oob_block)\n {\n+\tRIST_MARK_UNUSED(oob_block);\n \tif (!ctx)\n \t{\n \t\trist_log_priv3(RIST_LOG_ERROR, \"ctx is null on rist_oob_read call!\\n\");\n \t\treturn -1;\n \t}\n-\tif (!oob_block)\n-\t{\n-\t\trist_log_priv3(RIST_LOG_ERROR, \"oob_block is null on rist_oob_read call!\\n\");\n-\t\treturn -1;\n-\t}\n \tstruct rist_common_ctx *cctx = rist_struct_get_common(ctx);\n \tif (!cctx)\n \t\treturn -1;\n \n-\t*oob_block = NULL;\n-\n-\tif (!cctx->oob_data_enabled)\n-\t{\n-\t\trist_log_priv(cctx, RIST_LOG_ERROR,\n-\t\t\t\t\"rist_oob_read called but oob data is not enabled; call rist_oob_callback_set first\\n\");\n-\t\treturn -1;\n-\t}\n-\tif (cctx->oob_data_callback)\n-\t{\n-\t\trist_log_priv(cctx, RIST_LOG_ERROR,\n-\t\t\t\t\"rist_oob_read cannot be used while an oob callback is installed\\n\");\n-\t\treturn -1;\n-\t}\n-\n-\treturn rist_oob_dequeue_rx(cctx, oob_block);\n+\trist_log_priv(cctx, RIST_LOG_ERROR, \"rist_receiver_oob_read not implemented!\\n\");\n+\treturn 0;\n }\n \n int rist_oob_write(struct rist_ctx *ctx, const struct rist_oob_block *oob_block)\n@@ -815,9 +665,6 @@ int rist_oob_callback_set(struct rist_ctx *ctx,\n \tcctx->oob_data_callback_argument = arg;\n \tcctx->oob_queue_write_index = 0;\n \tcctx->oob_queue_read_index = 0;\n-\tcctx->oob_rx_queue_write_index = 0;\n-\tcctx->oob_rx_queue_read_index = 0;\n-\tcctx->oob_rx_current = NULL;\n \n \treturn 0;\n }\n@@ -840,112 +687,6 @@ int rist_recovery_rtt_multiplier_set(struct rist_ctx *ctx, int multiplier)\n \treturn rist_recovery_rtt_multiplier_set_internal(cctx, multiplier);\n }\n \n-/* Translate a recovery-depth exponent into ring capacity in packets\n- * (65536 << depth), clamped to [MIN, MAX] and to the platform maximum\n- * (rist_recovery_depth_platform_max) so the result always fits size_t.\n- * Returned as uint64_t so the arithmetic is exact regardless of size_t width. */\n-static uint64_t rist_recovery_depth_to_packets(int depth)\n-{\n-\tif (depth < RIST_RECOVERY_DEPTH_MIN)\n-\t\tdepth = RIST_RECOVERY_DEPTH_MIN;\n-\tif (depth > RIST_RECOVERY_DEPTH_MAX)\n-\t\tdepth = RIST_RECOVERY_DEPTH_MAX;\n-\tint platform_max = rist_recovery_depth_platform_max();\n-\tif (depth > platform_max)\n-\t\tdepth = platform_max;\n-\treturn (uint64_t)UINT16_SIZE << depth;\n-}\n-\n-/* Resize the context recovery ring to `packets` (already a power of two in\n- * [UINT16_SIZE, 2^32]). The caller MUST guarantee rist_start() has not\n- * completed, so the sender ring can be reallocated with no data in flight.\n- * Returns 0 on success, -2 on OOM / size unrepresentable on this platform. */\n-static int rist_recovery_depth_apply(struct rist_ctx *ctx, struct rist_common_ctx *cctx, uint64_t packets)\n-{\n-\tif (packets > (uint64_t)(SIZE_MAX / (sizeof(struct rist_buffer *) + sizeof(uint32_t)))) {\n-\t\trist_log_priv(cctx, RIST_LOG_ERROR,\n-\t\t\t\"Recovery depth too large to allocate on this platform (%llu packets)\\n\",\n-\t\t\t(unsigned long long)packets);\n-\t\treturn -2;\n-\t}\n-\tsize_t want = (size_t)packets;\n-\n-\tif (cctx->profile != RIST_PROFILE_ADVANCED) {\n-\t\tcctx->recovery_queue_max = want;\n-\t\trist_log_priv(cctx, RIST_LOG_INFO,\n-\t\t\t\"Recovery depth request stored (%zu packets); only the Advanced \"\n-\t\t\t\"profile uses it (Simple/Main are 16-bit, capped at %u packets).\\n\",\n-\t\t\twant, (unsigned)UINT16_SIZE);\n-\t\treturn 0;\n-\t}\n-\n-\t/* The receiver allocates its ring per-flow and reads recovery_queue_max\n-\t * directly, so nothing more to do there. The sender allocates its ring\n-\t * at create time, so resize it now - safe because rist_start() has not\n-\t * run yet and no data is flowing. On OOM the old ring (and\n-\t * recovery_queue_max) is left untouched. */\n-\tif (ctx->mode == RIST_SENDER_MODE && ctx->sender_ctx) {\n-\t\tstruct rist_sender *sender = ctx->sender_ctx;\n-\t\tif (sender->sender_queue_max != want) {\n-\t\t\tstruct rist_buffer **nq = calloc(want, sizeof(*nq));\n-\t\t\tuint32_t *ni = calloc(want, sizeof(*ni));\n-\t\t\tif (!nq || !ni) {\n-\t\t\t\tfree(nq);\n-\t\t\t\tfree(ni);\n-\t\t\t\trist_log_priv(cctx, RIST_LOG_ERROR,\n-\t\t\t\t\t\"OOM resizing sender recovery ring to %zu packets\\n\", want);\n-\t\t\t\treturn -2;\n-\t\t\t}\n-\t\t\tfree(sender->sender_queue);\n-\t\t\tfree(sender->seq_index);\n-\t\t\tsender->sender_queue = nq;\n-\t\t\tsender->seq_index = ni;\n-\t\t\tsender->sender_queue_max = want;\n-\t\t\tsender->sender_queue_delete_index = 1;\n-\t\t\tatomic_store_explicit(&sender->sender_queue_write_index, 1, memory_order_release);\n-\t\t\tatomic_store_explicit(&sender->sender_queue_read_index, 0, memory_order_release);\n-\t\t}\n-\t}\n-\n-\tcctx->recovery_queue_max = want;\n-\trist_log_priv(cctx, RIST_LOG_INFO,\n-\t\t\"Advanced recovery buffer set to %zu packets (NACK window ~%zu packets, \"\n-\t\t\"~%zu MB of index arrays per context)\\n\",\n-\t\twant, want / 2,\n-\t\t(want * (sizeof(struct rist_buffer *) + sizeof(uint32_t))) / (1024 * 1024));\n-\treturn 0;\n-}\n-\n-int rist_recovery_depth_set(struct rist_ctx *ctx, uint8_t depth)\n-{\n-\tstruct rist_common_ctx *cctx = rist_struct_get_common(ctx);\n-\tif (!cctx)\n-\t\treturn -1;\n-\n-\tif (atomic_load_explicit(&cctx->startup_complete, memory_order_acquire)) {\n-\t\trist_log_priv(cctx, RIST_LOG_ERROR,\n-\t\t\t\"rist_recovery_depth_set must be called before rist_start()\\n\");\n-\t\treturn -1;\n-\t}\n-\n-\tif (depth > RIST_RECOVERY_DEPTH_MAX) {\n-\t\trist_log_priv(cctx, RIST_LOG_WARN,\n-\t\t\t\"recovery depth %u out of range, clamping to %u\\n\",\n-\t\t\t(unsigned)depth, (unsigned)RIST_RECOVERY_DEPTH_MAX);\n-\t\tdepth = RIST_RECOVERY_DEPTH_MAX;\n-\t}\n-\n-\tint platform_max = rist_recovery_depth_platform_max();\n-\tif (depth > platform_max) {\n-\t\trist_log_priv(cctx, RIST_LOG_WARN,\n-\t\t\t\"recovery depth %u exceeds what this platform can address, \"\n-\t\t\t\"capping to %d\\n\", (unsigned)depth, platform_max);\n-\t\tdepth = platform_max;\n-\t}\n-\n-\treturn rist_recovery_depth_apply(ctx, cctx, rist_recovery_depth_to_packets(depth));\n-}\n-\n int rist_auth_handler_set(struct rist_ctx *ctx,\n \t\t\t\t\t\t  int (*conn_cb)(void *arg, const char *connecting_ip, uint16_t connecting_port, const char *local_ip, uint16_t local_port, struct rist_peer *peer),\n \t\t\t\t\t\t  int (*disconn_cb)(void *arg, struct rist_peer *peer),\n@@ -975,17 +716,15 @@ int rist_stats_free(const struct rist_stats *stats_container)\n \n uint32_t rist_peer_get_id(const struct rist_peer *peer)\n {\n-    if (!peer)\n-        return 0;\n     return peer->adv_peer_id;\n }\n \n uint32_t rist_peer_get_cname(const struct rist_peer *peer, const char **cname)\n {\n-\tif (peer && cname)\n+\tif (peer)\n \t{\n \t\t*cname = &peer->cname[0];\n-\t\treturn (uint32_t)strnlen(*cname, sizeof(peer->cname));\n+\t\treturn (uint32_t)strlen(*cname);\n \t}\n \telse\n \t\treturn 0;\n@@ -1010,7 +749,7 @@ int rist_peer_update_secret(struct rist_peer *peer, const char* password) {\n \tpthread_mutex_lock(&peer->peer_lock);\n \tsize_t password_len = strlen(password);\n \tstruct rist_key *inactive_key = (peer->key_rx_odd_active) ? &peer->key_tx : &peer->key_tx_odd;\n-\trist_log_priv(get_cctx(peer), RIST_LOG_INFO, \"Updating passphrase for peer #%u\\n\", peer->adv_peer_id);\n+\trist_log_priv(get_cctx(peer), RIST_LOG_INFO, \"Updating passphrase to %s\\n\", password);\n \t_librist_crypto_psk_set_passphrase(inactive_key, (const uint8_t *)password, password_len);\n \tstruct rist_peer *child = peer->child;\n \twhile (child != NULL) {\n@@ -1037,7 +776,6 @@ int rist_logging_settings_free(const struct rist_logging_settings **logging_sett\n int rist_logging_settings_free2(struct rist_logging_settings **logging_settings)\n {\n \tif (*logging_settings) {\n-\t\trist_logging_unset_global_if_matches(*logging_settings);\n \t\tfree((void *)*logging_settings);\n \t\t*logging_settings = NULL;\n \t}\n@@ -1084,9 +822,9 @@ int rist_stats_callback_set(struct rist_ctx *ctx, int statsinterval, int (*stats\n \t\treturn -1;\n \t}\n \tstruct rist_common_ctx *cctx = rist_struct_get_common(ctx);\n+\tpthread_mutex_lock(&cctx->stats_lock);\n \tif (RIST_UNLIKELY(!cctx))\n \t\treturn -1;\n-\tpthread_mutex_lock(&cctx->stats_lock);\n \n \tif (statsinterval != 0)\n \t{\n@@ -1102,11 +840,6 @@ int rist_stats_callback_set(struct rist_ctx *ctx, int statsinterval, int (*stats\n \t\t\t\tf = f->next;\n \t\t\t}\n \t\t}\n-\t\telse if (ctx->mode == RIST_SENDER_MODE && ctx->sender_ctx)\n-\t\t{\n-\t\t\t/* sender loop reads its own copy, not the common one */\n-\t\t\tctx->sender_ctx->stats_report_time = statsinterval * RIST_CLOCK;\n-\t\t}\n \t}\n \tpthread_mutex_unlock(&cctx->stats_lock);\n \treturn 0;\n@@ -1136,8 +869,6 @@ int rist_parse_udp_address2(const char *url, struct rist_udp_config **udp_config\n \t{\n \t\t// Default options on new struct (specific for udp url)\n \t\tstruct rist_udp_config *output_udp_config = calloc(1, sizeof(struct rist_udp_config));\n-\t\tif (!output_udp_config)\n-\t\t\treturn -1;\n \t\toutput_udp_config->version = RIST_UDP_CONFIG_VERSION;\n \t\toutput_udp_config->stream_id = 0; // Accept all on receiver, auto-generate on sender\n \t\tret = parse_url_udp_options(url, output_udp_config);\n@@ -1154,11 +885,11 @@ int rist_parse_udp_address2(const char *url, struct rist_udp_config **udp_config\n \treturn ret;\n }\n \n-int rist_peer_config_defaults_set_versioned(struct rist_peer_config *peer_config, int version)\n+int rist_peer_config_defaults_set(struct rist_peer_config *peer_config)\n {\n \tif (peer_config)\n \t{\n-\t\tpeer_config->version = version;\n+\t\tpeer_config->version = RIST_PEER_CONFIG_VERSION;\n \t\tpeer_config->virt_dst_port = RIST_DEFAULT_VIRT_DST_PORT;\n \t\tpeer_config->recovery_mode = RIST_DEFAULT_RECOVERY_MODE;\n \t\tpeer_config->recovery_maxbitrate = RIST_DEFAULT_RECOVERY_MAXBITRATE;\n@@ -1171,43 +902,12 @@ int rist_peer_config_defaults_set_versioned(struct rist_peer_config *peer_config\n \t\tpeer_config->congestion_control_mode = RIST_DEFAULT_CONGESTION_CONTROL_MODE;\n \t\tpeer_config->min_retries = RIST_DEFAULT_MIN_RETRIES;\n \t\tpeer_config->max_retries = RIST_DEFAULT_MAX_RETRIES;\n-\t\tif (version >= 1)\n-\t\t{\n-\t\t\tpeer_config->split_mode = LIBRIST_SPLIT_MODE_OFF;\n-\t\t\tpeer_config->merge_mode = LIBRIST_MERGE_MODE_OFF;\n-\t\t}\n-\t\tif (version >= 4)\n-\t\t{\n-\t\t\tpeer_config->profile = RIST_DEFAULT_PROFILE;\n-\t\t\tpeer_config->profile_set = 0;\n-\t\t}\n-\t\tif (version >= 5)\n-\t\t{\n-\t\t\tpeer_config->recovery_priority = RIST_DEFAULT_RECOVERY_PRIORITY;\n-\t\t\tpeer_config->recovery_depth = RIST_RECOVERY_DEPTH_DEFAULT;\n-\t\t}\n-\t\tif (version >= 6)\n-\t\t{\n-\t\t\tpeer_config->rtt_drop = RIST_DEFAULT_RTT_DROP;\n-\t\t\tpeer_config->rtt_restore = RIST_DEFAULT_RTT_RESTORE;\n-\t\t\tpeer_config->rtt_drop_settle = RIST_DEFAULT_RTT_DROP_SETTLE;\n-\t\t\tpeer_config->rtt_drop_trickle = RIST_DEFAULT_RTT_DROP_TRICKLE;\n-\t\t}\n \t\treturn 0;\n \t}\n \telse\n \t\treturn -1;\n }\n \n-/* Legacy API wrapper symbol exported for ABI compatibility.\n- * Since pre-existing compiled binaries calling this function do not pass their\n- * struct size or compile-time version, we assume version 0 (baseline layout)\n- * to guarantee that we never write out-of-bounds on legacy client structures. */\n-int rist_peer_config_defaults_set(struct rist_peer_config *peer_config)\n-{\n-\treturn rist_peer_config_defaults_set_versioned(peer_config, 0);\n-}\n-\n int rist_parse_address(const char *url, const struct rist_peer_config **peer_config)\n {\n \treturn rist_parse_address2(url, (struct rist_peer_config **)peer_config);\n@@ -1215,15 +915,14 @@ int rist_parse_address(const char *url, const struct rist_peer_config **peer_con\n \n int rist_parse_address2(const char *url, struct rist_peer_config **peer_config)\n {\n-\tif (!url)\n-\t\treturn -1;\n+\n \tchar * url_local = strdup(url);\n \tint ret = 0;\n \tif (*peer_config == NULL)\n \t{\n \t\t// Default options on new struct (rist url)\n \t\tstruct rist_peer_config *output_peer_config = calloc(1, sizeof(struct rist_peer_config));\n-\t\trist_peer_config_defaults_set_versioned(output_peer_config, RIST_PEER_CONFIG_VERSION);\n+\t\trist_peer_config_defaults_set(output_peer_config);\n \t\tret = parse_url_options(url_local, output_peer_config);\n \t\t*peer_config = output_peer_config;\n \t}\n@@ -1249,17 +948,6 @@ static int rist_receiver_peer_create(struct rist_receiver *ctx,\n \tif (!p)\n \t\treturn -1;\n \n-\tif (config->version >= 1) {\n-\t\tif (ctx->common.PEERS == NULL) {\n-\t\t\tctx->merge_mode = config->merge_mode;\n-\t\t} else if (ctx->merge_mode != config->merge_mode) {\n-\t\t\trist_log_priv(&ctx->common, RIST_LOG_WARN,\n-\t\t\t\t\"peer added with merge_mode=%u but the receiver is already \"\n-\t\t\t\t\"running with merge_mode=%u; new peer's merge config ignored\\n\",\n-\t\t\t\tconfig->merge_mode, ctx->merge_mode);\n-\t\t}\n-\t}\n-\n \tp->peer_ssrc = prand_u32();\n \tif (ctx->common.profile == RIST_PROFILE_SIMPLE)\n \t{\n@@ -1271,7 +959,7 @@ static int rist_receiver_peer_create(struct rist_receiver *ctx,\n \t\t\treturn -1;\n \t\t}\n \n-\t\tsnprintf((char *)config->address, sizeof(config->address), \"%s:%d\", p->url, p->local_port + 1);\n+\t\tsprintf((char *)config->address, \"%s:%d\", p->url, p->local_port + 1);\n \t\tp_rtcp = rist_receiver_peer_insert_local(ctx, config);\n \t\tp_rtcp->peer_ssrc = p->peer_ssrc;\n \t\tif (!p_rtcp)\n@@ -1313,27 +1001,10 @@ static int rist_sender_peer_create(struct rist_sender *ctx,\n \tif (!newpeer)\n \t\treturn -1;\n \n-\tif (config->version >= 1) {\n-\t\tif (ctx->peer_lst_len == 0) {\n-\t\t\tctx->split_mode = config->split_mode;\n-\t\t} else if (ctx->split_mode != config->split_mode) {\n-\t\t\trist_log_priv(&ctx->common, RIST_LOG_WARN,\n-\t\t\t\t\"peer added with split_mode=%u but the session is already \"\n-\t\t\t\t\"running with split_mode=%u; new peer's split config ignored\\n\",\n-\t\t\t\tconfig->split_mode, ctx->split_mode);\n-\t\t}\n-\t\tif (ctx->split_mode == LIBRIST_SPLIT_MODE_HALF) {\n-\t\t\trist_log_priv(&ctx->common, RIST_LOG_WARN,\n-\t\t\t\t\"split=half is active; the receiver MUST be configured with \"\n-\t\t\t\t\"merge=pairs or merge=auto, otherwise downstream consumers \"\n-\t\t\t\t\"will see runt payloads.\\n\");\n-\t\t}\n-\t}\n-\n \t// TODO: Validate config data (virt_dst_port != 0 for example)\n \n \tnewpeer->is_data = true;\n-\tif (config->weight != RIST_PEER_WEIGHT_DUPLICATE)\n+\tif (config->weight > 0)\n \t\tnewpeer->w_count = config->weight;\n \tpeer_append(newpeer);\n \n@@ -1342,7 +1013,8 @@ static int rist_sender_peer_create(struct rist_sender *ctx,\n \t\tstruct rist_peer *peer_rtcp = rist_sender_peer_insert_local(ctx, config, true);\n \t\tif (!peer_rtcp)\n \t\t{\n-\t\t\trist_peer_remove(&ctx->common, newpeer, NULL);\n+\t\t\t// TODO: remove from peerlist (create sender_delete peer function)\n+\t\t\tfree(newpeer);\n \t\t\treturn -1;\n \t\t}\n \t\tpeer_rtcp->peer_data = newpeer;\n@@ -1379,100 +1051,6 @@ static int rist_sender_peer_create(struct rist_sender *ctx,\n \treturn 0;\n }\n \n-/* If config carries a ?profile= request (version >= 4 && profile_set),\n- * either apply it to the context (if unlocked and different) or refuse\n- * the peer (if locked and different).  Caller must hold peerlist_lock.\n- * Returns 0 on success/no-op, -1 on conflict (caller must abort). */\n-static int apply_url_profile_override(struct rist_common_ctx *cctx,\n-                                      const struct rist_peer_config *config)\n-{\n-\tif (config->version < 4 || !config->profile_set)\n-\t\treturn 0;\n-\tif (config->profile == cctx->profile)\n-\t\treturn 0;\n-\tif (atomic_load_explicit(&cctx->profile_locked, memory_order_acquire)) {\n-\t\trist_log_priv(cctx, RIST_LOG_WARN,\n-\t\t\t\"?profile=%d in URL refused: context profile is locked to %d \"\n-\t\t\t\"(rist_start has run or a prior peer fixed the profile).\\n\",\n-\t\t\t(int)config->profile, (int)cctx->profile);\n-\t\treturn -1;\n-\t}\n-\tenum rist_profile old = cctx->profile;\n-\tcctx->profile = config->profile;\n-\tif (config->profile == RIST_PROFILE_ADVANCED && old != RIST_PROFILE_ADVANCED)\n-\t\tinit_advanced_state(cctx);\n-\trist_log_priv(cctx, RIST_LOG_INFO,\n-\t\t\"Context profile changed from %d to %d by ?profile= URL parameter\\n\",\n-\t\t(int)old, (int)config->profile);\n-\treturn 0;\n-}\n-\n-/* Latch the context-wide CBR output setting; a second request for a different\n- * value is refused rather than resolved. Applied after rist_start() too, since\n- * flows copy the value as they are created. Caller must hold peerlist_lock.\n- * Returns 0 on success or no-op, -1 on conflict. */\n-static int set_cbr_output_locked(struct rist_common_ctx *cctx, bool want,\n-                                 const char *origin)\n-{\n-\tif (cctx->cbr_output_set) {\n-\t\tif (want == cctx->cbr_output)\n-\t\t\treturn 0;\n-\t\trist_log_priv(cctx, RIST_LOG_ERROR,\n-\t\t\t\"CBR output pacing %d from %s refused: already set to %d, and a \"\n-\t\t\t\"context's outputs share one release schedule.\\n\",\n-\t\t\twant ? 1 : 0, origin, cctx->cbr_output ? 1 : 0);\n-\t\treturn -1;\n-\t}\n-\n-\t/* Latch even on a value equal to the default, else 0 then 1 both succeed. */\n-\tcctx->cbr_output_set = true;\n-\tif (want == cctx->cbr_output)\n-\t\treturn 0;\n-\tcctx->cbr_output = want;\n-\trist_log_priv(cctx, RIST_LOG_INFO,\n-\t\t\"CBR output pacing %s by %s\\n\", want ? \"enabled\" : \"disabled\", origin);\n-\treturn 0;\n-}\n-\n-int rist_receiver_set_cbr_output(struct rist_ctx *rist_ctx, bool enable)\n-{\n-\tif (RIST_UNLIKELY(!rist_ctx)) {\n-\t\trist_log_priv3(RIST_LOG_ERROR,\n-\t\t\t\"ctx is null on rist_receiver_set_cbr_output call!\\n\");\n-\t\treturn -1;\n-\t}\n-\tif (RIST_UNLIKELY(rist_ctx->mode != RIST_RECEIVER_MODE ||\n-\t                  !rist_ctx->receiver_ctx)) {\n-\t\trist_log_priv3(RIST_LOG_ERROR,\n-\t\t\t\"rist_receiver_set_cbr_output: CTX not set up for receiving\\n\");\n-\t\treturn -1;\n-\t}\n-\tstruct rist_common_ctx *cctx = &rist_ctx->receiver_ctx->common;\n-\tpthread_mutex_lock(&cctx->peerlist_lock);\n-\tint ret = set_cbr_output_locked(cctx, enable, \"rist_receiver_set_cbr_output()\");\n-\tpthread_mutex_unlock(&cctx->peerlist_lock);\n-\treturn ret;\n-}\n-\n-/* If config carries a ?recovery-depth= request (version >= 5 and not\n- * DEFAULT), apply it to the context. Best-effort: ignored with a warning if\n- * rist_start has run (the sender ring cannot be resized with data in flight)\n- * since it must be set up front. Caller must hold peerlist_lock. Runs after\n- * apply_url_profile_override so a combined ?profile=2&recovery-depth=5\n- * URL sees the Advanced profile already in effect. */\n-static void apply_url_recovery_depth(struct rist_common_ctx *cctx, struct rist_ctx *ctx,\n-                                     const struct rist_peer_config *config)\n-{\n-\tif (config->version < 5 || config->recovery_depth == RIST_RECOVERY_DEPTH_DEFAULT)\n-\t\treturn;\n-\tif (atomic_load_explicit(&cctx->startup_complete, memory_order_acquire)) {\n-\t\trist_log_priv(cctx, RIST_LOG_WARN,\n-\t\t\t\"?recovery-depth in URL ignored: it must be set before rist_start().\\n\");\n-\t\treturn;\n-\t}\n-\trist_recovery_depth_apply(ctx, cctx, rist_recovery_depth_to_packets(config->recovery_depth));\n-}\n-\n int rist_peer_create(struct rist_ctx *ctx, struct rist_peer **peer, const struct rist_peer_config *config) {\n \tif (!ctx) {\n \t\trist_log_priv3(RIST_LOG_ERROR, \"rist_peer_create call with null ctx\\n\");\n@@ -1483,33 +1061,21 @@ int rist_peer_create(struct rist_ctx *ctx, struct rist_peer **peer, const struct\n \tif (ctx->mode == RIST_RECEIVER_MODE && ctx->receiver_ctx) {\n \t\tcctx = &ctx->receiver_ctx->common;\n \t\tpthread_mutex_lock(&cctx->peerlist_lock);\n-\t\tif (apply_url_profile_override(cctx, config) < 0) {\n-\t\t\tpthread_mutex_unlock(&cctx->peerlist_lock);\n-\t\t\treturn -1;\n-\t\t}\n-\t\tapply_url_recovery_depth(cctx, ctx, config);\n \t\tret = rist_receiver_peer_create(ctx->receiver_ctx, peer, config);\n \t}\n \telse if (ctx->mode == RIST_SENDER_MODE && ctx->sender_ctx) {\n \t\tcctx = &ctx->sender_ctx->common;\n \t\tpthread_mutex_lock(&cctx->peerlist_lock);\n-\t\tif (apply_url_profile_override(cctx, config) < 0) {\n-\t\t\tpthread_mutex_unlock(&cctx->peerlist_lock);\n-\t\t\treturn -1;\n-\t\t}\n-\t\tapply_url_recovery_depth(cctx, ctx, config);\n \t\tret  =rist_sender_peer_create(ctx->sender_ctx, peer, config);\n \t}\n \telse\n \t\treturn -1;\n-\tif (ret == 0)\n-\t\tatomic_store_explicit(&cctx->profile_locked, true, memory_order_release);\n \tpthread_mutex_unlock(&cctx->peerlist_lock);\n \treturn ret;\n }\n \n int rist_peer_get_socket(struct rist_peer *peer, int *socket, int *socket_extra) {\n-\tif (peer == NULL || socket == NULL)\n+\tif (socket == NULL)\n \t\treturn -1;\n \tif (peer->parent != NULL)\n \t\treturn -1;\n@@ -1591,7 +1157,7 @@ static PTHREAD_START_FUNC(sender_data_fd_read_loop, arg)\n \tconst size_t bufsize = ctx->data_fd_max_packet_size > 0 ? ctx->data_fd_max_packet_size : 1500;\n \tuint8_t *buf = malloc(bufsize);\n \tif (!buf)\n-\t\treturn 0;\n+\t\treturn NULL;\n \n \tconst bool is_tun = (ctx->data_fd_flags & RIST_DATA_FD_FLAG_TUN) != 0;\n \n@@ -1625,12 +1191,11 @@ static PTHREAD_START_FUNC(sender_data_fd_read_loop, arg)\n \t}\n \n \tfree(buf);\n-\treturn 0;\n+\treturn NULL;\n }\n \n static int rist_sender_start(struct rist_sender *ctx)\n {\n-\tatomic_store_explicit(&ctx->common.profile_locked, true, memory_order_release);\n \tpthread_mutex_lock(&ctx->mutex);\n \tif (!ctx->protocol_running) {\n \t\tif (rist_thread_create(&ctx->common, &ctx->sender_thread, NULL, sender_pthread_protocol, (void *)ctx) != 0)\n@@ -1667,7 +1232,6 @@ unlock_failed:\n \n static int rist_receiver_start(struct rist_receiver *ctx)\n {\n-\tatomic_store_explicit(&ctx->common.profile_locked, true, memory_order_release);\n \tpthread_mutex_lock(&ctx->mutex);\n \tif (!ctx->protocol_running)\n \t{\n@@ -1831,11 +1395,6 @@ int rist_receiver_set_output_fifo_size(struct rist_ctx *ctx, uint32_t desired_si\n \t\trist_log_priv2(ctx->receiver_ctx->common.logging_settings, RIST_LOG_ERROR, \"rist_receiver_set_fifo_size must be called before starting\\n\");\n \t\treturn -3;\n \t}\n-\tif (desired_size == 0 || desired_size > 65536)\n-\t{\n-\t\trist_log_priv2(ctx->receiver_ctx->common.logging_settings, RIST_LOG_ERROR, \"Desired fifo size must be between 1 and 65536\\n\");\n-\t\treturn -4;\n-\t}\n \tif ((desired_size & (desired_size -1)) != 0)\n \t{\n \t\trist_log_priv2(ctx->receiver_ctx->common.logging_settings, RIST_LOG_ERROR, \"Desired fifo size must be a power of 2\\n\");\ndiff --git a/src/rist_pacer.c b/src/rist_pacer.c\ndeleted file mode 100644\nindex fc64084..0000000\n--- a/src/rist_pacer.c\n+++ /dev/null\n@@ -1,144 +0,0 @@\n-/* librist. Copyright © 2026 SipRadius LLC. All right reserved.\n- * Author: Sergio Ammirata <sergio@ammirata.net>\n- *\n- * SPDX-License-Identifier: BSD-2-Clause\n- */\n-\n-/* Constant-bitrate output pacing. See rist_pacer.h for the rationale. */\n-\n-#include \"rist_pacer.h\"\n-\n-#include <string.h>\n-\n-void rist_pacer_rate_init(struct rist_pacer_rate *r, uint64_t seed_bps)\n-{\n-\tmemset(r, 0, sizeof(*r));\n-\tif (seed_bps) {\n-\t\tr->eight_times = seed_bps * 8ULL;\n-\t\tr->bps = seed_bps;\n-\t}\n-}\n-\n-void rist_pacer_rate_add(struct rist_pacer_rate *r, size_t bytes, uint64_t now_us)\n-{\n-\tif (!r->last_calc_us) {\n-\t\t/* Anchor the first window; these bytes cannot be timed. */\n-\t\tr->last_calc_us = now_us;\n-\t\tr->bytes = 0;\n-\t\treturn;\n-\t}\n-\n-\tr->bytes += bytes;\n-\n-\tuint64_t dt = now_us - r->last_calc_us;\n-\tif (dt < RIST_PACER_RATE_WINDOW_US)\n-\t\treturn;\n-\n-\tif (dt <= RIST_PACER_RATE_WINDOW_US * RIST_PACER_RATE_STALL_FACTOR) {\n-\t\tuint64_t bitrate = (8ULL * r->bytes * 1000000ULL) / dt;\n-\t\tif (r->eight_times == 0)\n-\t\t\tr->eight_times = bitrate * 8ULL;  /* prime, skip the 8-window ramp */\n-\t\telse\n-\t\t\tr->eight_times += bitrate - r->eight_times / 8;\n-\t\tr->bps = r->eight_times / 8;\n-\t}\n-\t/* else: the window spans an outage; keep the last good rate. */\n-\n-\tr->last_calc_us = now_us;\n-\tr->bytes = 0;\n-}\n-\n-void rist_pacer_init(struct rist_pacer *p, double target_latency_ms,\n-                     uint64_t min_sleep_ns, uint64_t max_sleep_ns)\n-{\n-\tmemset(p, 0, sizeof(*p));\n-\tp->target_latency_ms = target_latency_ms;\n-\tp->min_sleep_ns = min_sleep_ns;\n-\tp->max_sleep_ns = max_sleep_ns;\n-\tp->max_slip_ns = RIST_PACER_MAX_SLIP_NS;\n-}\n-\n-void rist_pacer_reset(struct rist_pacer *p)\n-{\n-\tp->next_due_ns = 0;\n-}\n-\n-uint64_t rist_pacer_interval_ns(const struct rist_pacer *p, uint64_t rate_bps,\n-                                size_t bytes, size_t occupancy)\n-{\n-\tif (!rate_bps || !bytes)\n-\t\treturn 0;\n-\n-\tuint64_t interval_ns = ((uint64_t)bytes * 8ULL * 1000000000ULL) / rate_bps;\n-\n-\tif (p->target_latency_ms <= 0.0)\n-\t\treturn interval_ns;\n-\n-\t/* Above target shortens to drain, below lengthens to fill; clamped so a\n-\t * transient excursion cannot slew the rate far. */\n-\tdouble target_bytes = (double)rate_bps / 8.0 * (p->target_latency_ms / 1000.0);\n-\tdouble target_dg = target_bytes / (double)bytes;\n-\tif (target_dg < 1.0)\n-\t\ttarget_dg = 1.0;\n-\n-\tdouble err = ((double)occupancy - target_dg) / target_dg;\n-\tdouble factor = 1.0 - RIST_PACER_FEEDBACK_K * err;\n-\tif (factor < RIST_PACER_FEEDBACK_MIN)\n-\t\tfactor = RIST_PACER_FEEDBACK_MIN;\n-\tif (factor > RIST_PACER_FEEDBACK_MAX)\n-\t\tfactor = RIST_PACER_FEEDBACK_MAX;\n-\n-\treturn (uint64_t)((double)interval_ns * factor);\n-}\n-\n-uint64_t rist_pacer_due_ns(struct rist_pacer *p, uint64_t now_ns)\n-{\n-\tif (p->next_due_ns == 0) {\n-\t\tp->next_due_ns = now_ns;\n-\t} else if (now_ns > p->next_due_ns + p->max_slip_ns) {\n-\t\tp->next_due_ns = now_ns;\n-\t\tp->underruns++;\n-\t}\n-\treturn p->next_due_ns;\n-}\n-\n-void rist_pacer_advance(struct rist_pacer *p, uint64_t interval_ns)\n-{\n-\tp->next_due_ns += interval_ns;\n-}\n-\n-unsigned rist_pacer_quota(struct rist_pacer *p, uint64_t now_ns,\n-                          uint64_t interval_ns, unsigned max)\n-{\n-\t/* No estimate: nothing to pace against, so do not gate. */\n-\tif (interval_ns == 0)\n-\t\treturn max;\n-\n-\t/* Shared first-send and slip handling. */\n-\t(void)rist_pacer_due_ns(p, now_ns);\n-\n-\tunsigned n = 0;\n-\twhile (n < max && p->next_due_ns <= now_ns) {\n-\t\tp->next_due_ns += interval_ns;\n-\t\tn++;\n-\t}\n-\treturn n;\n-}\n-\n-uint64_t rist_pacer_sleep_ns(const struct rist_pacer *p, uint64_t now_ns)\n-{\n-\tuint64_t sleep_ns;\n-\n-\tif (p->next_due_ns == 0)\n-\t\tsleep_ns = p->max_sleep_ns;   /* nothing scheduled: idle normally */\n-\telse if (p->next_due_ns <= now_ns)\n-\t\tsleep_ns = p->min_sleep_ns;   /* already due: wake as soon as allowed */\n-\telse\n-\t\tsleep_ns = p->next_due_ns - now_ns;\n-\n-\tif (sleep_ns < p->min_sleep_ns)\n-\t\tsleep_ns = p->min_sleep_ns;\n-\tif (p->max_sleep_ns && sleep_ns > p->max_sleep_ns)\n-\t\tsleep_ns = p->max_sleep_ns;\n-\treturn sleep_ns;\n-}\ndiff --git a/src/rist_pacer.h b/src/rist_pacer.h\ndeleted file mode 100644\nindex 8bbe17c..0000000\n--- a/src/rist_pacer.h\n+++ /dev/null\n@@ -1,122 +0,0 @@\n-/* librist. Copyright © 2026 SipRadius LLC. All right reserved.\n- * Author: Sergio Ammirata <sergio@ammirata.net>\n- *\n- * SPDX-License-Identifier: BSD-2-Clause\n- */\n-\n-/* Constant-bitrate output pacing: a rate estimator and a schedule derived from\n- * it, for spacing output that would otherwise be released in batches.\n- *\n- * Takes byte counts and returns times; it never inspects payload contents, so it\n- * suits any CBR payload. Callers that can read a clock reference out of their\n- * payload should use it to derive a rate and pass that in.\n- *\n- * Schedules but owns no thread, so a caller with its own wait keeps using it.\n- * Not thread-safe; the estimator and the scheduler share no state, so a caller\n- * may run them on separate threads and move the rate across itself.\n- */\n-\n-#ifndef RIST_PACER_H\n-#define RIST_PACER_H\n-\n-#include <stdint.h>\n-#include <stddef.h>\n-\n-#ifdef __cplusplus\n-extern \"C\" {\n-#endif\n-\n-/* Rate estimator: 1 s windows, 8x EWMA, mirroring librist's\n- * rist_calculate_bitrate so a stream measured here and there agrees. */\n-#define RIST_PACER_RATE_WINDOW_US   1000000ULL\n-/* A window this many times over-length means data resumed after an outage; it is\n- * discarded so the rate freezes rather than decaying towards zero. */\n-#define RIST_PACER_RATE_STALL_FACTOR 3ULL\n-\n-/* Behind by more than this is an outage, not drift: resynchronise to now rather\n- * than catch up, which would emit the arrears as a burst. */\n-#define RIST_PACER_MAX_SLIP_NS      50000000ULL\n-\n-/* Occupancy-trim gain, and the clamp on the resulting correction. */\n-#define RIST_PACER_FEEDBACK_K       0.5\n-#define RIST_PACER_FEEDBACK_MIN     0.80\n-#define RIST_PACER_FEEDBACK_MAX     1.20\n-\n-/* ---------------------------------------------------------------- estimator */\n-\n-struct rist_pacer_rate {\n-\tuint64_t last_calc_us;\n-\tuint64_t bytes;\n-\tuint64_t eight_times;   /* 8x EWMA of the bitrate, as librist keeps it */\n-\tuint64_t bps;           /* published estimate */\n-};\n-\n-/* seed_bps 0 when nothing is known: reports 0 until the first full window, which\n- * callers must treat as \"do not pace\" rather than guess a rate. */\n-void rist_pacer_rate_init(struct rist_pacer_rate *r, uint64_t seed_bps);\n-\n-/* Feed one datagram. now_us must come from a monotonic clock. */\n-void rist_pacer_rate_add(struct rist_pacer_rate *r, size_t bytes, uint64_t now_us);\n-\n-static inline uint64_t rist_pacer_rate_bps(const struct rist_pacer_rate *r)\n-{\n-\treturn r->bps;\n-}\n-\n-/* ---------------------------------------------------------------- scheduler */\n-\n-struct rist_pacer {\n-\tuint64_t next_due_ns;      /* 0 until the first datagram establishes it */\n-\tuint64_t max_slip_ns;\n-\tdouble   target_latency_ms; /* occupancy feedback target; 0 disables it */\n-\n-\t/* Bound rist_pacer_sleep_ns() only, never the per-datagram interval. */\n-\tuint64_t min_sleep_ns;\n-\tuint64_t max_sleep_ns;\n-\n-\tuint64_t underruns;        /* schedule resyncs from slip */\n-};\n-\n-/* min_sleep_ns 0 suits a dedicated pacing thread, where a floor would coarsen the\n- * spacing it exists to hold; a shared event loop wants one to bound its wakeup\n- * rate. target_latency_ms 0 disables the occupancy trim. */\n-void rist_pacer_init(struct rist_pacer *p, double target_latency_ms,\n-                     uint64_t min_sleep_ns, uint64_t max_sleep_ns);\n-\n-/* Re-anchor on the next datagram, keeping the configuration. For a discontinuity\n- * the caller already knows about, which slip would otherwise count as an\n- * underrun. */\n-void rist_pacer_reset(struct rist_pacer *p);\n-\n-/* Time a datagram of `bytes` occupies at `rate_bps`, trimmed towards\n- * target_latency_ms by `occupancy` (the caller's queue depth in datagrams). The\n- * trim corrects source-versus-local clock drift without a payload clock. Returns\n- * 0 when rate_bps is 0, meaning do not pace.\n- *\n- * Never clamped to min_sleep_ns: raising the interval to a sleep floor would pace\n- * the stream below its own rate. The floor belongs on the sleep. */\n-uint64_t rist_pacer_interval_ns(const struct rist_pacer *p, uint64_t rate_bps,\n-                                size_t bytes, size_t occupancy);\n-\n-/* Absolute time the next datagram may be sent, establishing the schedule on the\n- * first call and resynchronising past max_slip_ns. */\n-uint64_t rist_pacer_due_ns(struct rist_pacer *p, uint64_t now_ns);\n-\n-/* Move the schedule on by one datagram. Call after sending. */\n-void rist_pacer_advance(struct rist_pacer *p, uint64_t interval_ns);\n-\n-/* How many datagrams are due by now_ns, up to max, advancing past each; for a\n- * caller that wakes on a timer and releases a batch. More than one per wake is\n- * normal and is what keeps a sleep floor coarser than the interval honest. */\n-unsigned rist_pacer_quota(struct rist_pacer *p, uint64_t now_ns,\n-                          uint64_t interval_ns, unsigned max);\n-\n-/* Time until the next datagram is due, clamped to [min_sleep_ns, max_sleep_ns],\n- * or max_sleep_ns when nothing is scheduled yet. */\n-uint64_t rist_pacer_sleep_ns(const struct rist_pacer *p, uint64_t now_ns);\n-\n-#ifdef __cplusplus\n-}\n-#endif\n-\n-#endif /* RIST_PACER_H */\ndiff --git a/src/stats.c b/src/stats.c\nindex d757827..1042f85 100644\n--- a/src/stats.c\n+++ b/src/stats.c\n@@ -14,41 +14,23 @@\n #include <string.h>\n #include \"cjson/cJSON.h\"\n \n-/* Bump on any incompatible shape change to the stats JSON payloads. */\n-#define RIST_STATS_JSON_SCHEMA_VERSION 5\n-\n-/* How often to repeat the no-retransmission-budget warning. */\n-#define RIST_BANDWIDTH_WARN_INTERVAL (30000ULL * RIST_CLOCK)\n-\n static double round_two_digits(double number)\n {\n \tlong new_number = (long)(number * 100);\n \treturn (double)(new_number) / 100;\n }\n \n-static const char *rist_profile_name(int profile)\n-{\n-\tswitch (profile) {\n-\tcase RIST_PROFILE_SIMPLE:   return \"simple\";\n-\tcase RIST_PROFILE_MAIN:     return \"main\";\n-\tcase RIST_PROFILE_ADVANCED: return \"advanced\";\n-\tdefault:                    return \"unknown\";\n-\t}\n-}\n-\n void rist_sender_flow_statistics(struct rist_sender *ctx)\n {\n \tcJSON *stats = cJSON_CreateObject();\n-\tcJSON_AddNumberToObject(stats, \"schema_version\", RIST_STATS_JSON_SCHEMA_VERSION);\n \tcJSON *rist_sender_stats = cJSON_AddObjectToObject(stats, \"sender-stats\");\n-\tcJSON *peers = cJSON_AddArrayToObject(rist_sender_stats, \"peers\");\n \n \tpthread_mutex_lock(&ctx->common.peerlist_lock);\n \tfor (size_t j = 0; j < ctx->peer_lst_len; j++) {\n \t\tstruct rist_peer *peer = ctx->peer_lst[j];\n \t\tif (!peer->dead && peer->authenticated) {\n \t\t\tcJSON *peer_obj = rist_sender_peer_statistics(peer);\n-\t\t\tcJSON_AddItemToArray(peers, peer_obj);\n+\t\t\tcJSON_AddItemToArray(rist_sender_stats, peer_obj);\n \t\t}\n \t}\n \tpthread_mutex_unlock(&ctx->common.peerlist_lock);\n@@ -61,15 +43,6 @@ void rist_sender_flow_statistics(struct rist_sender *ctx)\n \t\tcJSON_AddNumberToObject(udp_queue_obj, \"packets_per_second\", 1000 * ctx->sender_queue_size / ctx->sender_queue_timelength);\n \telse\n \t\tcJSON_AddNumberToObject(udp_queue_obj, \"packets_per_second\", 0);\n-\n-\tif (ctx->split_mode != LIBRIST_SPLIT_MODE_OFF) {\n-\t\tcJSON *split_obj = cJSON_AddObjectToObject(rist_sender_stats, \"split\");\n-\t\tconst char *mode_str = ctx->split_mode == LIBRIST_SPLIT_MODE_AUTO ? \"auto\" : \"half\";\n-\t\tcJSON_AddStringToObject(split_obj, \"mode\", mode_str);\n-\t\tcJSON_AddNumberToObject(split_obj, \"pairs_emitted\", (double)ctx->stats_pairs_emitted);\n-\t\tcJSON_AddNumberToObject(split_obj, \"fallback_not_ts\", (double)ctx->stats_split_fallback_not_ts);\n-\t}\n-\n \tchar *stats_json = cJSON_PrintUnformatted(stats);\n \tcJSON_Delete(stats);\n \n@@ -120,32 +93,13 @@ cJSON *rist_sender_peer_statistics(struct rist_peer *peer)\n \n \tstruct rist_common_ctx *cctx = get_cctx(peer);\n \n-\tif (peer->is_data\n-\t    && rist_retransmit_budget_starved(bitrate,\n-\t\t\t\t\t      (size_t)peer->config.recovery_maxbitrate * 1000)) {\n-\t\tuint64_t now = timestampNTP_u64();\n-\t\tif (rist_bandwidth_warn_due(peer->bandwidth_warn_ts,\n-\t\t\t\t\t    RIST_BANDWIDTH_WARN_INTERVAL, now)) {\n-\t\t\tpeer->bandwidth_warn_ts = now;\n-\t\t\trist_log_priv(cctx, RIST_LOG_WARN,\n-\t\t\t\t\"Peer #%\"PRIu32\": no retransmission budget. Payload alone is %zu kbps \"\n-\t\t\t\t\"against a %u kbps bandwidth ceiling, and that ceiling covers payload \"\n-\t\t\t\t\"plus retransmissions, so every NACK is refused (%\"PRIu32\" this interval) \"\n-\t\t\t\t\"and lost packets are never recovered. Raise ?bandwidth= above the \"\n-\t\t\t\t\"payload rate, leaving headroom for recovery.\\n\",\n-\t\t\t\tpeer->adv_peer_id, bitrate / 1000, peer->config.recovery_maxbitrate,\n-\t\t\t\tpeer->stats_sender_instant.bandwidth_skip);\n-\t\t}\n-\t}\n-\n-\tcJSON *peer_obj = cJSON_CreateObject();\n+\tcJSON *stats = cJSON_CreateObject();\n+\tcJSON *rist_sender_stats = cJSON_AddObjectToObject(stats, \"sender-stats\");\n+\tcJSON *peer_obj = cJSON_AddObjectToObject(rist_sender_stats, \"peer\");\n \tcJSON_AddNumberToObject(peer_obj, \"flow_id\", peer->adv_flow_id);\n \tcJSON_AddNumberToObject(peer_obj, \"id\", peer->adv_peer_id);\n \tcJSON_AddStringToObject(peer_obj, \"cname\", peer->receiver_name);\n \tcJSON_AddStringToObject(peer_obj, \"type\", peer->is_data ? \"data\" : \"rtcp\");\n-\tcJSON_AddNumberToObject(peer_obj, \"profile\", cctx->profile);\n-\tcJSON_AddStringToObject(peer_obj, \"profile_name\", rist_profile_name(cctx->profile));\n-\tcJSON_AddBoolToObject(peer_obj, \"advanced_active\", peer->is_advanced ? 1 : 0);\n \tif (peer->miface[0])\n \t\tcJSON_AddStringToObject(peer_obj, \"miface\", peer->miface);\n \tcJSON *json_stats = cJSON_AddObjectToObject(peer_obj, \"stats\");\n@@ -154,8 +108,6 @@ cJSON *rist_sender_peer_statistics(struct rist_peer *peer)\n \tcJSON_AddNumberToObject(json_stats, \"ts_null\", (double)peer->stats_sender_instant.ts_null);\n \tcJSON_AddNumberToObject(json_stats, \"received\", (double)peer->stats_sender_instant.received);\n \tcJSON_AddNumberToObject(json_stats, \"retransmitted\", (double)peer->stats_sender_instant.retrans);\n-\tcJSON_AddNumberToObject(json_stats, \"sent_bytes\", (double)peer->stats_sender_instant.sent_bytes);\n-\tcJSON_AddNumberToObject(json_stats, \"retransmitted_bytes\", (double)peer->stats_sender_instant.retransmitted_bytes);\n \tcJSON_AddNumberToObject(json_stats, \"bandwidth\", (double)bitrate);\n \tcJSON_AddNumberToObject(json_stats, \"retry_bandwidth\", (double)retry_bitrate);\n \tcJSON_AddNumberToObject(json_stats, \"ts_nulls_bandwidth\", (double)ts_nulls_bitrate);\n@@ -166,22 +118,14 @@ cJSON *rist_sender_peer_statistics(struct rist_peer *peer)\n \tcJSON_AddNumberToObject(json_stats, \"avg_rtt\", (double)avg_rtt / RIST_CLOCK);\n \tcJSON_AddNumberToObject(json_stats, \"retry_buffer_size\", (double)retry_buf_size);\n \tcJSON_AddNumberToObject(json_stats, \"cooldown_time\", (double)time_left);\n-\tcJSON_AddBoolToObject(json_stats, \"rtt_muted\", peer->rtt_muted ? 1 : 0);\n-\tcJSON_AddNumberToObject(json_stats, \"rtt_mute_events\", (double)peer->rtt_mute_count);\n-\tcJSON *stats = cJSON_CreateObject();\n-\tcJSON_AddNumberToObject(stats, \"schema_version\", RIST_STATS_JSON_SCHEMA_VERSION);\n-\tcJSON *rist_sender_stats = cJSON_AddObjectToObject(stats, \"sender-stats\");\n-\tcJSON *peers = cJSON_AddArrayToObject(rist_sender_stats, \"peers\");\n-\tcJSON_AddItemToArray(peers, cJSON_Duplicate(peer_obj, true));\n+\tcJSON *peer_duplicate = cJSON_Duplicate(peer_obj, true);\n \tchar *stats_string = cJSON_PrintUnformatted(stats);\n \tcJSON_Delete(stats);\n \n \tstats_container->stats_json = stats_string;\n \tstats_container->json_size = (uint32_t)strlen(stats_string);\n-\tstrncpy(stats_container->stats.sender_peer.cname,\n-\t\tpeer->receiver_name,\n-\t\tsizeof(stats_container->stats.sender_peer.cname) - 1);\n-\tstats_container->stats.sender_peer.cname[sizeof(stats_container->stats.sender_peer.cname) - 1] = '\\0';\n+\tstats_container->stats.sender_peer.cname[0] = '\\0';\n+\tstrncpy(stats_container->stats.sender_peer.cname, peer->receiver_name, RIST_MAX_STRING_SHORT);\n \tstats_container->stats.sender_peer.peer_id = peer->adv_peer_id;\n \tstats_container->stats.sender_peer.bandwidth = bitrate;\n \tstats_container->stats.sender_peer.retry_bandwidth = retry_bitrate;\n@@ -191,12 +135,6 @@ cJSON *rist_sender_peer_statistics(struct rist_peer *peer)\n \tstats_container->stats.sender_peer.retransmitted = peer->stats_sender_instant.retrans;\n \tstats_container->stats.sender_peer.quality = Q;\n \tstats_container->stats.sender_peer.rtt = avg_rtt / RIST_CLOCK;\n-\tstats_container->stats.sender_peer.sent_bytes = peer->stats_sender_instant.sent_bytes;\n-\tstats_container->stats.sender_peer.retransmitted_bytes = peer->stats_sender_instant.retransmitted_bytes;\n-\tstats_container->stats.sender_peer.profile = (uint8_t)cctx->profile;\n-\tstats_container->stats.sender_peer.advanced_active = peer->is_advanced ? 1 : 0;\n-\tstats_container->stats.sender_peer.rtt_muted = peer->rtt_muted ? 1 : 0;\n-\tstats_container->stats.sender_peer.rtt_mute_events = peer->rtt_mute_count;\n \n \tif (cctx->stats_callback != NULL)\n \t\tcctx->stats_callback(cctx->stats_callback_argument, stats_container);\n@@ -206,7 +144,7 @@ cJSON *rist_sender_peer_statistics(struct rist_peer *peer)\n \tmemset(&peer->stats_sender_instant, 0, sizeof(peer->stats_sender_instant));\n \tpthread_mutex_unlock(&(get_cctx(peer)->stats_lock));\n \n-\treturn peer_obj;\n+\treturn peer_duplicate;\n }\n \n void rist_receiver_flow_statistics(struct rist_receiver *ctx, struct rist_flow *flow)\n@@ -234,16 +172,10 @@ void rist_receiver_flow_statistics(struct rist_receiver *ctx, struct rist_flow *\n \tstats_container->stats.receiver_flow.peers = calloc(flow->peer_lst_len, sizeof(struct rist_stats_receiver_peer));\n \n \tcJSON *stats = cJSON_CreateObject();\n-\tcJSON_AddNumberToObject(stats, \"schema_version\", RIST_STATS_JSON_SCHEMA_VERSION);\n \tcJSON *stats_obj = cJSON_AddObjectToObject(stats, \"receiver-stats\");\n \tcJSON *flow_obj = cJSON_AddObjectToObject(stats_obj, \"flowinstant\");\n \tcJSON_AddNumberToObject(flow_obj, \"flow_id\", flow->flow_id);\n \tcJSON_AddNumberToObject(flow_obj, \"dead\",  flow->dead);\n-\tcJSON_AddNumberToObject(flow_obj, \"profile\", ctx->common.profile);\n-\tcJSON_AddStringToObject(flow_obj, \"profile_name\", rist_profile_name(ctx->common.profile));\n-\tcJSON_AddNumberToObject(flow_obj, \"seq_bits\", flow->short_seq ? 16 : 32);\n-\tcJSON_AddBoolToObject(flow_obj, \"advanced_active\",\n-\t\t(ctx->common.profile >= RIST_PROFILE_ADVANCED && !flow->short_seq) ? 1 : 0);\n \tcJSON *json_stats = cJSON_AddObjectToObject(flow_obj, \"stats\");\n \tcJSON *peers = cJSON_AddArrayToObject(flow_obj, \"peers\");\n \tuint32_t flow_rtt = 0;\n@@ -263,16 +195,8 @@ void rist_receiver_flow_statistics(struct rist_receiver *ctx, struct rist_flow *\n \t\tcJSON *peer_obj = cJSON_CreateObject();\n \t\tcJSON_AddNumberToObject(peer_obj, \"id\", peer->adv_peer_id);\n \t\tcJSON_AddNumberToObject(peer_obj, \"dead\", peer->dead);\n-\t\t/* Listener-mode children have url=NULL; the configured URL\n-\t\t * lives on the parent peer. */\n-\t\tconst char *peer_url_str = peer->url;\n-\t\tif ((peer_url_str == NULL || peer_url_str[0] == '\\0') && peer->parent && peer->parent->url) {\n-\t\t\tpeer_url_str = peer->parent->url;\n-\t\t}\n-\t\tcJSON_AddStringToObject(peer_obj, \"url\", peer_url_str ? peer_url_str : \"\");\n \t\tcJSON *peer_stats = cJSON_AddObjectToObject(peer_obj, \"stats\");\n \t\tcJSON_AddNumberToObject(peer_stats, \"received_data\", (double)peer->stats_receiver_instant.received);\n-\t\tcJSON_AddNumberToObject(peer_stats, \"received_bytes\", (double)peer->stats_receiver_instant.received_bytes);\n \t\tcJSON_AddNumberToObject(peer_stats, \"ts_null\", (double)peer->stats_receiver_instant.ts_null);\t\n \t\tcJSON_AddNumberToObject(peer_stats, \"received_rtcp\", (double)peer->stats_receiver_instant.received_rtcp);\n \t\tcJSON_AddNumberToObject(peer_stats, \"sent_rtcp\", (double)peer->stats_receiver_instant.sent_rtcp);\n@@ -284,7 +208,6 @@ void rist_receiver_flow_statistics(struct rist_receiver *ctx, struct rist_flow *\n \n \t\tstats_container->stats.receiver_flow.peers[i].peer_id = peer->adv_peer_id;\n \t\tstats_container->stats.receiver_flow.peers[i].received_data = peer->stats_receiver_instant.received;\n-\t\tstats_container->stats.receiver_flow.peers[i].received_bytes = peer->stats_receiver_instant.received_bytes;\n \t\tstats_container->stats.receiver_flow.peers[i].received_rtcp = peer->stats_receiver_instant.received_rtcp;\n \t\tstats_container->stats.receiver_flow.peers[i].sent_rtcp = peer->stats_receiver_instant.sent_rtcp;\n \t\tstats_container->stats.receiver_flow.peers[i].rtt = peer->last_rtt / RIST_CLOCK;\n@@ -349,7 +272,6 @@ void rist_receiver_flow_statistics(struct rist_receiver *ctx, struct rist_flow *\n \t}\n \tcJSON_AddNumberToObject(json_stats, \"quality\", Q);\n \tcJSON_AddNumberToObject(json_stats, \"received\", (double)flow->stats_instant.received);\n-\tcJSON_AddNumberToObject(json_stats, \"received_bytes\", (double)flow->stats_instant.received_bytes);\n \tcJSON_AddNumberToObject(json_stats, \"dropped_late\", (double)flow->stats_instant.dropped_late);\n \tcJSON_AddNumberToObject(json_stats, \"dropped_full\", (double)flow->stats_instant.dropped_full);\n \tcJSON_AddNumberToObject(json_stats, \"missing\", (double)flow->stats_instant.missing);\n@@ -369,26 +291,12 @@ void rist_receiver_flow_statistics(struct rist_receiver *ctx, struct rist_flow *\n \tcJSON_AddNumberToObject(json_stats, \"min_inter_packet_spacing\", (double)flow->stats_instant.min_ips);\n \tcJSON_AddNumberToObject(json_stats, \"cur_inter_packet_spacing\", (double)flow->stats_instant.cur_ips);\n \tcJSON_AddNumberToObject(json_stats, \"max_inter_packet_spacing\", (double)flow->stats_instant.max_ips);\n-\t/* Decay flow bitrate counters when no traffic has arrived since the last tick. */\n-\trist_refresh_flow_bitrate(&flow->bw);\n-\trist_refresh_flow_bitrate(&flow->bw_retries);\n-\trist_refresh_flow_bitrate(&flow->bw_rejected);\n-\trist_refresh_flow_bitrate(&flow->bw_tsnull);\n \tcJSON_AddNumberToObject(json_stats, \"bitrate\", (double)flow->bw.bitrate);\n \tcJSON_AddNumberToObject(json_stats, \"bitrate_retries\", (double)flow->bw_retries.bitrate);\n \tcJSON_AddNumberToObject(json_stats, \"bitrate_rejected\", (double)flow->bw_rejected.bitrate);\n \tcJSON_AddNumberToObject(json_stats, \"bitrate_ts_nulls\", (double)flow->bw_tsnull.bitrate);\n \tcJSON_AddNumberToObject(json_stats, \"bitrate_payload\", (double)flow->bw.bitrate + (double)flow->bw_tsnull.bitrate);\n \n-\tif (ctx->merge_mode != LIBRIST_MERGE_MODE_OFF) {\n-\t\tcJSON *merge_obj = cJSON_AddObjectToObject(flow_obj, \"merge\");\n-\t\tconst char *mode_str = ctx->merge_mode == LIBRIST_MERGE_MODE_AUTO ? \"auto\" : \"pairs\";\n-\t\tcJSON_AddStringToObject(merge_obj, \"mode\", mode_str);\n-\t\tcJSON_AddNumberToObject(merge_obj, \"pairs_merged\", (double)ctx->stats_pairs_merged);\n-\t\tcJSON_AddNumberToObject(merge_obj, \"orphan_first_delivered\", (double)ctx->stats_orphan_first_delivered);\n-\t\tcJSON_AddNumberToObject(merge_obj, \"orphan_last_delivered\", (double)ctx->stats_orphan_last_delivered);\n-\t}\n-\n \tchar *stats_string = cJSON_PrintUnformatted(stats);\n \tcJSON_Delete(stats);\n \n@@ -405,7 +313,6 @@ void rist_receiver_flow_statistics(struct rist_receiver *ctx, struct rist_flow *\n \t//stats_container->stats.receiver_flow.ts_nulls_bandwidth = flow->bw_tsnull.bitrate;\n \tstats_container->stats.receiver_flow.sent = flow->peer_lst_len ? flow_sent_instant / flow->peer_lst_len : 0;\n \tstats_container->stats.receiver_flow.received = flow->stats_instant.received;\n-\tstats_container->stats.receiver_flow.received_bytes = flow->stats_instant.received_bytes;\n \tstats_container->stats.receiver_flow.missing = flow->stats_instant.missing;\n \tstats_container->stats.receiver_flow.reordered = flow->stats_instant.reordered;\n \tstats_container->stats.receiver_flow.recovered = flow->stats_instant.recovered;\n@@ -417,10 +324,6 @@ void rist_receiver_flow_statistics(struct rist_receiver *ctx, struct rist_flow *\n \tstats_container->stats.receiver_flow.max_inter_packet_spacing = flow->stats_instant.max_ips;\n \tstats_container->stats.receiver_flow.rtt = flow->peer_lst_len ? (flow_rtt / flow->peer_lst_len)/RIST_CLOCK : 0;\n \tstats_container->stats.receiver_flow.avg_buffer_time = avg_buffer_duration;\n-\tstats_container->stats.receiver_flow.profile = (uint8_t)ctx->common.profile;\n-\tstats_container->stats.receiver_flow.seq_bits = flow->short_seq ? 16 : 32;\n-\tstats_container->stats.receiver_flow.advanced_active =\n-\t\t(ctx->common.profile >= RIST_PROFILE_ADVANCED && !flow->short_seq) ? 1 : 0;\n \n \t/* CALLBACK CALL */\n \tif (ctx->common.stats_callback != NULL)\ndiff --git a/src/transport-private.h b/src/transport-private.h\ndeleted file mode 100644\nindex c3c46d0..0000000\n--- a/src/transport-private.h\n+++ /dev/null\n@@ -1,57 +0,0 @@\n-/* librist. Copyright © 2024 SipRadius LLC. All right reserved.\n- *\n- * SPDX-License-Identifier: BSD-2-Clause\n- */\n-\n-#ifndef RIST_TRANSPORT_PRIVATE_H\n-#define RIST_TRANSPORT_PRIVATE_H\n-\n-/* struct pollfd for the rist_transport_poll() prototype below, gated on\n- * _WIN32_WINNT >= 0x0600 in mingw-w64.  Mirrors src/libevsocket.c. */\n-#ifdef _WIN32\n-# if !defined(_WIN32_WINNT) || _WIN32_WINNT < 0x0600\n-#  undef _WIN32_WINNT\n-#  define _WIN32_WINNT 0x0600\n-# endif\n-# include <winsock2.h>\n-#else\n-# include <poll.h>\n-#endif\n-\n-#include \"common/attributes.h\"\n-#include \"librist/transport.h\"\n-\n-#include <stddef.h>\n-\n-struct rist_peer;\n-struct rist_common_ctx;\n-\n-/**\n- * Wrappers that route through the transport ops vtable when one is\n- * installed, falling back to the real POSIX syscalls otherwise.\n- * These are the ONLY functions that should touch the wire in the\n- * library hot-path — raw sendto/recvfrom/poll/sendmsg calls must\n- * not appear anywhere else.\n- */\n-\n-RIST_PRIV ssize_t rist_transport_sendto(struct rist_peer *peer,\n-                                        const void *buf, size_t len,\n-                                        int flags);\n-\n-RIST_PRIV ssize_t rist_transport_recvfrom(struct rist_peer *peer,\n-                                          void *buf, size_t len,\n-                                          int flags,\n-                                          struct sockaddr *addr,\n-                                          socklen_t *addrlen);\n-\n-RIST_PRIV int rist_transport_poll(struct rist_common_ctx *ctx,\n-                                  struct pollfd *fds, int nfds,\n-                                  int timeout_ms);\n-\n-#ifndef _WIN32\n-RIST_PRIV ssize_t rist_transport_sendmsg(struct rist_peer *peer,\n-                                         const struct msghdr *msg,\n-                                         int flags);\n-#endif\n-\n-#endif /* RIST_TRANSPORT_PRIVATE_H */\ndiff --git a/src/transport.c b/src/transport.c\ndeleted file mode 100644\nindex 47598e0..0000000\n--- a/src/transport.c\n+++ /dev/null\n@@ -1,152 +0,0 @@\n-/* librist. Copyright © 2024 SipRadius LLC. All right reserved.\n- *\n- * SPDX-License-Identifier: BSD-2-Clause\n- */\n-\n-#include \"transport-private.h\"\n-#include \"rist-private.h\"\n-#include \"libevsocket.h\"\n-#include \"librist/transport.h\"\n-\n-#ifdef _WIN32\n-#if !defined(_WIN32_WINNT) || _WIN32_WINNT < 0x0600\n-# undef _WIN32_WINNT\n-# define _WIN32_WINNT 0x0600\n-#endif\n-#include <winsock2.h>\n-\n-typedef unsigned long int nfds_t;\n-#include \"contrib/poll_win.c\"\n-#else\n-#include <sys/socket.h>\n-#include <poll.h>\n-#endif\n-\n-#include <string.h>\n-#include <errno.h>\n-\n-/* ------------------------------------------------------------------ */\n-/* Public API                                                          */\n-/* ------------------------------------------------------------------ */\n-\n-int rist_transport_set(struct rist_ctx *ctx,\n-                       const struct rist_transport_ops *ops)\n-{\n-\tstruct rist_common_ctx *cctx = rist_struct_get_common(ctx);\n-\tif (!cctx)\n-\t\treturn -1;\n-\n-\tif (atomic_load_explicit(&cctx->startup_complete, memory_order_acquire)) {\n-\t\treturn -1;\n-\t}\n-\n-\tif (ops) {\n-\t\tcctx->transport = *ops;\n-\t\tcctx->transport_active = true;\n-\t\tif (cctx->evctx && ops->poll)\n-\t\t\tevsocket_set_poll_override(cctx->evctx, ops->poll,\n-\t\t\t                           ops->opaque);\n-\t} else {\n-\t\tmemset(&cctx->transport, 0, sizeof(cctx->transport));\n-\t\tcctx->transport_active = false;\n-\t\tif (cctx->evctx)\n-\t\t\tevsocket_set_poll_override(cctx->evctx, NULL, NULL);\n-\t}\n-\treturn 0;\n-}\n-\n-/* ------------------------------------------------------------------ */\n-/* Internal wrappers                                                   */\n-/* ------------------------------------------------------------------ */\n-\n-ssize_t rist_transport_sendto(struct rist_peer *peer,\n-                              const void *buf, size_t len,\n-                              int flags)\n-{\n-\tstruct rist_common_ctx *cctx = get_cctx(peer);\n-\n-\tif (cctx->transport_active && cctx->transport.sendto) {\n-\t\treturn cctx->transport.sendto(cctx->transport.opaque,\n-\t\t                              peer->sd, buf, len, flags,\n-\t\t                              &peer->u.address,\n-\t\t                              peer->address_len);\n-\t}\n-\n-\treturn sendto(peer->sd, (const char *)buf, len, flags,\n-\t              &peer->u.address, peer->address_len);\n-}\n-\n-ssize_t rist_transport_recvfrom(struct rist_peer *peer,\n-                                void *buf, size_t len,\n-                                int flags,\n-                                struct sockaddr *addr,\n-                                socklen_t *addrlen)\n-{\n-\tstruct rist_common_ctx *cctx = get_cctx(peer);\n-\n-\tif (cctx->transport_active && cctx->transport.recvfrom) {\n-\t\treturn cctx->transport.recvfrom(cctx->transport.opaque,\n-\t\t                                peer->sd, buf, len, flags,\n-\t\t                                addr, addrlen);\n-\t}\n-\n-\treturn recvfrom(peer->sd, (char *)buf, len, flags, addr, addrlen);\n-}\n-\n-int rist_transport_poll(struct rist_common_ctx *ctx,\n-                        struct pollfd *fds, int nfds,\n-                        int timeout_ms)\n-{\n-\tif (ctx->transport_active && ctx->transport.poll) {\n-\t\treturn ctx->transport.poll(ctx->transport.opaque,\n-\t\t                           fds, nfds, timeout_ms);\n-\t}\n-\n-\treturn poll(fds, nfds, timeout_ms);\n-}\n-\n-#ifndef _WIN32\n-ssize_t rist_transport_sendmsg(struct rist_peer *peer,\n-                               const struct msghdr *msg,\n-                               int flags)\n-{\n-\tstruct rist_common_ctx *cctx = get_cctx(peer);\n-\n-\tif (cctx->transport_active && cctx->transport.sendmsg) {\n-\t\treturn cctx->transport.sendmsg(cctx->transport.opaque,\n-\t\t                               peer->sd, msg, flags);\n-\t}\n-\n-\t/* If custom transport is active but sendmsg is NULL, linearise\n-\t * and fall back to the sendto callback. */\n-\tif (cctx->transport_active && cctx->transport.sendto) {\n-\t\tsize_t total = 0;\n-\t\tfor (size_t i = 0; i < (size_t)msg->msg_iovlen; i++)\n-\t\t\ttotal += msg->msg_iov[i].iov_len;\n-\n-\t\tuint8_t stackbuf[RIST_MAX_PACKET_SIZE + 128];\n-\t\tuint8_t *flat = (total <= sizeof(stackbuf)) ? stackbuf : malloc(total);\n-\t\tif (!flat)\n-\t\t\treturn -1;\n-\n-\t\tsize_t off = 0;\n-\t\tfor (size_t i = 0; i < (size_t)msg->msg_iovlen; i++) {\n-\t\t\tmemcpy(flat + off, msg->msg_iov[i].iov_base,\n-\t\t\t       msg->msg_iov[i].iov_len);\n-\t\t\toff += msg->msg_iov[i].iov_len;\n-\t\t}\n-\n-\t\tssize_t ret = cctx->transport.sendto(\n-\t\t\tcctx->transport.opaque, peer->sd,\n-\t\t\tflat, total, flags,\n-\t\t\tmsg->msg_name, msg->msg_namelen);\n-\n-\t\tif (flat != stackbuf)\n-\t\t\tfree(flat);\n-\n-\t\treturn ret;\n-\t}\n-\n-\treturn sendmsg(peer->sd, msg, flags);\n-}\n-#endif\ndiff --git a/src/tun_cidr.c b/src/tun_cidr.c\ndeleted file mode 100644\nindex 636cd9d..0000000\n--- a/src/tun_cidr.c\n+++ /dev/null\n@@ -1,67 +0,0 @@\n-/* librist. Copyright © 2026 SipRadius LLC. All right reserved.\n- * Author: Sergio Ammirata, Ph.D. <sergio@ammirata.net>\n- *\n- * SPDX-License-Identifier: BSD-2-Clause\n- */\n-\n-#include \"librist/tun.h\"\n-\n-#ifdef _WIN32\n-#include <winsock2.h>\n-#include <ws2tcpip.h>\n-#else\n-#include <arpa/inet.h>\n-#endif\n-#include <stdlib.h>\n-#include <string.h>\n-\n-int rist_tun_parse_cidr(const char *cidr, char *ip_out, size_t ip_len, int *prefix_out)\n-{\n-\tconst char *slash;\n-\tsize_t ip_part_len;\n-\tint prefix;\n-\tint family;\n-\n-\tif (!cidr || !ip_out || ip_len == 0 || !prefix_out)\n-\t\treturn -1;\n-\n-\tslash = strchr(cidr, '/');\n-\tif (!slash) {\n-\t\tif (strlen(cidr) >= ip_len)\n-\t\t\treturn -1;\n-\t\tstrncpy(ip_out, cidr, ip_len - 1);\n-\t\tip_out[ip_len - 1] = '\\0';\n-\t\tprefix = strchr(cidr, ':') ? 128 : 32;\n-\t} else {\n-\t\tip_part_len = (size_t)(slash - cidr);\n-\t\tif (ip_part_len == 0 || ip_part_len >= ip_len)\n-\t\t\treturn -1;\n-\t\tmemcpy(ip_out, cidr, ip_part_len);\n-\t\tip_out[ip_part_len] = '\\0';\n-\t\tprefix = atoi(slash + 1);\n-\t}\n-\n-\tif (strchr(ip_out, ':') != NULL)\n-\t\tfamily = AF_INET6;\n-\telse\n-\t\tfamily = AF_INET;\n-\n-\tif (family == AF_INET6) {\n-\t\tstruct in6_addr addr6;\n-\n-\t\tif (inet_pton(AF_INET6, ip_out, &addr6) != 1)\n-\t\t\treturn -1;\n-\t\tif (prefix < 0 || prefix > 128)\n-\t\t\treturn -1;\n-\t} else {\n-\t\tstruct in_addr addr4;\n-\n-\t\tif (inet_pton(AF_INET, ip_out, &addr4) != 1)\n-\t\t\treturn -1;\n-\t\tif (prefix < 0 || prefix > 32)\n-\t\t\treturn -1;\n-\t}\n-\n-\t*prefix_out = prefix;\n-\treturn 0;\n-}\ndiff --git a/src/tun_darwin.c b/src/tun_darwin.c\nindex bf4f202..44b80c3 100644\n--- a/src/tun_darwin.c\n+++ b/src/tun_darwin.c\n@@ -16,7 +16,6 @@\n #include \"librist/tun.h\"\n \n #include <errno.h>\n-#include <limits.h>\n #include <stdio.h>\n #include <stdlib.h>\n #include <string.h>\n@@ -28,7 +27,6 @@\n #include <net/if.h>\n #include <net/if_utun.h>\n #include <netinet/in.h>\n-#include <netinet6/in6_var.h>\n #include <arpa/inet.h>\n #include <net/route.h>\n \n@@ -70,17 +68,8 @@ int rist_tun_open(const char *requested_name, char *actual_name, size_t name_len\n \t */\n \tunsigned int unit = 0;\n \tif (requested_name && requested_name[0]) {\n-\t\tif (strncmp(requested_name, \"utun\", 4) == 0) {\n-\t\t\tconst char *suffix = requested_name + 4;\n-\t\t\tchar *endptr = NULL;\n-\t\t\terrno = 0;\n-\t\t\tunsigned long parsed = strtoul(suffix, &endptr, 10);\n-\t\t\tif (suffix[0] != '\\0' && errno == 0 &&\n-\t\t\t    endptr != suffix && *endptr == '\\0' &&\n-\t\t\t    parsed < (UINT_MAX - 1)) {\n-\t\t\t\tunit = (unsigned int)parsed + 1;\n-\t\t\t}\n-\t\t}\n+\t\tif (strncmp(requested_name, \"utun\", 4) == 0)\n+\t\t\tunit = (unsigned int)atoi(requested_name + 4) + 1;\n \t}\n \tsc.sc_unit = unit;\n \n@@ -130,7 +119,7 @@ int rist_tun_read(int fd, uint8_t *buf, size_t len)\n int rist_tun_write(int fd, const uint8_t *buf, size_t len)\n {\n \tuint8_t tmp[TUN_DARWIN_BUFSIZE];\n-\tif (len > sizeof(tmp) - UTUN_HEADER_SIZE)\n+\tif (len + UTUN_HEADER_SIZE > sizeof(tmp))\n \t\treturn -1;\n \n \tuint32_t af;\n@@ -151,62 +140,6 @@ int rist_tun_write(int fd, const uint8_t *buf, size_t len)\n \n int rist_tun_set_ip(const char *dev, const char *ip, int prefix_len)\n {\n-\tstruct in6_addr addr6;\n-\tstruct in_addr addr4;\n-\n-\tif (inet_pton(AF_INET6, ip, &addr6) == 1) {\n-\t\tstruct in6_aliasreq ifra6;\n-\n-\t\tif (prefix_len < 0 || prefix_len > 128) {\n-\t\t\tfprintf(stderr, \"Invalid IPv6 prefix length: %d\\n\", prefix_len);\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\tmemset(&ifra6, 0, sizeof(ifra6));\n-\t\tstrncpy(ifra6.ifra_name, dev, IFNAMSIZ - 1);\n-\n-\t\tifra6.ifra_addr.sin6_len = sizeof(ifra6.ifra_addr);\n-\t\tifra6.ifra_addr.sin6_family = AF_INET6;\n-\t\tifra6.ifra_addr.sin6_addr = addr6;\n-\n-\t\tifra6.ifra_prefixmask.sin6_len = sizeof(ifra6.ifra_prefixmask);\n-\t\tifra6.ifra_prefixmask.sin6_family = AF_INET6;\n-\t\tfor (int i = 0; i < prefix_len / 8; i++)\n-\t\t\tifra6.ifra_prefixmask.sin6_addr.s6_addr[i] = 0xff;\n-\t\tif (prefix_len % 8)\n-\t\t\tifra6.ifra_prefixmask.sin6_addr.s6_addr[prefix_len / 8] =\n-\t\t\t\t(uint8_t)(0xff << (8 - (prefix_len % 8)));\n-\n-\t\tifra6.ifra_dstaddr.sin6_len = sizeof(ifra6.ifra_dstaddr);\n-\t\tifra6.ifra_dstaddr.sin6_family = AF_INET6;\n-\t\tifra6.ifra_dstaddr.sin6_addr = addr6;\n-\n-\t\tint s = socket(AF_INET6, SOCK_DGRAM, 0);\n-\t\tif (s < 0) {\n-\t\t\tperror(\"socket AF_INET6\");\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\tif (ioctl(s, SIOCAIFADDR_IN6, &ifra6) < 0) {\n-\t\t\tperror(\"ioctl SIOCAIFADDR_IN6\");\n-\t\t\tclose(s);\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\tclose(s);\n-\t\treturn 0;\n-\t}\n-\n-\tif (inet_pton(AF_INET, ip, &addr4) != 1) {\n-\t\tfprintf(stderr, \"Invalid IP address: %s\\n\", ip);\n-\t\treturn -1;\n-\t}\n-\n-\tif (prefix_len < 0 || prefix_len > 32) {\n-\t\tfprintf(stderr, \"Invalid IPv4 prefix length: %d\\n\", prefix_len);\n-\t\treturn -1;\n-\t}\n-\n \tstruct ifaliasreq ifra;\n \tmemset(&ifra, 0, sizeof(ifra));\n \tstrncpy(ifra.ifra_name, dev, IFNAMSIZ - 1);\n@@ -214,17 +147,16 @@ int rist_tun_set_ip(const char *dev, const char *ip, int prefix_len)\n \tstruct sockaddr_in *addr = (struct sockaddr_in *)&ifra.ifra_addr;\n \taddr->sin_len = sizeof(*addr);\n \taddr->sin_family = AF_INET;\n-\taddr->sin_addr = addr4;\n+\tif (inet_pton(AF_INET, ip, &addr->sin_addr) != 1) {\n+\t\tfprintf(stderr, \"Invalid IP address: %s\\n\", ip);\n+\t\treturn -1;\n+\t}\n \n \tstruct sockaddr_in *mask = (struct sockaddr_in *)&ifra.ifra_mask;\n \tmask->sin_len = sizeof(*mask);\n \tmask->sin_family = AF_INET;\n-\tif (prefix_len == 0)\n-\t\tmask->sin_addr.s_addr = 0;\n-\telse if (prefix_len == 32)\n-\t\tmask->sin_addr.s_addr = htonl(0xFFFFFFFFu);\n-\telse\n-\t\tmask->sin_addr.s_addr = htonl(~((1U << (32 - prefix_len)) - 1));\n+\tmask->sin_addr.s_addr = prefix_len ?\n+\t\thtonl(~((1U << (32 - prefix_len)) - 1)) : 0;\n \n \tstruct sockaddr_in *dst = (struct sockaddr_in *)&ifra.ifra_broadaddr;\n \tdst->sin_len = sizeof(*dst);\ndiff --git a/src/tun_emscripten.c b/src/tun_emscripten.c\ndeleted file mode 100644\nindex 492487a..0000000\n--- a/src/tun_emscripten.c\n+++ /dev/null\n@@ -1,40 +0,0 @@\n-/* librist. Copyright © 2024 SipRadius LLC. All right reserved.\n- *\n- * SPDX-License-Identifier: BSD-2-Clause\n- *\n- * TUN device stubs for Emscripten/WASM — no network interfaces available.\n- */\n-\n-#ifdef __EMSCRIPTEN__\n-\n-#include \"librist/tun.h\"\n-#include <errno.h>\n-\n-int rist_tun_open(const char *requested_name, char *actual_name, size_t name_len)\n-{\n-\t(void)requested_name;\n-\t(void)actual_name;\n-\t(void)name_len;\n-\terrno = ENOTSUP;\n-\treturn -1;\n-}\n-\n-int rist_tun_read(int fd, uint8_t *buf, size_t len)\n-{\n-\t(void)fd;\n-\t(void)buf;\n-\t(void)len;\n-\terrno = ENOTSUP;\n-\treturn -1;\n-}\n-\n-int rist_tun_write(int fd, const uint8_t *buf, size_t len)\n-{\n-\t(void)fd;\n-\t(void)buf;\n-\t(void)len;\n-\terrno = ENOTSUP;\n-\treturn -1;\n-}\n-\n-#endif /* __EMSCRIPTEN__ */\ndiff --git a/src/tun_linux.c b/src/tun_linux.c\nindex 58d5678..f276968 100644\n--- a/src/tun_linux.c\n+++ b/src/tun_linux.c\n@@ -17,9 +17,6 @@\n #include <sys/socket.h>\n #include <net/if.h>\n #include <linux/if_tun.h>\n-#include <linux/netlink.h>\n-#include <linux/rtnetlink.h>\n-#include <linux/if_addr.h>\n #include <arpa/inet.h>\n \n int rist_tun_open(const char *requested_name, char *actual_name, size_t name_len)\n@@ -65,17 +62,17 @@ int rist_tun_write(int fd, const uint8_t *buf, size_t len)\n \treturn (int)write(fd, buf, len);\n }\n \n-static int tun_ioctl_socket(int family)\n+static int tun_ioctl_socket(void)\n {\n-\tint s = socket(family, SOCK_DGRAM, 0);\n+\tint s = socket(AF_INET, SOCK_DGRAM, 0);\n \tif (s < 0)\n-\t\tperror(family == AF_INET6 ? \"socket AF_INET6\" : \"socket AF_INET\");\n+\t\tperror(\"socket AF_INET\");\n \treturn s;\n }\n \n-static int tun_set_ipv4(const char *dev, const char *ip, int prefix_len)\n+int rist_tun_set_ip(const char *dev, const char *ip, int prefix_len)\n {\n-\tint s = tun_ioctl_socket(AF_INET);\n+\tint s = tun_ioctl_socket();\n \tif (s < 0)\n \t\treturn -1;\n \n@@ -87,7 +84,7 @@ static int tun_set_ipv4(const char *dev, const char *ip, int prefix_len)\n \taddr->sin_family = AF_INET;\n \n \tif (inet_pton(AF_INET, ip, &addr->sin_addr) != 1) {\n-\t\tfprintf(stderr, \"Invalid IPv4 address: %s\\n\", ip);\n+\t\tfprintf(stderr, \"Invalid IP address: %s\\n\", ip);\n \t\tclose(s);\n \t\treturn -1;\n \t}\n@@ -98,13 +95,7 @@ static int tun_set_ipv4(const char *dev, const char *ip, int prefix_len)\n \t\treturn -1;\n \t}\n \n-\tuint32_t mask;\n-\tif (prefix_len == 0)\n-\t\tmask = 0;\n-\telse if (prefix_len == 32)\n-\t\tmask = htonl(0xFFFFFFFFu);\n-\telse\n-\t\tmask = htonl(~((1U << (32 - prefix_len)) - 1));\n+\tuint32_t mask = prefix_len ? htonl(~((1U << (32 - prefix_len)) - 1)) : 0;\n \tstruct sockaddr_in *netmask = (struct sockaddr_in *)&ifr.ifr_netmask;\n \tnetmask->sin_family = AF_INET;\n \tnetmask->sin_addr.s_addr = mask;\n@@ -119,156 +110,9 @@ static int tun_set_ipv4(const char *dev, const char *ip, int prefix_len)\n \treturn 0;\n }\n \n-static int tun_netlink_ack(int fd)\n-{\n-\tchar buf[512];\n-\tstruct iovec iov = { .iov_base = buf, .iov_len = sizeof(buf) };\n-\tstruct sockaddr_nl sa;\n-\tstruct msghdr msg = {\n-\t\t.msg_name = &sa,\n-\t\t.msg_namelen = sizeof(sa),\n-\t\t.msg_iov = &iov,\n-\t\t.msg_iovlen = 1,\n-\t};\n-\n-\tif (recvmsg(fd, &msg, 0) < 0) {\n-\t\tperror(\"netlink recvmsg\");\n-\t\treturn -1;\n-\t}\n-\n-\tstruct nlmsghdr *nlh = (struct nlmsghdr *)buf;\n-\tif (nlh->nlmsg_type == NLMSG_ERROR) {\n-\t\tstruct nlmsgerr *err = (struct nlmsgerr *)NLMSG_DATA(nlh);\n-\t\tif (err->error != 0) {\n-\t\t\terrno = -err->error;\n-\t\t\tperror(\"netlink RTM_NEWADDR\");\n-\t\t\treturn -1;\n-\t\t}\n-\t}\n-\treturn 0;\n-}\n-\n-static int tun_add_rtattr(struct nlmsghdr *nlh, size_t max_len,\n-                          unsigned short type, const void *data, size_t len)\n-{\n-\tstruct rtattr *rta;\n-\tsize_t new_len;\n-\n-\tnew_len = NLMSG_ALIGN(nlh->nlmsg_len) + RTA_ALIGN(RTA_LENGTH(len));\n-\tif (new_len > max_len)\n-\t\treturn -1;\n-\n-\trta = (struct rtattr *)((char *)nlh + NLMSG_ALIGN(nlh->nlmsg_len));\n-\trta->rta_type = type;\n-\trta->rta_len = (unsigned short)RTA_LENGTH(len);\n-\tmemcpy(RTA_DATA(rta), data, len);\n-\tnlh->nlmsg_len = (unsigned int)new_len;\n-\treturn 0;\n-}\n-\n-static int tun_set_ipv6(const char *dev, const char *ip, int prefix_len)\n-{\n-\tstruct in6_addr addr6;\n-\tunsigned int ifindex;\n-\tint fd;\n-\tchar reqbuf[512];\n-\tstruct nlmsghdr *nlh;\n-\tstruct ifaddrmsg *ifa;\n-\tstruct sockaddr_nl sa;\n-\n-\tif (inet_pton(AF_INET6, ip, &addr6) != 1) {\n-\t\tfprintf(stderr, \"Invalid IPv6 address: %s\\n\", ip);\n-\t\treturn -1;\n-\t}\n-\n-\tifindex = if_nametoindex(dev);\n-\tif (ifindex == 0) {\n-\t\tperror(\"if_nametoindex\");\n-\t\treturn -1;\n-\t}\n-\n-\tfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);\n-\tif (fd < 0) {\n-\t\tperror(\"socket AF_NETLINK\");\n-\t\treturn -1;\n-\t}\n-\n-\tmemset(reqbuf, 0, sizeof(reqbuf));\n-\tnlh = (struct nlmsghdr *)reqbuf;\n-\tifa = (struct ifaddrmsg *)(nlh + 1);\n-\n-\tnlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct ifaddrmsg));\n-\tnlh->nlmsg_type = RTM_NEWADDR;\n-\tnlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_ACK;\n-\tnlh->nlmsg_seq = 1;\n-\n-\tifa->ifa_family = AF_INET6;\n-\tifa->ifa_prefixlen = (unsigned char)prefix_len;\n-\tifa->ifa_flags = IFA_F_NODAD;\n-\tifa->ifa_scope = RT_SCOPE_UNIVERSE;\n-\tifa->ifa_index = ifindex;\n-\n-\tif (tun_add_rtattr(nlh, sizeof(reqbuf), IFA_LOCAL, &addr6, sizeof(addr6)) != 0 ||\n-\t    tun_add_rtattr(nlh, sizeof(reqbuf), IFA_ADDRESS, &addr6, sizeof(addr6)) != 0) {\n-\t\tfprintf(stderr, \"netlink attribute too large\\n\");\n-\t\tclose(fd);\n-\t\treturn -1;\n-\t}\n-\n-\tmemset(&sa, 0, sizeof(sa));\n-\tsa.nl_family = AF_NETLINK;\n-\n-\tstruct iovec iov = { .iov_base = reqbuf, .iov_len = nlh->nlmsg_len };\n-\tstruct msghdr msg = {\n-\t\t.msg_name = &sa,\n-\t\t.msg_namelen = sizeof(sa),\n-\t\t.msg_iov = &iov,\n-\t\t.msg_iovlen = 1,\n-\t};\n-\n-\tif (sendmsg(fd, &msg, 0) < 0) {\n-\t\tperror(\"netlink sendmsg\");\n-\t\tclose(fd);\n-\t\treturn -1;\n-\t}\n-\n-\tif (tun_netlink_ack(fd) != 0) {\n-\t\tclose(fd);\n-\t\treturn -1;\n-\t}\n-\n-\tclose(fd);\n-\treturn 0;\n-}\n-\n-int rist_tun_set_ip(const char *dev, const char *ip, int prefix_len)\n-{\n-\tstruct in6_addr addr6;\n-\tstruct in_addr addr4;\n-\n-\tif (inet_pton(AF_INET6, ip, &addr6) == 1) {\n-\t\tif (prefix_len < 0 || prefix_len > 128) {\n-\t\t\tfprintf(stderr, \"Invalid IPv6 prefix length: %d\\n\", prefix_len);\n-\t\t\treturn -1;\n-\t\t}\n-\t\treturn tun_set_ipv6(dev, ip, prefix_len);\n-\t}\n-\n-\tif (inet_pton(AF_INET, ip, &addr4) == 1) {\n-\t\tif (prefix_len < 0 || prefix_len > 32) {\n-\t\t\tfprintf(stderr, \"Invalid IPv4 prefix length: %d\\n\", prefix_len);\n-\t\t\treturn -1;\n-\t\t}\n-\t\treturn tun_set_ipv4(dev, ip, prefix_len);\n-\t}\n-\n-\tfprintf(stderr, \"Invalid IP address: %s\\n\", ip);\n-\treturn -1;\n-}\n-\n int rist_tun_set_mtu(const char *dev, int mtu)\n {\n-\tint s = tun_ioctl_socket(AF_INET);\n+\tint s = tun_ioctl_socket();\n \tif (s < 0)\n \t\treturn -1;\n \n@@ -289,7 +133,7 @@ int rist_tun_set_mtu(const char *dev, int mtu)\n \n int rist_tun_bring_up(const char *dev)\n {\n-\tint s = tun_ioctl_socket(AF_INET);\n+\tint s = tun_ioctl_socket();\n \tif (s < 0)\n \t\treturn -1;\n \ndiff --git a/src/udp-private.h b/src/udp-private.h\nindex a8e01ba..c459fb5 100755\n--- a/src/udp-private.h\n+++ b/src/udp-private.h\n@@ -42,11 +42,5 @@ RIST_PRIV int rist_set_url(struct rist_peer *peer);\n RIST_PRIV void rist_create_socket(struct rist_peer *peer);\n RIST_PRIV size_t rist_get_sender_retry_queue_size(struct rist_sender *ctx);\n \n-/* Rate-limited log on send failure. Emits a PMTU-specific hint when errno\n- * is EMSGSIZE/WSAEMSGSIZE, otherwise the generic failure line. attempted\n- * is the size we tried to send; sock_errno is errno (or WSAGetLastError on\n- * Windows) captured immediately after the failing send call. */\n-RIST_PRIV void _librist_log_send_error(struct rist_peer *p, int sock_errno,\n-                                       size_t attempted, const char *origin);\n \n #endif\ndiff --git a/src/udp.c b/src/udp.c\nindex ff7634a..acb66f9 100755\n--- a/src/udp.c\n+++ b/src/udp.c\n@@ -11,7 +11,6 @@\n #include \"proto/protocol_gre.h\"\n #include \"udp-private.h\"\n #include \"rist-private.h\"\n-#include \"rist-retx-domain.h\"\n #include \"log-private.h\"\n #include \"socket-shim.h\"\n #include \"endian-shim.h\"\n@@ -22,8 +21,6 @@\n #endif\n #include \"crypto/psk.h\"\n #include \"mpegts.h\"\n-#include <lz4.h>\n-#include \"transport-private.h\"\n #include <stdlib.h>\n #include <stddef.h>\n #include <errno.h>\n@@ -31,45 +28,7 @@\n #include <assert.h>\n #include <fcntl.h>\n \n-/* EMSGSIZE-aware send-failure logger, rate-limited to one line per peer\n- * every 5 seconds to avoid drowning the log when the application is\n- * pushing a stream of oversized packets. The hint on the PMTU case is\n- * deliberately explicit and actionable: an operator who sees this once\n- * should be able to fix it by lowering the application packet size\n- * (e.g. risttunnel -m), without having to chase ICMP filtering. */\n-void _librist_log_send_error(struct rist_peer *p, int sock_errno,\n-                             size_t attempted, const char *origin)\n-{\n-\tstruct rist_common_ctx *ctx = get_cctx(p);\n-\tuint64_t now = timestampNTP_u64();\n-\tbool is_pmtu;\n-#ifdef _WIN32\n-\tis_pmtu = (sock_errno == WSAEMSGSIZE);\n-#else\n-\tis_pmtu = (sock_errno == EMSGSIZE);\n-#endif\n-\tif (is_pmtu) {\n-\t\tconst uint64_t five_seconds = (uint64_t)5 * 65536 * 1000;\n-\t\tif (p->last_pmtu_error_log != 0 &&\n-\t\t    now - p->last_pmtu_error_log < five_seconds)\n-\t\t\treturn;\n-\t\tp->last_pmtu_error_log = now;\n-\t\trist_log_priv(ctx, RIST_LOG_ERROR,\n-\t\t\t\"PMTU exceeded sending %zu-byte datagram via %s (errno=%d). The path \"\n-\t\t\t\"MTU to this peer is smaller than our RIST packet size and the \"\n-\t\t\t\"don't-fragment bit is set, so the kernel refused to fragment. \"\n-\t\t\t\"Lower the application packet size (e.g. risttunnel -m, or upstream \"\n-\t\t\t\"MPEG-TS UDP size) so that payload + ~40 bytes of headers fits in the \"\n-\t\t\t\"path MTU. Further PMTU errors on this peer will be silenced for 5s.\\n\",\n-\t\t\tattempted, origin, sock_errno);\n-\t} else {\n-\t\trist_log_priv(ctx, RIST_LOG_ERROR,\n-\t\t\t\"Send failed via %s: errno=%d, size=%zu, socket=%d\\n\",\n-\t\t\torigin, sock_errno, attempted, p->sd);\n-\t}\n-}\n-\n-size_t rist_send_seq_rtcp(struct rist_peer *p, uint32_t seq_rtp, uint8_t payload_type, uint8_t *payload, size_t payload_len, uint64_t source_time, uint16_t src_port, uint16_t dst_port, bool retry, uint16_t ts_null_bytes)\n+size_t rist_send_seq_rtcp(struct rist_peer *p, uint16_t seq_rtp, uint8_t payload_type, uint8_t *payload, size_t payload_len, uint64_t source_time, uint16_t src_port, uint16_t dst_port, bool retry, uint16_t ts_null_bytes)\n {\n \tstruct rist_common_ctx *ctx = get_cctx(p);\n \tuint8_t *data;\n@@ -80,129 +39,6 @@ size_t rist_send_seq_rtcp(struct rist_peer *p, uint32_t seq_rtp, uint8_t payload\n \tuint8_t *_payload = NULL;\n \t_payload = payload;\n \n-\t/* Advanced Profile (VSF TR-06-3): build RTP-based packet directly,\n-\t * bypassing GRE framing entirely. Control and OOB are handled\n-\t * separately through rist_adv_send_control().\n-\t *\n-\t * TR-06-3 Section 9 (interop): an Advanced device \"shall start in Main\n-\t * Profile mode\" and only switch to Advanced framing for a peer once that\n-\t * peer advertises Advanced capability (I=1 in its Main keep-alives, which\n-\t * sets remote_supports_advanced). Until then we emit Main-conformant\n-\t * media so a Main-only peer can decode it. */\n-\tif (ctx->profile == RIST_PROFILE_ADVANCED &&\n-\t    p->remote_supports_advanced &&\n-\t    payload_type != RIST_PAYLOAD_TYPE_DATA_OOB &&\n-\t    payload_type != RIST_PAYLOAD_TYPE_RTCP &&\n-\t    payload_type != RIST_PAYLOAD_TYPE_RTCP_NACK) {\n-\t\tuint8_t adv_buf[RIST_MAX_PACKET_SIZE + RIST_ADV_MAX_FIXED_HEADER];\n-\t\tstruct rist_adv_params params;\n-\t\tmemset(&params, 0, sizeof(params));\n-\n-\t\tparams.seq = seq_rtp;\n-\t\tparams.timestamp = timestampRTP_u32(1, source_time);\n-\t\tparams.ssrc = rist_adv_ssrc_protected(ctx->adv_ssrc_base);\n-\t\tparams.enc_type = RIST_ADV_TYPE_DIRECT;\n-\t\tparams.psk_mode = RIST_ADV_PSK_NONE;\n-\t\tparams.lpc_mode = RIST_ADV_LPC_NONE;\n-\t\tparams.first_frag = true;\n-\t\tparams.last_frag = true;\n-\t\tparams.expedite = false;\n-\t\tparams.retransmit = retry;\n-\n-\t\tstruct rist_adv_flow_id fid;\n-\t\tif (dst_port || src_port) {\n-\t\t\tfid.outer = htons(dst_port);\n-\t\t\tfid.inner_hi = (uint8_t)((src_port >> 4) & 0xFF);\n-\t\t\tfid.inner_lo_sub = (uint8_t)((src_port & 0x0F) << 4);\n-\t\t\tparams.flow_id = &fid;\n-\t\t}\n-\n-\t\tuint8_t *wire_payload = payload;\n-\t\tsize_t wire_payload_len = payload_len;\n-\t\tuint8_t lz4_buf[RIST_MAX_PACKET_SIZE];\n-\t\tuint8_t enc_buf[RIST_MAX_PACKET_SIZE];\n-\t\tuint8_t psk_iv_bytes[RIST_ADV_PSK_IV_SIZE];\n-\n-\t\tif (p->compression && payload_len > 0) {\n-\t\t\tint bound = LZ4_compressBound((int)payload_len);\n-\t\t\tif (bound > 0 && (size_t)bound <= sizeof(lz4_buf)) {\n-\t\t\t\tint clen = LZ4_compress_default((const char *)payload,\n-\t\t\t\t\t(char *)lz4_buf, (int)payload_len, (int)sizeof(lz4_buf));\n-\t\t\t\tif (clen > 0 && (size_t)clen < payload_len) {\n-\t\t\t\t\twire_payload = lz4_buf;\n-\t\t\t\t\twire_payload_len = (size_t)clen;\n-\t\t\t\t\tparams.lpc_mode = RIST_ADV_LPC_LZ4;\n-\t\t\t\t}\n-\t\t\t}\n-\t\t}\n-\n-\t\tif (p->key_tx.key_size > 0) {\n-\t\t\tuint32_t seq_nbe = htobe32(seq_rtp);\n-\t\t\tparams.psk_mode = RIST_ADV_PSK_AES_CTR;\n-\t\t\tuint8_t old_nonce[4];\n-\t\t\tmemcpy(old_nonce, p->key_tx.gre_nonce, 4);\n-\t\t\t_librist_crypto_psk_encrypt(&p->key_tx, seq_nbe, 1,\n-\t\t\t                            wire_payload, enc_buf, wire_payload_len);\n-\t\t\tif (p->key_tx.csprng_failed) {\n-\t\t\t\trist_log_priv(ctx, RIST_LOG_ERROR,\n-\t\t\t\t\t\"Advanced Profile: PSK encrypt failed (CSPRNG)\\n\");\n-\t\t\t\treturn 0;\n-\t\t\t}\n-\t\t\tif (memcmp(old_nonce, p->key_tx.gre_nonce, 4) != 0)\n-\t\t\t\trist_adv_send_psk_nonce(p, p->key_tx.gre_nonce,\n-\t\t\t\t                        (uint16_t)p->key_tx.key_size);\n-\t\t\twire_payload = enc_buf;\n-\t\t\tparams.psk_nonce = p->key_tx.gre_nonce;\n-\t\t\tmemcpy(psk_iv_bytes, &seq_nbe, sizeof(seq_nbe));\n-\t\t\tparams.psk_iv = psk_iv_bytes;\n-\t\t}\n-\n-\t\tint total = rist_adv_build(adv_buf, &params, wire_payload, wire_payload_len);\n-\t\tif (total < 0) {\n-\t\t\trist_log_priv(ctx, RIST_LOG_ERROR, \"Advanced Profile: failed to build packet\\n\");\n-\t\t\treturn 0;\n-\t\t}\n-\n-\t\tif (RIST_UNLIKELY((p->sender_ctx && p->sender_ctx->simulate_loss) ||\n-\t\t                   (p->receiver_ctx && p->receiver_ctx->simulate_loss))) {\n-\t\t\tuint16_t loss_percentage = p->sender_ctx ?\n-\t\t\t\tp->sender_ctx->loss_percentage : p->receiver_ctx->loss_percentage;\n-\t\t\tuint16_t compare = rand() % 1001;\n-\t\t\tif (compare <= loss_percentage) {\n-\t\t\t\tret = total;\n-\t\t\t\tgoto adv_out;\n-\t\t\t}\n-\t\t}\n-\n-\t\tint retries_count = 0;\n-\t\tint errorcode = 0;\n-\t\tdo {\n-\t\t\tret = rist_transport_sendto(p, adv_buf, (size_t)total, 0);\n-\t\t\tif (RIST_UNLIKELY(ret < 0)) {\n-\t\t\t\terrorcode = errno;\n-\t\t\t\tretries_count++;\n-\t\t\t} else {\n-\t\t\t\terrorcode = 0;\n-\t\t\t\tbreak;\n-\t\t\t}\n-\t\t} while (errorcode == EAGAIN && retries_count < RIST_MAX_SEND_RETRIES);\n-\n-\t\tif (RIST_UNLIKELY(ret < 0))\n-\t\t\t_librist_log_send_error(p, errorcode, (size_t)total, \"advanced-profile sendto\");\n-\n-adv_out:\n-\t\tif (ret > 0) {\n-\t\t\tp->stats_sender_instant.sent++;\n-\t\t\tp->stats_sender_instant.sent_bytes += (uint64_t)ret;\n-\t\t\tif (ts_null_bytes)\n-\t\t\t\tp->stats_sender_instant.ts_null++;\n-\t\t\tp->stats_receiver_instant.sent_rtcp++;\n-\t\t\trist_calculate_bitrate((size_t)ret, &p->bw);\n-\t\t\trist_calculate_bitrate(ts_null_bytes, &p->ts_nulls_bw);\n-\t\t}\n-\t\treturn (size_t)ret;\n-\t}\n-\n \t// TODO: write directly on the payload to make it faster\n \tuint8_t header_buf[RIST_MAX_HEADER_SIZE] = {0};\n \tuint16_t proto_type;\n@@ -271,7 +107,7 @@ adv_out:\n \tif (ctx->profile == RIST_PROFILE_SIMPLE) {\n \t\t// retry when kernel buffer is full instead of dropping packet (EAGAIN)\n \t\tdo {\n-\t\t\tret = rist_transport_sendto(p, data, len, 0);\n+\t\t\tret = sendto(p->sd,(const char*)data, len, 0, &(p->u.address), p->address_len);\n \t\t\tif (RIST_UNLIKELY(ret < 0))\n \t\t\t{\n \t\t\t\terrorcode = errno;\n@@ -284,21 +120,15 @@ adv_out:\n \t\t} while (errorcode == EAGAIN && retries < RIST_MAX_SEND_RETRIES);\n \t\tif (RIST_UNLIKELY(retries > (RIST_MAX_SEND_RETRIES / 5)))\n \t\t\trist_log_priv(ctx, RIST_LOG_WARN, \"UDP Pacing Send Succeded after retries=%d, ret=%d, socket=%d\\n\", retries, ret, p->sd);\n-\t\tif (RIST_UNLIKELY(ret < 0))\n-\t\t\t_librist_log_send_error(p, errorcode, len, \"simple-profile sendto\");\n \t}\n \telse\n \t\tret = _librist_proto_gre_send_data(p, payload_type, proto_type, data, len, src_port, dst_port, p->rist_gre_version);\n \n out:\n-\tif (RIST_UNLIKELY(ret <= 0 && ctx->profile == RIST_PROFILE_SIMPLE && errorcode == 0)) {\n-\t\t/* Generic safety net for ret == 0 or ret < 0 without errorcode\n-\t\t * captured (out-of-band failure paths). PMTU-aware send errors\n-\t\t * have already been logged above. */\n+\tif (RIST_UNLIKELY(ret <= 0)) {\n \t\trist_log_priv(ctx, RIST_LOG_ERROR, \"\\tSend failed: errno=%d, ret=%d, socket=%d\\n\", errno, ret, p->sd);\n-\t} else if (ret > 0) {\n+\t} else {\n \t\tp->stats_sender_instant.sent++;\n-\t\tp->stats_sender_instant.sent_bytes += (uint64_t)ret;\n \t\tif (ts_null_bytes)\n \t\t\tp->stats_sender_instant.ts_null++;\n \t\tp->stats_receiver_instant.sent_rtcp++;\n@@ -332,7 +162,7 @@ int rist_send_common_rtcp(struct rist_peer *p, uint8_t payload_type, uint8_t *pa\n \tif (RIST_UNLIKELY(p->config.timing_mode == RIST_TIMING_MODE_ARRIVAL) && !p->receiver_mode)\n \t\tsource_time = timestampNTP_u64();\n \n-\tsize_t ret = rist_send_seq_rtcp(p, seq_rtp, payload_type, payload, payload_len, source_time, src_port, dst_port, false, ts_null_bytes);\n+\tsize_t ret = rist_send_seq_rtcp(p, (uint16_t)seq_rtp, payload_type, payload, payload_len, source_time, src_port, dst_port, false, ts_null_bytes);\n \n \tif ((!p->compression && ret < payload_len) || ret == (size_t)-1)\n \t{\n@@ -351,19 +181,9 @@ int rist_send_common_rtcp(struct rist_peer *p, uint8_t payload_type, uint8_t *pa\n \t}\n \telse\n \t{\n-\t\t/* Advanced-profile media is already bitrate-accounted in\n-\t\t * rist_send_seq_rtcp (it builds and sends before this shared\n-\t\t * path); counting it again here would double-count p->bw. */\n-\t\tbool adv_media_accounted = (cctx->profile == RIST_PROFILE_ADVANCED &&\n-\t\t\tp->remote_supports_advanced &&\n-\t\t\tpayload_type != RIST_PAYLOAD_TYPE_DATA_OOB &&\n-\t\t\tpayload_type != RIST_PAYLOAD_TYPE_RTCP &&\n-\t\t\tpayload_type != RIST_PAYLOAD_TYPE_RTCP_NACK);\n-\t\tif (!adv_media_accounted) {\n-\t\t\t// update bandwidth value\n-\t\t\trist_calculate_bitrate(ret, &p->bw);\n-\t\t\trist_calculate_bitrate(ts_null_bytes, &p->ts_nulls_bw);\n-\t\t}\n+\t\t// update bandwidth value\n+\t\trist_calculate_bitrate(ret, &p->bw);\n+\t\trist_calculate_bitrate(ts_null_bytes, &p->ts_nulls_bw);\n \t}\n \n \t// TODO:\n@@ -379,7 +199,7 @@ int rist_send_common_rtcp(struct rist_peer *p, uint8_t payload_type, uint8_t *pa\n int rist_set_url(struct rist_peer *peer)\n {\n \tchar host[512];\n-\tuint16_t port = 0;\n+\tuint16_t port;\n \tint local;\n \tif (!peer->url) {\n \t\tif (peer->local_port > 0) {\n@@ -490,7 +310,7 @@ void rist_create_socket(struct rist_peer *peer)\n \n \tif (peer->listening) {\n \t\tconst char* host;\n-\t\tuint16_t port = 0;\n+\t\tuint16_t port;\n \n \t\tchar buffer[256];\n \t\tif (peer->u.address.sa_family == AF_INET) {\n@@ -518,8 +338,7 @@ void rist_create_socket(struct rist_peer *peer)\n \t\t\tpeer->multicast_receiver = IN6_IS_ADDR_MULTICAST(&addrv6->sin6_addr);\n \t\t}\n \n-\t\tpeer->sd = udpsocket_open_bind_mcast(host, port, peer->miface,\n-\t\t\tpeer->config.multicast_ttl, peer->config.multicast_source);\n+\t\tpeer->sd = udpsocket_open_bind(host, port, peer->miface);\n \t\tif (peer->sd >= 0) {\n \t\t\tif (port == 0)\n \t\t\t{\n@@ -565,6 +384,7 @@ void rist_create_socket(struct rist_peer *peer)\n \t\t}\n \t\t// We use sendto ... so, no need to connect directly here\n \t\tpeer->sd = udpsocket_open(peer->address_family);\n+\t\t// TODO : set max hops\n \t\tif (peer->sd >= 0)\n \t\t\trist_log_priv(get_cctx(peer), RIST_LOG_INFO, \"Starting in URL connect mode (%d)\\n\", peer->sd);\n \t\telse {\n@@ -575,24 +395,15 @@ void rist_create_socket(struct rist_peer *peer)\n \t\t\trist_log_priv(get_cctx(peer), RIST_LOG_INFO, \"Binding socket to %s\\n\", peer->miface);\n \t\t\tif (inet_pton(AF_INET, peer->miface,  &((struct sockaddr_in *)&ss)->sin_addr) != 0) {\n \t\t\t\t((struct sockaddr_in *)&ss)->sin_family = AF_INET;\n-\t\t\t\t((struct sockaddr_in *)&ss)->sin_port = htons(peer->config.local_port);\n \t\t\t\tif (bind(peer->sd, (struct sockaddr*)&ss, sizeof(struct sockaddr_in)) != 0) {\n-#ifdef _WIN32\n-\t\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_ERROR, \"Couldn't bind to %s:%u: WSAGetLastError=%d\\n\", peer->miface, peer->config.local_port, WSAGetLastError());\n-#else\n-\t\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_ERROR, \"Couldn't bind to %s:%u: %s\\n\", peer->miface, peer->config.local_port, strerror(errno));\n-#endif\n+\t\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_ERROR, \"Couldn't bind to %s: %s\\n\", peer->miface, strerror(errno));\n \t\t\t\t}\n \t\t\t}\n \t\t\telse if (inet_pton(AF_INET6, peer->miface, &((struct sockaddr_in6 *)&ss)->sin6_addr) != 0) {\n \t\t\t\t((struct sockaddr_in6 *)&ss)->sin6_family = AF_INET6;\n-\t\t\t\t((struct sockaddr_in6 *)&ss)->sin6_port = htons(peer->config.local_port);\n+\t\t\t\t((struct sockaddr_in6 *)&ss)->sin6_port =0;\n \t\t\t\tif (bind(peer->sd, (struct sockaddr*)&ss, sizeof(struct sockaddr_in6)) != 0) {\n-#ifdef _WIN32\n-\t\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_ERROR, \"Couldn't bind to %s:%u: WSAGetLastError=%d\\n\", peer->miface, peer->config.local_port, WSAGetLastError());\n-#else\n-\t\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_ERROR, \"Couldn't bind to %s:%u: %s\\n\", peer->miface, peer->config.local_port, strerror(errno));\n-#endif\n+\t\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_ERROR, \"Couldn't bind to %s: %s\\n\", peer->miface, strerror(errno));\n \t\t\t\t}\n \t\t\t}\n #ifdef __linux__\n@@ -621,35 +432,8 @@ void rist_create_socket(struct rist_peer *peer)\n \t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_ERROR, \"No method available to bind to %s please supply an IP to bind to\\n\", peer->miface);\n \t\t\t}\n #endif\n-\t\t} else if (peer->config.local_port > 0) {\n-\t\t\tstruct sockaddr_storage ss = {0};\n-\t\t\tif (peer->address_family == AF_INET6) {\n-\t\t\t\t((struct sockaddr_in6 *)&ss)->sin6_family = AF_INET6;\n-\t\t\t\t((struct sockaddr_in6 *)&ss)->sin6_port = htons(peer->config.local_port);\n-\t\t\t\t((struct sockaddr_in6 *)&ss)->sin6_addr = in6addr_any;\n-\t\t\t} else {\n-\t\t\t\t((struct sockaddr_in *)&ss)->sin_family = AF_INET;\n-\t\t\t\t((struct sockaddr_in *)&ss)->sin_port = htons(peer->config.local_port);\n-\t\t\t\t((struct sockaddr_in *)&ss)->sin_addr.s_addr = INADDR_ANY;\n-\t\t\t}\n-\t\t\tsocklen_t ss_len = peer->address_family == AF_INET6 ? sizeof(struct sockaddr_in6) : sizeof(struct sockaddr_in);\n-\t\t\tif (bind(peer->sd, (struct sockaddr *)&ss, ss_len) != 0) {\n-#ifdef _WIN32\n-\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_WARN, \"Couldn't bind to local port %u: WSAGetLastError=%d\\n\", peer->config.local_port, WSAGetLastError());\n-#else\n-\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_WARN, \"Couldn't bind to local port %u: %s\\n\", peer->config.local_port, strerror(errno));\n-#endif\n-\t\t\t} else {\n-\t\t\t\trist_log_priv(get_cctx(peer), RIST_LOG_INFO, \"Bound caller socket to local port %u\\n\", peer->config.local_port);\n-\t\t\t}\n \t\t}\n-\t\tif (peer->multicast_sender && peer->config.multicast_ttl > 0)\n-\t\t\tudpsocket_set_mcast_ttl(peer->sd, peer->address_family, peer->config.multicast_ttl);\n-\t\tudpsocket_set_dontfragment(peer->sd, peer->address_family);\n-\t\tif (peer->config.local_port > 0)\n-\t\t\tpeer->local_port = peer->config.local_port;\n-\t\telse\n-\t\t\tpeer->local_port = 32768 + (get_cctx(peer)->peer_counter % 28232);\n+\t\tpeer->local_port = 32768 + (get_cctx(peer)->peer_counter % 28232);\n #ifdef _WIN32\n \t\tudpsocket_set_nonblocking(peer->sd);\n #endif\n@@ -699,54 +483,11 @@ int rist_receiver_periodic_rtcp(struct rist_peer *peer) {\n \treturn rist_send_common_rtcp(peer, payload_type, &rtcp_buf[RIST_MAX_PAYLOAD_OFFSET], payload_len, 0, peer->local_port, peer->remote_port, 0, 0);\n }\n \n-static int rist_receiver_send_nacks_advanced(struct rist_peer *peer, uint32_t seq_array[], size_t array_len)\n-{\n-\tstruct rist_common_ctx *ctx = get_cctx(peer);\n-\tuint32_t media_ssrc = rist_adv_ssrc_protected(ctx->adv_ssrc_base);\n-\n-\tif (peer->receiver_ctx->nack_type == RIST_NACK_BITMASK) {\n-\t\tuint32_t pss = seq_array[0];\n-\t\tuint32_t blp = 0;\n-\t\tfor (size_t i = 1; i < array_len; i++) {\n-\t\t\tuint32_t offset = seq_array[i] - pss;\n-\t\t\tif (offset >= 1 && offset <= 32) {\n-\t\t\t\tblp |= (1u << (offset - 1));\n-\t\t\t} else {\n-\t\t\t\trist_adv_send_nack_bitmask(peer, media_ssrc, pss, blp);\n-\t\t\t\tpss = seq_array[i];\n-\t\t\t\tblp = 0;\n-\t\t\t}\n-\t\t}\n-\t\trist_adv_send_nack_bitmask(peer, media_ssrc, pss, blp);\n-\t} else {\n-\t\tuint32_t pss = seq_array[0];\n-\t\tuint32_t nalp = 0;\n-\t\tfor (size_t i = 1; i < array_len; i++) {\n-\t\t\tif (seq_array[i] == pss + nalp + 1) {\n-\t\t\t\tnalp++;\n-\t\t\t} else {\n-\t\t\t\trist_adv_send_nack_range(peer, media_ssrc, pss, nalp);\n-\t\t\t\tpss = seq_array[i];\n-\t\t\t\tnalp = 0;\n-\t\t\t}\n-\t\t}\n-\t\trist_adv_send_nack_range(peer, media_ssrc, pss, nalp);\n-\t}\n-\treturn 0;\n-}\n-\n int rist_receiver_send_nacks(struct rist_peer *peer, uint32_t seq_array[], size_t array_len)\n {\n-\tif (get_cctx(peer)->debug && array_len > 0)\n-\t\trist_log_priv(get_cctx(peer), RIST_LOG_DEBUG, \"Sending %zu nacks starting with %\"PRIu32\"\\n\",\n+\tif (get_cctx(peer)->debug)\n+\t\trist_log_priv(get_cctx(peer), RIST_LOG_DEBUG, \"Sending %d nacks starting with %\"PRIu32\"\\n\",\n \t\tarray_len, seq_array[0]);\n-\n-\tif (get_cctx(peer)->profile == RIST_PROFILE_ADVANCED &&\n-\t    peer->is_advanced && peer->remote_supports_advanced &&\n-\t    array_len > 0) {\n-\t\treturn rist_receiver_send_nacks_advanced(peer, seq_array, array_len);\n-\t}\n-\n \tuint8_t payload_type = RIST_PAYLOAD_TYPE_RTCP;\n \tuint8_t *rtcp_buf = get_cctx(peer)->buf.rtcp;\n \n@@ -754,9 +495,11 @@ int rist_receiver_send_nacks(struct rist_peer *peer, uint32_t seq_array[], size_\n \trist_rtcp_write_empty_rr(rtcp_buf, &payload_len, peer->adv_flow_id);\n \trist_rtcp_write_sdes(rtcp_buf, &payload_len, peer->cname, peer->adv_flow_id);\n \tif (RIST_LIKELY(array_len > 0)) {\n+\t\t// Add nack requests (if any)\n \t\tstruct rist_rtp_nack_record *rec;\n \t\tuint32_t fci_count = 1;\n \n+\t\t// Now the NACK message\n \t\tif (peer->receiver_ctx->nack_type == RIST_NACK_BITMASK)\n \t\t{\n \t\t\tstruct rist_rtcp_nack_bitmask *rtcp = (struct rist_rtcp_nack_bitmask *)(rtcp_buf + RIST_MAX_PAYLOAD_OFFSET + payload_len);\n@@ -907,12 +650,7 @@ int rist_sender_enqueue(struct rist_sender *ctx, const void *data, size_t len, u\n \t}\n \n \tctx->last_datagram_time = datagram_time;\n-\t/* Worst-case write into payload_out is (count - 1) * 204 = 6 * 204 = 1224\n-\t * bytes (if exactly one of the seven 204-byte packets is suppressed),\n-\t * sitting after the 8-byte rist_rtp_hdr_ext at offset 0. The previous\n-\t * sizing of 6 * 204 + 4 was 4 bytes short and let suppress_null_packets\n-\t * smash four bytes of stack past tmp_buf. */\n-\tuint8_t tmp_buf[7 * 204 + sizeof(struct rist_rtp_hdr_ext)];\n+\tuint8_t tmp_buf[6 * 204 + 4];//Max size needed with at least 1 pkt suppressed\n \tint ts_null_bytes = 0;\n \tif (ctx->null_packet_suppression && len <= 7 * 204)\n \t{\n@@ -946,32 +684,16 @@ int rist_sender_enqueue(struct rist_sender *ctx, const void *data, size_t len, u\n \treturn 0;\n }\n \n-/* Share this leg should take in the weighted payload rotation. Normally the\n- * configured weight, but a leg that just rejoined after an RTT mute ramps back\n- * linearly over the rejoin dwell: it drained while muted, so it measures well\n- * until it carries again, and handing back the full share at once simply\n- * refills the queue and mutes it a second later. */\n-static uint32_t rist_peer_effective_weight(const struct rist_peer *peer, uint64_t now)\n-{\n-\t/* Ramp over the rejoin dwell, which is twice the drop dwell. */\n-\treturn rist_rtt_ramped_weight(peer->config.weight, peer->rtt_ramp_start,\n-\t\t\t\t      (uint64_t)peer->config.rtt_drop_settle * RIST_CLOCK * 2,\n-\t\t\t\t      now);\n-}\n-\n void rist_sender_send_data_balanced(struct rist_sender *ctx, struct rist_buffer *buffer)\n {\n \tstruct rist_peer *peer;\n \tstruct rist_peer *selected_peer_by_weight = NULL;\n-\tstruct rist_peer *fallback = NULL;\n-\tuint64_t fallback_rtt = UINT64_MAX;\n \tuint32_t max_remainder = 0;\n \tint peercnt;\n \tbool looped = false;\n \n \t//We can do it safely here, since this function is only to be called once per packet\n \tbuffer->seq = ctx->common.seq++;\n-\tuint32_t wire_seq = (ctx->common.profile == RIST_PROFILE_ADVANCED) ? buffer->seq : (uint32_t)buffer->seq_rtp;\n \tuint64_t now = timestampNTP_u64();\n \n peer_select:\n@@ -985,53 +707,18 @@ peer_select:\n \t\tif (!peer->listening && !peer->multicast_sender && !eap_is_authenticated(peer->eap_ctx))\n \t\t\tcontinue;\n #endif\n-\t\tbool hard_dead = !rist_peer_may_send(peer->dead, peer->dead_since,\n-\t\t\t\t\t\t     peer->recovery_buffer_ticks, now);\n+\t\t/* A peer is \"hard dead\" only after the recovery buffer grace period expires.\n+\t\t * This prevents single-path streams from being interrupted by temporary\n+\t\t * ECHO/RTCP response delays while still eventually stopping if the peer\n+\t\t * is truly gone. */\n+\t\tbool hard_dead = peer->dead && (peer->dead_since + peer->recovery_buffer_ticks) < now;\n \t\tif ((!peer->listening && !peer->authenticated) || hard_dead\n \t\t\t|| (peer->listening && !peer->child_alive_count)) {\n \t\t\tctx->weight_counter -= peer->config.weight;\n \t\t\tif (ctx->weight_counter <= 0) {\n \t\t\t\tctx->weight_counter = ctx->total_weight;\n \t\t\t}\n-\t\t\tpeer->w_count = rist_peer_effective_weight(peer, now);\n-\t\t\tcontinue;\n-\t\t}\n-\n-\t\t/* RTT-muted leg: skipped in the unique-payload rotation. With\n-\t\t * ?rtt-drop-trickle=N, still send a deduped duplicate every Nth packet\n-\t\t * so RTT keeps sampling for a warm restore without stalling the buffer.\n-\t\t * A stalled (briefly silent) leg is skipped too but never trickled: its\n-\t\t * return path is down, so RTCP/keepalive alone probes for recovery.\n-\t\t * The trickle also stops once the leg is queued deeper than the buffer,\n-\t\t * where it can only deliver unusable packets; RTCP keeps measuring RTT,\n-\t\t * and with nothing queued behind them those measurements get honest. */\n-\t\tif (peer->rtt_muted || peer->stalled) {\n-\t\t\tif (peer->rtt_muted && peer->config.rtt_drop_trickle > 0 && !looped && !peer->dead\n-\t\t\t\t&& rist_rtt_trickle_useful(peer->eight_times_rtt / 8,\n-\t\t\t\t\t\t\t   (uint64_t)peer->config.recovery_length_max * RIST_CLOCK)) {\n-\t\t\t\tif (++peer->rtt_trickle_counter >= peer->config.rtt_drop_trickle) {\n-\t\t\t\t\tpeer->rtt_trickle_counter = 0;\n-\t\t\t\t\tuint8_t *payload = buffer->data;\n-\t\t\t\t\trist_send_common_rtcp(peer, buffer->type, &payload[RIST_MAX_PAYLOAD_OFFSET], buffer->size, buffer->source_time, buffer->src_port, buffer->dst_port, wire_seq, buffer->ts_null_bytes);\n-\t\t\t\t}\n-\t\t\t}\n-\t\t\tctx->weight_counter -= peer->config.weight;\n-\t\t\tif (ctx->weight_counter <= 0) {\n-\t\t\t\tctx->weight_counter = ctx->total_weight;\n-\t\t\t}\n-\t\t\tpeer->w_count = rist_peer_effective_weight(peer, now);\n-\t\t\t/* Remember the best skipped leg for the safety net below: a\n-\t\t\t * muted (late but deliverable) leg beats a stalled one (return\n-\t\t\t * path down); lowest RTT breaks ties. */\n-\t\t\tif (!peer->dead) {\n-\t\t\t\tuint64_t s = peer->eight_times_rtt ? peer->eight_times_rtt / 8 : UINT64_MAX;\n-\t\t\t\tif (!fallback\n-\t\t\t\t    || (fallback->stalled && !peer->stalled)\n-\t\t\t\t    || (fallback->stalled == peer->stalled && s < fallback_rtt)) {\n-\t\t\t\t\tfallback = peer;\n-\t\t\t\t\tfallback_rtt = s;\n-\t\t\t\t}\n-\t\t\t}\n+\t\t\tpeer->w_count = peer->config.weight;\n \t\t\tcontinue;\n \t\t}\n \t\tpeercnt++;\n@@ -1042,7 +729,7 @@ peer_select:\n \t\t/* * * * * * * * * * * * * * * * * * */\n \t\t/*************************************/\n \n-\t\tif (peer->config.weight == RIST_PEER_WEIGHT_DUPLICATE && !looped) {\n+\t\tif (peer->config.weight == 0 && !looped) {\n \t\t\tif (peer->listening) {\n \t\t\t\tstruct rist_peer *child = peer->child;\n \t\t\t\twhile (child) {\n@@ -1052,16 +739,15 @@ peer_select:\n \t\t\t\t\t\t//do nothing\n \t\t\t\t\t} else\n #endif\n-\t\t\t\t\tif (child->authenticated && child->is_data && rist_peer_may_send(child->dead, child->dead_since, peer->recovery_buffer_ticks, now)) {\n+\t\t\t\t\tif (child->authenticated && child->is_data && (!child->dead || (child->dead && (child->dead_since + peer->recovery_buffer_ticks) < now))) {\n \t\t\t\t\t\tuint8_t *payload = buffer->data;\n-\t\t\t\t\t\trist_send_common_rtcp(child, buffer->type, &payload[RIST_MAX_PAYLOAD_OFFSET], buffer->size, buffer->source_time, buffer->src_port, buffer->dst_port, wire_seq, buffer->ts_null_bytes);\n+\t\t\t\t\t\trist_send_common_rtcp(child, buffer->type, &payload[RIST_MAX_PAYLOAD_OFFSET], buffer->size, buffer->source_time, buffer->src_port, buffer->dst_port, buffer->seq_rtp, buffer->ts_null_bytes);\n \t\t\t\t\t}\n \t\t\t\t\tchild = child->sibling_next;\n \t\t\t\t}\n-\t\t\t} else {\n-\t\t\t\t/* Eligibility already decided by hard_dead above. */\n+\t\t\t} else if (!peer->dead || (peer->dead && (peer->dead_since + peer->recovery_buffer_ticks) < now)) {\n \t\t\t\tuint8_t *payload = buffer->data;\n-\t\t\t\trist_send_common_rtcp(peer, buffer->type, &payload[RIST_MAX_PAYLOAD_OFFSET], buffer->size, buffer->source_time, buffer->src_port, buffer->dst_port, wire_seq, buffer->ts_null_bytes);\n+\t\t\t\trist_send_common_rtcp(peer, buffer->type, &payload[RIST_MAX_PAYLOAD_OFFSET], buffer->size, buffer->source_time, buffer->src_port, buffer->dst_port, buffer->seq_rtp, buffer->ts_null_bytes);\n \t\t\t}\n \t\t} else {\n \t\t\t/* Election of next peer */\n@@ -1085,16 +771,15 @@ peer_select:\n \t\t\t\t\t\t//do nothing\n \t\t\t\t\t} else\n #endif\n-\t\t\t\tif (child->authenticated && child->is_data && rist_peer_may_send(child->dead, child->dead_since, peer->recovery_buffer_ticks, now)) {\n+\t\t\t\tif (child->authenticated && child->is_data && (!child->dead || (child->dead && (child->dead_since + peer->recovery_buffer_ticks) < now))) {\n \t\t\t\t\tuint8_t *payload = buffer->data;\n-\t\t\t\t\trist_send_common_rtcp(child, buffer->type, &payload[RIST_MAX_PAYLOAD_OFFSET], buffer->size, buffer->source_time, buffer->src_port, buffer->dst_port, wire_seq, buffer->ts_null_bytes);\n+\t\t\t\t\trist_send_common_rtcp(child, buffer->type, &payload[RIST_MAX_PAYLOAD_OFFSET], buffer->size, buffer->source_time, buffer->src_port, buffer->dst_port,  buffer->seq_rtp, buffer->ts_null_bytes);\n \t\t\t\t}\n \t\t\t\tchild = child->sibling_next;\n \t\t\t}\n-\t\t} else {\n-\t\t\t/* Eligibility already decided by hard_dead above. */\n+\t\t} else if (!peer->dead || (peer->dead && (peer->dead_since + peer->recovery_buffer_ticks) < now)) {\n \t\t\tuint8_t *payload = buffer->data;\n-\t\t\trist_send_common_rtcp(peer, buffer->type, &payload[RIST_MAX_PAYLOAD_OFFSET], buffer->size, buffer->source_time, buffer->src_port, buffer->dst_port, wire_seq, buffer->ts_null_bytes);\n+\t\t\trist_send_common_rtcp(peer, buffer->type, &payload[RIST_MAX_PAYLOAD_OFFSET], buffer->size, buffer->source_time, buffer->src_port, buffer->dst_port, buffer->seq_rtp,  buffer->ts_null_bytes);\n \t\t}\n \t\tctx->weight_counter--;\n \t\tpeer->w_count--;\n@@ -1104,37 +789,17 @@ peer_select:\n \t\tpeer = ctx->common.PEERS;\n \t\tctx->weight_counter = ctx->total_weight;\n \t\tfor (; peer; peer = peer->next) {\n-\t\t\tpeer->w_count = rist_peer_effective_weight(peer, now);\n+\t\t\tpeer->w_count = peer->config.weight;\n \t\t}\n \t\tif (!looped && !selected_peer_by_weight && peercnt > 0)\n \t\t\tgoto peer_select;\n \t}\n-\n-\t/* Safety net: every eligible leg was rtt-muted or stalled, so nothing\n-\t * carried this packet. The mute and stall guards run independently and\n-\t * can jointly leave no carrier; never silently drop a live packet when a\n-\t * usable leg exists. Egress on the best skipped leg. */\n-\tif (peercnt == 0 && !selected_peer_by_weight && fallback) {\n-\t\tuint8_t *payload = buffer->data;\n-\t\trist_send_common_rtcp(fallback, buffer->type, &payload[RIST_MAX_PAYLOAD_OFFSET], buffer->size, buffer->source_time, buffer->src_port, buffer->dst_port, wire_seq, buffer->ts_null_bytes);\n-\t}\n }\n \n-static size_t rist_sender_index_get(struct rist_sender *ctx, uint32_t seq,\n-\t\t\t\t     const struct rist_peer *target)\n+static size_t rist_sender_index_get(struct rist_sender *ctx, uint32_t seq)\n {\n-\tif (ctx->common.profile == RIST_PROFILE_ADVANCED) {\n-\t\t/* A peer that negotiated down to Main NACKs in the 16-bit RTP\n-\t\t * sequence domain (nack_seq_msb = 0), which never matches the\n-\t\t * 32-bit advanced seq_index. Serve its retransmits from the\n-\t\t * parallel RTP index. Advanced-negotiated peers keep the 32-bit\n-\t\t * path, so the production Advanced<->Advanced lookup is unchanged. */\n-\t\tif (ctx->seq_rtp_index && target &&\n-\t\t    rist_retx_use_rtp_domain(ctx->common.profile, target->remote_supports_advanced))\n-\t\t\treturn ctx->seq_rtp_index[(uint16_t)seq];\n-\t\treturn ctx->seq_index[seq & (ctx->sender_queue_max - 1)];\n-\t}\n-\treturn ctx->seq_index[(uint16_t)seq];\n+\tsize_t idx = ctx->seq_index[(uint16_t)seq];\n+\treturn idx;\n }\n \n size_t rist_get_sender_retry_queue_size(struct rist_sender *ctx)\n@@ -1144,35 +809,6 @@ size_t rist_get_sender_retry_queue_size(struct rist_sender *ctx)\n \treturn retry_queue_size;\n }\n \n-/* Pick the lowest-RTT healthy bonded sibling in the same sequence domain to\n- * egress a retransmit when the NACK's own leg is RTT-muted or stalled (its\n- * latency/silence would make the recovery packet arrive too late). Returns the\n- * original leg if none qualifies. Caller holds peerlist_lock. */\n-static struct rist_peer *rist_retx_healthy_egress(struct rist_sender *ctx,\n-\t\t\t\t\t\t  struct rist_peer *muted)\n-{\n-\tuint64_t now = timestampNTP_u64();\n-\tstruct rist_peer *best = NULL;\n-\tuint64_t best_rtt = UINT64_MAX;\n-\tfor (struct rist_peer *peer = ctx->common.PEERS; peer; peer = peer->next) {\n-\t\tif (!peer->is_data || peer->parent || peer->listening)\n-\t\t\tcontinue;\n-\t\tif (peer->rtt_muted || peer->stalled || !peer->authenticated)\n-\t\t\tcontinue;\n-\t\tif (!rist_peer_may_send(peer->dead, peer->dead_since, peer->recovery_buffer_ticks, now))\n-\t\t\tcontinue;\n-\t\tstruct rist_peer *egress = peer->peer_data ? peer->peer_data : peer;\n-\t\tif (egress->remote_supports_advanced != muted->remote_supports_advanced)\n-\t\t\tcontinue;\n-\t\tuint64_t rtt = egress->eight_times_rtt ? egress->eight_times_rtt : UINT64_MAX;\n-\t\tif (rtt < best_rtt) {\n-\t\t\tbest_rtt = rtt;\n-\t\t\tbest = egress;\n-\t\t}\n-\t}\n-\treturn best ? best : muted;\n-}\n-\n /* This function must return, 0 when there is nothing to send, < 0 on error and > 0 for bytes sent */\n ssize_t rist_retry_dequeue(struct rist_sender *ctx)\n {\n@@ -1197,24 +833,10 @@ ssize_t rist_retry_dequeue(struct rist_sender *ctx)\n \t\treturn -1;\n \t}\n \n-\t/* The peer that will actually receive this retransmission. Its\n-\t * negotiated profile (remote_supports_advanced) decides both the\n-\t * retransmit framing (rist_send_seq_rtcp) and, for an Advanced sender,\n-\t * which sequence domain to resolve the request in. */\n-\tstruct rist_peer *idx_target = retry->peer->peer_data ? retry->peer->peer_data : retry->peer;\n-\tbool main_domain_retx = rist_retx_use_rtp_domain(ctx->common.profile,\n-\t\t\t\t\t\t\t idx_target->remote_supports_advanced);\n-\n-\t/* Redirect off a muted/stalled leg to a healthy sibling (unchanged when\n-\t * none qualifies, so no behaviour change unless a leg was pulled). */\n-\tstruct rist_peer *egress = idx_target;\n-\tif (idx_target->rtt_muted || idx_target->stalled)\n-\t\tegress = rist_retx_healthy_egress(ctx, idx_target);\n-\n \t// If they request a non-sense seq number, we will catch it when we check the seq number against\n \t// the one on that buffer position and it does not match\n \n-\tsize_t idx = rist_sender_index_get(ctx, retry->seq, idx_target);\n+\tsize_t idx = rist_sender_index_get(ctx, retry->seq);\n \tif (RIST_UNLIKELY(ctx->sender_queue[idx] == NULL)) {\n \t\trist_log_priv(&ctx->common, RIST_LOG_DEBUG,\n \t\t\t\" Couldn't find block %\" PRIu32 \" (i=%zu/r=%zu/w=%zu/d=%zu/rs=%zu), consider increasing the buffer size\\n\",\n@@ -1222,14 +844,11 @@ ssize_t rist_retry_dequeue(struct rist_sender *ctx)\n \t\t\trist_get_sender_retry_queue_size(ctx));\n \t\tretry->peer->stats_sender_instant.retrans_skip++;\n \t\treturn -1;\n-\t} else if (RIST_UNLIKELY(\n-\t\t(ctx->common.profile == RIST_PROFILE_ADVANCED && !main_domain_retx)\n-\t\t\t? (retry->seq != ctx->sender_queue[idx]->seq)\n-\t\t\t: ((uint16_t)retry->seq != ctx->sender_queue[idx]->seq_rtp))) {\n+\t} else if (RIST_UNLIKELY((uint16_t)retry->seq != ctx->sender_queue[idx]->seq_rtp)) {\n \t\trist_log_priv(&ctx->common, RIST_LOG_DEBUG,\n-\t\t\t\" Couldn't find block %\" PRIu32 \" (i=%zu/r=%zu/w=%zu/d=%zu/rs=%zu), found an old one instead %\" PRIu32 \" (%\" PRIu64 \"), bitrate is too high\\n\",\n-\t\t\tretry->seq, idx, atomic_load_explicit(&ctx->sender_queue_read_index, memory_order_acquire), atomic_load_explicit(&ctx->sender_queue_write_index, memory_order_acquire), ctx->sender_queue_delete_index,\n-\t\t\trist_get_sender_retry_queue_size(ctx), ctx->sender_queue[idx]->seq, ctx->sender_queue_max);\n+\t\t\t\" Couldn't find block %\" PRIu16 \" (i=%zu/r=%zu/w=%zu/d=%zu/rs=%zu), found an old one instead %\" PRIu32 \" (%\" PRIu64 \"), bitrate is too high\\n\",\n+\t\t\t(uint16_t)retry->seq, idx, atomic_load_explicit(&ctx->sender_queue_read_index, memory_order_acquire), atomic_load_explicit(&ctx->sender_queue_write_index, memory_order_acquire), ctx->sender_queue_delete_index,\n+\t\t\trist_get_sender_retry_queue_size(ctx), ctx->sender_queue[idx]->seq_rtp, ctx->sender_queue_max);\n \t\tretry->peer->stats_sender_instant.retrans_skip++;\n \t\treturn -1;\n \t}\n@@ -1307,13 +926,12 @@ ssize_t rist_retry_dequeue(struct rist_sender *ctx)\n \n \tuint16_t src_port = buffer->src_port;\n \tif (src_port == 0)\n-\t\tsrc_port = 32768 + egress->adv_peer_id;\n-\tuint32_t retry_wire_seq = (ctx->common.profile == RIST_PROFILE_ADVANCED && !main_domain_retx) ? buffer->seq : (uint32_t)buffer->seq_rtp;\n-\tret = rist_send_seq_rtcp(egress, retry_wire_seq, buffer->type, &payload[RIST_MAX_PAYLOAD_OFFSET], buffer->size, buffer->source_time, src_port, (egress->config.virt_dst_port & ~1UL), true, ctx->sender_queue[idx]->ts_null_bytes);\n+\t\tsrc_port = 32768 + retry->peer->peer_data->adv_peer_id;\n+\tret = rist_send_seq_rtcp(retry->peer->peer_data, buffer->seq_rtp, buffer->type, &payload[RIST_MAX_PAYLOAD_OFFSET], buffer->size, buffer->source_time, src_port, (retry->peer->peer_data->config.virt_dst_port & ~1UL), true, ctx->sender_queue[idx]->ts_null_bytes);\n \t// update bandwidth value\n \trist_calculate_bitrate(ret, retry_bw);\n \n-\tif ((ret == (size_t)-1) || (!egress->compression && ret < buffer->size)) {\n+\tif ((ret == (size_t)-1) || (ret < buffer->size)) {\n \t\trist_log_priv(&ctx->common, RIST_LOG_ERROR,\n \t\t\t\"Resending of packet failed %zu != %zu for seq %\"PRIu32\"\\n\", ret, buffer->size, retry->seq);\n \t\tretry->peer->stats_sender_instant.retrans_skip++;\n@@ -1321,16 +939,17 @@ ssize_t rist_retry_dequeue(struct rist_sender *ctx)\n \t}\n \n \tbuffer->transmit_count++;\n-\tegress->stats_sender_instant.retrans++;\n-\tegress->stats_sender_instant.retransmitted_bytes += (uint64_t)ret;\n+\tif (retry->peer->peer_data)\n+\t\tretry->peer->peer_data->stats_sender_instant.retrans++;\n+\telse\n+\t\tretry->peer->stats_sender_instant.retrans++;\n \treturn ret;\n }\n \n void rist_retry_enqueue(struct rist_sender *ctx, uint32_t seq, struct rist_peer *peer)\n {\n \tuint64_t now = timestampNTP_u64();\n-\tstruct rist_peer *idx_target = peer ? (peer->peer_data ? peer->peer_data : peer) : NULL;\n-\tsize_t idx = rist_sender_index_get(ctx, seq, idx_target);\n+\tsize_t idx = rist_sender_index_get(ctx, seq);\n \tstruct rist_buffer *buffer = ctx->sender_queue[idx];\n \tstruct rist_retry *retry;\n \ndiff --git a/src/udpsocket.c b/src/udpsocket.c\nindex 606e1dd..d3014d4 100644\n--- a/src/udpsocket.c\n+++ b/src/udpsocket.c\n@@ -7,77 +7,16 @@\n \n #include \"librist/udpsocket.h\"\n #include \"log-private.h\"\n-#include \"pthread-shim.h\"\n #ifdef _WIN32\n #include <ws2ipdef.h>\n-#include <mstcpip.h>\n #ifndef MCAST_JOIN_GROUP\n #define MCAST_JOIN_GROUP 41\n #endif\n-#ifndef MCAST_JOIN_SOURCE_GROUP\n-#define MCAST_JOIN_SOURCE_GROUP 45\n-#endif\n-/* SIO_UDP_CONNRESET lives in mstcpip.h but some MinGW SDKs are stale; the\n- * IOCTL code itself is stable (XP+). Provide a fallback definition. */\n-#ifndef SIO_UDP_CONNRESET\n-#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)\n-#endif\n #endif\n \n /* Private functions */\n static const int yes = 1; // no = 0;\n \n-#ifdef _WIN32\n-/* WSAStartup must run before the first socket()/getaddrinfo() call in the\n- * process. init_common_ctx() does it for code paths that go through\n- * rist_sender_create()/rist_receiver_create(), but tools can hit\n- * udpsocket_open_connect() earlier via rist_logging_set() (e.g. when\n- * ristreceiver -r is used to forward stats over UDP). Initialise here on\n- * first use, guarded by InitOnceExecuteOnce / pthread_once so concurrent\n- * callers don't race. WSAStartup is ref-counted by Windows, so the\n- * duplicate call from init_common_ctx (kept for backwards-compat with\n- * out-of-tree callers that link directly against rist-common.c) is\n- * harmless. */\n-\n-#if !defined(_WIN32) || HAVE_PTHREADS\n-#if defined(__GNU__)\n-static pthread_once_t winsock_init_once = {__PTHREAD_ONCE_INIT};\n-#else\n-static pthread_once_t winsock_init_once = PTHREAD_ONCE_INIT;\n-#endif\n-#endif\n-#if defined(_WIN32) && !HAVE_PTHREADS\n-static INIT_ONCE winsock_init_once = INIT_ONCE_STATIC_INIT;\n-#endif\n-\n-#if HAVE_PTHREADS\n-static void _librist_udpsocket_init_winsock_func(void)\n-#else\n-static BOOL WINAPI _librist_udpsocket_init_winsock_func(PINIT_ONCE InitOnce, PVOID Parameter, PVOID *Context)\n-#endif\n-{\n-\tWSADATA wsaData;\n-\tint ret = WSAStartup(MAKEWORD(2, 2), &wsaData);\n-\tif (ret != 0) {\n-\t\trist_log_priv3(RIST_LOG_ERROR, \"WSAStartup failed: %d\\n\", ret);\n-\t}\n-#if !HAVE_PTHREADS\n-\treturn TRUE;\n-#endif\n-}\n-\n-static void _librist_udpsocket_init_winsock(void)\n-{\n-#if HAVE_PTHREADS\n-\tpthread_once(&winsock_init_once, _librist_udpsocket_init_winsock_func);\n-#else\n-\tInitOnceExecuteOnce(&winsock_init_once, _librist_udpsocket_init_winsock_func, NULL, NULL);\n-#endif\n-}\n-#else\n-static inline void _librist_udpsocket_init_winsock(void) { }\n-#endif\n-\n /* Public API */\n \n int udpsocket_resolve_host(const char *host, uint16_t port, struct sockaddr *addr)\n@@ -85,8 +24,6 @@ int udpsocket_resolve_host(const char *host, uint16_t port, struct sockaddr *add\n \tstruct sockaddr_in *a4 = (struct sockaddr_in *)addr;\n \tstruct sockaddr_in6 *a6 = (struct sockaddr_in6 *)addr;\n \n-\t_librist_udpsocket_init_winsock();\n-\n \t/* Pre-check for numeric IPv6 */\n \tif (inet_pton(AF_INET6, host, &a6->sin6_addr) > 0) {\n \t\ta6->sin6_family = AF_INET6;\n@@ -116,142 +53,21 @@ int udpsocket_resolve_host(const char *host, uint16_t port, struct sockaddr *add\n \treturn 0;\n }\n \n-/* Set IP-level \"don't fragment\" so the kernel surfaces EMSGSIZE when our\n- * UDP datagrams exceed the path MTU, instead of silently IP-fragmenting\n- * them and watching the fragments black-hole at an intermediate router.\n- * This matches the behaviour of every other modern UDP transport.\n- *\n- * Best-effort: any platform that does not understand the option keeps the\n- * default (fragmenting) behaviour. We log the failure so it is visible. */\n-void udpsocket_set_dontfragment(int sd, uint16_t af)\n-{\n-#if defined(__linux__) && defined(IP_MTU_DISCOVER) && defined(IP_PMTUDISC_DO)\n-\tif (af == AF_INET) {\n-\t\tint val = IP_PMTUDISC_DO;\n-\t\tif (setsockopt(sd, IPPROTO_IP, IP_MTU_DISCOVER, &val, sizeof(val)) < 0)\n-\t\t\trist_log_priv3(RIST_LOG_WARN,\n-\t\t\t\t\"setsockopt(IP_MTU_DISCOVER=DO) failed: %s; oversized \"\n-\t\t\t\t\"datagrams may be silently IP-fragmented\\n\", strerror(errno));\n-\t}\n-#if defined(IPV6_MTU_DISCOVER) && defined(IPV6_PMTUDISC_DO)\n-\telse if (af == AF_INET6) {\n-\t\tint val = IPV6_PMTUDISC_DO;\n-\t\tif (setsockopt(sd, IPPROTO_IPV6, IPV6_MTU_DISCOVER, &val, sizeof(val)) < 0)\n-\t\t\trist_log_priv3(RIST_LOG_WARN,\n-\t\t\t\t\"setsockopt(IPV6_MTU_DISCOVER=DO) failed: %s\\n\", strerror(errno));\n-\t}\n-#endif\n-#elif defined(_WIN32) && defined(IP_DONTFRAGMENT)\n-\tDWORD val = 1;\n-\tif (af == AF_INET) {\n-\t\tif (setsockopt(sd, IPPROTO_IP, IP_DONTFRAGMENT,\n-\t\t               (const char *)&val, sizeof(val)) == SOCKET_ERROR)\n-\t\t\trist_log_priv3(RIST_LOG_WARN,\n-\t\t\t\t\"setsockopt(IP_DONTFRAGMENT) failed: WSAGetLastError=%d\\n\",\n-\t\t\t\tWSAGetLastError());\n-\t}\n-#if defined(IPV6_DONTFRAG)\n-\telse if (af == AF_INET6) {\n-\t\tif (setsockopt(sd, IPPROTO_IPV6, IPV6_DONTFRAG,\n-\t\t               (const char *)&val, sizeof(val)) == SOCKET_ERROR)\n-\t\t\trist_log_priv3(RIST_LOG_WARN,\n-\t\t\t\t\"setsockopt(IPV6_DONTFRAG) failed: WSAGetLastError=%d\\n\",\n-\t\t\t\tWSAGetLastError());\n-\t}\n-#endif\n-#elif defined(IP_DONTFRAG)\n-\tint val = 1;\n-\tif (af == AF_INET) {\n-\t\tif (setsockopt(sd, IPPROTO_IP, IP_DONTFRAG, &val, sizeof(val)) < 0)\n-\t\t\trist_log_priv3(RIST_LOG_WARN,\n-\t\t\t\t\"setsockopt(IP_DONTFRAG) failed: %s\\n\", strerror(errno));\n-\t}\n-#if defined(IPV6_DONTFRAG)\n-\telse if (af == AF_INET6) {\n-\t\tif (setsockopt(sd, IPPROTO_IPV6, IPV6_DONTFRAG, &val, sizeof(val)) < 0)\n-\t\t\trist_log_priv3(RIST_LOG_WARN,\n-\t\t\t\t\"setsockopt(IPV6_DONTFRAG) failed: %s\\n\", strerror(errno));\n-\t}\n-#endif\n-#else\n-\t(void)sd; (void)af;\n-#endif\n-}\n-\n-int udpsocket_set_mcast_ttl(int sd, uint16_t af, uint32_t ttl)\n-{\n-\tif (ttl == 0)\n-\t\treturn 0;\n-\tif (af == AF_INET6) {\n-#ifdef IPV6_MULTICAST_HOPS\n-\t\tint hops = (int)ttl;\n-\t\tif (setsockopt(sd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, (const char *)&hops, sizeof(hops)) < 0) {\n-#ifdef _WIN32\n-\t\t\trist_log_priv3(RIST_LOG_WARN, \"setsockopt(IPV6_MULTICAST_HOPS) failed: WSAGetLastError=%d\\n\", WSAGetLastError());\n-#else\n-\t\t\trist_log_priv3(RIST_LOG_WARN, \"setsockopt(IPV6_MULTICAST_HOPS) failed: %s\\n\", strerror(errno));\n-#endif\n-\t\t\treturn -1;\n-\t\t}\n-#else\n-\t\t(void)sd; (void)ttl;\n-#endif\n-\t} else {\n-#ifdef IP_MULTICAST_TTL\n-\t\tint val = (int)ttl;\n-\t\tif (setsockopt(sd, IPPROTO_IP, IP_MULTICAST_TTL, (const char *)&val, sizeof(val)) < 0) {\n-#ifdef _WIN32\n-\t\t\trist_log_priv3(RIST_LOG_WARN, \"setsockopt(IP_MULTICAST_TTL) failed: WSAGetLastError=%d\\n\", WSAGetLastError());\n-#else\n-\t\t\trist_log_priv3(RIST_LOG_WARN, \"setsockopt(IP_MULTICAST_TTL) failed: %s\\n\", strerror(errno));\n-#endif\n-\t\t\treturn -1;\n-\t\t}\n-#else\n-\t\t(void)sd; (void)ttl;\n-#endif\n-\t}\n-\treturn 0;\n-}\n-\n int udpsocket_open(uint16_t af)\n {\n-\t_librist_udpsocket_init_winsock();\n \tint sd = socket(af, SOCK_DGRAM, 0);\n \tif (sd < 0) {\n #ifdef _WIN32\n \t\tsd = -1 * WSAGetLastError();\n #endif\n-\t\treturn sd;\n \t}\n-#ifdef _WIN32\n-\t/* Disable the synthetic WSAECONNRESET indication that Windows enqueues\n-\t * on a UDP socket when an outbound packet provokes an ICMP\n-\t * port-unreachable reply. The indication has a long-standing data-loss\n-\t * footgun: the error sits in the recv queue and, on some orderings,\n-\t * the next recvfrom() drains a real datagram alongside it (or returns\n-\t * WSAEMSGSIZE and silently consumes one whole datagram). librist\n-\t * already detects peer loss via RTCP keepalive timeouts, so we don't\n-\t * need this signal. See rist/librist#209. */\n-\tBOOL connreset_new_behavior = FALSE;\n-\tDWORD connreset_bytes = 0;\n-\tif (WSAIoctl(sd, SIO_UDP_CONNRESET,\n-\t             &connreset_new_behavior, sizeof(connreset_new_behavior),\n-\t             NULL, 0, &connreset_bytes, NULL, NULL) == SOCKET_ERROR) {\n-\t\trist_log_priv3(RIST_LOG_WARN,\n-\t\t\t\"WSAIoctl(SIO_UDP_CONNRESET, FALSE) failed: WSAGetLastError=%d; \"\n-\t\t\t\"WSAECONNRESET indications remain enabled\\n\",\n-\t\t\tWSAGetLastError());\n-\t}\n-#endif\n \treturn sd;\n }\n \n int udpsocket_set_optimal_buffer_size(int sd)\n {\n-\t/* Request the OS-allowed maximum; the kernel clamps to net.core.rmem_max. */\n+\tuint32_t bufsize = UDPSOCKET_SOCK_BUFSIZE;\n \tuint32_t current_recvbuf = udpsocket_get_buffer_size(sd);\n-\tuint32_t bufsize = UDPSOCKET_SOCK_BUFSIZE_MAX;\n \tif (current_recvbuf < bufsize){\n \t\tsetsockopt(sd, SOL_SOCKET, SO_RCVBUF, (char *)&bufsize, sizeof(uint32_t));\n \t\tcurrent_recvbuf = udpsocket_get_buffer_size(sd);\n@@ -262,8 +78,7 @@ int udpsocket_set_optimal_buffer_size(int sd)\n \t\t}\n #endif\n \t}\n-\t/* A clamped result above the 1 MB floor is fine; only fall back below it. */\n-\tif (current_recvbuf < UDPSOCKET_SOCK_BUFSIZE){\n+\tif (current_recvbuf < bufsize){\n \t\t// Settle for a smaller size\n \t\tbufsize = UDPSOCKET_SOCK_BUFSIZE/5;\n \t\tsetsockopt(sd, SOL_SOCKET, SO_RCVBUF, (char *)&bufsize, sizeof(uint32_t));\n@@ -274,19 +89,18 @@ int udpsocket_set_optimal_buffer_size(int sd)\n \t\t\tcurrent_recvbuf = udpsocket_get_buffer_size(sd);\n \t\t}\n #endif\n-\t\tif (current_recvbuf < bufsize){\n-\t\t\trist_log_priv3( RIST_LOG_ERROR, \"Your UDP receive buffer is set < 200 kbytes (%\"PRIu32\") and the kernel denied our request for an increase. It's recommended to set your net.core.rmem_max setting to at least 200 kbyte for best results.\", current_recvbuf);\n-\t\t\treturn -1;\n-\t\t}\n+\t}\n+\tif (current_recvbuf < bufsize){\n+\t\trist_log_priv3( RIST_LOG_ERROR, \"Your UDP receive buffer is set < 200 kbytes (%\"PRIu32\") and the kernel denied our request for an increase. It's recommended to set your net.core.rmem_max setting to at least 200 kbyte for best results.\", current_recvbuf);\n+\t\treturn -1;\n \t}\n \treturn 0;\n }\n \n int udpsocket_set_optimal_buffer_send_size(int sd)\n {\n-\t/* Mirror the recv path: request the OS max, clamp to net.core.wmem_max. */\n+\tuint32_t bufsize = UDPSOCKET_SOCK_BUFSIZE;\n \tuint32_t current_sendbuf = udpsocket_get_buffer_send_size(sd);\n-\tuint32_t bufsize = UDPSOCKET_SOCK_BUFSIZE_MAX;\n \tif (current_sendbuf < bufsize){\n \t\tsetsockopt(sd, SOL_SOCKET, SO_SNDBUF, (char *)&bufsize, sizeof(uint32_t));\n \t\tcurrent_sendbuf = udpsocket_get_buffer_send_size(sd);\n@@ -297,7 +111,7 @@ int udpsocket_set_optimal_buffer_send_size(int sd)\n \t\t}\n #endif\n \t}\n-\tif (current_sendbuf < UDPSOCKET_SOCK_BUFSIZE){\n+\tif (current_sendbuf < bufsize){\n \t\t// Settle for a smaller size\n \t\tbufsize = UDPSOCKET_SOCK_BUFSIZE/5;\n \t\tsetsockopt(sd, SOL_SOCKET, SO_SNDBUF, (char *)&bufsize, sizeof(uint32_t));\n@@ -308,10 +122,10 @@ int udpsocket_set_optimal_buffer_send_size(int sd)\n \t\t\tcurrent_sendbuf = udpsocket_get_buffer_send_size(sd);\n \t\t}\n #endif\n-\t\tif (current_sendbuf < bufsize){\n-\t\t\trist_log_priv3( RIST_LOG_ERROR, \"Your UDP send buffer is set < 200 kbytes (%\"PRIu32\") and the kernel denied our request for an increase. It's recommended to set your net.core.wmem_max setting to at least 200 kbyte for best results.\", current_sendbuf);\n-\t\t\treturn -1;\n-\t\t}\n+\t}\n+\tif (current_sendbuf < bufsize){\n+\t\trist_log_priv3( RIST_LOG_ERROR, \"Your UDP send buffer is set < 200 kbytes (%\"PRIu32\") and the kernel denied our request for an increase. It's recommended to set your net.core.rmem_max setting to at least 200 kbyte for best results.\", current_sendbuf);\n+\t\treturn -1;\n \t}\n \treturn 0;\n }\n@@ -372,102 +186,37 @@ int udpsocket_set_mcast_iface(int sd, const char *mciface, uint16_t family)\n }\n \n bool is_ip_address(const char *ipaddress, int family) {\n-\tunion {\n-\t\tstruct in_addr v4;\n-\t\tstruct in6_addr v6;\n-\t} buf;\n-\treturn inet_pton(family, ipaddress, &buf) == 1;\n+\tstruct sockaddr_in sa;\n+\tint result = inet_pton(family, ipaddress, &(sa.sin_addr));\n+\treturn result == 1;\n }\n \n-int udpsocket_join_mcast_group(int sd, const char* miface, struct sockaddr* sa, uint16_t family, const char *ssm_source) {\n+int udpsocket_join_mcast_group(int sd, const char* miface, struct sockaddr* sa, uint16_t family) {\n+\tif (family != AF_INET)\n+\t\treturn -1;\n+\tchar address[INET6_ADDRSTRLEN];\n \tchar mcastaddress[INET6_ADDRSTRLEN];\n+\tstruct sockaddr_in *mcast_v4 = (struct sockaddr_in *)sa;\n+\tinet_ntop(AF_INET, &(mcast_v4->sin_addr), mcastaddress, INET_ADDRSTRLEN);\n \tuint32_t src_addr = htonl(INADDR_ANY);\n \tint ifindex = 0;\n \n-\tif (miface != NULL && miface[0] != '\\0') {\n-\t\tif (is_ip_address(miface, AF_INET)) {\n-\t\t\tinet_pton(AF_INET, miface, &src_addr);\n-\t\t} else {\n+\tif (is_ip_address(miface, AF_INET))\t{\n+\t\tinet_pton(AF_INET, miface, &src_addr);\n+\t} else if (miface != NULL && miface[0] != '\\0') {\n #ifndef _WIN32\n-\t\t\tifindex = if_nametoindex(miface);\n+\t\tifindex = if_nametoindex(miface);\n #else\n-\t\t\tifindex = atoi(miface);\n+\t\tifindex = atoi(miface);\n #endif\n-\t\t\tif (!ifindex) {\n-\t\t\t\trist_log_priv3(RIST_LOG_ERROR, \"Failed to get interface index error: %s\\n\", strerror(errno));\n-\t\t\t\trist_log_priv3(RIST_LOG_INFO, \"Falling back to joining via default route\\n\");\n-\t\t\t}\n+\t\tif (!ifindex) {\n+\t\t\trist_log_priv3(RIST_LOG_ERROR, \"Failed to get interface index error: %s\\n\", strerror(errno));\n+\t\t\trist_log_priv3(RIST_LOG_INFO, \"Falling back to joining via default route\\n\");\n \t\t}\n \t}\n-\n-\tif (family == AF_INET6) {\n-\t\tstruct sockaddr_in6 *mcast_v6 = (struct sockaddr_in6 *)sa;\n-\t\tinet_ntop(AF_INET6, &mcast_v6->sin6_addr, mcastaddress, INET6_ADDRSTRLEN);\n-#ifdef MCAST_JOIN_GROUP\n-\t\tstruct group_req gr;\n-\t\tmemset(&gr, 0, sizeof(gr));\n-\t\tgr.gr_interface = ifindex;\n-\t\tmemcpy(&gr.gr_group, sa, sizeof(struct sockaddr_in6));\n-\t\trist_log_priv3(RIST_LOG_INFO, \"Joining IPv6 multicast address: %s\\n\", mcastaddress);\n-\t\tif (setsockopt(sd, IPPROTO_IPV6, MCAST_JOIN_GROUP, (const char *)&gr, sizeof(gr)) == 0)\n-\t\t\treturn 0;\n-#endif\n-#ifdef IPV6_JOIN_GROUP\n-\t\t{\n-\t\t\tstruct ipv6_mreq mreq6;\n-\t\t\tmemcpy(&mreq6.ipv6mr_multiaddr, &mcast_v6->sin6_addr, sizeof(mreq6.ipv6mr_multiaddr));\n-\t\t\tmreq6.ipv6mr_interface = ifindex;\n-\t\t\tif (setsockopt(sd, IPPROTO_IPV6, IPV6_JOIN_GROUP, (const char *)&mreq6, sizeof(mreq6)) < 0) {\n-#ifdef _WIN32\n-\t\t\t\trist_log_priv3(RIST_LOG_ERROR, \"Failed to join IPv6 multicast group: WSAGetLastError=%d\\n\", WSAGetLastError());\n-#else\n-\t\t\t\trist_log_priv3(RIST_LOG_ERROR, \"Failed to join IPv6 multicast group: %s\\n\", strerror(errno));\n-#endif\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\t\t\treturn 0;\n-\t\t}\n-#endif\n-\t\trist_log_priv3(RIST_LOG_ERROR, \"IPv6 multicast join not supported on this platform\\n\");\n-\t\treturn -1;\n-\t}\n-\n-\t/* IPv4 path */\n-\tstruct sockaddr_in *mcast_v4 = (struct sockaddr_in *)sa;\n-\tinet_ntop(AF_INET, &(mcast_v4->sin_addr), mcastaddress, INET_ADDRSTRLEN);\n-\n-\t/* SSM (source-specific multicast) when ssm_source is provided */\n-\tif (ssm_source != NULL && ssm_source[0] != '\\0') {\n-#ifdef IP_ADD_SOURCE_MEMBERSHIP\n-\t\tstruct ip_mreq_source mreqs;\n-\t\tmemset(&mreqs, 0, sizeof(mreqs));\n-\t\tmreqs.imr_multiaddr = mcast_v4->sin_addr;\n-\t\tmreqs.imr_interface.s_addr = src_addr;\n-\t\tif (inet_pton(AF_INET, ssm_source, &mreqs.imr_sourceaddr) != 1) {\n-\t\t\trist_log_priv3(RIST_LOG_ERROR, \"Invalid SSM source address: %s\\n\", ssm_source);\n-\t\t\treturn -1;\n-\t\t}\n-\t\trist_log_priv3(RIST_LOG_INFO, \"SSM join: group %s source %s\\n\", mcastaddress, ssm_source);\n-\t\tif (setsockopt(sd, IPPROTO_IP, IP_ADD_SOURCE_MEMBERSHIP, (const char *)&mreqs, sizeof(mreqs)) < 0) {\n-#ifdef _WIN32\n-\t\t\trist_log_priv3(RIST_LOG_ERROR, \"Failed to join SSM group (IP_ADD_SOURCE_MEMBERSHIP): WSAGetLastError=%d\\n\", WSAGetLastError());\n-#else\n-\t\t\trist_log_priv3(RIST_LOG_ERROR, \"Failed to join SSM group (IP_ADD_SOURCE_MEMBERSHIP): %s\\n\", strerror(errno));\n-#endif\n-\t\t\treturn -1;\n-\t\t}\n-\t\treturn 0;\n-#else\n-\t\trist_log_priv3(RIST_LOG_ERROR, \"SSM (source-specific multicast) not supported on this platform\\n\");\n-\t\treturn -1;\n-#endif\n-\t}\n-\n-\t/* ASM (any-source multicast) */\n #ifdef MCAST_JOIN_GROUP\n \tif (ifindex) {\n \t\tstruct group_req gr;\n-\t\tmemset(&gr, 0, sizeof(gr));\n \t\tgr.gr_interface = ifindex;\n \t\tmemcpy(&gr.gr_group, mcast_v4, sizeof(*mcast_v4));\n \t\trist_log_priv3(RIST_LOG_INFO, \"Joining multicast address: %s with %s\\n\", mcastaddress, miface);\n@@ -476,19 +225,19 @@ int udpsocket_join_mcast_group(int sd, const char* miface, struct sockaddr* sa,\n \t\t}\n \t}\n #endif\n-\t{\n-\t\tchar address[INET6_ADDRSTRLEN];\n-\t\tinet_ntop(AF_INET, &(src_addr), address, INET_ADDRSTRLEN);\n-\t\trist_log_priv3(RIST_LOG_INFO, \"Joining multicast address: %s from IP %s\\n\", mcastaddress, address);\n-\t}\n+\tinet_ntop(AF_INET, &(src_addr), address, INET_ADDRSTRLEN);\n+\trist_log_priv3(RIST_LOG_INFO, \"Joining multicast address: %s from IP %s\\n\", mcastaddress, address);\n \tstruct ip_mreq group;\n \tgroup.imr_multiaddr.s_addr = mcast_v4->sin_addr.s_addr;\n \tgroup.imr_interface.s_addr = src_addr;\n \tif (setsockopt(sd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char *)&group, sizeof(group)) < 0) {\n-\t\trist_log_priv3(RIST_LOG_ERROR, \"Failed to join multicast group\\n\");\n-\t\treturn -1;\n+\t\trist_log_priv3( RIST_LOG_ERROR, \"Failed to join multicast group\\n\");\n+\t\tgoto fail;\n \t}\n \treturn 0;\n+\n+fail:\n+\treturn -1;\n }\n \n int udpsocket_open_connect(const char *host, uint16_t port, const char *mciface)\n@@ -519,43 +268,26 @@ int udpsocket_open_connect(const char *host, uint16_t port, const char *mciface)\n \n \tif (setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, (char *)&yes, sizeof(int)) < 0) {\n \t\t/* Non-critical error */\n-#ifdef _WIN32\n-\t\trist_log_priv3( RIST_LOG_ERROR,\"Cannot set SO_REUSEADDR: WSAGetLastError=%d\\n\", WSAGetLastError());\n-#else\n \t\trist_log_priv3( RIST_LOG_ERROR,\"Cannot set SO_REUSEADDR: %s\\n\", strerror(errno));\n-#endif\n \t}\n \tif (setsockopt(sd, proto, ttlcmd, (char *)&ttl, sizeof(ttl)) < 0) {\n \t\t/* Non-critical error */\n-#ifdef _WIN32\n-\t\trist_log_priv3( RIST_LOG_ERROR,\"Cannot set socket MAX HOPS: WSAGetLastError=%d\\n\", WSAGetLastError());\n-#else\n \t\trist_log_priv3( RIST_LOG_ERROR,\"Cannot set socket MAX HOPS: %s\\n\", strerror(errno));\n-#endif\n \t}\n \tif (mciface && mciface[0] != '\\0')\n \t\tudpsocket_set_mcast_iface(sd, mciface, raw.sin6_family);\n \n \tif (connect(sd, (struct sockaddr *)&raw, addrlen) < 0) {\n-#ifdef _WIN32\n-\t\tint winerr = WSAGetLastError();\n-\t\trist_log_priv3( RIST_LOG_ERROR, \"connect() failed: WSAGetLastError=%d\\n\", winerr);\n-\t\tudpsocket_close(sd);\n-\t\treturn -1;\n-#else\n \t\tint err = errno;\n \t\tudpsocket_close(sd);\n \t\terrno = err;\n \t\treturn -1;\n-#endif\n \t}\n-\tudpsocket_set_dontfragment(sd, raw.sin6_family);\n \n \treturn sd;\n }\n \n-int udpsocket_open_bind_mcast(const char *host, uint16_t port, const char *mciface,\n-                               uint32_t ttl, const char *ssm_source)\n+int udpsocket_open_bind(const char *host, uint16_t port, const char *mciface)\n {\n \tint sd;\n \tstruct sockaddr_in6 raw;\n@@ -577,47 +309,26 @@ int udpsocket_open_bind_mcast(const char *host, uint16_t port, const char *mcifa\n \t\tis_multicast = IN_MULTICAST(ntohl(tmp->sin_addr.s_addr));\n \t}\n \tif (setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, (char *)&yes, sizeof(int)) < 0) {\n-#ifdef _WIN32\n-\t\trist_log_priv3(RIST_LOG_ERROR, \"Cannot set SO_REUSEADDR: WSAGetLastError=%d\\n\", WSAGetLastError());\n-#else\n-\t\trist_log_priv3(RIST_LOG_ERROR, \"Cannot set SO_REUSEADDR: %s\\n\", strerror(errno));\n-#endif\n+\t\t/* Non-critical error */\n+\t\trist_log_priv3( RIST_LOG_ERROR, \"Cannot set SO_REUSEADDR: %s\\n\", strerror(errno));\n \t}\n-\tif (bind(sd, (struct sockaddr *)&raw, addrlen) < 0) {\n-#ifdef _WIN32\n-\t\trist_log_priv3(RIST_LOG_ERROR, \"Could not bind to interface: WSAGetLastError=%d\\n\", WSAGetLastError());\n-#else\n-\t\trist_log_priv3(RIST_LOG_ERROR, \"Could not bind to interface: %s\\n\", strerror(errno));\n-#endif\n+\tif (bind(sd, (struct sockaddr *)&raw, addrlen) < 0)\t{\n+\t\trist_log_priv3( RIST_LOG_ERROR, \"Could not bind to interface: %s\\n\", strerror(errno));\n \t\tudpsocket_close(sd);\n \t\treturn -1;\n \t}\n-\tudpsocket_set_dontfragment(sd, raw.sin6_family);\n-\tif (is_multicast) {\n-\t\tif (ttl > 0)\n-\t\t\tudpsocket_set_mcast_ttl(sd, raw.sin6_family, ttl);\n-\t\tif (udpsocket_join_mcast_group(sd, mciface, (struct sockaddr *)&raw, raw.sin6_family, ssm_source) != 0) {\n-\t\t\trist_log_priv3(RIST_LOG_ERROR, \"Could not join multicast group: %s on %s\\n\", host, mciface);\n-\t\t\tudpsocket_close(sd);\n-\t\t\treturn -1;\n-\t\t}\n-\t}\n+\tif (is_multicast && udpsocket_join_mcast_group(sd, mciface, (struct sockaddr *)&raw, raw.sin6_family) != 0) {\n+\t\trist_log_priv3( RIST_LOG_ERROR, \"Could not join multicast group: %s on %s\\n\", host, mciface);\n+\t\treturn -1;\n+\t} \n \treturn sd;\n }\n \n-int udpsocket_open_bind(const char *host, uint16_t port, const char *mciface)\n-{\n-\treturn udpsocket_open_bind_mcast(host, port, mciface, 0, NULL);\n-}\n-\n int udpsocket_set_nonblocking(int sd)\n {\n #ifdef _WIN32\n \tu_long iMode=1;\n-\tif (ioctlsocket(sd, FIONBIO, &iMode) != 0) {\n-\t\trist_log_priv3(RIST_LOG_WARN, \"ioctlsocket(FIONBIO) failed: WSAGetLastError=%d\\n\", WSAGetLastError());\n-\t\treturn -1;\n-\t}\n+\tioctlsocket(sd, FIONBIO, &iMode);\n #else\n \tRIST_MARK_UNUSED(sd);\n #endif\n@@ -753,7 +464,6 @@ int udpsocket_parse_url(char *url, char *address, int address_maxlen, uint16_t *\n \n \tif (strlen(p_addr) > 0) {\n \t\tstrncpy(address, p_addr, address_maxlen);\n-\t\taddress[address_maxlen - 1] = '\\0';\n \t} else if ( !using_sqbrkts) {\n \t\tsprintf(address, \"0.0.0.0\");\n \t} else {\ndiff --git a/subprojects/cmocka.wrap b/subprojects/cmocka.wrap\ndeleted file mode 100644\nindex 50b4c48..0000000\n--- a/subprojects/cmocka.wrap\n+++ /dev/null\n@@ -1,13 +0,0 @@\n-[wrap-file]\n-directory = cmocka-1.1.8\n-source_url = https://cmocka.org/files/1.1/cmocka-1.1.8.tar.xz\n-source_filename = cmocka-1.1.8.tar.xz\n-source_hash = 58435b558766d7f4c729ba163bdf3aec38bed3bc766dab684e3526ed0aa7c780\n-patch_filename = cmocka_1.1.8-1_patch.zip\n-patch_url = https://wrapdb.mesonbuild.com/v2/cmocka_1.1.8-1/get_patch\n-patch_hash = ed475edc7c511d531bf0b14e6743d124f048eb00225c52fce2a8f196bd29a345\n-source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/cmocka_1.1.8-1/cmocka-1.1.8.tar.xz\n-wrapdb_version = 1.1.8-1\n-\n-[provide]\n-cmocka = cmocka_dep\ndiff --git a/test/rist/meson.build b/test/rist/meson.build\nindex bf2c260..5293036 100644\n--- a/test/rist/meson.build\n+++ b/test/rist/meson.build\n@@ -32,254 +32,10 @@ test_sender_restart = executable('test_sender_restart',\n \t\t\t\t\t\t\t\t\tstdatomic_dependency\n                                 ])\n \n-test_reflector = executable('test_reflector',\n-                            'test_reflector.c',\n-                            extra_sources,\n-                            include_directories: inc,\n-                            link_with: librist,\n-                            dependencies: [\n-                                threads,\n-                                tools_dependencies,\n-                                stdatomic_dependency\n-                            ])\n-\n-# issue #188 coverage: listener-side cname re-association.  Three\n-# modes (plaintext / psk / srp) exercise the SRP-only gate: only an\n-# authenticated per-peer SRP session may migrate an existing peer\n-# record onto a new source tuple.\n-test_nat_port_rebind = executable('test_nat_port_rebind',\n-                                  'test_nat_port_rebind.c',\n-                                  extra_sources,\n-                                  include_directories: inc,\n-                                  link_with: librist,\n-                                  dependencies: [\n-                                      threads,\n-                                      tools_dependencies,\n-                                      stdatomic_dependency\n-                                  ])\n-\n-# Security regression guard: a shared-PSK peer that forges the victim's\n-# cname must NOT be absorbed into the victim's peer record (cname is not\n-# a per-peer secret under PSK).\n-test_psk_cname_hijack = executable('test_psk_cname_hijack',\n-                                   'test_psk_cname_hijack.c',\n-                                   extra_sources,\n-                                   include_directories: inc,\n-                                   link_with: librist,\n-                                   dependencies: [\n-                                       threads,\n-                                       tools_dependencies,\n-                                       stdatomic_dependency\n-                                   ])\n-\n-# Multipath safety: two same-cname bonded paths must not be merged by\n-# the cname re-association, and a healthy path must not rebind.\n-test_multipath_cname = executable('test_multipath_cname',\n-                                  'test_multipath_cname.c',\n-                                  extra_sources,\n-                                  include_directories: inc,\n-                                  link_with: librist,\n-                                  dependencies: [\n-                                      threads,\n-                                      tools_dependencies,\n-                                      stdatomic_dependency\n-                                  ])\n-\n-# Multipath safety: one bonded leg rebinds mid-stream while the other\n-# stays alive; the survivor must be undisturbed and the stream must not\n-# stall.\n-test_multipath_leg_rebind = executable('test_multipath_leg_rebind',\n-                                       'test_multipath_leg_rebind.c',\n-                                       extra_sources,\n-                                       include_directories: inc,\n-                                       link_with: librist,\n-                                       dependencies: [\n-                                           threads,\n-                                           tools_dependencies,\n-                                           stdatomic_dependency\n-                                       ])\n-\n-# Dynamic RTT-based bonded-leg muting (?rtt-drop=): a real two-leg bonded\n-# sender must mute a leg whose RTT spikes over the ceiling, reroute payload to\n-# the healthy leg, restore on recovery, and never black out the stream.\n-test_rtt_mute_bonding = executable('test_rtt_mute_bonding',\n-                                   'test_rtt_mute_bonding.c',\n-                                   extra_sources,\n-                                   include_directories: inc,\n-                                   link_with: librist,\n-                                   dependencies: [\n-                                       threads,\n-                                       tools_dependencies,\n-                                       stdatomic_dependency\n-                                   ])\n-\n-# issue #188 coverage: receiver-caller socket rebind on session\n-# silence (plaintext + shared-PSK; SRP uses listener-side path).\n-test_caller_socket_rebind = executable('test_caller_socket_rebind',\n-                                       'test_caller_socket_rebind.c',\n-                                       extra_sources,\n-                                       include_directories: inc,\n-                                       link_with: librist,\n-                                       dependencies: [\n-                                           threads,\n-                                           tools_dependencies,\n-                                           stdatomic_dependency\n-                                       ])\n-\n-test_eap_authenticator_restart = executable('test_eap_authenticator_restart',\n-                                       'test_eap_authenticator_restart.c',\n-                                       extra_sources,\n-                                       include_directories: inc,\n-                                       link_with: librist,\n-                                       dependencies: [\n-                                           threads,\n-                                           tools_dependencies,\n-                                           stdatomic_dependency\n-                                       ])\n-\n-\n-test_oob_read = executable('test_oob_read',\n-                           'test_oob_read.c',\n-                           extra_sources,\n-                           include_directories: inc,\n-                           link_with: librist,\n-                           dependencies: [\n-                               threads,\n-                               tools_dependencies,\n-                               stdatomic_dependency\n-                           ])\n-\n \n ###Sender restart test (bug fix verification)\n test('Sender restart test', test_sender_restart, args: ['5'], suite: ['regression', 'sender'])\n \n-###Out-of-band polling read (rist_oob_read) round-trip\n-test('OOB polling read Main profile', test_oob_read,\n-     args: ['1', 'rist://@127.0.0.1:8190', 'rist://127.0.0.1:8190'],\n-     suite: ['regression', 'oob'], timeout: 60)\n-test('OOB polling read Advanced profile', test_oob_read,\n-     args: ['2', 'rist://@127.0.0.1:8192', 'rist://127.0.0.1:8192'],\n-     suite: ['regression', 'oob'], timeout: 60)\n-\n-###issue #188 NAT source-port rebind recovery\n-test('NAT port rebind cname re-association (plaintext gate refuses)', test_nat_port_rebind,\n-     suite: ['regression', 'nat'], timeout: 90)\n-test('NAT port rebind cname re-association (psk gate refuses)', test_nat_port_rebind,\n-     args: ['psk'], suite: ['regression', 'nat'], timeout: 90)\n-test('Receiver caller socket rebind on silence (plaintext)', test_caller_socket_rebind,\n-     suite: ['regression', 'nat'], timeout: 90)\n-test('Receiver caller socket rebind on silence (psk)', test_caller_socket_rebind,\n-     args: ['psk'], suite: ['regression', 'nat'], timeout: 90)\n-# PSK cname forgery must not take over an existing peer record.\n-test('NAT rebind PSK cname hijack is refused', test_psk_cname_hijack,\n-     suite: ['regression', 'nat', 'security'], timeout: 90)\n-# Two bonded same-cname paths must stay distinct and not rebind.\n-test('Multipath same-cname paths stay distinct (psk)', test_multipath_cname,\n-     suite: ['regression', 'nat', 'multipath'], timeout: 90)\n-# RTT-based bonded-leg auto-mute: mute on spike, reroute, restore, guard.\n-test('Bonded leg auto-mutes on RTT spike and recovers', test_rtt_mute_bonding,\n-     suite: ['regression', 'multipath'], timeout: 120)\n-if have_srp\n-\t# Positive path: an authenticated SRP caller IS migrated onto the\n-\t# new source tuple after a NAT source-port rebind.\n-\ttest('NAT port rebind cname re-association (srp migration)', test_nat_port_rebind,\n-\t     args: ['srp'], suite: ['regression', 'nat', 'srp'], timeout: 120)\n-\t# Two bonded SRP paths with the SAME username/password + cname must\n-\t# stay distinct even though the SRP gate lets re-association fire.\n-\ttest('Multipath same-cname paths stay distinct (srp)', test_multipath_cname,\n-\t     args: ['srp'], suite: ['regression', 'nat', 'multipath', 'srp'], timeout: 120)\n-\t# One bonded SRP leg rebinds mid-stream while the other stays alive.\n-\ttest('Multipath one leg rebinds mid-stream (srp)', test_multipath_leg_rebind,\n-\t     suite: ['regression', 'nat', 'multipath', 'srp'], timeout: 120)\n-\t# Regression for the EAP-SRP no-rebind bug: an SRP caller whose\n-\t# listener restarts (all session state lost) must reset EAP and\n-\t# re-authenticate on the fresh socket by itself, with no operator\n-\t# intervention.  Before the fix the SRP caller was killed and stayed\n-\t# idle forever.\n-\ttest('Receiver caller socket rebind on silence (srp)', test_caller_socket_rebind,\n-\t     args: ['srp'], suite: ['regression', 'nat', 'srp'], timeout: 120)\n-\t# Regression for the EAP-SRP re-auth lockout after an *authenticator*\n-\t# (listener/hub) restart: the fresh authenticator re-challenges with\n-\t# IDENTITY, but a caller parked in EAP SUCCESS refused it forever, so the\n-\t# hub sat in \"Waiting for EAP\" and carried 0 packets.  Distinct from the\n-\t# silence rebind above (the authenticator is NOT silent here); recovery\n-\t# must come from the identity-request re-auth path.\n-\ttest('EAP-SRP caller re-auth after authenticator restart (srp)', test_eap_authenticator_restart,\n-\t     suite: ['regression', 'srp'], timeout: 120)\n-\t# Regression for the bonded EAP-SRP sender-leg flap bug (issue #224).\n-\t# A bonded caller's leg is silenced (100% loss both ways) with its\n-\t# interface left UP, so its miface-bound socket persists on the same\n-\t# source tuple; the listener purges the timed-out leg while the other\n-\t# leg keeps the bond alive.  When the loss clears the caller must reset\n-\t# EAP and re-authenticate the leg on its own -- before the fix it\n-\t# stayed wedged in stale EAP SUCCESS forever.  This needs real\n-\t# interfaces + tc packet loss (a loopback leg has no miface binding and\n-\t# self-heals via a new-port handshake, hiding the bug), so it runs only\n-\t# on Linux as root with netns/tc and cleanly SKIPs (exit 77) otherwise.\n-\tif get_option('built_tools')\n-\t\ttest('Bonded SRP leg re-authenticates after a same-tuple flap (srp, netns)',\n-\t\t     find_program('test_bonded_leg_flap_netns.sh'),\n-\t\t     args: [ristsender_exe, ristreceiver_exe],\n-\t\t     suite: ['netns', 'nat', 'multipath', 'srp'], timeout: 120)\n-\t\t# Companion: a sub-grace stall must NOT re-auth. Same topology, but a\n-\t\t# short silence (under the buffer-derived grace) must ride through on\n-\t\t# the existing session instead of tearing down and re-handshaking.\n-\t\ttest('Bonded SRP leg rides through a sub-grace stall without re-auth (srp, netns)',\n-\t\t     find_program('test_bonded_leg_stall_grace_netns.sh'),\n-\t\t     args: [ristsender_exe, ristreceiver_exe],\n-\t\t     suite: ['netns', 'nat', 'multipath', 'srp'], timeout: 120)\n-\t\t# A leg muted for high RTT plus its only sibling stalled for silence\n-\t\t# must not leave the balancer with zero carriers: the receiver must\n-\t\t# keep getting data on the reinstated leg instead of starving.\n-\t\ttest('Bonded muted+stalled split keeps a carrier (srp, netns)',\n-\t\t     find_program('test_bonded_carrier_never_starved_netns.sh'),\n-\t\t     args: [ristsender_exe, ristreceiver_exe],\n-\t\t     suite: ['netns', 'nat', 'multipath', 'srp'], timeout: 120)\n-\tendif\n-endif\n-\n-###?profile= URL override tests (rist_*_create profile != URL profile)\n-test('?profile=2 URL overrides Main to Advanced',\n-     test_send_receive,\n-     args: ['1', 'rist://@127.0.0.1:7080?profile=2', 'rist://127.0.0.1:7080?profile=2', '0'],\n-     suite: ['url-profile', 'advanced', 'unicast', 'server'])\n-test('?profile=2 URL overrides Main to Advanced + 10% loss',\n-     test_send_receive,\n-     args: ['1', 'rist://@127.0.0.1:7082?profile=2&rtt-max=10&rtt-min=1', 'rist://127.0.0.1:7082?profile=2&rtt-max=10&rtt-min=1', '10'],\n-     suite: ['url-profile', 'advanced', 'unicast', 'server'])\n-test('?profile=1 URL overrides Advanced to Main',\n-     test_send_receive,\n-     args: ['2', 'rist://@127.0.0.1:7084?profile=1', 'rist://127.0.0.1:7084?profile=1', '0'],\n-     suite: ['url-profile', 'main', 'unicast', 'server'])\n-test('?profile=2 URL overrides Simple to Advanced',\n-     test_send_receive,\n-     args: ['0', 'rist://@127.0.0.1:7086?profile=2', 'rist://127.0.0.1:7086?profile=2', '0'],\n-     suite: ['url-profile', 'advanced', 'unicast', 'server'])\n-\n-###Cross-profile interop (TR-06-3 Section 9): mismatched sender/receiver profiles\n-### on one flow. Exercises the Advanced sender's Main-Profile fallback (sends\n-### Main-conformant media until the peer advertises I=1) and the Advanced\n-### receiver's mid-stream framing-transition reset.\n-test('Interop - Advanced sender to Main receiver',\n-     test_send_receive,\n-     args: ['1', 'rist://@127.0.0.1:7090?profile=1', 'rist://127.0.0.1:7090?profile=2', '0'],\n-     suite: ['interop', 'advanced', 'main', 'unicast'])\n-test('Interop - Advanced sender to Main receiver + 10% loss',\n-     test_send_receive,\n-     args: ['1', 'rist://@127.0.0.1:7092?profile=1&rtt-max=10&rtt-min=1', 'rist://127.0.0.1:7092?profile=2&rtt-max=10&rtt-min=1', '10'],\n-     suite: ['interop', 'advanced', 'main', 'unicast'])\n-test('Interop - Main sender to Advanced receiver',\n-     test_send_receive,\n-     args: ['2', 'rist://@127.0.0.1:7094?profile=2', 'rist://127.0.0.1:7094?profile=1', '0'],\n-     suite: ['interop', 'advanced', 'main', 'unicast'])\n-test('Interop - Main sender to Advanced receiver + 10% loss',\n-     test_send_receive,\n-     args: ['2', 'rist://@127.0.0.1:7096?profile=2&rtt-max=10&rtt-min=1', 'rist://127.0.0.1:7096?profile=1&rtt-max=10&rtt-min=1', '10'],\n-     suite: ['interop', 'advanced', 'main', 'unicast'])\n-\n-###Reflector tests\n-test('Reflector functionality', test_reflector, suite: ['main', 'reflector'], timeout: 120)\n-\n ###Simple profile tests\n #Unicast\n test('Simple profile unicast', test_send_receive, args: ['0', 'rist://@127.0.0.1:1234', 'rist://127.0.0.1:1234', '0'], suite: ['simple', 'unicast'])\n@@ -308,49 +64,6 @@ test('Main profile receive client mode, sender server mode packet loss 25%', tes\n test('Main profile encryption receive server mode, sender client mode', test_send_receive, args: ['1', 'rist://@127.0.0.1:6001?secret=12345678&aes-type=128', 'rist://127.0.0.1:6001?secret=12345678&aes-type=128', '0'],suite: ['main', 'unicast', 'server', 'encryption'])\n test('Main profile encryption receive client mode, sender server mode ', test_send_receive, args: ['1', 'rist://127.0.0.1:6002?secret=12345678&aes-type=128', 'rist://@127.0.0.1:6002?secret=12345678&aes-type=128', '0'],suite: ['main', 'unicast', 'client', 'encryption'])\n test('Main profile encryption receive client mode, sender server mode AES256 ', test_send_receive, args: ['1', 'rist://127.0.0.1:6007?secret=12345678&aes-type=256', 'rist://@127.0.0.1:6007?secret=12345678&aes-type=256', '0'],suite: ['main', 'unicast', 'client', 'encryption'])\n-#Caller-controlled seq (RIST_DATA_FLAGS_USE_SEQ) passthrough\n-test('Main profile USE_SEQ passthrough', test_send_receive, args: ['1', 'rist://@127.0.0.1:6020?rtt-max=10&rtt-min=1', 'rist://127.0.0.1:6020?rtt-max=10&rtt-min=1', '0', '0', '1'], suite: ['main', 'unicast', 'use_seq'])\n-test('Main profile USE_SEQ passthrough 10% loss', test_send_receive, args: ['1', 'rist://@127.0.0.1:6021?rtt-max=10&rtt-min=1', 'rist://127.0.0.1:6021?rtt-max=10&rtt-min=1', '10', '0', '1'], suite: ['main', 'unicast', 'use_seq'])\n-###Advanced profile tests (VSF TR-06-3):\n-test('Advanced profile receive server mode, sender client mode', test_send_receive, args: ['2', 'rist://@127.0.0.1:7001?rtt-max=10&rtt-min=1', 'rist://127.0.0.1:7001?rtt-max=10&rtt-min=1', '0'],suite: ['advanced', 'unicast', 'server'])\n-test('Advanced profile receive server mode, sender client mode packet loss 10%', test_send_receive, args: ['2', 'rist://@127.0.0.1:7002?rtt-max=10&rtt-min=1', 'rist://127.0.0.1:7002?rtt-max=10&rtt-min=1', '10'],suite: ['advanced', 'unicast', 'server'])\n-test('Advanced profile receive client mode, sender server mode', test_send_receive, args: ['2', 'rist://127.0.0.1:7003?rtt-max=10&rtt-min=1', 'rist://@127.0.0.1:7003?rtt-max=10&rtt-min=1', '0'],suite: ['advanced', 'unicast', 'client'])\n-test('Advanced profile receive client mode, sender server mode packet loss 10%', test_send_receive, args: ['2', 'rist://127.0.0.1:7004?rtt-max=10&rtt-min=1', 'rist://@127.0.0.1:7004?rtt-max=10&rtt-min=1', '10'],suite: ['advanced', 'unicast', 'client'])\n-\n-###Phase C: Native control plane, 32-bit seq, Type 8 wrapping\n-test('Advanced profile server mode packet loss 25% (native NACK)', test_send_receive, args: ['2', 'rist://@127.0.0.1:7010?rtt-max=10&rtt-min=1', 'rist://127.0.0.1:7010?rtt-max=10&rtt-min=1', '25'],suite: ['advanced', 'unicast', 'server', 'phase-c'])\n-test('Advanced profile client mode packet loss 25% (native NACK)', test_send_receive, args: ['2', 'rist://127.0.0.1:7011?rtt-max=10&rtt-min=1', 'rist://@127.0.0.1:7011?rtt-max=10&rtt-min=1', '25'],suite: ['advanced', 'unicast', 'client', 'phase-c'])\n-\n-###Phase D1: PSK encryption (AES-CTR, mode 1)\n-test('Advanced profile encryption server mode AES128', test_send_receive, args: ['2', 'rist://@127.0.0.1:7020?secret=12345678&aes-type=128', 'rist://127.0.0.1:7020?secret=12345678&aes-type=128', '0'],suite: ['advanced', 'unicast', 'server', 'encryption', 'phase-d1'])\n-test('Advanced profile encryption client mode AES128', test_send_receive, args: ['2', 'rist://127.0.0.1:7021?secret=12345678&aes-type=128', 'rist://@127.0.0.1:7021?secret=12345678&aes-type=128', '0'],suite: ['advanced', 'unicast', 'client', 'encryption', 'phase-d1'])\n-test('Advanced profile encryption server mode AES256', test_send_receive, args: ['2', 'rist://@127.0.0.1:7022?secret=12345678&aes-type=256', 'rist://127.0.0.1:7022?secret=12345678&aes-type=256', '0'],suite: ['advanced', 'unicast', 'server', 'encryption', 'phase-d1'])\n-test('Advanced profile encryption server mode AES128 + 10% loss', test_send_receive, args: ['2', 'rist://@127.0.0.1:7023?secret=12345678&aes-type=128', 'rist://127.0.0.1:7023?secret=12345678&aes-type=128', '10'],suite: ['advanced', 'unicast', 'server', 'encryption', 'phase-d1'])\n-test('Advanced profile encryption mismatch (should fail)', test_send_receive, args: ['2', 'rist://@127.0.0.1:7030?secret=12345678&aes-type=128', 'rist://127.0.0.1:7030', '0'], should_fail: true)\n-test('Advanced profile encryption mismatch reverse (should fail)', test_send_receive, args: ['2', 'rist://@127.0.0.1:7031', 'rist://127.0.0.1:7031?secret=12345678&aes-type=128', '0'], should_fail: true)\n-\n-###Flow ID: stream-id via virt-dst-port on native AP Type 5\n-test('Advanced profile Flow ID server mode virt-dst-port', test_send_receive, args: ['2', 'rist://@127.0.0.1:7040?virt-dst-port=5000', 'rist://127.0.0.1:7040?virt-dst-port=5000', '0'],suite: ['advanced', 'unicast', 'server', 'flow-id'])\n-test('Advanced profile Flow ID client mode virt-dst-port', test_send_receive, args: ['2', 'rist://127.0.0.1:7041?virt-dst-port=5000', 'rist://@127.0.0.1:7041?virt-dst-port=5000', '0'],suite: ['advanced', 'unicast', 'client', 'flow-id'])\n-\n-###Phase D4: Flow Attribute messages (CI=0x8001)\n-test('Advanced profile Flow Attribute server mode', test_send_receive, args: ['2', 'rist://@127.0.0.1:7060?rtt-max=10&rtt-min=1', 'rist://127.0.0.1:7060?rtt-max=10&rtt-min=1', '0', '0', '0', '1'],suite: ['advanced', 'unicast', 'server', 'flow-attr'])\n-test('Advanced profile Flow Attribute client mode', test_send_receive, args: ['2', 'rist://127.0.0.1:7061?rtt-max=10&rtt-min=1', 'rist://@127.0.0.1:7061?rtt-max=10&rtt-min=1', '0', '0', '0', '1'],suite: ['advanced', 'unicast', 'client', 'flow-attr'])\n-\n-###Phase D6: LZ4 payload compression\n-test('Advanced profile LZ4 compression server mode', test_send_receive, args: ['2', 'rist://@127.0.0.1:7050', 'rist://127.0.0.1:7050?compression=1', '0'],suite: ['advanced', 'unicast', 'server', 'lz4'])\n-test('Advanced profile LZ4 compression client mode', test_send_receive, args: ['2', 'rist://127.0.0.1:7051', 'rist://@127.0.0.1:7051?compression=1', '0'],suite: ['advanced', 'unicast', 'client', 'lz4'])\n-test('Advanced profile LZ4 compression + 10% loss', test_send_receive, args: ['2', 'rist://@127.0.0.1:7052', 'rist://127.0.0.1:7052?compression=1', '10'],suite: ['advanced', 'unicast', 'server', 'lz4'])\n-test('Advanced profile LZ4 + AES128 server mode', test_send_receive, args: ['2', 'rist://@127.0.0.1:7053?secret=12345678&aes-type=128', 'rist://127.0.0.1:7053?secret=12345678&aes-type=128&compression=1', '0'],suite: ['advanced', 'unicast', 'server', 'lz4', 'encryption'])\n-\n-###SRP authentication (Advanced Profile)\n-if have_srp\n-\ttest('Advanced profile SRP auth server mode AES128', test_send_receive, args: ['2', 'rist://@127.0.0.1:7070?secret=12345678&aes-type=128&username=testuser&password=testpassword', 'rist://127.0.0.1:7070?secret=12345678&aes-type=128&username=testuser&password=testpassword', '0'],suite: ['advanced', 'unicast', 'server', 'encryption', 'srp'], should_fail: false)\n-\ttest('Advanced profile SRP auth client mode AES128', test_send_receive, args: ['2', 'rist://127.0.0.1:7071?secret=12345678&aes-type=128&username=testuser&password=testpassword', 'rist://@127.0.0.1:7071?secret=12345678&aes-type=128&username=testuser&password=testpassword', '0'],suite: ['advanced', 'unicast', 'client', 'encryption', 'srp'], should_fail: false)\n-\ttest('Advanced profile SRP auth client no SRP (should fail)', test_send_receive, args: ['2', 'rist://127.0.0.1:7072?secret=12345678&aes-type=128', 'rist://@127.0.0.1:7072?secret=12345678&aes-type=128&username=testuser&password=testpassword', '0'], suite: ['advanced', 'unicast', 'server', 'encryption', 'srp'], should_fail: true)\n-\ttest('Advanced profile SRP auth password mismatch (should fail)', test_send_receive, args: ['2', 'rist://127.0.0.1:7073?secret=12345678&aes-type=128&username=testuser&password=wrongpassword', 'rist://@127.0.0.1:7073?secret=12345678&aes-type=128&username=testuser&password=testpassword', '0'],suite: ['advanced', 'unicast', 'server', 'encryption', 'srp'], should_fail: true)\n-endif\n-\n #Encryption tests where 1 side has enabled encryption these should fail\n test('Main profile encryption receive server mode unencrypted, sender client mode', test_send_receive, args: ['1', 'rist://@127.0.0.1:6003', 'rist://127.0.0.1:6003?secret=12345678&aes-type=128', '0'], should_fail: true)\n test('Main profile encryption receive server mode, sender client mode unencrypted', test_send_receive, args: ['1', 'rist://@127.0.0.1:6004?secret=12345678&aes-type=128', 'rist://127.0.0.1:6004', '0'], should_fail: true)\ndiff --git a/test/rist/test_bonded_carrier_never_starved_netns.sh b/test/rist/test_bonded_carrier_never_starved_netns.sh\ndeleted file mode 100755\nindex 0fa43b8..0000000\n--- a/test/rist/test_bonded_carrier_never_starved_netns.sh\n+++ /dev/null\n@@ -1,185 +0,0 @@\n-#!/bin/bash\n-# librist. Copyright © 2026 SipRadius LLC.\n-# SPDX-License-Identifier: BSD-2-Clause\n-#\n-# Regression for the RTT-mute / stall \"no carrier\" starvation. The sender pulls\n-# a leg from the payload rotation for two independent reasons: RTT-mute (leg RTT\n-# over the ceiling) and stall (leg gone silent on its return path). Each check\n-# keeps at least one leg out of its OWN mechanism, but they run independently:\n-# one leg muted for high RTT PLUS its only sibling stalled for silence leaves\n-# the balancer with zero carriers, and it then drops the payload. The receiver\n-# starves on both legs at once even though one leg is still perfectly able to\n-# deliver (just late).\n-#\n-# This drives exactly that split on a two-leg bonded caller, in sequence so the\n-# mute genuinely fires before the stall:\n-#   1. leg A (ca): +300 ms each way -> smoothed RTT ~600 ms > rtt-drop=400, and\n-#      with leg B still healthy it is actually MUTED (its share moves to B).\n-#   2. leg B (cb): 100% loss both ways -> silent on its return path -> STALLED.\n-#      Now A is muted and B is stalled: the balancer has no carrier.\n-# The buffer is large (6 s) so the liveness timeout (~12 s) does not tear leg B\n-# down during the window: it stays authenticated-but-stalled, holding the split.\n-#\n-# Before the fix both legs were skipped and the receiver's per-interval\n-# \"received\" collapsed to ~0 (only the muted leg's 1-in-100 trickle). After it,\n-# the muted leg is kept as the sole carrier and data keeps flowing, late but\n-# continuous.\n-#\n-# Needs real interfaces + tc, so it runs only on Linux as root with netns/tc and\n-# SKIPs (77) otherwise.\n-#\n-# Usage:   test_bonded_carrier_never_starved_netns.sh <ristsender> <ristreceiver>\n-# Exit:    0 = a carrier survived the muted+stalled split (fixed)\n-#          1 = receiver starved on both legs at once (regression)\n-#          77 = SKIP (needs Linux root + netns + tc)\n-#          99 = setup error / INDETERMINATE (the split did not form)\n-set -u\n-\n-TX=\"${1:-}\"\n-RX=\"${2:-}\"\n-USER=carrieruser; PASS=carrierpass; CN=testcn\n-RXIP=10.0.2.61\n-NS=rist_carrier_snd\n-RXLOG=\"$(mktemp)\"; TXLOG=\"$(mktemp)\"\n-\n-skip()  { echo \"SKIP: $*\"; cleanup 2>/dev/null; exit 77; }\n-setup_err() { echo \"SETUP ERROR: $*\"; cleanup 2>/dev/null; exit 99; }\n-\n-cleanup() {\n-  ip netns del \"$NS\" 2>/dev/null\n-  ip link del veth-ca 2>/dev/null\n-  ip link del veth-cb 2>/dev/null\n-  ip link del cbr0   2>/dev/null\n-  [ -n \"${TXPID:-}\" ] && kill \"$TXPID\" 2>/dev/null\n-  [ -n \"${RXPID:-}\" ] && kill \"$RXPID\" 2>/dev/null\n-  [ -n \"${FEEDPID:-}\" ] && kill \"$FEEDPID\" 2>/dev/null\n-  rm -f \"$RXLOG\" \"$TXLOG\" 2>/dev/null\n-}\n-trap cleanup EXIT\n-\n-# ---- environment gate: skip cleanly where we cannot reproduce ----------\n-[ \"$(uname -s)\" = \"Linux\" ] || skip \"not Linux\"\n-[ \"$(id -u)\" = \"0\" ] || skip \"needs root (CAP_NET_ADMIN) for netns/veth/tc\"\n-[ -n \"$TX\" ] && [ -x \"$TX\" ] || setup_err \"ristsender not found ($TX)\"\n-[ -n \"$RX\" ] && [ -x \"$RX\" ] || setup_err \"ristreceiver not found ($RX)\"\n-command -v ip >/dev/null 2>&1 || skip \"iproute2 'ip' missing\"\n-command -v tc >/dev/null 2>&1 || skip \"iproute2 'tc' missing\"\n-ip netns add \"$NS\" 2>/dev/null || skip \"cannot create netns (no CAP_NET_ADMIN?)\"\n-\n-# ---- topology: bridge in root ns, two veth legs into a sender netns -----\n-ip link add cbr0 type bridge                        || setup_err \"bridge add\"\n-ip addr add ${RXIP}/24 dev cbr0                      || setup_err \"bridge addr\"\n-ip link set cbr0 up                                  || setup_err \"bridge up\"\n-\n-ip link add veth-ca type veth peer name ca           || setup_err \"veth-ca\"\n-ip link add veth-cb type veth peer name cb           || setup_err \"veth-cb\"\n-ip link set veth-ca master cbr0; ip link set veth-ca up\n-ip link set veth-cb master cbr0; ip link set veth-cb up\n-ip link set ca netns \"$NS\"; ip link set cb netns \"$NS\"\n-ip netns exec \"$NS\" ip addr add 10.0.2.30/24 dev ca\n-ip netns exec \"$NS\" ip addr add 10.0.2.131/24 dev cb\n-ip netns exec \"$NS\" ip link set ca up\n-ip netns exec \"$NS\" ip link set cb up\n-ip netns exec \"$NS\" ip link set lo up\n-# same-subnet multi-homing: disable rp_filter so miface egress is kept\n-ip netns exec \"$NS\" sysctl -qw net.ipv4.conf.all.rp_filter=0\n-ip netns exec \"$NS\" sysctl -qw net.ipv4.conf.ca.rp_filter=0\n-ip netns exec \"$NS\" sysctl -qw net.ipv4.conf.cb.rp_filter=0\n-sysctl -qw net.ipv4.conf.all.rp_filter=0 >/dev/null 2>&1\n-\n-ip netns exec \"$NS\" ping -c1 -W2 -I ca ${RXIP} >/dev/null 2>&1 || skip \"leg A no connectivity\"\n-ip netns exec \"$NS\" ping -c1 -W2 -I cb ${RXIP} >/dev/null 2>&1 || skip \"leg B no connectivity\"\n-\n-# ---- receiver: listener, advanced profile, SRP authenticator -----------\n-# 6 s buffer keeps the liveness timeout (~2x buffer) well above the ~8 s\n-# impairment, so leg B stays authenticated-but-stalled. -S 500 gives fine\n-# stats sampling for the received-per-interval measurement below.\n-$RX -p 2 -v 6 -S 500 \\\n-  -i \"rist://@${RXIP}:2041?username=${USER}&password=${PASS}&buffer=6000\" \\\n-  -o \"udp://127.0.0.1:12356\" >\"$RXLOG\" 2>&1 &\n-RXPID=$!\n-sleep 1\n-\n-# udp feeder inside the sender netns -> ristsender input (~50 pkt/s)\n-ip netns exec \"$NS\" bash -c 'while :; do printf \"RISTTESTPACKET%08d\" $RANDOM > /dev/udp/127.0.0.1/5567; sleep 0.02; done' &\n-FEEDPID=$!\n-sleep 0.5\n-\n-# ---- sender: bonded caller, two SRP legs with RTT auto-mute configured --\n-RTTQS=\"rtt-drop=400&rtt-restore=200&rtt-drop-settle=1000&rtt-drop-trickle=100\"\n-ip netns exec \"$NS\" $TX -p 2 -v 6 \\\n-  -i \"udp://@127.0.0.1:5567\" \\\n-  -o \"rist://${RXIP}:2041?miface=ca&weight=5&username=${USER}&password=${PASS}&cname=${CN}&buffer=6000&${RTTQS},rist://${RXIP}:2041?miface=cb&weight=5&username=${USER}&password=${PASS}&cname=${CN}&buffer=6000&${RTTQS}\" \\\n-  >\"$TXLOG\" 2>&1 &\n-TXPID=$!\n-\n-echo \"== warmup 12s (both legs authenticate + stream) ==\"\n-sleep 12\n-auths_setup=$(grep -c \"Successfully authenticated\" \"$RXLOG\")\n-echo \"after warmup: successful auths=${auths_setup}\"\n-if [ \"$auths_setup\" -lt 2 ]; then\n-  echo \"INDETERMINATE: both legs did not authenticate before the impairment.\"\n-  exit 99\n-fi\n-\n-echo \"== step 1: leg A (ca) +300ms each way -> RTT over ceiling, mutes (B healthy) ==\"\n-ip netns exec \"$NS\" tc qdisc add dev ca root netem delay 300ms || setup_err \"tc add ca\"\n-tc qdisc add dev veth-ca root netem delay 300ms               || setup_err \"tc add veth-ca\"\n-sleep 4   # RTT ramp + drop-settle: leg A actually mutes while B still carries\n-\n-echo \"== step 2: leg B (cb) 100% loss both ways -> stalls; A must be kept carrier ==\"\n-ip netns exec \"$NS\" tc qdisc add dev cb root netem loss 100%  || setup_err \"tc add cb\"\n-tc qdisc add dev veth-cb root netem loss 100%                 || setup_err \"tc add veth-cb\"\n-\n-echo \"== let the split form (3s) ==\"\n-sleep 3\n-rx_off=$(wc -l < \"$RXLOG\")\n-\n-echo \"== measure received-per-interval for 6s while muted+stalled ==\"\n-sleep 6\n-\n-# ---- teardown impairment; observe recovery briefly ----------------------\n-ip netns exec \"$NS\" tc qdisc del dev ca root 2>/dev/null\n-tc qdisc del dev veth-ca root 2>/dev/null\n-ip netns exec \"$NS\" tc qdisc del dev cb root 2>/dev/null\n-tc qdisc del dev veth-cb root 2>/dev/null\n-\n-# ---- INDETERMINATE guard: the muted+stalled split must actually form -----\n-muted=$(grep -c \"over .*ceiling\" \"$TXLOG\")\n-stalled=$(grep -c \"stalled: silent\" \"$TXLOG\")\n-echo \"== split check: mute events=${muted}, stall events=${stalled} ==\"\n-if [ \"$muted\" -eq 0 ] || [ \"$stalled\" -eq 0 ]; then\n-  echo \"INDETERMINATE: impairment did not produce a muted+stalled split\"\\\n-       \"(mute=${muted}, stall=${stalled}); cannot judge the carrier guard.\"\n-  exit 99\n-fi\n-\n-# ---- verdict: with both legs pulled, did the receiver keep getting data? -\n-# Per-interval flow \"received\" over the measurement window. Streaming ~50 pkt/s\n-# at -S 500 is ~25/interval when a carrier survives; starvation leaves only the\n-# muted leg's 1-in-100 trickle, i.e. ~0.\n-window=$(tail -n +\"$((rx_off + 1))\" \"$RXLOG\" | grep '\"receiver-stats\"')\n-intervals=$(printf '%s\\n' \"$window\" | grep -c '\"received\"')\n-ok=$(printf '%s\\n' \"$window\" | grep -o '\"received\":[0-9]*' \\\n-       | grep -o '[0-9]*' | awk '$1 >= 10 {c++} END {print c+0}')\n-maxrx=$(printf '%s\\n' \"$window\" | grep -o '\"received\":[0-9]*' \\\n-       | grep -o '[0-9]*' | sort -n | tail -1)\n-echo \"== during split: ${intervals} stats intervals, ${ok} with received>=10,\"\\\n-     \"peak received/interval=${maxrx:-0} ==\"\n-\n-if [ \"${intervals:-0}\" -lt 3 ]; then\n-  echo \"INDETERMINATE: too few receiver-stats samples in the window\"\\\n-       \"(${intervals}); cannot judge starvation.\"\n-  exit 99\n-fi\n-if [ \"${ok:-0}\" -lt 3 ]; then\n-  echo \"FAIL: receiver starved during the muted+stalled split\"\\\n-       \"(${ok}/${intervals} intervals carried data, peak ${maxrx:-0}/interval);\"\\\n-       \"both legs were pulled and the payload was dropped.\"\n-  exit 1\n-fi\n-\n-echo \"PASS: a carrier survived the muted+stalled split\"\\\n-     \"(${ok}/${intervals} intervals carried data, peak ${maxrx}/interval);\"\\\n-     \"data kept flowing on the reinstated leg instead of starving.\"\n-exit 0\ndiff --git a/test/rist/test_bonded_leg_flap_netns.sh b/test/rist/test_bonded_leg_flap_netns.sh\ndeleted file mode 100755\nindex 65ca0a3..0000000\n--- a/test/rist/test_bonded_leg_flap_netns.sh\n+++ /dev/null\n@@ -1,182 +0,0 @@\n-#!/bin/bash\n-# librist. Copyright © 2026 SipRadius LLC.\n-# SPDX-License-Identifier: BSD-2-Clause\n-#\n-# Regression for the bonded EAP-SRP sender-leg re-authentication bug\n-# (upstream issue #224).\n-#\n-# A bonded caller (sender) with EAP-SRP has two legs to one listening\n-# receiver.  One leg is silenced (100% packet loss BOTH ways) long enough\n-# for the listener to time out and purge that leg's peer, while the other\n-# leg keeps the bond -- and the caller process -- alive.  Crucially the\n-# silenced leg's interface stays UP, so its miface-bound UDP socket is\n-# never torn down: when the loss clears the leg resumes from the EXACT\n-# same source tuple, with the caller still in EAP SUCCESS.  The listener\n-# purged the session and now silently drops the caller's data while it\n-# waits for an EAPOL START the caller never sends (EAP is authenticator-\n-# driven and the caller believes it is still authenticated).  The leg is\n-# wedged forever.\n-#\n-# This is why the reproduction needs real interfaces + tc packet loss and\n-# cannot be an in-process loopback test: a loopback leg has no miface\n-# binding, so it is recreated on timeout with a fresh ephemeral port and\n-# self-heals with a cold handshake -- hiding the bug.  Likewise an\n-# \"ip link set ... down\" makes the sends fail and forces the same\n-# new-port reconnect.  Only same-tuple silence reproduces the wedge.\n-#\n-# The fix drives the caller to reset EAP and re-run the SRP handshake on\n-# that leg once it has been silent past session_timeout, so the leg\n-# re-authenticates AND rejoins the weighted bond on its own.\n-#\n-# Two discriminators (the receiver is a stable listener the whole time, so\n-# the flow never resets):\n-#   1. Successful EAP authentications.  Two legs authenticate at startup; a\n-#      third is only possible if the flapped leg re-authenticated on its own.\n-#      Before any fix it stays wedged at two and the receiver floods\n-#      \"handshake is still pending\".\n-#   2. Legs the sender is balancing over after recovery, counted as the\n-#      distinct mifaces in the final sender-stats line.  A leg only appears\n-#      there while it is authenticated and in the balancing rotation, so this\n-#      catches the reintegration defect where a leg re-authenticates but is\n-#      left out of balancing and streams only NACK retransmits (returns 1\n-#      instead of 2).\n-#\n-# Usage:   test_bonded_leg_flap_netns.sh <ristsender> <ristreceiver>\n-# Exit:    0 = leg re-authenticated and rejoined the bond (fixed)\n-#          1 = leg wedged or did not rejoin balancing (bug)\n-#          77 = SKIP (needs Linux root + netns + tc)   99 = setup error\n-set -u\n-\n-TX=\"${1:-}\"\n-RX=\"${2:-}\"\n-USER=flapuser; PASS=flappass; CN=testcn\n-RXIP=10.0.1.51\n-NS=rist224_snd\n-RXLOG=\"$(mktemp)\"; TXLOG=\"$(mktemp)\"\n-\n-skip()  { echo \"SKIP: $*\"; cleanup 2>/dev/null; exit 77; }\n-setup_err() { echo \"SETUP ERROR: $*\"; cleanup 2>/dev/null; exit 99; }\n-\n-cleanup() {\n-  ip netns del \"$NS\" 2>/dev/null\n-  ip link del veth-a 2>/dev/null\n-  ip link del veth-b 2>/dev/null\n-  ip link del rbr0   2>/dev/null\n-  [ -n \"${TXPID:-}\" ] && kill \"$TXPID\" 2>/dev/null\n-  [ -n \"${RXPID:-}\" ] && kill \"$RXPID\" 2>/dev/null\n-  [ -n \"${FEEDPID:-}\" ] && kill \"$FEEDPID\" 2>/dev/null\n-  rm -f \"$RXLOG\" \"$TXLOG\" 2>/dev/null\n-}\n-trap cleanup EXIT\n-\n-# ---- environment gate: skip cleanly where we cannot reproduce ----------\n-[ \"$(uname -s)\" = \"Linux\" ] || skip \"not Linux\"\n-[ \"$(id -u)\" = \"0\" ] || skip \"needs root (CAP_NET_ADMIN) for netns/veth/tc\"\n-[ -n \"$TX\" ] && [ -x \"$TX\" ] || setup_err \"ristsender not found ($TX)\"\n-[ -n \"$RX\" ] && [ -x \"$RX\" ] || setup_err \"ristreceiver not found ($RX)\"\n-command -v ip >/dev/null 2>&1 || skip \"iproute2 'ip' missing\"\n-command -v tc >/dev/null 2>&1 || skip \"iproute2 'tc' missing\"\n-ip netns add \"$NS\" 2>/dev/null || skip \"cannot create netns (no CAP_NET_ADMIN?)\"\n-\n-# ---- topology: bridge in root ns, two veth legs into a sender netns -----\n-ip link add rbr0 type bridge                        || setup_err \"bridge add\"\n-ip addr add ${RXIP}/24 dev rbr0                      || setup_err \"bridge addr\"\n-ip link set rbr0 up                                  || setup_err \"bridge up\"\n-\n-ip link add veth-a type veth peer name sa            || setup_err \"veth-a\"\n-ip link add veth-b type veth peer name sb            || setup_err \"veth-b\"\n-ip link set veth-a master rbr0; ip link set veth-a up\n-ip link set veth-b master rbr0; ip link set veth-b up\n-ip link set sa netns \"$NS\"; ip link set sb netns \"$NS\"\n-ip netns exec \"$NS\" ip addr add 10.0.1.20/24 dev sa\n-ip netns exec \"$NS\" ip addr add 10.0.1.122/24 dev sb\n-ip netns exec \"$NS\" ip link set sa up\n-ip netns exec \"$NS\" ip link set sb up\n-ip netns exec \"$NS\" ip link set lo up\n-# same-subnet multi-homing: disable rp_filter so miface egress is kept\n-ip netns exec \"$NS\" sysctl -qw net.ipv4.conf.all.rp_filter=0\n-ip netns exec \"$NS\" sysctl -qw net.ipv4.conf.sa.rp_filter=0\n-ip netns exec \"$NS\" sysctl -qw net.ipv4.conf.sb.rp_filter=0\n-sysctl -qw net.ipv4.conf.all.rp_filter=0 >/dev/null 2>&1\n-\n-ip netns exec \"$NS\" ping -c1 -W2 -I sa ${RXIP} >/dev/null 2>&1 || skip \"leg A no connectivity\"\n-ip netns exec \"$NS\" ping -c1 -W2 -I sb ${RXIP} >/dev/null 2>&1 || skip \"leg B no connectivity\"\n-\n-# ---- receiver: listener, advanced profile, SRP authenticator -----------\n-$RX -p 2 -v 6 \\\n-  -i \"rist://@${RXIP}:2030?username=${USER}&password=${PASS}\" \\\n-  -o \"udp://127.0.0.1:12345\" >\"$RXLOG\" 2>&1 &\n-RXPID=$!\n-sleep 1\n-\n-# udp feeder inside the sender netns -> ristsender input\n-ip netns exec \"$NS\" bash -c 'while :; do printf \"RISTTESTPACKET%08d\" $RANDOM > /dev/udp/127.0.0.1/5556; sleep 0.02; done' &\n-FEEDPID=$!\n-sleep 0.5\n-\n-# ---- sender: bonded caller, two SRP legs (same cname + creds) ----------\n-ip netns exec \"$NS\" $TX -p 2 -v 6 \\\n-  -i \"udp://@127.0.0.1:5556\" \\\n-  -o \"rist://${RXIP}:2030?miface=sa&weight=10&username=${USER}&password=${PASS}&cname=${CN},rist://${RXIP}:2030?miface=sb&weight=5&username=${USER}&password=${PASS}&cname=${CN}\" \\\n-  >\"$TXLOG\" 2>&1 &\n-TXPID=$!\n-\n-echo \"== warmup 12s (both legs authenticate + stream) ==\"\n-sleep 12\n-auths_setup=$(grep -c \"Successfully authenticated\" \"$RXLOG\")\n-echo \"after warmup: successful auths=${auths_setup}\"\n-if [ \"$auths_setup\" -lt 2 ]; then\n-  echo \"INDETERMINATE: both legs did not authenticate before the flap.\"\n-  exit 99\n-fi\n-\n-echo \"== silence leg B (sb): 100% loss both ways, interface stays UP, 10s ==\"\n-ip netns exec \"$NS\" tc qdisc add dev sb root netem loss 100% || setup_err \"tc add sb\"\n-tc qdisc add dev veth-b root netem loss 100%                 || setup_err \"tc add veth-b\"\n-sleep 10\n-echo \"== leg B restored (same socket / same source tuple) ==\"\n-ip netns exec \"$NS\" tc qdisc del dev sb root 2>/dev/null\n-tc qdisc del dev veth-b root 2>/dev/null\n-echo \"== observe 28s (leg B must re-authenticate AND rejoin balancing) ==\"\n-sleep 28\n-\n-auths_final=$(grep -c \"Successfully authenticated\" \"$RXLOG\")\n-flood=$(grep -c \"handshake is still pending\" \"$RXLOG\")\n-resets=$(grep -c \"reset EAP and re-initiated the SRP\" \"$TXLOG\")\n-\n-# Reintegration check: the sender only lists a leg in its per-peer stats\n-# while that leg is authenticated and in the weighted balancing rotation, so\n-# the number of distinct mifaces in the final sender-stats line is the number\n-# of legs actually carrying balanced data.  Re-authenticating alone is not\n-# enough -- a leg that re-auths but is left out of balancing streams only NACK\n-# retransmits and never returns to its share (that was the original defect).\n-last_sstats=$(grep '\"sender-stats\"' \"$TXLOG\" | tail -1)\n-legs_balancing=$(printf '%s' \"$last_sstats\" | grep -o '\"miface\":\"[^\"]*\"' | sort -u | wc -l)\n-legs_balancing=$(printf '%s' \"$legs_balancing\" | tr -d ' ')\n-echo \"== after flap: successful auths ${auths_setup} -> ${auths_final}, \"\\\n-     \"handshake-pending flood=${flood}, sender EAP resets=${resets}, \"\\\n-     \"legs balancing after recovery=${legs_balancing} ==\"\n-\n-# A fresh EAP authentication after the flap proves the wedged leg\n-# re-handshook on its own.  Without the fix the count never moves and the\n-# receiver floods \"handshake is still pending\".\n-if [ \"$auths_final\" -le \"$auths_setup\" ]; then\n-  echo \"FAIL (issue #224): the flapped SRP leg never re-authenticated; it\"\\\n-       \"stayed wedged in EAP SUCCESS while the listener waited for an\"\\\n-       \"EAPOL START it never sent (successful auths stuck at ${auths_final},\"\\\n-       \"handshake-pending flood=${flood}).\"\n-  exit 1\n-fi\n-\n-if [ \"${legs_balancing:-0}\" -lt 2 ]; then\n-  echo \"FAIL (issue #224 reintegration): the flapped leg re-authenticated\"\\\n-       \"(successful auths ${auths_setup} -> ${auths_final}) but did not rejoin\"\\\n-       \"the weighted bond; the sender balances over only ${legs_balancing} leg\"\\\n-       \"so the returning leg carries no share.\"\n-  exit 1\n-fi\n-\n-echo \"PASS: the flapped SRP leg reset EAP, re-authenticated (successful auths\"\\\n-     \"${auths_setup} -> ${auths_final}) and rejoined the weighted bond\"\\\n-     \"(${legs_balancing} legs balancing) on its own.\"\n-exit 0\ndiff --git a/test/rist/test_bonded_leg_stall_grace_netns.sh b/test/rist/test_bonded_leg_stall_grace_netns.sh\ndeleted file mode 100755\nindex 9344776..0000000\n--- a/test/rist/test_bonded_leg_stall_grace_netns.sh\n+++ /dev/null\n@@ -1,157 +0,0 @@\n-#!/bin/bash\n-# librist. Copyright © 2026 SipRadius LLC.\n-# SPDX-License-Identifier: BSD-2-Clause\n-#\n-# Companion to test_bonded_leg_flap_netns.sh (issue #224). That test silences a\n-# bonded leg long enough to exceed the liveness timeout and asserts the leg tears\n-# down and re-handshakes. This one asserts the opposite for a SHORT stall: a leg\n-# that goes silent for less than the liveness timeout must be fast-muted but kept\n-# authenticated and resume on the SAME session, with no re-auth churn.\n-#\n-# Setup mirrors the #224 test: a bonded EAP-SRP caller with two legs to one\n-# listening receiver, one leg silenced (100% loss both ways, interface UP so the\n-# source tuple persists) while the other keeps the bond alive. The difference is\n-# the buffer: large, so the liveness timeout (max of session_timeout and twice\n-# the buffer) is ~12 s, well above the 6 s silence.\n-#\n-# Before routing and session lifetime were split, any silence past the (then\n-# 250 ms) liveness timeout tore the leg down and forced a cold re-handshake. Now:\n-#   1. No sender EAP reset  (\"reset EAP and re-initiated the SRP\" must stay 0).\n-#   2. No new receiver auth  (successful-auth count must not move: the leg keeps\n-#      its original session across the stall).\n-#   3. The leg is back in the weighted bond after recovery (two mifaces in the\n-#      final sender-stats line), proving the mute self-cleared on resume.\n-#\n-# Needs real interfaces + tc loss, so it runs only on Linux as root with\n-# netns/tc and SKIPs (77) otherwise.\n-#\n-# Usage:   test_bonded_leg_stall_grace_netns.sh <ristsender> <ristreceiver>\n-# Exit:    0 = short stall rode through on the same session (fixed)\n-#          1 = leg re-authenticated on a sub-grace stall (regression)\n-#          77 = SKIP (needs Linux root + netns + tc)   99 = setup error\n-set -u\n-\n-TX=\"${1:-}\"\n-RX=\"${2:-}\"\n-USER=graceuser; PASS=gracepass; CN=testcn\n-RXIP=10.0.2.51\n-NS=rist_grace_snd\n-RXLOG=\"$(mktemp)\"; TXLOG=\"$(mktemp)\"\n-\n-skip()  { echo \"SKIP: $*\"; cleanup 2>/dev/null; exit 77; }\n-setup_err() { echo \"SETUP ERROR: $*\"; cleanup 2>/dev/null; exit 99; }\n-\n-cleanup() {\n-  ip netns del \"$NS\" 2>/dev/null\n-  ip link del veth-ga 2>/dev/null\n-  ip link del veth-gb 2>/dev/null\n-  ip link del gbr0   2>/dev/null\n-  [ -n \"${TXPID:-}\" ] && kill \"$TXPID\" 2>/dev/null\n-  [ -n \"${RXPID:-}\" ] && kill \"$RXPID\" 2>/dev/null\n-  [ -n \"${FEEDPID:-}\" ] && kill \"$FEEDPID\" 2>/dev/null\n-  rm -f \"$RXLOG\" \"$TXLOG\" 2>/dev/null\n-}\n-trap cleanup EXIT\n-\n-# ---- environment gate: skip cleanly where we cannot reproduce ----------\n-[ \"$(uname -s)\" = \"Linux\" ] || skip \"not Linux\"\n-[ \"$(id -u)\" = \"0\" ] || skip \"needs root (CAP_NET_ADMIN) for netns/veth/tc\"\n-[ -n \"$TX\" ] && [ -x \"$TX\" ] || setup_err \"ristsender not found ($TX)\"\n-[ -n \"$RX\" ] && [ -x \"$RX\" ] || setup_err \"ristreceiver not found ($RX)\"\n-command -v ip >/dev/null 2>&1 || skip \"iproute2 'ip' missing\"\n-command -v tc >/dev/null 2>&1 || skip \"iproute2 'tc' missing\"\n-ip netns add \"$NS\" 2>/dev/null || skip \"cannot create netns (no CAP_NET_ADMIN?)\"\n-\n-# ---- topology: bridge in root ns, two veth legs into a sender netns -----\n-ip link add gbr0 type bridge                        || setup_err \"bridge add\"\n-ip addr add ${RXIP}/24 dev gbr0                      || setup_err \"bridge addr\"\n-ip link set gbr0 up                                  || setup_err \"bridge up\"\n-\n-ip link add veth-ga type veth peer name ga           || setup_err \"veth-ga\"\n-ip link add veth-gb type veth peer name gb           || setup_err \"veth-gb\"\n-ip link set veth-ga master gbr0; ip link set veth-ga up\n-ip link set veth-gb master gbr0; ip link set veth-gb up\n-ip link set ga netns \"$NS\"; ip link set gb netns \"$NS\"\n-ip netns exec \"$NS\" ip addr add 10.0.2.20/24 dev ga\n-ip netns exec \"$NS\" ip addr add 10.0.2.122/24 dev gb\n-ip netns exec \"$NS\" ip link set ga up\n-ip netns exec \"$NS\" ip link set gb up\n-ip netns exec \"$NS\" ip link set lo up\n-# same-subnet multi-homing: disable rp_filter so miface egress is kept\n-ip netns exec \"$NS\" sysctl -qw net.ipv4.conf.all.rp_filter=0\n-ip netns exec \"$NS\" sysctl -qw net.ipv4.conf.ga.rp_filter=0\n-ip netns exec \"$NS\" sysctl -qw net.ipv4.conf.gb.rp_filter=0\n-sysctl -qw net.ipv4.conf.all.rp_filter=0 >/dev/null 2>&1\n-\n-ip netns exec \"$NS\" ping -c1 -W2 -I ga ${RXIP} >/dev/null 2>&1 || skip \"leg A no connectivity\"\n-ip netns exec \"$NS\" ping -c1 -W2 -I gb ${RXIP} >/dev/null 2>&1 || skip \"leg B no connectivity\"\n-\n-# ---- receiver: listener, advanced profile, SRP authenticator -----------\n-# Large buffer so the re-auth grace (~2x buffer) is ~12 s, comfortably above\n-# the 6 s stall below.\n-$RX -p 2 -v 6 \\\n-  -i \"rist://@${RXIP}:2031?username=${USER}&password=${PASS}&buffer=6000\" \\\n-  -o \"udp://127.0.0.1:12346\" >\"$RXLOG\" 2>&1 &\n-RXPID=$!\n-sleep 1\n-\n-# udp feeder inside the sender netns -> ristsender input\n-ip netns exec \"$NS\" bash -c 'while :; do printf \"RISTTESTPACKET%08d\" $RANDOM > /dev/udp/127.0.0.1/5557; sleep 0.02; done' &\n-FEEDPID=$!\n-sleep 0.5\n-\n-# ---- sender: bonded caller, two SRP legs (same cname + creds) ----------\n-ip netns exec \"$NS\" $TX -p 2 -v 6 \\\n-  -i \"udp://@127.0.0.1:5557\" \\\n-  -o \"rist://${RXIP}:2031?miface=ga&weight=10&username=${USER}&password=${PASS}&cname=${CN}&buffer=6000,rist://${RXIP}:2031?miface=gb&weight=5&username=${USER}&password=${PASS}&cname=${CN}&buffer=6000\" \\\n-  >\"$TXLOG\" 2>&1 &\n-TXPID=$!\n-\n-echo \"== warmup 12s (both legs authenticate + stream) ==\"\n-sleep 12\n-auths_setup=$(grep -c \"Successfully authenticated\" \"$RXLOG\")\n-echo \"after warmup: successful auths=${auths_setup}\"\n-if [ \"$auths_setup\" -lt 2 ]; then\n-  echo \"INDETERMINATE: both legs did not authenticate before the stall.\"\n-  exit 99\n-fi\n-\n-echo \"== stall leg B (gb): 100% loss both ways, interface UP, 6s (< grace) ==\"\n-ip netns exec \"$NS\" tc qdisc add dev gb root netem loss 100% || setup_err \"tc add gb\"\n-tc qdisc add dev veth-gb root netem loss 100%                || setup_err \"tc add veth-gb\"\n-sleep 6\n-echo \"== leg B restored (same socket / same source tuple) ==\"\n-ip netns exec \"$NS\" tc qdisc del dev gb root 2>/dev/null\n-tc qdisc del dev veth-gb root 2>/dev/null\n-echo \"== observe 15s (leg B must resume on the SAME session, no re-auth) ==\"\n-sleep 15\n-\n-auths_final=$(grep -c \"Successfully authenticated\" \"$RXLOG\")\n-resets=$(grep -c \"reset EAP and re-initiated the SRP\" \"$TXLOG\")\n-last_sstats=$(grep '\"sender-stats\"' \"$TXLOG\" | tail -1)\n-legs_balancing=$(printf '%s' \"$last_sstats\" | grep -o '\"miface\":\"[^\"]*\"' | sort -u | wc -l)\n-legs_balancing=$(printf '%s' \"$legs_balancing\" | tr -d ' ')\n-echo \"== after stall: successful auths ${auths_setup} -> ${auths_final}, \"\\\n-     \"sender EAP resets=${resets}, legs balancing after recovery=${legs_balancing} ==\"\n-\n-# 1 + 2: a sub-grace stall must not tear the session down. Any sender EAP reset\n-# or a rise in the receiver's successful-auth count means the leg re-handshook\n-# instead of riding through on its existing session.\n-if [ \"$resets\" -ne 0 ] || [ \"$auths_final\" -ne \"$auths_setup\" ]; then\n-  echo \"FAIL: a 6s stall (under the ~12s grace) forced a re-auth\"\\\n-       \"(sender EAP resets=${resets}, successful auths ${auths_setup} ->\"\\\n-       \"${auths_final}); the leg should have resumed on the same session.\"\n-  exit 1\n-fi\n-\n-# 3: the muted leg self-cleared on resume and is back in the weighted bond.\n-if [ \"${legs_balancing:-0}\" -lt 2 ]; then\n-  echo \"FAIL: leg B did not rejoin the weighted bond after the stall\"\\\n-       \"(sender balances over only ${legs_balancing} leg).\"\n-  exit 1\n-fi\n-\n-echo \"PASS: the 6s stall rode through on the same session (no EAP reset,\"\\\n-     \"successful auths stayed ${auths_setup}) and leg B rejoined the bond\"\\\n-     \"(${legs_balancing} legs balancing).\"\n-exit 0\ndiff --git a/test/rist/test_caller_socket_rebind.c b/test/rist/test_caller_socket_rebind.c\ndeleted file mode 100644\nindex 5816b4b..0000000\n--- a/test/rist/test_caller_socket_rebind.c\n+++ /dev/null\n@@ -1,373 +0,0 @@\n-/* librist. Copyright © 2026 SipRadius LLC.\n- * SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Coverage for the receiver-caller socket rebind introduced for\n- * upstream issue #188 (silent recovery from sender silence on\n- * plaintext, shared-PSK and EAP-SRP callers).\n- *\n- * The srp mode is the regression for the EAP-SRP no-rebind bug: a\n- * NAT'd/single-port SRP caller whose listener restarts (losing all\n- * session state) must reset its EAP context and re-authenticate on the\n- * fresh socket by itself.  Before the fix, SRP callers bailed out of the\n- * rebind and were killed, staying idle forever until a manual restart.\n- *\n- * Usage:\n- *   test_caller_socket_rebind [psk|srp]\n- *\n- * Topology (single hop, in-process):\n- *   sender(listener, 127.0.0.1:PORT) <-- receiver(caller, plain|psk|srp)\n- *\n- * Sequence:\n- *   1. Spin up sender + receiver, wait for handshake.\n- *   2. Push a data burst and confirm the receiver decodes it.\n- *   3. Snapshot the receiver peer's local socket fd and local port.\n- *   4. Destroy the sender.  Receiver now sees silence.\n- *   5. Wait > session_timeout.\n- *   6. Re-create a sender on the same listen port.\n- *   7. Wait a couple more session_timeouts for the rebind +\n- *      handshake to fire.\n- *   8. Push another data burst through the recovered path.\n- *   9. PASS when the receiver peer is still alive, its local port\n- *      has changed (rebind happened), rebind_attempts > 0, the new\n- *      sender's listener saw the reconnected caller, AND fresh data\n- *      flowed end to end after recovery.  Also checks that the rebind\n- *      backoff reset once the peer re-authenticated.\n- *\n- * Without the rebind, the receiver peer is killed and never\n- * recovers without operator intervention.\n- *\n- * Exit codes:\n- *   0  - rebind fired, new sender saw the caller, and data flowed again\n- *   1  - receiver was killed, never rebound, or no data post-recovery\n- *   2  - test setup never reached an initial handshake / data path\n- *  99  - setup error\n- */\n-\n-#include \"librist/librist.h\"\n-#include \"librist/librist_srp.h\"\n-#include \"rist-private.h\"\n-#include <inttypes.h>\n-#include <stdbool.h>\n-#include <stdio.h>\n-#include <stdlib.h>\n-#include <string.h>\n-\n-#ifdef _WIN32\n-#include <windows.h>\n-#else\n-#include <unistd.h>\n-#endif\n-\n-#define SRP_USER \"rebinduser\"\n-#define SRP_PASS \"rebindpass-7421\"\n-\n-static struct rist_logging_settings *log_settings = NULL;\n-\n-static struct {\n-\tbool first_seen;\n-\tbool second_seen;\n-\tstruct rist_peer *second_peer;\n-} cb_state;\n-static pthread_mutex_t cb_lock;\n-\n-/* Count of data blocks the receiver actually decoded/delivered. Proves the\n- * post-reconnect path carries real payload, not just a connection event. */\n-static unsigned rx_data_count;\n-\n-static int rx_data_cb(void *arg, struct rist_data_block *b) {\n-\t(void)arg;\n-\tpthread_mutex_lock(&cb_lock);\n-\trx_data_count++;\n-\tpthread_mutex_unlock(&cb_lock);\n-\trist_receiver_data_block_free2(&b);\n-\treturn 0;\n-}\n-\n-static unsigned rx_data_snapshot(void) {\n-\tpthread_mutex_lock(&cb_lock);\n-\tunsigned v = rx_data_count;\n-\tpthread_mutex_unlock(&cb_lock);\n-\treturn v;\n-}\n-\n-/* Send a short burst of RTP-ish payload from the sender. */\n-static void send_burst(struct rist_ctx *tx, uint32_t start, int count) {\n-\tfor (int i = 0; i < count; i++) {\n-\t\tuint32_t v = start + (uint32_t)i;\n-\t\tstruct rist_data_block blk;\n-\t\tmemset(&blk, 0, sizeof(blk));\n-\t\tblk.payload = &v;\n-\t\tblk.payload_len = sizeof(v);\n-\t\tblk.virt_src_port = 1968;\n-\t\trist_sender_data_write(tx, &blk);\n-\t\tusleep(20000); /* ~50 pkt/s */\n-\t}\n-}\n-\n-static int log_cb(void *arg, enum rist_log_level level, const char *msg) {\n-\t(void)arg;\n-\tconst char *tag = level <= RIST_LOG_ERROR ? \"E\" :\n-\t                  level <= RIST_LOG_WARN  ? \"W\" :\n-\t                  level <= RIST_LOG_INFO  ? \"I\" : \"D\";\n-\tfprintf(stderr, \"%s| %s\", tag, msg);\n-\treturn 0;\n-}\n-\n-static void sender_status_cb(void *arg, struct rist_peer *peer,\n-                             enum rist_connection_status status)\n-{\n-\tbool *which = (bool *)arg;\n-\tif (status == RIST_CONNECTION_ESTABLISHED ||\n-\t    status == RIST_CLIENT_CONNECTED) {\n-\t\tpthread_mutex_lock(&cb_lock);\n-\t\t*which = true;\n-\t\tif (which == &cb_state.second_seen)\n-\t\t\tcb_state.second_peer = peer;\n-\t\tpthread_mutex_unlock(&cb_lock);\n-\t\tfprintf(stderr, \">> %s sender CONNECTED (peer=%p)\\n\",\n-\t\t        which == &cb_state.first_seen ? \"first\" : \"second\",\n-\t\t        (void *)peer);\n-\t}\n-}\n-\n-static struct rist_ctx *spawn_sender(int port, void *cb_arg, const char *crypto, bool srp) {\n-\tstruct rist_ctx *tx = NULL;\n-\tif (rist_sender_create(&tx, RIST_PROFILE_MAIN, 0, log_settings) != 0)\n-\t\treturn NULL;\n-\tif (rist_connection_status_callback_set(tx, sender_status_cb, cb_arg) != 0) {\n-\t\trist_destroy(tx);\n-\t\treturn NULL;\n-\t}\n-\tchar url[256];\n-\tsnprintf(url, sizeof(url),\n-\t         \"rist://@127.0.0.1:%d?session-timeout=2000&keepalive-interval=500%s\",\n-\t         port, crypto);\n-\tstruct rist_peer_config *pcfg = NULL;\n-\tif (rist_parse_address2(url, (void *)&pcfg) != 0) {\n-\t\trist_destroy(tx);\n-\t\treturn NULL;\n-\t}\n-\tstruct rist_peer *peer = NULL;\n-\tif (rist_peer_create(tx, &peer, pcfg) != 0) {\n-\t\tfree(pcfg);\n-\t\trist_destroy(tx);\n-\t\treturn NULL;\n-\t}\n-\tfree(pcfg);\n-\t/* Listener side is the SRP authenticator: it supplies the\n-\t * username/password and creates the verifier internally. */\n-\tif (srp && rist_enable_eap_srp_2(peer, SRP_USER, SRP_PASS, NULL, NULL) != 0) {\n-\t\trist_destroy(tx);\n-\t\treturn NULL;\n-\t}\n-\tif (rist_start(tx) != 0) {\n-\t\trist_destroy(tx);\n-\t\treturn NULL;\n-\t}\n-\treturn tx;\n-}\n-\n-int main(int argc, char *argv[]) {\n-\tbool use_psk = (argc > 1 && strcmp(argv[1], \"psk\") == 0);\n-\tbool use_srp = (argc > 1 && strcmp(argv[1], \"srp\") == 0);\n-\tconst char *crypto_suffix = use_psk ? \"&secret=testkey1234&aes-type=128\" : \"\";\n-\tconst int listen_port = use_psk ? 22001 : use_srp ? 22002 : 22000;\n-\t/* SRP needs a full re-handshake after the restart, so give the\n-\t * handshake and reconnect windows more slack than plaintext/PSK. */\n-\tconst unsigned handshake_wait = use_srp ? 5000000 : 2000000;\n-\tconst unsigned reconnect_wait = use_srp ? 6000000 : 4000000;\n-\tmemset(&cb_state, 0, sizeof(cb_state));\n-\tpthread_mutex_init(&cb_lock, NULL);\n-\n-\tif (rist_logging_set(&log_settings, RIST_LOG_INFO, log_cb,\n-\t                     NULL, NULL, stderr) != 0) {\n-\t\tfprintf(stderr, \"logging setup failed\\n\");\n-\t\treturn 99;\n-\t}\n-\n-\tfprintf(stderr, \"== mode: %s ==\\n\",\n-\t        use_psk ? \"PSK encrypted\" : use_srp ? \"SRP authenticated\" : \"plaintext\");\n-\n-\t/* spawn the first sender */\n-\tfprintf(stderr, \"== spawning first sender on :%d ==\\n\", listen_port);\n-\tstruct rist_ctx *tx1 = spawn_sender(listen_port, &cb_state.first_seen, crypto_suffix, use_srp);\n-\tif (!tx1) {\n-\t\tfprintf(stderr, \"sender create failed\\n\");\n-\t\treturn 99;\n-\t}\n-\tusleep(200000);\n-\n-\t/* spawn the receiver caller, no local-port pin, short\n-\t * session_timeout so the test runs fast. */\n-\tfprintf(stderr, \"== spawning receiver caller ==\\n\");\n-\tstruct rist_ctx *rx = NULL;\n-\tif (rist_receiver_create(&rx, RIST_PROFILE_MAIN, log_settings) != 0) {\n-\t\tfprintf(stderr, \"rx ctx create failed\\n\");\n-\t\trist_destroy(tx1);\n-\t\treturn 99;\n-\t}\n-\tchar rx_url[256];\n-\tsnprintf(rx_url, sizeof(rx_url),\n-\t         \"rist://127.0.0.1:%d?session-timeout=2000&keepalive-interval=500\"\n-\t         \"&buffer=200&cname=fix2-test%s\",\n-\t         listen_port, crypto_suffix);\n-\tstruct rist_peer_config *rx_pcfg = NULL;\n-\tif (rist_parse_address2(rx_url, (void *)&rx_pcfg) != 0) {\n-\t\tfprintf(stderr, \"rx url parse failed\\n\");\n-\t\trist_destroy(rx); rist_destroy(tx1);\n-\t\treturn 99;\n-\t}\n-\tstruct rist_peer *rx_peer = NULL;\n-\tif (rist_peer_create(rx, &rx_peer, rx_pcfg) != 0) {\n-\t\tfprintf(stderr, \"rx peer create failed\\n\");\n-\t\tfree(rx_pcfg); rist_destroy(rx); rist_destroy(tx1);\n-\t\treturn 99;\n-\t}\n-\tfree(rx_pcfg);\n-\t/* Caller side is the SRP authenticatee. */\n-\tif (use_srp && rist_enable_eap_srp_2(rx_peer, SRP_USER, SRP_PASS, NULL, NULL) != 0) {\n-\t\tfprintf(stderr, \"rx SRP enable failed\\n\");\n-\t\trist_destroy(rx); rist_destroy(tx1);\n-\t\treturn 99;\n-\t}\n-\tif (rist_receiver_data_callback_set2(rx, rx_data_cb, NULL) != 0) {\n-\t\tfprintf(stderr, \"rx data callback set failed\\n\");\n-\t\trist_destroy(rx); rist_destroy(tx1);\n-\t\treturn 99;\n-\t}\n-\tif (rist_start(rx) != 0) {\n-\t\tfprintf(stderr, \"rx start failed\\n\");\n-\t\trist_destroy(rx); rist_destroy(tx1);\n-\t\treturn 99;\n-\t}\n-\n-\t/* wait for handshake */\n-\tusleep(handshake_wait);\n-\tpthread_mutex_lock(&cb_lock);\n-\tbool first = cb_state.first_seen;\n-\tpthread_mutex_unlock(&cb_lock);\n-\t/* The sender-side connection event is best-effort (in SRP mode the EAP\n-\t * layer authenticates the peer before the RTCP SDES that drives the\n-\t * status callback, so it may never fire). The authoritative \"setup\n-\t * reached a working session\" signal is real data below. */\n-\tfprintf(stderr, \"== sender connection callback seen: %s ==\\n\",\n-\t        first ? \"yes\" : \"no (relying on data-flow check)\");\n-\n-\tint original_sd = rx_peer->sd;\n-\tuint16_t original_port = rx_peer->local_port;\n-\tfprintf(stderr, \"== handshake complete: receiver peer sd=%d \"\n-\t                \"local_port=%u ==\\n\",\n-\t        original_sd, (unsigned)original_port);\n-\n-\t/* Confirm the pre-outage data path actually carries payload. */\n-\tsend_burst(tx1, 0, 25);\n-\tusleep(1000000); /* let the receiver buffer (200ms) drain to the cb */\n-\tif (rx_data_snapshot() == 0) {\n-\t\tfprintf(stderr, \"INDETERMINATE: no data received before the outage; \"\n-\t\t                \"test setup is broken.\\n\");\n-\t\trist_destroy(rx); rist_destroy(tx1);\n-\t\tfree(log_settings);\n-\t\treturn 2;\n-\t}\n-\n-\t/* destroy the sender: receiver now sees silence */\n-\tfprintf(stderr, \"== destroying first sender, waiting for silence \"\n-\t                \"(> session_timeout) ==\\n\");\n-\trist_destroy(tx1);\n-\tusleep(4000000); /* > 2 * session_timeout to make sure the\n-\t                  * receiver-side timeout check ran. */\n-\n-\tbool dead_now = rx_peer->dead;\n-\tuint32_t rebinds = rx_peer->rebind_attempts;\n-\tint sd_now = rx_peer->sd;\n-\tuint16_t port_now = rx_peer->local_port;\n-\tfprintf(stderr, \"== after silence window: rebind_attempts=%\"PRIu32\n-\t                \", dead=%d, sd=%d (was %d), local_port=%u (was %u) ==\\n\",\n-\t        rebinds, dead_now, sd_now, original_sd,\n-\t        (unsigned)port_now, (unsigned)original_port);\n-\n-\tif (rebinds == 0) {\n-\t\tfprintf(stderr, \"FAIL: rebind never fired; rebind_attempts is 0.\\n\");\n-\t\trist_destroy(rx);\n-\t\tfree(log_settings);\n-\t\treturn 1;\n-\t}\n-\tif (dead_now) {\n-\t\tfprintf(stderr, \"FAIL: receiver peer was killed; rebind should \"\n-\t\t                \"have kept it alive across the silence.\\n\");\n-\t\trist_destroy(rx);\n-\t\tfree(log_settings);\n-\t\treturn 1;\n-\t}\n-\tif (port_now == original_port) {\n-\t\tfprintf(stderr, \"FAIL: local port did not change; rebind did \"\n-\t\t                \"not actually re-bind the socket.\\n\");\n-\t\trist_destroy(rx);\n-\t\tfree(log_settings);\n-\t\treturn 1;\n-\t}\n-\n-\t/* bring the sender back up; expect the rebound caller to find it */\n-\tfprintf(stderr, \"== bringing a new sender up on :%d ==\\n\", listen_port);\n-\tstruct rist_ctx *tx2 = spawn_sender(listen_port, &cb_state.second_seen, crypto_suffix, use_srp);\n-\tif (!tx2) {\n-\t\tfprintf(stderr, \"second sender create failed\\n\");\n-\t\trist_destroy(rx);\n-\t\tfree(log_settings);\n-\t\treturn 99;\n-\t}\n-\n-\t/* give the next keepalive + (re-)handshake time to land */\n-\tusleep(reconnect_wait);\n-\n-\t/* Drive real payload through the re-established path and confirm the\n-\t * receiver decodes it: a connection event alone does not prove the\n-\t * (re-)negotiated keys / flow actually work end to end. */\n-\tunsigned data_before = rx_data_snapshot();\n-\tsend_burst(tx2, 1000, 50);\n-\tusleep(1200000); /* > buffer so post-recovery payload reaches the cb */\n-\tunsigned data_after = rx_data_snapshot();\n-\n-\t/* Backoff must reset once the peer re-authenticates (see\n-\t * rist_peer_authenticate), so a later outage retries promptly instead\n-\t * of waiting out an ever-growing gap. */\n-\tuint32_t rebinds_after = rx_peer->rebind_attempts;\n-\n-\tpthread_mutex_lock(&cb_lock);\n-\tbool second = cb_state.second_seen;\n-\tpthread_mutex_unlock(&cb_lock);\n-\n-\trist_destroy(rx);\n-\trist_destroy(tx2);\n-\tfree(log_settings);\n-\n-\t/* Authoritative regression signal: real payload crossed the recovered\n-\t * link. This proves the socket rebind AND the EAP re-handshake AND the\n-\t * re-negotiated keys all work end to end. */\n-\tif (data_after <= data_before) {\n-\t\tfprintf(stderr, \"FAIL: caller reconnected but no data flowed after \"\n-\t\t                \"recovery (before=%u after=%u); keys/flow did not \"\n-\t\t                \"recover end to end.\\n\", data_before, data_after);\n-\t\treturn 1;\n-\t}\n-\n-\t/* Best-effort corroboration; not authoritative (see note above). */\n-\tif (!second)\n-\t\tfprintf(stderr, \"note: sender-side reconnect callback did not fire, \"\n-\t\t                \"but data flow confirms recovery.\\n\");\n-\n-\tif (rebinds_after != 0) {\n-\t\tfprintf(stderr, \"WARN: rebind_attempts did not reset after \"\n-\t\t                \"reconnect (=%\"PRIu32\"); backoff will keep growing \"\n-\t\t                \"across outages.\\n\", rebinds_after);\n-\t}\n-\n-\tfprintf(stderr, \"PASS: receiver auto-rebound from local_port %u to %u \"\n-\t                \"after %\"PRIu32\" attempt(s), the new sender saw the \"\n-\t                \"reconnected caller, and %u data blocks flowed after \"\n-\t                \"recovery (backoff reset -> %\"PRIu32\") without operator \"\n-\t                \"intervention.\\n\",\n-\t        (unsigned)original_port, (unsigned)port_now, rebinds,\n-\t        data_after - data_before, rebinds_after);\n-\treturn 0;\n-}\ndiff --git a/test/rist/test_eap_authenticator_restart.c b/test/rist/test_eap_authenticator_restart.c\ndeleted file mode 100644\nindex 9932e0c..0000000\n--- a/test/rist/test_eap_authenticator_restart.c\n+++ /dev/null\n@@ -1,237 +0,0 @@\n-/* librist. Copyright © 2026 SipRadius LLC.\n- * SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Regression for the EAP-SRP re-authentication lockout after an\n- * *authenticator* (listener/hub) restart.\n- *\n- * process_eap_request_identity() refused every EAP_REQUEST_IDENTITY once the\n- * authenticatee reached EAP_AUTH_STATE_SUCCESS (hardening against a forged\n- * single-packet reset). That is correct for a lone forged\n- * packet, but it also permanently locks out the *legitimate* case: when the\n- * authenticator restarts it loses all session state, treats the caller as a\n- * brand-new peer and drives a fresh handshake by re-sending IDENTITY. A caller\n- * still parked in SUCCESS refused those forever, so the hub sat in \"Waiting for\n- * EAP authentication to happen\" and the data plane carried 0 packets until an\n- * operator restarted the caller.\n- *\n- * This is distinct from the caller *socket* rebind (test_caller_socket_rebind):\n- * that recovers only from listener *silence* (NAT rebind / dead socket). Here\n- * the restarted authenticator is NOT silent -- it actively re-challenges -- so\n- * the silence rebind never fires. We prove that by pinning a long\n- * session_timeout so the rebind path is disabled for the whole test; recovery\n- * can therefore only come from the identity-request re-auth path under test.\n- *\n- * Topology (single hop, in-process):\n- *   sender(listener/authenticator, 127.0.0.1:PORT) <-- receiver(caller/authenticatee)\n- *\n- * Sequence:\n- *   1. Spin up authenticator + authenticatee, wait for the SRP handshake.\n- *   2. Push a data burst and confirm the caller decodes it.\n- *   3. Destroy the authenticator and immediately recreate it on the same port\n- *      (a firmware upgrade / service restart). The caller never goes silent.\n- *   4. Wait for the fresh authenticator to re-challenge and the caller to\n- *      re-authenticate on its own.\n- *   5. Push another burst and confirm it flows.\n- *\n- * PASS when post-restart payload flows AND rebind_attempts == 0 (recovery came\n- * from the identity re-auth path, not the silence rebind). Before the fix the\n- * caller stays locked in SUCCESS, no data flows, and the test fails.\n- *\n- * Exit codes:\n- *   0  - caller re-authenticated against the restarted authenticator, data flowed\n- *   1  - caller never recovered (locked out) / no data after restart\n- *   2  - setup never reached an initial handshake / data path\n- *  99  - setup error\n- */\n-\n-#include \"librist/librist.h\"\n-#include \"librist/librist_srp.h\"\n-#include \"rist-private.h\"\n-#include <inttypes.h>\n-#include <stdbool.h>\n-#include <stdio.h>\n-#include <stdlib.h>\n-#include <string.h>\n-#ifdef _WIN32\n-#include <windows.h>\n-#else\n-#include <unistd.h>\n-#endif\n-\n-#define SRP_USER \"restartuser\"\n-#define SRP_PASS \"restartpass-9137\"\n-\n-/* Long enough that try_caller_socket_rebind (needs silence beyond\n- * max(session_timeout, 4*keepalive)) cannot fire during this test, so recovery\n- * is attributable solely to the identity-request re-auth path. */\n-#define SESSION_TIMEOUT_MS 30000\n-#define KEEPALIVE_MS       500\n-\n-static struct rist_logging_settings *log_settings = NULL;\n-static pthread_mutex_t cb_lock;\n-static unsigned rx_data_count;\n-\n-static int rx_data_cb(void *arg, struct rist_data_block *b) {\n-\t(void)arg;\n-\tpthread_mutex_lock(&cb_lock);\n-\trx_data_count++;\n-\tpthread_mutex_unlock(&cb_lock);\n-\trist_receiver_data_block_free2(&b);\n-\treturn 0;\n-}\n-\n-static unsigned rx_data_snapshot(void) {\n-\tpthread_mutex_lock(&cb_lock);\n-\tunsigned v = rx_data_count;\n-\tpthread_mutex_unlock(&cb_lock);\n-\treturn v;\n-}\n-\n-static void send_burst(struct rist_ctx *tx, uint32_t start, int count) {\n-\tfor (int i = 0; i < count; i++) {\n-\t\tuint32_t v = start + (uint32_t)i;\n-\t\tstruct rist_data_block blk;\n-\t\tmemset(&blk, 0, sizeof(blk));\n-\t\tblk.payload = &v;\n-\t\tblk.payload_len = sizeof(v);\n-\t\tblk.virt_src_port = 1968;\n-\t\trist_sender_data_write(tx, &blk);\n-\t\tusleep(20000); /* ~50 pkt/s */\n-\t}\n-}\n-\n-static int log_cb(void *arg, enum rist_log_level level, const char *msg) {\n-\t(void)arg;\n-\tconst char *tag = level <= RIST_LOG_ERROR ? \"E\" :\n-\t                  level <= RIST_LOG_WARN  ? \"W\" :\n-\t                  level <= RIST_LOG_INFO  ? \"I\" : \"D\";\n-\tfprintf(stderr, \"%s| %s\", tag, msg);\n-\treturn 0;\n-}\n-\n-/* Listener == SRP authenticator. */\n-static struct rist_ctx *spawn_authenticator(int port) {\n-\tstruct rist_ctx *tx = NULL;\n-\tif (rist_sender_create(&tx, RIST_PROFILE_MAIN, 0, log_settings) != 0)\n-\t\treturn NULL;\n-\tchar url[256];\n-\tsnprintf(url, sizeof(url),\n-\t         \"rist://@127.0.0.1:%d?session-timeout=%d&keepalive-interval=%d\",\n-\t         port, SESSION_TIMEOUT_MS, KEEPALIVE_MS);\n-\tstruct rist_peer_config *pcfg = NULL;\n-\tif (rist_parse_address2(url, (void *)&pcfg) != 0) { rist_destroy(tx); return NULL; }\n-\tstruct rist_peer *peer = NULL;\n-\tif (rist_peer_create(tx, &peer, pcfg) != 0) { free(pcfg); rist_destroy(tx); return NULL; }\n-\tfree(pcfg);\n-\tif (rist_enable_eap_srp_2(peer, SRP_USER, SRP_PASS, NULL, NULL) != 0) { rist_destroy(tx); return NULL; }\n-\tif (rist_start(tx) != 0) { rist_destroy(tx); return NULL; }\n-\treturn tx;\n-}\n-\n-int main(void) {\n-\tconst int listen_port = 22012;\n-\tmemset(&rx_data_count, 0, sizeof(rx_data_count));\n-\tpthread_mutex_init(&cb_lock, NULL);\n-\n-\tif (rist_logging_set(&log_settings, RIST_LOG_INFO, log_cb, NULL, NULL, stderr) != 0) {\n-\t\tfprintf(stderr, \"logging setup failed\\n\");\n-\t\treturn 99;\n-\t}\n-\n-\tfprintf(stderr, \"== spawning first authenticator (listener) on :%d ==\\n\", listen_port);\n-\tstruct rist_ctx *tx1 = spawn_authenticator(listen_port);\n-\tif (!tx1) { fprintf(stderr, \"authenticator create failed\\n\"); return 99; }\n-\tusleep(200000);\n-\n-\tfprintf(stderr, \"== spawning caller (authenticatee) ==\\n\");\n-\tstruct rist_ctx *rx = NULL;\n-\tif (rist_receiver_create(&rx, RIST_PROFILE_MAIN, log_settings) != 0) {\n-\t\tfprintf(stderr, \"rx ctx create failed\\n\"); rist_destroy(tx1); return 99;\n-\t}\n-\tchar rx_url[256];\n-\tsnprintf(rx_url, sizeof(rx_url),\n-\t         \"rist://127.0.0.1:%d?session-timeout=%d&keepalive-interval=%d\"\n-\t         \"&buffer=200&cname=restart-test\",\n-\t         listen_port, SESSION_TIMEOUT_MS, KEEPALIVE_MS);\n-\tstruct rist_peer_config *rx_pcfg = NULL;\n-\tif (rist_parse_address2(rx_url, (void *)&rx_pcfg) != 0) {\n-\t\tfprintf(stderr, \"rx url parse failed\\n\"); rist_destroy(rx); rist_destroy(tx1); return 99;\n-\t}\n-\tstruct rist_peer *rx_peer = NULL;\n-\tif (rist_peer_create(rx, &rx_peer, rx_pcfg) != 0) {\n-\t\tfprintf(stderr, \"rx peer create failed\\n\"); free(rx_pcfg); rist_destroy(rx); rist_destroy(tx1); return 99;\n-\t}\n-\tfree(rx_pcfg);\n-\tif (rist_enable_eap_srp_2(rx_peer, SRP_USER, SRP_PASS, NULL, NULL) != 0) {\n-\t\tfprintf(stderr, \"rx SRP enable failed\\n\"); rist_destroy(rx); rist_destroy(tx1); return 99;\n-\t}\n-\tif (rist_receiver_data_callback_set2(rx, rx_data_cb, NULL) != 0) {\n-\t\tfprintf(stderr, \"rx data callback set failed\\n\"); rist_destroy(rx); rist_destroy(tx1); return 99;\n-\t}\n-\tif (rist_start(rx) != 0) {\n-\t\tfprintf(stderr, \"rx start failed\\n\"); rist_destroy(rx); rist_destroy(tx1); return 99;\n-\t}\n-\n-\t/* wait for the initial SRP handshake */\n-\tusleep(5000000);\n-\n-\t/* Confirm the pre-restart data path carries payload. */\n-\tsend_burst(tx1, 0, 25);\n-\tusleep(1000000);\n-\tif (rx_data_snapshot() == 0) {\n-\t\tfprintf(stderr, \"INDETERMINATE: no data before restart; setup broken.\\n\");\n-\t\trist_destroy(rx); rist_destroy(tx1); free(log_settings); return 2;\n-\t}\n-\tfprintf(stderr, \"== initial auth + data path OK (%u blocks) ==\\n\", rx_data_snapshot());\n-\n-\t/* Restart the authenticator: destroy and immediately recreate on the same\n-\t * port. The caller never goes silent (long session_timeout), so the socket\n-\t * rebind cannot fire -- only the identity re-auth path can recover us. */\n-\tfprintf(stderr, \"== restarting authenticator (fresh EAP state, same port) ==\\n\");\n-\trist_destroy(tx1);\n-\tusleep(200000); /* brief gap, well under session_timeout/keepalive windows */\n-\tstruct rist_ctx *tx2 = spawn_authenticator(listen_port);\n-\tif (!tx2) {\n-\t\tfprintf(stderr, \"second authenticator create failed\\n\");\n-\t\trist_destroy(rx); free(log_settings); return 99;\n-\t}\n-\n-\t/* Give the fresh authenticator time to re-challenge (IDENTITY) and the\n-\t * caller time to notice the persistence and re-run the SRP handshake. */\n-\tusleep(8000000);\n-\n-\tunsigned data_before = rx_data_snapshot();\n-\tsend_burst(tx2, 1000, 50);\n-\tusleep(1200000);\n-\tunsigned data_after = rx_data_snapshot();\n-\n-\tuint32_t rebinds = rx_peer->rebind_attempts;\n-\tbool dead = rx_peer->dead;\n-\n-\trist_destroy(rx);\n-\trist_destroy(tx2);\n-\tfree(log_settings);\n-\n-\tfprintf(stderr, \"== after restart: data_before=%u data_after=%u \"\n-\t                \"rebind_attempts=%\"PRIu32\" dead=%d ==\\n\",\n-\t        data_before, data_after, rebinds, dead);\n-\n-\tif (data_after <= data_before) {\n-\t\tfprintf(stderr, \"FAIL: caller did NOT recover after the authenticator \"\n-\t\t                \"restart; no data flowed (locked in SUCCESS, refusing \"\n-\t\t                \"the fresh IDENTITY forever).\\n\");\n-\t\treturn 1;\n-\t}\n-\tif (rebinds != 0) {\n-\t\tfprintf(stderr, \"FAIL: recovery came from the socket rebind \"\n-\t\t                \"(rebind_attempts=%\"PRIu32\"), not the identity re-auth \"\n-\t\t                \"path this test targets; tighten the timing.\\n\", rebinds);\n-\t\treturn 1;\n-\t}\n-\n-\tfprintf(stderr, \"PASS: caller re-authenticated against the restarted \"\n-\t                \"authenticator on its own (%u blocks flowed after restart, \"\n-\t                \"no socket rebind) -- no operator intervention.\\n\",\n-\t        data_after - data_before);\n-\treturn 0;\n-}\ndiff --git a/test/rist/test_multipath_cname.c b/test/rist/test_multipath_cname.c\ndeleted file mode 100644\nindex 8eb97ba..0000000\n--- a/test/rist/test_multipath_cname.c\n+++ /dev/null\n@@ -1,259 +0,0 @@\n-/* librist. Copyright © 2026 SipRadius LLC.\n- * SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Multipath safety check for the cname re-association / caller-side\n- * rebind work (issue #188 and its SRP-only hardening).\n- *\n- * A multipath caller-receiver legitimately presents the SAME cname on\n- * every path.  The listener-side re-association must therefore NOT\n- * collapse two concurrently-alive paths that share a cname into one\n- * peer record, and the caller-side socket rebind must NOT fire on a\n- * path that is healthy.\n- *\n- * Topology (all on 127.0.0.1, MAIN profile):\n- *   sender(listener, streams data)\n- *       <-- receiver path A (caller, cname=X, local-port A, weight 5)\n- *       <-- receiver path B (caller, cname=X, local-port B, weight 5)\n- *\n- * Both paths belong to ONE receiver ctx and share one cname.  They\n- * arrive at the sender as two source tuples under the same listening\n- * parent, i.e. two duplicate-cname siblings.\n- *\n- * Two crypto modes:\n- *   psk  - both paths use the same shared PSK (listener-side\n- *          re-association is SRP-gated, so it never fires here)\n- *   srp  - both paths authenticate with the SAME username/password\n- *          (listener-side re-association CAN fire for SRP, so the\n- *          alive-duplicate guard is what must keep the paths apart)\n- *\n- * Usage:\n- *   test_multipath_cname [srp]\n- *\n- * Pass conditions:\n- *   1. The sender sees TWO distinct callers and keeps seeing two\n- *      across steady state (no spurious cname merge).\n- *   2. The receiver gets the stream.\n- *   3. Neither receiver path rebinds its socket while healthy\n- *      (rebind_attempts == 0) and neither is dead.\n- *\n- * Exit codes:\n- *   0  - both paths stayed distinct, data flowed, no spurious rebind\n- *   1  - paths merged, a healthy path rebound, or a path died\n- *   2  - INDETERMINATE: both paths never established / no data\n- *  99  - setup error\n- */\n-\n-#include \"librist/librist.h\"\n-#include \"librist/librist_srp.h\"\n-#include \"rist-private.h\"\n-#include <inttypes.h>\n-#include <stdbool.h>\n-#include <stdio.h>\n-#include <stdlib.h>\n-#include <string.h>\n-\n-#ifdef _WIN32\n-#include <windows.h>\n-#else\n-#include <unistd.h>\n-#endif\n-\n-#define CNAME \"bonded-1\"\n-#define PSK \"sharedgroupkey9090\"\n-#define SRP_USER \"bondeduser\"\n-#define SRP_PASS \"bondedpass-5566\"\n-\n-static struct rist_logging_settings *log_settings = NULL;\n-\n-static pthread_mutex_t tracker_lock;\n-#define MAX_TRACKED_PEERS 16\n-static struct {\n-\tstruct rist_peer *seen[MAX_TRACKED_PEERS];\n-\tint count;\n-} tracker;\n-\n-static volatile int rx_pkts = 0;\n-static volatile bool sender_run = true;\n-\n-static int log_cb(void *arg, enum rist_log_level level, const char *msg) {\n-\t(void)arg;\n-\tconst char *tag = level <= RIST_LOG_ERROR ? \"E\" :\n-\t                  level <= RIST_LOG_WARN  ? \"W\" :\n-\t                  level <= RIST_LOG_INFO  ? \"I\" : \"D\";\n-\tfprintf(stderr, \"%s| %s\", tag, msg);\n-\treturn 0;\n-}\n-\n-static void sender_status_cb(void *arg, struct rist_peer *peer,\n-                             enum rist_connection_status status) {\n-\t(void)arg;\n-\tif (status == RIST_CONNECTION_ESTABLISHED ||\n-\t    status == RIST_CLIENT_CONNECTED) {\n-\t\tpthread_mutex_lock(&tracker_lock);\n-\t\tbool seen = false;\n-\t\tfor (int i = 0; i < tracker.count; i++)\n-\t\t\tif (tracker.seen[i] == peer) { seen = true; break; }\n-\t\tif (!seen && tracker.count < MAX_TRACKED_PEERS) {\n-\t\t\ttracker.seen[tracker.count++] = peer;\n-\t\t\tfprintf(stderr, \">> sender CONNECTED (peer=%p, distinct_callers=%d)\\n\",\n-\t\t\t        (void *)peer, tracker.count);\n-\t\t}\n-\t\tpthread_mutex_unlock(&tracker_lock);\n-\t}\n-}\n-\n-static int rx_data_cb(void *arg, struct rist_data_block *b) {\n-\t(void)arg;\n-\trx_pkts++;\n-\trist_receiver_data_block_free2(&b);\n-\treturn 0;\n-}\n-\n-static PTHREAD_START_FUNC(sender_feed, arg) {\n-\tstruct rist_ctx *tx = arg;\n-\tuint32_t counter = 0;\n-\twhile (sender_run) {\n-\t\tstruct rist_data_block blk;\n-\t\tmemset(&blk, 0, sizeof(blk));\n-\t\tblk.payload = &counter;\n-\t\tblk.payload_len = sizeof(counter);\n-\t\tblk.virt_src_port = 1968;\n-\t\trist_sender_data_write(tx, &blk);\n-\t\tcounter++;\n-\t\tusleep(20000); /* ~50 pkt/s */\n-\t}\n-\treturn 0;\n-}\n-\n-static struct rist_peer *add_path(struct rist_ctx *rx, int listen_port,\n-                                  int local_port, const char *crypto, bool srp) {\n-\tchar url[320];\n-\tsnprintf(url, sizeof(url),\n-\t         \"rist://127.0.0.1:%d?local-port=%d&cname=%s&weight=5\"\n-\t         \"&session-timeout=30000&keepalive-interval=1000%s\",\n-\t         listen_port, local_port, CNAME, crypto);\n-\tstruct rist_peer_config *pcfg = NULL;\n-\tif (rist_parse_address2(url, (void *)&pcfg) != 0)\n-\t\treturn NULL;\n-\tstruct rist_peer *peer = NULL;\n-\tif (rist_peer_create(rx, &peer, pcfg) != 0) { free(pcfg); return NULL; }\n-\tfree(pcfg);\n-\tif (srp && rist_enable_eap_srp_2(peer, SRP_USER, SRP_PASS, NULL, NULL) != 0)\n-\t\treturn NULL;\n-\treturn peer;\n-}\n-\n-int main(int argc, char *argv[]) {\n-\tbool use_srp = (argc > 1 && strcmp(argv[1], \"srp\") == 0);\n-\tint listen_port = use_srp ? 19958 : 19960;\n-\tint path_a_port = use_srp ? 20071 : 20061;\n-\tint path_b_port = use_srp ? 20072 : 20062;\n-\tchar crypto[96];\n-\tsnprintf(crypto, sizeof(crypto), use_srp ? \"\" : \"&secret=%s&aes-type=128\", PSK);\n-\tchar url[320];\n-\n-\tif (rist_logging_set(&log_settings, RIST_LOG_INFO, log_cb, NULL, NULL, stderr) != 0)\n-\t\treturn 99;\n-\tmemset(&tracker, 0, sizeof(tracker));\n-\tpthread_mutex_init(&tracker_lock, NULL);\n-\tfprintf(stderr, \"== mode: %s ==\\n\", use_srp ? \"SRP (same user/pass on both paths)\"\n-\t                                            : \"shared PSK\");\n-\n-\t/* sender: listener, MAIN, streams data. */\n-\tstruct rist_ctx *tx = NULL;\n-\tif (rist_sender_create(&tx, RIST_PROFILE_MAIN, 0, log_settings) != 0) return 99;\n-\trist_connection_status_callback_set(tx, sender_status_cb, NULL);\n-\tsnprintf(url, sizeof(url),\n-\t         \"rist://@127.0.0.1:%d?session-timeout=30000&keepalive-interval=1000%s\",\n-\t         listen_port, crypto);\n-\tstruct rist_peer_config *tx_pcfg = NULL;\n-\tif (rist_parse_address2(url, (void *)&tx_pcfg) != 0) { rist_destroy(tx); return 99; }\n-\tstruct rist_peer *tx_peer = NULL;\n-\tif (rist_peer_create(tx, &tx_peer, tx_pcfg) != 0) { free(tx_pcfg); rist_destroy(tx); return 99; }\n-\tfree(tx_pcfg);\n-\tif (use_srp && rist_enable_eap_srp_2(tx_peer, SRP_USER, SRP_PASS, NULL, NULL) != 0) {\n-\t\trist_destroy(tx); return 99;\n-\t}\n-\tif (rist_start(tx) != 0) { rist_destroy(tx); return 99; }\n-\n-\tpthread_t feeder;\n-\tpthread_create(&feeder, NULL, sender_feed, tx);\n-\tusleep(200000);\n-\n-\t/* receiver: ONE ctx, TWO caller paths, same cname. */\n-\tfprintf(stderr, \"\\n== receiver brings up two bonded paths (cname=%s) ==\\n\", CNAME);\n-\tstruct rist_ctx *rx = NULL;\n-\tif (rist_receiver_create(&rx, RIST_PROFILE_MAIN, log_settings) != 0) {\n-\t\tsender_run = false; pthread_join(feeder, NULL); rist_destroy(tx); return 99;\n-\t}\n-\tstruct rist_peer *path_a = add_path(rx, listen_port, path_a_port, crypto, use_srp);\n-\tstruct rist_peer *path_b = add_path(rx, listen_port, path_b_port, crypto, use_srp);\n-\tif (!path_a || !path_b) {\n-\t\tsender_run = false; pthread_join(feeder, NULL);\n-\t\trist_destroy(rx); rist_destroy(tx); return 99;\n-\t}\n-\tif (rist_receiver_data_callback_set2(rx, rx_data_cb, NULL) != 0 ||\n-\t    rist_start(rx) != 0) {\n-\t\tsender_run = false; pthread_join(feeder, NULL);\n-\t\trist_destroy(rx); rist_destroy(tx); return 99;\n-\t}\n-\n-\tusleep(use_srp ? 7000000 : 4000000); /* both handshakes complete (SRP is slower) */\n-\n-\tpthread_mutex_lock(&tracker_lock);\n-\tint after_setup = tracker.count;\n-\tpthread_mutex_unlock(&tracker_lock);\n-\tint data_after_setup = rx_pkts;\n-\tfprintf(stderr, \"after setup: distinct_callers=%d, rx_pkts=%d\\n\",\n-\t        after_setup, data_after_setup);\n-\n-\t/* steady state: confirm nothing merges or rebinds later. */\n-\tusleep(4000000);\n-\n-\tpthread_mutex_lock(&tracker_lock);\n-\tint after_steady = tracker.count;\n-\tpthread_mutex_unlock(&tracker_lock);\n-\tuint32_t reb_a = path_a->rebind_attempts;\n-\tuint32_t reb_b = path_b->rebind_attempts;\n-\tint dead_a = path_a->dead;\n-\tint dead_b = path_b->dead;\n-\tfprintf(stderr, \"\\n== steady state: distinct_callers=%d, rx_pkts=%d, \"\n-\t                \"rebind_a=%\"PRIu32\" rebind_b=%\"PRIu32\" dead_a=%d dead_b=%d ==\\n\",\n-\t        after_steady, rx_pkts, reb_a, reb_b, dead_a, dead_b);\n-\n-\tsender_run = false;\n-\tpthread_join(feeder, NULL);\n-\trist_destroy(rx);\n-\trist_destroy(tx);\n-\tfree(log_settings);\n-\n-\tif (after_setup < 2 || data_after_setup == 0) {\n-\t\tfprintf(stderr, \"INDETERMINATE: both paths never established or no \"\n-\t\t                \"data flowed (distinct_callers=%d, rx_pkts=%d).\\n\",\n-\t\t        after_setup, data_after_setup);\n-\t\treturn 2;\n-\t}\n-\n-\tif (after_steady != 2) {\n-\t\tfprintf(stderr, \"FAIL: expected 2 distinct callers, got %d; the two \"\n-\t\t                \"same-cname paths were wrongly merged.\\n\", after_steady);\n-\t\treturn 1;\n-\t}\n-\tif (reb_a != 0 || reb_b != 0) {\n-\t\tfprintf(stderr, \"FAIL: a healthy path rebound its socket \"\n-\t\t                \"(rebind_a=%\"PRIu32\", rebind_b=%\"PRIu32\").\\n\",\n-\t\t        reb_a, reb_b);\n-\t\treturn 1;\n-\t}\n-\tif (dead_a || dead_b) {\n-\t\tfprintf(stderr, \"FAIL: a path died (dead_a=%d, dead_b=%d).\\n\",\n-\t\t        dead_a, dead_b);\n-\t\treturn 1;\n-\t}\n-\n-\tfprintf(stderr, \"\\nPASS: two bonded %s paths sharing cname \\\"%s\\\" stayed \"\n-\t                \"distinct (distinct_callers=2), the stream flowed \"\n-\t                \"(rx_pkts=%d), and neither healthy path rebound.\\n\",\n-\t        use_srp ? \"SRP\" : \"PSK\", CNAME, rx_pkts);\n-\treturn 0;\n-}\ndiff --git a/test/rist/test_multipath_leg_rebind.c b/test/rist/test_multipath_leg_rebind.c\ndeleted file mode 100644\nindex 6cfd725..0000000\n--- a/test/rist/test_multipath_leg_rebind.c\n+++ /dev/null\n@@ -1,270 +0,0 @@\n-/* librist. Copyright © 2026 SipRadius LLC.\n- * SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Mid-stream rebind of ONE bonded leg while the other stays alive.\n- *\n- * This models a NAT source-port rebind on a single leg of a bonded\n- * SRP receiver: from the listening sender's point of view, that leg's\n- * source tuple goes silent and a new tuple (same cname, same SRP\n- * credentials) appears, while the other leg keeps delivering the whole\n- * time.  SRP is used because that is the only mode in which the\n- * listener-side cname re-association is eligible to fire, so this\n- * directly exercises the alive-duplicate guard.\n- *\n- * Sequence (all on 127.0.0.1, MAIN profile, SRP same user/pass):\n- *   1. sender(listener) streams; receiver brings up legs A and B\n- *      (one ctx, same cname, weight 5).  Both authenticate -> 2\n- *      distinct callers, stream flows.\n- *   2. leg A is destroyed (its tuple disappears) and left silent past\n- *      the re-association window; leg B keeps delivering.\n- *   3. leg A' is added on a fresh local port (same cname + creds),\n- *      modelling A's rebound tuple.  While A is silent, B is still\n- *      alive sharing the cname, so the re-association must REFUSE to\n- *      migrate A' into A; A' rejoins as a fresh leg.\n- *\n- * Pass conditions:\n- *   - The stream never stalls: data keeps arriving while leg A is down\n- *     (B carries it) and after A' rejoins.\n- *   - The surviving leg B is untouched: not dead, never rebound.\n- *   - A' rejoins as a separate caller (3 distinct callers seen), i.e.\n- *     the alive-duplicate guard refused the merge while B was alive.\n- *\n- * Exit codes:\n- *   0  - stream stayed up, leg B undisturbed, A' rejoined separately\n- *   1  - stream stalled, leg B disturbed, or A' wrongly merged\n- *   2  - INDETERMINATE: both legs never established / no data\n- *  99  - setup error\n- */\n-\n-#include \"librist/librist.h\"\n-#include \"librist/librist_srp.h\"\n-#include \"rist-private.h\"\n-#include <inttypes.h>\n-#include <stdbool.h>\n-#include <stdio.h>\n-#include <stdlib.h>\n-#include <string.h>\n-\n-#ifdef _WIN32\n-#include <windows.h>\n-#else\n-#include <unistd.h>\n-#endif\n-\n-#define LISTEN_PORT 19956\n-#define LEG_A_PORT 20081\n-#define LEG_B_PORT 20082\n-#define LEG_APRIME_PORT 20083\n-#define CNAME \"bonded-leg\"\n-#define SRP_USER \"leguser\"\n-#define SRP_PASS \"legpass-3344\"\n-\n-static struct rist_logging_settings *log_settings = NULL;\n-\n-static pthread_mutex_t tracker_lock;\n-#define MAX_TRACKED_PEERS 16\n-static struct {\n-\tstruct rist_peer *seen[MAX_TRACKED_PEERS];\n-\tint count;\n-} tracker;\n-\n-static volatile int rx_pkts = 0;\n-static volatile bool sender_run = true;\n-\n-static int log_cb(void *arg, enum rist_log_level level, const char *msg) {\n-\t(void)arg;\n-\tconst char *tag = level <= RIST_LOG_ERROR ? \"E\" :\n-\t                  level <= RIST_LOG_WARN  ? \"W\" :\n-\t                  level <= RIST_LOG_INFO  ? \"I\" : \"D\";\n-\tfprintf(stderr, \"%s| %s\", tag, msg);\n-\treturn 0;\n-}\n-\n-static void sender_status_cb(void *arg, struct rist_peer *peer,\n-                             enum rist_connection_status status) {\n-\t(void)arg;\n-\tif (status == RIST_CONNECTION_ESTABLISHED ||\n-\t    status == RIST_CLIENT_CONNECTED) {\n-\t\tpthread_mutex_lock(&tracker_lock);\n-\t\tbool seen = false;\n-\t\tfor (int i = 0; i < tracker.count; i++)\n-\t\t\tif (tracker.seen[i] == peer) { seen = true; break; }\n-\t\tif (!seen && tracker.count < MAX_TRACKED_PEERS) {\n-\t\t\ttracker.seen[tracker.count++] = peer;\n-\t\t\tfprintf(stderr, \">> sender CONNECTED (peer=%p, distinct_callers=%d)\\n\",\n-\t\t\t        (void *)peer, tracker.count);\n-\t\t}\n-\t\tpthread_mutex_unlock(&tracker_lock);\n-\t}\n-}\n-\n-static int rx_data_cb(void *arg, struct rist_data_block *b) {\n-\t(void)arg;\n-\trx_pkts++;\n-\trist_receiver_data_block_free2(&b);\n-\treturn 0;\n-}\n-\n-static PTHREAD_START_FUNC(sender_feed, arg) {\n-\tstruct rist_ctx *tx = arg;\n-\tuint32_t counter = 0;\n-\twhile (sender_run) {\n-\t\tstruct rist_data_block blk;\n-\t\tmemset(&blk, 0, sizeof(blk));\n-\t\tblk.payload = &counter;\n-\t\tblk.payload_len = sizeof(counter);\n-\t\tblk.virt_src_port = 1968;\n-\t\trist_sender_data_write(tx, &blk);\n-\t\tcounter++;\n-\t\tusleep(20000); /* ~50 pkt/s */\n-\t}\n-\treturn 0;\n-}\n-\n-static struct rist_peer *add_srp_leg(struct rist_ctx *rx, int local_port) {\n-\tchar url[320];\n-\tsnprintf(url, sizeof(url),\n-\t         \"rist://127.0.0.1:%d?local-port=%d&cname=%s&weight=5\"\n-\t         \"&session-timeout=30000&keepalive-interval=1000\",\n-\t         LISTEN_PORT, local_port, CNAME);\n-\tstruct rist_peer_config *pcfg = NULL;\n-\tif (rist_parse_address2(url, (void *)&pcfg) != 0)\n-\t\treturn NULL;\n-\tstruct rist_peer *peer = NULL;\n-\tif (rist_peer_create(rx, &peer, pcfg) != 0) { free(pcfg); return NULL; }\n-\tfree(pcfg);\n-\tif (rist_enable_eap_srp_2(peer, SRP_USER, SRP_PASS, NULL, NULL) != 0)\n-\t\treturn NULL;\n-\treturn peer;\n-}\n-\n-int main(void) {\n-\tchar url[320];\n-\n-\tif (rist_logging_set(&log_settings, RIST_LOG_INFO, log_cb, NULL, NULL, stderr) != 0)\n-\t\treturn 99;\n-\tmemset(&tracker, 0, sizeof(tracker));\n-\tpthread_mutex_init(&tracker_lock, NULL);\n-\n-\t/* sender: listener, MAIN, single-user SRP authenticator, streams. */\n-\tstruct rist_ctx *tx = NULL;\n-\tif (rist_sender_create(&tx, RIST_PROFILE_MAIN, 0, log_settings) != 0) return 99;\n-\trist_connection_status_callback_set(tx, sender_status_cb, NULL);\n-\tsnprintf(url, sizeof(url),\n-\t         \"rist://@127.0.0.1:%d?session-timeout=30000&keepalive-interval=1000\",\n-\t         LISTEN_PORT);\n-\tstruct rist_peer_config *tx_pcfg = NULL;\n-\tif (rist_parse_address2(url, (void *)&tx_pcfg) != 0) { rist_destroy(tx); return 99; }\n-\tstruct rist_peer *tx_peer = NULL;\n-\tif (rist_peer_create(tx, &tx_peer, tx_pcfg) != 0) { free(tx_pcfg); rist_destroy(tx); return 99; }\n-\tfree(tx_pcfg);\n-\tif (rist_enable_eap_srp_2(tx_peer, SRP_USER, SRP_PASS, NULL, NULL) != 0) {\n-\t\trist_destroy(tx); return 99;\n-\t}\n-\tif (rist_start(tx) != 0) { rist_destroy(tx); return 99; }\n-\n-\tpthread_t feeder;\n-\tpthread_create(&feeder, NULL, sender_feed, tx);\n-\tusleep(200000);\n-\n-\t/* receiver: one ctx, two bonded SRP legs, same cname + creds. */\n-\tfprintf(stderr, \"\\n== receiver brings up bonded legs A and B (cname=%s) ==\\n\", CNAME);\n-\tstruct rist_ctx *rx = NULL;\n-\tif (rist_receiver_create(&rx, RIST_PROFILE_MAIN, log_settings) != 0) {\n-\t\tsender_run = false; pthread_join(feeder, NULL); rist_destroy(tx); return 99;\n-\t}\n-\tstruct rist_peer *leg_a = add_srp_leg(rx, LEG_A_PORT);\n-\tstruct rist_peer *leg_b = add_srp_leg(rx, LEG_B_PORT);\n-\tif (!leg_a || !leg_b) {\n-\t\tsender_run = false; pthread_join(feeder, NULL);\n-\t\trist_destroy(rx); rist_destroy(tx); return 99;\n-\t}\n-\tif (rist_receiver_data_callback_set2(rx, rx_data_cb, NULL) != 0 ||\n-\t    rist_start(rx) != 0) {\n-\t\tsender_run = false; pthread_join(feeder, NULL);\n-\t\trist_destroy(rx); rist_destroy(tx); return 99;\n-\t}\n-\n-\tusleep(7000000); /* both SRP handshakes complete */\n-\n-\tpthread_mutex_lock(&tracker_lock);\n-\tint distinct_setup = tracker.count;\n-\tpthread_mutex_unlock(&tracker_lock);\n-\tint rx_before = rx_pkts;\n-\tfprintf(stderr, \"after setup: distinct_callers=%d, rx_pkts=%d\\n\",\n-\t        distinct_setup, rx_before);\n-\n-\t/* model leg A's NAT rebind: drop its tuple, let it go silent past\n-\t * the re-association window while leg B keeps delivering. */\n-\tfprintf(stderr, \"\\n== leg A tuple drops (NAT rebind); leg B keeps streaming ==\\n\");\n-\trist_peer_destroy(rx, leg_a);\n-\tusleep(3000000); /* > 2 * keepalive: A is now a silent candidate */\n-\tint rx_mid = rx_pkts;\n-\tfprintf(stderr, \"while leg A down: rx_pkts=%d (was %d)\\n\", rx_mid, rx_before);\n-\n-\t/* leg A reappears on a fresh source tuple (same cname + creds). */\n-\tfprintf(stderr, \"\\n== leg A' reappears on a new local port (same cname/creds) ==\\n\");\n-\tstruct rist_peer *leg_aprime = add_srp_leg(rx, LEG_APRIME_PORT);\n-\tif (!leg_aprime) {\n-\t\tsender_run = false; pthread_join(feeder, NULL);\n-\t\trist_destroy(rx); rist_destroy(tx); return 99;\n-\t}\n-\tusleep(7000000); /* A' handshake; re-association must refuse merge */\n-\n-\tpthread_mutex_lock(&tracker_lock);\n-\tint distinct_rebind = tracker.count;\n-\tpthread_mutex_unlock(&tracker_lock);\n-\tint rx_after = rx_pkts;\n-\tuint32_t reb_b = leg_b->rebind_attempts;\n-\tint dead_b = leg_b->dead;\n-\tfprintf(stderr, \"\\n== after A' rejoin: distinct_callers=%d, rx_pkts=%d, \"\n-\t                \"leg_b rebind=%\"PRIu32\" dead=%d ==\\n\",\n-\t        distinct_rebind, rx_after, reb_b, dead_b);\n-\n-\tsender_run = false;\n-\tpthread_join(feeder, NULL);\n-\trist_destroy(rx);\n-\trist_destroy(tx);\n-\tfree(log_settings);\n-\n-\tif (distinct_setup < 2 || rx_before == 0) {\n-\t\tfprintf(stderr, \"INDETERMINATE: both legs never established or no data \"\n-\t\t                \"(distinct_callers=%d, rx_pkts=%d).\\n\", distinct_setup, rx_before);\n-\t\treturn 2;\n-\t}\n-\tif (rx_mid <= rx_before) {\n-\t\tfprintf(stderr, \"FAIL: stream stalled while leg A was down; bonding did \"\n-\t\t                \"not keep flowing on leg B (rx %d -> %d).\\n\", rx_before, rx_mid);\n-\t\treturn 1;\n-\t}\n-\tif (rx_after <= rx_mid) {\n-\t\tfprintf(stderr, \"FAIL: stream stalled after leg A' rejoined \"\n-\t\t                \"(rx %d -> %d).\\n\", rx_mid, rx_after);\n-\t\treturn 1;\n-\t}\n-\tif (dead_b) {\n-\t\tfprintf(stderr, \"FAIL: surviving leg B died during the other leg's \"\n-\t\t                \"rebind.\\n\");\n-\t\treturn 1;\n-\t}\n-\tif (reb_b != 0) {\n-\t\tfprintf(stderr, \"FAIL: surviving leg B rebound its own socket \"\n-\t\t                \"(rebind_attempts=%\"PRIu32\"); it should have been \"\n-\t\t                \"undisturbed.\\n\", reb_b);\n-\t\treturn 1;\n-\t}\n-\tif (distinct_rebind != 3) {\n-\t\tfprintf(stderr, \"FAIL: expected leg A' to rejoin as a separate caller \"\n-\t\t                \"(3 distinct), got %d; the alive-duplicate guard did not \"\n-\t\t                \"behave as expected while leg B was alive.\\n\", distinct_rebind);\n-\t\treturn 1;\n-\t}\n-\n-\tfprintf(stderr, \"\\nPASS: one bonded SRP leg rebound mid-stream while the \"\n-\t                \"other kept delivering (rx %d->%d->%d, no stall); surviving \"\n-\t                \"leg B was undisturbed (not dead, no rebind) and the rebound \"\n-\t                \"leg rejoined as a separate caller (distinct_callers=3, no \"\n-\t                \"wrong merge while a same-cname leg was alive).\\n\",\n-\t        rx_before, rx_mid, rx_after);\n-\treturn 0;\n-}\ndiff --git a/test/rist/test_nat_port_rebind.c b/test/rist/test_nat_port_rebind.c\ndeleted file mode 100644\nindex c665e0e..0000000\n--- a/test/rist/test_nat_port_rebind.c\n+++ /dev/null\n@@ -1,296 +0,0 @@\n-/* librist. Copyright © 2026 SipRadius LLC.\n- * SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Coverage for the listener-side cname re-association introduced\n- * for upstream issue #188 (NAT source-port rebind on a calling\n- * receiver), and for the security hardening that restricts it to\n- * authenticated SRP sessions only.\n- *\n- * Topology:\n- *   sender(listener, 127.0.0.1:LISTEN_PORT) <-- receiver(caller, src=PORT_A) round 1\n- *                                           <-- receiver(caller, src=PORT_B) round 2\n- *\n- * Both receivers use the same cname so they look like the same\n- * logical caller to the sender.  The expected distinct-caller count\n- * on the sender depends on the crypto mode:\n- *\n- *   plaintext  -> 2 distinct callers  (gate refuses to migrate; the\n- *                                      plaintext recovery path is the\n- *                                      receiver-side socket rebind,\n- *                                      covered by\n- *                                      test_caller_socket_rebind.c)\n- *   psk        -> 2 distinct callers  (gate refuses to migrate; under\n- *                                      a shared PSK the cname is not a\n- *                                      per-peer secret, so a forged\n- *                                      cname must NOT be allowed to\n- *                                      take over an existing peer\n- *                                      record -- see the security\n- *                                      reproducer test_psk_cname_hijack.c)\n- *   srp        -> 1 distinct caller   (migration fires; the caller is\n- *                                      authenticated with a per-peer\n- *                                      SRP session, so absorbing the\n- *                                      new source tuple into the\n- *                                      existing peer record is safe)\n- *\n- * The test passes when the observed count matches the expected count\n- * for the chosen mode.  A mismatch indicates either a regression in\n- * the SRP-only gate (plaintext/psk silently migrated -> 1 instead of\n- * 2) or in the migration logic itself (srp failed to migrate -> 2\n- * instead of 1).\n- *\n- * Usage:\n- *   test_nat_port_rebind [psk|srp]\n- *\n- * Exit codes:\n- *   0  - observed count matched expected count for the chosen mode\n- *   1  - count mismatch (regression in gate or in migration logic)\n- *   2  - round 1 handshake never completed (test setup broken)\n- *  99  - test setup error (logging, ctx create, etc.)\n- */\n-\n-#include \"librist/librist.h\"\n-#include \"librist/librist_srp.h\"\n-#include <stdio.h>\n-#include <stdlib.h>\n-#include <string.h>\n-#include <stdbool.h>\n-#include \"pthread-shim.h\"\n-\n-#ifdef _WIN32\n-#include <windows.h>\n-#define usleep(x) Sleep((x)/1000)\n-#else\n-#include <unistd.h>\n-#endif\n-\n-#define SRP_USER \"rebinduser\"\n-#define SRP_PASS \"rebindpass-7421\"\n-\n-static struct rist_logging_settings *log_settings = NULL;\n-\n-/* Each distinct rist_peer pointer on the listener side represents\n- * one logical caller arrival (the callback gates on\n- * send_first_connection_event so duplicates per peer do not fire). */\n-#define MAX_TRACKED_PEERS 16\n-static struct {\n-\tstruct rist_peer *seen[MAX_TRACKED_PEERS];\n-\tint count;\n-} tracker;\n-static pthread_mutex_t tracker_lock;\n-\n-static int log_cb(void *arg, enum rist_log_level level, const char *msg) {\n-\t(void)arg;\n-\tconst char *tag = level <= RIST_LOG_ERROR ? \"E\" :\n-\t                  level <= RIST_LOG_WARN  ? \"W\" :\n-\t                  level <= RIST_LOG_INFO  ? \"I\" : \"D\";\n-\tfprintf(stderr, \"%s| %s\", tag, msg);\n-\treturn 0;\n-}\n-\n-static void sender_status_cb(void *arg, struct rist_peer *peer,\n-                             enum rist_connection_status status)\n-{\n-\t(void)arg;\n-\tif (status == RIST_CONNECTION_ESTABLISHED ||\n-\t    status == RIST_CLIENT_CONNECTED) {\n-\t\tpthread_mutex_lock(&tracker_lock);\n-\t\tbool seen = false;\n-\t\tfor (int i = 0; i < tracker.count; i++) {\n-\t\t\tif (tracker.seen[i] == peer) { seen = true; break; }\n-\t\t}\n-\t\tif (!seen && tracker.count < MAX_TRACKED_PEERS) {\n-\t\t\ttracker.seen[tracker.count++] = peer;\n-\t\t\tfprintf(stderr, \">> sender CONNECTED (peer=%p, \"\n-\t\t\t                \"distinct_callers=%d)\\n\",\n-\t\t\t        (void *)peer, tracker.count);\n-\t\t}\n-\t\tpthread_mutex_unlock(&tracker_lock);\n-\t} else if (status == RIST_CONNECTION_TIMED_OUT ||\n-\t           status == RIST_CLIENT_TIMED_OUT) {\n-\t\tfprintf(stderr, \">> sender TIMED_OUT (peer=%p)\\n\", (void *)peer);\n-\t}\n-}\n-\n-static struct rist_ctx *start_receiver_with_local_port(const char *url, bool srp) {\n-\tstruct rist_ctx *rx = NULL;\n-\tif (rist_receiver_create(&rx, RIST_PROFILE_MAIN, log_settings) != 0)\n-\t\treturn NULL;\n-\tstruct rist_peer_config *pcfg = NULL;\n-\tif (rist_parse_address2(url, (void *)&pcfg) != 0) {\n-\t\trist_destroy(rx);\n-\t\treturn NULL;\n-\t}\n-\tstruct rist_peer *peer = NULL;\n-\tif (rist_peer_create(rx, &peer, pcfg) != 0) {\n-\t\tfree(pcfg);\n-\t\trist_destroy(rx);\n-\t\treturn NULL;\n-\t}\n-\tfree(pcfg);\n-\tif (srp && rist_enable_eap_srp_2(peer, SRP_USER, SRP_PASS, NULL, NULL) != 0) {\n-\t\trist_destroy(rx);\n-\t\treturn NULL;\n-\t}\n-\tif (rist_start(rx) != 0) {\n-\t\trist_destroy(rx);\n-\t\treturn NULL;\n-\t}\n-\treturn rx;\n-}\n-\n-int main(int argc, char *argv[]) {\n-\tbool use_psk = (argc > 1 && strcmp(argv[1], \"psk\") == 0);\n-\tbool use_srp = (argc > 1 && strcmp(argv[1], \"srp\") == 0);\n-\tconst char *crypto_suffix = use_psk ? \"&secret=testkey1234&aes-type=128\" : \"\";\n-\tconst char *mode_name = use_psk ? \"PSK encrypted\"\n-\t                       : use_srp ? \"SRP authenticated\"\n-\t                                 : \"plaintext\";\n-\tint listen_port = use_psk ? 19998 : use_srp ? 19997 : 20998;\n-\tchar tx_url[256];\n-\tchar rx_url[256];\n-\tsnprintf(tx_url, sizeof(tx_url),\n-\t         \"rist://@127.0.0.1:%d?session-timeout=30000&keepalive-interval=1000%s\",\n-\t         listen_port, crypto_suffix);\n-\n-\tmemset(&tracker, 0, sizeof(tracker));\n-\tpthread_mutex_init(&tracker_lock, NULL);\n-\n-\tif (rist_logging_set(&log_settings, RIST_LOG_INFO, log_cb,\n-\t                     NULL, NULL, stderr) != 0) {\n-\t\tfprintf(stderr, \"logging setup failed\\n\");\n-\t\treturn 99;\n-\t}\n-\n-\t/* sender, listener mode, MAIN profile.\n-\t * session-timeout is set well above the inter-round gap so the\n-\t * round-1 peer record is still in the listener's child list when\n-\t * the round-2 packets arrive but has been silent long enough to\n-\t * cross the cname-reassociation safeguard window. */\n-\tstruct rist_ctx *tx = NULL;\n-\tif (rist_sender_create(&tx, RIST_PROFILE_MAIN, 0, log_settings) != 0) {\n-\t\tfprintf(stderr, \"sender ctx create failed\\n\");\n-\t\treturn 99;\n-\t}\n-\tif (rist_connection_status_callback_set(tx, sender_status_cb, NULL) != 0) {\n-\t\tfprintf(stderr, \"status cb set failed\\n\");\n-\t\trist_destroy(tx);\n-\t\treturn 99;\n-\t}\n-\tfprintf(stderr, \"== mode: %s, listen port %d ==\\n\", mode_name, listen_port);\n-\tstruct rist_peer_config *tx_pcfg = NULL;\n-\tif (rist_parse_address2(tx_url, (void *)&tx_pcfg) != 0) {\n-\t\tfprintf(stderr, \"sender url parse failed\\n\");\n-\t\trist_destroy(tx);\n-\t\treturn 99;\n-\t}\n-\tstruct rist_peer *tx_peer = NULL;\n-\tif (rist_peer_create(tx, &tx_peer, tx_pcfg) != 0) {\n-\t\tfprintf(stderr, \"sender peer create failed\\n\");\n-\t\tfree(tx_pcfg);\n-\t\trist_destroy(tx);\n-\t\treturn 99;\n-\t}\n-\tfree(tx_pcfg);\n-\t/* listener single-user SRP authenticator: knows the same\n-\t * username/password and creates the verifier internally. */\n-\tif (use_srp && rist_enable_eap_srp_2(tx_peer, SRP_USER, SRP_PASS, NULL, NULL) != 0) {\n-\t\tfprintf(stderr, \"sender SRP enable failed\\n\");\n-\t\trist_destroy(tx);\n-\t\treturn 99;\n-\t}\n-\tif (rist_start(tx) != 0) {\n-\t\tfprintf(stderr, \"sender start failed\\n\");\n-\t\trist_destroy(tx);\n-\t\treturn 99;\n-\t}\n-\tusleep(200000); /* let listener bind */\n-\n-\t/* round 1: receiver caller, source port 20001, cname \"rebind-test\".\n-\t * session-timeout matches the sender so the receiver-side\n-\t * silence-recovery path does not fire during this test. */\n-\tfprintf(stderr, \"\\n== round 1: receiver from local-port=20001 ==\\n\");\n-\tsnprintf(rx_url, sizeof(rx_url),\n-\t         \"rist://127.0.0.1:%d?local-port=20001&cname=rebind-test\"\n-\t         \"&session-timeout=30000&keepalive-interval=1000%s\",\n-\t         listen_port, crypto_suffix);\n-\tstruct rist_ctx *rx1 = start_receiver_with_local_port(rx_url, use_srp);\n-\tif (!rx1) {\n-\t\tfprintf(stderr, \"rx1 setup failed\\n\");\n-\t\trist_destroy(tx);\n-\t\treturn 99;\n-\t}\n-\n-\t/* let handshake complete (SRP needs an extra round trip) */\n-\tusleep(use_srp ? 5000000 : 3000000);\n-\n-\tpthread_mutex_lock(&tracker_lock);\n-\tint after_round1 = tracker.count;\n-\tpthread_mutex_unlock(&tracker_lock);\n-\tfprintf(stderr, \"after round 1: distinct callers on sender = %d\\n\",\n-\t        after_round1);\n-\n-\t/* tear down round-1 receiver, wait long enough that its peer\n-\t * record on the sender side has been silent past the\n-\t * cname-reassociation safeguard window (2 keepalive intervals),\n-\t * then start round-2 with a different source port and the same\n-\t * cname.  The sibling is still well within session_timeout, so\n-\t * it is still in the listener's child list when round 2 arrives. */\n-\tfprintf(stderr, \"\\n== destroying rx1; waiting for silence ==\\n\");\n-\trist_destroy(rx1);\n-\tusleep(3000000); /* > 2 * keepalive_interval, < session_timeout */\n-\n-\tfprintf(stderr, \"\\n== round 2: receiver from local-port=20002, \"\n-\t                \"same cname ==\\n\");\n-\tsnprintf(rx_url, sizeof(rx_url),\n-\t         \"rist://127.0.0.1:%d?local-port=20002&cname=rebind-test\"\n-\t         \"&session-timeout=30000&keepalive-interval=1000%s\",\n-\t         listen_port, crypto_suffix);\n-\tstruct rist_ctx *rx2 = start_receiver_with_local_port(rx_url, use_srp);\n-\tif (!rx2) {\n-\t\tfprintf(stderr, \"rx2 setup failed\\n\");\n-\t\trist_destroy(tx);\n-\t\treturn 99;\n-\t}\n-\n-\t/* let round-2 handshake complete */\n-\tusleep(use_srp ? 5000000 : 3000000);\n-\n-\tpthread_mutex_lock(&tracker_lock);\n-\tint after_round2 = tracker.count;\n-\tpthread_mutex_unlock(&tracker_lock);\n-\tfprintf(stderr, \"\\n== after round 2: distinct callers on sender = %d ==\\n\",\n-\t        after_round2);\n-\n-\trist_destroy(rx2);\n-\trist_destroy(tx);\n-\tfree(log_settings);\n-\n-\tif (after_round1 == 0) {\n-\t\tfprintf(stderr, \"INDETERMINATE: round 1 handshake never completed.\\n\");\n-\t\treturn 2;\n-\t}\n-\n-\tint expected = use_srp ? 1 : 2;\n-\tif (after_round2 == expected) {\n-\t\tif (use_srp)\n-\t\t\tfprintf(stderr, \"PASS: authenticated-SRP migration absorbed \"\n-\t\t\t                \"the new tuple into the existing peer record \"\n-\t\t\t                \"(distinct_callers=%d as expected).\\n\",\n-\t\t\t        after_round2);\n-\t\telse\n-\t\t\tfprintf(stderr, \"PASS: SRP-only gate refused migration in %s \"\n-\t\t\t                \"mode (distinct_callers=%d as expected; \"\n-\t\t\t                \"receiver-side recovery is covered \"\n-\t\t\t                \"separately).\\n\", mode_name, after_round2);\n-\t\treturn 0;\n-\t}\n-\tif (use_srp)\n-\t\tfprintf(stderr, \"FAIL: authenticated-SRP migration did not fire; \"\n-\t\t                \"distinct_callers=%d, expected 1.\\n\",\n-\t\t        after_round2);\n-\telse\n-\t\tfprintf(stderr, \"FAIL: SRP-only gate did NOT refuse migration in \"\n-\t\t                \"%s mode; distinct_callers=%d, expected 2.\\n\",\n-\t\t        mode_name, after_round2);\n-\treturn 1;\n-}\ndiff --git a/test/rist/test_oob_read.c b/test/rist/test_oob_read.c\ndeleted file mode 100644\nindex 5bc6451..0000000\n--- a/test/rist/test_oob_read.c\n+++ /dev/null\n@@ -1,196 +0,0 @@\n-/* librist. Copyright © 2024 SipRadius LLC. All right reserved.\n- * SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Coverage for the polling out-of-band receive API (rist_oob_read).\n- * A sender pushes OOB packets to a receiver that has oob enabled with a\n- * NULL callback; the receiver must be able to drain them through\n- * rist_oob_read() and see the exact payloads back.\n- */\n-\n-#include \"librist/librist.h\"\n-#include \"rist-private.h\"\n-#include <stdatomic.h>\n-#include <inttypes.h>\n-#include <string.h>\n-\n-#ifdef _WIN32\n-#include <windows.h>\n-#define msleep(ms) Sleep(ms)\n-#else\n-#include <unistd.h>\n-#define msleep(ms) usleep((ms) * 1000)\n-#endif\n-\n-#define OOB_PAYLOAD_PREFIX \"RIST-OOB-TEST-PACKET-\"\n-#define OOB_TARGET_COUNT 25\n-\n-atomic_ulong stop;\n-\n-struct rist_logging_settings *logging_settings_sender = NULL;\n-struct rist_logging_settings *logging_settings_receiver = NULL;\n-char *senderstring = \"sender\";\n-char *receiverstring = \"receiver\";\n-\n-static struct rist_peer *sender_peer = NULL;\n-\n-static int log_callback(void *arg, int level, const char *msg) {\n-    /* Print but never fail the test on a log line: oob is best effort and a\n-     * transient \"no oob peer\" during connection setup is expected. */\n-    if (level <= RIST_LOG_ERROR)\n-        fprintf(stdout, \"[%s] [ERROR] %s\", (char *)arg, msg);\n-    else\n-        fprintf(stdout, \"[%s] %s\", (char *)arg, msg);\n-    return 0;\n-}\n-\n-static struct rist_ctx *setup_rist_receiver(int profile, const char *url) {\n-    struct rist_ctx *ctx;\n-    if (rist_receiver_create(&ctx, profile, logging_settings_receiver) != 0)\n-        return NULL;\n-    struct rist_peer_config *peer_config = NULL;\n-    if (rist_parse_address2(url, (void *)&peer_config))\n-        return NULL;\n-    struct rist_peer *peer;\n-    if (rist_peer_create(ctx, &peer, peer_config) == -1) {\n-        free((void *)peer_config);\n-        return NULL;\n-    }\n-    free((void *)peer_config);\n-    if (rist_oob_callback_set(ctx, NULL, NULL) != 0) {\n-        rist_log(logging_settings_receiver, RIST_LOG_ERROR, \"Could not enable oob on receiver\\n\");\n-        return NULL;\n-    }\n-    if (rist_start(ctx) == -1)\n-        return NULL;\n-    return ctx;\n-}\n-\n-static struct rist_ctx *setup_rist_sender(int profile, const char *url) {\n-    struct rist_ctx *ctx;\n-    if (rist_sender_create(&ctx, profile, 0, logging_settings_sender) != 0)\n-        return NULL;\n-    const struct rist_peer_config *peer_config = NULL;\n-    if (rist_parse_address2(url, (void *)&peer_config))\n-        return NULL;\n-    if (rist_peer_create(ctx, &sender_peer, peer_config) == -1) {\n-        free((void *)peer_config);\n-        return NULL;\n-    }\n-    free((void *)peer_config);\n-    if (rist_oob_callback_set(ctx, NULL, NULL) != 0) {\n-        rist_log(logging_settings_sender, RIST_LOG_ERROR, \"Could not enable oob on sender\\n\");\n-        return NULL;\n-    }\n-    if (rist_start(ctx) == -1)\n-        return NULL;\n-    return ctx;\n-}\n-\n-static PTHREAD_START_FUNC(send_oob, arg) {\n-    struct rist_ctx *sender = arg;\n-    char buffer[128];\n-    int counter = 0;\n-    /* ~6s worth of attempts at ~2ms spacing */\n-    while (counter < 3000 && !atomic_load(&stop)) {\n-        int len = snprintf(buffer, sizeof(buffer), \"%s%d\", OOB_PAYLOAD_PREFIX, counter);\n-        struct rist_oob_block oob = { 0 };\n-        oob.peer = sender_peer;\n-        oob.payload = buffer;\n-        oob.payload_len = (size_t)len + 1; /* include NUL for easy compare */\n-        rist_oob_write(sender, &oob); /* best effort; ignore transient errors */\n-        counter++;\n-        msleep(2);\n-    }\n-    return 0;\n-}\n-\n-int main(int argc, char *argv[]) {\n-    int profile = (argc > 1) ? atoi(argv[1]) : 1;\n-    const char *recv_url = (argc > 2) ? argv[2] : \"rist://@127.0.0.1:8190\";\n-    const char *send_url = (argc > 3) ? argv[3] : \"rist://127.0.0.1:8190\";\n-\n-    atomic_init(&stop, 0);\n-\n-    fprintf(stdout, \"Testing oob polling read on profile %d (recv %s, send %s)\\n\",\n-            profile, recv_url, send_url);\n-\n-    if (rist_logging_set(&logging_settings_sender, RIST_LOG_INFO, log_callback, senderstring, NULL, stderr) != 0)\n-        return 99;\n-    if (rist_logging_set(&logging_settings_receiver, RIST_LOG_INFO, log_callback, receiverstring, NULL, stderr) != 0)\n-        return 99;\n-\n-    struct rist_ctx *receiver_ctx = setup_rist_receiver(profile, recv_url);\n-    struct rist_ctx *sender_ctx = setup_rist_sender(profile, send_url);\n-    int ret = 0;\n-    if (!receiver_ctx || !sender_ctx) {\n-        ret = 99;\n-        goto out;\n-    }\n-\n-    pthread_t send_loop;\n-    if (pthread_create(&send_loop, NULL, send_oob, (void *)sender_ctx) != 0) {\n-        ret = 99;\n-        goto out;\n-    }\n-\n-    int received = 0;\n-    int last_seen = -1;\n-    bool ordered = true;\n-    /* poll for up to ~8 seconds */\n-    for (int i = 0; i < 4000 && received < OOB_TARGET_COUNT; i++) {\n-        const struct rist_oob_block *oob = NULL;\n-        int avail = rist_oob_read(receiver_ctx, &oob);\n-        if (avail < 0) {\n-            fprintf(stderr, \"rist_oob_read returned error %d\\n\", avail);\n-            ret = 1;\n-            break;\n-        }\n-        if (avail == 0 || oob == NULL) {\n-            msleep(2);\n-            continue;\n-        }\n-        /* validate payload shape */\n-        if (oob->payload_len < strlen(OOB_PAYLOAD_PREFIX) + 1 ||\n-            memcmp(oob->payload, OOB_PAYLOAD_PREFIX, strlen(OOB_PAYLOAD_PREFIX)) != 0) {\n-            fprintf(stderr, \"Unexpected oob payload (len %zu)\\n\", oob->payload_len);\n-            ret = 1;\n-            break;\n-        }\n-        int seq = atoi((const char *)oob->payload + strlen(OOB_PAYLOAD_PREFIX));\n-        if (seq <= last_seen)\n-            ordered = false; /* not fatal: oob is unordered/best effort, just note */\n-        last_seen = seq;\n-        if (oob->peer == NULL) {\n-            fprintf(stderr, \"oob block missing peer\\n\");\n-            ret = 1;\n-            break;\n-        }\n-        received++;\n-    }\n-\n-    atomic_store(&stop, 1);\n-    pthread_join(send_loop, NULL);\n-\n-    fprintf(stdout, \"Received %d oob packets (target %d), ordered=%s\\n\",\n-            received, OOB_TARGET_COUNT, ordered ? \"yes\" : \"no\");\n-    if (ret == 0 && received < OOB_TARGET_COUNT) {\n-        fprintf(stderr, \"FAIL: only %d/%d oob packets read back\\n\", received, OOB_TARGET_COUNT);\n-        ret = 1;\n-    }\n-\n-out:\n-    if (sender_ctx)\n-        rist_destroy(sender_ctx);\n-    if (receiver_ctx)\n-        rist_destroy(receiver_ctx);\n-    free(logging_settings_sender);\n-    free(logging_settings_receiver);\n-    if (ret > 0 && ret != 99) {\n-        fprintf(stderr, \"FAIL\\n\");\n-        return ret;\n-    }\n-    if (ret == 99)\n-        return 99;\n-    fprintf(stdout, \"OK\\n\");\n-    return 0;\n-}\ndiff --git a/test/rist/test_psk_cname_hijack.c b/test/rist/test_psk_cname_hijack.c\ndeleted file mode 100644\nindex 9270af1..0000000\n--- a/test/rist/test_psk_cname_hijack.c\n+++ /dev/null\n@@ -1,267 +0,0 @@\n-/* librist. Copyright © 2026 SipRadius LLC.\n- * SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Security regression guard: shared-PSK cname identity takeover.\n- *\n- * The original defect: listener-side cname re-association\n- * (try_listener_reassociate_by_cname) authenticated the migrating\n- * source using only the cname.  Under a shared PSK the cname is not a\n- * per-peer secret, so an independent endpoint that held the group PSK\n- * and knew the victim's cname could be absorbed into the victim's peer\n- * record once the victim fell silent -- an identity takeover.\n- *\n- * The fix restricts the migration to peers with an authenticated\n- * per-peer SRP session (eap_is_authenticated()).  Under a shared PSK\n- * the gate must now refuse migration, so the attacker must remain a\n- * SEPARATE caller and must never be absorbed into the victim's record.\n- *\n- * Topology (all on 127.0.0.1, MAIN profile, shared PSK):\n- *   sender(listener, PSK, streams data)\n- *       <-- victim  (caller, cname=X, src port A)   round 1\n- *       <-- attacker(caller, cname=X, src port B)   round 2\n- *\n- * The attacker is a SEPARATE rist_ctx with its own socket and its own\n- * receiver thread.  It shares ONLY the PSK and the cname with the\n- * victim -- it never touches the victim's socket, keys, or peer object.\n- *\n- * Regression signal: with the fix in place the attacker is NOT\n- * absorbed, so the sender sees it as a new distinct caller\n- * (distinct_callers increments).  If the attacker is ever absorbed\n- * (distinct_callers stays at the post-victim count) the PSK identity\n- * takeover has been reintroduced.\n- *\n- * Note: a listener-sender under a shared PSK broadcasts to every\n- * authenticated PSK caller, so the attacker receiving media is NOT by\n- * itself the defect -- absorption into the victim's record is.\n- *\n- * Exit codes:\n- *   0  - FIX HOLDS: PSK attacker was NOT absorbed (stayed a separate\n- *        caller); the SRP-only gate refused the migration\n- *   1  - REGRESSION: PSK attacker was absorbed into the victim's peer\n- *        record (PSK cname identity takeover reintroduced)\n- *   2  - INDETERMINATE: victim never established / never got data\n- *  99  - setup error\n- */\n-\n-#include \"librist/librist.h\"\n-#include <stdio.h>\n-#include <stdlib.h>\n-#include <string.h>\n-#include <stdbool.h>\n-#include \"pthread-shim.h\"\n-\n-#ifdef _WIN32\n-#include <windows.h>\n-#define usleep(x) Sleep((x)/1000)\n-#else\n-#include <unistd.h>\n-#endif\n-\n-#define LISTEN_PORT 19900\n-#define VICTIM_PORT 20011\n-#define ATTACKER_PORT 20012\n-#define CNAME \"stream-1\"\n-#define PSK \"sharedgroupkey4321\"\n-\n-static struct rist_logging_settings *log_settings = NULL;\n-\n-static pthread_mutex_t tracker_lock;\n-#define MAX_TRACKED_PEERS 16\n-static struct {\n-\tstruct rist_peer *seen[MAX_TRACKED_PEERS];\n-\tint count;\n-} tracker;\n-\n-static volatile int victim_pkts = 0;\n-static volatile int attacker_pkts = 0;\n-static volatile bool sender_run = true;\n-\n-static int log_cb(void *arg, enum rist_log_level level, const char *msg) {\n-\t(void)arg;\n-\tconst char *tag = level <= RIST_LOG_ERROR ? \"E\" :\n-\t                  level <= RIST_LOG_WARN  ? \"W\" :\n-\t                  level <= RIST_LOG_INFO  ? \"I\" : \"D\";\n-\tfprintf(stderr, \"%s| %s\", tag, msg);\n-\treturn 0;\n-}\n-\n-static void sender_status_cb(void *arg, struct rist_peer *peer,\n-                             enum rist_connection_status status) {\n-\t(void)arg;\n-\tif (status == RIST_CONNECTION_ESTABLISHED ||\n-\t    status == RIST_CLIENT_CONNECTED) {\n-\t\tpthread_mutex_lock(&tracker_lock);\n-\t\tbool seen = false;\n-\t\tfor (int i = 0; i < tracker.count; i++)\n-\t\t\tif (tracker.seen[i] == peer) { seen = true; break; }\n-\t\tif (!seen && tracker.count < MAX_TRACKED_PEERS) {\n-\t\t\ttracker.seen[tracker.count++] = peer;\n-\t\t\tfprintf(stderr, \">> sender CONNECTED (peer=%p, distinct_callers=%d)\\n\",\n-\t\t\t        (void *)peer, tracker.count);\n-\t\t}\n-\t\tpthread_mutex_unlock(&tracker_lock);\n-\t}\n-}\n-\n-static int victim_data_cb(void *arg, struct rist_data_block *b) {\n-\t(void)arg;\n-\tvictim_pkts++;\n-\trist_receiver_data_block_free2(&b);\n-\treturn 0;\n-}\n-static int attacker_data_cb(void *arg, struct rist_data_block *b) {\n-\t(void)arg;\n-\tattacker_pkts++;\n-\trist_receiver_data_block_free2(&b);\n-\treturn 0;\n-}\n-\n-static struct rist_ctx *start_receiver(const char *url, receiver_data_callback2_t cb) {\n-\tstruct rist_ctx *rx = NULL;\n-\tif (rist_receiver_create(&rx, RIST_PROFILE_MAIN, log_settings) != 0)\n-\t\treturn NULL;\n-\tstruct rist_peer_config *pcfg = NULL;\n-\tif (rist_parse_address2(url, (void *)&pcfg) != 0) { rist_destroy(rx); return NULL; }\n-\tstruct rist_peer *peer = NULL;\n-\tif (rist_peer_create(rx, &peer, pcfg) != 0) { free(pcfg); rist_destroy(rx); return NULL; }\n-\tfree(pcfg);\n-\tif (rist_receiver_data_callback_set2(rx, cb, NULL) != 0) { rist_destroy(rx); return NULL; }\n-\tif (rist_start(rx) != 0) { rist_destroy(rx); return NULL; }\n-\treturn rx;\n-}\n-\n-static PTHREAD_START_FUNC(sender_feed, arg) {\n-\tstruct rist_ctx *tx = arg;\n-\tuint32_t counter = 0;\n-\twhile (sender_run) {\n-\t\tstruct rist_data_block blk;\n-\t\tmemset(&blk, 0, sizeof(blk));\n-\t\tblk.payload = &counter;\n-\t\tblk.payload_len = sizeof(counter);\n-\t\tblk.virt_src_port = 1968;\n-\t\trist_sender_data_write(tx, &blk);\n-\t\tcounter++;\n-\t\tusleep(20000); /* ~50 pkt/s */\n-\t}\n-\treturn 0;\n-}\n-\n-int main(int argc, char *argv[]) {\n-\tchar url[320];\n-\t/* Negative control: attacker presents a DIFFERENT cname.  Expect no\n-\t * absorption (the cname is what drives the takeover). */\n-\tbool control = (argc > 1 && strcmp(argv[1], \"control\") == 0);\n-\tconst char *attacker_cname = control ? \"other-stream\" : CNAME;\n-\n-\tif (rist_logging_set(&log_settings, RIST_LOG_INFO, log_cb, NULL, NULL, stderr) != 0)\n-\t\treturn 99;\n-\n-\tmemset(&tracker, 0, sizeof(tracker));\n-\tpthread_mutex_init(&tracker_lock, NULL);\n-\n-\t/* sender: listener, MAIN, shared PSK, streams data. */\n-\tstruct rist_ctx *tx = NULL;\n-\tif (rist_sender_create(&tx, RIST_PROFILE_MAIN, 0, log_settings) != 0) return 99;\n-\trist_connection_status_callback_set(tx, sender_status_cb, NULL);\n-\tsnprintf(url, sizeof(url),\n-\t         \"rist://@127.0.0.1:%d?session-timeout=30000&keepalive-interval=1000\"\n-\t         \"&secret=%s&aes-type=128\", LISTEN_PORT, PSK);\n-\tstruct rist_peer_config *tx_pcfg = NULL;\n-\tif (rist_parse_address2(url, (void *)&tx_pcfg) != 0) { rist_destroy(tx); return 99; }\n-\tstruct rist_peer *tx_peer = NULL;\n-\tif (rist_peer_create(tx, &tx_peer, tx_pcfg) != 0) { free(tx_pcfg); rist_destroy(tx); return 99; }\n-\tfree(tx_pcfg);\n-\tif (rist_start(tx) != 0) { rist_destroy(tx); return 99; }\n-\n-\tpthread_t feeder;\n-\tpthread_create(&feeder, NULL, sender_feed, tx);\n-\tusleep(200000);\n-\n-\t/* round 1: victim caller. */\n-\tfprintf(stderr, \"\\n== round 1: VICTIM connects (port %d, cname %s) ==\\n\",\n-\t        VICTIM_PORT, CNAME);\n-\tsnprintf(url, sizeof(url),\n-\t         \"rist://127.0.0.1:%d?local-port=%d&cname=%s\"\n-\t         \"&session-timeout=30000&keepalive-interval=1000&secret=%s&aes-type=128\",\n-\t         LISTEN_PORT, VICTIM_PORT, CNAME, PSK);\n-\tstruct rist_ctx *victim = start_receiver(url, victim_data_cb);\n-\tif (!victim) { sender_run = false; pthread_join(feeder, NULL); rist_destroy(tx); return 99; }\n-\tusleep(3000000);\n-\n-\tpthread_mutex_lock(&tracker_lock);\n-\tint callers_after_victim = tracker.count;\n-\tpthread_mutex_unlock(&tracker_lock);\n-\tint victim_before = victim_pkts;\n-\tfprintf(stderr, \"after victim: distinct_callers=%d, victim_pkts=%d\\n\",\n-\t        callers_after_victim, victim_before);\n-\n-\t/* victim falls silent (modelled by teardown), past 2*keepalive. */\n-\tfprintf(stderr, \"\\n== victim goes silent ==\\n\");\n-\trist_destroy(victim);\n-\tusleep(3000000);\n-\n-\t/* round 2: independent ATTACKER, shares ONLY PSK + cname. */\n-\tfprintf(stderr, \"\\n== round 2: ATTACKER connects (port %d, cname=%s, same PSK)%s ==\\n\",\n-\t        ATTACKER_PORT, attacker_cname, control ? \" [CONTROL: different cname]\" : \"\");\n-\tsnprintf(url, sizeof(url),\n-\t         \"rist://127.0.0.1:%d?local-port=%d&cname=%s\"\n-\t         \"&session-timeout=30000&keepalive-interval=1000&secret=%s&aes-type=128\",\n-\t         LISTEN_PORT, ATTACKER_PORT, attacker_cname, PSK);\n-\tint attacker_start = attacker_pkts;\n-\tstruct rist_ctx *attacker = start_receiver(url, attacker_data_cb);\n-\tif (!attacker) { sender_run = false; pthread_join(feeder, NULL); rist_destroy(tx); return 99; }\n-\tusleep(4000000);\n-\n-\tpthread_mutex_lock(&tracker_lock);\n-\tint callers_after_attacker = tracker.count;\n-\tpthread_mutex_unlock(&tracker_lock);\n-\tint attacker_got = attacker_pkts - attacker_start;\n-\tfprintf(stderr, \"\\n== after attacker: distinct_callers=%d, attacker_pkts=%d ==\\n\",\n-\t        callers_after_attacker, attacker_got);\n-\n-\tsender_run = false;\n-\tpthread_join(feeder, NULL);\n-\trist_destroy(attacker);\n-\trist_destroy(tx);\n-\tfree(log_settings);\n-\n-\tif (victim_before == 0) {\n-\t\tfprintf(stderr, \"INDETERMINATE: victim never received the stream.\\n\");\n-\t\treturn 2;\n-\t}\n-\n-\tbool absorbed = (callers_after_attacker == callers_after_victim); /* no new distinct caller */\n-\tbool attacker_received = (attacker_got > 0);\n-\n-\tif (control) {\n-\t\t/* Control: a different cname is never a migration candidate.\n-\t\t * With the SRP-only fix neither cname is absorbed under PSK, so\n-\t\t * this just confirms the different-cname caller stays separate. */\n-\t\tfprintf(stderr, \"\\nCONTROL (different cname): absorbed=%d \"\n-\t\t                \"(distinct_callers %d->%d), attacker_pkts=%d\\n\",\n-\t\t        absorbed, callers_after_victim, callers_after_attacker, attacker_got);\n-\t\tif (!absorbed) {\n-\t\t\tfprintf(stderr, \"CONTROL OK: different-cname caller stayed \"\n-\t\t\t                \"separate.\\n\");\n-\t\t\treturn 0;\n-\t\t}\n-\t\tfprintf(stderr, \"CONTROL UNEXPECTED: different cname was absorbed.\\n\");\n-\t\treturn 1;\n-\t}\n-\n-\tif (absorbed) {\n-\t\tfprintf(stderr, \"\\nREGRESSION: independent attacker holding only the \"\n-\t\t                \"shared PSK + cname \\\"%s\\\" was ABSORBED into the \"\n-\t\t                \"victim's peer record (distinct_callers stayed %d). \"\n-\t\t                \"Finding 1 (PSK cname identity takeover) has been \"\n-\t\t                \"reintroduced.\\n\",\n-\t\t        CNAME, callers_after_attacker);\n-\t\treturn 1;\n-\t}\n-\tfprintf(stderr, \"\\nFIX HOLDS: PSK attacker was NOT absorbed; it became a \"\n-\t                \"separate caller (distinct_callers %d->%d). The SRP-only \"\n-\t                \"gate refused the cname migration. (attacker_received=%d is \"\n-\t                \"expected PSK broadcast behaviour, not the defect.)\\n\",\n-\t        callers_after_victim, callers_after_attacker, attacker_received);\n-\treturn 0;\n-}\ndiff --git a/test/rist/test_reflector.c b/test/rist/test_reflector.c\ndeleted file mode 100644\nindex 07014f3..0000000\n--- a/test/rist/test_reflector.c\n+++ /dev/null\n@@ -1,535 +0,0 @@\n-/* librist. Copyright © RIST Community\n- * SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Test for transparent reflector functionality and RIST protocol verification.\n- */\n-\n-#include \"librist/librist.h\"\n-#include \"rist-private.h\"\n-#include <stdio.h>\n-#include <stdlib.h>\n-#include <string.h>\n-#include <stdatomic.h>\n-#include <inttypes.h>\n-\n-static struct rist_logging_settings *logging_settings = NULL;\n-static atomic_int failed;\n-\n-/* Upper bounds only: the waits below poll for the actual condition and\n- * break as soon as it holds, so these caps just limit how long a slow or\n- * loaded host may take before giving up. */\n-#define CONNECT_TIMEOUT_MS   30000\n-#define PHASE_RX_TIMEOUT_MS  20000\n-#define CONVERGE_TIMEOUT_MS  30000\n-\n-static int log_callback(void *arg, enum rist_log_level level, const char *msg) {\n-    (void)arg;\n-    if (level <= RIST_LOG_ERROR) {\n-        fprintf(stderr, \"[ERROR] %s\", msg);\n-    } else if (level == RIST_LOG_WARN) {\n-        fprintf(stdout, \"[WARN] %s\", msg);\n-    }\n-    return 0;\n-}\n-\n-static uint64_t get_time_ms(void) {\n-#ifdef _WIN32\n-    return GetTickCount64();\n-#else\n-    struct timespec ts;\n-    clock_gettime(CLOCK_MONOTONIC, &ts);\n-    return (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;\n-#endif\n-}\n-\n-static struct rist_ctx *setup_receiver(const char *url) {\n-    struct rist_ctx *ctx;\n-    if (rist_receiver_create(&ctx, RIST_PROFILE_MAIN, logging_settings) != 0) {\n-        fprintf(stderr, \"Could not create receiver context\\n\");\n-        return NULL;\n-    }\n-    struct rist_peer_config *peer_config = NULL;\n-    if (rist_parse_address2(url, (void *)&peer_config) != 0) {\n-        fprintf(stderr, \"Could not parse receiver address: %s\\n\", url);\n-        rist_destroy(ctx);\n-        return NULL;\n-    }\n-    struct rist_peer *peer;\n-    if (rist_peer_create(ctx, &peer, peer_config) == -1) {\n-        fprintf(stderr, \"Could not create receiver peer\\n\");\n-        free((void *)peer_config);\n-        rist_destroy(ctx);\n-        return NULL;\n-    }\n-    free((void *)peer_config);\n-    if (rist_start(ctx) == -1) {\n-        fprintf(stderr, \"Could not start receiver\\n\");\n-        rist_destroy(ctx);\n-        return NULL;\n-    }\n-    return ctx;\n-}\n-\n-static struct rist_ctx *setup_sender(const char *url) {\n-    struct rist_ctx *ctx;\n-    if (rist_sender_create(&ctx, RIST_PROFILE_MAIN, 0, logging_settings) != 0) {\n-        fprintf(stderr, \"Could not create sender context\\n\");\n-        return NULL;\n-    }\n-    struct rist_peer_config *peer_config = NULL;\n-    if (rist_parse_address2(url, (void *)&peer_config) != 0) {\n-        fprintf(stderr, \"Could not parse sender address: %s\\n\", url);\n-        rist_destroy(ctx);\n-        return NULL;\n-    }\n-    struct rist_peer *peer;\n-    if (rist_peer_create(ctx, &peer, peer_config) == -1) {\n-        fprintf(stderr, \"Could not create sender peer\\n\");\n-        free((void *)peer_config);\n-        rist_destroy(ctx);\n-        return NULL;\n-    }\n-    free((void *)peer_config);\n-    if (rist_start(ctx) == -1) {\n-        fprintf(stderr, \"Could not start sender\\n\");\n-        rist_destroy(ctx);\n-        return NULL;\n-    }\n-    return ctx;\n-}\n-\n-int main(int argc, char *argv[]) {\n-    (void)argc;\n-    (void)argv;\n-    int ret = 0;\n-    atomic_init(&failed, 0);\n-    uint8_t payload[1316];\n-    struct rist_data_block data_block = { 0 };\n-\n-    if (rist_logging_set(&logging_settings, RIST_LOG_DEBUG, log_callback, NULL, NULL, stderr) != 0) {\n-        fprintf(stderr, \"Failed to setup logging\\n\");\n-        return 99;\n-    }\n-\n-    fprintf(stdout, \"\\n============================================\\n\");\n-    fprintf(stdout, \"  STARTING RIST MEDIA REFLECTOR TEST SUITE  \\n\");\n-    fprintf(stdout, \"============================================\\n\\n\");\n-\n-    /* 1. Setup transparent reflector receiver, publisher sender, and subscriber receiver */\n-    const char *reflector_url  = \"rist://@127.0.0.1:45678?secret=12345678&aes-type=128&rtt-max=200&rtt-min=1&buffer-min=50&buffer-max=1500&reflector=1\";\n-    const char *publisher_url  = \"rist://127.0.0.1:45678?secret=12345678&aes-type=128&rtt-max=200&rtt-min=1&buffer-min=50&buffer-max=1500\";\n-    const char *subscriber_url = \"rist://127.0.0.1:45678?secret=12345678&aes-type=128&rtt-max=200&rtt-min=1&buffer-min=50&buffer-max=1500\";\n-\n-    struct rist_ctx *reflector_ctx = setup_receiver(reflector_url);\n-    struct rist_ctx *pub_ctx = setup_sender(publisher_url);\n-    struct rist_ctx *sub_ctx = setup_receiver(subscriber_url);\n-\n-    if (!reflector_ctx || !pub_ctx || !sub_ctx) {\n-        fprintf(stderr, \"FAIL: Could not initialize contexts!\\n\");\n-        ret = 1;\n-        goto cleanup;\n-    }\n-\n-    /* Wait for handshake and connection establishment */\n-    fprintf(stdout, \"Waiting for connections to establish...\\n\");\n-    struct rist_common_ctx *cctx_ref = rist_struct_get_common(reflector_ctx);\n-    int connected_peers = 0;\n-    uint64_t conn_start = get_time_ms();\n-    while (connected_peers < 2) {\n-        connected_peers = 0;\n-        pthread_mutex_lock(&cctx_ref->peerlist_lock);\n-        struct rist_peer *p = cctx_ref->PEERS;\n-        while (p) {\n-            if (!p->dead && p->parent && p->authenticated) {\n-                connected_peers++;\n-            }\n-            p = p->next;\n-        }\n-        pthread_mutex_unlock(&cctx_ref->peerlist_lock);\n-        if (connected_peers < 2) {\n-            usleep(100000); // 100ms\n-        }\n-        if (get_time_ms() - conn_start > CONNECT_TIMEOUT_MS) {\n-            fprintf(stderr, \"Timeout waiting for peers to connect! Connected peers: %d/2\\n\", connected_peers);\n-            atomic_store(&failed, 1);\n-            ret = 1;\n-            goto cleanup;\n-        }\n-    }\n-    fprintf(stdout, \"Both peers connected and authenticated successfully!\\n\");\n-    /* Additional brief sleep to let internal buffers initialize */\n-    usleep(500000);\n-\n-    // Connection stabilized, proceeding to PHASE 1\n-    /* -------------------------------------------------------------\n-     * PHASE 1: Basic Reflector & RTP Protocol Verification\n-     * ------------------------------------------------------------- */\n-    fprintf(stdout, \"\\n--- PHASE 1: RTP Data Reflection Verification ---\\n\");\n-    memset(payload, 0, sizeof(payload));\n-    payload[0] = 0x47; /* MPEG-TS Sync byte */\n-\n-    data_block.payload = payload;\n-    data_block.payload_len = sizeof(payload);\n-\n-    int packets_to_send = 100;\n-    int packets_sent = 0;\n-    for (int i = 0; i < packets_to_send; i++) {\n-        snprintf((char *)payload + 4, sizeof(payload) - 4, \"RTP-Packet-%d\", i);\n-        if (rist_sender_data_write(pub_ctx, &data_block) < 0) {\n-            fprintf(stderr, \"Failed to write publisher data packet %d\\n\", i);\n-            atomic_store(&failed, 1);\n-            break;\n-        }\n-        packets_sent++;\n-        usleep(1000);\n-    }\n-    fprintf(stdout, \"Sent %d encrypted RTP packets from Publisher.\\n\", packets_sent);\n-\n-    /* Read packets on Subscriber AND Reflector local playout */\n-    int sub_packets_received = 0;\n-    int ref_packets_received = 0;\n-    uint64_t start_time = get_time_ms();\n-    while (sub_packets_received < packets_to_send || ref_packets_received < packets_to_send) {\n-        if (sub_packets_received < packets_to_send) {\n-            struct rist_data_block *b = NULL;\n-            int queue_length = rist_receiver_data_read2(sub_ctx, &b, 5);\n-            if (queue_length > 0 && b) {\n-                char expected[64];\n-                snprintf(expected, sizeof(expected), \"RTP-Packet-%d\", sub_packets_received);\n-                if (strcmp(expected, (char *)b->payload + 4) != 0) {\n-                    fprintf(stderr, \"Subscriber payload mismatch! Got '%s', expected '%s'\\n\", (char *)b->payload + 4, expected);\n-                    atomic_store(&failed, 1);\n-                }\n-                sub_packets_received++;\n-                rist_receiver_data_block_free2((struct rist_data_block **const)&b);\n-            }\n-        }\n-        if (ref_packets_received < packets_to_send) {\n-            struct rist_data_block *b = NULL;\n-            int queue_length = rist_receiver_data_read2(reflector_ctx, &b, 5);\n-            if (queue_length > 0 && b) {\n-                char expected[64];\n-                snprintf(expected, sizeof(expected), \"RTP-Packet-%d\", ref_packets_received);\n-                if (strcmp(expected, (char *)b->payload + 4) != 0) {\n-                    fprintf(stderr, \"Reflector playout payload mismatch! Got '%s', expected '%s'\\n\", (char *)b->payload + 4, expected);\n-                    atomic_store(&failed, 1);\n-                }\n-                ref_packets_received++;\n-                rist_receiver_data_block_free2((struct rist_data_block **const)&b);\n-            }\n-        }\n-        if (get_time_ms() - start_time > PHASE_RX_TIMEOUT_MS) {\n-            /* If we have received the vast majority of packets (e.g. > 80), we consider it a pass\n-             * since initial stream synchronization packet drops are a normal protocol characteristic. */\n-            if (sub_packets_received > 80 && ref_packets_received > 80) {\n-                break;\n-            }\n-            fprintf(stderr, \"Timeout waiting for reflected RTP packets. Got Sub: %d/%d, Ref: %d/%d\\n\", sub_packets_received, packets_to_send, ref_packets_received, packets_to_send);\n-            atomic_store(&failed, 1);\n-            break;\n-        }\n-    }\n-    fprintf(stdout, \"Subscriber received %d and Reflector received %d decrypted RTP packets successfully!\\n\", sub_packets_received, ref_packets_received);\n-\n-    /* Verify Reflector Context Peer List and classification */\n-    struct rist_common_ctx *cctx = rist_struct_get_common(reflector_ctx);\n-    pthread_mutex_lock(&cctx->peerlist_lock);\n-    struct rist_peer *p = cctx->PEERS;\n-    bool found_publisher = false;\n-    bool found_subscriber = false;\n-\n-    while (p) {\n-        /* Filter internal simple profile RTCP peers if any, focus on main peers */\n-        if (!p->dead && p->parent) {\n-            if (p->is_reflector_publisher) {\n-                found_publisher = true;\n-                fprintf(stdout, \"Verified Reflector Peer list: Found Publisher peer (ID: %u)\\n\", p->adv_peer_id);\n-            } else {\n-                found_subscriber = true;\n-                fprintf(stdout, \"Verified Reflector Peer list: Found Subscriber peer (ID: %u)\\n\", p->adv_peer_id);\n-            }\n-        }\n-        p = p->next;\n-    }\n-    pthread_mutex_unlock(&cctx->peerlist_lock);\n-\n-    if (!found_publisher) {\n-        fprintf(stderr, \"FAIL: Publisher role was not dynamically classified!\\n\");\n-        atomic_store(&failed, 1);\n-    }\n-    if (!found_subscriber) {\n-        fprintf(stderr, \"FAIL: Subscriber role was not dynamically classified!\\n\");\n-        atomic_store(&failed, 1);\n-    }\n-\n-    if (atomic_load(&failed)) goto cleanup;\n-\n-    /* -------------------------------------------------------------\n-     * PHASE 2: RTCP Protocol & Feedback Loop (NACKs)\n-     * ------------------------------------------------------------- */\n-    fprintf(stdout, \"\\n--- PHASE 2: RTCP NACK Feedback Routing Verification ---\\n\");\n-    /* Enable simulated loss on reflector and subscriber receivers to trigger dual-link NACKs */\n-    reflector_ctx->receiver_ctx->simulate_loss = true;\n-    reflector_ctx->receiver_ctx->loss_percentage = 150; /* 15% loss */\n-    sub_ctx->receiver_ctx->simulate_loss = true;\n-    sub_ctx->receiver_ctx->loss_percentage = 200; /* 20% loss */\n-    fprintf(stdout, \"Simulated 15%% loss on Reflector and 20%% loss on Subscriber.\\n\");\n-\n-    packets_to_send = 200;\n-    packets_sent = 0;\n-    for (int i = 0; i < packets_to_send; i++) {\n-        snprintf((char *)payload + 4, sizeof(payload) - 4, \"NACK-Test-Packet-%d\", i);\n-        if (rist_sender_data_write(pub_ctx, &data_block) < 0) {\n-            fprintf(stderr, \"Failed to write publisher data packet %d\\n\", i);\n-            atomic_store(&failed, 1);\n-            break;\n-        }\n-        packets_sent++;\n-        usleep(2000);\n-    }\n-    fprintf(stdout, \"Sent %d encrypted RTP packets from Publisher under simulated loss.\\n\", packets_sent);\n-\n-    /* Read packets on Subscriber AND Reflector local playout */\n-    sub_packets_received = 0;\n-    ref_packets_received = 0;\n-    start_time = get_time_ms();\n-    while (sub_packets_received < packets_to_send || ref_packets_received < packets_to_send) {\n-        if (sub_packets_received < packets_to_send) {\n-            struct rist_data_block *b = NULL;\n-            int queue_length = rist_receiver_data_read2(sub_ctx, &b, 10);\n-            if (queue_length > 0 && b) {\n-                char expected[64];\n-                snprintf(expected, sizeof(expected), \"NACK-Test-Packet-%d\", sub_packets_received);\n-                if (strcmp(expected, (char *)b->payload + 4) != 0) {\n-                    fprintf(stderr, \"Subscriber Phase 2 payload mismatch! Got '%s', expected '%s'\\n\", (char *)b->payload + 4, expected);\n-                    atomic_store(&failed, 1);\n-                }\n-                sub_packets_received++;\n-                rist_receiver_data_block_free2((struct rist_data_block **const)&b);\n-            }\n-        }\n-        if (ref_packets_received < packets_to_send) {\n-            struct rist_data_block *b = NULL;\n-            int queue_length = rist_receiver_data_read2(reflector_ctx, &b, 10);\n-            if (queue_length > 0 && b) {\n-                char expected[64];\n-                snprintf(expected, sizeof(expected), \"NACK-Test-Packet-%d\", ref_packets_received);\n-                if (strcmp(expected, (char *)b->payload + 4) != 0) {\n-                    fprintf(stderr, \"Reflector playout Phase 2 payload mismatch! Got '%s', expected '%s'\\n\", (char *)b->payload + 4, expected);\n-                    atomic_store(&failed, 1);\n-                }\n-                ref_packets_received++;\n-                rist_receiver_data_block_free2((struct rist_data_block **const)&b);\n-            }\n-        }\n-        if (get_time_ms() - start_time > PHASE_RX_TIMEOUT_MS) {\n-            /* If we have received the vast majority of packets (e.g. > 160), we consider it a pass\n-             * to avoid failing the test suite due to statistical packet loss fluctuations. */\n-            if (sub_packets_received > 160 && ref_packets_received > 160) {\n-                break;\n-            }\n-            fprintf(stderr, \"Timeout waiting for Phase 2 packets. Got Sub: %d/%d, Ref: %d/%d (NACK/retransmission loop failed!)\\n\",\n-                    sub_packets_received, packets_to_send, ref_packets_received, packets_to_send);\n-            atomic_store(&failed, 1);\n-            break;\n-        }\n-    }\n-    fprintf(stdout, \"Subscriber received %d and Reflector received %d RTP packets successfully. NACK/Retransmission loop worked through the Reflector!\\n\",\n-            sub_packets_received, ref_packets_received);\n-\n-    /* Disable simulated loss for subsequent phases to ensure RTT stability */\n-    reflector_ctx->receiver_ctx->simulate_loss = false;\n-    sub_ctx->receiver_ctx->simulate_loss = false;\n-\n-    if (atomic_load(&failed)) goto cleanup;\n-    /* -------------------------------------------------------------\n-     * PHASE 3: Natural RTT & Direct Buffer Verification\n-     * ------------------------------------------------------------- */\n-    fprintf(stdout, \"\\n--- PHASE 3: RTT Measurement & Direct Buffer Verification ---\\n\");\n-\n-    /* RIST_CLOCK = 4294967 NTP ticks per millisecond */\n-    const uint64_t RIST_CLK = 4294967ULL;\n-\n-    /* Wait for convergence by polling the actual state: echo EWMA RTTs\n-     * non-zero and both jitter buffers scaled down out of their initial\n-     * maximum, bounded by CONVERGE_TIMEOUT_MS. */\n-    struct rist_common_ctx *pub_cctx = rist_struct_get_common(pub_ctx);\n-    struct rist_common_ctx *sub_cctx = rist_struct_get_common(sub_ctx);\n-\n-    uint64_t final_pub_eight_times_rtt  = 0;\n-    uint64_t final_sub_eight_times_rtt  = 0;\n-    uint64_t final_ref_eight_times_rtt  = 0;\n-    uint64_t sub_recovery_buf_ticks     = 0;\n-    uint64_t sub_sender_max_buf_ticks   = 0;\n-    uint64_t ref_pub_recovery_buf_ticks   = 0;\n-    uint64_t ref_pub_sender_max_buf_ticks = 0;\n-    const uint64_t initial_buf_ms = 775ULL;\n-\n-    fprintf(stdout, \"Waiting (up to %d ms) for natural RTT convergence and buffer scaling...\\n\",\n-            CONVERGE_TIMEOUT_MS);\n-    uint64_t converge_start = get_time_ms();\n-    while (1) {\n-        final_pub_eight_times_rtt = 0;\n-        final_sub_eight_times_rtt = 0;\n-        final_ref_eight_times_rtt = 0;\n-        sub_recovery_buf_ticks = 0;\n-        sub_sender_max_buf_ticks = 0;\n-        ref_pub_recovery_buf_ticks = 0;\n-        ref_pub_sender_max_buf_ticks = 0;\n-\n-        pthread_mutex_lock(&pub_cctx->peerlist_lock);\n-        for (struct rist_peer *pp = pub_cctx->PEERS; pp; pp = pp->next) {\n-            if (!pp->dead && pp->authenticated) {\n-                final_pub_eight_times_rtt = pp->eight_times_rtt;\n-                break;\n-            }\n-        }\n-        pthread_mutex_unlock(&pub_cctx->peerlist_lock);\n-\n-        pthread_mutex_lock(&sub_cctx->peerlist_lock);\n-        for (struct rist_peer *sp = sub_cctx->PEERS; sp; sp = sp->next) {\n-            if (!sp->dead && sp->authenticated) {\n-                final_sub_eight_times_rtt  = sp->eight_times_rtt;\n-                sub_recovery_buf_ticks     = sp->recovery_buffer_ticks;\n-                sub_sender_max_buf_ticks   = sp->sender_max_buffer_ticks;\n-                break;\n-            }\n-        }\n-        pthread_mutex_unlock(&sub_cctx->peerlist_lock);\n-\n-        pthread_mutex_lock(&cctx->peerlist_lock);\n-        for (struct rist_peer *rp = cctx->PEERS; rp; rp = rp->next) {\n-            if (!rp->dead && rp->parent && rp->authenticated && rp->is_reflector_publisher) {\n-                final_ref_eight_times_rtt    = rp->eight_times_rtt;\n-                ref_pub_recovery_buf_ticks   = rp->recovery_buffer_ticks;\n-                ref_pub_sender_max_buf_ticks = rp->sender_max_buffer_ticks;\n-                break;\n-            }\n-        }\n-        pthread_mutex_unlock(&cctx->peerlist_lock);\n-\n-        uint64_t s_buf = sub_recovery_buf_ticks / RIST_CLK;\n-        uint64_t r_buf = ref_pub_recovery_buf_ticks / RIST_CLK;\n-        bool rtt_ready = final_pub_eight_times_rtt && final_sub_eight_times_rtt &&\n-                         final_ref_eight_times_rtt;\n-        /* [50, initial) satisfies both the \"shrank\" and the [50..800] range\n-         * assertions below (initial_buf_ms < 800). */\n-        bool sub_ready = s_buf >= 50ULL && s_buf < initial_buf_ms;\n-        bool ref_ready = r_buf >= 50ULL && r_buf < initial_buf_ms;\n-        if (rtt_ready && sub_ready && ref_ready)\n-            break;\n-        if (get_time_ms() - converge_start > CONVERGE_TIMEOUT_MS) {\n-            fprintf(stderr, \"WARN: RTT/buffer convergence not fully reached within %d ms; \"\n-                            \"asserting on last-read values.\\n\", CONVERGE_TIMEOUT_MS);\n-            break;\n-        }\n-        usleep(250000); /* 250ms */\n-    }\n-\n-    uint64_t pub_measured_rtt_ms = final_pub_eight_times_rtt / 8 / RIST_CLK;\n-    uint64_t sub_measured_rtt_ms = final_sub_eight_times_rtt / 8 / RIST_CLK;\n-    uint64_t ref_measured_rtt_ms = final_ref_eight_times_rtt / 8 / RIST_CLK;\n-    uint64_t sub_buf_ms          = sub_recovery_buf_ticks / RIST_CLK;\n-    uint64_t sub_maxbuf_ms       = sub_sender_max_buf_ticks / RIST_CLK;\n-    uint64_t ref_pub_buf_ms      = ref_pub_recovery_buf_ticks / RIST_CLK;\n-    uint64_t ref_pub_maxbuf_ms   = ref_pub_sender_max_buf_ticks / RIST_CLK;\n-    size_t pub_sender_min_time_ms = pub_ctx->sender_ctx->sender_recover_min_time;\n-\n-    fprintf(stdout, \"\\n--- VERIFICATION RESULTS ---\\n\");\n-    fprintf(stdout, \"Publisher Natural RTT:                      %\" PRIu64 \" ms\\n\", pub_measured_rtt_ms);\n-    fprintf(stdout, \"Subscriber Natural RTT:                     %\" PRIu64 \" ms\\n\", sub_measured_rtt_ms);\n-    fprintf(stdout, \"Reflector Natural RTT:                      %\" PRIu64 \" ms\\n\", ref_measured_rtt_ms);\n-    fprintf(stdout, \"Publisher sender_recover_min_time:          %\" PRIu64 \" ms\\n\", pub_sender_min_time_ms);\n-    fprintf(stdout, \"Subscriber recovery_buffer_ticks:           %\" PRIu64 \" ms\\n\", sub_buf_ms);\n-    fprintf(stdout, \"Subscriber sender_max_buffer_ticks:         %\" PRIu64 \" ms\\n\", sub_maxbuf_ms);\n-    fprintf(stdout, \"Reflector-Receiver recovery_buffer:         %\" PRIu64 \" ms\\n\", ref_pub_buf_ms);\n-    fprintf(stdout, \"Reflector-Receiver sender_max_buffer:       %\" PRIu64 \" ms\\n\", ref_pub_maxbuf_ms);\n-\n-    /* Assert that RTT calculations are working */\n-    if (final_pub_eight_times_rtt == 0 || final_sub_eight_times_rtt == 0 || final_ref_eight_times_rtt == 0) {\n-        fprintf(stderr, \"FAIL: Measured RTTs are zero - echo exchange is broken!\\n\");\n-        atomic_store(&failed, 1);\n-    } else {\n-        fprintf(stdout, \"PASS: Measured RTTs are non-zero.\\n\");\n-    }\n-\n-    /* Verify Publisher buffer size matches our expectations */\n-    if (pub_sender_min_time_ms < 1500) {\n-        fprintf(stderr, \"FAIL: Publisher buffer (%zu ms) is smaller than configured max (1500ms)!\\n\", pub_sender_min_time_ms);\n-        atomic_store(&failed, 1);\n-    } else {\n-        fprintf(stdout, \"PASS: Publisher buffer (%zu ms) matches expected configuration.\\n\", pub_sender_min_time_ms);\n-    }\n-\n-    /* Verify Subscriber buffer dynamic sizing and shrinkage */\n-    if (sub_buf_ms >= initial_buf_ms) {\n-        fprintf(stderr, \"FAIL: Subscriber buffer (%\" PRIu64 \"ms) did not shrink from initial (%\" PRIu64 \"ms)!\\n\", sub_buf_ms, initial_buf_ms);\n-        atomic_store(&failed, 1);\n-    } else {\n-        fprintf(stdout, \"PASS: Subscriber buffer (%\" PRIu64 \"ms) successfully shrank from initial (%\" PRIu64 \"ms).\\n\", sub_buf_ms, initial_buf_ms);\n-    }\n-\n-    if (sub_buf_ms < 50ULL || sub_buf_ms > 800ULL) {\n-        fprintf(stderr, \"FAIL: Subscriber buffer (%\" PRIu64 \"ms) out of expected dynamic range [50ms .. 800ms]!\\n\", sub_buf_ms);\n-        atomic_store(&failed, 1);\n-    } else {\n-        fprintf(stdout, \"PASS: Subscriber buffer (%\" PRIu64 \"ms) is in expected dynamic range [50ms .. 800ms].\\n\", sub_buf_ms);\n-    }\n-\n-    /* Verify Reflector-Receiver buffer dynamic sizing and shrinkage */\n-    if (ref_pub_buf_ms >= initial_buf_ms) {\n-        fprintf(stderr, \"FAIL: Reflector-Receiver buffer (%\" PRIu64 \"ms) did not shrink from initial (%\" PRIu64 \"ms)!\\n\", ref_pub_buf_ms, initial_buf_ms);\n-        atomic_store(&failed, 1);\n-    } else {\n-        fprintf(stdout, \"PASS: Reflector-Receiver buffer (%\" PRIu64 \"ms) successfully shrank from initial (%\" PRIu64 \"ms).\\n\", ref_pub_buf_ms, initial_buf_ms);\n-    }\n-\n-    if (ref_pub_buf_ms < 50ULL || ref_pub_buf_ms > 800ULL) {\n-        fprintf(stderr, \"FAIL: Reflector-Receiver Reflector-Receiver buffer (%\" PRIu64 \"ms) out of expected dynamic range [50ms .. 800ms]!\\n\", ref_pub_buf_ms);\n-        atomic_store(&failed, 1);\n-    } else {\n-        fprintf(stdout, \"PASS: Reflector-Receiver buffer (%\" PRIu64 \"ms) is in expected dynamic range [50ms .. 800ms].\\n\", ref_pub_buf_ms);\n-    }\n-\n-    /* Verify critical retransmission invariant: Publisher buffer >= Subscriber/Reflector buffer */\n-    if ((uint64_t)pub_sender_min_time_ms < sub_buf_ms) {\n-        fprintf(stderr, \"FAIL: Retransmission Invariant Broken! Publisher buffer (%zu ms) < Subscriber buffer (%\" PRIu64 \" ms)!\\n\", pub_sender_min_time_ms, sub_buf_ms);\n-        atomic_store(&failed, 1);\n-    } else {\n-        fprintf(stdout, \"PASS: Retransmission Invariant Verified: Publisher buffer (%zu ms) >= Subscriber buffer (%\" PRIu64 \" ms).\\n\", pub_sender_min_time_ms, sub_buf_ms);\n-    }\n-\n-    if ((uint64_t)pub_sender_min_time_ms < ref_pub_buf_ms) {\n-        fprintf(stderr, \"FAIL: Retransmission Invariant Broken! Publisher buffer (%zu ms) < Reflector buffer (%\" PRIu64 \" ms)!\\n\", pub_sender_min_time_ms, ref_pub_buf_ms);\n-        atomic_store(&failed, 1);\n-    } else {\n-        fprintf(stdout, \"PASS: Retransmission Invariant Verified: Publisher buffer (%zu ms) >= Reflector buffer (%\" PRIu64 \" ms).\\n\", pub_sender_min_time_ms, ref_pub_buf_ms);\n-    }\n-\n-    if (!atomic_load(&failed))\n-        fprintf(stdout, \"SUCCESS: Direct buffer verification passed!\\n\");\n-\n-    if (atomic_load(&failed)) goto cleanup;\n-\n-\n-cleanup:\n-    fprintf(stdout, \"\\nCleaning up contexts...\\n\");\n-    if (pub_ctx) rist_destroy(pub_ctx);\n-    if (sub_ctx) rist_destroy(sub_ctx);\n-    if (reflector_ctx) rist_destroy(reflector_ctx);\n-    free(logging_settings);\n-\n-    if (atomic_load(&failed)) {\n-        fprintf(stderr, \"\\n============================================\\n\");\n-        fprintf(stderr, \"           TEST SUITE RESULT: FAIL          \\n\");\n-        fprintf(stderr, \"============================================\\n\\n\");\n-        ret = 1;\n-    } else {\n-        fprintf(stdout, \"\\n============================================\\n\");\n-        fprintf(stdout, \"           TEST SUITE RESULT: PASS          \\n\");\n-        fprintf(stdout, \"============================================\\n\\n\");\n-        ret = 0;\n-    }\n-\n-    return ret;\n-}\ndiff --git a/test/rist/test_rtt_mute_bonding.c b/test/rist/test_rtt_mute_bonding.c\ndeleted file mode 100644\nindex 53af432..0000000\n--- a/test/rist/test_rtt_mute_bonding.c\n+++ /dev/null\n@@ -1,343 +0,0 @@\n-/* librist. Copyright © 2026 SipRadius LLC.\n- * SPDX-License-Identifier: BSD-2-Clause\n- *\n- * End-to-end test for dynamic RTT-based bonded-leg muting (?rtt-drop=).\n- *\n- * A real two-leg bonded sender streams to a receiver over 127.0.0.1.  A\n- * latency spike on one leg is simulated by driving that leg's measured\n- * smoothed RTT (peer->eight_times_rtt) above the ?rtt-drop ceiling, which\n- * is exactly the signal the production evaluator (rist_sender_rtt_mute_check)\n- * consumes.  We then observe, through the real sender balancer and its\n- * per-leg TX counters (stats_sender_instant.sent), that:\n- *\n- *   1. both legs share the unique payload before any spike;\n- *   2. a sustained spike on leg B mutes it: leg A carries 100% of the\n- *      unique payload (leg B's data TX freezes, trickle=0) and the stream\n- *      keeps flowing;\n- *   3. once leg B's RTT recovers it rejoins the bond and carries data again;\n- *   4. when BOTH legs are over the ceiling the last-healthy-leg guard keeps\n- *      one of them in the bond (never a full black-out).\n- *\n- * Runs anywhere the public library + loopback do; no root, no netem.\n- *\n- * Exit codes:\n- *   0  - mute, reroute, recovery and last-leg guard all behaved\n- *   1  - a behavioural assertion failed\n- *   2  - INDETERMINATE: bonding never established / no data flowed\n- *  99  - setup error\n- */\n-\n-#include \"librist/librist.h\"\n-#include \"rist-private.h\"\n-\n-#include <inttypes.h>\n-#include <stdbool.h>\n-#include <stdio.h>\n-#include <stdlib.h>\n-#include <string.h>\n-\n-#ifdef _WIN32\n-#include <windows.h>\n-#define msleep(ms) Sleep(ms)\n-#else\n-#include <unistd.h>\n-#define msleep(ms) usleep((ms) * 1000)\n-#endif\n-\n-#ifndef RIST_CLOCK\n-#define RIST_CLOCK (4294967LL)\n-#endif\n-\n-#define LEG_A_PORT 20141\n-#define LEG_B_PORT 20142\n-#define RTT_DROP_MS     200\n-#define RTT_RESTORE_MS  100\n-#define RTT_SETTLE_MS   1000\n-\n-/* smoothed RTT (ms) -> peer->eight_times_rtt (8x, in RIST_CLOCK ticks) */\n-#define MS_TO_RTT8(ms) ((uint64_t)(ms) * (uint64_t)RIST_CLOCK * 8ULL)\n-\n-static struct rist_logging_settings *log_settings = NULL;\n-static volatile int rx_pkts = 0;\n-static volatile bool sender_run = true;\n-\n-static int log_cb(void *arg, enum rist_log_level level, const char *msg) {\n-\t(void)arg;\n-\tconst char *tag = level <= RIST_LOG_ERROR ? \"E\" :\n-\t                  level <= RIST_LOG_WARN  ? \"W\" :\n-\t                  level <= RIST_LOG_INFO  ? \"I\" : \"D\";\n-\tfprintf(stderr, \"%s| %s\", tag, msg);\n-\treturn 0;\n-}\n-\n-static int rx_data_cb(void *arg, struct rist_data_block *b) {\n-\t(void)arg;\n-\trx_pkts++;\n-\trist_receiver_data_block_free2(&b);\n-\treturn 0;\n-}\n-\n-static PTHREAD_START_FUNC(sender_feed, arg) {\n-\tstruct rist_ctx *tx = arg;\n-\tuint32_t counter = 0;\n-\tuint8_t buf[256];\n-\tmemset(buf, 0, sizeof(buf));\n-\twhile (sender_run) {\n-\t\tstruct rist_data_block blk;\n-\t\tmemset(&blk, 0, sizeof(blk));\n-\t\tmemcpy(buf, &counter, sizeof(counter));\n-\t\tblk.payload = buf;\n-\t\tblk.payload_len = sizeof(buf);\n-\t\tblk.virt_src_port = 1968;\n-\t\trist_sender_data_write(tx, &blk);\n-\t\tcounter++;\n-\t\tmsleep(10); /* ~100 pkt/s */\n-\t}\n-\treturn 0;\n-}\n-\n-/* Pin one or both legs' smoothed RTT to rtt_ms for hold_ms, re-writing under\n- * peerlist_lock frequently so the evaluator's 1 Hz sample always sees it (real\n- * loopback echo responses would otherwise decay it back toward zero). */\n-static void pin_rtt(struct rist_peer *a, struct rist_peer *b, int rtt_ms, int hold_ms) {\n-\tstruct rist_common_ctx *cctx = &a->sender_ctx->common;\n-\tfor (int elapsed = 0; elapsed < hold_ms; elapsed += 50) {\n-\t\tpthread_mutex_lock(&cctx->peerlist_lock);\n-\t\tif (a) a->eight_times_rtt = MS_TO_RTT8(rtt_ms);\n-\t\tif (b) b->eight_times_rtt = MS_TO_RTT8(rtt_ms);\n-\t\tpthread_mutex_unlock(&cctx->peerlist_lock);\n-\t\tmsleep(50);\n-\t}\n-}\n-\n-static struct rist_peer *add_leg(struct rist_ctx *tx, int dst_port) {\n-\tchar url[320];\n-\tsnprintf(url, sizeof(url),\n-\t         \"rist://127.0.0.1:%d?weight=5&rtt-drop=%d&rtt-restore=%d\"\n-\t         \"&rtt-drop-settle=%d&rtt-drop-trickle=0\"\n-\t         \"&session-timeout=30000&keepalive-interval=1000\",\n-\t         dst_port, RTT_DROP_MS, RTT_RESTORE_MS, RTT_SETTLE_MS);\n-\tstruct rist_peer_config *pcfg = NULL;\n-\tif (rist_parse_address2(url, (void *)&pcfg) != 0)\n-\t\treturn NULL;\n-\tstruct rist_peer *peer = NULL;\n-\tif (rist_peer_create(tx, &peer, pcfg) != 0) { free(pcfg); return NULL; }\n-\tfree(pcfg);\n-\treturn peer;\n-}\n-\n-static struct rist_peer *add_listen(struct rist_ctx *rx, int port) {\n-\tchar url[320];\n-\tsnprintf(url, sizeof(url),\n-\t         \"rist://@127.0.0.1:%d?session-timeout=30000&keepalive-interval=1000\",\n-\t         port);\n-\tstruct rist_peer_config *pcfg = NULL;\n-\tif (rist_parse_address2(url, (void *)&pcfg) != 0)\n-\t\treturn NULL;\n-\tstruct rist_peer *peer = NULL;\n-\tif (rist_peer_create(rx, &peer, pcfg) != 0) { free(pcfg); return NULL; }\n-\tfree(pcfg);\n-\treturn peer;\n-}\n-\n-int main(void) {\n-\tif (rist_logging_set(&log_settings, RIST_LOG_WARN, log_cb, NULL, NULL, stderr) != 0)\n-\t\treturn 99;\n-\n-\t/* receiver: one ctx, two bonded listening legs (same flow). */\n-\tstruct rist_ctx *rx = NULL;\n-\tif (rist_receiver_create(&rx, RIST_PROFILE_MAIN, log_settings) != 0) return 99;\n-\tif (!add_listen(rx, LEG_A_PORT) || !add_listen(rx, LEG_B_PORT)) {\n-\t\trist_destroy(rx); return 99;\n-\t}\n-\tif (rist_receiver_data_callback_set2(rx, rx_data_cb, NULL) != 0 ||\n-\t    rist_start(rx) != 0) { rist_destroy(rx); return 99; }\n-\n-\t/* sender: caller, two bonded legs to the two receiver ports. */\n-\tstruct rist_ctx *tx = NULL;\n-\tif (rist_sender_create(&tx, RIST_PROFILE_MAIN, 0, log_settings) != 0) {\n-\t\trist_destroy(rx); return 99;\n-\t}\n-\tstruct rist_peer *leg_a = add_leg(tx, LEG_A_PORT);\n-\tstruct rist_peer *leg_b = add_leg(tx, LEG_B_PORT);\n-\tif (!leg_a || !leg_b) { rist_destroy(tx); rist_destroy(rx); return 99; }\n-\tif (rist_start(tx) != 0) { rist_destroy(tx); rist_destroy(rx); return 99; }\n-\n-\tpthread_t feeder;\n-\tpthread_create(&feeder, NULL, sender_feed, tx);\n-\n-\t/* warm up: both legs authenticate and start carrying data. */\n-\tmsleep(4000);\n-\tuint64_t base_a = leg_a->stats_sender_instant.sent;\n-\tuint64_t base_b = leg_b->stats_sender_instant.sent;\n-\tint rx_warm = rx_pkts;\n-\tfprintf(stderr, \"warmup: leg_a.sent=%\"PRIu64\" leg_b.sent=%\"PRIu64\" rx=%d\\n\",\n-\t        base_a, base_b, rx_warm);\n-\tfprintf(stderr, \"diag: leg_b cfg rtt_drop=%u restore=%u settle=%u trickle=%u | \"\n-\t        \"is_data=%d listening=%d parent=%p auth=%d e8rtt=%\"PRIu64\" (~%\"PRIu64\"ms)\\n\",\n-\t        leg_b->config.rtt_drop, leg_b->config.rtt_restore,\n-\t        leg_b->config.rtt_drop_settle, leg_b->config.rtt_drop_trickle,\n-\t        leg_b->is_data, leg_b->listening, (void *)leg_b->parent,\n-\t        leg_b->authenticated, leg_b->eight_times_rtt,\n-\t        (uint64_t)(leg_b->eight_times_rtt / 8 / RIST_CLOCK));\n-\n-\t/* == Phase 1: spike leg B over the ceiling; it must mute == */\n-\tfprintf(stderr, \"\\n== phase 1: leg B RTT spike (%dms > %dms ceiling) ==\\n\",\n-\t        400, RTT_DROP_MS);\n-\tint waited = 0;\n-\twhile (!leg_b->rtt_muted && waited < 9000) {\n-\t\tpin_rtt(leg_b, NULL, 400, 500);\n-\t\twaited += 500;\n-\t}\n-\tbool muted_b = leg_b->rtt_muted;\n-\tuint32_t mute_events_b = leg_b->rtt_mute_count;\n-\tfprintf(stderr, \"leg B muted=%d mute_events=%u after %dms of spike\\n\",\n-\t        muted_b, mute_events_b, waited);\n-\t/* Measure the freeze AFTER the mute engages. With trickle disabled on this\n-\t * leg, a muted leg must carry neither new unique payload (diverted by the\n-\t * balancer) nor retransmissions (rerouted to a healthy sibling), so its\n-\t * total send rate collapses while healthy leg A keeps the stream flowing. */\n-\tuint64_t a0 = leg_a->stats_sender_instant.sent;\n-\tuint64_t b0 = leg_b->stats_sender_instant.sent;\n-\tuint32_t b0_retx = leg_b->stats_sender_instant.retrans;\n-\tint rx0 = rx_pkts;\n-\tpin_rtt(leg_b, NULL, 400, 3000); /* keep it muted */\n-\tuint64_t a1 = leg_a->stats_sender_instant.sent;\n-\tuint64_t b1 = leg_b->stats_sender_instant.sent;\n-\tuint32_t b1_retx = leg_b->stats_sender_instant.retrans;\n-\tint rx1 = rx_pkts;\n-\tuint64_t b_sent = b1 - b0;\n-\tuint32_t b_retx = b1_retx - b0_retx;\n-\tfprintf(stderr, \"while muted: dA=%\"PRIu64\" dB=%\"PRIu64\" (retx=%u) drx=%d\\n\",\n-\t        a1 - a0, b_sent, b_retx, rx1 - rx0);\n-\n-\t/* == Phase 2: leg B recovers; it must rejoin the bond == */\n-\tfprintf(stderr, \"\\n== phase 2: leg B RTT recovers ==\\n\");\n-\twaited = 0;\n-\twhile (leg_b->rtt_muted && waited < 9000) {\n-\t\tpin_rtt(leg_b, NULL, 5, 500);\n-\t\twaited += 500;\n-\t}\n-\tbool restored_b = !leg_b->rtt_muted;\n-\tfprintf(stderr, \"leg B muted=%d after %dms of recovery\\n\",\n-\t        leg_b->rtt_muted, waited);\n-\t/* Measure that leg B carries data again after rejoining. */\n-\tuint64_t b1r = leg_b->stats_sender_instant.sent;\n-\tint rx1r = rx_pkts;\n-\tpin_rtt(leg_b, NULL, 5, 2500);\n-\tuint64_t b2 = leg_b->stats_sender_instant.sent;\n-\tint rx2 = rx_pkts;\n-\tfprintf(stderr, \"after rejoin: dB=%\"PRIu64\" drx=%d\\n\", b2 - b1r, rx2 - rx1r);\n-\n-\t/* == Phase 3: both legs over the ceiling; guard keeps one active == */\n-\tfprintf(stderr, \"\\n== phase 3: both legs over ceiling (last-leg guard) ==\\n\");\n-\tpin_rtt(leg_a, leg_b, 400, 7000);\n-\tbool a_muted = leg_a->rtt_muted;\n-\tbool b_muted = leg_b->rtt_muted;\n-\tfprintf(stderr, \"both-high: leg_a muted=%d leg_b muted=%d\\n\", a_muted, b_muted);\n-\n-\t/* == Phase 4: trickle keeps a muted leg measurable (default behaviour) ==\n-\t * Re-mute leg B (leg A recovers naturally and stays active) with a 1-in-10\n-\t * trickle enabled: the muted leg must carry a low rate of redundant\n-\t * duplicate packets so RTT stays sampled for a warm restore. */\n-\tfprintf(stderr, \"\\n== phase 4: trickle on a muted leg (1-in-%d) ==\\n\", 10);\n-\tstruct rist_common_ctx *cctx = &leg_b->sender_ctx->common;\n-\tpthread_mutex_lock(&cctx->peerlist_lock);\n-\tleg_b->config.rtt_drop_trickle = 10;\n-\tleg_b->rtt_trickle_counter = 0;\n-\tpthread_mutex_unlock(&cctx->peerlist_lock);\n-\twaited = 0;\n-\twhile (!leg_b->rtt_muted && waited < 9000) {\n-\t\tpin_rtt(leg_b, NULL, 400, 500);\n-\t\twaited += 500;\n-\t}\n-\tbool trickle_muted = leg_b->rtt_muted;\n-\tuint64_t tb0 = leg_b->stats_sender_instant.sent;\n-\tuint32_t tb0_retx = leg_b->stats_sender_instant.retrans;\n-\tuint64_t ta0 = leg_a->stats_sender_instant.sent;\n-\tpin_rtt(leg_b, NULL, 400, 3000);\n-\tuint64_t tb1 = leg_b->stats_sender_instant.sent;\n-\tuint32_t tb1_retx = leg_b->stats_sender_instant.retrans;\n-\tuint64_t ta1 = leg_a->stats_sender_instant.sent;\n-\tuint64_t trickle_new = (tb1 - tb0) - (uint64_t)(tb1_retx - tb0_retx);\n-\tuint64_t healthy_new = ta1 - ta0;\n-\tfprintf(stderr, \"trickle: leg_b muted=%d trickle_dups=%\"PRIu64\" vs leg_a=%\"PRIu64\"\\n\",\n-\t        trickle_muted, trickle_new, healthy_new);\n-\n-\tsender_run = false;\n-\tpthread_join(feeder, NULL);\n-\trist_destroy(tx);\n-\trist_destroy(rx);\n-\tfree(log_settings);\n-\n-\t/* ---- verdicts ---- */\n-\tif (base_a == 0 || base_b == 0 || rx_warm == 0) {\n-\t\tfprintf(stderr, \"INDETERMINATE: bonding never established or no data \"\n-\t\t                \"(leg_a.sent=%\"PRIu64\" leg_b.sent=%\"PRIu64\" rx=%d).\\n\",\n-\t\t        base_a, base_b, rx_warm);\n-\t\treturn 2;\n-\t}\n-\tif (!muted_b) {\n-\t\tfprintf(stderr, \"FAIL: leg B stayed in the bond despite a sustained \"\n-\t\t                \"RTT spike over the ceiling.\\n\");\n-\t\treturn 1;\n-\t}\n-\tif (mute_events_b == 0) {\n-\t\tfprintf(stderr, \"FAIL: rtt_mute_events metric did not count the mute \"\n-\t\t                \"transition (got 0).\\n\");\n-\t\treturn 1;\n-\t}\n-\tif ((a1 - a0) == 0 || (rx1 - rx0) == 0) {\n-\t\tfprintf(stderr, \"FAIL: stream did not keep flowing on healthy leg A \"\n-\t\t                \"while B was muted (dA=%\"PRIu64\", drx=%d).\\n\",\n-\t\t        a1 - a0, rx1 - rx0);\n-\t\treturn 1;\n-\t}\n-\t/* With trickle disabled, a muted leg must go essentially silent: the\n-\t * balancer diverts new payload and retransmissions are rerouted to the\n-\t * healthy leg. A couple of packets can straddle the mute-transition\n-\t * boundary, so require B's total send rate to collapse to under 10% of\n-\t * what leg A carries. */\n-\tif (b_sent * 10 >= (a1 - a0)) {\n-\t\tfprintf(stderr, \"FAIL: muted leg B still carried significant traffic \"\n-\t\t                \"(B=%\"PRIu64\" retx=%u vs A=%\"PRIu64\"; mute/reroute did \"\n-\t\t                \"not take effect).\\n\", b_sent, b_retx, a1 - a0);\n-\t\treturn 1;\n-\t}\n-\tif (!restored_b) {\n-\t\tfprintf(stderr, \"FAIL: leg B did not rejoin the bond after RTT \"\n-\t\t                \"recovered.\\n\");\n-\t\treturn 1;\n-\t}\n-\tif ((b2 - b1r) == 0) {\n-\t\tfprintf(stderr, \"FAIL: restored leg B carried no payload after \"\n-\t\t                \"rejoining (dB=%\"PRIu64\").\\n\", b2 - b1r);\n-\t\treturn 1;\n-\t}\n-\tif (a_muted && b_muted) {\n-\t\tfprintf(stderr, \"FAIL: last-healthy-leg guard failed; both legs muted \"\n-\t\t                \"with every leg over the ceiling (stream black-out).\\n\");\n-\t\treturn 1;\n-\t}\n-\tif (!trickle_muted) {\n-\t\tfprintf(stderr, \"FAIL: leg B did not re-mute for the trickle phase.\\n\");\n-\t\treturn 1;\n-\t}\n-\tif (trickle_new == 0) {\n-\t\tfprintf(stderr, \"FAIL: trickle sent no redundant packets on the muted \"\n-\t\t                \"leg; warm-restore probing is not working.\\n\");\n-\t\treturn 1;\n-\t}\n-\tif (trickle_new * 2 >= healthy_new) {\n-\t\tfprintf(stderr, \"FAIL: trickle rate not sparse (dups=%\"PRIu64\" vs \"\n-\t\t                \"healthy=%\"PRIu64\"); a muted leg must only carry a low \"\n-\t\t                \"redundant rate.\\n\", trickle_new, healthy_new);\n-\t\treturn 1;\n-\t}\n-\n-\tfprintf(stderr, \"\\nPASS: bonded leg muted on RTT spike (payload moved to the \"\n-\t                \"healthy leg, stream never stalled), rejoined on recovery, and \"\n-\t                \"the last-healthy-leg guard kept one leg active when both were \"\n-\t                \"over the ceiling.\\n\");\n-\treturn 0;\n-}\ndiff --git a/test/rist/test_send_receive.c b/test/rist/test_send_receive.c\nindex 0631d63..21d9da4 100644\n--- a/test/rist/test_send_receive.c\n+++ b/test/rist/test_send_receive.c\n@@ -8,7 +8,6 @@\n #include \"librist/librist.h\"\n #include \"rist-private.h\"\n #include <stdatomic.h>\n-#include <inttypes.h>\n #include \"mpegts.h\"\n #include \"endian-shim.h\"\n \n@@ -18,11 +17,6 @@\n \n atomic_ulong failed;\n atomic_ulong stop;\n-atomic_ulong flow_attr_received;\n-int use_seq = 0;\n-int check_flow_attr = 0;\n-int has_loss = 0;\n-#define USE_SEQ_START 1000\n \n struct rist_logging_settings *logging_settings_sender = NULL;\n struct rist_logging_settings *logging_settings_receiver = NULL;\n@@ -34,22 +28,18 @@ int log_callback(void *arg, int level, const char *msg) {\n         fprintf(stdout, \"[%s] %s\",(char*)arg, msg);\n     if (level <= RIST_LOG_ERROR) {\n \tfprintf(stdout, \"[%s] [ERROR] %s\", (char* )arg, msg);\n-\tif (!has_loss) {\n-\t    atomic_store(&failed, 1);\n-\t    atomic_store(&stop, 1);\n-\t}\n+\t/* This SHOULD fail the test, I've disabled it so that we pass the encryption tests.\n+\t   in the encryption test we are hitting a condition where the linux crypto stuff seems\n+\t   to not be initialized quickly enough, and we print error messages because decryption\n+\t   is not working correctly, however this is an intermittent issue and solves itself.\n+\t   Furthermore it is not triggered by the CLI tools.\n+\t   This should be investigated and fixed */\n+        atomic_store(&failed, 1);\n+        atomic_store(&stop, 1);\n     }\n     return 0;\n }\n \n-static int flow_attr_callback(void *arg, struct rist_peer *peer, const char *json, size_t json_len) {\n-    (void)arg;\n-    (void)peer;\n-    if (json && json_len > 0)\n-        atomic_fetch_add(&flow_attr_received, 1);\n-    return 0;\n-}\n-\n struct rist_ctx *setup_rist_receiver(int profile, const char *url) {\n     struct rist_ctx *ctx;\n \tif (rist_receiver_create(&ctx, profile, logging_settings_receiver) != 0) {\n@@ -80,10 +70,7 @@ struct rist_ctx *setup_rist_receiver(int profile, const char *url) {\n     }\n #endif\n     free((void *)peer_config);\n-    if (check_flow_attr) {\n-        rist_receiver_flow_attr_callback_set(ctx, flow_attr_callback, NULL);\n-    }\n-    if (rist_start(ctx) == -1) {\n+\tif (rist_start(ctx) == -1) {\n \t\trist_log(logging_settings_receiver, RIST_LOG_ERROR, \"Could not start rist sender\\n\");\n \t\treturn NULL;\n \t}\n@@ -156,15 +143,10 @@ static PTHREAD_START_FUNC(send_data, arg) {\n             else {\n                 hdr->flags1 = htobe16(0x1111);\n             }\n-            int pkt_id = use_seq ? (send_counter + USE_SEQ_START) : send_counter;\n-            sprintf(&buffer[offset+sizeof(*hdr)+1], \"DEADBEAF TEST PACKET #%i-%i\", pkt_id, ts);\n+            sprintf(&buffer[offset+sizeof(*hdr)+1], \"DEADBEAF TEST PACKET #%i-%i\", send_counter, ts);\n         }\n         data.payload = &buffer;\n         data.payload_len = 188 * random_num;\n-        if (use_seq) {\n-            data.flags = RIST_DATA_FLAGS_USE_SEQ;\n-            data.seq = (uint64_t)(send_counter + USE_SEQ_START);\n-        }\n         int ret = rist_sender_data_write(rist_sender, &data);\n         if (ret < 0) {\n             fprintf(stderr, \"Failed to send test packet with error code %d!\\n\", ret);\n@@ -197,30 +179,24 @@ static PTHREAD_START_FUNC(send_data, arg) {\n }\n \n int main(int argc, char *argv[]) {\n-    if (argc < 5 || argc > 8) {\n+    if (argc != 5 && argc != 6) {\n         return 99;\n     }\n     int profile = atoi(argv[1]);\n     char *url1 = strdup(argv[2]);\n     char *url2 = strdup(argv[3]);\n     int losspercent = atoi(argv[4]) * 10;\n-    has_loss = losspercent > 0;\n     int npd = 0;\n     int ret = 0;\n \n-    if (argc >= 6)\n+    if (argc == 6)\n         npd = atoi(argv[5]);\n-    if (argc >= 7)\n-        use_seq = atoi(argv[6]);\n-    if (argc >= 8)\n-        check_flow_attr = atoi(argv[7]);\n \n     struct rist_ctx *receiver_ctx = NULL;\n     struct rist_ctx *sender_ctx = NULL;\n \n     atomic_init(&failed, 0);\n     atomic_init(&stop, 0);\n-    atomic_init(&flow_attr_received, 0);\n \n \n     fprintf(stdout, \"Testing profile %i with receiver url %s and sender url %s and losspercentage: %i\\n\", profile, url1, url2, losspercent);\n@@ -273,13 +249,6 @@ int main(int argc, char *argv[]) {\n             if (!got_first) {\n                 receive_count = (int)b->seq;\n \t\t\t\tgot_first = true;\n-\t\t\t\tif (use_seq && (int)b->seq < USE_SEQ_START) {\n-\t\t\t\t\tfprintf(stderr, \"USE_SEQ: first seq %\"PRIu64\" < expected start %d, \"\n-\t\t\t\t\t        \"sender-supplied seq was not preserved\\n\",\n-\t\t\t\t\t        b->seq, USE_SEQ_START);\n-\t\t\t\t\tatomic_store(&failed, 1);\n-\t\t\t\t\tatomic_store(&stop, 1);\n-\t\t\t\t}\n \t\t\t}\n             // Check entire mpegts structure\n             int tsindex = (int)(b->payload_len / 188);\n@@ -315,21 +284,8 @@ int main(int argc, char *argv[]) {\n             rist_receiver_data_block_free2((struct rist_data_block **const)&b);\n         }\n     }\n-\tint expected = 16000 * (1000 - losspercent) / 1000;\n-\tint min_receive = (expected < 12500) ? expected * 5 / 6 : 12500;\n-\tif (!got_first || receive_count < min_receive) {\n-\t\tfprintf(stderr, \"Received %d packets, minimum required %d\\n\",\n-\t\t\treceive_count, min_receive);\n+\tif (!got_first || receive_count < 12500)\n \t\tatomic_store(&failed, 1);\n-\t}\n-\tif (check_flow_attr) {\n-\t\tunsigned long fa_count = atomic_load(&flow_attr_received);\n-\t\tfprintf(stdout, \"Flow attributes received: %lu\\n\", fa_count);\n-\t\tif (fa_count == 0) {\n-\t\t\tfprintf(stderr, \"FAIL: expected at least one flow attribute callback\\n\");\n-\t\t\tatomic_store(&failed, 1);\n-\t\t}\n-\t}\n \tif (atomic_load(&failed))\n \t\tret = 1;\n \tpthread_join(send_loop, NULL);\ndiff --git a/test/rist/unit/logging_init_order.c b/test/rist/unit/logging_init_order.c\ndeleted file mode 100644\nindex 63d1303..0000000\n--- a/test/rist/unit/logging_init_order.c\n+++ /dev/null\n@@ -1,50 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause */\n-\n-/*\n- * Regression test for issue #208 (rist/librist).\n- *\n- * On Windows 11 (and any caller that uses the remote-log socket path\n- * before any rist_*_create), rist_logging_set with a non-NULL `address`\n- * argument used to fail because WSAStartup had not been called yet.\n- * The bug was specifically reachable when the public logging API ran\n- * before rist_sender_create / rist_receiver_create.\n- *\n- * This test reproduces that exact call ordering with a `udp://` remote\n- * address. On POSIX it serves as a baseline / smoke test; on Windows\n- * (run under wine in CI, see .gitlab-ci.yml build-win64) it will fail\n- * with WSANOTINITIALISED if winsock initialisation ever regresses.\n- *\n- * The test does NOT exercise any rist_*_create call and does NOT\n- * require the remote log endpoint to actually exist - we only assert\n- * that the UDP socket can be opened.\n- */\n-\n-#include \"librist/librist.h\"\n-#include \"librist/logging.h\"\n-#include <stdio.h>\n-#include <stdlib.h>\n-#include <string.h>\n-\n-int main(void)\n-{\n-\tstruct rist_logging_settings *settings = NULL;\n-\tchar address[] = \"127.0.0.1:65530\"; /* unprivileged + unlikely to clash */\n-\n-\tint ret = rist_logging_set(&settings, RIST_LOG_DEBUG,\n-\t                           NULL, NULL, address, NULL);\n-\tif (ret != 0) {\n-\t\tfprintf(stderr,\n-\t\t        \"FAIL: rist_logging_set with remote address returned %d\\n\",\n-\t\t        ret);\n-\t\treturn 1;\n-\t}\n-\tif (settings == NULL) {\n-\t\tfprintf(stderr,\n-\t\t        \"FAIL: rist_logging_set returned 0 but settings is NULL\\n\");\n-\t\treturn 1;\n-\t}\n-\n-\trist_logging_settings_free2(&settings);\n-\tprintf(\"OK\\n\");\n-\treturn 0;\n-}\ndiff --git a/test/rist/unit/meson.build b/test/rist/unit/meson.build\nindex 595bff3..6f11956 100644\n--- a/test/rist/unit/meson.build\n+++ b/test/rist/unit/meson.build\n@@ -1,237 +1,15 @@\n-cmocka = dependency('cmocka', required: false, fallback: ['cmocka', 'cmocka_dep'])\n+cmocka = meson.get_compiler('c').find_library('cmocka', required: false)\n \n if cmocka.found()\n \tif have_srp\n-\t\t# stdatomic_dependency mirrors the main librist target so the\n-\t\t# srp_unit translation units pick up the compat/msvc shim when\n-\t\t# MSVC's <stdatomic.h> requires /experimental:c11atomics.\n-\t\tsrp_unit_deps = [threads, cmocka, crypto_deps, stdatomic_dependency]\n-\t\tif host_machine.system() == 'windows'\n-\t\t\tsrp_unit_deps += cc.find_library('bcrypt')  # random.c calls BCryptGenRandom\n-\t\tendif\n \t\tsrp_unit = executable('srp_unit', rev_target,\n \t\t\t\t\t\t\t\t'srp_examples.c',\n \t\t\t\t\t\t\t\t'../../../contrib/pthread-shim.c',\n \t\t\t\t\t\t\t\t'../../../src/crypto/random.c',\n \t\t\t\t\t\t\t\tinclude_directories : inc,\n-\t\t\t\t\t\t\t\tdependencies : srp_unit_deps,\n+\t\t\t\t\t\t\t\tdependencies : [threads, cmocka,crypto_deps],\n \t\t)\n \n \t\ttest('srp_unit_test', srp_unit, suite:['unit'])\n \tendif\n-endif\n-\n-# Regression test for #208: rist_logging_set with a remote (udp://)\n-# address must succeed before any rist_*_create call. Does not require\n-# cmocka - links against the public librist API only.\n-logging_init_order = executable('logging_init_order',\n-                                'logging_init_order.c',\n-                                include_directories : inc,\n-                                link_with : librist)\n-test('logging_init_order', logging_init_order, suite:['unit'])\n-\n-# Advanced Profile (TR-06-3) header parse/build round-trip tests.\n-# Compiles adv.c directly to access RIST_PRIV (hidden-visibility) symbols.\n-adv_test_deps = [lz4_lib]\n-if host_machine.system() == 'windows'\n-\tadv_test_deps += cc.find_library('ws2_32')\n-endif\n-test_adv_roundtrip = executable('test_adv_roundtrip',\n-                                'test_adv_roundtrip.c',\n-                                '../../../src/adv.c',\n-                                include_directories : inc,\n-                                dependencies : adv_test_deps)\n-test('adv_roundtrip', test_adv_roundtrip, suite:['unit'])\n-\n-# EAP v4 crypto primitives (HKDF-Expand-SHA256 + AES-256-GCM) known-answer tests.\n-# Compiles eap_v4_crypto.c directly to reach RIST_PRIV (hidden-visibility)\n-# symbols. Gated on have_srp, which implies a real crypto backend (mbedTLS or\n-# Nettle) is present; the built-in fallback has no AEAD.\n-if have_srp\n-\teap_v4_crypto_deps = [crypto_deps]\n-\tif host_machine.system() == 'windows'\n-\t\teap_v4_crypto_deps += cc.find_library('bcrypt')  # mbedcrypto entropy_poll calls BCryptGenRandom\n-\tendif\n-\ttest_eap_v4_crypto = executable('test_eap_v4_crypto',\n-\t                                'test_eap_v4_crypto.c',\n-\t                                '../../../src/crypto/eap_v4_crypto.c',\n-\t                                include_directories : inc,\n-\t                                dependencies : eap_v4_crypto_deps)\n-\ttest('eap_v4_crypto', test_eap_v4_crypto, suite:['unit'])\n-endif\n-\n-# URL ?profile= parsing exercised through the public rist_parse_address2 API.\n-test_url_profile_parse = executable('test_url_profile_parse',\n-                                    'test_url_profile_parse.c',\n-                                    include_directories : inc,\n-                                    link_with : librist)\n-test('url_profile_parse', test_url_profile_parse, suite:['unit'])\n-\n-# ?profile= URL override semantics inside rist_peer_create.\n-test_url_profile_override = executable('test_url_profile_override',\n-                                       'test_url_profile_override.c',\n-                                       include_directories : inc,\n-                                       link_with : librist)\n-test('url_profile_override', test_url_profile_override, suite:['unit'])\n-\n-# ?srp-compat= URL parsing exercised through the public rist_parse_address2 API.\n-test_url_srp_compat_parse = executable('test_url_srp_compat_parse',\n-                                       'test_url_srp_compat_parse.c',\n-                                       include_directories : inc,\n-                                       link_with : librist)\n-test('url_srp_compat_parse', test_url_srp_compat_parse, suite:['unit'])\n-\n-# Versioned peer-config defaults gate fields by RIST_PEER_CONFIG_VERSION.\n-test_peer_config_versioned_defaults = executable('test_peer_config_versioned_defaults',\n-                                                 'test_peer_config_versioned_defaults.c',\n-                                                 include_directories : inc,\n-                                                 link_with : librist)\n-test('peer_config_versioned_defaults', test_peer_config_versioned_defaults, suite:['unit'])\n-\n-# ?recovery-priority= URL parsing exercised through the public API.\n-test_url_recovery_priority_parse = executable('test_url_recovery_priority_parse',\n-                                              'test_url_recovery_priority_parse.c',\n-                                              include_directories : inc,\n-                                              link_with : librist)\n-test('url_recovery_priority_parse', test_url_recovery_priority_parse, suite:['unit'])\n-\n-# NACK routing peer-selection predicate (header-only, no link needed).\n-test_nack_peer_select = executable('test_nack_peer_select',\n-                                   'test_nack_peer_select.c',\n-                                   include_directories : inc)\n-test('nack_peer_select', test_nack_peer_select, suite:['unit'])\n-\n-# Receiver gap-detector sequence-number gap helper (rist_seq_gap): verifies\n-# 16-bit wrap for short-sequence (Simple/Main) flows vs the true >64k gap for\n-# 32-bit (Advanced) flows. Pure, link-free.\n-test_seq_gap = executable('test_seq_gap',\n-                          'test_seq_gap.c',\n-                          include_directories : inc)\n-test('seq_gap', test_seq_gap, suite:['unit'])\n-\n-# Expected-next-sequence helper (rist_seq_next): verifies 16-bit wrap for\n-# short-sequence flows (incl. odd successors) and 32-bit successor for\n-# Advanced flows. Pure, link-free.\n-test_seq_next = executable('test_seq_next',\n-                           'test_seq_next.c',\n-                           include_directories : inc)\n-test('seq_next', test_seq_next, suite:['unit'])\n-\n-# rist_recovery_depth_set() level -> ring-size mapping/resize contract.\n-# Includes rist-private.h to inspect internal ring sizes.\n-test_recovery_depth = executable('test_recovery_depth',\n-                                 'test_recovery_depth.c',\n-                                 include_directories : inc,\n-                                 link_with : librist,\n-                                 dependencies : [threads, stdatomic_dependency])\n-test('recovery_depth', test_recovery_depth, suite:['unit'])\n-\n-# udp:// scheme-prefix parsing: guards prefix[16] against copy-length\n-# underflow (leading '/') and out-of-bounds NUL write (over-long scheme).\n-test_url_udp_prefix_parse = executable('test_url_udp_prefix_parse',\n-                                       'test_url_udp_prefix_parse.c',\n-                                       include_directories : inc,\n-                                       link_with : librist)\n-test('url_udp_prefix_parse', test_url_udp_prefix_parse, suite:['unit'])\n-\n-# ?recovery-depth= URL parsing exercised through the public API.\n-test_url_recovery_depth_parse = executable('test_url_recovery_depth_parse',\n-                                           'test_url_recovery_depth_parse.c',\n-                                           include_directories : inc,\n-                                           link_with : librist)\n-test('url_recovery_depth_parse', test_url_recovery_depth_parse, suite:['unit'])\n-\n-# Retransmission sequence-domain predicate (header-only, no link needed):\n-# an Advanced head-end serving a Main-downgraded peer must resolve that\n-# peer's NACKs in the 16-bit RTP domain, else recovered_packets_total = 0.\n-test_retx_seq_domain = executable('test_retx_seq_domain',\n-                                  'test_retx_seq_domain.c',\n-                                  include_directories : inc)\n-test('retx_seq_domain', test_retx_seq_domain, suite:['unit'])\n-\n-# Advanced Profile receive-side 1 MHz source-clock reconstruction\n-# (rist_adv_ts_reconstruct): wrap-safe delta accumulation, retransmit\n-# positioning, and the bounded-delta fallback to arrival. Pure, link-free.\n-test_adv_ts_reconstruct = executable('test_adv_ts_reconstruct',\n-                                     'test_adv_ts_reconstruct.c',\n-                                     include_directories : inc)\n-test('adv_ts_reconstruct', test_adv_ts_reconstruct, suite:['unit'])\n-\n-# Flow re-anchor staleness guard: a packet from a leg queued deeper than the\n-# buffer must not become the flow baseline. Header-only, no link.\n-test_reanchor = executable('test_reanchor',\n-                           'test_reanchor.c',\n-                           include_directories : inc)\n-test('reanchor', test_reanchor, suite:['unit'])\n-\n-# Grace period that keeps a silent peer in the send rotation for one recovery\n-# buffer window (#196), so a late RTCP reply does not cut a single-path stream\n-# whose receiver is still there. Header-only, no link.\n-test_send_grace = executable('test_send_grace',\n-                             'test_send_grace.c',\n-                             include_directories : inc)\n-test('send_grace', test_send_grace, suite:['unit'])\n-\n-# RTT-based bonded-leg muting hysteresis state machine (header-only, no link).\n-test_rtt_mute = executable('test_rtt_mute',\n-                           'test_rtt_mute.c',\n-                           include_directories : inc)\n-test('rtt_mute', test_rtt_mute, suite:['unit'])\n-\n-# Retransmission-budget guard: ?bandwidth= caps payload plus retransmissions\n-# together, so a ceiling at or under the payload rate refuses every NACK while\n-# only logging it at debug level. Header-only, no link.\n-test_bandwidth_guard = executable('test_bandwidth_guard',\n-                                  'test_bandwidth_guard.c',\n-                                  include_directories : inc)\n-test('bandwidth_guard', test_bandwidth_guard, suite:['unit'])\n-\n-# recovery_maxbitrate = 0 through the C API means \"unset\", not \"no ceiling\",\n-# and must be replaced with the default before a peer runs (#229).\n-test_recovery_maxbitrate_zero = executable('test_recovery_maxbitrate_zero',\n-                                           'test_recovery_maxbitrate_zero.c',\n-                                           include_directories : inc,\n-                                           link_with : librist)\n-test('recovery_maxbitrate_zero', test_recovery_maxbitrate_zero, suite:['unit'])\n-\n-# ?rtt-drop= et al URL parsing exercised through the public API.\n-test_url_rtt_drop_parse = executable('test_url_rtt_drop_parse',\n-                                     'test_url_rtt_drop_parse.c',\n-                                     include_directories : inc,\n-                                     link_with : librist)\n-test('url_rtt_drop_parse', test_url_rtt_drop_parse, suite:['unit'])\n-\n-# ?multiplex-mode= / ?rtp-ptype= URL parsing at the compiled-in udp config\n-# version, so a bump cannot leave either unreachable.\n-test_url_udp_versioned_params = executable('test_url_udp_versioned_params',\n-                                           'test_url_udp_versioned_params.c',\n-                                           include_directories : inc,\n-                                           link_with : librist)\n-test('url_udp_versioned_params', test_url_udp_versioned_params, suite:['unit'])\n-\n-# TUN CIDR parsing (IPv4 + IPv6) used by risttunnel -l.\n-test_tun_cidr_parse = executable('test_tun_cidr_parse',\n-                                 'test_tun_cidr_parse.c',\n-                                 include_directories : inc,\n-                                 link_with : librist)\n-test('tun_cidr_parse', test_tun_cidr_parse, suite:['unit'])\n-\n-# CBR output pacing scheduler (rist_pacer): rate estimate, occupancy trim,\n-# due-time schedule and slip resync. Every failure mode of this module still\n-# emits packets -- at the wrong cadence or the wrong rate -- so the invariants\n-# are asserted rather than inspected, including a full-sequence cross-check\n-# against an independent transcription of the algorithm.\n-test_rist_pacer = executable('test_rist_pacer',\n-                             'rist_pacer_test.c',\n-                             '../../../src/rist_pacer.c',\n-                             include_directories : inc)\n-test('rist_pacer', test_rist_pacer, suite:['unit'])\n-\n-# ?cbr-output= parsing plus its context-wide override semantics. The setting is\n-# not per-peer -- a flow's legs share one output -- so conflicting peers must be\n-# refused rather than silently resolved.\n-test_url_cbr_output = executable('test_url_cbr_output',\n-                                 'test_url_cbr_output.c',\n-                                 include_directories : inc,\n-                                 link_with : librist)\n-test('url_cbr_output', test_url_cbr_output, suite:['unit'])\n+endif\n\\ No newline at end of file\ndiff --git a/test/rist/unit/rist_pacer_test.c b/test/rist/unit/rist_pacer_test.c\ndeleted file mode 100644\nindex 5451f1c..0000000\n--- a/test/rist/unit/rist_pacer_test.c\n+++ /dev/null\n@@ -1,434 +0,0 @@\n-/* librist. Copyright © 2026 SipRadius LLC. All right reserved.\n- * Author: Sergio Ammirata <sergio@ammirata.net>\n- *\n- * SPDX-License-Identifier: BSD-2-Clause\n- */\n-\n-/* Unit tests for the CBR pacing scheduler (rist_pacer).\n- *\n- * Every failure mode of this module still emits packets, at the wrong cadence or\n- * the wrong rate, so the invariants are asserted rather than inspected. */\n-\n-#include \"rist_pacer.h\"\n-\n-#include <assert.h>\n-#include <stdio.h>\n-\n-/* A representative CBR datagram: seven MPEG-TS packets at 71.25 Mbps. */\n-#define DGRAM_BYTES  (7 * 188)\n-#define RATE_BPS     71250000ULL\n-\n-static void test_rate_converges(void)\n-{\n-\tstruct rist_pacer_rate r;\n-\tuint64_t now_us = 1000000;\n-\n-\trist_pacer_rate_init(&r, 0);\n-\tassert(rist_pacer_rate_bps(&r) == 0);   /* nothing known yet: do not pace */\n-\n-\t/* Six seconds of exact-rate traffic, fed one datagram at a time. */\n-\tuint64_t step_us = (uint64_t)DGRAM_BYTES * 8ULL * 1000000ULL / RATE_BPS;\n-\tfor (int i = 0; i < 6 * 1000000 / (int)step_us; i++) {\n-\t\tnow_us += step_us;\n-\t\trist_pacer_rate_add(&r, DGRAM_BYTES, now_us);\n-\t}\n-\n-\tuint64_t bps = rist_pacer_rate_bps(&r);\n-\tdouble err = ((double)bps - (double)RATE_BPS) / (double)RATE_BPS;\n-\tif (err < 0)\n-\t\terr = -err;\n-\tassert(err < 0.01);\n-\tprintf(\"  rate converged to %llu bps (want %llu, err %.3f%%)\\n\",\n-\t       (unsigned long long)bps, (unsigned long long)RATE_BPS, err * 100.0);\n-}\n-\n-static void test_rate_freezes_across_outage(void)\n-{\n-\tstruct rist_pacer_rate r;\n-\tuint64_t now_us = 1000000;\n-\n-\trist_pacer_rate_init(&r, RATE_BPS);\n-\tuint64_t before = rist_pacer_rate_bps(&r);\n-\tassert(before == RATE_BPS);\n-\n-\t/* Data resuming 30 s later must not average in as a near-zero rate. */\n-\trist_pacer_rate_add(&r, DGRAM_BYTES, now_us);\n-\tnow_us += 30000000;\n-\trist_pacer_rate_add(&r, DGRAM_BYTES, now_us);\n-\n-\tassert(rist_pacer_rate_bps(&r) == before);\n-\tprintf(\"  rate held at %llu bps across a 30 s outage\\n\",\n-\t       (unsigned long long)before);\n-}\n-\n-static void test_interval_ignores_sleep_floor(void)\n-{\n-\tstruct rist_pacer p;\n-\n-\t/* Clamping a 148 us interval up to a 250 us floor would pace at 42 Mbps\n-\t * instead of 71: smooth-looking output, 40% too slow. */\n-\trist_pacer_init(&p, 0.0, 250000, 5000000);\n-\n-\tuint64_t interval = rist_pacer_interval_ns(&p, RATE_BPS, DGRAM_BYTES, 0);\n-\tuint64_t want = (uint64_t)DGRAM_BYTES * 8ULL * 1000000000ULL / RATE_BPS;\n-\tassert(interval == want);\n-\tassert(interval < p.min_sleep_ns);\n-\n-\tassert(rist_pacer_interval_ns(&p, 0, DGRAM_BYTES, 0) == 0);\n-\tprintf(\"  interval stayed at %llu ns, below the %llu ns sleep floor\\n\",\n-\t       (unsigned long long)interval, (unsigned long long)p.min_sleep_ns);\n-}\n-\n-static void test_feedback_direction_and_clamp(void)\n-{\n-\tstruct rist_pacer p, raw;\n-\trist_pacer_init(&p, 40.0, 0, 5000000);\n-\trist_pacer_init(&raw, 0.0, 0, 5000000);\n-\n-\t/* Reference needs feedback off: occupancy 0 is itself a large fill trim. */\n-\tuint64_t base = rist_pacer_interval_ns(&raw, RATE_BPS, DGRAM_BYTES, 0);\n-\n-\t/* Target fill is 40 ms at this rate, i.e. ~271 datagrams. */\n-\tdouble target_dg = ((double)RATE_BPS / 8.0 * 0.040) / (double)DGRAM_BYTES;\n-\tuint64_t at_target = rist_pacer_interval_ns(&p, RATE_BPS, DGRAM_BYTES,\n-\t                                            (size_t)target_dg);\n-\tuint64_t over = rist_pacer_interval_ns(&p, RATE_BPS, DGRAM_BYTES,\n-\t                                       (size_t)(target_dg * 4));\n-\tuint64_t under = rist_pacer_interval_ns(&p, RATE_BPS, DGRAM_BYTES, 0);\n-\n-\t/* At target the trim is a no-op; above it drains, below it fills. */\n-\tdouble at_err = ((double)at_target - (double)base) / (double)base;\n-\tif (at_err < 0)\n-\t\tat_err = -at_err;\n-\tassert(at_err < 0.01);\n-\tassert(over < at_target);\n-\tassert(under > at_target);\n-\n-\t/* Both directions clamped, so no queue excursion can slew the output rate\n-\t * more than 20% away from the measured one. */\n-\tassert(over >= (uint64_t)((double)base * RIST_PACER_FEEDBACK_MIN) - 1);\n-\tassert(under <= (uint64_t)((double)base * RIST_PACER_FEEDBACK_MAX) + 1);\n-\tprintf(\"  feedback: %llu ns at target (raw %llu), %llu when full, %llu when empty\\n\",\n-\t       (unsigned long long)at_target, (unsigned long long)base,\n-\t       (unsigned long long)over, (unsigned long long)under);\n-}\n-\n-static void test_quota_averages_the_true_rate(void)\n-{\n-\tstruct rist_pacer p;\n-\trist_pacer_init(&p, 0.0, 250000, 5000000);\n-\n-\tuint64_t interval = rist_pacer_interval_ns(&p, RATE_BPS, DGRAM_BYTES, 0);\n-\tuint64_t now_ns = 1000000000ULL;\n-\tuint64_t released = 0;\n-\tconst int wakes = 4000;   /* 1 s of wakes at the 250 us floor */\n-\n-\tfor (int i = 0; i < wakes; i++) {\n-\t\treleased += rist_pacer_quota(&p, now_ns, interval, 64);\n-\t\tnow_ns += 250000;\n-\t}\n-\n-\t/* A floor coarser than the interval must not throttle: the batch absorbs it. */\n-\tdouble got_bps = (double)released * DGRAM_BYTES * 8.0;\n-\tdouble err = (got_bps - (double)RATE_BPS) / (double)RATE_BPS;\n-\tif (err < 0)\n-\t\terr = -err;\n-\tassert(err < 0.01);\n-\tprintf(\"  quota over %d wakes released %llu datagrams = %.2f Mbps (err %.3f%%)\\n\",\n-\t       wakes, (unsigned long long)released, got_bps / 1e6, err * 100.0);\n-}\n-\n-static void test_quota_ungated_without_an_estimate(void)\n-{\n-\tstruct rist_pacer p;\n-\trist_pacer_init(&p, 0.0, 250000, 5000000);\n-\n-\t/* No rate means no authority to hold anything back. */\n-\tassert(rist_pacer_quota(&p, 1000000000ULL, 0, 32) == 32);\n-\tprintf(\"  no estimate: quota passes everything through\\n\");\n-}\n-\n-static void test_slip_resyncs_without_catching_up(void)\n-{\n-\tstruct rist_pacer p;\n-\trist_pacer_init(&p, 0.0, 250000, 5000000);\n-\n-\tuint64_t interval = rist_pacer_interval_ns(&p, RATE_BPS, DGRAM_BYTES, 0);\n-\tuint64_t now_ns = 1000000000ULL;\n-\n-\trist_pacer_quota(&p, now_ns, interval, 64);\n-\tassert(p.underruns == 0);\n-\n-\t/* Draining 2 s of arrears would be ~13500 datagrams in one wake. */\n-\tnow_ns += 2000000000ULL;\n-\tunsigned n = rist_pacer_quota(&p, now_ns, interval, 64);\n-\tassert(p.underruns == 1);\n-\tassert(n <= 1);\n-\tprintf(\"  2 s stall: resynced with %u datagram(s), %llu underrun(s)\\n\",\n-\t       n, (unsigned long long)p.underruns);\n-\n-\t/* Lateness inside the window is drift, to be corrected, not a resync. */\n-\tuint64_t before = p.underruns;\n-\tnow_ns += RIST_PACER_MAX_SLIP_NS / 2;\n-\trist_pacer_quota(&p, now_ns, interval, 64);\n-\tassert(p.underruns == before);\n-\tprintf(\"  lateness inside the slip window did not resync\\n\");\n-}\n-\n-static void test_sleep_clamped_both_ends(void)\n-{\n-\tstruct rist_pacer p;\n-\trist_pacer_init(&p, 0.0, 250000, 5000000);\n-\tuint64_t now_ns = 1000000000ULL;\n-\n-\t/* Idle, nothing scheduled: wait the normal idle interval. */\n-\tassert(rist_pacer_sleep_ns(&p, now_ns) == 5000000);\n-\n-\t/* Sooner than the floor: floored, bounding the wakeup rate. */\n-\trist_pacer_due_ns(&p, now_ns);\n-\trist_pacer_advance(&p, 148000);\n-\tassert(rist_pacer_sleep_ns(&p, now_ns) == 250000);\n-\n-\t/* A far-future due time still respects the ceiling. */\n-\trist_pacer_reset(&p);\n-\trist_pacer_due_ns(&p, now_ns);\n-\trist_pacer_advance(&p, 900000000ULL);\n-\tassert(rist_pacer_sleep_ns(&p, now_ns) == 5000000);\n-\n-\t/* Already due: wake as soon as the floor allows. */\n-\trist_pacer_reset(&p);\n-\trist_pacer_due_ns(&p, now_ns);\n-\tassert(rist_pacer_sleep_ns(&p, now_ns + 1000) == 250000);\n-\n-\tprintf(\"  sleep clamped to [250 us, 5 ms] in all four cases\\n\");\n-}\n-\n-static void test_reset_keeps_configuration(void)\n-{\n-\tstruct rist_pacer p;\n-\trist_pacer_init(&p, 40.0, 250000, 5000000);\n-\n-\tuint64_t now_ns = 1000000000ULL;\n-\trist_pacer_due_ns(&p, now_ns);\n-\trist_pacer_advance(&p, 148000);\n-\tassert(p.next_due_ns != 0);\n-\n-\trist_pacer_reset(&p);\n-\tassert(p.next_due_ns == 0);\n-\tassert(p.min_sleep_ns == 250000 && p.max_sleep_ns == 5000000);\n-\tassert(p.target_latency_ms == 40.0);\n-\n-\t/* A known discontinuity must not be charged as an underrun. */\n-\tuint64_t before = p.underruns;\n-\trist_pacer_due_ns(&p, now_ns + 10000000000ULL);\n-\tassert(p.underruns == before);\n-\tprintf(\"  reset re-anchors the schedule without counting an underrun\\n\");\n-}\n-\n-/* A replay of the receiver's output gate over a bursty source, where the whole of\n- * a burst shares one release deadline. Modelled in the units the gate uses, so a\n- * regression shows up as a batch - the symptom that matters - rather than as a\n- * changed constant. */\n-#define GATE_WAKE_NS     107000ULL          /* measured output-loop wake gap */\n-#define GATE_HOLD_NS     10000000ULL        /* 2 x the 5 ms wake ceiling */\n-#define GATE_BURST       12\n-#define GATE_QUEUE       64                 /* undrained bursts; one is the observed depth */\n-\n-static void gate_replay(uint64_t hold_ns, int reanchor_when_idle,\n-                        unsigned duty, unsigned *max_per_wake, double *out_bps)\n-{\n-\tstruct rist_pacer p;\n-\trist_pacer_init(&p, 0.0, 250000, 5000000);\n-\n-\tuint64_t interval = (uint64_t)DGRAM_BYTES * 8ULL * 1000000000ULL / RATE_BPS;\n-\tuint64_t now = 1000000000ULL;\n-\tuint64_t next_arrival = now;\n-\tunsigned released = 0, worst = 0;\n-\tconst unsigned wakes = 200000;\n-\n-\t/* Deadlines rather than a count, because a burst carries one between all of\n-\t * it and a packet stays overdue against it across however many wakes it\n-\t * waits - which is what a tight ceiling eventually trips on. */\n-\tuint64_t due[GATE_QUEUE];\n-\tunsigned left[GATE_QUEUE];\n-\tunsigned qh = 0, qt = 0;\n-\n-\tfor (unsigned w = 0; w < wakes; w++, now += GATE_WAKE_NS) {\n-\t\twhile (next_arrival <= now) {\n-\t\t\tassert(qt - qh < GATE_QUEUE);\n-\t\t\tdue[qt & (GATE_QUEUE - 1)] = next_arrival;\n-\t\t\tleft[qt & (GATE_QUEUE - 1)] = GATE_BURST;\n-\t\t\tqt++;\n-\t\t\tnext_arrival += duty * GATE_BURST * interval;\n-\t\t}\n-\n-\t\tunsigned this_wake = 0;\n-\t\tint stopped_on_pacer = 0;\n-\t\twhile (qh != qt) {\n-\t\t\tuint64_t head_due = due[qh & (GATE_QUEUE - 1)];\n-\t\t\tuint64_t overdue = now > head_due ? now - head_due : 0;\n-\t\t\tif (rist_pacer_due_ns(&p, now) > now) {\n-\t\t\t\tif (overdue < hold_ns) {\n-\t\t\t\t\tstopped_on_pacer = 1;\n-\t\t\t\t\tbreak;\n-\t\t\t\t}\n-\t\t\t\trist_pacer_reset(&p);\n-\t\t\t\trist_pacer_due_ns(&p, now);\n-\t\t\t}\n-\t\t\trist_pacer_advance(&p, interval);\n-\t\t\tif (--left[qh & (GATE_QUEUE - 1)] == 0)\n-\t\t\t\tqh++;\n-\t\t\tthis_wake++;\n-\t\t\treleased++;\n-\t\t}\n-\t\tif (reanchor_when_idle && !stopped_on_pacer)\n-\t\t\trist_pacer_reset(&p);\n-\n-\t\tif (w > wakes / 10 && this_wake > worst)\n-\t\t\tworst = this_wake;\n-\t}\n-\n-\t*max_per_wake = worst;\n-\t*out_bps = (double)released * DGRAM_BYTES * 8.0\n-\t         / ((double)wakes * GATE_WAKE_NS / 1e9);\n-}\n-\n-static void test_burst_spreads_at_the_paced_cadence(void)\n-{\n-\tunsigned worst;\n-\tdouble bps;\n-\n-\t/* Both rules in force: the burst leaves one datagram at a time, and the\n-\t * spreading costs nothing in throughput. */\n-\tgate_replay(GATE_HOLD_NS, 1, 1, &worst, &bps);\n-\tassert(worst == 1);\n-\tdouble err = (bps - (double)RATE_BPS) / (double)RATE_BPS;\n-\tif (err < 0)\n-\t\terr = -err;\n-\tassert(err < 0.01);\n-\tprintf(\"  burst of %d spread to %u datagram per wake at %.2f Mbps\\n\",\n-\t       GATE_BURST, worst, bps / 1e6);\n-\n-\t/* A ceiling under the drain time re-anchors mid-burst, and the rest of the\n-\t * burst follows it out in the same wake. */\n-\tgate_replay(1000000ULL, 1, 1, &worst, &bps);\n-\tassert(worst > 1);\n-\tprintf(\"  ceiling below the drain released %u at once\\n\", worst);\n-\n-\t/* A source that pauses between bursts leaves the schedule behind the clock.\n-\t * Re-anchored it still spreads; left running it spends the gap at once. */\n-\tgate_replay(GATE_HOLD_NS, 1, 2, &worst, &bps);\n-\tassert(worst == 1);\n-\tprintf(\"  gapped source, re-anchored: still %u per wake\\n\", worst);\n-\n-\tgate_replay(GATE_HOLD_NS, 0, 2, &worst, &bps);\n-\tassert(worst > 1);\n-\tprintf(\"  gapped source, schedule left running: %u at once\\n\", worst);\n-}\n-\n-/* A second, independent implementation of the schedule, so the whole sequence of\n- * send times can be compared rather than individual calls. Deliberately not\n- * factored against the module: a shared helper would defeat the purpose. */\n-#define REF_TARGET_LATENCY_MS 40.0\n-#define REF_FEEDBACK_K        0.5\n-#define REF_MAX_SLIP_NS       50000000ULL\n-\n-static uint64_t ref_feedback(uint64_t interval_ns, size_t occ, uint64_t rate, size_t len)\n-{\n-\tdouble target_bytes = (double)rate / 8.0 * (REF_TARGET_LATENCY_MS / 1000.0);\n-\tdouble target_dg = target_bytes / (double)(len ? len : 1);\n-\tif (target_dg < 1.0)\n-\t\ttarget_dg = 1.0;\n-\tdouble err = ((double)occ - target_dg) / target_dg;\n-\tdouble factor = 1.0 - REF_FEEDBACK_K * err;\n-\tif (factor < 0.80)\n-\t\tfactor = 0.80;\n-\tif (factor > 1.20)\n-\t\tfactor = 1.20;\n-\treturn (uint64_t)((double)interval_ns * factor);\n-}\n-\n-static void test_matches_reference_implementation(void)\n-{\n-\tstruct rist_pacer p;\n-\trist_pacer_init(&p, REF_TARGET_LATENCY_MS, 0, 0);\n-\n-\tuint64_t ref_next = 0, ref_underruns = 0;\n-\tuint64_t now_ns = 5000000000ULL;\n-\tunsigned compared = 0;\n-\n-\tfor (int i = 0; i < 20000; i++) {\n-\t\t/* Sweeps both feedback clamps, a rate step, a no-estimate stretch and a\n-\t\t * gap past the slip threshold. */\n-\t\tsize_t occ = (size_t)((i * 37) % 600);\n-\t\tsize_t len = DGRAM_BYTES;\n-\t\tuint64_t rate = (i > 500 && i < 700) ? 0\n-\t\t              : (i < 9000 ? RATE_BPS : RATE_BPS + 250000);\n-\t\tif (i == 12000)\n-\t\t\tnow_ns += 3 * RIST_PACER_MAX_SLIP_NS;\n-\n-\t\t/* --- reference --- */\n-\t\tuint64_t ref_due = 0;\n-\t\tint ref_paced = 0;\n-\t\tif (rate == 0) {\n-\t\t\tref_next = 0;\n-\t\t} else {\n-\t\t\tuint64_t iv = ((uint64_t)len * 8ULL * 1000000000ULL) / rate;\n-\t\t\tiv = ref_feedback(iv, occ, rate, len);\n-\t\t\tif (ref_next == 0) {\n-\t\t\t\tref_next = now_ns;\n-\t\t\t} else if (now_ns > ref_next + REF_MAX_SLIP_NS) {\n-\t\t\t\tref_next = now_ns;\n-\t\t\t\tref_underruns++;\n-\t\t\t}\n-\t\t\tref_due = ref_next;\n-\t\t\tref_next += iv;\n-\t\t\tref_paced = 1;\n-\t\t}\n-\n-\t\t/* --- module --- */\n-\t\tuint64_t mod_due = 0;\n-\t\tint mod_paced = 0;\n-\t\tuint64_t iv = rist_pacer_interval_ns(&p, rate, len, occ);\n-\t\tif (iv == 0) {\n-\t\t\trist_pacer_reset(&p);\n-\t\t} else {\n-\t\t\tmod_due = rist_pacer_due_ns(&p, now_ns);\n-\t\t\trist_pacer_advance(&p, iv);\n-\t\t\tmod_paced = 1;\n-\t\t}\n-\n-\t\tassert(mod_paced == ref_paced);\n-\t\tif (ref_paced) {\n-\t\t\tassert(mod_due == ref_due);\n-\t\t\tcompared++;\n-\t\t}\n-\n-\t\t/* Advance as a real sender would: to the due time it just honoured. */\n-\t\tnow_ns = (ref_paced && ref_due > now_ns) ? ref_due + 1000 : now_ns + 148000;\n-\t}\n-\n-\tassert(p.underruns == ref_underruns);\n-\tassert(ref_underruns >= 1);\n-\tprintf(\"  reference cross-check: %u send times identical, \"\n-\t       \"%llu underrun(s) both ways\\n\",\n-\t       compared, (unsigned long long)ref_underruns);\n-}\n-\n-int main(void)\n-{\n-\tprintf(\"shared CBR pacing scheduler\\n\");\n-\ttest_rate_converges();\n-\ttest_rate_freezes_across_outage();\n-\ttest_interval_ignores_sleep_floor();\n-\ttest_feedback_direction_and_clamp();\n-\ttest_quota_averages_the_true_rate();\n-\ttest_quota_ungated_without_an_estimate();\n-\ttest_slip_resyncs_without_catching_up();\n-\ttest_sleep_clamped_both_ends();\n-\ttest_reset_keeps_configuration();\n-\ttest_burst_spreads_at_the_paced_cadence();\n-\ttest_matches_reference_implementation();\n-\tprintf(\"all pacer invariants held\\n\");\n-\treturn 0;\n-}\ndiff --git a/test/rist/unit/srp_examples.c b/test/rist/unit/srp_examples.c\nindex 80c9fe6..6f835b5 100644\n--- a/test/rist/unit/srp_examples.c\n+++ b/test/rist/unit/srp_examples.c\n@@ -1,7 +1,4 @@\n-/* This set of unit tests covers the entire SRP flow, with deterministic\n- * a/b/salt fixtures (DEBUG_USE_EXAMPLE_CONSTANTS=1) on the 2048-bit\n- * NG_DEFAULT group from RFC 5054 Appendix A.  Fixtures were regenerated\n- * for librist 0.2.16+ audit3 H3 (RFC 5054 PAD compliance, §2.6). */\n+//This set of unit tests covers the entire SRP flow verified against the example in the VSF document\n \n #include \"config.h\"\n #include <stdarg.h>\n@@ -19,11 +16,10 @@\n #define DEBUG_USE_EXAMPLE_CONSTANTS 1\n \n #if HAVE_MBEDTLS\n-// musl's sched.h declares calloc, so include it before we redefine\n-// calloc to the cmocka allocator (POSIX-only header).\n-#ifndef _WIN32\n+// musl's sched.h includes a prototype for calloc, so we need to make\n+// sure it's already been included before we redefine it to something\n+// that won't expand to a valid prototype.\n #include <sched.h>\n-#endif\n \n #define malloc(size) _test_malloc(size, __FILE__, __LINE__)\n #define calloc(num, size) _test_calloc(num, size, __FILE__, __LINE__)\n@@ -33,18 +29,6 @@\n #include \"src/crypto/srp.c\"\n #include \"src/crypto/srp_constants.c\"\n \n-/* random.c (compiled into the srp_unit binary) calls rist_log_priv3() on\n- * CSPRNG init failure since release-0.2.16. The unit test deliberately\n- * does not link the full logging machinery, so provide a no-op stub\n- * here to satisfy the linker. The behaviour being logged - CSPRNG seed\n- * failure - is not reachable from the SRP unit test paths anyway. */\n-#include \"librist/logging.h\"\n-void rist_log_priv3(enum rist_log_level level, const char *format, ...)\n-{\n-\t(void)level;\n-\t(void)format;\n-}\n-\n static void hexstr_to_uint(const char *hexstr, uint8_t *buf, size_t buf_len) {\n \tfor (size_t i = 0, j = 0; j < buf_len; i += 2, j++)\n \t\tbuf[j] = (hexstr[i] % 32 + 9) % 25 * 16 + (hexstr[i + 1] % 32 + 9) % 25;\n@@ -77,30 +61,13 @@ struct srp_test_state {\n \tstruct librist_crypto_srp_client_ctx *correct_hash_client;\n };\n \n-/* The deterministic test exchange uses the 2048-bit RFC 5054 group\n- * (LIBRIST_SRP_NG_DEFAULT, also the only enum-selectable group since\n- * librist 0.2.15 dropped the sub-1024-bit groups for security).  The\n- * full hex literal is inlined here so the test can pass it through the\n- * (N_string, g_string) verifier-create and authenticator-create APIs;\n- * client_ctx_create uses default_ng=true to exercise the enum path. */\n-static const char SRP_TEST_N_2048[] =\n-\t\"AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4\"\n-\t\"A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF60\"\n-\t\"95179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF\"\n-\t\"747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B907\"\n-\t\"8717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB37861\"\n-\t\"60279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DB\"\n-\t\"FBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73\";\n-static const char SRP_TEST_G_2048[] = \"2\";\n-\n static int srp_test_state_setup(void **state) {\n \t*state = calloc(sizeof(struct srp_test_state), 1);\n \tstruct srp_test_state *s = *state;\n-\ts->n = SRP_TEST_N_2048;\n-\ts->g = SRP_TEST_G_2048;\n+\tlibrist_get_ng_constants(LIBRIST_SRP_NG_512, &s->n, &s->g);\n #if HAVE_MBEDTLS\n \tlibrist_crypto_srp_create_verifier(s->n, s->g, \"rist\", \"mainprofile\", &s->salt, &s->salt_len, &s->incorrect_hash_verifier, &s->verifier_len, false);\n-\ts->wrong_hash_authenticator = librist_crypto_srp_authenticator_ctx_create(s->n, s->g, s->incorrect_hash_verifier, s->verifier_len, s->salt, s->salt_len, false, false);\n+\ts->wrong_hash_authenticator = librist_crypto_srp_authenticator_ctx_create(s->n, s->g, s->incorrect_hash_verifier, s->verifier_len, s->salt, s->salt_len, false);\n \tsize_t v_len = s->verifier_len;\n \tfree(s->salt);\n \ts->salt = NULL;\n@@ -109,15 +76,17 @@ static int srp_test_state_setup(void **state) {\n #if HAVE_MBEDTLS\n \tassert(v_len == s->verifier_len);\n #endif\n-\ts->correct_hash_authenticator = librist_crypto_srp_authenticator_ctx_create(s->n, s->g, s->correct_hash_verifier, s->verifier_len, s->salt, s->salt_len, true, false);\n+\ts->correct_hash_authenticator = librist_crypto_srp_authenticator_ctx_create(s->n, s->g, s->correct_hash_verifier, s->verifier_len, s->salt, s->salt_len, true);\n+\n \n-\t/* Client uses default_ng=true (NG_DEFAULT = 2048-bit) which matches\n-\t * the authenticator's inlined SRP_TEST_N_2048 above.  Passing custom\n-\t * sub-1024-bit N/g would (correctly) be rejected by audit3 L3. */\n+    const char N_hex[] = \"D66AAFE8E245F9AC245A199F62CE61AB8FA90A4D80C71CD2ADFD0B9DA163B29F2A34AFBDB3B1B5D0102559CE63D8B6E86B0AA59C14E79D4AA62D1748E4249DF3\";\n+\tuint8_t N[(sizeof(N_hex) -1)/2];\n+\thexstr_to_uint(N_hex, N, sizeof(N));\n+\tuint8_t g[1] = {0x02};\n #if HAVE_MBEDTLS\n-\ts->wrong_hash_client = librist_crypto_srp_client_ctx_create(true, NULL, 0, NULL, 0, s->salt, s->salt_len, false, false);\n+\ts->wrong_hash_client = librist_crypto_srp_client_ctx_create(false, N, sizeof(N), g, sizeof(g), s->salt, s->salt_len, false);\n #endif\n-\ts->correct_hash_client = librist_crypto_srp_client_ctx_create(true, NULL, 0, NULL, 0, s->salt, s->salt_len, true, false);\n+\ts->correct_hash_client = librist_crypto_srp_client_ctx_create(false, N, sizeof(N), g, sizeof(g), s->salt, s->salt_len, true);\n \treturn 0;\n }\n \n@@ -176,13 +145,20 @@ static void test_get_default_ng(void **state) {\n \t\t\"FBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73\"\n \t);\n    \tassert_string_equal(g, \"2\");\n+\n+\t//This N,g pair is used in the VSF example flow.\n+\tn = NULL;\n+\tg = NULL;\n+\tassert_int_equal(librist_get_ng_constants(LIBRIST_SRP_NG_512, &n, &g), 0);\n+\tassert_string_equal(n, \"D66AAFE8E245F9AC245A199F62CE61AB8FA90A4D80C71CD2ADFD0B9DA163B29F2A34AFBDB3B1B5D0102559CE63D8B6E86B0AA59C14E79D4AA62D1748E4249DF3\");\n+\tassert_string_equal(g, \"2\");\n }\n \n #if HAVE_MBEDTLS\n static void test_srp_wrong_hashing_verifier_create(void **state) {\n \tstruct srp_test_state *s = *state;\n \tconst char sample_salt[] = \"72F9D5383B7EB7599FB63028F47475B60A55F313D40E0BE023E026C97C0A2C32\";\n-\tconst char sample_verifier[] = \"8A5F1D5A055C15889711054E7E3BA993D5A28773A345751BA24236AB35355B3536064A4D54D67FD41044CE543FCFD9DB99378ABD8AC5FA949BD2FC093A93E62CC501F5B70359254C9E895048CC884DF8FEA57233D985FD613EF5CA6D821B37D12C4836F4736EBBEDCFA7C944A906721355CB7ADE19756CF487807A9EDB527B78D4097D4B84EFF2F1CFE978B80F464A0002D6AE8058E37556E81154F889AE15EE1698E9F8F60281B3D1BBEDDD2EB28F5F83B9F88142E8204675F3D1F81E3FFD3149880C38299F975A757594162F70465E6BD0868A6C578319BA1262C2789B0E8CD13CF36AFC4332566EB4BCDC1FE1681B21B66D55879004DFC510FC4B33EEBB08\";\n+\tconst char sample_verifier[] = \"557EA208F87A23C28936423EC16ABE6BD959933DFBEFC0B36EBD9335DE3997C97DDFA081D64CFBC6EFBFD5BE19F2ED9F77922FD7E88BBA6C6B310A9018EC4305\";\n \n \tuint8_t *salt = NULL;\n \tsize_t salt_len = 0;\n@@ -206,10 +182,10 @@ static void test_srp_wrong_hashing_verifier_create(void **state) {\n \n static void test_srp_wrong_hashing_auth_ctx_create(void **state) {\n \tstruct srp_test_state *s = *state;\n-\tstruct librist_crypto_srp_authenticator_ctx * ctx = librist_crypto_srp_authenticator_ctx_create(s->n, s->g, s->incorrect_hash_verifier, s->verifier_len, s->salt, s->salt_len, false, false);\n-\tassert_true(ctx != NULL);\n+\tstruct librist_crypto_srp_authenticator_ctx * ctx = librist_crypto_srp_authenticator_ctx_create(s->n, s->g, s->incorrect_hash_verifier, s->verifier_len, s->salt, s->salt_len, false);\n+\tassert_non_null(ctx);\n \n-\tconst char well_known_n[] = \"AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF6095179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B9078717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB3786160279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DBFBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73\";\n+\tconst char well_known_n[] = \"D66AAFE8E245F9AC245A199F62CE61AB8FA90A4D80C71CD2ADFD0B9DA163B29F2A34AFBDB3B1B5D0102559CE63D8B6E86B0AA59C14E79D4AA62D1748E4249DF3\";\n \tuint8_t n[(sizeof(well_known_n) -1)/2];\n \tuint8_t g[1];\n \n@@ -230,12 +206,12 @@ static void test_srp_wrong_hashing_auth_ctx_create(void **state) {\n \n static void test_srp_wrong_hashing_auth_handle_A(void **state) {\n \tstruct srp_test_state *s = *state;\n-\tconst char client_A_hex[] = \"545DD89CD403BA71172016F156A537A2D369B8551004AB521CC62D76B71BD278E687294A3D265B96393A582D8823E4BB3A7960F641D7A01DD7E13C982F06B0522EC147B1451C63F099FD08A9D5A6FD5CA73907B13E0672DEFAEF976BEA78E8F4C3E60E85B86FE68F84658D3A792D90F2FB834E657C5F1E6AAA532A3D3F4F2D747D8F3D0C0CC8F999773ED4FFE159A8B8ACB2761C6C523C68BC866EE464091B6F86720EFFB02824AC1FB31675B7F07DD2292B937C9EDE73C2420A3204CA0BBD519274B5D35771019265BE5E213C9634540A0D56EA94BA306AD1965EFF986AF8963ECE5E30E057517A0D0082205E1086520039A03D60D739FCD7BB335CBB3AF39A\";\n+\tconst char client_A_hex[] = \"92C4CEFB95A1AE2E576A252B19273FD4613F44FDA4AC8CC84A089D5740756223943882BAD34CB55F35139CDDB60E0D19ACD2B884CFB27F53C8EA969269ABE014\";\n \tuint8_t client_A[(sizeof(client_A_hex) -1)/2];\n \thexstr_to_uint(client_A_hex, client_A, sizeof(client_A));\n \tassert_int_equal(librist_crypto_srp_authenticator_handle_A(s->wrong_hash_authenticator, client_A, sizeof(client_A)), 0);\n \n-\tconst char expected_B[] = \"0C455F4A7D081DF6E3AFA0E41D121D6BAE8A28C40ABFB7898E24417C18EE6279D3A8D87A64B0B05D5B9EBB304BDEA075478052F9ED69F49AF56E5F7B321DA414289FB66B62CF7D109386AFB19B1B80C49A63B14B7F8E5649D3E6AA5C09C91BAAD31A049A719975D40F05C6B11A8E64E6CA4C0FE9D8CF45079117C78649AB058CFC36746051E6792FF143851DF3F370B56F3F3630E47B39AE42987B294762214BE29B90B9E9B980CFB5C86151DE43D1D27ECE270CE2D9FD8AB688F50311D2A786D4E339BDFE12DB544A1F782CF2ECB1B3236BDFC40152CEAB9E54851CBEDB7B1E21F068D7D9627C2A94EF91F255269993F194FC1FB07C1E466AE852B18C7A5FED\";\n+\tconst char expected_B[] = \"85CAE0C578E6927B78BEB173FB0F9BFC8ECB4C13542BB8BE3B0F3447B3764A234177E22D180DCAD21F33302248B7452916DC58ABD309C8A77440A228B8516A4E\";\n \n \tuint8_t B[(sizeof(expected_B) -1)/2];\n \tassert_int_equal(librist_crypto_srp_authenticator_write_B_bytes(s->wrong_hash_authenticator, B, sizeof(B)), sizeof(B));\n@@ -247,13 +223,13 @@ static void test_srp_wrong_hashing_auth_handle_A(void **state) {\n \n static void test_srp_wrong_hashing_auth_verify_M1(void **state) {\n \tstruct srp_test_state *s = *state;\n-\tconst char client_M1_hex[] = \"75C3E739333518C4F8C6636D1F3D54663C4CBDB6FEDC49CD82354BC145C79040\";\n+\tconst char client_M1_hex[] = \"EBFC2D79BEB3CBF7BA83C27E2B51524F8CD3F3B2C4804815AD2516D465DF80C9\";\n \tuint8_t client_M1[(sizeof(client_M1_hex) -1)/2];\n \thexstr_to_uint(client_M1_hex, client_M1, sizeof(client_M1));\n \n \tassert_int_equal(librist_crypto_srp_authenticator_verify_m1(s->wrong_hash_authenticator, \"rist\", client_M1), 0);\n \n-\tconst char expected_m2[] = \"D20842186401BE2179E01C49130B6113CD0A89E28EE4B363529C60477E32AA4A\";\n+\tconst char expected_m2[] = \"FB14D73B5ACBBA101E5A799F80EBCBB43D83890E23DED979110EEFF109C0441A\";\n \tchar m2_hex[sizeof(expected_m2)];\n \tuint8_t m2[SHA256_DIGEST_LENGTH];\n \n@@ -273,24 +249,23 @@ static void test_srp_client_ctx_create(void **state) {\n \tuint8_t salt[(sizeof(salt_hex) -1)/2];\n \thexstr_to_uint(salt_hex, salt, sizeof(salt));\n \n-\t/* Default-NG path. */\n-\tstruct librist_crypto_srp_client_ctx *ctx = librist_crypto_srp_client_ctx_create(true, NULL, 0, NULL, 0, salt, sizeof(salt), true, false);\n-\tassert_true(ctx != NULL);\n-\n-\t/* librist_srp_client_write_A_bytes returns a byte string of length\n-\t * mbedtls_mpi_size(N) — 256 bytes for NG_DEFAULT (2048 bits). */\n-\tuint8_t A[256];\n-\tassert_int_equal(librist_crypto_srp_client_write_A_bytes(ctx, A, sizeof(A)), (ssize_t)sizeof(A));\n+\tstruct librist_crypto_srp_client_ctx *ctx = librist_crypto_srp_client_ctx_create(true, NULL, 0, NULL, 0, salt, sizeof(salt), true);\n+\tassert_non_null(ctx);\n \tlibrist_crypto_srp_client_ctx_free(ctx);\n \n-\t/* Custom-N path: anything below the 1024-bit floor is rightly\n-\t * rejected by audit3 L3.  Use NG_2048 hex literal. */\n-\tuint8_t N[256];\n-\thexstr_to_uint(SRP_TEST_N_2048, N, sizeof(N));\n+    const char N_hex[] = \"D66AAFE8E245F9AC245A199F62CE61AB8FA90A4D80C71CD2ADFD0B9DA163B29F2A34AFBDB3B1B5D0102559CE63D8B6E86B0AA59C14E79D4AA62D1748E4249DF3\";\n+\tuint8_t N[(sizeof(N_hex) -1)/2];\n+\thexstr_to_uint(N_hex, N, sizeof(N));\n \tuint8_t g[1] = {0x02};\n-\tctx = librist_crypto_srp_client_ctx_create(false, N, sizeof(N), g, sizeof(g), salt, sizeof(salt), true, false);\n-\tassert_true(ctx != NULL);\n-\tassert_int_equal(librist_crypto_srp_client_write_A_bytes(ctx, A, sizeof(A)), (ssize_t)sizeof(A));\n+\tctx = librist_crypto_srp_client_ctx_create(false, N, sizeof(N), g, sizeof(g), salt, sizeof(salt), true);\n+\n+\tconst char expected_A[] = \"92C4CEFB95A1AE2E576A252B19273FD4613F44FDA4AC8CC84A089D5740756223943882BAD34CB55F35139CDDB60E0D19ACD2B884CFB27F53C8EA969269ABE014\";\n+\tuint8_t A[(sizeof(expected_A)-1)/2];\n+\tassert_int_equal(librist_crypto_srp_client_write_A_bytes(ctx, A, sizeof(A)), sizeof(A));\n+\n+\tchar a_hex[sizeof(expected_A)];\n+\tuint_to_hex(A, sizeof(A), a_hex);\n+\tassert_string_equal(a_hex, expected_A);\n \tlibrist_crypto_srp_client_ctx_free(ctx);\n }\n \n@@ -298,13 +273,13 @@ static void test_srp_client_ctx_create(void **state) {\n \n static void test_srp_wrong_hashing_client_handle_B(void **state) {\n \tstruct srp_test_state *ctx = *state;\n-\tconst char server_B[] = \"0C455F4A7D081DF6E3AFA0E41D121D6BAE8A28C40ABFB7898E24417C18EE6279D3A8D87A64B0B05D5B9EBB304BDEA075478052F9ED69F49AF56E5F7B321DA414289FB66B62CF7D109386AFB19B1B80C49A63B14B7F8E5649D3E6AA5C09C91BAAD31A049A719975D40F05C6B11A8E64E6CA4C0FE9D8CF45079117C78649AB058CFC36746051E6792FF143851DF3F370B56F3F3630E47B39AE42987B294762214BE29B90B9E9B980CFB5C86151DE43D1D27ECE270CE2D9FD8AB688F50311D2A786D4E339BDFE12DB544A1F782CF2ECB1B3236BDFC40152CEAB9E54851CBEDB7B1E21F068D7D9627C2A94EF91F255269993F194FC1FB07C1E466AE852B18C7A5FED\";\n+\tconst char server_B[] = \"85CAE0C578E6927B78BEB173FB0F9BFC8ECB4C13542BB8BE3B0F3447B3764A234177E22D180DCAD21F33302248B7452916DC58ABD309C8A77440A228B8516A4E\";\n \tuint8_t B[(sizeof(server_B) -1)/2];\n \thexstr_to_uint(server_B, B, sizeof(B));\n \n \tassert_int_equal(librist_crypto_srp_client_handle_B(ctx->wrong_hash_client, B, sizeof(B), \"rist\", \"mainprofile\"), 0);\n \n-\tconst char expected_M1[] = \"75C3E739333518C4F8C6636D1F3D54663C4CBDB6FEDC49CD82354BC145C79040\";\n+\tconst char expected_M1[] = \"EBFC2D79BEB3CBF7BA83C27E2B51524F8CD3F3B2C4804815AD2516D465DF80C9\";\n \tuint8_t m1[SHA256_DIGEST_LENGTH];\n \n \tlibrist_crypto_srp_client_write_M1_bytes(ctx->wrong_hash_client, m1);\n@@ -316,7 +291,7 @@ static void test_srp_wrong_hashing_client_handle_B(void **state) {\n \n static void test_srp_wrong_hashing_client_verify_M2(void **state) {\n \tstruct srp_test_state *ctx = *state;\n-\tconst char server_M2[] = \"D20842186401BE2179E01C49130B6113CD0A89E28EE4B363529C60477E32AA4A\";\n+\tconst char server_M2[] = \"FB14D73B5ACBBA101E5A799F80EBCBB43D83890E23DED979110EEFF109C0441A\";\n \tuint8_t M2[(sizeof(server_M2)-1)/2];\n \thexstr_to_uint(server_M2, M2, sizeof(M2));\n \tassert_int_equal(librist_crypto_srp_client_verify_m2(ctx->wrong_hash_client, M2), 0);\n@@ -327,7 +302,7 @@ static void test_srp_wrong_hashing_client_verify_M2(void **state) {\n static void test_srp_correct_hashing_verifier_create(void **state) {\n \tstruct srp_test_state *s = *state;\n \tconst char sample_salt[] = \"72F9D5383B7EB7599FB63028F47475B60A55F313D40E0BE023E026C97C0A2C32\";\n-\tconst char sample_verifier[] = \"16B380409C1D6A43A96B42DD0FAC130D54A1932205F51F26AC13FB5332331C7B66A313ED969E24CB2AC5447C04FFC6565BC9FEA75A79D865FF7BB0DD65C62065EAAE7A27048F3B4C1FC0502C622FFE5B196400AD9470DB9F9DFB55CC4710081FDAEE3B63B69C15D43E189EF3E6E1C1FB1A9268F8E6DCDF16E1726585B883960EE09B318D3DD9E1C93D1B3EC98C148C00927028C1ED14D342B72811B962C233B71096BDD2EE505539DDC04ED03FDAA69926417E86016406480F8EB41317FF3D5E3B4735C76BCE67333B1F1E5E6A467E7E45A70D66EE1FC474A179697C5690AC1A525D2ADD050CC9D9824232AEC6FD8206CBEA5144AA2AC31B9865CEACF3BA2A72\";\n+\tconst char sample_verifier[] = \"2E06FEA163D6E9FF0FA7ED6C59233389D0DBA0C08C0F72F6DAD1E2A3D8B92A772F070439D1C11B87FA990D2DAF04EB830CC77D61ACC4B253297379CD8E6DC3AF\";\n \n \tuint8_t *salt = NULL;\n \tsize_t salt_len = 0;\n@@ -351,10 +326,10 @@ static void test_srp_correct_hashing_verifier_create(void **state) {\n \n static void test_srp_correct_hashing_auth_ctx_create(void **state) {\n \tstruct srp_test_state *s = *state;\n-\tstruct librist_crypto_srp_authenticator_ctx * ctx = librist_crypto_srp_authenticator_ctx_create(s->n, s->g, s->correct_hash_verifier, s->verifier_len, s->salt, s->salt_len, true, false);\n-\tassert_true(ctx != NULL);\n+\tstruct librist_crypto_srp_authenticator_ctx * ctx = librist_crypto_srp_authenticator_ctx_create(s->n, s->g, s->correct_hash_verifier, s->verifier_len, s->salt, s->salt_len, true);\n+\tassert_non_null(ctx);\n \n-\tconst char well_known_n[] = \"AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF6095179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B9078717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB3786160279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DBFBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73\";\n+\tconst char well_known_n[] = \"D66AAFE8E245F9AC245A199F62CE61AB8FA90A4D80C71CD2ADFD0B9DA163B29F2A34AFBDB3B1B5D0102559CE63D8B6E86B0AA59C14E79D4AA62D1748E4249DF3\";\n \tuint8_t n[(sizeof(well_known_n) -1)/2];\n \tuint8_t g[1];\n \n@@ -375,12 +350,12 @@ static void test_srp_correct_hashing_auth_ctx_create(void **state) {\n \n static void test_srp_correct_hashing_auth_handle_A(void **state) {\n \tstruct srp_test_state *s = *state;\n-\tconst char client_A_hex[] = \"545DD89CD403BA71172016F156A537A2D369B8551004AB521CC62D76B71BD278E687294A3D265B96393A582D8823E4BB3A7960F641D7A01DD7E13C982F06B0522EC147B1451C63F099FD08A9D5A6FD5CA73907B13E0672DEFAEF976BEA78E8F4C3E60E85B86FE68F84658D3A792D90F2FB834E657C5F1E6AAA532A3D3F4F2D747D8F3D0C0CC8F999773ED4FFE159A8B8ACB2761C6C523C68BC866EE464091B6F86720EFFB02824AC1FB31675B7F07DD2292B937C9EDE73C2420A3204CA0BBD519274B5D35771019265BE5E213C9634540A0D56EA94BA306AD1965EFF986AF8963ECE5E30E057517A0D0082205E1086520039A03D60D739FCD7BB335CBB3AF39A\";\n+\tconst char client_A_hex[] = \"92C4CEFB95A1AE2E576A252B19273FD4613F44FDA4AC8CC84A089D5740756223943882BAD34CB55F35139CDDB60E0D19ACD2B884CFB27F53C8EA969269ABE014\";\n \tuint8_t client_A[(sizeof(client_A_hex) -1)/2];\n \thexstr_to_uint(client_A_hex, client_A, sizeof(client_A));\n \tassert_int_equal(librist_crypto_srp_authenticator_handle_A(s->correct_hash_authenticator, client_A, sizeof(client_A)), 0);\n \n-\tconst char expected_B[] = \"461F82DB9BBD64DD580800C38B854437F0AE29CA14B0AD4A03797CA4EB6A27CD3C1B90E06E1C539A5FFE61E905497E78E8433F5303BEC8ECB23008DA86EBFB1B1B2FED35129BBC2ED346A810CC2A0AB20E44E2B94E048C9F9A17ABD87651CD1F2642873E487E0DDB3987D68F1B831CA8598AB88B377FAA7B06DCFE0E83A6D97FFB50D429285518209A4AEFA66F5A2BA499918209362CF0907EDC9E265156FCB8A945027F4DCDE178B8169D796187B79AA133E3BE02AF81C6AEC0B675D5F9E25E78CE00D5A0FE3BADC7106A2DAFB078BF30EF8677DD4D1EE60B50B110446C576CDDA3FA930C837938FE4AC4CF2F28185A2DD87F9524F1D5746E93D9A8FFF53626\";\n+\tconst char expected_B[] = \"858CDC811B5EEAA7F58C12767D309EBD2DF1D46F59EF5686052E6511CF853CA4E66910BDBD28CBEAE2F2DEE7F6BF3756757BD69E88D48C77B5371A82EF52AD84\";\n \n \tuint8_t B[(sizeof(expected_B) -1)/2];\n \tassert_int_equal(librist_crypto_srp_authenticator_write_B_bytes(s->correct_hash_authenticator, B, sizeof(B)), sizeof(B));\n@@ -392,13 +367,13 @@ static void test_srp_correct_hashing_auth_handle_A(void **state) {\n \n static void test_srp_correct_hashing_auth_verify_M1(void **state) {\n \tstruct srp_test_state *s = *state;\n-\tconst char client_M1_hex[] = \"2EE41138D2C447E7469EB589B89CF96FAF869B55DD684897DAB173056F1D8F90\";\n+\tconst char client_M1_hex[] = \"E28147C801BAB9C37647C1FF4A29FA720E3F5676434FB85EA9A752CC1F9B1AD4\";\n \tuint8_t client_M1[(sizeof(client_M1_hex) -1)/2];\n \thexstr_to_uint(client_M1_hex, client_M1, sizeof(client_M1));\n \n \tassert_int_equal(librist_crypto_srp_authenticator_verify_m1(s->correct_hash_authenticator, \"rist\", client_M1), 0);\n \n-\tconst char expected_m2[] = \"28E0412112CD83DDC97B3395AB0D27F5C0A1EB4FA89205CD505957F53988A639\";\n+\tconst char expected_m2[] = \"84F19797916FBDCAB1321CA78B575B145B586150248AFAA156361B8BCB139B32\";\n \tchar m2_hex[sizeof(expected_m2)];\n \tuint8_t m2[SHA256_DIGEST_LENGTH];\n \n@@ -411,13 +386,13 @@ static void test_srp_correct_hashing_auth_verify_M1(void **state) {\n \n static void test_srp_correct_hashing_client_handle_B(void **state) {\n \tstruct srp_test_state *ctx = *state;\n-\tconst char server_B[] = \"461F82DB9BBD64DD580800C38B854437F0AE29CA14B0AD4A03797CA4EB6A27CD3C1B90E06E1C539A5FFE61E905497E78E8433F5303BEC8ECB23008DA86EBFB1B1B2FED35129BBC2ED346A810CC2A0AB20E44E2B94E048C9F9A17ABD87651CD1F2642873E487E0DDB3987D68F1B831CA8598AB88B377FAA7B06DCFE0E83A6D97FFB50D429285518209A4AEFA66F5A2BA499918209362CF0907EDC9E265156FCB8A945027F4DCDE178B8169D796187B79AA133E3BE02AF81C6AEC0B675D5F9E25E78CE00D5A0FE3BADC7106A2DAFB078BF30EF8677DD4D1EE60B50B110446C576CDDA3FA930C837938FE4AC4CF2F28185A2DD87F9524F1D5746E93D9A8FFF53626\";\n+\tconst char server_B[] = \"858CDC811B5EEAA7F58C12767D309EBD2DF1D46F59EF5686052E6511CF853CA4E66910BDBD28CBEAE2F2DEE7F6BF3756757BD69E88D48C77B5371A82EF52AD84\";\n \tuint8_t B[(sizeof(server_B) -1)/2];\n \thexstr_to_uint(server_B, B, sizeof(B));\n \n \tassert_int_equal(librist_crypto_srp_client_handle_B(ctx->correct_hash_client, B, sizeof(B), \"rist\", \"mainprofile\"), 0);\n \n-\tconst char expected_M1[] = \"2EE41138D2C447E7469EB589B89CF96FAF869B55DD684897DAB173056F1D8F90\";\n+\tconst char expected_M1[] = \"E28147C801BAB9C37647C1FF4A29FA720E3F5676434FB85EA9A752CC1F9B1AD4\";\n \tuint8_t m1[SHA256_DIGEST_LENGTH];\n \n \tlibrist_crypto_srp_client_write_M1_bytes(ctx->correct_hash_client, m1);\n@@ -429,96 +404,12 @@ static void test_srp_correct_hashing_client_handle_B(void **state) {\n \n static void test_srp_correct_hashing_client_verify_M2(void **state) {\n \tstruct srp_test_state *ctx = *state;\n-\tconst char server_M2[] = \"28E0412112CD83DDC97B3395AB0D27F5C0A1EB4FA89205CD505957F53988A639\";\n+\tconst char server_M2[] = \"84F19797916FBDCAB1321CA78B575B145B586150248AFAA156361B8BCB139B32\";\n \tuint8_t M2[(sizeof(server_M2)-1)/2];\n \thexstr_to_uint(server_M2, M2, sizeof(M2));\n \tassert_int_equal(librist_crypto_srp_client_verify_m2(ctx->correct_hash_client, M2), 0);\n }\n \n-/* Fresh authenticator+client pair using only the public SRP API.  These\n- * tests do not depend on the deterministic fixture above (a/b are random\n- * each run) and exercise the srp-compat=1 plumbing end-to-end.\n- *\n- * Four exchanges on the 2048-bit NG_DEFAULT group:\n- *   1. Both PAD (default)               → handshake succeeds (verify_m1 == 0)\n- *   2. Both LEGACY (unpadded)           → handshake succeeds (verify_m1 == 0)\n- *   3. Authenticator PAD, client LEGACY → handshake fails (verify_m1 != 0)\n- *   4. Authenticator LEGACY, client PAD → handshake fails (verify_m1 != 0)\n- *\n- * Cases 3 and 4 are the \"operator forgot to set srp-compat=1 on one side\"\n- * scenarios.  They MUST fail at M1 (otherwise the legacy-bypass would be\n- * leaking through), but we do not attempt to detect the cross-mode case\n- * cryptographically — that is impossible without changing the wire\n- * protocol because the SRP-6a identity (Av^u)^b = (B - kg^x)^(a+ux)\n- * only holds when both sides use the same k. */\n-static int srp_run_exchange(bool auth_legacy, bool client_legacy) {\n-\tconst char *username = \"rist\";\n-\tconst char *password = \"mainprofile\";\n-\tconst char *n = NULL;\n-\tconst char *g = NULL;\n-\tif (librist_get_ng_constants(LIBRIST_SRP_NG_DEFAULT, &n, &g) != 0)\n-\t\treturn -100;\n-\n-\tuint8_t *salt = NULL;\n-\tsize_t   salt_len = 0;\n-\tuint8_t *verifier = NULL;\n-\tsize_t   verifier_len = 0;\n-\tif (librist_crypto_srp_create_verifier(n, g, username, password, &salt, &salt_len, &verifier, &verifier_len, true) != 0)\n-\t\treturn -101;\n-\n-\tstruct librist_crypto_srp_authenticator_ctx *auth =\n-\t\tlibrist_crypto_srp_authenticator_ctx_create(n, g, verifier, verifier_len, salt, salt_len, true, auth_legacy);\n-\tstruct librist_crypto_srp_client_ctx *client =\n-\t\tlibrist_crypto_srp_client_ctx_create(true, NULL, 0, NULL, 0, salt, salt_len, true, client_legacy);\n-\tint ret = -102;\n-\tif (auth == NULL || client == NULL)\n-\t\tgoto out;\n-\n-\tuint8_t A[256];\n-\tint alen = librist_crypto_srp_client_write_A_bytes(client, A, sizeof(A));\n-\tif (alen != (int)sizeof(A))\n-\t\tgoto out;\n-\tif (librist_crypto_srp_authenticator_handle_A(auth, A, sizeof(A)) != 0)\n-\t\tgoto out;\n-\n-\tuint8_t B[256];\n-\tif (librist_crypto_srp_authenticator_write_B_bytes(auth, B, sizeof(B)) != (int)sizeof(B))\n-\t\tgoto out;\n-\tif (librist_crypto_srp_client_handle_B(client, B, sizeof(B), username, password) != 0)\n-\t\tgoto out;\n-\n-\tuint8_t M1[SHA256_DIGEST_LENGTH];\n-\tlibrist_crypto_srp_client_write_M1_bytes(client, M1);\n-\n-\t/* The value under test: 0 on a matching mode-pair, -2 on a\n-\t * mode-mismatch the diagnostic recognised, -1 on opaque failure. */\n-\tret = librist_crypto_srp_authenticator_verify_m1(auth, username, M1);\n-\n-out:\n-\tlibrist_crypto_srp_authenticator_ctx_free(auth);\n-\tlibrist_crypto_srp_client_ctx_free(client);\n-\tfree(verifier);\n-\tfree(salt);\n-\treturn ret;\n-}\n-\n-static void test_srp_compat_both_pad(void **state) {\n-\t(void)state;\n-\tassert_int_equal(srp_run_exchange(false, false), 0);\n-}\n-static void test_srp_compat_both_legacy(void **state) {\n-\t(void)state;\n-\tassert_int_equal(srp_run_exchange(true, true), 0);\n-}\n-static void test_srp_compat_mismatch_auth_pad_client_legacy(void **state) {\n-\t(void)state;\n-\tassert_int_not_equal(srp_run_exchange(false, true), 0);\n-}\n-static void test_srp_compat_mismatch_auth_legacy_client_pad(void **state) {\n-\t(void)state;\n-\tassert_int_not_equal(srp_run_exchange(true, false), 0);\n-}\n-\n int main(void) {\n \tconst struct CMUnitTest tests[] = {\n \t\tcmocka_unit_test(test_hash_func),\n@@ -541,10 +432,6 @@ int main(void) {\n #endif\n \t\tcmocka_unit_test(test_srp_correct_hashing_client_handle_B),\n \t\tcmocka_unit_test(test_srp_correct_hashing_client_verify_M2),\n-\t\tcmocka_unit_test(test_srp_compat_both_pad),\n-\t\tcmocka_unit_test(test_srp_compat_both_legacy),\n-\t\tcmocka_unit_test(test_srp_compat_mismatch_auth_pad_client_legacy),\n-\t\tcmocka_unit_test(test_srp_compat_mismatch_auth_legacy_client_pad),\n \t};\n \n     return cmocka_run_group_tests(tests, srp_test_state_setup, srp_test_state_teardown);\ndiff --git a/test/rist/unit/test_adv_roundtrip.c b/test/rist/unit/test_adv_roundtrip.c\ndeleted file mode 100644\nindex dd91562..0000000\n--- a/test/rist/unit/test_adv_roundtrip.c\n+++ /dev/null\n@@ -1,622 +0,0 @@\n-/*\n- * Copyright © 2024-2026 SipRadius LLC\n- * All rights reserved.\n- *\n- * SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Unit test: Advanced Profile (TR-06-3) header parse/build round-trip.\n- * Verifies that building a packet and parsing it back produces identical\n- * field values for all flag combinations.\n- */\n-\n-#include \"proto/adv.h\"\n-#include \"proto/rist_time.h\"\n-#include <lz4.h>\n-#include <stdio.h>\n-#include <stdlib.h>\n-#include <string.h>\n-#include <assert.h>\n-#include <inttypes.h>\n-\n-static int test_count = 0;\n-static int pass_count = 0;\n-\n-#define CHECK(cond, msg) do { \\\n-\ttest_count++; \\\n-\tif (!(cond)) { \\\n-\t\tfprintf(stderr, \"FAIL: %s (line %d)\\n\", msg, __LINE__); \\\n-\t\treturn 1; \\\n-\t} \\\n-\tpass_count++; \\\n-} while (0)\n-\n-static int test_basic_roundtrip(void)\n-{\n-\tuint8_t buf[2048];\n-\tconst uint8_t payload[] = \"Hello RIST Advanced Profile!\";\n-\tsize_t payload_len = sizeof(payload);\n-\n-\tstruct rist_adv_params params;\n-\tmemset(&params, 0, sizeof(params));\n-\tparams.seq = 0x12345678;\n-\tparams.timestamp = 1000000;\n-\tparams.ssrc = 0xAABBCC00; /* Even = protected */\n-\tparams.enc_type = RIST_ADV_TYPE_DIRECT;\n-\tparams.psk_mode = RIST_ADV_PSK_NONE;\n-\tparams.lpc_mode = RIST_ADV_LPC_NONE;\n-\tparams.first_frag = true;\n-\tparams.last_frag = true;\n-\tparams.expedite = false;\n-\tparams.retransmit = false;\n-\n-\tint total = rist_adv_build(buf, &params, payload, payload_len);\n-\tCHECK(total > 0, \"rist_adv_build should return positive length\");\n-\tCHECK((size_t)total == RIST_ADV_HEADER_MIN + payload_len,\n-\t      \"total should be header_min + payload\");\n-\n-\tstruct rist_adv_parsed parsed;\n-\tint rc = rist_adv_parse(buf, (size_t)total, &parsed);\n-\tCHECK(rc == 0, \"rist_adv_parse should succeed\");\n-\n-\tCHECK(parsed.seq == 0x12345678, \"seq round-trip\");\n-\tCHECK(parsed.timestamp == 1000000, \"timestamp round-trip\");\n-\tCHECK(parsed.ssrc == 0xAABBCC00, \"ssrc round-trip\");\n-\tCHECK(parsed.enc_type == RIST_ADV_TYPE_DIRECT, \"enc_type round-trip\");\n-\tCHECK(parsed.psk_mode == RIST_ADV_PSK_NONE, \"psk_mode round-trip\");\n-\tCHECK(parsed.lpc_mode == RIST_ADV_LPC_NONE, \"lpc_mode round-trip\");\n-\tCHECK(parsed.first_frag == true, \"first_frag round-trip\");\n-\tCHECK(parsed.last_frag == true, \"last_frag round-trip\");\n-\tCHECK(parsed.expedite == false, \"expedite round-trip\");\n-\tCHECK(parsed.retransmit == false, \"retransmit round-trip\");\n-\tCHECK(parsed.has_flow_id == false, \"no flow_id\");\n-\tCHECK(parsed.has_pfd == false, \"no pfd\");\n-\tCHECK(parsed.has_hdr_ext == false, \"no hdr_ext\");\n-\tCHECK(parsed.payload_len == payload_len, \"payload length\");\n-\tCHECK(memcmp(parsed.payload, payload, payload_len) == 0, \"payload data\");\n-\n-\treturn 0;\n-}\n-\n-static int test_flags_roundtrip(void)\n-{\n-\tuint8_t buf[2048];\n-\tconst uint8_t payload[] = {0x01, 0x02, 0x03};\n-\n-\tstruct rist_adv_params params;\n-\tmemset(&params, 0, sizeof(params));\n-\tparams.seq = 0xFFFF0001;\n-\tparams.timestamp = 999999;\n-\tparams.ssrc = 0x00000002; /* Even */\n-\tparams.enc_type = RIST_ADV_TYPE_CONTROL;\n-\tparams.psk_mode = RIST_ADV_PSK_NONE;\n-\tparams.lpc_mode = RIST_ADV_LPC_NONE;\n-\tparams.first_frag = true;\n-\tparams.last_frag = true;\n-\tparams.expedite = true;\n-\tparams.retransmit = true;\n-\n-\tint total = rist_adv_build(buf, &params, payload, sizeof(payload));\n-\tCHECK(total > 0, \"build with E+R flags\");\n-\n-\tstruct rist_adv_parsed parsed;\n-\tCHECK(rist_adv_parse(buf, (size_t)total, &parsed) == 0, \"parse with E+R\");\n-\tCHECK(parsed.expedite == true, \"E flag round-trip\");\n-\tCHECK(parsed.retransmit == true, \"R flag round-trip\");\n-\tCHECK(parsed.enc_type == RIST_ADV_TYPE_CONTROL, \"control type round-trip\");\n-\tCHECK(parsed.seq == 0xFFFF0001, \"32-bit seq wrap round-trip\");\n-\n-\treturn 0;\n-}\n-\n-static int test_flow_id_roundtrip(void)\n-{\n-\tuint8_t buf[2048];\n-\tconst uint8_t payload[] = {0xAA};\n-\n-\tstruct rist_adv_flow_id fid;\n-\tfid.outer = htons(0x1234);\n-\tfid.inner_hi = 0xAB;\n-\tfid.inner_lo_sub = 0xC5; /* inner[3:0]=0xC, sub=0x5 */\n-\n-\tstruct rist_adv_params params;\n-\tmemset(&params, 0, sizeof(params));\n-\tparams.seq = 100;\n-\tparams.timestamp = 500;\n-\tparams.ssrc = 0x10;\n-\tparams.enc_type = RIST_ADV_TYPE_DIRECT;\n-\tparams.first_frag = true;\n-\tparams.last_frag = true;\n-\tparams.flow_id = &fid;\n-\n-\tint total = rist_adv_build(buf, &params, payload, sizeof(payload));\n-\tCHECK(total > 0, \"build with flow_id\");\n-\n-\tstruct rist_adv_parsed parsed;\n-\tCHECK(rist_adv_parse(buf, (size_t)total, &parsed) == 0, \"parse with flow_id\");\n-\tCHECK(parsed.has_flow_id == true, \"I flag present\");\n-\tCHECK(parsed.flow_id != NULL, \"flow_id pointer\");\n-\tCHECK(rist_adv_flow_outer(parsed.flow_id) == 0x1234, \"flow outer\");\n-\tCHECK(rist_adv_flow_inner(parsed.flow_id) == 0xABC, \"flow inner\");\n-\tCHECK(rist_adv_flow_sub(parsed.flow_id) == 0x5, \"flow sub\");\n-\n-\treturn 0;\n-}\n-\n-static int test_pfd_roundtrip(void)\n-{\n-\tuint8_t buf[2048];\n-\tconst uint8_t payload[] = {0x55};\n-\n-\tstruct rist_adv_pfd pfd;\n-\tpfd.value = htonl((uint32_t)(0x1 << 28) | 0x0ABCDEF); /* type=1, value=0x0ABCDEF */\n-\n-\tstruct rist_adv_params params;\n-\tmemset(&params, 0, sizeof(params));\n-\tparams.seq = 200;\n-\tparams.timestamp = 1000;\n-\tparams.ssrc = 0x20;\n-\tparams.enc_type = RIST_ADV_TYPE_DIRECT;\n-\tparams.first_frag = true;\n-\tparams.last_frag = true;\n-\tparams.pfd = &pfd;\n-\n-\tint total = rist_adv_build(buf, &params, payload, sizeof(payload));\n-\tCHECK(total > 0, \"build with pfd\");\n-\n-\tstruct rist_adv_parsed parsed;\n-\tCHECK(rist_adv_parse(buf, (size_t)total, &parsed) == 0, \"parse with pfd\");\n-\tCHECK(parsed.has_pfd == true, \"P flag present\");\n-\tCHECK(parsed.pfd != NULL, \"pfd pointer\");\n-\tCHECK(rist_adv_pfd_id_type(parsed.pfd) == 1, \"pfd id_type\");\n-\tCHECK(rist_adv_pfd_id_value(parsed.pfd) == 0x0ABCDEF, \"pfd id_value\");\n-\n-\treturn 0;\n-}\n-\n-static int test_all_psk_modes(void)\n-{\n-\tuint8_t buf[2048];\n-\tconst uint8_t payload[] = {0x42};\n-\n-\tfor (uint8_t psk = 0; psk <= 7; psk++) {\n-\t\tuint8_t hash[16], nonce[4], iv[4];\n-\t\tmemset(hash, psk, sizeof(hash));\n-\t\tmemset(nonce, psk + 0x10, sizeof(nonce));\n-\t\tmemset(iv, psk + 0x20, sizeof(iv));\n-\n-\t\tstruct rist_adv_params params;\n-\t\tmemset(&params, 0, sizeof(params));\n-\t\tparams.seq = 300 + psk;\n-\t\tparams.timestamp = 2000;\n-\t\tparams.ssrc = 0x30;\n-\t\tparams.enc_type = RIST_ADV_TYPE_DIRECT;\n-\t\tparams.psk_mode = psk;\n-\t\tparams.first_frag = true;\n-\t\tparams.last_frag = true;\n-\n-\t\tif (rist_adv_psk_has_hash(psk))  params.psk_hash = hash;\n-\t\tif (rist_adv_psk_has_nonce(psk)) params.psk_nonce = nonce;\n-\t\tif (rist_adv_psk_has_iv(psk))    params.psk_iv = iv;\n-\n-\t\tint total = rist_adv_build(buf, &params, payload, sizeof(payload));\n-\t\tCHECK(total > 0, \"build with psk mode\");\n-\n-\t\tstruct rist_adv_parsed parsed;\n-\t\tchar msg[64];\n-\t\tsnprintf(msg, sizeof(msg), \"parse psk_mode=%u\", psk);\n-\t\tCHECK(rist_adv_parse(buf, (size_t)total, &parsed) == 0, msg);\n-\n-\t\tsnprintf(msg, sizeof(msg), \"psk_mode=%u round-trip\", psk);\n-\t\tCHECK(parsed.psk_mode == psk, msg);\n-\n-\t\tif (rist_adv_psk_has_hash(psk)) {\n-\t\t\tsnprintf(msg, sizeof(msg), \"psk=%u hash present\", psk);\n-\t\t\tCHECK(parsed.psk_hash != NULL, msg);\n-\t\t\tCHECK(memcmp(parsed.psk_hash, hash, 16) == 0, msg);\n-\t\t}\n-\t\tif (rist_adv_psk_has_nonce(psk)) {\n-\t\t\tsnprintf(msg, sizeof(msg), \"psk=%u nonce present\", psk);\n-\t\t\tCHECK(parsed.psk_nonce != NULL, msg);\n-\t\t\tCHECK(memcmp(parsed.psk_nonce, nonce, 4) == 0, msg);\n-\t\t}\n-\t\tif (rist_adv_psk_has_iv(psk)) {\n-\t\t\tsnprintf(msg, sizeof(msg), \"psk=%u iv present\", psk);\n-\t\t\tCHECK(parsed.psk_iv != NULL, msg);\n-\t\t\tCHECK(memcmp(parsed.psk_iv, iv, 4) == 0, msg);\n-\t\t}\n-\t}\n-\n-\treturn 0;\n-}\n-\n-static int test_seq32_wraparound(void)\n-{\n-\tuint8_t buf[2048];\n-\tconst uint8_t payload[] = {0};\n-\tuint32_t test_seqs[] = {0, 1, 0xFFFF, 0x10000, 0xFFFFFFFE, 0xFFFFFFFF};\n-\n-\tfor (size_t i = 0; i < sizeof(test_seqs) / sizeof(test_seqs[0]); i++) {\n-\t\tstruct rist_adv_params params;\n-\t\tmemset(&params, 0, sizeof(params));\n-\t\tparams.seq = test_seqs[i];\n-\t\tparams.timestamp = 0;\n-\t\tparams.ssrc = 0x40;\n-\t\tparams.enc_type = RIST_ADV_TYPE_DIRECT;\n-\t\tparams.first_frag = true;\n-\t\tparams.last_frag = true;\n-\n-\t\tint total = rist_adv_build(buf, &params, payload, sizeof(payload));\n-\t\tCHECK(total > 0, \"build seq32\");\n-\n-\t\tstruct rist_adv_parsed parsed;\n-\t\tCHECK(rist_adv_parse(buf, (size_t)total, &parsed) == 0, \"parse seq32\");\n-\n-\t\tchar msg[64];\n-\t\tsnprintf(msg, sizeof(msg), \"seq32=0x%08X round-trip\", test_seqs[i]);\n-\t\tCHECK(parsed.seq == test_seqs[i], msg);\n-\t}\n-\n-\treturn 0;\n-}\n-\n-static int test_ssrc_parity(void)\n-{\n-\tCHECK(rist_adv_ssrc_is_protected(0x00000000) == true, \"even ssrc=0 is protected\");\n-\tCHECK(rist_adv_ssrc_is_protected(0x00000001) == false, \"odd ssrc=1 is unprotected\");\n-\tCHECK(rist_adv_ssrc_is_protected(0xFFFFFFFE) == true, \"even ssrc max is protected\");\n-\tCHECK(rist_adv_ssrc_is_protected(0xFFFFFFFF) == false, \"odd ssrc max is unprotected\");\n-\tCHECK(rist_adv_ssrc_protected(0x12345679) == 0x12345678, \"protected rounds down\");\n-\tCHECK(rist_adv_ssrc_unprotected(0x12345678) == 0x12345679, \"unprotected sets bit 0\");\n-\treturn 0;\n-}\n-\n-static int test_psk_hdr_sizes(void)\n-{\n-\tCHECK(rist_adv_psk_hdr_size(0) == 0, \"psk=0: 0 bytes\");\n-\tCHECK(rist_adv_psk_hdr_size(1) == 8, \"psk=1 (AES-CTR): nonce+iv = 8\");\n-\tCHECK(rist_adv_psk_hdr_size(2) == 20, \"psk=2 (HMAC): hash+nonce = 20\");\n-\tCHECK(rist_adv_psk_hdr_size(3) == 24, \"psk=3 (AES-CTR-HMAC): hash+nonce+iv = 24\");\n-\tCHECK(rist_adv_psk_hdr_size(4) == 24, \"psk=4 (AES-GCM): 24\");\n-\tCHECK(rist_adv_psk_hdr_size(5) == 24, \"psk=5 (CHACHA20): 24\");\n-\tCHECK(rist_adv_psk_hdr_size(6) == 8, \"psk=6 (user-no-hash): nonce+iv = 8\");\n-\tCHECK(rist_adv_psk_hdr_size(7) == 24, \"psk=7 (user-hash): hash+nonce+iv = 24\");\n-\treturn 0;\n-}\n-\n-static int test_malformed_packets(void)\n-{\n-\tstruct rist_adv_parsed parsed;\n-\n-\tCHECK(rist_adv_parse(NULL, 0, &parsed) == -1, \"null buf rejected\");\n-\n-\tuint8_t short_pkt[8] = {0};\n-\tCHECK(rist_adv_parse(short_pkt, sizeof(short_pkt), &parsed) == -1, \"short packet rejected\");\n-\n-\t/* Valid-looking RTP header but wrong version */\n-\tuint8_t bad_version[32];\n-\tmemset(bad_version, 0, sizeof(bad_version));\n-\tbad_version[0] = 0x40; /* V=1 instead of V=2 */\n-\tbad_version[1] = RIST_ADV_PT;\n-\tCHECK(rist_adv_parse(bad_version, sizeof(bad_version), &parsed) == -1, \"wrong RTP version rejected\");\n-\n-\treturn 0;\n-}\n-\n-/* NTP (1 s = 2^32, per timestampNTP_u64) to the Advanced Profile's 1 MHz RTP\n- * timestamp (TR-06-3 Section 5.2.1), exercising the production conversion in\n- * timestampRTP_u32(1, .). The 32-bit 1 MHz counter wraps at ~4295 s, so the\n- * test values stay within the non-wrapping range. */\n-static int test_timestamp_conversion(void)\n-{\n-\tuint64_t test_values[] = {\n-\t\t0,\n-\t\t1ULL << 32,             /* exactly 1 second */\n-\t\t(uint64_t)10 << 32,     /* 10 seconds */\n-\t\t(uint64_t)3600 << 32,   /* 1 hour */\n-\t\t1ULL << 31,             /* 0.5 seconds */\n-\t\t(uint64_t)4294 << 32,   /* ~4294 s, just under the uint32 wrap */\n-\t};\n-\n-\t/* Sub-1-MHz-tick NTP resolution is lost on the round-trip; one 1 MHz tick\n-\t * is 2^32/1e6 ~= 4295 NTP units, so allow up to two ticks of slack. */\n-\tuint64_t tol = (2ULL << 32) / RIST_ADV_CLOCK_HZ;\n-\n-\tfor (size_t i = 0; i < sizeof(test_values) / sizeof(test_values[0]); i++) {\n-\t\tuint64_t ntp_orig = test_values[i];\n-\t\tuint32_t ts_1mhz = timestampRTP_u32(1, ntp_orig);\n-\t\tuint64_t ntp_back = ((uint64_t)ts_1mhz << 32) / RIST_ADV_CLOCK_HZ;\n-\n-\t\tint64_t diff = (int64_t)ntp_orig - (int64_t)ntp_back;\n-\t\tif (diff < 0) diff = -diff;\n-\t\tchar msg[160];\n-\t\tsnprintf(msg, sizeof(msg), \"ts round-trip ntp=%\" PRIu64 \" -> 1mhz=%u -> ntp=%\" PRIu64 \" (diff=%\" PRId64 \")\",\n-\t\t\tntp_orig, ts_1mhz, ntp_back, diff);\n-\t\tCHECK((uint64_t)diff <= tol, msg);\n-\t}\n-\n-\t/* Exact conversions at the 1 MHz clock: whole and half seconds map to\n-\t * their microsecond counts, and zero maps to zero. */\n-\tCHECK(timestampRTP_u32(1, 1ULL << 32) == 1000000, \"1 second NTP -> 1000000 us\");\n-\tCHECK(timestampRTP_u32(1, 1ULL << 31) == 500000, \"0.5 second NTP -> 500000 us\");\n-\tCHECK(timestampRTP_u32(1, 0) == 0, \"0 NTP -> 0 us\");\n-\n-\treturn 0;\n-}\n-\n-/* Verify SSRC-based seq_index mapping: (uint16_t)(seq & 0xFFFF) must\n- * give correct low-16 bits for 32-bit seq values. */\n-static int test_seq_index_mapping(void)\n-{\n-\t/* seq_index is indexed by low 16 bits of the 32-bit seq */\n-\tuint32_t test_seqs[] = { 0, 1, 0xFFFF, 0x10000, 0x1FFFF, 0x20000, 0xFFFFFFFF };\n-\tuint16_t expected[]  = { 0, 1, 0xFFFF, 0x0000,  0xFFFF,  0x0000,  0xFFFF };\n-\n-\tfor (size_t i = 0; i < sizeof(test_seqs) / sizeof(test_seqs[0]); i++) {\n-\t\tuint16_t idx = (uint16_t)(test_seqs[i] & 0xFFFF);\n-\t\tchar msg[128];\n-\t\tsnprintf(msg, sizeof(msg), \"seq_index 0x%08x -> 0x%04x\", test_seqs[i], idx);\n-\t\tCHECK(idx == expected[i], msg);\n-\t}\n-\n-\treturn 0;\n-}\n-\n-static int test_type8_roundtrip(void)\n-{\n-\tprintf(\"--- test_type8_roundtrip ---\\n\");\n-\n-\t/* Simulate a GRE keepalive payload wrapped in Type 8 */\n-\tuint8_t fake_gre[] = { 0x00, 0x00, 0x08, 0x00, /* GRE flags + proto */\n-\t                        0xDE, 0xAD, 0xBE, 0xEF, /* some payload */\n-\t                        0xCA, 0xFE, 0xBA, 0xBE };\n-\tsize_t gre_len = sizeof(fake_gre);\n-\n-\tuint8_t pkt[512];\n-\tstruct rist_adv_params params;\n-\tmemset(&params, 0, sizeof(params));\n-\tparams.seq = 0x00010042;\n-\tparams.timestamp = 123456;\n-\tparams.ssrc = 0xAABBCC00;  /* even = Protected */\n-\tparams.enc_type = RIST_ADV_TYPE_GRE_MAIN;\n-\tparams.first_frag = true;\n-\tparams.last_frag = true;\n-\tparams.expedite = true;\n-\n-\tint total = rist_adv_build(pkt, &params, fake_gre, gre_len);\n-\tCHECK(total > 0, \"Type 8 build should succeed\");\n-\n-\tstruct rist_adv_parsed parsed;\n-\tint rc = rist_adv_parse(pkt, (size_t)total, &parsed);\n-\tCHECK(rc == 0, \"Type 8 parse should succeed\");\n-\tCHECK(parsed.enc_type == RIST_ADV_TYPE_GRE_MAIN, \"enc_type should be GRE_MAIN (8)\");\n-\tCHECK(parsed.seq == 0x00010042, \"seq should round-trip\");\n-\tCHECK(parsed.payload_len == gre_len, \"payload length should match GRE length\");\n-\tCHECK(memcmp(parsed.payload, fake_gre, gre_len) == 0, \"GRE payload should round-trip\");\n-\n-\treturn 0;\n-}\n-\n-static int test_seq32_index_full_range(void)\n-{\n-\tprintf(\"--- test_seq32_index_full_range ---\\n\");\n-\n-\t/* Verify that 32-bit seqs map to unique slots within a power-of-2 queue.\n-\t * queue_max = 524288 = RIST_SERVER_QUEUE_BUFFERS.\n-\t * Two seqs that differ only in the high 16 bits must map to different slots. */\n-\tuint32_t queue_max = 524288; /* RIST_SERVER_QUEUE_BUFFERS */\n-\tuint32_t mask = queue_max - 1;\n-\n-\tuint32_t seq_a = 0x00000042;\n-\tuint32_t seq_b = 0x00010042; /* differs in bit 16 */\n-\tuint32_t seq_c = 0x00040042; /* differs in bit 18 — within queue_max range */\n-\n-\tuint32_t idx_a = seq_a & mask;\n-\tuint32_t idx_b = seq_b & mask;\n-\tuint32_t idx_c = seq_c & mask;\n-\n-\tCHECK(idx_a != idx_b, \"seq_a and seq_b must map to different slots with 32-bit index\");\n-\tCHECK(idx_a != idx_c, \"seq_a and seq_c must map to different slots\");\n-\tCHECK(idx_b != idx_c, \"seq_b and seq_c must map to different slots\");\n-\n-\t/* Verify the same seqs WOULD collide with 16-bit truncation */\n-\tCHECK(((uint16_t)seq_a) == ((uint16_t)seq_b), \"seq_a and seq_b collide in 16-bit\");\n-\tCHECK(((uint16_t)seq_a) == ((uint16_t)seq_c), \"seq_a and seq_c collide in 16-bit\");\n-\n-\t/* Seqs separated by exactly queue_max map to the same slot (correct circular wrap) */\n-\tuint32_t seq_wrap = seq_a + queue_max;\n-\tCHECK((seq_wrap & mask) == idx_a, \"seq separated by queue_max should wrap to same slot\");\n-\n-\t/* Verify wraparound: seq & mask stays within bounds */\n-\tuint32_t seq_max = 0xFFFFFFFF;\n-\tCHECK((seq_max & mask) < queue_max, \"max seq should be within queue bounds\");\n-\tCHECK((0 & mask) == 0, \"seq 0 should map to slot 0\");\n-\n-\treturn 0;\n-}\n-\n-static int test_flow_id_virt_port_mapping(void)\n-{\n-\tprintf(\"--- test_flow_id_virt_port_mapping ---\\n\");\n-\n-\tuint8_t buf[2048];\n-\tconst uint8_t payload[] = {0xBB};\n-\n-\tuint16_t dst_port = 5000;\n-\tuint16_t src_port = 1971;\n-\n-\tstruct rist_adv_flow_id fid;\n-\tfid.outer = htons(dst_port);\n-\tfid.inner_hi = (uint8_t)((src_port >> 4) & 0xFF);\n-\tfid.inner_lo_sub = (uint8_t)((src_port & 0x0F) << 4);\n-\n-\tstruct rist_adv_params params;\n-\tmemset(&params, 0, sizeof(params));\n-\tparams.seq = 42;\n-\tparams.timestamp = 100;\n-\tparams.ssrc = 0x50;\n-\tparams.enc_type = RIST_ADV_TYPE_DIRECT;\n-\tparams.first_frag = true;\n-\tparams.last_frag = true;\n-\tparams.flow_id = &fid;\n-\n-\tint total = rist_adv_build(buf, &params, payload, sizeof(payload));\n-\tCHECK(total > 0, \"build with flow_id for virt port\");\n-\n-\tstruct rist_adv_parsed parsed;\n-\tCHECK(rist_adv_parse(buf, (size_t)total, &parsed) == 0, \"parse flow_id virt port\");\n-\tCHECK(parsed.has_flow_id, \"I flag set\");\n-\n-\tuint16_t recovered_dst = rist_adv_flow_outer(parsed.flow_id);\n-\tuint16_t recovered_src = rist_adv_flow_inner(parsed.flow_id);\n-\n-\tchar msg[128];\n-\tsnprintf(msg, sizeof(msg), \"dst_port round-trip: sent=%u got=%u\", dst_port, recovered_dst);\n-\tCHECK(recovered_dst == dst_port, msg);\n-\n-\tsnprintf(msg, sizeof(msg), \"src_port round-trip (12-bit): sent=%u got=%u\", src_port & 0xFFF, recovered_src);\n-\tCHECK(recovered_src == (src_port & 0xFFF), msg);\n-\n-\t/* Test with default RIST ports */\n-\tuint16_t dst2 = 1968;\n-\tuint16_t src2 = 32768 + 7; /* ephemeral-range port */\n-\tstruct rist_adv_flow_id fid2;\n-\tfid2.outer = htons(dst2);\n-\tfid2.inner_hi = (uint8_t)((src2 >> 4) & 0xFF);\n-\tfid2.inner_lo_sub = (uint8_t)((src2 & 0x0F) << 4);\n-\n-\tparams.flow_id = &fid2;\n-\tparams.seq = 43;\n-\ttotal = rist_adv_build(buf, &params, payload, sizeof(payload));\n-\tCHECK(total > 0, \"build with default ports\");\n-\n-\tCHECK(rist_adv_parse(buf, (size_t)total, &parsed) == 0, \"parse default ports\");\n-\tCHECK(rist_adv_flow_outer(parsed.flow_id) == 1968, \"default dst_port 1968\");\n-\n-\t/* Inner is 12-bit, so only low 12 bits of 32775 survive */\n-\tCHECK(rist_adv_flow_inner(parsed.flow_id) == (src2 & 0xFFF), \"ephemeral src_port low 12 bits\");\n-\n-\treturn 0;\n-}\n-\n-static int test_lz4_header_roundtrip(void)\n-{\n-\tuint8_t payload[] = \"Hello, this is a test payload for LZ4 compression roundtrip \"\n-\t                    \"with enough data to make compression meaningful in the test.\";\n-\tuint8_t compressed[256];\n-\tuint8_t decompressed[256];\n-\n-\tint clen = LZ4_compress_default((const char *)payload, (char *)compressed,\n-\t                                (int)sizeof(payload), (int)sizeof(compressed));\n-\tCHECK(clen > 0, \"LZ4 compress succeeds\");\n-\n-\tint dlen = LZ4_decompress_safe((const char *)compressed, (char *)decompressed,\n-\t                               clen, (int)sizeof(decompressed));\n-\tCHECK(dlen == (int)sizeof(payload), \"LZ4 decompress returns original size\");\n-\tCHECK(memcmp(payload, decompressed, sizeof(payload)) == 0, \"LZ4 roundtrip data matches\");\n-\n-\tuint8_t buf[1024];\n-\tstruct rist_adv_params params;\n-\tmemset(&params, 0, sizeof(params));\n-\tparams.seq = 99;\n-\tparams.timestamp = 12345;\n-\tparams.ssrc = 0x1000;\n-\tparams.enc_type = RIST_ADV_TYPE_DIRECT;\n-\tparams.lpc_mode = RIST_ADV_LPC_LZ4;\n-\tparams.first_frag = true;\n-\tparams.last_frag = true;\n-\n-\tint total = rist_adv_build(buf, &params, compressed, (size_t)clen);\n-\tCHECK(total > 0, \"build LZ4 compressed packet\");\n-\n-\tstruct rist_adv_parsed parsed;\n-\tCHECK(rist_adv_parse(buf, (size_t)total, &parsed) == 0, \"parse LZ4 packet\");\n-\tCHECK(parsed.lpc_mode == RIST_ADV_LPC_LZ4, \"LPC mode is LZ4\");\n-\tCHECK(parsed.payload_len == (size_t)clen, \"compressed payload length preserved\");\n-\n-\tdlen = LZ4_decompress_safe((const char *)parsed.payload, (char *)decompressed,\n-\t                           (int)parsed.payload_len, (int)sizeof(decompressed));\n-\tCHECK(dlen == (int)sizeof(payload), \"decompressed from parsed packet\");\n-\tCHECK(memcmp(payload, decompressed, sizeof(payload)) == 0, \"data matches after parse+decompress\");\n-\n-\treturn 0;\n-}\n-\n-static int test_flow_attr_control_roundtrip(void)\n-{\n-\tprintf(\"--- test_flow_attr_control_roundtrip ---\\n\");\n-\n-\tuint8_t buf[2048];\n-\tconst char *json = \"{\\\"session\\\":\\\"test\\\",\\\"virt_dst_port\\\":5000,\\\"profile\\\":\\\"advanced\\\",\"\n-\t                   \"\\\"flow_id\\\":5000,\\\"flow_inner\\\":0}\";\n-\tsize_t json_len = strlen(json);\n-\n-\t/* Build a CI=0x8001 control body: CI(2) + Len(2) + JSON */\n-\tuint8_t ctrl[4 + 512];\n-\tsize_t off = 0;\n-\tctrl[off++] = (0x8001 >> 8) & 0xFF;\n-\tctrl[off++] = 0x8001 & 0xFF;\n-\tuint16_t body_len = (uint16_t)json_len;\n-\tctrl[off++] = (body_len >> 8) & 0xFF;\n-\tctrl[off++] = body_len & 0xFF;\n-\tmemcpy(&ctrl[off], json, json_len);\n-\toff += json_len;\n-\n-\t/* Wrap in an AP Type 4 (Control) packet */\n-\tstruct rist_adv_params params;\n-\tmemset(&params, 0, sizeof(params));\n-\tparams.seq = 777;\n-\tparams.timestamp = 54321;\n-\tparams.ssrc = 0x60 | 1; /* odd = unprotected */\n-\tparams.enc_type = RIST_ADV_TYPE_CONTROL;\n-\tparams.first_frag = true;\n-\tparams.last_frag = true;\n-\tparams.expedite = true;\n-\n-\tint total = rist_adv_build(buf, &params, ctrl, off);\n-\tCHECK(total > 0, \"build flow_attr control packet\");\n-\n-\t/* Parse the AP envelope */\n-\tstruct rist_adv_parsed parsed;\n-\tCHECK(rist_adv_parse(buf, (size_t)total, &parsed) == 0, \"parse flow_attr control\");\n-\tCHECK(parsed.enc_type == RIST_ADV_TYPE_CONTROL, \"enc_type should be CONTROL\");\n-\tCHECK(parsed.payload_len >= 4, \"control payload should be at least CI+Len\");\n-\n-\t/* Extract CI and body from the control payload */\n-\tconst uint8_t *cp = parsed.payload;\n-\tuint16_t ci = (uint16_t)((cp[0] << 8) | cp[1]);\n-\tuint16_t bl = (uint16_t)((cp[2] << 8) | cp[3]);\n-\tCHECK(ci == 0x8001, \"CI should be FLOW_ATTR\");\n-\tCHECK(bl == json_len, \"body length matches JSON length\");\n-\tCHECK(memcmp(cp + 4, json, json_len) == 0, \"JSON payload matches\");\n-\n-\treturn 0;\n-}\n-\n-int main(void)\n-{\n-\tint failures = 0;\n-\tfailures += test_basic_roundtrip();\n-\tfailures += test_flags_roundtrip();\n-\tfailures += test_flow_id_roundtrip();\n-\tfailures += test_pfd_roundtrip();\n-\tfailures += test_all_psk_modes();\n-\tfailures += test_seq32_wraparound();\n-\tfailures += test_ssrc_parity();\n-\tfailures += test_psk_hdr_sizes();\n-\tfailures += test_malformed_packets();\n-\tfailures += test_timestamp_conversion();\n-\tfailures += test_seq_index_mapping();\n-\tfailures += test_type8_roundtrip();\n-\tfailures += test_seq32_index_full_range();\n-\tfailures += test_flow_id_virt_port_mapping();\n-\tfailures += test_lz4_header_roundtrip();\n-\tfailures += test_flow_attr_control_roundtrip();\n-\n-\tprintf(\"Advanced Profile round-trip tests: %d/%d passed\\n\", pass_count, test_count);\n-\tif (failures) {\n-\t\tprintf(\"FAILED\\n\");\n-\t\treturn 1;\n-\t}\n-\tprintf(\"ALL PASSED\\n\");\n-\treturn 0;\n-}\ndiff --git a/test/rist/unit/test_adv_ts_reconstruct.c b/test/rist/unit/test_adv_ts_reconstruct.c\ndeleted file mode 100644\nindex 4bea728..0000000\n--- a/test/rist/unit/test_adv_ts_reconstruct.c\n+++ /dev/null\n@@ -1,228 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Unit test for rist_adv_ts_reconstruct() - the Advanced Profile receive-side\n- * source-clock reconstruction (src/rist-adv-ts.h). Verifies: immediate\n- * dejitter off the source clock (as the Main profile does), wrap-safe delta\n- * accumulation, retransmit positioning without state change, following a\n- * source discontinuity, and the first-window rate check that corrects a broken\n- * clock (an older 65536x-fast sender) back to arrival. Pure, link-free:\n- * exercises the header inline directly. */\n-\n-#include \"rist-adv-ts.h\"\n-\n-#include <inttypes.h>\n-#include <stdio.h>\n-\n-static int failures = 0;\n-\n-#define CHECK(cond, ...)                                                \\\n-    do {                                                                \\\n-        if (!(cond)) {                                                  \\\n-            fprintf(stderr, \"FAIL %s:%d: \" #cond, __FILE__, __LINE__);  \\\n-            fprintf(stderr, \" - \"); fprintf(stderr, __VA_ARGS__);       \\\n-            fprintf(stderr, \"\\n\");                                      \\\n-            failures++;                                                 \\\n-        }                                                               \\\n-    } while (0)\n-\n-/* NTP tick helpers (1 s = 2^32). All cast to uint64_t so printf args match\n- * PRIu64 exactly on every ABI. us matches the helper's exact 2^26/15625. */\n-#define NTP_SEC(s)  ((uint64_t)((uint64_t)(s) << 32))\n-#define NTP_MS(ms)  ((uint64_t)(((uint64_t)(ms) << 32) / 1000))\n-#define NTP_US(us)  ((uint64_t)(((uint64_t)(us) * 67108864ULL) / 15625ULL))\n-\n-static uint64_t absdiff(uint64_t a, uint64_t b) { return a > b ? a - b : b - a; }\n-\n-static const uint64_t WINDOW = NTP_SEC(1);   /* rate-verification window */\n-\n-/* Drive a conformant 1 MHz sender (wire us == elapsed us) through the window\n- * so the clock is verified and kept. Returns the seed time t0; afterwards\n- * last == 1000000 and accum ~= t0 + NTP_US(1000000). */\n-static uint64_t seed_and_verify(struct rist_adv_ts_state *st, uint64_t t0)\n-{\n-\tuint64_t out = rist_adv_ts_reconstruct(st, 0, t0, WINDOW, true);\n-\tCHECK(out == t0, \"first packet stamps arrival(seed): got %\" PRIu64, out);\n-\tfor (uint32_t ms = 200; ms < 1000; ms += 200) {\n-\t\tuint64_t now = t0 + NTP_MS(ms);\n-\t\tout = rist_adv_ts_reconstruct(st, ms * 1000u, now, WINDOW, true);\n-\t\tCHECK(absdiff(out, t0 + NTP_US(ms * 1000)) <= 3,\n-\t\t      \"calibrating packet uses source clock at %ums\", ms);\n-\t\tCHECK(st->calibrating, \"still calibrating before the window closes\");\n-\t}\n-\tuint64_t now = t0 + NTP_SEC(1);\n-\tout = rist_adv_ts_reconstruct(st, 1000000u, now, WINDOW, true);\n-\tCHECK(!st->calibrating, \"verdict taken at window close\");\n-\tCHECK(st->use_source, \"conformant clock kept\");\n-\tCHECK(absdiff(out, t0 + NTP_SEC(1)) <= 3, \"source ~= +1s: got %\" PRIu64, out);\n-\treturn t0;\n-}\n-\n-/* First live packet stamps arrival and anchors the clock. */\n-static void test_seed_is_arrival(void)\n-{\n-\tstruct rist_adv_ts_state st = {0};\n-\tuint64_t now = NTP_SEC(1000);\n-\tuint64_t out = rist_adv_ts_reconstruct(&st, 500000, now, WINDOW, true);\n-\tCHECK(out == now, \"first packet must stamp arrival: got %\" PRIu64, out);\n-\tCHECK(st.seeded && st.calibrating, \"clock anchored after first live packet\");\n-}\n-\n-/* A retransmit arriving before any live packet has no reference: arrival. */\n-static void test_retry_before_seed(void)\n-{\n-\tstruct rist_adv_ts_state st = {0};\n-\tuint64_t now = NTP_SEC(1000);\n-\tuint64_t out = rist_adv_ts_reconstruct(&st, 12345, now, WINDOW, false);\n-\tCHECK(out == now, \"retry before seed -> arrival: got %\" PRIu64, out);\n-\tCHECK(!st.seeded, \"retry must not anchor the clock\");\n-}\n-\n-/* From the very first packets - inside the verification window - output tracks\n- * the source clock, not arrival jitter, just like the Main profile. */\n-static void test_source_used_immediately(void)\n-{\n-\tstruct rist_adv_ts_state st = {0};\n-\tuint64_t t0 = NTP_SEC(1000);\n-\trist_adv_ts_reconstruct(&st, 0, t0, WINDOW, true); /* seed */\n-\n-\t/* Source +40 ms; arrival jittered to +55 ms. Still calibrating, but the\n-\t * output already follows the source clock. */\n-\tuint64_t out = rist_adv_ts_reconstruct(&st, 40000, t0 + NTP_MS(55),\n-\t\t\t\t\t       WINDOW, true);\n-\tCHECK(absdiff(out, t0 + NTP_US(40000)) <= 3,\n-\t      \"source used immediately: got %\" PRIu64 \" want ~%\" PRIu64,\n-\t      out, t0 + NTP_US(40000));\n-\tCHECK(st.calibrating, \"still within the verification window\");\n-}\n-\n-/* Once verified, in-order packets keep tracking the source clock. */\n-static void test_verified_tracks_source(void)\n-{\n-\tstruct rist_adv_ts_state st = {0};\n-\tuint64_t t0 = seed_and_verify(&st, NTP_SEC(1000));\n-\n-\t/* +40 ms of source; arrival jittered late to +55 ms. */\n-\tuint64_t out = rist_adv_ts_reconstruct(&st, 1040000u, t0 + NTP_MS(1055),\n-\t\t\t\t\t       WINDOW, true);\n-\tCHECK(absdiff(out, t0 + NTP_US(1040000)) <= 4,\n-\t      \"tracks source not arrival: got %\" PRIu64, out);\n-\n-\t/* +40 ms more; arrival jittered early to +30 ms. */\n-\tout = rist_adv_ts_reconstruct(&st, 1080000u, t0 + NTP_MS(1085),\n-\t\t\t\t      WINDOW, true);\n-\tCHECK(absdiff(out, t0 + NTP_US(1080000)) <= 5,\n-\t      \"stays on source clock: got %\" PRIu64, out);\n-}\n-\n-/* A broken sender (pre-fix 65536x-fast timestamp) is used optimistically at\n- * first, so early output races ahead of arrival; the first-window rate check\n- * then rejects it and the peer corrects to arrival for the whole session. */\n-static void test_broken_sender_corrected(void)\n-{\n-\tstruct rist_adv_ts_state st = {0};\n-\tuint64_t t0 = NTP_SEC(1000);\n-\trist_adv_ts_reconstruct(&st, 0, t0, WINDOW, true); /* seed */\n-\n-\t/* 20 ms spacing keeps each broken delta (20000*65536) under 2^31 so the\n-\t * signed delta stays positive; ~50 packets span the 1 s window. */\n-\tuint32_t wire = 0;\n-\tuint64_t now = t0;\n-\tfor (int i = 1; i <= 51; i++) {\n-\t\twire += 20000u * 65536u;\n-\t\tnow = t0 + NTP_MS(20 * i);\n-\t\tuint64_t out = rist_adv_ts_reconstruct(&st, wire, now, WINDOW, true);\n-\t\tif (i < 50) /* still calibrating: optimistic source races ahead */\n-\t\t\tCHECK(out > now + NTP_SEC(1),\n-\t\t\t      \"broken clock used optimistically before verdict: got %\" PRIu64,\n-\t\t\t      out);\n-\t}\n-\tCHECK(!st.calibrating, \"verdict taken\");\n-\tCHECK(!st.use_source, \"broken clock rejected\");\n-\n-\t/* Corrected for good: later packets and retries all use arrival. */\n-\tuint64_t later = t0 + NTP_SEC(5);\n-\tCHECK(rist_adv_ts_reconstruct(&st, wire + 50000u, later, WINDOW, true) == later,\n-\t      \"corrected: live packet uses arrival\");\n-\tCHECK(rist_adv_ts_reconstruct(&st, 12345, later, WINDOW, false) == later,\n-\t      \"corrected: retry uses arrival\");\n-}\n-\n-/* The 32-bit 1 MHz field wraps every ~71.6 min; a small forward delta across\n- * the wrap boundary must stay small on the verified source clock. */\n-static void test_wrap(void)\n-{\n-\tstruct rist_adv_ts_state st = {0};\n-\tuint64_t t0 = seed_and_verify(&st, NTP_SEC(1000));\n-\n-\t/* Place the verified clock just below the 32-bit wrap. */\n-\tuint64_t src = t0 + NTP_SEC(10);\n-\tst.last = UINT32_MAX - 500; /* 500 us before wrap */\n-\tst.accum = src;\n-\n-\t/* 1000 us later the wire value wraps past 0 to 499. */\n-\tuint64_t out = rist_adv_ts_reconstruct(&st, 499, src + NTP_US(1000),\n-\t\t\t\t\t       WINDOW, true);\n-\tCHECK(absdiff(out, src + NTP_US(1000)) <= 2,\n-\t      \"wrap yields +1000us, not a ~71min jump: got %\" PRIu64, out);\n-}\n-\n-/* A retransmit is positioned relative to the verified clock but must not move\n- * it: the next live packet continues from where the live stream left off. */\n-static void test_retry_positions_without_advancing(void)\n-{\n-\tstruct rist_adv_ts_state st = {0};\n-\tuint64_t t0 = seed_and_verify(&st, NTP_SEC(1000));\n-\trist_adv_ts_reconstruct(&st, 1100000u, t0 + NTP_MS(1100), WINDOW, true);\n-\tuint32_t last_before = st.last;\n-\tuint64_t accum_before = st.accum;\n-\n-\t/* An older packet (source +1020 ms from t0) is recovered late. */\n-\tuint64_t r = rist_adv_ts_reconstruct(&st, 1020000u, t0 + NTP_MS(1250),\n-\t\t\t\t\t     WINDOW, false);\n-\tCHECK(absdiff(r, t0 + NTP_US(1020000)) <= 4,\n-\t      \"retry stamped at its own source time: got %\" PRIu64, r);\n-\tCHECK(st.last == last_before && st.accum == accum_before,\n-\t      \"retry must not mutate the clock\");\n-\n-\t/* Next live packet continues from +1100 ms, not from the retry. */\n-\tuint64_t out = rist_adv_ts_reconstruct(&st, 1140000u, t0 + NTP_MS(1300),\n-\t\t\t\t\t       WINDOW, true);\n-\tCHECK(absdiff(out, t0 + NTP_US(1140000)) <= 4,\n-\t      \"live stream resumes after retry: got %\" PRIu64, out);\n-}\n-\n-/* Once verified, the source clock is trusted like the Main profile: a sender\n- * clock discontinuity is followed (reconstructed through), not snapped to\n- * arrival, which would inject worse jitter than the jump itself. */\n-static void test_discontinuity_follows_source(void)\n-{\n-\tstruct rist_adv_ts_state st = {0};\n-\tuint64_t t0 = seed_and_verify(&st, NTP_SEC(1000));\n-\n-\t/* A large forward step in the source timestamp (+500 s). */\n-\tuint64_t now = t0 + NTP_MS(1080);\n-\tuint64_t out = rist_adv_ts_reconstruct(&st, 1000000u + 500000000u, now, WINDOW, true);\n-\tCHECK(out != now, \"discontinuity is followed, not snapped to arrival: got %\" PRIu64, out);\n-\tCHECK(out > now + NTP_SEC(100),\n-\t      \"output jumps forward with the source clock: got %\" PRIu64, out);\n-\tCHECK(st.use_source, \"source clock stays in use through a discontinuity\");\n-}\n-\n-int main(void)\n-{\n-\ttest_seed_is_arrival();\n-\ttest_retry_before_seed();\n-\ttest_source_used_immediately();\n-\ttest_verified_tracks_source();\n-\ttest_broken_sender_corrected();\n-\ttest_wrap();\n-\ttest_retry_positions_without_advancing();\n-\ttest_discontinuity_follows_source();\n-\n-\tif (failures) {\n-\t\tfprintf(stderr, \"adv_ts_reconstruct: %d check(s) failed\\n\", failures);\n-\t\treturn 1;\n-\t}\n-\tprintf(\"adv_ts_reconstruct: all checks passed\\n\");\n-\treturn 0;\n-}\ndiff --git a/test/rist/unit/test_bandwidth_guard.c b/test/rist/unit/test_bandwidth_guard.c\ndeleted file mode 100644\nindex c5357d1..0000000\n--- a/test/rist/unit/test_bandwidth_guard.c\n+++ /dev/null\n@@ -1,68 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Unit test for the sender's retransmission-budget guard: whether a bandwidth\n- * ceiling leaves any room for retransmissions, and how often to say so.\n- * Pure, link-free. */\n-\n-#include <stdio.h>\n-\n-#include \"../../../src/rist-bandwidth-guard.h\"\n-\n-static int failures;\n-\n-static void check(const char *name, bool got, bool want)\n-{\n-\tif (got != want) {\n-\t\tfprintf(stderr, \"FAIL: %s -> %d (want %d)\\n\", name, got, want);\n-\t\tfailures++;\n-\t}\n-}\n-\n-/* Bits per second throughout, as the caller passes. */\n-#define MBPS(x) ((size_t)((x) * 1000000))\n-\n-int main(void)\n-{\n-\t/* Comfortable headroom: the ceiling is well clear of the payload. The\n-\t * worst case measured in #227 wanted ~13.4 Mbps of payload plus\n-\t * retransmissions behind ~9.2 Mbps of payload, so a 20 Mbps ceiling is\n-\t * not close to starved. */\n-\tcheck(\"ample_headroom\",\n-\t      rist_retransmit_budget_starved(MBPS(9.2), MBPS(20)), false);\n-\t/* A thin budget is still a budget; this guard only catches the case\n-\t * where there is none at all. */\n-\tcheck(\"thin_but_nonzero\",\n-\t      rist_retransmit_budget_starved(MBPS(9.2), MBPS(9.3)), false);\n-\n-\t/* The ceiling covers payload plus retransmissions, so a ceiling at or\n-\t * under the payload rate leaves nothing. 8.8 Mbps against that same\n-\t * 9.15 Mbps payload is the configuration that refused every NACK. */\n-\tcheck(\"ceiling_below_payload\",\n-\t      rist_retransmit_budget_starved(MBPS(9.15), MBPS(8.8)), true);\n-\tcheck(\"ceiling_equals_payload\",\n-\t      rist_retransmit_budget_starved(MBPS(9), MBPS(9)), true);\n-\tcheck(\"ceiling_just_above_payload\",\n-\t      rist_retransmit_budget_starved(MBPS(9), MBPS(9) + 1), false);\n-\n-\t/* An unset ceiling has nothing to have exceeded; ?bandwidth=0 is\n-\t * ignored by the URL parser, so 0 means \"left at the default\". */\n-\tcheck(\"no_ceiling\", rist_retransmit_budget_starved(MBPS(100), 0), false);\n-\t/* Nor does an idle stream warn. */\n-\tcheck(\"idle_stream\", rist_retransmit_budget_starved(0, MBPS(20)), false);\n-\n-\t/* The warning repeats on an interval rather than firing once, since a\n-\t * line at startup is long gone by the time anyone investigates. */\n-\tcheck(\"first_time_always_due\", rist_bandwidth_warn_due(0, 100, 0), true);\n-\tcheck(\"not_due_yet\", rist_bandwidth_warn_due(1000, 100, 1050), false);\n-\tcheck(\"due_at_interval\", rist_bandwidth_warn_due(1000, 100, 1100), true);\n-\tcheck(\"due_past_interval\", rist_bandwidth_warn_due(1000, 100, 5000), true);\n-\t/* A clock that moved backwards must not stall the warning forever. */\n-\tcheck(\"clock_went_backwards\", rist_bandwidth_warn_due(1000, 100, 500), true);\n-\n-\tif (failures == 0) {\n-\t\tprintf(\"OK\\n\");\n-\t\treturn 0;\n-\t}\n-\tfprintf(stderr, \"Total failures: %d\\n\", failures);\n-\treturn 1;\n-}\ndiff --git a/test/rist/unit/test_eap_v4_crypto.c b/test/rist/unit/test_eap_v4_crypto.c\ndeleted file mode 100644\nindex 8b5ba39..0000000\n--- a/test/rist/unit/test_eap_v4_crypto.c\n+++ /dev/null\n@@ -1,141 +0,0 @@\n-/* librist. Copyright © 2026 SipRadius LLC. All right reserved.\n- * Author: Sergio Ammirata, Ph.D. <sergio@ammirata.net>\n- *\n- * SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Known-answer tests for the EAP v4 crypto primitives:\n- *   - HKDF-Expand-SHA256 against RFC 5869 Test Case 1.\n- *   - AES-256-GCM against the McGrew/Viega GCM spec Test Case 14, plus a\n- *     round-trip and a tag-tamper rejection.\n- */\n-\n-#include \"crypto/eap_v4_crypto.h\"\n-#include <stdint.h>\n-#include <stdio.h>\n-#include <string.h>\n-\n-static int g_failures = 0;\n-\n-static void from_hex(const char *hex, uint8_t *out, size_t out_len)\n-{\n-\tfor (size_t i = 0; i < out_len; i++) {\n-\t\tunsigned v;\n-\t\tsscanf(hex + 2 * i, \"%2x\", &v);\n-\t\tout[i] = (uint8_t)v;\n-\t}\n-}\n-\n-static void check(const char *name, const uint8_t *got, const uint8_t *want, size_t len)\n-{\n-\tif (memcmp(got, want, len) != 0) {\n-\t\tprintf(\"FAIL: %s\\n\", name);\n-\t\tg_failures++;\n-\t} else {\n-\t\tprintf(\"ok: %s\\n\", name);\n-\t}\n-}\n-\n-/* RFC 5869 Test Case 1 (SHA-256). Our function is HKDF-Expand, so we feed the\n- * RFC's PRK and info and expect the RFC's OKM. */\n-static void test_hkdf_rfc5869_case1(void)\n-{\n-\tuint8_t prk[32], info[10], okm[42], want[42];\n-\tfrom_hex(\"077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5\", prk, 32);\n-\tfrom_hex(\"f0f1f2f3f4f5f6f7f8f9\", info, 10);\n-\tfrom_hex(\"3cb25f25faacd57a90434f64d0362f2a\"\n-\t\t\t \"2d2d0a90cf1a5a4c5db02d56ecc4c5bf\"\n-\t\t\t \"34007208d5b887185865\", want, 42);\n-\tint rc = _librist_crypto_hkdf_expand_sha256(prk, 32, info, 10, okm, 42);\n-\tif (rc != 0) { printf(\"FAIL: hkdf rc=%d (backend?)\\n\", rc); g_failures++; return; }\n-\tcheck(\"hkdf-expand rfc5869 case1\", okm, want, 42);\n-}\n-\n-/* v4's per-direction keys are HKDF-Expand of the same K under distinct labels.\n- * Guard that the directions derive different keys (a shared key with restarting\n- * nonces is catastrophic under GCM) and that derivation is deterministic. */\n-static void test_hkdf_direction_separation(void)\n-{\n-\tuint8_t prk[32];\n-\tfor (int i = 0; i < 32; i++) prk[i] = (uint8_t)(0x10 + i);\n-\tconst char *c2s = \"RIST-EAP-v4 pass c2s\";\n-\tconst char *s2c = \"RIST-EAP-v4 pass s2c\";\n-\tuint8_t kc[32], ks[32], kc_again[32];\n-\tint rc = _librist_crypto_hkdf_expand_sha256(prk, 32, (const uint8_t *)c2s, strlen(c2s), kc, 32);\n-\trc |= _librist_crypto_hkdf_expand_sha256(prk, 32, (const uint8_t *)s2c, strlen(s2c), ks, 32);\n-\trc |= _librist_crypto_hkdf_expand_sha256(prk, 32, (const uint8_t *)c2s, strlen(c2s), kc_again, 32);\n-\tif (rc != 0) { printf(\"FAIL: hkdf dir rc=%d (backend?)\\n\", rc); g_failures++; return; }\n-\tif (memcmp(kc, ks, 32) == 0) { printf(\"FAIL: c2s and s2c keys are identical\\n\"); g_failures++; }\n-\telse printf(\"ok: per-direction keys differ\\n\");\n-\tcheck(\"hkdf derivation is deterministic\", kc, kc_again, 32);\n-}\n-\n-/* GCM spec Test Case 14: 256-bit all-zero key, 96-bit all-zero IV, 16-byte\n- * all-zero plaintext, empty AAD. */\n-static void test_gcm_kat_case14(void)\n-{\n-\tuint8_t key[32] = {0}, iv[12] = {0}, pt[16] = {0};\n-\tuint8_t ct[16], tag[16];\n-\tuint8_t want_ct[16], want_tag[16];\n-\tfrom_hex(\"cea7403d4d606b6e074ec5d3baf39d18\", want_ct, 16);\n-\tfrom_hex(\"d0d1c8a799996bf0265b98b5d48ab919\", want_tag, 16);\n-\tint rc = _librist_crypto_aes_gcm_encrypt(key, iv, 12, NULL, 0, pt, 16, ct, tag, 16);\n-\tif (rc != 0) { printf(\"FAIL: gcm encrypt rc=%d (backend?)\\n\", rc); g_failures++; return; }\n-\tcheck(\"gcm-256 KAT case14 ciphertext\", ct, want_ct, 16);\n-\tcheck(\"gcm-256 KAT case14 tag\", tag, want_tag, 16);\n-\n-\t/* decrypt the KAT back */\n-\tuint8_t out[16];\n-\trc = _librist_crypto_aes_gcm_decrypt(key, iv, 12, NULL, 0, ct, 16, tag, 16, out);\n-\tif (rc != 0) { printf(\"FAIL: gcm decrypt of KAT rc=%d\\n\", rc); g_failures++; }\n-\telse check(\"gcm-256 KAT case14 decrypt\", out, pt, 16);\n-}\n-\n-/* Round-trip with AAD, and a tampered-tag rejection. */\n-static void test_gcm_roundtrip_and_tamper(void)\n-{\n-\tuint8_t key[32], iv[12];\n-\tfor (int i = 0; i < 32; i++) key[i] = (uint8_t)(i + 1);\n-\tfor (int i = 0; i < 12; i++) iv[i] = (uint8_t)(0xa0 + i);\n-\tconst uint8_t pt[] = \"correct horse battery staple\";\n-\tconst size_t pt_len = sizeof(pt) - 1;\n-\tconst uint8_t aad[] = {0x02, 0x07, 0x13, 0x10, 0x00};\n-\tuint8_t ct[64], tag[16], out[64];\n-\n-\tint rc = _librist_crypto_aes_gcm_encrypt(key, iv, 12, aad, sizeof(aad), pt, pt_len, ct, tag, 16);\n-\tif (rc != 0) { printf(\"FAIL: gcm rt encrypt rc=%d\\n\", rc); g_failures++; return; }\n-\n-\trc = _librist_crypto_aes_gcm_decrypt(key, iv, 12, aad, sizeof(aad), ct, pt_len, tag, 16, out);\n-\tif (rc != 0) { printf(\"FAIL: gcm rt decrypt rc=%d\\n\", rc); g_failures++; }\n-\telse check(\"gcm round-trip plaintext\", out, pt, pt_len);\n-\n-\t/* Flip a tag bit: decrypt MUST fail and MUST zero the output. */\n-\tuint8_t bad_tag[16];\n-\tmemcpy(bad_tag, tag, 16);\n-\tbad_tag[0] ^= 0x01;\n-\tmemset(out, 0xcc, sizeof(out));\n-\trc = _librist_crypto_aes_gcm_decrypt(key, iv, 12, aad, sizeof(aad), ct, pt_len, bad_tag, 16, out);\n-\tif (rc == 0) { printf(\"FAIL: gcm accepted a tampered tag\\n\"); g_failures++; }\n-\telse printf(\"ok: gcm rejects tampered tag\\n\");\n-\n-\t/* Tampered AAD MUST also fail. */\n-\tuint8_t bad_aad[5];\n-\tmemcpy(bad_aad, aad, 5);\n-\tbad_aad[0] ^= 0x01;\n-\trc = _librist_crypto_aes_gcm_decrypt(key, iv, 12, bad_aad, sizeof(bad_aad), ct, pt_len, tag, 16, out);\n-\tif (rc == 0) { printf(\"FAIL: gcm accepted tampered AAD\\n\"); g_failures++; }\n-\telse printf(\"ok: gcm rejects tampered AAD\\n\");\n-}\n-\n-int main(void)\n-{\n-\ttest_hkdf_rfc5869_case1();\n-\ttest_hkdf_direction_separation();\n-\ttest_gcm_kat_case14();\n-\ttest_gcm_roundtrip_and_tamper();\n-\tif (g_failures) {\n-\t\tprintf(\"%d FAILURE(S)\\n\", g_failures);\n-\t\treturn 1;\n-\t}\n-\tprintf(\"all eap_v4_crypto KATs passed\\n\");\n-\treturn 0;\n-}\ndiff --git a/test/rist/unit/test_nack_peer_select.c b/test/rist/unit/test_nack_peer_select.c\ndeleted file mode 100644\nindex d11ea89..0000000\n--- a/test/rist/unit/test_nack_peer_select.c\n+++ /dev/null\n@@ -1,103 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Unit test for the NACK routing peer-selection predicate\n- * (rist_nack_peer_preferred).  Drives the same loop send_nack_group\n- * runs, over small in-memory candidate tables, and asserts which peer\n- * wins.  No library link or threads required. */\n-\n-#include \"../../../src/rist-nack-select.h\"\n-\n-#include <stdint.h>\n-#include <stdio.h>\n-\n-struct cand {\n-\tuint32_t priority;\n-\tuint64_t rtt;\n-};\n-\n-/* Mirror of the send_nack_group selection loop, returning the index of\n- * the chosen candidate (-1 if none). */\n-static int select_index(const struct cand *c, int n)\n-{\n-\tint chosen = -1;\n-\tuint32_t best_priority = 0;\n-\tuint64_t best_rtt = UINT64_MAX;\n-\tfor (int i = 0; i < n; i++) {\n-\t\tif (rist_nack_peer_preferred(c[i].priority, c[i].rtt,\n-\t\t                             best_priority, best_rtt)) {\n-\t\t\tchosen = i;\n-\t\t\tbest_priority = c[i].priority;\n-\t\t\tbest_rtt = c[i].rtt;\n-\t\t}\n-\t}\n-\treturn chosen;\n-}\n-\n-static int check(const char *name, const struct cand *c, int n, int want)\n-{\n-\tint got = select_index(c, n);\n-\tif (got != want) {\n-\t\tfprintf(stderr, \"FAIL: %s -> index %d (want %d)\\n\",\n-\t\t        name, got, want);\n-\t\treturn 1;\n-\t}\n-\treturn 0;\n-}\n-\n-int main(void)\n-{\n-\tint failures = 0;\n-\n-\t/* Legacy behaviour: all default priority 0 -> lowest RTT wins. */\n-\tstruct cand all_zero[] = {\n-\t\t{0, 50}, {0, 20}, {0, 80},\n-\t};\n-\tfailures += check(\"all_zero_lowest_rtt\", all_zero, 3, 1);\n-\n-\t/* A higher-priority peer wins even with a much larger RTT: a\n-\t * duplicate/relay feed has 0 RTT but no retransmit buffer, while\n-\t * the recovery peer has high RTT and holds the buffer. */\n-\tstruct cand prio_over_rtt[] = {\n-\t\t{0, 1},        /* duplicate feed: tiny RTT, no buffer  */\n-\t\t{1000, 9000},  /* recovery peer: big RTT, holds buffer */\n-\t};\n-\tfailures += check(\"priority_beats_low_rtt\", prio_over_rtt, 2, 1);\n-\n-\t/* Among equal (highest) priority, lowest RTT breaks the tie. */\n-\tstruct cand tie_break[] = {\n-\t\t{5, 100}, {5, 40}, {5, 70}, {1, 1},\n-\t};\n-\tfailures += check(\"tie_break_lowest_rtt\", tie_break, 4, 1);\n-\n-\t/* Single candidate with a measured RTT is chosen. */\n-\tstruct cand single[] = { {0, 1000} };\n-\tfailures += check(\"single_candidate\", single, 1, 0);\n-\n-\t/* Legacy parity: a default-priority peer whose RTT has never been\n-\t * measured (UINT64_MAX sentinel) is NOT selected, matching the\n-\t * historical send_nack_group behaviour. */\n-\tstruct cand unmeasured_zero[] = { {0, UINT64_MAX} };\n-\tfailures += check(\"unmeasured_rtt_priority_zero_skipped\",\n-\t                  unmeasured_zero, 1, -1);\n-\n-\t/* But a high-priority peer is selected even with an unmeasured RTT:\n-\t * priority short-circuits the RTT comparison.  This is what lets a\n-\t * recovery peer receive NACKs before its first RTT sample. */\n-\tstruct cand unmeasured_high[] = { {1000, UINT64_MAX} };\n-\tfailures += check(\"unmeasured_rtt_high_priority_selected\",\n-\t                  unmeasured_high, 1, 0);\n-\n-\t/* No candidates -> none chosen. */\n-\tfailures += check(\"empty\", NULL, 0, -1);\n-\n-\t/* First-seen wins on a full priority+RTT tie (stable selection). */\n-\tstruct cand full_tie[] = { {3, 30}, {3, 30} };\n-\tfailures += check(\"full_tie_first_wins\", full_tie, 2, 0);\n-\n-\tif (failures == 0) {\n-\t\tprintf(\"OK\\n\");\n-\t\treturn 0;\n-\t}\n-\tfprintf(stderr, \"Total failures: %d\\n\", failures);\n-\treturn 1;\n-}\ndiff --git a/test/rist/unit/test_peer_config_versioned_defaults.c b/test/rist/unit/test_peer_config_versioned_defaults.c\ndeleted file mode 100644\nindex a587a59..0000000\n--- a/test/rist/unit/test_peer_config_versioned_defaults.c\n+++ /dev/null\n@@ -1,105 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Verifies rist_peer_config_defaults_set_versioned() writes only fields\n- * that exist at or below the requested version, so a config struct from\n- * an older RIST_PEER_CONFIG_VERSION is never written past its end.\n- *\n- * Field introduction versions (see include/librist/peer.h history):\n- *   split_mode, merge_mode : version 1\n- *   profile, profile_set   : version 4\n- *   rtt_drop et al         : version 6\n- *\n- * Links against the public library only; no cmocka. */\n-\n-#include \"librist/librist.h\"\n-#include \"librist/peer.h\"\n-\n-#include <stdio.h>\n-#include <string.h>\n-\n-#define SENTINEL 0x5a5a5a5a\n-\n-static int check(int version, int expect_split_set, int expect_profile_set,\n-                 int expect_rtt_set)\n-{\n-\tstruct rist_peer_config cfg;\n-\tmemset(&cfg, 0, sizeof(cfg));\n-\tcfg.split_mode = SENTINEL;\n-\tcfg.merge_mode = SENTINEL;\n-\tcfg.profile = (enum rist_profile)SENTINEL;\n-\tcfg.profile_set = SENTINEL;\n-\tcfg.rtt_drop = SENTINEL;\n-\tcfg.rtt_restore = SENTINEL;\n-\tcfg.rtt_drop_settle = SENTINEL;\n-\tcfg.rtt_drop_trickle = SENTINEL;\n-\n-\tif (rist_peer_config_defaults_set_versioned(&cfg, version) != 0) {\n-\t\tfprintf(stderr, \"FAIL: versioned(v=%d) returned error\\n\", version);\n-\t\treturn 1;\n-\t}\n-\n-\tint failures = 0;\n-\tif (cfg.version != version) {\n-\t\tfprintf(stderr, \"FAIL: v=%d recorded version=%d\\n\",\n-\t\t        version, cfg.version);\n-\t\tfailures++;\n-\t}\n-\n-\tint split_written = (cfg.split_mode != SENTINEL) ||\n-\t                    (cfg.merge_mode != SENTINEL);\n-\tif (split_written != expect_split_set) {\n-\t\tfprintf(stderr, \"FAIL: v=%d split/merge written=%d (want %d)\\n\",\n-\t\t        version, split_written, expect_split_set);\n-\t\tfailures++;\n-\t}\n-\n-\tint profile_written = ((int)cfg.profile != (int)(enum rist_profile)SENTINEL) ||\n-\t                      (cfg.profile_set != SENTINEL);\n-\tif (profile_written != expect_profile_set) {\n-\t\tfprintf(stderr, \"FAIL: v=%d profile written=%d (want %d)\\n\",\n-\t\t        version, profile_written, expect_profile_set);\n-\t\tfailures++;\n-\t}\n-\n-\tint rtt_written = (cfg.rtt_drop != SENTINEL) || (cfg.rtt_restore != SENTINEL) ||\n-\t                  (cfg.rtt_drop_settle != SENTINEL) || (cfg.rtt_drop_trickle != SENTINEL);\n-\tif (rtt_written != expect_rtt_set) {\n-\t\tfprintf(stderr, \"FAIL: v=%d rtt fields written=%d (want %d)\\n\",\n-\t\t        version, rtt_written, expect_rtt_set);\n-\t\tfailures++;\n-\t}\n-\treturn failures;\n-}\n-\n-int main(void)\n-{\n-\tint failures = 0;\n-\n-\tfailures += check(0, 0, 0, 0);\n-\tfailures += check(1, 1, 0, 0);\n-\tfailures += check(3, 1, 0, 0);\n-\tfailures += check(4, 1, 1, 0);\n-\tfailures += check(5, 1, 1, 0);\n-\tfailures += check(6, 1, 1, 1);\n-\n-\t/* The public header maps rist_peer_config_defaults_set() to the\n-\t * caller's compiled RIST_PEER_CONFIG_VERSION. */\n-\tstruct rist_peer_config cfg;\n-\tmemset(&cfg, 0, sizeof(cfg));\n-\tif (rist_peer_config_defaults_set(&cfg) != 0) {\n-\t\tfprintf(stderr, \"FAIL: defaults_set returned error\\n\");\n-\t\tfailures++;\n-\t} else if (cfg.version != RIST_PEER_CONFIG_VERSION) {\n-\t\tfprintf(stderr,\n-\t\t        \"FAIL: defaults_set recorded version=%d (want %d)\\n\",\n-\t\t        cfg.version, RIST_PEER_CONFIG_VERSION);\n-\t\tfailures++;\n-\t}\n-\n-\tif (failures == 0) {\n-\t\tprintf(\"OK\\n\");\n-\t\treturn 0;\n-\t}\n-\tfprintf(stderr, \"Total failures: %d\\n\", failures);\n-\treturn 1;\n-}\ndiff --git a/test/rist/unit/test_reanchor.c b/test/rist/unit/test_reanchor.c\ndeleted file mode 100644\nindex d68979f..0000000\n--- a/test/rist/unit/test_reanchor.c\n+++ /dev/null\n@@ -1,88 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Unit test for rist_flow_reanchor_check() - the guard that decides whether a\n- * packet may become the new baseline for a flow whose buffer was just emptied.\n- * Pure, link-free. */\n-\n-#include <inttypes.h>\n-#include <stdio.h>\n-\n-#include \"../../../src/rist-reanchor.h\"\n-\n-static int failures;\n-\n-static const char *name_of(enum rist_reanchor_action a)\n-{\n-\tswitch (a) {\n-\tcase RIST_REANCHOR_OK:\n-\t\treturn \"OK\";\n-\tcase RIST_REANCHOR_WAIT:\n-\t\treturn \"WAIT\";\n-\tcase RIST_REANCHOR_FORCED:\n-\t\treturn \"FORCED\";\n-\t}\n-\treturn \"?\";\n-}\n-\n-static void check(const char *name, enum rist_reanchor_action got,\n-\t\t  enum rist_reanchor_action want)\n-{\n-\tif (got != want) {\n-\t\tfprintf(stderr, \"FAIL: %s -> %s (want %s)\\n\", name, name_of(got),\n-\t\t\tname_of(want));\n-\t\tfailures++;\n-\t}\n-}\n-\n-/* One buffer window, and source times on the same scale. */\n-#define BUF 1800\n-#define MAX 100000\n-\n-int main(void)\n-{\n-\t/* No baseline yet: the first packet of a flow, and the tick right after\n-\t * a clock wrap zeroes the high-water mark. */\n-\tcheck(\"no_baseline\", rist_flow_reanchor_check(50000, 0, BUF, 0, 0),\n-\t      RIST_REANCHOR_OK);\n-\t/* No known buffer depth: nothing to measure staleness against. */\n-\tcheck(\"no_buffer\", rist_flow_reanchor_check(50000, MAX, 0, 0, 0),\n-\t      RIST_REANCHOR_OK);\n-\n-\t/* A current packet anchors, whether it is ahead of the high-water mark\n-\t * or behind it by less than the buffer can absorb. */\n-\tcheck(\"ahead\", rist_flow_reanchor_check(MAX + 500, MAX, BUF, 0, 0),\n-\t      RIST_REANCHOR_OK);\n-\tcheck(\"behind_within_buffer\",\n-\t      rist_flow_reanchor_check(MAX - 1000, MAX, BUF, 0, 0), RIST_REANCHOR_OK);\n-\t/* Exactly one buffer behind is still deliverable. */\n-\tcheck(\"behind_exactly_buffer\",\n-\t      rist_flow_reanchor_check(MAX - BUF, MAX, BUF, 0, 0), RIST_REANCHOR_OK);\n-\n-\t/* Past that it cannot be output at all, so it is not a baseline. */\n-\tcheck(\"behind_past_buffer\",\n-\t      rist_flow_reanchor_check(MAX - BUF - 1, MAX, BUF, 0, 0),\n-\t      RIST_REANCHOR_WAIT);\n-\t/* The 15 s lag that a collapsed bonded leg delivers. */\n-\tcheck(\"behind_lagged_leg\",\n-\t      rist_flow_reanchor_check(MAX - 15000, MAX, BUF, 0, 0),\n-\t      RIST_REANCHOR_WAIT);\n-\n-\t/* The wait is bounded: once a buffer window has passed with nothing\n-\t * better, the stale packet is taken rather than stalling the flow. */\n-\tcheck(\"wait_still_short\",\n-\t      rist_flow_reanchor_check(MAX - 15000, MAX, BUF, 1000, 1000 + BUF - 1),\n-\t      RIST_REANCHOR_WAIT);\n-\tcheck(\"wait_expired\",\n-\t      rist_flow_reanchor_check(MAX - 15000, MAX, BUF, 1000, 1000 + BUF),\n-\t      RIST_REANCHOR_FORCED);\n-\tcheck(\"wait_long_expired\",\n-\t      rist_flow_reanchor_check(MAX - 15000, MAX, BUF, 1000, 1000 + 10 * BUF),\n-\t      RIST_REANCHOR_FORCED);\n-\n-\tif (failures == 0) {\n-\t\tprintf(\"OK\\n\");\n-\t\treturn 0;\n-\t}\n-\tfprintf(stderr, \"Total failures: %d\\n\", failures);\n-\treturn 1;\n-}\ndiff --git a/test/rist/unit/test_recovery_depth.c b/test/rist/unit/test_recovery_depth.c\ndeleted file mode 100644\nindex 1f80d58..0000000\n--- a/test/rist/unit/test_recovery_depth.c\n+++ /dev/null\n@@ -1,172 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Unit test for rist_recovery_depth_set() (exponent-based API).\n- *\n- * recovery-depth N sizes the Advanced retransmission ring to\n- * (UINT16_SIZE << N) packets. This test verifies the public setter's contract:\n- *   - the default Advanced ring is RIST_SERVER_QUEUE_BUFFERS (== depth 3, 8x),\n- *   - each depth maps to (UINT16_SIZE << depth) packets,\n- *   - an Advanced sender's ring (sender_queue_max) is resized in place\n- *     before rist_start(),\n- *   - the Advanced receiver records the new capacity (recovery_queue_max),\n- *   - Simple/Main store the request but never resize their 16-bit ring,\n- *   - a NULL context is rejected.\n- *\n- * Test depths are kept modest (<= 6, i.e. <= 4M entries) to bound the\n- * allocations performed here.\n- *\n- * Includes rist-private.h to inspect the internal ring sizes; links\n- * against the public library for the symbols. */\n-\n-#include \"librist/librist.h\"\n-#include \"rist-private.h\"\n-\n-#include <stdatomic.h>\n-#include <inttypes.h>\n-#include <stdio.h>\n-\n-#define DEPTH_PKTS(n) ((size_t)UINT16_SIZE << (n))\n-#define DEFAULT_PKTS  ((size_t)RIST_SERVER_QUEUE_BUFFERS) /* depth 3, 524288 */\n-\n-static int failures;\n-\n-#define CHECK(cond, ...)                                                   \\\n-\tdo {                                                                   \\\n-\t\tif (!(cond)) {                                                     \\\n-\t\t\tfprintf(stderr, \"FAIL (%s:%d): \", __func__, __LINE__);         \\\n-\t\t\tfprintf(stderr, __VA_ARGS__);                                  \\\n-\t\t\tfprintf(stderr, \"\\n\");                                         \\\n-\t\t\tfailures++;                                                    \\\n-\t\t}                                                                  \\\n-\t} while (0)\n-\n-static size_t sender_ring(struct rist_ctx *ctx)\n-{\n-\treturn ctx->sender_ctx->sender_queue_max;\n-}\n-\n-static size_t common_max(struct rist_ctx *ctx)\n-{\n-\treturn rist_struct_get_common(ctx)->recovery_queue_max;\n-}\n-\n-static void test_default_is_depth_3(void)\n-{\n-\t/* The compile-time default ring must equal depth RIST_RECOVERY_DEPTH_DEFAULT. */\n-\tCHECK(DEFAULT_PKTS == DEPTH_PKTS(RIST_RECOVERY_DEPTH_DEFAULT),\n-\t\t\"RIST_SERVER_QUEUE_BUFFERS=%zu != depth %d (%zu)\",\n-\t\tDEFAULT_PKTS, RIST_RECOVERY_DEPTH_DEFAULT, DEPTH_PKTS(RIST_RECOVERY_DEPTH_DEFAULT));\n-\tCHECK(RIST_RECOVERY_DEPTH_DEFAULT == 3, \"default depth changed from 3\");\n-}\n-\n-static void test_advanced_sender(void)\n-{\n-\tstruct rist_ctx *ctx = NULL;\n-\tif (rist_sender_create(&ctx, RIST_PROFILE_ADVANCED, 0, NULL) != 0 || !ctx) {\n-\t\tfprintf(stderr, \"FAIL: could not create advanced sender\\n\");\n-\t\tfailures++;\n-\t\treturn;\n-\t}\n-\n-\tCHECK(common_max(ctx) == DEFAULT_PKTS,\n-\t\t\"default recovery_queue_max=%zu want %zu\", common_max(ctx), DEFAULT_PKTS);\n-\tCHECK(sender_ring(ctx) == DEFAULT_PKTS,\n-\t\t\"default sender_queue_max=%zu want %zu\", sender_ring(ctx), DEFAULT_PKTS);\n-\n-\tstatic const int depths[] = { 0, 1, 4, 6, 3 };\n-\tfor (size_t i = 0; i < sizeof(depths) / sizeof(depths[0]); i++) {\n-\t\tint d = depths[i];\n-\t\tCHECK(rist_recovery_depth_set(ctx, (uint8_t)d) == 0, \"set(%d) failed\", d);\n-\t\tCHECK(sender_ring(ctx) == DEPTH_PKTS(d),\n-\t\t\t\"depth %d sender_queue_max=%zu want %zu\", d, sender_ring(ctx), DEPTH_PKTS(d));\n-\t\tCHECK(common_max(ctx) == DEPTH_PKTS(d),\n-\t\t\t\"depth %d recovery_queue_max=%zu want %zu\", d, common_max(ctx), DEPTH_PKTS(d));\n-\t}\n-\n-\trist_destroy(ctx);\n-}\n-\n-static void test_advanced_receiver(void)\n-{\n-\tstruct rist_ctx *ctx = NULL;\n-\tif (rist_receiver_create(&ctx, RIST_PROFILE_ADVANCED, NULL) != 0 || !ctx) {\n-\t\tfprintf(stderr, \"FAIL: could not create advanced receiver\\n\");\n-\t\tfailures++;\n-\t\treturn;\n-\t}\n-\n-\tCHECK(common_max(ctx) == DEFAULT_PKTS,\n-\t\t\"default recovery_queue_max=%zu want %zu\", common_max(ctx), DEFAULT_PKTS);\n-\n-\t/* Receiver rings are created per-flow at peer-add time, so the setter\n-\t * just records the capacity the next flow will use. */\n-\tCHECK(rist_recovery_depth_set(ctx, 5) == 0, \"set(5) failed\");\n-\tCHECK(common_max(ctx) == DEPTH_PKTS(5),\n-\t\t\"receiver recovery_queue_max=%zu want %zu\", common_max(ctx), DEPTH_PKTS(5));\n-\n-\trist_destroy(ctx);\n-}\n-\n-static void test_main_profile_sender(void)\n-{\n-\tstruct rist_ctx *ctx = NULL;\n-\tif (rist_sender_create(&ctx, RIST_PROFILE_MAIN, 0, NULL) != 0 || !ctx) {\n-\t\tfprintf(stderr, \"FAIL: could not create main sender\\n\");\n-\t\tfailures++;\n-\t\treturn;\n-\t}\n-\n-\tsize_t before = sender_ring(ctx);\n-\n-\t/* The setter accepts the call and records the request, but a 16-bit\n-\t * profile must never grow its ring. */\n-\tCHECK(rist_recovery_depth_set(ctx, 6) == 0, \"set on main failed\");\n-\tCHECK(sender_ring(ctx) == before,\n-\t\t\"main sender_queue_max changed: %zu -> %zu\", before, sender_ring(ctx));\n-\n-\trist_destroy(ctx);\n-}\n-\n-static void test_out_of_range_clamps(void)\n-{\n-\t/* Use a receiver: it records recovery_queue_max without allocating the\n-\t * (potentially huge) ring up front, so we can check the clamp of an\n-\t * over-range request without depending on the host's overcommit behavior. */\n-\tstruct rist_ctx *ctx = NULL;\n-\tif (rist_receiver_create(&ctx, RIST_PROFILE_ADVANCED, NULL) != 0 || !ctx) {\n-\t\tfprintf(stderr, \"FAIL: could not create advanced receiver (clamp)\\n\");\n-\t\tfailures++;\n-\t\treturn;\n-\t}\n-\t/* expect the platform cap, computed with the same helper the library uses */\n-\tint pmax = rist_recovery_depth_platform_max();\n-\tCHECK(rist_recovery_depth_set(ctx, 200) == 0, \"set(200) (clamped) failed\");\n-\tCHECK(common_max(ctx) == DEPTH_PKTS(pmax),\n-\t\t\"clamp: recovery_queue_max=%zu want %zu (depth %d)\",\n-\t\tcommon_max(ctx), DEPTH_PKTS(pmax), pmax);\n-\trist_destroy(ctx);\n-}\n-\n-static void test_null_ctx(void)\n-{\n-\tCHECK(rist_recovery_depth_set(NULL, 6) == -1, \"NULL ctx should return -1\");\n-}\n-\n-int main(void)\n-{\n-\tfailures = 0;\n-\n-\ttest_default_is_depth_3();\n-\ttest_advanced_sender();\n-\ttest_advanced_receiver();\n-\ttest_main_profile_sender();\n-\ttest_out_of_range_clamps();\n-\ttest_null_ctx();\n-\n-\tif (failures == 0) {\n-\t\tprintf(\"OK\\n\");\n-\t\treturn 0;\n-\t}\n-\tfprintf(stderr, \"Total failures: %d\\n\", failures);\n-\treturn 1;\n-}\ndiff --git a/test/rist/unit/test_recovery_maxbitrate_zero.c b/test/rist/unit/test_recovery_maxbitrate_zero.c\ndeleted file mode 100644\nindex 5b08a73..0000000\n--- a/test/rist/unit/test_recovery_maxbitrate_zero.c\n+++ /dev/null\n@@ -1,135 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * A recovery_maxbitrate of 0 through the C API must be read as \"unset\" and\n- * replaced with the default, not as \"no ceiling\" (#229). Driven through the\n- * public API, asserting on the log. */\n-\n-#include \"librist/librist.h\"\n-#include \"librist/logging.h\"\n-#include \"librist/peer.h\"\n-#include \"librist/receiver.h\"\n-#include \"librist/sender.h\"\n-\n-#include <stdbool.h>\n-#include <stdio.h>\n-#include <string.h>\n-\n-/* Room for every line a peer emits at INFO; overflow is dropped rather than\n- * truncating mid-assertion. */\n-static char captured[16384];\n-static size_t captured_len;\n-\n-static int capture(void *arg, enum rist_log_level level, const char *msg)\n-{\n-\t(void)arg;\n-\t(void)level;\n-\tif (!msg)\n-\t\treturn 0;\n-\tsize_t len = strlen(msg);\n-\tif (len < sizeof(captured) - captured_len) {\n-\t\tmemcpy(captured + captured_len, msg, len);\n-\t\tcaptured_len += len;\n-\t\tcaptured[captured_len] = '\\0';\n-\t}\n-\treturn 0;\n-}\n-\n-static void capture_reset(void)\n-{\n-\tcaptured[0] = '\\0';\n-\tcaptured_len = 0;\n-}\n-\n-static int failures;\n-\n-static void expect(const char *name, const char *needle, bool want)\n-{\n-\tbool got = strstr(captured, needle) != NULL;\n-\tif (got != want) {\n-\t\tfprintf(stderr, \"FAIL: %s: %s \\\"%s\\\"\\n\", name,\n-\t\t        want ? \"expected but did not find\" : \"unexpectedly found\",\n-\t\t        needle);\n-\t\tfailures++;\n-\t}\n-}\n-\n-/* Creates one peer from url on a fresh context of the given role, leaving the\n- * log of that peer's creation in captured. maxbitrate is written into the\n- * parsed config the way an application using the C API would. */\n-static int make_peer(struct rist_logging_settings *logs, bool receiver,\n-                     const char *url, uint32_t maxbitrate)\n-{\n-\tstruct rist_ctx *ctx = NULL;\n-\tint ret = receiver ? rist_receiver_create(&ctx, RIST_PROFILE_MAIN, logs)\n-\t                   : rist_sender_create(&ctx, RIST_PROFILE_MAIN, 0, logs);\n-\tif (ret != 0) {\n-\t\tfprintf(stderr, \"rist_%s_create failed\\n\", receiver ? \"receiver\" : \"sender\");\n-\t\treturn -1;\n-\t}\n-\n-\tstruct rist_peer_config *cfg = NULL;\n-\tstruct rist_peer *peer = NULL;\n-\tint fails = 0;\n-\n-\tif (rist_parse_address2(url, &cfg) != 0 || cfg == NULL) {\n-\t\tfprintf(stderr, \"rist_parse_address2(%s) failed\\n\", url);\n-\t\tfails = -1;\n-\t\tgoto cleanup;\n-\t}\n-\tcfg->recovery_maxbitrate = maxbitrate;\n-\n-\tcapture_reset();\n-\tif (rist_peer_create(ctx, &peer, cfg) != 0) {\n-\t\tfprintf(stderr, \"rist_peer_create(%s, maxbitrate=%u) failed\\n\", url, maxbitrate);\n-\t\tfails = -1;\n-\t}\n-\n-cleanup:\n-\tif (cfg)\n-\t\trist_peer_config_free2(&cfg);\n-\trist_destroy(ctx);\n-\treturn fails;\n-}\n-\n-int main(void)\n-{\n-\tstruct rist_logging_settings *logs = NULL;\n-\t/* INFO, and no stream, so the receiver's config line is captured and the\n-\t * test stays quiet on success. */\n-\tif (rist_logging_set(&logs, RIST_LOG_INFO, capture, NULL, NULL, NULL) != 0) {\n-\t\tfprintf(stderr, \"rist_logging_set failed\\n\");\n-\t\treturn 1;\n-\t}\n-\n-\t/* A zero is corrected, and says so rather than silently. */\n-\tif (make_peer(logs, false, \"rist://127.0.0.1:30200\", 0) < 0)\n-\t\tfailures++;\n-\telse {\n-\t\texpect(\"sender_zero\", \"recovery-maxbitrate of 0\", true);\n-\t\texpect(\"sender_zero\", \"100000 kbps default\", true);\n-\t}\n-\n-\t/* A real ceiling is left alone. */\n-\tif (make_peer(logs, false, \"rist://127.0.0.1:30202\", 8000) < 0)\n-\t\tfailures++;\n-\telse\n-\t\texpect(\"sender_nonzero\", \"recovery-maxbitrate of 0\", false);\n-\n-\t/* The correction lands before the derived settings are computed, not just\n-\t * in the warning: the receiver's own config line reports the default. */\n-\tif (make_peer(logs, true, \"rist://@127.0.0.1:30204\", 0) < 0)\n-\t\tfailures++;\n-\telse {\n-\t\texpect(\"receiver_zero\", \"recovery-maxbitrate of 0\", true);\n-\t\texpect(\"receiver_zero\", \"maxrate=100000/\", true);\n-\t}\n-\n-\trist_logging_settings_free2(&logs);\n-\n-\tif (failures == 0) {\n-\t\tprintf(\"OK\\n\");\n-\t\treturn 0;\n-\t}\n-\tfprintf(stderr, \"Total failures: %d\\n\", failures);\n-\treturn 1;\n-}\ndiff --git a/test/rist/unit/test_retx_seq_domain.c b/test/rist/unit/test_retx_seq_domain.c\ndeleted file mode 100644\nindex 5ed0aee..0000000\n--- a/test/rist/unit/test_retx_seq_domain.c\n+++ /dev/null\n@@ -1,57 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Unit test for the retransmission sequence-domain predicate\n- * (rist_retx_use_rtp_domain).  Regression guard for the mixed-profile\n- * ARQ defect: an Advanced head-end serving a peer that negotiated down\n- * to Main must resolve/verify/frame that peer's retransmits in the\n- * 16-bit RTP sequence domain, not the 32-bit advanced domain, or the\n- * Main peer's NACKs never resolve and nothing is retransmitted\n- * (recovered_packets_total stays 0).  No library link or threads. */\n-\n-#include \"../../../src/rist-retx-domain.h\"\n-\n-#include <stdio.h>\n-\n-static int check(const char *name, enum rist_profile profile,\n-                 bool remote_adv, bool want)\n-{\n-\tbool got = rist_retx_use_rtp_domain(profile, remote_adv);\n-\tif (got != want) {\n-\t\tfprintf(stderr, \"FAIL: %s -> %d (want %d)\\n\", name, got, want);\n-\t\treturn 1;\n-\t}\n-\treturn 0;\n-}\n-\n-int main(void)\n-{\n-\tint failures = 0;\n-\n-\t/* The bug case: Advanced context, peer has NOT advertised Advanced\n-\t * (negotiated down to Main) -> must use the 16-bit RTP domain. */\n-\tfailures += check(\"advanced_ctx_main_peer\",\n-\t                  RIST_PROFILE_ADVANCED, false, true);\n-\n-\t/* Production Advanced<->Advanced: peer advertised Advanced -> keep\n-\t * the 32-bit advanced domain (unchanged behaviour). */\n-\tfailures += check(\"advanced_ctx_advanced_peer\",\n-\t                  RIST_PROFILE_ADVANCED, true, false);\n-\n-\t/* Main context: its primary index is already the RTP domain, so the\n-\t * predicate must not divert (regardless of the peer flag). */\n-\tfailures += check(\"main_ctx_main_peer\",\n-\t                  RIST_PROFILE_MAIN, false, false);\n-\tfailures += check(\"main_ctx_adv_flag\",\n-\t                  RIST_PROFILE_MAIN, true, false);\n-\n-\t/* Simple context likewise. */\n-\tfailures += check(\"simple_ctx\",\n-\t                  RIST_PROFILE_SIMPLE, false, false);\n-\n-\tif (failures == 0) {\n-\t\tprintf(\"OK\\n\");\n-\t\treturn 0;\n-\t}\n-\tfprintf(stderr, \"Total failures: %d\\n\", failures);\n-\treturn 1;\n-}\ndiff --git a/test/rist/unit/test_rtt_mute.c b/test/rist/unit/test_rtt_mute.c\ndeleted file mode 100644\nindex 967fe18..0000000\n--- a/test/rist/unit/test_rtt_mute.c\n+++ /dev/null\n@@ -1,199 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Unit test for the RTT-based bonded-leg muting hysteresis state machine\n- * (rist_rtt_mute_step).  Drives sample sequences through the pure function\n- * and asserts the drop/restore transitions and dwell behaviour.  No library\n- * link or threads required. */\n-\n-#include \"../../../src/rist-rtt-mute.h\"\n-\n-#include <stdint.h>\n-#include <stdio.h>\n-\n-/* Thresholds in abstract \"tick\" units; the production caller uses RIST_CLOCK\n- * ticks but the machine is unit-agnostic. */\n-#define DROP     500\n-#define RESTORE  400\n-#define SETTLE   3\n-\n-static int failures;\n-\n-static void expect(const char *name, enum rist_rtt_mute_action got,\n-                   enum rist_rtt_mute_action want, bool muted, bool want_muted)\n-{\n-\tif (got != want || muted != want_muted) {\n-\t\tfprintf(stderr, \"FAIL: %s -> action %d muted %d (want action %d muted %d)\\n\",\n-\t\t        name, got, muted, want, want_muted);\n-\t\tfailures++;\n-\t}\n-}\n-\n-static void check(const char *name, bool got, bool want)\n-{\n-\tif (got != want) {\n-\t\tfprintf(stderr, \"FAIL: %s -> %d (want %d)\\n\", name, got, want);\n-\t\tfailures++;\n-\t}\n-}\n-\n-static void check_weight(const char *name, uint32_t got, uint32_t want)\n-{\n-\tif (got != want) {\n-\t\tfprintf(stderr, \"FAIL: %s -> %u (want %u)\\n\", name, got, want);\n-\t\tfailures++;\n-\t}\n-}\n-\n-int main(void)\n-{\n-\t/* A leg that never crosses the ceiling stays active. */\n-\t{\n-\t\tstruct rist_rtt_mute_state st = {0};\n-\t\tenum rist_rtt_mute_action a = RIST_RTT_MUTE_NONE;\n-\t\tfor (uint64_t t = 0; t < 10; t++)\n-\t\t\ta = rist_rtt_mute_step(&st, 100, DROP, RESTORE, SETTLE, SETTLE, t);\n-\t\texpect(\"stays_active_below_ceiling\", a, RIST_RTT_MUTE_NONE, st.muted, false);\n-\t}\n-\n-\t/* Over the ceiling but not long enough to satisfy the dwell: no drop. */\n-\t{\n-\t\tstruct rist_rtt_mute_state st = {0};\n-\t\trist_rtt_mute_step(&st, 900, DROP, RESTORE, SETTLE, SETTLE, 0); /* arm pending */\n-\t\tenum rist_rtt_mute_action a =\n-\t\t\trist_rtt_mute_step(&st, 900, DROP, RESTORE, SETTLE, SETTLE, 1); /* dwell not met */\n-\t\texpect(\"no_drop_before_settle\", a, RIST_RTT_MUTE_NONE, st.muted, false);\n-\t}\n-\n-\t/* Sustained over the ceiling past the dwell: drop. */\n-\t{\n-\t\tstruct rist_rtt_mute_state st = {0};\n-\t\trist_rtt_mute_step(&st, 900, DROP, RESTORE, SETTLE, SETTLE, 0);\n-\t\trist_rtt_mute_step(&st, 900, DROP, RESTORE, SETTLE, SETTLE, 1);\n-\t\trist_rtt_mute_step(&st, 900, DROP, RESTORE, SETTLE, SETTLE, 2);\n-\t\tenum rist_rtt_mute_action a =\n-\t\t\trist_rtt_mute_step(&st, 900, DROP, RESTORE, SETTLE, SETTLE, 3); /* now-since == SETTLE */\n-\t\texpect(\"drop_after_settle\", a, RIST_RTT_MUTE_DROP, st.muted, true);\n-\t}\n-\n-\t/* A transient dip back under the ceiling resets the dwell (anti-flap). */\n-\t{\n-\t\tstruct rist_rtt_mute_state st = {0};\n-\t\trist_rtt_mute_step(&st, 900, DROP, RESTORE, SETTLE, SETTLE, 0);\n-\t\trist_rtt_mute_step(&st, 900, DROP, RESTORE, SETTLE, SETTLE, 1);\n-\t\trist_rtt_mute_step(&st, 100, DROP, RESTORE, SETTLE, SETTLE, 2); /* dip: clears pending */\n-\t\tenum rist_rtt_mute_action a =\n-\t\t\trist_rtt_mute_step(&st, 900, DROP, RESTORE, SETTLE, SETTLE, 3); /* re-arm, no drop yet */\n-\t\texpect(\"dip_resets_dwell\", a, RIST_RTT_MUTE_NONE, st.muted, false);\n-\t}\n-\n-\t/* Hysteresis: while muted, RTT between restore and drop does NOT restore. */\n-\t{\n-\t\tstruct rist_rtt_mute_state st = { .muted = true };\n-\t\tenum rist_rtt_mute_action a = RIST_RTT_MUTE_NONE;\n-\t\tfor (uint64_t t = 0; t < 10; t++)\n-\t\t\ta = rist_rtt_mute_step(&st, 450, DROP, RESTORE, SETTLE, SETTLE, t); /* in the band */\n-\t\texpect(\"no_restore_in_hysteresis_band\", a, RIST_RTT_MUTE_NONE, st.muted, true);\n-\t}\n-\n-\t/* Sustained below the restore low-water past the dwell: restore. */\n-\t{\n-\t\tstruct rist_rtt_mute_state st = { .muted = true };\n-\t\trist_rtt_mute_step(&st, 100, DROP, RESTORE, SETTLE, SETTLE, 0);\n-\t\trist_rtt_mute_step(&st, 100, DROP, RESTORE, SETTLE, SETTLE, 1);\n-\t\trist_rtt_mute_step(&st, 100, DROP, RESTORE, SETTLE, SETTLE, 2);\n-\t\tenum rist_rtt_mute_action a =\n-\t\t\trist_rtt_mute_step(&st, 100, DROP, RESTORE, SETTLE, SETTLE, 3);\n-\t\texpect(\"restore_after_settle\", a, RIST_RTT_MUTE_RESTORE, st.muted, false);\n-\t}\n-\n-\t/* drop == 0 disables the feature and force-restores a muted leg. */\n-\t{\n-\t\tstruct rist_rtt_mute_state st = { .muted = true };\n-\t\tenum rist_rtt_mute_action a =\n-\t\t\trist_rtt_mute_step(&st, 9999, 0, RESTORE, SETTLE, SETTLE, 0);\n-\t\texpect(\"disabled_forces_restore\", a, RIST_RTT_MUTE_RESTORE, st.muted, false);\n-\t}\n-\n-\t/* drop == 0 on an already-active leg is a no-op. */\n-\t{\n-\t\tstruct rist_rtt_mute_state st = {0};\n-\t\tenum rist_rtt_mute_action a =\n-\t\t\trist_rtt_mute_step(&st, 9999, 0, RESTORE, SETTLE, SETTLE, 0);\n-\t\texpect(\"disabled_active_noop\", a, RIST_RTT_MUTE_NONE, st.muted, false);\n-\t}\n-\n-\t/* Asymmetric dwell: a leg drops fast (drop_settle) but only rejoins after\n-\t * the longer restore_settle, so a marginal link cannot flap back in. With\n-\t * drop_settle 1 and restore_settle 5, muting engages quickly... */\n-\t{\n-\t\tstruct rist_rtt_mute_state st = {0};\n-\t\trist_rtt_mute_step(&st, 900, DROP, RESTORE, 1, 5, 0); /* arm */\n-\t\tenum rist_rtt_mute_action a =\n-\t\t\trist_rtt_mute_step(&st, 900, DROP, RESTORE, 1, 5, 1); /* fast drop */\n-\t\texpect(\"asymmetric_fast_drop\", a, RIST_RTT_MUTE_DROP, st.muted, true);\n-\n-\t\t/* ...but rejoining waits the full restore_settle: below the low-water\n-\t\t * for less than restore_settle does NOT restore yet. */\n-\t\trist_rtt_mute_step(&st, 100, DROP, RESTORE, 1, 5, 2); /* arm restore */\n-\t\ta = rist_rtt_mute_step(&st, 100, DROP, RESTORE, 1, 5, 5); /* dwell 3 < 5 */\n-\t\texpect(\"asymmetric_slow_restore_hold\", a, RIST_RTT_MUTE_NONE, st.muted, true);\n-\n-\t\t/* Past the longer window it finally restores. */\n-\t\ta = rist_rtt_mute_step(&st, 100, DROP, RESTORE, 1, 5, 7); /* dwell 5 == 5 */\n-\t\texpect(\"asymmetric_slow_restore_fires\", a, RIST_RTT_MUTE_RESTORE, st.muted, false);\n-\t}\n-\n-\t/* Sole-carrier handover: when every leg wants muting one must keep\n-\t * carrying, and the role is sticky so the payload does not ping-pong. */\n-\t{\n-\t\t/* A marginally better challenger does not take the role, even\n-\t\t * once the incumbent has served its minimum. */\n-\t\tcheck(\"handover_rejects_small_margin\",\n-\t\t      rist_rtt_sole_carrier_handover(126, 85, 10, 3, 2), false);\n-\t\t/* Twice as good is enough. */\n-\t\tcheck(\"handover_accepts_clear_margin\",\n-\t\t      rist_rtt_sole_carrier_handover(1162, 406, 10, 3, 2), true);\n-\t\t/* ...but not before the incumbent has held for the minimum. */\n-\t\tcheck(\"handover_holds_until_min\",\n-\t\t      rist_rtt_sole_carrier_handover(1162, 406, 2, 3, 2), false);\n-\t\t/* Exactly at the margin and exactly at the minimum both count. */\n-\t\tcheck(\"handover_boundaries\",\n-\t\t      rist_rtt_sole_carrier_handover(800, 400, 3, 3, 2), true);\n-\t}\n-\n-\t/* Post-restore weight ramp. */\n-\t{\n-\t\t/* Never muted (no ramp start) or a finished ramp: full weight. */\n-\t\tcheck_weight(\"ramp_absent\", rist_rtt_ramped_weight(10, 0, 100, 50), 10);\n-\t\tcheck_weight(\"ramp_complete\", rist_rtt_ramped_weight(10, 0 + 1, 100, 201), 10);\n-\t\t/* Linear across the window. */\n-\t\tcheck_weight(\"ramp_quarter\", rist_rtt_ramped_weight(10, 100, 100, 125), 2);\n-\t\tcheck_weight(\"ramp_half\", rist_rtt_ramped_weight(10, 100, 100, 150), 5);\n-\t\tcheck_weight(\"ramp_end\", rist_rtt_ramped_weight(10, 100, 100, 200), 10);\n-\t\t/* A ramping leg never drops out of the rotation entirely. */\n-\t\tcheck_weight(\"ramp_floor_is_one\", rist_rtt_ramped_weight(10, 100, 100, 101), 1);\n-\t\t/* Weight 1 and duplicate legs (0) are left alone. */\n-\t\tcheck_weight(\"ramp_skips_weight_one\", rist_rtt_ramped_weight(1, 100, 100, 150), 1);\n-\t\tcheck_weight(\"ramp_skips_duplicate\", rist_rtt_ramped_weight(0, 100, 100, 150), 0);\n-\t}\n-\n-\t/* Trickle cutoff on a leg queued deeper than the buffer. */\n-\t{\n-\t\t/* One-way delay well inside the buffer: keep the leg warm. */\n-\t\tcheck(\"trickle_within_buffer\", rist_rtt_trickle_useful(400, 1800), true);\n-\t\t/* One way is half the round trip, so the cutoff is twice the buffer. */\n-\t\tcheck(\"trickle_at_cutoff\", rist_rtt_trickle_useful(3600, 1800), true);\n-\t\tcheck(\"trickle_past_cutoff\", rist_rtt_trickle_useful(3602, 1800), false);\n-\t\t/* The 33 s round trips seen on a collapsed leg. */\n-\t\tcheck(\"trickle_collapsed_leg\", rist_rtt_trickle_useful(33000, 1800), false);\n-\t\t/* Unknown buffer: no basis to stop. */\n-\t\tcheck(\"trickle_unknown_buffer\", rist_rtt_trickle_useful(33000, 0), true);\n-\t}\n-\n-\tif (failures == 0) {\n-\t\tprintf(\"OK\\n\");\n-\t\treturn 0;\n-\t}\n-\tfprintf(stderr, \"Total failures: %d\\n\", failures);\n-\treturn 1;\n-}\ndiff --git a/test/rist/unit/test_send_grace.c b/test/rist/unit/test_send_grace.c\ndeleted file mode 100644\nindex 153d39a..0000000\n--- a/test/rist/unit/test_send_grace.c\n+++ /dev/null\n@@ -1,66 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Unit test for rist_peer_may_send() - the grace period that keeps a peer in\n- * the send rotation for one recovery buffer window after it is declared dead.\n- * Pure, link-free. */\n-\n-#include <stdio.h>\n-\n-#include \"../../../src/rist-send-grace.h\"\n-\n-static int failures;\n-\n-static void check(const char *name, bool got, bool want)\n-{\n-\tif (got != want) {\n-\t\tfprintf(stderr, \"FAIL: %s -> %s (want %s)\\n\", name,\n-\t\t\tgot ? \"send\" : \"drop\", want ? \"send\" : \"drop\");\n-\t\tfailures++;\n-\t}\n-}\n-\n-/* One buffer window, and a death well clear of zero so a backwards clock is\n- * expressible. */\n-#define GRACE 4000\n-#define DIED 100000\n-\n-int main(void)\n-{\n-\t/* A live peer is always eligible, whatever the clock says. */\n-\tcheck(\"alive\", rist_peer_may_send(false, 0, GRACE, DIED), true);\n-\tcheck(\"alive_no_grace\", rist_peer_may_send(false, 0, 0, DIED), true);\n-\n-\t/* A dead peer keeps its place until the window runs out. This is the\n-\t * single-path case from #196: the return path went quiet but the\n-\t * receiver is still there, so cutting the stream strands it. */\n-\tcheck(\"died_now\", rist_peer_may_send(true, DIED, GRACE, DIED), true);\n-\tcheck(\"inside_window\",\n-\t      rist_peer_may_send(true, DIED, GRACE, DIED + GRACE / 2), true);\n-\tcheck(\"last_tick_of_window\",\n-\t      rist_peer_may_send(true, DIED, GRACE, DIED + GRACE), true);\n-\n-\t/* Past the window there is nothing left to wait for. */\n-\tcheck(\"just_past_window\",\n-\t      rist_peer_may_send(true, DIED, GRACE, DIED + GRACE + 1), false);\n-\tcheck(\"long_gone\",\n-\t      rist_peer_may_send(true, DIED, GRACE, DIED + 100 * GRACE), false);\n-\n-\t/* No configured window: a dead peer is dropped at once. */\n-\tcheck(\"no_grace\", rist_peer_may_send(true, DIED, 0, DIED), true);\n-\tcheck(\"no_grace_next_tick\", rist_peer_may_send(true, DIED, 0, DIED + 1),\n-\t      false);\n-\n-\t/* A clock that moved backwards must not read as an expired window and\n-\t * cut a stream that is still fine. */\n-\tcheck(\"clock_went_backwards\",\n-\t      rist_peer_may_send(true, DIED, GRACE, DIED - 10 * GRACE), true);\n-\tcheck(\"clock_went_back_one_tick\",\n-\t      rist_peer_may_send(true, DIED, GRACE, DIED - 1), true);\n-\n-\tif (failures == 0) {\n-\t\tprintf(\"OK\\n\");\n-\t\treturn 0;\n-\t}\n-\tfprintf(stderr, \"Total failures: %d\\n\", failures);\n-\treturn 1;\n-}\ndiff --git a/test/rist/unit/test_seq_gap.c b/test/rist/unit/test_seq_gap.c\ndeleted file mode 100644\nindex 1e2960d..0000000\n--- a/test/rist/unit/test_seq_gap.c\n+++ /dev/null\n@@ -1,92 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Unit test for rist_seq_gap() - the forward sequence-number gap helper used\n- * by the receiver gap detector (receiver_mark_missing). The key property under\n- * test is that short-sequence (Simple/Main, 16-bit) flows wrap modulo 65536\n- * while full 32-bit (Advanced) flows report the true gap, so a genuine >64k\n- * gap on an Advanced flow is no longer silently truncated to a 16-bit value.\n- * Pure, link-free: mirrors the static-inline definition from rist-private.h. */\n-\n-#include <inttypes.h>\n-#include <stdbool.h>\n-#include <stdint.h>\n-#include <stdio.h>\n-\n-/* The helper is a static inline in rist-private.h, which drags in the entire\n- * private header graph. Mirror just the definition here (kept byte-identical)\n- * so the test stays a pure, link-free unit. A divergence between the two is a\n- * deliberate review signal: update both. */\n-#ifndef UINT16_MAX\n-#define UINT16_MAX 0xFFFFu\n-#endif\n-static inline uint32_t rist_seq_gap(uint32_t current, uint32_t last,\n-                                    bool short_seq)\n-{\n-\tuint32_t gap = current - last;\n-\treturn short_seq ? (gap & UINT16_MAX) : gap;\n-}\n-\n-static int failures = 0;\n-\n-#define CHECK(cond, ...)                                                \\\n-    do {                                                                \\\n-        if (!(cond)) {                                                  \\\n-            fprintf(stderr, \"FAIL %s:%d: \" #cond, __FILE__, __LINE__);  \\\n-            fprintf(stderr, \" - \"); fprintf(stderr, __VA_ARGS__);       \\\n-            fprintf(stderr, \"\\n\");                                      \\\n-            failures++;                                                 \\\n-        }                                                               \\\n-    } while (0)\n-\n-/* T1 - small in-order gap is identical for both framings. */\n-static void test_small_gap(void)\n-{\n-    CHECK(rist_seq_gap(105, 100, true) == 5, \"short small gap\");\n-    CHECK(rist_seq_gap(105, 100, false) == 5, \"long small gap\");\n-}\n-\n-/* T2 - 16-bit wrap: short flows wrap modulo 65536. */\n-static void test_short_wrap(void)\n-{\n-    /* last=65530, current=4 -> 16-bit forward gap is 10. */\n-    CHECK(rist_seq_gap(4u, 65530u, true) == 10u,\n-          \"short wrap gap: got %u want 10\", rist_seq_gap(4u, 65530u, true));\n-}\n-\n-/* T3 - the regression we are fixing: a real >64k gap on a 32-bit flow.\n- * The old unconditional `& UINT16_MAX` truncated this to a tiny value and the\n- * detector mis-counted the loss; the 32-bit path must report the true gap. */\n-static void test_long_gap_not_truncated(void)\n-{\n-    uint32_t gap = rist_seq_gap(70000u, 0u, false);\n-    CHECK(gap == 70000u, \"long >64k gap must be exact: got %u want 70000\", gap);\n-\n-    /* Same numbers under short framing DO truncate (70000 & 0xFFFF = 4464),\n-     * documenting why the 16-bit path must stay separate. */\n-    CHECK(rist_seq_gap(70000u, 0u, true) == (70000u & UINT16_MAX),\n-          \"short framing truncates to 16 bits by design\");\n-}\n-\n-/* T4 - 32-bit wrap is well-defined unsigned arithmetic. */\n-static void test_long_wrap(void)\n-{\n-    /* last near the top of the 32-bit space, current just past wrap. */\n-    CHECK(rist_seq_gap(5u, 0xFFFFFFFBu, false) == 10u,\n-          \"long 32-bit wrap gap: got %u want 10\",\n-          rist_seq_gap(5u, 0xFFFFFFFBu, false));\n-}\n-\n-int main(void)\n-{\n-    test_small_gap();\n-    test_short_wrap();\n-    test_long_gap_not_truncated();\n-    test_long_wrap();\n-\n-    if (failures > 0) {\n-        fprintf(stderr, \"[test_seq_gap] %d FAILURES\\n\", failures);\n-        return 1;\n-    }\n-    fprintf(stderr, \"[test_seq_gap] all tests passed\\n\");\n-    return 0;\n-}\ndiff --git a/test/rist/unit/test_seq_next.c b/test/rist/unit/test_seq_next.c\ndeleted file mode 100644\nindex d05371b..0000000\n--- a/test/rist/unit/test_seq_next.c\n+++ /dev/null\n@@ -1,76 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Unit test for rist_seq_next() - the \"expected next sequence number\" helper\n- * used by the receiver out-of-order / late guard (receiver_enqueue). The\n- * regression under test: the short_seq successor must wrap at 16 bits with\n- * `& UINT16_MAX` (0xFFFF), not the historical `& (UINT16_MAX - 1)` (0xFFFE)\n- * which forced the value even and mis-computed every odd successor.\n- * Pure, link-free: mirrors the static-inline definition from rist-private.h. */\n-\n-#include <inttypes.h>\n-#include <stdbool.h>\n-#include <stdint.h>\n-#include <stdio.h>\n-\n-#ifndef UINT16_MAX\n-#define UINT16_MAX 0xFFFFu\n-#endif\n-static inline uint32_t rist_seq_next(uint32_t last, bool short_seq)\n-{\n-\tuint32_t next = last + 1;\n-\treturn short_seq ? (next & UINT16_MAX) : next;\n-}\n-\n-static int failures = 0;\n-\n-#define CHECK(cond, ...)                                                \\\n-    do {                                                                \\\n-        if (!(cond)) {                                                  \\\n-            fprintf(stderr, \"FAIL %s:%d: \" #cond, __FILE__, __LINE__);  \\\n-            fprintf(stderr, \" - \"); fprintf(stderr, __VA_ARGS__);       \\\n-            fprintf(stderr, \"\\n\");                                      \\\n-            failures++;                                                 \\\n-        }                                                               \\\n-    } while (0)\n-\n-/* T1 - the regression: an odd successor must be returned exactly. Under the\n- * old 0xFFFE mask, rist_seq_next(100, true) would have returned 100 (even). */\n-static void test_odd_successor(void)\n-{\n-    CHECK(rist_seq_next(100u, true) == 101u,\n-          \"short odd successor: got %u want 101\", rist_seq_next(100u, true));\n-    CHECK(rist_seq_next(101u, true) == 102u,\n-          \"short even successor: got %u want 102\", rist_seq_next(101u, true));\n-}\n-\n-/* T2 - 16-bit wrap: short flows wrap modulo 65536. */\n-static void test_short_wrap(void)\n-{\n-    CHECK(rist_seq_next(65535u, true) == 0u,\n-          \"short wrap: got %u want 0\", rist_seq_next(65535u, true));\n-    CHECK(rist_seq_next(65534u, true) == 65535u,\n-          \"short top: got %u want 65535\", rist_seq_next(65534u, true));\n-}\n-\n-/* T3 - 32-bit flows do not wrap at 16 bits and wrap correctly at 32. */\n-static void test_long(void)\n-{\n-    CHECK(rist_seq_next(65535u, false) == 65536u,\n-          \"long no 16-bit wrap: got %u want 65536\", rist_seq_next(65535u, false));\n-    CHECK(rist_seq_next(0xFFFFFFFFu, false) == 0u,\n-          \"long 32-bit wrap: got %u want 0\", rist_seq_next(0xFFFFFFFFu, false));\n-}\n-\n-int main(void)\n-{\n-    test_odd_successor();\n-    test_short_wrap();\n-    test_long();\n-\n-    if (failures > 0) {\n-        fprintf(stderr, \"[test_seq_next] %d FAILURES\\n\", failures);\n-        return 1;\n-    }\n-    fprintf(stderr, \"[test_seq_next] all tests passed\\n\");\n-    return 0;\n-}\ndiff --git a/test/rist/unit/test_tun_cidr_parse.c b/test/rist/unit/test_tun_cidr_parse.c\ndeleted file mode 100644\nindex 96ae1e7..0000000\n--- a/test/rist/unit/test_tun_cidr_parse.c\n+++ /dev/null\n@@ -1,54 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause */\n-\n-#include \"librist/tun.h\"\n-\n-#include <stdio.h>\n-#include <string.h>\n-\n-static int expect_ok(const char *cidr, const char *want_ip, int want_prefix)\n-{\n-\tchar ip[64];\n-\tint prefix = -1;\n-\n-\tif (rist_tun_parse_cidr(cidr, ip, sizeof(ip), &prefix) != 0) {\n-\t\tfprintf(stderr, \"FAIL: expected success for %s\\n\", cidr);\n-\t\treturn 1;\n-\t}\n-\tif (strcmp(ip, want_ip) != 0 || prefix != want_prefix) {\n-\t\tfprintf(stderr,\n-\t\t        \"FAIL: %s -> ip=%s prefix=%d (want ip=%s prefix=%d)\\n\",\n-\t\t        cidr, ip, prefix, want_ip, want_prefix);\n-\t\treturn 1;\n-\t}\n-\treturn 0;\n-}\n-\n-static int expect_fail(const char *cidr)\n-{\n-\tchar ip[64];\n-\tint prefix = 0;\n-\n-\tif (rist_tun_parse_cidr(cidr, ip, sizeof(ip), &prefix) == 0) {\n-\t\tfprintf(stderr, \"FAIL: expected error for %s\\n\", cidr);\n-\t\treturn 1;\n-\t}\n-\treturn 0;\n-}\n-\n-int main(void)\n-{\n-\tint failures = 0;\n-\n-\tfailures += expect_ok(\"10.0.0.1\", \"10.0.0.1\", 32);\n-\tfailures += expect_ok(\"10.0.0.1/24\", \"10.0.0.1\", 24);\n-\tfailures += expect_ok(\"fd00:0:ffff::1\", \"fd00:0:ffff::1\", 128);\n-\tfailures += expect_ok(\"fd00:0:ffff::1/64\", \"fd00:0:ffff::1\", 64);\n-\tfailures += expect_ok(\"2001:db8::2/48\", \"2001:db8::2\", 48);\n-\n-\tfailures += expect_fail(\"not-an-ip\");\n-\tfailures += expect_fail(\"10.0.0.1/33\");\n-\tfailures += expect_fail(\"fd00::1/129\");\n-\tfailures += expect_fail(\"\");\n-\n-\treturn failures ? 1 : 0;\n-}\ndiff --git a/test/rist/unit/test_url_cbr_output.c b/test/rist/unit/test_url_cbr_output.c\ndeleted file mode 100644\nindex 89d743d..0000000\n--- a/test/rist/unit/test_url_cbr_output.c\n+++ /dev/null\n@@ -1,199 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * ?cbr-output= parsing on output URLs and rist_receiver_set_cbr_output().\n- * Pacing happens in the flow's output loop ahead of the fan-out, so the setting\n- * belongs to the context: whichever route claims it first wins, and a later\n- * disagreement from either route is refused rather than resolved.\n- * Intentionally avoids cmocka so it runs anywhere the public library does. */\n-\n-#include \"librist/librist.h\"\n-#include \"librist/urlparam.h\"\n-#include \"librist/receiver.h\"\n-\n-#include <stdio.h>\n-#include <stdlib.h>\n-#include <string.h>\n-\n-static int expect_out_parsed(const char *url, int want_set, int want_val)\n-{\n-\tstruct rist_udp_config *cfg = NULL;\n-\tint fails = 0;\n-\tif (rist_parse_udp_address2(url, &cfg) != 0 || !cfg) {\n-\t\tfprintf(stderr, \"FAIL: could not parse output url %s\\n\", url);\n-\t\tif (cfg) rist_udp_config_free2(&cfg);\n-\t\treturn 1;\n-\t}\n-\tif (cfg->cbr_output_set != want_set || cfg->cbr_output != want_val) {\n-\t\tfprintf(stderr, \"FAIL: %s -> set=%d val=%d (want %d/%d)\\n\",\n-\t\t        url, cfg->cbr_output_set, cfg->cbr_output, want_set, want_val);\n-\t\tfails++;\n-\t}\n-\trist_udp_config_free2(&cfg);\n-\treturn fails;\n-}\n-\n-/* The output URL is where the parameter belongs, since it governs output. */\n-static int test_output_url_parsing(void)\n-{\n-\tint fails = 0;\n-\tfails += expect_out_parsed(\"udp://127.0.0.1:31100\", 0, 0);\n-\tfails += expect_out_parsed(\"udp://127.0.0.1:31100?cbr-output=1\", 1, 1);\n-\tfails += expect_out_parsed(\"udp://127.0.0.1:31100?cbr-output=0\", 1, 0);\n-\n-\tstruct rist_udp_config *cfg = NULL;\n-\tif (rist_parse_udp_address2(\"udp://127.0.0.1:31100?cbr-output=2\", &cfg) == 0) {\n-\t\tfprintf(stderr, \"FAIL: output url cbr-output=2 accepted\\n\");\n-\t\tfails++;\n-\t}\n-\tif (cfg) rist_udp_config_free2(&cfg);\n-\n-\tif (!fails)\n-\t\tprintf(\"  output urls: 0|1 accepted, absent stays unset, junk rejected\\n\");\n-\treturn fails;\n-}\n-\n-/* What ristreceiver does with each -o url: parse it, and if it stated a value,\n- * hand that to the API. Returns the API result, or -2 if the url did not ask. */\n-static int apply_out_url(struct rist_ctx *ctx, const char *url)\n-{\n-\tstruct rist_udp_config *cfg = NULL;\n-\tif (rist_parse_udp_address2(url, &cfg) != 0 || !cfg) {\n-\t\tfprintf(stderr, \"could not parse output url %s\\n\", url);\n-\t\tif (cfg) rist_udp_config_free2(&cfg);\n-\t\treturn -3;\n-\t}\n-\tint ret = -2;\n-\tif (cfg->version >= 2 && cfg->cbr_output_set)\n-\t\tret = rist_receiver_set_cbr_output(ctx, cfg->cbr_output != 0);\n-\trist_udp_config_free2(&cfg);\n-\treturn ret;\n-}\n-\n-/* Outputs agreeing is fine; the setting is context-wide. */\n-static int test_two_agreeing_output_urls(struct rist_logging_settings *logs)\n-{\n-\tstruct rist_ctx *ctx = NULL;\n-\tif (rist_receiver_create(&ctx, RIST_PROFILE_MAIN, logs) != 0) {\n-\t\tfprintf(stderr, \"agreeing: rist_receiver_create failed\\n\");\n-\t\treturn 1;\n-\t}\n-\tint fails = 0;\n-\tif (apply_out_url(ctx, \"udp://127.0.0.1:31200?cbr-output=1\") != 0 ||\n-\t    apply_out_url(ctx, \"udp://127.0.0.1:31201?cbr-output=1\") != 0) {\n-\t\tfprintf(stderr, \"agreeing: two outputs asking for 1 were not accepted\\n\");\n-\t\tfails++;\n-\t} else {\n-\t\tprintf(\"  two outputs both asking for cbr-output=1 accepted\\n\");\n-\t}\n-\trist_destroy(ctx);\n-\treturn fails;\n-}\n-\n-/* first then second on one context: the second must be refused. */\n-static int test_conflicting_output_urls(struct rist_logging_settings *logs,\n-                                       int first, int second)\n-{\n-\tstruct rist_ctx *ctx = NULL;\n-\tif (rist_receiver_create(&ctx, RIST_PROFILE_MAIN, logs) != 0) {\n-\t\tfprintf(stderr, \"conflict: rist_receiver_create failed\\n\");\n-\t\treturn 1;\n-\t}\n-\tchar u1[96], u2[96];\n-\tsnprintf(u1, sizeof(u1), \"udp://127.0.0.1:31210?cbr-output=%d\", first);\n-\tsnprintf(u2, sizeof(u2), \"udp://127.0.0.1:31211?cbr-output=%d\", second);\n-\n-\tint fails = 0;\n-\tif (apply_out_url(ctx, u1) != 0) {\n-\t\tfprintf(stderr, \"conflict: first output (%s) rejected\\n\", u1);\n-\t\tfails++;\n-\t} else if (apply_out_url(ctx, u2) == 0) {\n-\t\tfprintf(stderr, \"conflict: %s accepted after %s; expected refusal\\n\", u2, u1);\n-\t\tfails++;\n-\t} else {\n-\t\tprintf(\"  output cbr-output=%d after cbr-output=%d refused\\n\", second, first);\n-\t}\n-\trist_destroy(ctx);\n-\treturn fails;\n-}\n-\n-/* The API is the route a run script or provisioning layer uses, and it shares\n- * the latch with the output-URL route. */\n-static int test_api_and_url_share_the_latch(struct rist_logging_settings *logs)\n-{\n-\tstruct rist_ctx *ctx = NULL;\n-\tif (rist_receiver_create(&ctx, RIST_PROFILE_MAIN, logs) != 0) {\n-\t\tfprintf(stderr, \"api: rist_receiver_create failed\\n\");\n-\t\treturn 1;\n-\t}\n-\tint fails = 0;\n-\n-\tif (rist_receiver_set_cbr_output(ctx, true) != 0) {\n-\t\tfprintf(stderr, \"api: first call rejected\\n\");\n-\t\tfails++;\n-\t}\n-\tif (rist_receiver_set_cbr_output(ctx, true) != 0) {\n-\t\tfprintf(stderr, \"api: agreeing repeat rejected\\n\");\n-\t\tfails++;\n-\t}\n-\tif (rist_receiver_set_cbr_output(ctx, false) == 0) {\n-\t\tfprintf(stderr, \"api: disagreeing call accepted; expected refusal\\n\");\n-\t\tfails++;\n-\t}\n-\n-\t/* An output URL disagreeing with the API must lose too. */\n-\tif (apply_out_url(ctx, \"udp://127.0.0.1:31202?cbr-output=0\") == 0) {\n-\t\tfprintf(stderr, \"api: output url disagreeing with the API accepted\\n\");\n-\t\tfails++;\n-\t}\n-\n-\tif (!fails)\n-\t\tprintf(\"  api latches, agrees with itself, and refuses a conflicting url\\n\");\n-\trist_destroy(ctx);\n-\treturn fails;\n-}\n-\n-/* A sender context has no output loop to pace, so the call must not succeed. */\n-static int test_api_rejects_a_sender(struct rist_logging_settings *logs)\n-{\n-\tstruct rist_ctx *ctx = NULL;\n-\tif (rist_sender_create(&ctx, RIST_PROFILE_MAIN, 0, logs) != 0) {\n-\t\tfprintf(stderr, \"sender: rist_sender_create failed\\n\");\n-\t\treturn 1;\n-\t}\n-\tint fails = 0;\n-\tif (rist_receiver_set_cbr_output(ctx, true) == 0) {\n-\t\tfprintf(stderr, \"sender: accepted on a sender context\\n\");\n-\t\tfails++;\n-\t} else {\n-\t\tprintf(\"  api refused on a sender context\\n\");\n-\t}\n-\trist_destroy(ctx);\n-\treturn fails;\n-}\n-\n-int main(void)\n-{\n-\tstruct rist_logging_settings *logs = NULL;\n-\tif (rist_logging_set(&logs, RIST_LOG_ERROR, NULL, NULL, NULL, stderr) != 0) {\n-\t\tfprintf(stderr, \"rist_logging_set failed\\n\");\n-\t\treturn 1;\n-\t}\n-\n-\tprintf(\"?cbr-output= parsing and override semantics\\n\");\n-\tint fails = 0;\n-\tfails += test_output_url_parsing();\n-\tfails += test_api_and_url_share_the_latch(logs);\n-\tfails += test_api_rejects_a_sender(logs);\n-\tfails += test_two_agreeing_output_urls(logs);\n-\tfails += test_conflicting_output_urls(logs, 1, 0);\n-\t/* 0 equals the default, so it must still claim the setting. */\n-\tfails += test_conflicting_output_urls(logs, 0, 1);\n-\n-\tfree(logs);\n-\tif (fails) {\n-\t\tfprintf(stderr, \"%d failure(s)\\n\", fails);\n-\t\treturn 1;\n-\t}\n-\tprintf(\"all cbr-output invariants held\\n\");\n-\treturn 0;\n-}\ndiff --git a/test/rist/unit/test_url_profile_override.c b/test/rist/unit/test_url_profile_override.c\ndeleted file mode 100644\nindex c02a8c9..0000000\n--- a/test/rist/unit/test_url_profile_override.c\n+++ /dev/null\n@@ -1,176 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Public-API tests for the ?profile= URL override in rist_peer_create. */\n-\n-#include \"librist/librist.h\"\n-#include \"librist/peer.h\"\n-#include \"librist/receiver.h\"\n-#include \"librist/logging.h\"\n-#include \"librist/urlparam.h\"\n-\n-#include <stdio.h>\n-#include <stdlib.h>\n-#include <string.h>\n-\n-static int parse_cfg(const char *url, struct rist_peer_config **cfg)\n-{\n-\t*cfg = NULL;\n-\tint ret = rist_parse_address2(url, cfg);\n-\tif (ret != 0 || *cfg == NULL) {\n-\t\tfprintf(stderr, \"parse_cfg(%s) ret=%d cfg=%p\\n\",\n-\t\t        url, ret, (void *)*cfg);\n-\t\treturn -1;\n-\t}\n-\treturn 0;\n-}\n-\n-static int test_override_then_match(struct rist_logging_settings *logs)\n-{\n-\tstruct rist_ctx *ctx = NULL;\n-\tif (rist_receiver_create(&ctx, RIST_PROFILE_MAIN, logs) != 0) {\n-\t\tfprintf(stderr, \"test1: rist_receiver_create failed\\n\");\n-\t\treturn 1;\n-\t}\n-\n-\tstruct rist_peer_config *cfg1 = NULL, *cfg2 = NULL;\n-\tstruct rist_peer *p1 = NULL, *p2 = NULL;\n-\tint fails = 0;\n-\n-\tif (parse_cfg(\"rist://@127.0.0.1:30100?profile=2\", &cfg1) < 0) {\n-\t\tfails++;\n-\t\tgoto cleanup;\n-\t}\n-\tif (rist_peer_create(ctx, &p1, cfg1) != 0) {\n-\t\tfprintf(stderr,\n-\t\t        \"test1: first peer (?profile=2) rejected; override should have applied\\n\");\n-\t\tfails++;\n-\t\tgoto cleanup;\n-\t}\n-\n-\tif (parse_cfg(\"rist://@127.0.0.1:30102?profile=2\", &cfg2) < 0) {\n-\t\tfails++;\n-\t\tgoto cleanup;\n-\t}\n-\tif (rist_peer_create(ctx, &p2, cfg2) != 0) {\n-\t\tfprintf(stderr,\n-\t\t        \"test1: second peer with matching ?profile=2 rejected\\n\");\n-\t\tfails++;\n-\t}\n-\n-cleanup:\n-\tif (cfg1) rist_peer_config_free2(&cfg1);\n-\tif (cfg2) rist_peer_config_free2(&cfg2);\n-\trist_destroy(ctx);\n-\treturn fails;\n-}\n-\n-static int test_conflict_refused(struct rist_logging_settings *logs)\n-{\n-\tstruct rist_ctx *ctx = NULL;\n-\tif (rist_receiver_create(&ctx, RIST_PROFILE_MAIN, logs) != 0) {\n-\t\tfprintf(stderr, \"test2: rist_receiver_create failed\\n\");\n-\t\treturn 1;\n-\t}\n-\n-\tstruct rist_peer_config *cfg1 = NULL, *cfg2 = NULL;\n-\tstruct rist_peer *p1 = NULL, *p2 = NULL;\n-\tint fails = 0;\n-\n-\tif (parse_cfg(\"rist://@127.0.0.1:30110?profile=2\", &cfg1) < 0) {\n-\t\tfails++;\n-\t\tgoto cleanup;\n-\t}\n-\tif (rist_peer_create(ctx, &p1, cfg1) != 0) {\n-\t\tfprintf(stderr, \"test2: first peer rejected\\n\");\n-\t\tfails++;\n-\t\tgoto cleanup;\n-\t}\n-\n-\tif (parse_cfg(\"rist://@127.0.0.1:30112?profile=1\", &cfg2) < 0) {\n-\t\tfails++;\n-\t\tgoto cleanup;\n-\t}\n-\tint ret = rist_peer_create(ctx, &p2, cfg2);\n-\tif (ret != -1) {\n-\t\tfprintf(stderr,\n-\t\t        \"test2: conflicting ?profile=1 peer returned %d; expected -1\\n\",\n-\t\t        ret);\n-\t\tfails++;\n-\t}\n-\n-cleanup:\n-\tif (cfg1) rist_peer_config_free2(&cfg1);\n-\tif (cfg2) rist_peer_config_free2(&cfg2);\n-\trist_destroy(ctx);\n-\treturn fails;\n-}\n-\n-static int test_late_peer_refused(struct rist_logging_settings *logs)\n-{\n-\tstruct rist_ctx *ctx = NULL;\n-\tif (rist_receiver_create(&ctx, RIST_PROFILE_MAIN, logs) != 0) {\n-\t\tfprintf(stderr, \"test3: rist_receiver_create failed\\n\");\n-\t\treturn 1;\n-\t}\n-\n-\tstruct rist_peer_config *cfg_pre = NULL, *cfg_late = NULL;\n-\tstruct rist_peer *p_pre = NULL, *p_late = NULL;\n-\tint fails = 0;\n-\n-\tif (parse_cfg(\"rist://@127.0.0.1:30120\", &cfg_pre) < 0) {\n-\t\tfails++;\n-\t\tgoto cleanup;\n-\t}\n-\tif (rist_peer_create(ctx, &p_pre, cfg_pre) != 0) {\n-\t\tfprintf(stderr, \"test3: pre-start peer rejected\\n\");\n-\t\tfails++;\n-\t\tgoto cleanup;\n-\t}\n-\tif (rist_start(ctx) != 0) {\n-\t\tfprintf(stderr, \"test3: rist_start failed\\n\");\n-\t\tfails++;\n-\t\tgoto cleanup;\n-\t}\n-\n-\tif (parse_cfg(\"rist://@127.0.0.1:30122?profile=2\", &cfg_late) < 0) {\n-\t\tfails++;\n-\t\tgoto cleanup;\n-\t}\n-\tint ret = rist_peer_create(ctx, &p_late, cfg_late);\n-\tif (ret != -1) {\n-\t\tfprintf(stderr,\n-\t\t        \"test3: late peer with ?profile=2 returned %d; expected -1\\n\",\n-\t\t        ret);\n-\t\tfails++;\n-\t}\n-\n-cleanup:\n-\tif (cfg_pre) rist_peer_config_free2(&cfg_pre);\n-\tif (cfg_late) rist_peer_config_free2(&cfg_late);\n-\trist_destroy(ctx);\n-\treturn fails;\n-}\n-\n-int main(void)\n-{\n-\tstruct rist_logging_settings *logs = NULL;\n-\tif (rist_logging_set(&logs, RIST_LOG_WARN, NULL, NULL, NULL,\n-\t                     stderr) != 0) {\n-\t\tfprintf(stderr, \"rist_logging_set failed\\n\");\n-\t\treturn 1;\n-\t}\n-\n-\tint fails = 0;\n-\tfails += test_override_then_match(logs);\n-\tfails += test_conflict_refused(logs);\n-\tfails += test_late_peer_refused(logs);\n-\n-\trist_logging_settings_free2(&logs);\n-\n-\tif (fails == 0) {\n-\t\tprintf(\"OK\\n\");\n-\t\treturn 0;\n-\t}\n-\tfprintf(stderr, \"Total failures: %d\\n\", fails);\n-\treturn 1;\n-}\ndiff --git a/test/rist/unit/test_url_profile_parse.c b/test/rist/unit/test_url_profile_parse.c\ndeleted file mode 100644\nindex ed82387..0000000\n--- a/test/rist/unit/test_url_profile_parse.c\n+++ /dev/null\n@@ -1,108 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Intentionally avoids cmocka so it runs anywhere the public library\n- * does. */\n-\n-#include \"librist/librist.h\"\n-#include \"librist/peer.h\"\n-\n-#include <stdio.h>\n-#include <stdlib.h>\n-#include <string.h>\n-\n-static int expect_profile(const char *url,\n-                          enum rist_profile want_profile,\n-                          int want_set)\n-{\n-\tstruct rist_peer_config *cfg = NULL;\n-\tint ret = rist_parse_address2(url, &cfg);\n-\tif (ret != 0 || cfg == NULL) {\n-\t\tfprintf(stderr,\n-\t\t        \"FAIL: rist_parse_address2(%s) ret=%d cfg=%p\\n\",\n-\t\t        url, ret, (void *)cfg);\n-\t\tif (cfg)\n-\t\t\trist_peer_config_free2(&cfg);\n-\t\treturn 1;\n-\t}\n-\tif ((int)cfg->profile != (int)want_profile ||\n-\t    cfg->profile_set != want_set) {\n-\t\tfprintf(stderr,\n-\t\t        \"FAIL: %s -> profile=%d profile_set=%d \"\n-\t\t        \"(want profile=%d profile_set=%d)\\n\",\n-\t\t        url, (int)cfg->profile, cfg->profile_set,\n-\t\t        (int)want_profile, want_set);\n-\t\trist_peer_config_free2(&cfg);\n-\t\treturn 1;\n-\t}\n-\trist_peer_config_free2(&cfg);\n-\treturn 0;\n-}\n-\n-static int expect_parse_error(const char *url)\n-{\n-\tstruct rist_peer_config *cfg = NULL;\n-\tint ret = rist_parse_address2(url, &cfg);\n-\tif (ret == 0) {\n-\t\tfprintf(stderr,\n-\t\t        \"FAIL: %s expected parse error, got success\\n\", url);\n-\t\tif (cfg)\n-\t\t\trist_peer_config_free2(&cfg);\n-\t\treturn 1;\n-\t}\n-\tif (cfg)\n-\t\trist_peer_config_free2(&cfg);\n-\treturn 0;\n-}\n-\n-int main(void)\n-{\n-\tint failures = 0;\n-\n-\tfailures += expect_profile(\n-\t\t\"rist://@127.0.0.1:1234\",\n-\t\tRIST_DEFAULT_PROFILE, 0);\n-\n-\tfailures += expect_profile(\n-\t\t\"rist://@127.0.0.1:1234?profile=0\",\n-\t\tRIST_PROFILE_SIMPLE, 1);\n-\tfailures += expect_profile(\n-\t\t\"rist://@127.0.0.1:1234?profile=1\",\n-\t\tRIST_PROFILE_MAIN, 1);\n-\tfailures += expect_profile(\n-\t\t\"rist://@127.0.0.1:1234?profile=2\",\n-\t\tRIST_PROFILE_ADVANCED, 1);\n-\n-\tfailures += expect_profile(\n-\t\t\"rist://@127.0.0.1:1234?profile=2&buffer=100\",\n-\t\tRIST_PROFILE_ADVANCED, 1);\n-\tfailures += expect_profile(\n-\t\t\"rist://@127.0.0.1:1234?buffer=100&profile=2\",\n-\t\tRIST_PROFILE_ADVANCED, 1);\n-\n-\tfailures += expect_parse_error(\n-\t\t\"rist://@127.0.0.1:1234?profile=3\");\n-\tfailures += expect_parse_error(\n-\t\t\"rist://@127.0.0.1:1234?profile=garbage\");\n-\tfailures += expect_parse_error(\n-\t\t\"rist://@127.0.0.1:1234?profile=simple\");\n-\tfailures += expect_parse_error(\n-\t\t\"rist://@127.0.0.1:1234?profile=main\");\n-\tfailures += expect_parse_error(\n-\t\t\"rist://@127.0.0.1:1234?profile=advanced\");\n-\tfailures += expect_parse_error(\n-\t\t\"rist://@127.0.0.1:1234?profile=2foo\");\n-\tfailures += expect_parse_error(\n-\t\t\"rist://@127.0.0.1:1234?profile=-1\");\n-\n-\t/* Empty value: upstream parser yields val==NULL, treated as absent. */\n-\tfailures += expect_profile(\n-\t\t\"rist://@127.0.0.1:1234?profile=\",\n-\t\tRIST_DEFAULT_PROFILE, 0);\n-\n-\tif (failures == 0) {\n-\t\tprintf(\"OK\\n\");\n-\t\treturn 0;\n-\t}\n-\tfprintf(stderr, \"Total failures: %d\\n\", failures);\n-\treturn 1;\n-}\ndiff --git a/test/rist/unit/test_url_recovery_depth_parse.c b/test/rist/unit/test_url_recovery_depth_parse.c\ndeleted file mode 100644\nindex cfffd6b..0000000\n--- a/test/rist/unit/test_url_recovery_depth_parse.c\n+++ /dev/null\n@@ -1,84 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * ?recovery-depth= URL parsing exercised through the public\n- * rist_parse_address2 API.  recovery-depth is a numeric exponent\n- * (RIST_RECOVERY_DEPTH_MIN..MAX); the ring holds 65536 << depth packets.\n- * Intentionally avoids cmocka so it runs anywhere the public library does. */\n-\n-#include \"librist/librist.h\"\n-#include \"librist/peer.h\"\n-\n-#include <stdio.h>\n-#include <stdlib.h>\n-#include <string.h>\n-\n-static int expect_depth(const char *url, unsigned want)\n-{\n-\tstruct rist_peer_config *cfg = NULL;\n-\tint ret = rist_parse_address2(url, &cfg);\n-\tif (ret != 0 || cfg == NULL) {\n-\t\tfprintf(stderr, \"FAIL: rist_parse_address2(%s) ret=%d cfg=%p\\n\",\n-\t\t        url, ret, (void *)cfg);\n-\t\tif (cfg)\n-\t\t\trist_peer_config_free2(&cfg);\n-\t\treturn 1;\n-\t}\n-\tif (cfg->recovery_depth != want) {\n-\t\tfprintf(stderr, \"FAIL: %s -> recovery_depth=%u (want %u)\\n\",\n-\t\t        url, (unsigned)cfg->recovery_depth, want);\n-\t\trist_peer_config_free2(&cfg);\n-\t\treturn 1;\n-\t}\n-\trist_peer_config_free2(&cfg);\n-\treturn 0;\n-}\n-\n-/* An out-of-range / non-numeric token must be flagged (non-zero return) per\n- * the parse_url_options contract, and must not corrupt the default value. */\n-static int expect_parse_error(const char *url)\n-{\n-\tstruct rist_peer_config *cfg = NULL;\n-\tint ret = rist_parse_address2(url, &cfg);\n-\tint bad = 0;\n-\tif (ret == 0) {\n-\t\tfprintf(stderr, \"FAIL: %s expected parse error, got success\\n\", url);\n-\t\tbad = 1;\n-\t} else if (cfg != NULL && cfg->recovery_depth != RIST_RECOVERY_DEPTH_DEFAULT) {\n-\t\tfprintf(stderr, \"FAIL: %s left recovery_depth=%u (want default %d)\\n\",\n-\t\t        url, (unsigned)cfg->recovery_depth, RIST_RECOVERY_DEPTH_DEFAULT);\n-\t\tbad = 1;\n-\t}\n-\tif (cfg)\n-\t\trist_peer_config_free2(&cfg);\n-\treturn bad;\n-}\n-\n-int main(void)\n-{\n-\tint failures = 0;\n-\n-\t/* Absent parameter leaves the library default. */\n-\tfailures += expect_depth(\"rist://@127.0.0.1:1234\", RIST_RECOVERY_DEPTH_DEFAULT);\n-\n-\t/* Boundary and representative interior values. */\n-\tfailures += expect_depth(\"rist://@127.0.0.1:1234?recovery-depth=0\", 0);\n-\tfailures += expect_depth(\"rist://@127.0.0.1:1234?recovery-depth=3\", 3);\n-\tfailures += expect_depth(\"rist://@127.0.0.1:1234?recovery-depth=6\", 6);\n-\tfailures += expect_depth(\"rist://@127.0.0.1:1234?recovery-depth=16\", 16);\n-\n-\t/* Coexists with other parameters in either order. */\n-\tfailures += expect_depth(\"rist://@127.0.0.1:1234?buffer=100&recovery-depth=5\", 5);\n-\tfailures += expect_depth(\"rist://@127.0.0.1:1234?recovery-depth=8&bandwidth=8000\", 8);\n-\n-\t/* Out-of-range and non-numeric are rejected and leave the default. */\n-\tfailures += expect_parse_error(\"rist://@127.0.0.1:1234?recovery-depth=17\");\n-\tfailures += expect_parse_error(\"rist://@127.0.0.1:1234?recovery-depth=-1\");\n-\tfailures += expect_parse_error(\"rist://@127.0.0.1:1234?recovery-depth=large\");\n-\n-\tif (failures == 0) {\n-\t\tprintf(\"OK\\n\");\n-\t\treturn 0;\n-\t}\n-\tfprintf(stderr, \"Total failures: %d\\n\", failures);\n-\treturn 1;\n-}\ndiff --git a/test/rist/unit/test_url_recovery_priority_parse.c b/test/rist/unit/test_url_recovery_priority_parse.c\ndeleted file mode 100644\nindex 205977b..0000000\n--- a/test/rist/unit/test_url_recovery_priority_parse.c\n+++ /dev/null\n@@ -1,70 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * ?recovery-priority= URL parsing exercised through the public\n- * rist_parse_address2 API.  Intentionally avoids cmocka so it runs\n- * anywhere the public library does. */\n-\n-#include \"librist/librist.h\"\n-#include \"librist/peer.h\"\n-\n-#include <stdio.h>\n-#include <stdlib.h>\n-#include <string.h>\n-\n-static int expect_priority(const char *url, uint32_t want)\n-{\n-\tstruct rist_peer_config *cfg = NULL;\n-\tint ret = rist_parse_address2(url, &cfg);\n-\tif (ret != 0 || cfg == NULL) {\n-\t\tfprintf(stderr,\n-\t\t        \"FAIL: rist_parse_address2(%s) ret=%d cfg=%p\\n\",\n-\t\t        url, ret, (void *)cfg);\n-\t\tif (cfg)\n-\t\t\trist_peer_config_free2(&cfg);\n-\t\treturn 1;\n-\t}\n-\tif (cfg->recovery_priority != want) {\n-\t\tfprintf(stderr,\n-\t\t        \"FAIL: %s -> recovery_priority=%u (want %u)\\n\",\n-\t\t        url, cfg->recovery_priority, want);\n-\t\trist_peer_config_free2(&cfg);\n-\t\treturn 1;\n-\t}\n-\trist_peer_config_free2(&cfg);\n-\treturn 0;\n-}\n-\n-int main(void)\n-{\n-\tint failures = 0;\n-\n-\t/* Absent parameter keeps the default. */\n-\tfailures += expect_priority(\n-\t\t\"rist://@127.0.0.1:1234\",\n-\t\tRIST_DEFAULT_RECOVERY_PRIORITY);\n-\n-\tfailures += expect_priority(\n-\t\t\"rist://@127.0.0.1:1234?recovery-priority=0\", 0);\n-\tfailures += expect_priority(\n-\t\t\"rist://@127.0.0.1:1234?recovery-priority=1\", 1);\n-\tfailures += expect_priority(\n-\t\t\"rist://@127.0.0.1:1234?recovery-priority=1000\", 1000);\n-\n-\t/* Coexists with other parameters in either order. */\n-\tfailures += expect_priority(\n-\t\t\"rist://@127.0.0.1:1234?weight=1000&recovery-priority=5\", 5);\n-\tfailures += expect_priority(\n-\t\t\"rist://@127.0.0.1:1234?recovery-priority=5&buffer=100\", 5);\n-\n-\t/* Negative is rejected by the >=0 guard and leaves the default. */\n-\tfailures += expect_priority(\n-\t\t\"rist://@127.0.0.1:1234?recovery-priority=-1\",\n-\t\tRIST_DEFAULT_RECOVERY_PRIORITY);\n-\n-\tif (failures == 0) {\n-\t\tprintf(\"OK\\n\");\n-\t\treturn 0;\n-\t}\n-\tfprintf(stderr, \"Total failures: %d\\n\", failures);\n-\treturn 1;\n-}\ndiff --git a/test/rist/unit/test_url_rtt_drop_parse.c b/test/rist/unit/test_url_rtt_drop_parse.c\ndeleted file mode 100644\nindex 66ff10d..0000000\n--- a/test/rist/unit/test_url_rtt_drop_parse.c\n+++ /dev/null\n@@ -1,84 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * ?rtt-drop= / ?rtt-restore= / ?rtt-drop-settle= / ?rtt-drop-trickle= URL\n- * parsing exercised through the public rist_parse_address2 API.  Intentionally\n- * avoids cmocka so it runs anywhere the public library does. */\n-\n-#include \"librist/librist.h\"\n-#include \"librist/peer.h\"\n-\n-#include <stdio.h>\n-#include <stdlib.h>\n-#include <string.h>\n-\n-struct want {\n-\tuint32_t drop;\n-\tuint32_t restore;\n-\tuint32_t settle;\n-\tuint32_t trickle;\n-};\n-\n-static int expect_cfg(const char *url, struct want w)\n-{\n-\tstruct rist_peer_config *cfg = NULL;\n-\tint ret = rist_parse_address2(url, &cfg);\n-\tif (ret != 0 || cfg == NULL) {\n-\t\tfprintf(stderr, \"FAIL: rist_parse_address2(%s) ret=%d cfg=%p\\n\",\n-\t\t        url, ret, (void *)cfg);\n-\t\tif (cfg)\n-\t\t\trist_peer_config_free2(&cfg);\n-\t\treturn 1;\n-\t}\n-\tint bad = cfg->rtt_drop != w.drop || cfg->rtt_restore != w.restore\n-\t\t|| cfg->rtt_drop_settle != w.settle || cfg->rtt_drop_trickle != w.trickle;\n-\tif (bad) {\n-\t\tfprintf(stderr,\n-\t\t        \"FAIL: %s -> drop=%u restore=%u settle=%u trickle=%u \"\n-\t\t        \"(want %u/%u/%u/%u)\\n\",\n-\t\t        url, cfg->rtt_drop, cfg->rtt_restore, cfg->rtt_drop_settle,\n-\t\t        cfg->rtt_drop_trickle, w.drop, w.restore, w.settle, w.trickle);\n-\t}\n-\trist_peer_config_free2(&cfg);\n-\treturn bad ? 1 : 0;\n-}\n-\n-int main(void)\n-{\n-\tint failures = 0;\n-\n-\t/* Absent parameters keep the versioned defaults (feature disabled). */\n-\tfailures += expect_cfg(\"rist://@127.0.0.1:1234\",\n-\t\t(struct want){ RIST_DEFAULT_RTT_DROP, RIST_DEFAULT_RTT_RESTORE,\n-\t\t               RIST_DEFAULT_RTT_DROP_SETTLE, RIST_DEFAULT_RTT_DROP_TRICKLE });\n-\n-\t/* Each knob parses independently, leaving the rest at their defaults. */\n-\tfailures += expect_cfg(\"rist://@127.0.0.1:1234?rtt-drop=500\",\n-\t\t(struct want){ 500, RIST_DEFAULT_RTT_RESTORE,\n-\t\t               RIST_DEFAULT_RTT_DROP_SETTLE, RIST_DEFAULT_RTT_DROP_TRICKLE });\n-\tfailures += expect_cfg(\"rist://@127.0.0.1:1234?rtt-restore=300\",\n-\t\t(struct want){ RIST_DEFAULT_RTT_DROP, 300,\n-\t\t               RIST_DEFAULT_RTT_DROP_SETTLE, RIST_DEFAULT_RTT_DROP_TRICKLE });\n-\tfailures += expect_cfg(\"rist://@127.0.0.1:1234?rtt-drop-settle=5000\",\n-\t\t(struct want){ RIST_DEFAULT_RTT_DROP, RIST_DEFAULT_RTT_RESTORE,\n-\t\t               5000, RIST_DEFAULT_RTT_DROP_TRICKLE });\n-\tfailures += expect_cfg(\"rist://@127.0.0.1:1234?rtt-drop-trickle=50\",\n-\t\t(struct want){ RIST_DEFAULT_RTT_DROP, RIST_DEFAULT_RTT_RESTORE,\n-\t\t               RIST_DEFAULT_RTT_DROP_SETTLE, 50 });\n-\n-\t/* All four together, order-independent, coexisting with other params. */\n-\tfailures += expect_cfg(\n-\t\t\"rist://@127.0.0.1:1234?weight=5&rtt-drop=500&rtt-restore=350&rtt-drop-settle=2000&rtt-drop-trickle=40\",\n-\t\t(struct want){ 500, 350, 2000, 40 });\n-\n-\t/* Negative values are rejected by the >=0 guards and keep the defaults. */\n-\tfailures += expect_cfg(\"rist://@127.0.0.1:1234?rtt-drop=-1\",\n-\t\t(struct want){ RIST_DEFAULT_RTT_DROP, RIST_DEFAULT_RTT_RESTORE,\n-\t\t               RIST_DEFAULT_RTT_DROP_SETTLE, RIST_DEFAULT_RTT_DROP_TRICKLE });\n-\n-\tif (failures == 0) {\n-\t\tprintf(\"OK\\n\");\n-\t\treturn 0;\n-\t}\n-\tfprintf(stderr, \"Total failures: %d\\n\", failures);\n-\treturn 1;\n-}\ndiff --git a/test/rist/unit/test_url_srp_compat_parse.c b/test/rist/unit/test_url_srp_compat_parse.c\ndeleted file mode 100644\nindex f06ea15..0000000\n--- a/test/rist/unit/test_url_srp_compat_parse.c\n+++ /dev/null\n@@ -1,73 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * Intentionally avoids cmocka so it runs anywhere the public library\n- * does. */\n-\n-#include \"librist/librist.h\"\n-#include \"librist/peer.h\"\n-\n-#include <stdio.h>\n-#include <stdlib.h>\n-#include <string.h>\n-\n-static int expect_legacy(const char *url, int want)\n-{\n-\tstruct rist_peer_config *cfg = NULL;\n-\tint ret = rist_parse_address2(url, &cfg);\n-\tif (ret != 0 || cfg == NULL) {\n-\t\tfprintf(stderr,\n-\t\t        \"FAIL: rist_parse_address2(%s) ret=%d cfg=%p\\n\",\n-\t\t        url, ret, (void *)cfg);\n-\t\tif (cfg)\n-\t\t\trist_peer_config_free2(&cfg);\n-\t\treturn 1;\n-\t}\n-\tif (cfg->srp_compat_legacy != want) {\n-\t\tfprintf(stderr,\n-\t\t        \"FAIL: %s -> srp_compat_legacy=%d (want %d)\\n\",\n-\t\t        url, cfg->srp_compat_legacy, want);\n-\t\trist_peer_config_free2(&cfg);\n-\t\treturn 1;\n-\t}\n-\trist_peer_config_free2(&cfg);\n-\treturn 0;\n-}\n-\n-static int expect_parse_error(const char *url)\n-{\n-\tstruct rist_peer_config *cfg = NULL;\n-\tint ret = rist_parse_address2(url, &cfg);\n-\tif (ret == 0) {\n-\t\tfprintf(stderr,\n-\t\t        \"FAIL: %s expected parse error, got success\\n\", url);\n-\t\tif (cfg)\n-\t\t\trist_peer_config_free2(&cfg);\n-\t\treturn 1;\n-\t}\n-\tif (cfg)\n-\t\trist_peer_config_free2(&cfg);\n-\treturn 0;\n-}\n-\n-int main(void)\n-{\n-\tint failures = 0;\n-\n-\tfailures += expect_legacy(\"rist://@127.0.0.1:1234\", 0);\n-\tfailures += expect_legacy(\"rist://@127.0.0.1:1234?srp-compat=0\", 0);\n-\tfailures += expect_legacy(\"rist://@127.0.0.1:1234?srp-compat=1\", 1);\n-\n-\tfailures += expect_parse_error(\"rist://@127.0.0.1:1234?srp-compat=legacy\");\n-\tfailures += expect_parse_error(\"rist://@127.0.0.1:1234?srp-compat=auto\");\n-\tfailures += expect_parse_error(\"rist://@127.0.0.1:1234?srp-compat=2\");\n-\tfailures += expect_parse_error(\"rist://@127.0.0.1:1234?srp-compat=-1\");\n-\tfailures += expect_parse_error(\"rist://@127.0.0.1:1234?srp-compat=1foo\");\n-\tfailures += expect_parse_error(\"rist://@127.0.0.1:1234?srp-compat=garbage\");\n-\n-\tif (failures == 0) {\n-\t\tprintf(\"OK\\n\");\n-\t\treturn 0;\n-\t}\n-\tfprintf(stderr, \"Total failures: %d\\n\", failures);\n-\treturn 1;\n-}\ndiff --git a/test/rist/unit/test_url_udp_prefix_parse.c b/test/rist/unit/test_url_udp_prefix_parse.c\ndeleted file mode 100644\nindex 43aa048..0000000\n--- a/test/rist/unit/test_url_udp_prefix_parse.c\n+++ /dev/null\n@@ -1,75 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * udp:// scheme-prefix parsing exercised through the public\n- * rist_parse_udp_address2 API.  The prefix[] field is 16 bytes; the parser\n- * must not underflow its copy length on a leading '/' nor write the NUL\n- * terminator past the buffer on an over-long scheme.  Runs under ASan to\n- * catch the out-of-bounds write directly.  No cmocka: links the public API\n- * only, so it runs anywhere the library does. */\n-\n-#include \"librist/librist.h\"\n-\n-#include <stdio.h>\n-#include <string.h>\n-\n-static int expect_prefix(const char *url, const char *want_prefix, int want_rtp)\n-{\n-\tstruct rist_udp_config *cfg = NULL;\n-\tint ret = rist_parse_udp_address2(url, &cfg);\n-\tif (ret != 0 || cfg == NULL) {\n-\t\tfprintf(stderr, \"FAIL: rist_parse_udp_address2(%s) ret=%d cfg=%p\\n\",\n-\t\t        url, ret, (void *)cfg);\n-\t\tif (cfg)\n-\t\t\trist_udp_config_free2(&cfg);\n-\t\treturn 1;\n-\t}\n-\tint bad = 0;\n-\tif (strcmp(cfg->prefix, want_prefix) != 0) {\n-\t\tfprintf(stderr, \"FAIL: %s -> prefix=\\\"%s\\\" (want \\\"%s\\\")\\n\",\n-\t\t        url, cfg->prefix, want_prefix);\n-\t\tbad = 1;\n-\t}\n-\tif (cfg->rtp != want_rtp) {\n-\t\tfprintf(stderr, \"FAIL: %s -> rtp=%d (want %d)\\n\", url, cfg->rtp, want_rtp);\n-\t\tbad = 1;\n-\t}\n-\trist_udp_config_free2(&cfg);\n-\treturn bad;\n-}\n-\n-/* Must not crash or corrupt memory; we don't assert on the exact prefix here,\n- * only that the call returns without an out-of-bounds write (ASan gate). */\n-static int expect_no_crash(const char *url)\n-{\n-\tstruct rist_udp_config *cfg = NULL;\n-\tint ret = rist_parse_udp_address2(url, &cfg);\n-\t(void)ret;\n-\tif (cfg)\n-\t\trist_udp_config_free2(&cfg);\n-\treturn 0;\n-}\n-\n-int main(void)\n-{\n-\tint failures = 0;\n-\n-\t/* Normal schemes. */\n-\tfailures += expect_prefix(\"udp://127.0.0.1:1234\", \"udp\", 0);\n-\tfailures += expect_prefix(\"rtp://127.0.0.1:1234\", \"rtp\", 1);\n-\n-\t/* Leading '/': prefix_len is 0. The old code passed prefix_len-1 (SIZE_MAX)\n-\t   as the strncpy length and NUL-padded gigabytes over the 16-byte buffer. */\n-\tfailures += expect_no_crash(\"/127.0.0.1:1234\");\n-\n-\t/* Scheme longer than the 16-byte prefix buffer: the old code wrote the NUL\n-\t   terminator at prefix[prefix_len], out of bounds. */\n-\tfailures += expect_no_crash(\"averylongschemename://host:1234\");\n-\tfailures += expect_no_crash(\"0123456789abcdef0123456789/host\");\n-\n-\tif (failures) {\n-\t\tfprintf(stderr, \"%d failure(s)\\n\", failures);\n-\t\treturn 1;\n-\t}\n-\tprintf(\"all udp prefix parse cases passed\\n\");\n-\treturn 0;\n-}\ndiff --git a/test/rist/unit/test_url_udp_versioned_params.c b/test/rist/unit/test_url_udp_versioned_params.c\ndeleted file mode 100644\nindex 8092a61..0000000\n--- a/test/rist/unit/test_url_udp_versioned_params.c\n+++ /dev/null\n@@ -1,74 +0,0 @@\n-/* librist. SPDX-License-Identifier: BSD-2-Clause\n- *\n- * ?multiplex-mode= / ?rtp-ptype= URL parsing exercised through the public\n- * rist_parse_udp_address2 API.  Both are guarded on RIST_UDP_CONFIG_VERSION,\n- * so every case parses at whatever version the library was built with and a\n- * bump cannot leave them unreachable.  Intentionally avoids cmocka so it runs\n- * anywhere the public library does. */\n-\n-#include \"librist/librist.h\"\n-#include \"librist/headers.h\"\n-\n-#include <stdio.h>\n-\n-/* A want of -1 leaves that field unchecked.  Returns the failure count. */\n-static int expect_url(const char *url, int want_mode, int want_ptype)\n-{\n-\tstruct rist_udp_config *cfg = NULL;\n-\tint ret = rist_parse_udp_address2(url, &cfg);\n-\n-\tif (ret != 0 || cfg == NULL) {\n-\t\tfprintf(stderr, \"FAIL: %s rejected (ret=%d) at version %d\\n\",\n-\t\t        url, ret, RIST_UDP_CONFIG_VERSION);\n-\t\tif (cfg)\n-\t\t\trist_udp_config_free2(&cfg);\n-\t\treturn 1;\n-\t}\n-\n-\tint bad = 0;\n-\tif (want_mode >= 0 && (int)cfg->multiplex_mode != want_mode) {\n-\t\tfprintf(stderr, \"FAIL: %s -> multiplex_mode=%d (want %d)\\n\",\n-\t\t        url, (int)cfg->multiplex_mode, want_mode);\n-\t\tbad = 1;\n-\t}\n-\tif (want_ptype >= 0 && (int)cfg->rtp_ptype != want_ptype) {\n-\t\tfprintf(stderr, \"FAIL: %s -> rtp_ptype=%d (want %d)\\n\",\n-\t\t        url, (int)cfg->rtp_ptype, want_ptype);\n-\t\tbad = 1;\n-\t}\n-\trist_udp_config_free2(&cfg);\n-\treturn bad;\n-}\n-\n-int main(void)\n-{\n-\tint failures = 0;\n-\n-\t/* The version the parser stamps on a fresh config has to be one the\n-\t * gates below accept, or every guarded parameter is unreachable. */\n-\tif (RIST_UDP_CONFIG_VERSION < 1) {\n-\t\tfprintf(stderr, \"FAIL: RIST_UDP_CONFIG_VERSION=%d\\n\",\n-\t\t        RIST_UDP_CONFIG_VERSION);\n-\t\treturn 1;\n-\t}\n-\n-\tfailures += expect_url(\"udp://127.0.0.1:1234?multiplex-mode=0\", 0, -1);\n-\tfailures += expect_url(\"udp://127.0.0.1:1234?multiplex-mode=1\", 1, -1);\n-\tfailures += expect_url(\"udp://127.0.0.1:1234?multiplex-mode=2\", 2, -1);\n-\tfailures += expect_url(\"udp://127.0.0.1:1234?rtp-ptype=33\", -1, 33);\n-\n-\t/* Alongside unguarded parameters, in either order. */\n-\tfailures += expect_url(\"udp://224.0.0.1:31001?stream-id=1968&multiplex-mode=1\", 1, -1);\n-\tfailures += expect_url(\"udp://224.0.0.1:31001?multiplex-mode=1&stream-id=1968\", 1, -1);\n-\tfailures += expect_url(\"udp://224.0.0.1:31001?stream-id=1968&multiplex-mode=1&rtp-ptype=33\", 1, 33);\n-\n-\t/* Absent parameters leave the zeroed defaults. */\n-\tfailures += expect_url(\"udp://127.0.0.1:1234\", 0, 0);\n-\n-\tif (failures == 0) {\n-\t\tprintf(\"OK\\n\");\n-\t\treturn 0;\n-\t}\n-\tfprintf(stderr, \"Total failures: %d\\n\", failures);\n-\treturn 1;\n-}\ndiff --git a/tools/meson.build b/tools/meson.build\nindex 0387627..a874d90 100644\n--- a/tools/meson.build\n+++ b/tools/meson.build\n@@ -45,7 +45,7 @@ if compile_prometheus\n \ttools_dependencies += prometheus_dep\n endif\n \n-ristsender_exe = executable('ristsender',\n+executable('ristsender',\n \t['ristsender.c', 'yamlparse.c', 'oob_shared.c', srp_shared, tools_deps, rev_target],\n \tdependencies: [\n \t\tlibrist_dep,\n@@ -57,7 +57,7 @@ ristsender_exe = executable('ristsender',\n \tinclude_directories: inc,\n \tinstall: should_install)\n \n-ristreceiver_exe = executable('ristreceiver',\n+executable('ristreceiver',\n \t['ristreceiver.c', 'yamlparse.c', 'oob_shared.c', srp_shared, tools_deps, rev_target],\n \tdependencies: [\n \t\tlibrist_dep,\n@@ -110,7 +110,6 @@ if mbedcrypto_lib_found or use_nettle\n \t\t\tdependencies: [\n \t\t\t\tlibrist_dep,\n \t\t\t\tcrypto_deps,\n-\t\t\t\tthreads,\n \t\t\t],\n \t\t\tinclude_directories: inc,\n \t\t\tinstall: should_install)\ndiff --git a/tools/oob_shared.c b/tools/oob_shared.c\nindex a260256..62167b4 100644\n--- a/tools/oob_shared.c\n+++ b/tools/oob_shared.c\n@@ -70,9 +70,7 @@ int oob_build_api_payload(uint16_t *buffer, char *sourceip, char *destip, char *\n \tstruct ipheader *ip = (struct ipheader *) buffer;\n \t// unassigned protocol 252 used for API communication, api_id (54321 to identify this API message type)\n \tpopulate_ip_header(ip, sourceip, destip, RIST_OOB_API_IP_IDENT_AUTH, RIST_OOB_API_IP_PROTOCOL);\n-\t/* buffer is uint16_t*: pointer math must be done in bytes, or the\n-\t * message lands at 2x the header size and the receiver can't find it. */\n-\tmemcpy((char *)buffer + sizeof(struct ipheader), message, message_len);\n+\tmemcpy(buffer + sizeof(struct ipheader), message, message_len);\n \tint total_len = sizeof(struct ipheader) + message_len;\n \tip->iph_len = htons(total_len);\n \t// Calculate the checksum for integrity since there is no packet recovery\n@@ -85,15 +83,6 @@ char *oob_process_api_message(int buffer_len, char *buffer, int *message_len)\n \tstruct ipheader *ip = (struct ipheader *) buffer;\n \tint header_size = sizeof(struct ipheader);\n \n-\t/* The header fields are read before any other check, so the datagram\n-\t * must be at least a full header; otherwise the three checks below run\n-\t * on out-of-bounds bytes and message_len can go negative (unbounded\n-\t * %.*s read in the callers' log path). */\n-\tif (buffer_len < header_size) {\n-\t\t*message_len = RIST_OOB_ERROR_INVALID_LENGTH;\n-\t\treturn NULL;\n-\t}\n-\n \t// Check reported length vs buffer length\n \tif (htons(buffer_len) != ip->iph_len) {\n \t\t*message_len = RIST_OOB_ERROR_INVALID_LENGTH;\ndiff --git a/tools/prometheus-exporter.c b/tools/prometheus-exporter.c\nindex 5c5606e..78197dd 100644\n--- a/tools/prometheus-exporter.c\n+++ b/tools/prometheus-exporter.c\n@@ -52,15 +52,7 @@ struct rist_prometheus_client_flow_stats {\n \t\tdouble rist_client_flow_reordered_packets;\n \t\tdouble rist_client_flow_recovered_packets;\n \t\tdouble rist_client_flow_recovered_one_retry_packets;\n-\t\tdouble rist_client_flow_recovered_two_nacks_packets;\n-\t\tdouble rist_client_flow_recovered_three_nacks_packets;\n-\t\tdouble rist_client_flow_recovered_four_nacks_packets;\n-\t\tdouble rist_client_flow_recovered_more_nacks_packets;\n \t\tdouble rist_client_flow_lost_packets;\n-\t\tdouble rist_client_flow_retries_packets;\n-\t\tdouble rist_client_flow_dropped_late_packets;\n-\t\tdouble rist_client_flow_dropped_full_packets;\n-\t\tdouble rist_client_flow_duplicate_packets;\n \t} counters;\n \n \tstruct {\n@@ -77,19 +69,10 @@ struct rist_prometheus_client_flow_stats {\n \t\tdouble rist_client_flow_reordered_packets;\n \t\tdouble rist_client_flow_recovered_packets;\n \t\tdouble rist_client_flow_recovered_one_retry_packets;\n-\t\tdouble rist_client_flow_recovered_two_nacks_packets;\n-\t\tdouble rist_client_flow_recovered_three_nacks_packets;\n-\t\tdouble rist_client_flow_recovered_four_nacks_packets;\n-\t\tdouble rist_client_flow_recovered_more_nacks_packets;\n \t\tdouble rist_client_flow_lost_packets;\n-\t\tdouble rist_client_flow_retries_packets;\n-\t\tdouble rist_client_flow_dropped_late_packets;\n-\t\tdouble rist_client_flow_dropped_full_packets;\n-\t\tdouble rist_client_flow_duplicate_packets;\n \t\tdouble rist_client_flow_min_iat_seconds;\n \t\tdouble rist_client_flow_cur_iat_seconds;\n \t\tdouble rist_client_flow_max_iat_seconds;\n-\t\tdouble rist_client_flow_avg_buffer_time_seconds;\n \t\tdouble rist_client_flow_rtt_seconds;\n \t\tdouble rist_client_flow_quality_ratio;\n \t} container[16];\n@@ -97,11 +80,6 @@ struct rist_prometheus_client_flow_stats {\n \tint container_count;\n \tint container_offset;\n \tuint32_t flowid;\n-\t/* context profile (enum rist_profile), on-wire seq width (16/32), and\n-\t * whether Advanced framing is active, for the rist_client_flow_info series. */\n-\tuint8_t profile;\n-\tuint8_t seq_bits;\n-\tuint8_t advanced_active;\n \tchar cname[RIST_MAX_STRING_LONG];\n };\n \n@@ -130,52 +108,16 @@ struct rist_prometheus_sender_peer_stats {\n \t\tdouble rist_sender_peer_retransmitted_packets;\n \t\tdouble rist_sender_peer_rtt_seconds;\n \t\tdouble rist_sender_peer_quality_ratio;\n-\t\tdouble rist_sender_peer_rtt_muted;\n-\t\tdouble rist_sender_peer_rtt_mute_events;\n \t} container[16];\n \tint container_count;\n \tint container_offset;\n \n-\t/* context profile (enum rist_profile) and whether Advanced framing is\n-\t * currently active toward this peer, for the rist_sender_peer_info series. */\n-\tuint8_t profile;\n-\tuint8_t advanced_active;\n-\n \tchar cname[RIST_MAX_STRING_SHORT];\n \tchar *url;\n \tchar *local_url;\n \tbool from_callback;\n };\n \n-struct rist_prometheus_receiver_peer_stats {\n-\tuint32_t peer_id;\n-\tuint32_t flow_id;\n-\tuint64_t receiver_id;\n-\tchar *tags;\n-\tuint64_t created;\n-\tuint64_t last_updated;\n-\n-\tstruct {\n-\t\tdouble rist_receiver_peer_received_data_packets;\n-\t\tdouble rist_receiver_peer_received_bytes;\n-\t\tdouble rist_receiver_peer_received_rtcp_packets;\n-\t\tdouble rist_receiver_peer_sent_rtcp_packets;\n-\t} counters;\n-\n-\tstruct {\n-\t\tuint64_t updated;\n-\t\tdouble rist_receiver_peer_bandwidth_bps;\n-\t\tdouble rist_receiver_peer_avg_bandwidth_bps;\n-\t\tdouble rist_receiver_peer_received_data_packets;\n-\t\tdouble rist_receiver_peer_received_bytes;\n-\t\tdouble rist_receiver_peer_received_rtcp_packets;\n-\t\tdouble rist_receiver_peer_sent_rtcp_packets;\n-\t\tdouble rist_receiver_peer_rtt_seconds;\n-\t\tdouble rist_receiver_peer_avg_rtt_seconds;\n-\t} container[16];\n-\tint container_count;\n-\tint container_offset;\n-};\n \n struct rist_prometheus_stats {\n \tuint64_t last_cleanup;\n@@ -192,8 +134,6 @@ struct rist_prometheus_stats {\n \tsize_t client_cnt;\n \tstruct rist_prometheus_sender_peer_stats **sender_peers;\n \tsize_t sender_peer_cnt;\n-\tstruct rist_prometheus_receiver_peer_stats **receiver_peers;\n-\tsize_t receiver_peer_cnt;\n #if HAVE_LIBMICROHTTPD\n \tstruct MHD_Daemon *httpd;\n #endif\n@@ -205,17 +145,6 @@ uint64_t get_timestamp(void) {\n \treturn tv.tv_sec;\n }\n \n-/* Maps enum rist_profile to a label string for the *_info series. Kept local\n- * to the exporter so it does not depend on a library-internal symbol. */\n-static const char *prom_profile_name(uint8_t profile) {\n-\tswitch (profile) {\n-\tcase 0:  return \"simple\";\n-\tcase 1:  return \"main\";\n-\tcase 2:  return \"advanced\";\n-\tdefault: return \"unknown\";\n-\t}\n-}\n-\n #define str(s) #s\n #define PROMETHEUS_METRIC(name, help, unit, type) \\\n \"# HELP \"str(name)\" \"help\"\\n\" \\\n@@ -265,7 +194,7 @@ for (size_t c=0; c < ctx->cnt; c++) { \\\n \t\tif (ctx->single_stat_point) { \\\n \t\t\toffset += snprintf(out + offset * (out != NULL), remaining, \"\\n\"); \\\n \t\t} else { \\\n-\t\t\toffset += snprintf(out + offset * (out != NULL), remaining, \" %\"PRIu64\"\\n\", s->container[i].updated); \\\n+\t\t\toffset += snprintf(out + offset * (out != NULL), remaining, \"%\"PRIu64\"\\n\", s->container[i].updated); \\\n \t\t} \\\n \t\tremaining = MAX((out_size - offset), 0); \\\n \t} \\\n@@ -280,9 +209,6 @@ for (size_t c=0; c < ctx->cnt; c++) { \\\n #define PROMETHEUS_GAUGE_PRINT_SENDER_PEER(name, help, unit) PROMETHEUS_GAUGE_PRINT_R(name, help, unit, rist_prometheus_sender_peer_stats, sender_peers, sender_peer_cnt)\n #define PROMETHEUS_COUNTER_PRINT_SENDER_PEER(name, help, unit) PROMETHEUS_COUNTER_PRINT_R(name, help, unit, rist_prometheus_sender_peer_stats, sender_peers, sender_peer_cnt)\n \n-#define PROMETHEUS_GAUGE_PRINT_RECEIVER_PEER(name, help, unit) PROMETHEUS_GAUGE_PRINT_R(name, help, unit, rist_prometheus_receiver_peer_stats, receiver_peers, receiver_peer_cnt)\n-#define PROMETHEUS_COUNTER_PRINT_RECEIVER_PEER(name, help, unit) PROMETHEUS_COUNTER_PRINT_R(name, help, unit, rist_prometheus_receiver_peer_stats, receiver_peers, receiver_peer_cnt)\n-\n \n static int rist_prometheus_format_client_flow_stats(struct rist_prometheus_stats *ctx, char *out, int out_size) {\n \tif (ctx->client_cnt == 0)\n@@ -301,19 +227,10 @@ static int rist_prometheus_format_client_flow_stats(struct rist_prometheus_stats\n \tPROMETHEUS_COUNTER_PRINT_CLIENT(rist_client_flow_reordered_packets, \"Total number of reordered packets\", \"packets\")\n \tPROMETHEUS_COUNTER_PRINT_CLIENT(rist_client_flow_recovered_packets, \"Total number of recovered packets\", \"packets\")\n \tPROMETHEUS_COUNTER_PRINT_CLIENT(rist_client_flow_recovered_one_retry_packets, \"Total number of recovered after one retry packets\", \"packets\")\n-\tPROMETHEUS_COUNTER_PRINT_CLIENT(rist_client_flow_recovered_two_nacks_packets, \"Total number of packets recovered after two nacks\", \"packets\")\n-\tPROMETHEUS_COUNTER_PRINT_CLIENT(rist_client_flow_recovered_three_nacks_packets, \"Total number of packets recovered after three nacks\", \"packets\")\n-\tPROMETHEUS_COUNTER_PRINT_CLIENT(rist_client_flow_recovered_four_nacks_packets, \"Total number of packets recovered after four nacks\", \"packets\")\n-\tPROMETHEUS_COUNTER_PRINT_CLIENT(rist_client_flow_recovered_more_nacks_packets, \"Total number of packets recovered after more than four nacks\", \"packets\")\n \tPROMETHEUS_COUNTER_PRINT_CLIENT(rist_client_flow_lost_packets, \"Total number of lost packets\", \"packets\")\n-\tPROMETHEUS_COUNTER_PRINT_CLIENT(rist_client_flow_retries_packets, \"Total number of retransmissions requested (nacks queued)\", \"packets\")\n-\tPROMETHEUS_COUNTER_PRINT_CLIENT(rist_client_flow_dropped_late_packets, \"Total number of packets dropped for arriving too late\", \"packets\")\n-\tPROMETHEUS_COUNTER_PRINT_CLIENT(rist_client_flow_dropped_full_packets, \"Total number of packets dropped because the buffer was full\", \"packets\")\n-\tPROMETHEUS_COUNTER_PRINT_CLIENT(rist_client_flow_duplicate_packets, \"Total number of duplicate packets received\", \"packets\")\n \tPROMETHEUS_GAUGE_PRINT_CLIENT(rist_client_flow_min_iat_seconds, \"Minimum inter arrival time in seconds\", \"seconds\")\n \tPROMETHEUS_GAUGE_PRINT_CLIENT(rist_client_flow_cur_iat_seconds, \"Current inter arrival time in seconds\", \"seconds\")\n \tPROMETHEUS_GAUGE_PRINT_CLIENT(rist_client_flow_max_iat_seconds, \"Maximum inter arrival time in seconds\", \"seconds\")\n-\tPROMETHEUS_GAUGE_PRINT_CLIENT(rist_client_flow_avg_buffer_time_seconds, \"Average receiver buffer duration in seconds\", \"seconds\")\n \tPROMETHEUS_GAUGE_PRINT_CLIENT(rist_client_flow_rtt_seconds, \"Current RTT in seconds\", \"seconds\");\n \tPROMETHEUS_GAUGE_PRINT_CLIENT(rist_client_flow_quality_ratio, \"Current connection quality ratio\", \"ratio\");\n \treturn offset;\n@@ -332,85 +249,9 @@ static int rist_prometheus_format_sender_peer_stats(struct rist_prometheus_stats\n \tPROMETHEUS_GAUGE_PRINT_SENDER_PEER(rist_sender_peer_received_packets, \"Total number of packets received (rtcp)\", \"packets\")\n \tPROMETHEUS_GAUGE_PRINT_SENDER_PEER(rist_sender_peer_rtt_seconds, \"Current RTT in seconds\", \"seconds\");\n \tPROMETHEUS_GAUGE_PRINT_SENDER_PEER(rist_sender_peer_quality_ratio, \"Current connection quality ratio\", \"ratio\");\n-\tPROMETHEUS_GAUGE_PRINT_SENDER_PEER(rist_sender_peer_rtt_muted, \"Whether this bonded leg is currently muted by RTT auto-mute (1) or active (0)\", \"bool\");\n-\tPROMETHEUS_GAUGE_PRINT_SENDER_PEER(rist_sender_peer_rtt_mute_events, \"Cumulative number of RTT-triggered mute events on this leg\", \"events\");\n-\treturn offset;\n-}\n-\n-static int rist_prometheus_format_receiver_peer_stats(struct rist_prometheus_stats *ctx, char *out, int out_size) {\n-\tif (ctx->receiver_peer_cnt == 0)\n-\t\treturn 0;\n-\tint offset = 0;\n-\tint remaining = out_size;\n-\tPROMETHEUS_GAUGE_PRINT_RECEIVER_PEER(rist_receiver_peer_bandwidth_bps, \"The current bandwidth received from the peer\", \"bps\")\n-\tPROMETHEUS_GAUGE_PRINT_RECEIVER_PEER(rist_receiver_peer_avg_bandwidth_bps, \"The average bandwidth received from the peer\", \"bps\")\n-\tPROMETHEUS_COUNTER_PRINT_RECEIVER_PEER(rist_receiver_peer_received_data_packets, \"Total number of data packets received from the peer\", \"packets\")\n-\tPROMETHEUS_COUNTER_PRINT_RECEIVER_PEER(rist_receiver_peer_received_bytes, \"Total number of payload bytes received from the peer\", \"bytes\")\n-\tPROMETHEUS_COUNTER_PRINT_RECEIVER_PEER(rist_receiver_peer_received_rtcp_packets, \"Total number of RTCP packets received from the peer\", \"packets\")\n-\tPROMETHEUS_COUNTER_PRINT_RECEIVER_PEER(rist_receiver_peer_sent_rtcp_packets, \"Total number of RTCP packets sent to the peer\", \"packets\")\n-\tPROMETHEUS_GAUGE_PRINT_RECEIVER_PEER(rist_receiver_peer_rtt_seconds, \"Current RTT to the peer in seconds\", \"seconds\");\n-\tPROMETHEUS_GAUGE_PRINT_RECEIVER_PEER(rist_receiver_peer_avg_rtt_seconds, \"Average RTT to the peer in seconds\", \"seconds\");\n \treturn offset;\n }\n \n-/* Info series: constant value 1 carrying the flow profile, seq width, and\n- * advanced_active as labels, so a scrape can identify Advanced flows without\n- * changing the label set of the existing series. s->tags ends in '}', so\n- * reprint it without the trailing brace and append the extra labels. */\n-static int rist_prometheus_format_client_flow_info(struct rist_prometheus_stats *ctx, char *out, int out_size) {\n-\tif (ctx->client_cnt == 0)\n-\t\treturn 0;\n-\tint offset = 0;\n-\tint remaining = out_size;\n-\toffset += snprintf(out + offset * (out != NULL), remaining,\n-\t\tPROMETHEUS_GAUGE(rist_client_flow_info, \"Flow metadata; value is always 1, see profile, seq_bits and advanced_active labels\", \"info\"));\n-\tremaining = MAX((out_size - offset), 0);\n-\tfor (size_t c = 0; c < ctx->client_cnt; c++) {\n-\t\tstruct rist_prometheus_client_flow_stats *s = ctx->clients[c];\n-\t\tif (s->container_count == 0)\n-\t\t\tcontinue;\n-\t\tsize_t tlen = strlen(s->tags);\n-\t\toffset += snprintf(out + offset * (out != NULL), remaining,\n-\t\t\t\"rist_client_flow_info%.*s,profile=\\\"%s\\\",seq_bits=\\\"%u\\\",advanced_active=\\\"%u\\\"} 1\\n\",\n-\t\t\t(int)(tlen > 0 ? tlen - 1 : 0), s->tags,\n-\t\t\tprom_profile_name(s->profile), (unsigned)s->seq_bits,\n-\t\t\t(unsigned)(s->advanced_active ? 1 : 0));\n-\t\tremaining = MAX((out_size - offset), 0);\n-\t}\n-\treturn offset;\n-}\n-\n-static int rist_prometheus_format_sender_peer_info(struct rist_prometheus_stats *ctx, char *out, int out_size) {\n-\tif (ctx->sender_peer_cnt == 0)\n-\t\treturn 0;\n-\tint offset = 0;\n-\tint remaining = out_size;\n-\toffset += snprintf(out + offset * (out != NULL), remaining,\n-\t\tPROMETHEUS_GAUGE(rist_sender_peer_info, \"Sender peer metadata; value is always 1, see profile and advanced_active labels\", \"info\"));\n-\tremaining = MAX((out_size - offset), 0);\n-\tfor (size_t c = 0; c < ctx->sender_peer_cnt; c++) {\n-\t\tstruct rist_prometheus_sender_peer_stats *s = ctx->sender_peers[c];\n-\t\tif (s->container_count == 0 || s->tags == NULL)\n-\t\t\tcontinue;\n-\t\tsize_t tlen = strlen(s->tags);\n-\t\toffset += snprintf(out + offset * (out != NULL), remaining,\n-\t\t\t\"rist_sender_peer_info%.*s,profile=\\\"%s\\\",advanced_active=\\\"%u\\\"} 1\\n\",\n-\t\t\t(int)(tlen > 0 ? tlen - 1 : 0), s->tags,\n-\t\t\tprom_profile_name(s->profile), (unsigned)(s->advanced_active ? 1 : 0));\n-\t\tremaining = MAX((out_size - offset), 0);\n-\t}\n-\treturn offset;\n-}\n-\n-static void rist_prometheus_cleanup_stale_locked(struct rist_prometheus_stats *ctx, uint64_t now);\n-\n-/* Read a numeric JSON field, returning 0 when it is absent or not a number.\n- * Used for stats fields that only exist in newer schema versions. */\n-static double rist_json_num(const cJSON *obj, const char *key) {\n-\tconst cJSON *it = cJSON_GetObjectItem(obj, key);\n-\treturn cJSON_IsNumber(it) ? it->valuedouble : 0;\n-}\n-\n void rist_prometheus_handle_client_stats(struct rist_prometheus_stats *ctx, const struct rist_stats *stats_container, uint64_t now, uint64_t receiver_id) {\n \n \tconst struct rist_stats_receiver_flow *stats = &stats_container->stats.receiver_flow;\n@@ -459,27 +300,13 @@ void rist_prometheus_handle_client_stats(struct rist_prometheus_stats *ctx, cons\n \tdouble bitrate_rejected = 0;\n \tdouble bitrate_ts_nulls = 0;\n \tdouble bitrate_payload = 0;\n-\tdouble retries = 0, dropped_late = 0, dropped_full = 0, duplicates = 0;\n-\tdouble recovered_two = 0, recovered_three = 0, recovered_four = 0, recovered_more = 0;\n \tif (stats_container->version > 0) {\n \t\tbitrate_rejected = cJSON_GetObjectItem(flowstats,\"bitrate_rejected\")->valuedouble;\n \t\tbitrate_ts_nulls = cJSON_GetObjectItem(flowstats,\"bitrate_ts_nulls\")->valuedouble;\n \t\tbitrate_payload = cJSON_GetObjectItem(flowstats,\"bitrate_payload\")->valuedouble;\n-\t\tretries = rist_json_num(flowstats, \"retries\");\n-\t\tdropped_late = rist_json_num(flowstats, \"dropped_late\");\n-\t\tdropped_full = rist_json_num(flowstats, \"dropped_full\");\n-\t\tduplicates = rist_json_num(flowstats, \"duplicates\");\n-\t\trecovered_two = rist_json_num(flowstats, \"recovered_two_nacks\");\n-\t\trecovered_three = rist_json_num(flowstats, \"recovered_three_nacks\");\n-\t\trecovered_four = rist_json_num(flowstats, \"recovered_four_nacks\");\n-\t\trecovered_more = rist_json_num(flowstats, \"recovered_more_nacks\");\n \t}\n \tcJSON_Delete(receiverstats);\n \n-\ts->profile = stats->profile;\n-\ts->seq_bits = stats->seq_bits;\n-\ts->advanced_active = stats->advanced_active;\n-\n \ts->container[s->container_offset].rist_client_flow_peers = stats->peer_count;\n \ts->container[s->container_offset].rist_client_flow_bandwidth_bps = stats->bandwidth;\n \ts->container[s->container_offset].rist_client_flow_retry_bandwidth_bps = stats->retry_bandwidth;\n@@ -492,20 +319,10 @@ void rist_prometheus_handle_client_stats(struct rist_prometheus_stats *ctx, cons\n \ts->container[s->container_offset].rist_client_flow_reordered_packets = s->counters.rist_client_flow_reordered_packets += stats->reordered;\n \ts->container[s->container_offset].rist_client_flow_recovered_packets = s->counters.rist_client_flow_recovered_packets += stats->recovered;\n \ts->container[s->container_offset].rist_client_flow_recovered_one_retry_packets = s->counters.rist_client_flow_recovered_one_retry_packets += stats->recovered_one_retry;\n-\ts->container[s->container_offset].rist_client_flow_recovered_two_nacks_packets = s->counters.rist_client_flow_recovered_two_nacks_packets += recovered_two;\n-\ts->container[s->container_offset].rist_client_flow_recovered_three_nacks_packets = s->counters.rist_client_flow_recovered_three_nacks_packets += recovered_three;\n-\ts->container[s->container_offset].rist_client_flow_recovered_four_nacks_packets = s->counters.rist_client_flow_recovered_four_nacks_packets += recovered_four;\n-\ts->container[s->container_offset].rist_client_flow_recovered_more_nacks_packets = s->counters.rist_client_flow_recovered_more_nacks_packets += recovered_more;\n \ts->container[s->container_offset].rist_client_flow_lost_packets = s->counters.rist_client_flow_lost_packets += stats->lost;\n-\ts->container[s->container_offset].rist_client_flow_retries_packets = s->counters.rist_client_flow_retries_packets += retries;\n-\ts->container[s->container_offset].rist_client_flow_dropped_late_packets = s->counters.rist_client_flow_dropped_late_packets += dropped_late;\n-\ts->container[s->container_offset].rist_client_flow_dropped_full_packets = s->counters.rist_client_flow_dropped_full_packets += dropped_full;\n-\ts->container[s->container_offset].rist_client_flow_duplicate_packets = s->counters.rist_client_flow_duplicate_packets += duplicates;\n \ts->container[s->container_offset].rist_client_flow_min_iat_seconds = ((double)1 / (double)1000000) * stats->min_inter_packet_spacing;\n \ts->container[s->container_offset].rist_client_flow_cur_iat_seconds = ((double)1 / (double)1000000) * stats->cur_inter_packet_spacing;\n \ts->container[s->container_offset].rist_client_flow_max_iat_seconds = ((double)1 / (double)1000000) * stats->max_inter_packet_spacing;\n-\t/* avg_buffer_time is reported in milliseconds (see rist_stats_receiver_flow in librist/stats.h). */\n-\ts->container[s->container_offset].rist_client_flow_avg_buffer_time_seconds = ((double)stats->avg_buffer_time) / 1000.0;\n \ts->container[s->container_offset].rist_client_flow_rtt_seconds = ((double)1 / (double)1000) * stats->rtt;\n \ts->container[s->container_offset].rist_client_flow_quality_ratio = (double)stats->quality / 100.0;\n \ts->container[s->container_offset].updated = now;\n@@ -516,121 +333,9 @@ void rist_prometheus_handle_client_stats(struct rist_prometheus_stats *ctx, cons\n \t\t\ts->container_count++;\n \t\t}\n \t} else {\n-\t\t/* Single-stat-point mode: format reads container[0] only\n-\t\t * (container_count stays at 1), so the next write must land there. */\n-\t\ts->container_offset = 0;\n+\t\ts->container_offset = 1;\n \t\ts->container_count = 1;\n \t}\n-\n-\tfor (size_t pi = 0; pi < stats->peer_count; pi++) {\n-\t\tconst struct rist_stats_receiver_peer *pstats = &stats->peers[pi];\n-\t\tstruct rist_prometheus_receiver_peer_stats *ps = NULL;\n-\t\tfor (size_t i = 0; i < ctx->receiver_peer_cnt; i++) {\n-\t\t\tif (ctx->receiver_peers[i]->peer_id == pstats->peer_id &&\n-\t\t\t    ctx->receiver_peers[i]->flow_id == stats->flow_id &&\n-\t\t\t    ctx->receiver_peers[i]->receiver_id == receiver_id) {\n-\t\t\t\tps = ctx->receiver_peers[i];\n-\t\t\t\tbreak;\n-\t\t\t}\n-\t\t}\n-\t\tif (ps == NULL) {\n-\t\t\t/* Receiver peers are auto-discovered from the stats callback\n-\t\t\t * rather than explicitly registered; the URL is carried\n-\t\t\t * alongside the peer in the receiver-flow stats JSON. */\n-\t\t\tchar peer_url[256] = {0};\n-\t\t\tif (stats_container->stats_json) {\n-\t\t\t\tcJSON *url_root = cJSON_Parse(stats_container->stats_json);\n-\t\t\t\tif (url_root) {\n-\t\t\t\t\tcJSON *recv_stats = cJSON_GetObjectItem(url_root, \"receiver-stats\");\n-\t\t\t\t\tcJSON *flowinst = recv_stats ? cJSON_GetObjectItem(recv_stats, \"flowinstant\") : NULL;\n-\t\t\t\t\tcJSON *peers = flowinst ? cJSON_GetObjectItem(flowinst, \"peers\") : NULL;\n-\t\t\t\t\tif (cJSON_IsArray(peers)) {\n-\t\t\t\t\t\tfor (cJSON *p = peers->child; p != NULL; p = p->next) {\n-\t\t\t\t\t\t\tcJSON *id_item = cJSON_GetObjectItem(p, \"id\");\n-\t\t\t\t\t\t\tif (cJSON_IsNumber(id_item) && (uint32_t)id_item->valueint == pstats->peer_id) {\n-\t\t\t\t\t\t\t\tcJSON *url_item = cJSON_GetObjectItem(p, \"url\");\n-\t\t\t\t\t\t\t\tif (cJSON_IsString(url_item) && url_item->valuestring) {\n-\t\t\t\t\t\t\t\t\tstrncpy(peer_url, url_item->valuestring, sizeof(peer_url) - 1);\n-\t\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t\t\tbreak;\n-\t\t\t\t\t\t\t}\n-\t\t\t\t\t\t}\n-\t\t\t\t\t}\n-\t\t\t\t\tcJSON_Delete(url_root);\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tint res = snprintf(NULL, 0, \"{%speer_id=\\\"%\"PRIu32\"\\\",peer_url=\\\"%s\\\",flow_id=\\\"%\"PRIu32\"\\\",receiver_id=\\\"%\"PRIu64\"\\\"}\", ctx->tags, pstats->peer_id, peer_url, stats->flow_id, receiver_id);\n-\t\t\tif (res < 0)\n-\t\t\t\tcontinue;\n-\t\t\tsize_t len = (size_t)res + 1;\n-\t\t\tif (ctx->receiver_peers == NULL) {\n-\t\t\t\tctx->receiver_peers = malloc(sizeof(*ctx->receiver_peers));\n-\t\t\t} else {\n-\t\t\t\tstruct rist_prometheus_receiver_peer_stats **tmp = realloc(ctx->receiver_peers, sizeof(*ctx->receiver_peers) * (ctx->receiver_peer_cnt + 1));\n-\t\t\t\tif (tmp == NULL) {\n-\t\t\t\t\tfprintf(stderr, \"failed to realloc aborting\\n\");\n-\t\t\t\t\tabort();\n-\t\t\t\t}\n-\t\t\t\tctx->receiver_peers = tmp;\n-\t\t\t}\n-\t\t\tctx->receiver_peers[ctx->receiver_peer_cnt] = calloc(1, sizeof(*ctx->receiver_peers[ctx->receiver_peer_cnt]));\n-\t\t\tps = ctx->receiver_peers[ctx->receiver_peer_cnt];\n-\t\t\tps->peer_id = pstats->peer_id;\n-\t\t\tps->flow_id = stats->flow_id;\n-\t\t\tps->receiver_id = receiver_id;\n-\t\t\tps->created = now;\n-\t\t\tps->tags = calloc(1, len);\n-\t\t\tres = snprintf(ps->tags, len, \"{%speer_id=\\\"%\"PRIu32\"\\\",peer_url=\\\"%s\\\",flow_id=\\\"%\"PRIu32\"\\\",receiver_id=\\\"%\"PRIu64\"\\\"}\", ctx->tags, ps->peer_id, peer_url, ps->flow_id, ps->receiver_id);\n-\t\t\tassert(res >= 0);\n-\t\t\tctx->receiver_peer_cnt++;\n-\t\t}\n-\t\tps->container[ps->container_offset].rist_receiver_peer_bandwidth_bps = (double)pstats->bandwidth;\n-\t\tps->container[ps->container_offset].rist_receiver_peer_avg_bandwidth_bps = (double)pstats->avg_bandwidth;\n-\t\tps->container[ps->container_offset].rist_receiver_peer_received_data_packets = ps->counters.rist_receiver_peer_received_data_packets += (double)pstats->received_data;\n-\t\tps->container[ps->container_offset].rist_receiver_peer_received_bytes = ps->counters.rist_receiver_peer_received_bytes += (double)pstats->received_bytes;\n-\t\tps->container[ps->container_offset].rist_receiver_peer_received_rtcp_packets = ps->counters.rist_receiver_peer_received_rtcp_packets += (double)pstats->received_rtcp;\n-\t\tps->container[ps->container_offset].rist_receiver_peer_sent_rtcp_packets = ps->counters.rist_receiver_peer_sent_rtcp_packets += (double)pstats->sent_rtcp;\n-\t\tps->container[ps->container_offset].rist_receiver_peer_rtt_seconds = ((double)1 / (double)1000) * (double)pstats->rtt;\n-\t\tps->container[ps->container_offset].rist_receiver_peer_avg_rtt_seconds = ((double)1 / (double)1000) * pstats->avg_rtt;\n-\t\tps->container[ps->container_offset].updated = now;\n-\t\tps->last_updated = now;\n-\t\tif (!ctx->single_stat_point) {\n-\t\t\tps->container_offset = (ps->container_offset + 1) % 16;\n-\t\t\tif (ps->container_count < 16) {\n-\t\t\t\tps->container_count++;\n-\t\t\t}\n-\t\t} else {\n-\t\t\t/* Single-stat-point mode: format reads container[0] only\n-\t\t\t * (container_count stays at 1), so the next write must land there. */\n-\t\t\tps->container_offset = 0;\n-\t\t\tps->container_count = 1;\n-\t\t}\n-\t}\n-}\n-\n-/* Escape a string for use as a Prometheus label value (OpenMetrics:\n- * backslash, double-quote and newline must be escaped). Input here is the\n- * remote peer's SDES cname -- attacker-controlled bytes that would otherwise\n- * break out of the label and forge metric lines in the scrape. Input is\n- * capped at dst_size/2 so the escaped form always fits. */\n-static void prom_escape_label(char *dst, size_t dst_size, const char *src)\n-{\n-\tsize_t di = 0;\n-\tsize_t max_in = (dst_size / 2) - 1;\n-\tfor (size_t si = 0; src[si] && si < max_in && di + 3 < dst_size; si++) {\n-\t\tchar c = src[si];\n-\t\tif (c == '\\\\' || c == '\"') {\n-\t\t\tdst[di++] = '\\\\';\n-\t\t\tdst[di++] = c;\n-\t\t} else if (c == '\\n') {\n-\t\t\tdst[di++] = '\\\\';\n-\t\t\tdst[di++] = 'n';\n-\t\t} else if (c >= 32 && c < 127) {\n-\t\t\tdst[di++] = c;\n-\t\t}\n-\t}\n-\tdst[di] = '\\0';\n }\n \n void rist_prometheus_handle_sender_peer_stats(struct rist_prometheus_stats *ctx, const struct rist_stats *stats_container, uint64_t now, uint64_t sender_id) {\n@@ -648,7 +353,7 @@ void rist_prometheus_handle_sender_peer_stats(struct rist_prometheus_stats *ctx,\n \t\treturn;\n \n \tif (s->tags == NULL) {\n-\t\tprom_escape_label(s->cname, sizeof(s->cname), stats->cname);\n+\t\tmemcpy(s->cname, stats->cname, sizeof(s->cname)-1);\n \t\tif (s->local_url == NULL) {\n \t\t\tint res = snprintf(NULL, 0, \"{%speer_id=\\\"%\"PRIu32\"\\\",peer_url=\\\"%s\\\",cname=\\\"%s\\\",sender_id=\\\"%\"PRIu64\"\\\"}\",ctx->tags, s->peer_id, s->url, s->cname, sender_id);\n \t\t\tif (res < 0) {\n@@ -671,40 +376,24 @@ void rist_prometheus_handle_sender_peer_stats(struct rist_prometheus_stats *ctx,\n \t}\n \n \tcJSON *root = cJSON_Parse(stats_container->stats_json);\n+\tcJSON *senderstats = cJSON_GetObjectItem(root,\"sender-stats\");\n \tdouble ts_null = 0;\n \tdouble ts_nulls_bandwidth = 0;\n-\tif (root != NULL) {\n-\t\tcJSON *senderstats = cJSON_GetObjectItem(root, \"sender-stats\");\n-\t\tcJSON *peers = senderstats ? cJSON_GetObjectItem(senderstats, \"peers\") : NULL;\n-\t\tif (cJSON_IsArray(peers)) {\n-\t\t\tfor (cJSON *peer = peers->child; peer != NULL; peer = peer->next) {\n-\t\t\t\tcJSON *id_item = cJSON_GetObjectItem(peer, \"id\");\n-\t\t\t\tif (!cJSON_IsNumber(id_item) || stats->peer_id != (uint32_t)id_item->valueint) {\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\n-\t\t\t\tcJSON *peerstats = cJSON_GetObjectItem(peer, \"stats\");\n-\t\t\t\tif (stats_container->version > 0 && cJSON_IsObject(peerstats)) {\n-\t\t\t\t\tcJSON *ts_null_item = cJSON_GetObjectItem(peerstats, \"ts_null\");\n-\t\t\t\t\tcJSON *ts_nulls_bandwidth_item = cJSON_GetObjectItem(peerstats, \"ts_nulls_bandwidth\");\n-\t\t\t\t\tif (cJSON_IsNumber(ts_null_item)) {\n-\t\t\t\t\t\tts_null = ts_null_item->valuedouble;\n-\t\t\t\t\t}\n-\t\t\t\t\tif (cJSON_IsNumber(ts_nulls_bandwidth_item)) {\n-\t\t\t\t\t\tts_nulls_bandwidth = ts_nulls_bandwidth_item->valuedouble;\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t\tcJSON *profile_item = cJSON_GetObjectItem(peer, \"profile\");\n-\t\t\t\tif (cJSON_IsNumber(profile_item))\n-\t\t\t\t\ts->profile = (uint8_t)profile_item->valueint;\n-\t\t\t\tcJSON *adv_item = cJSON_GetObjectItem(peer, \"advanced_active\");\n-\t\t\t\tif (cJSON_IsBool(adv_item))\n-\t\t\t\t\ts->advanced_active = cJSON_IsTrue(adv_item) ? 1 : 0;\n-\t\t\t\tbreak;\n-\t\t\t}\n+\tcJSON *peer = senderstats->child;\n+\twhile( peer ) {\n+\t\tint peer_id = cJSON_GetObjectItem(peer,\"id\")->valueint;\n+\t\tif (stats->peer_id == (uint32_t)peer_id)\n+\t\t{\n+\t\t\tcJSON *peerstats = cJSON_GetObjectItem(peer,\"stats\");\n+\t\t\tif (stats_container->version > 0) {\n+\t\t\t\tts_null = cJSON_GetObjectItem(peerstats,\"ts_null\")->valuedouble;\n+\t\t\t\tts_nulls_bandwidth = cJSON_GetObjectItem(peerstats,\"ts_nulls_bandwidth\")->valuedouble;\n+\t\t\t}\t\n+\t\t\tbreak;\n \t\t}\n-\t\tcJSON_Delete(root);\n+\t\tpeer = peer->next;\n \t}\n+\tcJSON_Delete(senderstats);\n \n \ts->container[s->container_offset].rist_sender_peer_sent_packets = s->counters.rist_sender_peer_sent_packets += stats->sent;\n \ts->container[s->container_offset].rist_sender_peer_ts_null_packets = s->counters.rist_sender_peer_ts_null_packets += ts_null;\n@@ -715,8 +404,6 @@ void rist_prometheus_handle_sender_peer_stats(struct rist_prometheus_stats *ctx,\n \ts->container[s->container_offset].rist_sender_peer_ts_nulls_bandwidth_bps = ts_nulls_bandwidth;\n \ts->container[s->container_offset].rist_sender_peer_rtt_seconds= ((double)1 / (double)1000) * stats->rtt;\n \ts->container[s->container_offset].rist_sender_peer_quality_ratio = (double)stats->quality / 100.0;\n-\ts->container[s->container_offset].rist_sender_peer_rtt_muted = stats->rtt_muted ? 1.0 : 0.0;\n-\ts->container[s->container_offset].rist_sender_peer_rtt_mute_events = (double)stats->rtt_mute_events;\n \ts->container[s->container_offset].updated = now;\n \ts->last_updated = now;\n \tif (!ctx->single_stat_point) {\n@@ -725,93 +412,15 @@ void rist_prometheus_handle_sender_peer_stats(struct rist_prometheus_stats *ctx,\n \t\t\ts->container_count++;\n \t\t}\n \t} else {\n-\t\t/* Single-stat-point mode: format reads container[0] only\n-\t\t * (container_count stays at 1), so the next write must land there. */\n-\t\ts->container_offset = 0;\n+\t\ts->container_offset = 1;\n \t\ts->container_count = 1;\n \t}\n }\n \n void rist_prometheus_parse_sender_stats(struct rist_prometheus_stats *ctx, uint16_t version, char *stats_json, uint32_t json_size, uintptr_t id)\n {\n-\t(void)json_size;\n-\tif (ctx == NULL || stats_json == NULL)\n-\t\treturn;\n-\n-\tcJSON *root = cJSON_Parse(stats_json);\n-\tif (root == NULL)\n-\t\treturn;\n-\tcJSON *senderstats = cJSON_GetObjectItem(root, \"sender-stats\");\n-\tcJSON *peers = senderstats ? cJSON_GetObjectItem(senderstats, \"peers\") : NULL;\n-\tif (!cJSON_IsArray(peers)) {\n-\t\tcJSON_Delete(root);\n-\t\treturn;\n-\t}\n-\n-\tpthread_mutex_lock(&ctx->lock);\n-\tuint64_t now = get_timestamp();\n-\tfor (cJSON *peer = peers->child; peer != NULL; peer = peer->next) {\n-\t\tif (!cJSON_IsObject(peer))\n-\t\t\tcontinue;\n-\n-\t\tcJSON *id_item = cJSON_GetObjectItem(peer, \"id\");\n-\t\tcJSON *peerstats = cJSON_GetObjectItem(peer, \"stats\");\n-\t\tif (!cJSON_IsNumber(id_item) || !cJSON_IsObject(peerstats))\n-\t\t\tcontinue;\n-\n-\t\tstruct rist_stats stats_container = {0};\n-\t\tstats_container.version = version;\n-\t\tstats_container.stats_type = RIST_STATS_SENDER_PEER;\n-\t\t/* The handler re-parses stats_json to pull ts_null /\n-\t\t * ts_nulls_bandwidth out of the same peer object, so it has to\n-\t\t * see the original buffer here. */\n-\t\tstats_container.stats_json = stats_json;\n-\t\tstats_container.stats.sender_peer.peer_id = (uint32_t)id_item->valueint;\n-\n-\t\tcJSON *cname_item = cJSON_GetObjectItem(peer, \"cname\");\n-\t\tif (cJSON_IsString(cname_item) && cname_item->valuestring) {\n-\t\t\tstrncpy(stats_container.stats.sender_peer.cname,\n-\t\t\t        cname_item->valuestring,\n-\t\t\t        sizeof(stats_container.stats.sender_peer.cname) - 1);\n-\t\t}\n-\n-\t\tcJSON *item;\n-\t\titem = cJSON_GetObjectItem(peerstats, \"bandwidth\");\n-\t\tif (cJSON_IsNumber(item))\n-\t\t\tstats_container.stats.sender_peer.bandwidth = (size_t)item->valuedouble;\n-\t\titem = cJSON_GetObjectItem(peerstats, \"retry_bandwidth\");\n-\t\tif (cJSON_IsNumber(item))\n-\t\t\tstats_container.stats.sender_peer.retry_bandwidth = (size_t)item->valuedouble;\n-\t\titem = cJSON_GetObjectItem(peerstats, \"sent\");\n-\t\tif (cJSON_IsNumber(item))\n-\t\t\tstats_container.stats.sender_peer.sent = (uint64_t)item->valuedouble;\n-\t\titem = cJSON_GetObjectItem(peerstats, \"received\");\n-\t\tif (cJSON_IsNumber(item))\n-\t\t\tstats_container.stats.sender_peer.received = (uint64_t)item->valuedouble;\n-\t\titem = cJSON_GetObjectItem(peerstats, \"retransmitted\");\n-\t\tif (cJSON_IsNumber(item))\n-\t\t\tstats_container.stats.sender_peer.retransmitted = (uint64_t)item->valuedouble;\n-\t\titem = cJSON_GetObjectItem(peerstats, \"quality\");\n-\t\tif (cJSON_IsNumber(item))\n-\t\t\tstats_container.stats.sender_peer.quality = item->valuedouble;\n-\t\t/* JSON rtt is already in ms (last_rtt / RIST_CLOCK), and the\n-\t\t * handler converts ms -> seconds, so feed it directly. */\n-\t\titem = cJSON_GetObjectItem(peerstats, \"rtt\");\n-\t\tif (cJSON_IsNumber(item))\n-\t\t\tstats_container.stats.sender_peer.rtt = (uint32_t)item->valuedouble;\n-\t\titem = cJSON_GetObjectItem(peerstats, \"rtt_muted\");\n-\t\tif (cJSON_IsBool(item))\n-\t\t\tstats_container.stats.sender_peer.rtt_muted = cJSON_IsTrue(item) ? 1 : 0;\n-\t\titem = cJSON_GetObjectItem(peerstats, \"rtt_mute_events\");\n-\t\tif (cJSON_IsNumber(item))\n-\t\t\tstats_container.stats.sender_peer.rtt_mute_events = (uint32_t)item->valuedouble;\n-\n-\t\trist_prometheus_handle_sender_peer_stats(ctx, &stats_container, now, id);\n-\t}\n-\n-\trist_prometheus_cleanup_stale_locked(ctx, now);\n-\tpthread_mutex_unlock(&ctx->lock);\n-\tcJSON_Delete(root);\n+\t(void)ctx; (void)version; (void)stats_json; (void)json_size; (void)id;\n+\t// TODO: convert json stats to prometheus stats\n }\n \n void rist_prometheus_parse_stats(struct rist_prometheus_stats *ctx, const struct rist_stats *stats_container, uintptr_t id) {\n@@ -822,14 +431,8 @@ void rist_prometheus_parse_stats(struct rist_prometheus_stats *ctx, const struct\n \t} else if (stats_container->stats_type == RIST_STATS_SENDER_PEER) {\n \t\trist_prometheus_handle_sender_peer_stats(ctx, stats_container, now, id);\n \t}\n-\trist_prometheus_cleanup_stale_locked(ctx, now);\n-\tpthread_mutex_unlock(&ctx->lock);\n-}\n-\n-static void rist_prometheus_cleanup_stale_locked(struct rist_prometheus_stats *ctx, uint64_t now) {\n-\tif (!(now > ctx->last_cleanup && (now - ctx->last_cleanup) > 10))\n-\t\treturn;\n-\t{//clean expired receiver clients\n+\tif (now > ctx->last_cleanup && (now - ctx->last_cleanup) >10) {\n+\t\t{//clean expired receiver clients\n \t\t\tsize_t clnt_cnt_start = ctx->client_cnt;\n \t\t\tfor (size_t i = 0; i < ctx->client_cnt;) {\n \t\t\t\tif (ctx->clients[i]->last_updated < now && (now - ctx->clients[i]->last_updated) > 15) {\n@@ -885,33 +488,9 @@ static void rist_prometheus_cleanup_stale_locked(struct rist_prometheus_stats *c\n \t\t\t\t}\n \t\t\t}\n \t\t}\n-\t\t{//clean expired receiver peers\n-\t\t\tsize_t receiver_peer_cnt_start = ctx->receiver_peer_cnt;\n-\t\t\tfor (size_t i = 0; i < ctx->receiver_peer_cnt;) {\n-\t\t\t\tif (ctx->receiver_peers[i]->last_updated < now && (now - ctx->receiver_peers[i]->last_updated) > 15) {\n-\t\t\t\t\tfree(ctx->receiver_peers[i]->tags);\n-\t\t\t\t\tfree(ctx->receiver_peers[i]);\n-\t\t\t\t\tctx->receiver_peers[i] = ctx->receiver_peers[ctx->receiver_peer_cnt - 1];\n-\t\t\t\t\tctx->receiver_peer_cnt--;\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n-\t\t\t\ti++;\n-\t\t\t}\n-\t\t\tif (receiver_peer_cnt_start != ctx->receiver_peer_cnt) {\n-\t\t\t\tif (ctx->receiver_peer_cnt == 0) {\n-\t\t\t\t\tfree(ctx->receiver_peers);\n-\t\t\t\t\tctx->receiver_peers = NULL;\n-\t\t\t\t} else {\n-\t\t\t\t\tstruct rist_prometheus_receiver_peer_stats **tmp = realloc(ctx->receiver_peers, sizeof(*ctx->receiver_peers) * ctx->receiver_peer_cnt);\n-\t\t\t\t\tif (tmp == NULL) {\n-\t\t\t\t\t\tfprintf(stderr, \"realloc failed aborting\\n\");\n-\t\t\t\t\t\tabort();\n-\t\t\t\t\t}\n-\t\t\t\t\tctx->receiver_peers = tmp;\n-\t\t\t\t}\n-\t\t\t}\n-\t\t}\n \t\tctx->last_cleanup = now;\n+\t}\n+\tpthread_mutex_unlock(&ctx->lock);\n }\n \n static const char PROMETHEUS_EOF[] = \"# EOF\\n\";\n@@ -922,9 +501,6 @@ static int rist_prometheus_stats_format(struct rist_prometheus_stats *ctx) {\n \t\treturn 0;\n \t}\n \treq_size += rist_prometheus_format_sender_peer_stats(ctx, NULL, 0);\n-\treq_size += rist_prometheus_format_receiver_peer_stats(ctx, NULL, 0);\n-\treq_size += rist_prometheus_format_client_flow_info(ctx, NULL, 0);\n-\treq_size += rist_prometheus_format_sender_peer_info(ctx, NULL, 0);\n \treq_size += sizeof(PROMETHEUS_EOF) - 1;\n \n \tif ((size_t)(req_size+1) > ctx->format_buf_len) {\n@@ -934,27 +510,14 @@ static int rist_prometheus_stats_format(struct rist_prometheus_stats *ctx) {\n \tint size = rist_prometheus_format_client_flow_stats(ctx, ctx->format_buf, (int)ctx->format_buf_len);\n \n \tsize += rist_prometheus_format_sender_peer_stats(ctx, &ctx->format_buf[size], (int)ctx->format_buf_len - size);\n-\tsize += rist_prometheus_format_receiver_peer_stats(ctx, &ctx->format_buf[size], (int)ctx->format_buf_len - size);\n-\tsize += rist_prometheus_format_client_flow_info(ctx, &ctx->format_buf[size], (int)ctx->format_buf_len - size);\n-\tsize += rist_prometheus_format_sender_peer_info(ctx, &ctx->format_buf[size], (int)ctx->format_buf_len - size);\n \tsize += snprintf(&ctx->format_buf[size], ctx->format_buf_len - size, \"%s\", PROMETHEUS_EOF);\n-\t/* Multi-point mode accumulates samples between scrapes and resets\n-\t * after each one.  Single-stat-point mode keeps the latest sample\n-\t * live until the next stats callback overwrites it\n-\t * (cleanup_stale_locked still ages it out after 15 s). */\n-\tif (!ctx->single_stat_point) {\n-\t\tfor (size_t i=0; i < ctx->client_cnt; i++) {\n-\t\t\tctx->clients[i]->container_count = 0;\n-\t\t\tctx->clients[i]->container_offset = 0;\n-\t\t}\n-\t\tfor (size_t i=0; i < ctx->sender_peer_cnt; i++) {\n-\t\t\tctx->sender_peers[i]->container_count = 0;\n-\t\t\tctx->sender_peers[i]->container_offset = 0;\n-\t\t}\n-\t\tfor (size_t i=0; i < ctx->receiver_peer_cnt; i++) {\n-\t\t\tctx->receiver_peers[i]->container_count = 0;\n-\t\t\tctx->receiver_peers[i]->container_offset = 0;\n-\t\t}\n+\tfor (size_t i=0; i < ctx->client_cnt; i++) {\n+\t\tctx->clients[i]->container_count = 0;\n+\t\tctx->clients[i]->container_offset = 0;\n+\t}\n+\tfor (size_t i=0; i < ctx->sender_peer_cnt; i++) {\n+\t\tctx->sender_peers[i]->container_count = 0;\n+\t\tctx->sender_peers[i]->container_offset = 0;\n \t}\n \treturn size;\n }\n@@ -1037,35 +600,10 @@ void rist_prometheus_sender_add_peer(struct rist_prometheus_stats *ctx, uint64_t\n }\n \n #if HAVE_SOCK_UN_H\n-/* Loops on partial writes, retries EINTR, and uses MSG_NOSIGNAL when\n- * available so a closed peer cannot SIGPIPE the exporter.  Returns the\n- * number of bytes actually delivered. */\n-static ssize_t prometheus_write_all(int fd, const void *buf, size_t len) {\n-\tconst char *p = (const char *)buf;\n-\tsize_t remaining = len;\n-\twhile (remaining > 0) {\n-#ifdef MSG_NOSIGNAL\n-\t\tssize_t n = send(fd, p, remaining, MSG_NOSIGNAL);\n-#else\n-\t\tssize_t n = write(fd, p, remaining);\n-#endif\n-\t\tif (n < 0) {\n-\t\t\tif (errno == EINTR)\n-\t\t\t\tcontinue;\n-\t\t\treturn (ssize_t)(len - remaining);\n-\t\t}\n-\t\tif (n == 0)\n-\t\t\tbreak;\n-\t\tremaining -= (size_t)n;\n-\t\tp += n;\n-\t}\n-\treturn (ssize_t)(len - remaining);\n-}\n-\n void *rist_prometheus_stats_unix_socket_thread(void *arg) {\n \tstruct rist_prometheus_stats * ctx = (struct rist_prometheus_stats *)arg;\n \tfprintf(stderr, \"Starting unix socket thread\\n\");\n-\tint ret = listen(ctx->fd, SOMAXCONN);\n+\tint ret = listen(ctx->fd, 1);\n \tif (ret != 0) {\n \t\tfprintf(stderr, \"Error %s listening on unix socket\\n\", strerror(errno));\n \t\treturn NULL;\n@@ -1073,32 +611,13 @@ void *rist_prometheus_stats_unix_socket_thread(void *arg) {\n \twhile (true) {\n \t\tint fd = accept(ctx->fd, NULL, NULL);\n \t\tif (fd < 0) {\n-\t\t\tif (errno == EINTR)\n-\t\t\t\tcontinue;\n \t\t\tbreak;\n \t\t}\n \t\tpthread_mutex_lock(&ctx->lock);\n \t\tint size = rist_prometheus_stats_format(ctx);\n-\t\tssize_t wrote = (size > 0)\n-\t\t\t? prometheus_write_all(fd, ctx->format_buf, (size_t)size)\n-\t\t\t: 0;\n+\t\t(void)! write(fd, ctx->format_buf, size);\n \t\tpthread_mutex_unlock(&ctx->lock);\n-\t\t(void)wrote; /* best-effort; client may have hung up */\n-\t\t/* Linux close() of an AF_UNIX SOCK_STREAM with unread RX is\n-\t\t * abortive (RST, drops the TX queue we just wrote into).\n-\t\t * Drain RX (bounded by SO_RCVTIMEO) so close() does a clean\n-\t\t * FIN-FIN handshake. */\n-\t\tshutdown(fd, SHUT_WR);\n-\t\t{\n-\t\t\tstruct timeval tv = { .tv_sec = 0, .tv_usec = 200000 };\n-\t\t\tsetsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));\n-\t\t\tchar drain[512];\n-\t\t\twhile (1) {\n-\t\t\t\tssize_t r = recv(fd, drain, sizeof(drain), 0);\n-\t\t\t\tif (r <= 0)\n-\t\t\t\t\tbreak;\n-\t\t\t}\n-\t\t}\n+\t\tshutdown(fd, SHUT_RDWR);\n #ifndef _WIN32\n \t\tclose(fd);\n #else\n@@ -1290,11 +809,6 @@ void rist_prometheus_stats_destroy(struct rist_prometheus_stats *ctx) {\n \t\tfree(ctx->sender_peers[i]);\n \t}\n \tfree(ctx->sender_peers);\n-\tfor (size_t i=0; i < ctx->receiver_peer_cnt; i++) {\n-\t\tfree(ctx->receiver_peers[i]->tags);\n-\t\tfree(ctx->receiver_peers[i]);\n-\t}\n-\tfree(ctx->receiver_peers);\n \n #if HAVE_LIBMICROHTTPD\n \tif (ctx->httpd != NULL)\ndiff --git a/tools/rist2rist.c b/tools/rist2rist.c\nindex 011f992..7f2a23e 100644\n--- a/tools/rist2rist.c\n+++ b/tools/rist2rist.c\n@@ -29,7 +29,6 @@\n #include <unistd.h>\n #endif\n #include \"oob_shared.h\"\n-#include \"string-shim.h\"\n \n #define RIST2RIST_VERSION \"31\"\n \ndiff --git a/tools/ristreceiver.c b/tools/ristreceiver.c\nindex 06203d5..30a16dc 100644\n--- a/tools/ristreceiver.c\n+++ b/tools/ristreceiver.c\n@@ -35,7 +35,10 @@\n #include <sys/ioctl.h>\n #endif\n #include \"yamlparse.h\"\n-#include \"string-shim.h\"\n+\n+#if defined(_WIN32) || defined(_WIN64)\n+# define strtok_r strtok_s\n+#endif\n \n #define RISTRECEIVER_VERSION \"30\"\n \n@@ -50,15 +53,15 @@\n pthread_mutex_t signal_lock;\n static int signalReceived = 0;\n static struct rist_logging_settings logging_settings = LOGGING_SETTINGS_INITIALIZER;\n-enum rist_profile profile = RIST_DEFAULT_PROFILE;\n+enum rist_profile profile = RIST_PROFILE_MAIN;\n static int peer_connected_count = 0;\n static int noflow_counter = 0;\n \n #if HAVE_PROMETHEUS_SUPPORT\n struct rist_prometheus_stats *prom_stats_ctx;\n-int prometheus_multipoint = 0;\n-int prometheus_nocreated = 0;\n-int prometheus_httpd = 0;\n+bool prometheus_multipoint = false;\n+bool prometheus_nocreated = false;\n+bool prometheus_httpd = false;\n bool enable_prometheus = false;\n char *prometheus_tags = NULL;\n uint16_t prometheus_port = 9100;\n@@ -83,16 +86,15 @@ static struct option long_options[] = {\n #endif\n { \"config\",          required_argument, NULL, 'c' },\n { \"session-timeout-exit\",  no_argument, NULL, 'x' },\n-{ \"cbr-output\",      required_argument, NULL, 5 },\n { \"help\",            no_argument,       NULL, 'h' },\n { \"help-url\",        no_argument,       NULL, 'u' },\n #if HAVE_PROMETHEUS_SUPPORT\n { \"enable-metrics\",  no_argument,       NULL, 'M' },\n { \"metrics-tags\",    required_argument, NULL, 1 },\n-{ \"metrics-multipoint\",no_argument,     &prometheus_multipoint, 1 },\n-{ \"metrics-nocreated\",no_argument,      &prometheus_nocreated, 1 },\n+{ \"metrics-multipoint\",no_argument,     (int*)&prometheus_multipoint, true },\n+{ \"metrics-nocreated\",no_argument,      (int*)&prometheus_nocreated, true },\n #if HAVE_LIBMICROHTTPD\n-{ \"metrics-http\",    no_argument,      &prometheus_httpd, 1 },\n+{ \"metrics-http\",    no_argument,      (int*)&prometheus_httpd, true },\n { \"metrics-port\",    required_argument, NULL, 2 },\n { \"metrics-ip\",      required_argument, NULL, 3 },\n #endif //HAVE_LIBMICROHTTPD\n@@ -106,7 +108,6 @@ static struct option long_options[] = {\n const char help_str[] = \"Usage: %s [OPTIONS] \\nWhere OPTIONS are:\\n\"\n \"       -i | --inputurl  rist://...             * | Comma separated list of input rist URLs                  |\\n\"\n \"       -o | --outputurl udp://... or rtp://... * | Comma separated list of output udp or rtp URLs           |\\n\"\n-\"          | --cbr-output 0|1                     | Space the output at the stream rate (CBR consumers)     |\\n\"\n \"                                                 | Use tun://@ to write udp data to a tun device defined    |\\n\"\n \"                                                 | using the -t option                                      |\\n\"\n \"       -b | --buffer value                       | Default buffer size for packet retransmissions           |\\n\"\n@@ -214,7 +215,7 @@ static int cb_recv(void *arg, struct rist_data_block *b)\n \t\tstruct rist_udp_config *udp_config = callback_object->udp_config[i];\n \t\tbool found_it = false;\n \t\tint mux_mode = 0;\n-\t\tif (udp_config->version >= 1)\n+\t\tif (udp_config->version == 1)\n \t\t\tmux_mode = udp_config->multiplex_mode;\n \t\t// The stream-id on the udp url gets translated into the virtual destination port of the GRE tunnel\n \t\t// and we match on that. The other two muxing modes are not spec compliant and are only\n@@ -267,12 +268,6 @@ static int cb_recv(void *arg, struct rist_data_block *b)\n \t\t\t\t\t// for now, forward it all\n \t\t\t\t\t// use output_udp_config->mux_filter\n \t\t\t\t\tsize_t ipheader_bytes = sizeof(struct ipheader) + sizeof(struct udpheader);\n-\t\t\t\t\tif (b->payload_len < ipheader_bytes) {\n-\t\t\t\t\t\trist_log(&logging_settings, RIST_LOG_ERROR,\n-\t\t\t\t\t\t\t\"Short ipv4-mux payload (%zu < %zu), dropping\\n\",\n-\t\t\t\t\t\t\tb->payload_len, ipheader_bytes);\n-\t\t\t\t\t\tcontinue;\n-\t\t\t\t\t}\n \t\t\t\t\tpayload = (uint8_t *)b->payload;\n \t\t\t\t\tpayload += ipheader_bytes;\n \t\t\t\t\tpayload_len = b->payload_len - ipheader_bytes;\n@@ -354,8 +349,8 @@ static int rist_validate_tun_data(uint8_t *buffer, ssize_t buffer_len)\n \t\tprotocol = (int) ip->iph_protocol;\n \t\tpayload_len = (ssize_t)be16toh(ip->iph_len);\n \t\tif (payload_len != buffer_len) {\n-\t\t\trist_log(&logging_settings, RIST_LOG_INFO, \"Malformed ipv4 packet %zd != %zd\\n\",\n-\t\t\t\tpayload_len, buffer_len);\n+\t\t\trist_log(&logging_settings, RIST_LOG_INFO, \"Malformed ipv4 packet %d != %d\\n\",\n+\t\t\t\tpayload_len != buffer_len);\n \t\t\treturn -1;\n \t\t}\n \t}\n@@ -507,7 +502,6 @@ int main(int argc, char *argv[])\n \tconst struct rist_peer_config *peer_input_config[MAX_INPUT_COUNT];\n \tchar *inputurl = NULL;\n \tchar *outputurl = NULL;\n-\tint cbr_output_cli = -1;   /* -1 = not asked for; URLs may still ask */\n \tchar *oobtun = NULL;\n \tchar *shared_secret = NULL;\n \tint buffer = 0;\n@@ -628,14 +622,6 @@ int main(int argc, char *argv[])\n \t\t\tprometheus_httpd = true;\n \t\t\tprometheus_ip = strdup(optarg);\n \t\t\tbreak;\n-\t\tcase 5:\n-\t\t\tif (strcmp(optarg, \"0\") && strcmp(optarg, \"1\")) {\n-\t\t\t\trist_log(&logging_settings, RIST_LOG_ERROR,\n-\t\t\t\t\t\"Invalid --cbr-output '%s' (expected 0 or 1)\\n\", optarg);\n-\t\t\t\texit(1);\n-\t\t\t}\n-\t\t\tcbr_output_cli = atoi(optarg);\n-\t\t\tbreak;\n \t\tcase 4:\n \t\t\t//prometheus unix socket long opt\n \t\t\tenable_prometheus = true;\n@@ -880,7 +866,7 @@ int main(int argc, char *argv[])\n \t\t// Now parse the address 127.0.0.1:5000\n \t\tchar hostname[200] = {0};\n \t\tint outputlisten;\n-\t\tuint16_t outputport = 0;\n+\t\tuint16_t outputport;\n \t\tif (udpsocket_parse_url((void *)udp_config->address, hostname, 200, &outputport, &outputlisten) || !outputport || strlen(hostname) == 0) {\n \t\t\trist_log(&logging_settings, RIST_LOG_ERROR, \"Could not parse output url %s\\n\", outputtoken);\n \t\t\tgoto next;\n@@ -902,19 +888,6 @@ next:\n \t\toutputtoken = strtok_r(NULL, \",\", &saveptr2);\n \t}\n \n-\t/* Output pacing, from the command line and from any output URL asking for\n-\t * it. Both go through the same setter, so a disagreement between them is\n-\t * refused there rather than resolved differently here. */\n-\tif (cbr_output_cli >= 0 && rist_receiver_set_cbr_output(ctx, cbr_output_cli != 0) != 0)\n-\t\texit(1);\n-\tfor (size_t i = 0; i < MAX_OUTPUT_COUNT; i++) {\n-\t\tstruct rist_udp_config *uc = callback_object.udp_config[i];\n-\t\tif (!uc || uc->version < 2 || !uc->cbr_output_set)\n-\t\t\tcontinue;\n-\t\tif (rist_receiver_set_cbr_output(ctx, uc->cbr_output != 0) != 0)\n-\t\t\texit(1);\n-\t}\n-\n \tif (!atleast_one_socket_opened) {\n \t\texit(1);\n \t}\ndiff --git a/tools/ristsender.c b/tools/ristsender.c\nindex a9767f1..c2825b4 100644\n--- a/tools/ristsender.c\n+++ b/tools/ristsender.c\n@@ -33,9 +33,9 @@\n #include <sys/ioctl.h>\n #endif\n #include \"yamlparse.h\"\n-#include \"string-shim.h\"\n \n #if defined(_WIN32) || defined(_WIN64)\n+#define strtok_r strtok_s\n #define MSG_DONTWAIT (0)\n #endif\n \n@@ -48,10 +48,7 @@\n \n static int signalReceived = 0;\n static int peer_connected_count = 0;\n-static bool blind_send = false;\n static struct rist_logging_settings logging_settings = LOGGING_SETTINGS_INITIALIZER;\n-static struct rist_callback_object *g_callback_objects = NULL;\n-static int g_callback_object_count = 0;\n \n uint64_t prometheus_id = 0;\n \n@@ -68,10 +65,6 @@ struct rist_callback_object {\n \tstruct rist_ctx_wrap *sender_ctx;\n \tstruct rist_udp_config *udp_config;\n \tuint8_t recv[RIST_MAX_PACKET_SIZE + 100];\n-\tbool mcast_deferred;\n-\tchar mcast_host[256];\n-\tuint16_t mcast_port;\n-\tchar mcast_miface[RIST_MAX_STRING_SHORT];\n };\n \n struct rist_callback_tun_object {\n@@ -102,9 +95,9 @@ struct rist_sender_args {\n \n #if HAVE_PROMETHEUS_SUPPORT\n struct rist_prometheus_stats *prom_stats_ctx;\n-int prometheus_multipoint = 0;\n-int prometheus_nocreated = 0;\n-int prometheus_httpd = 0;\n+bool prometheus_multipoint = false;\n+bool prometheus_nocreated = false;\n+bool prometheus_httpd = false;\n bool enable_prometheus = false;\n char *prometheus_tags = NULL;\n uint16_t prometheus_port = 9100;\n@@ -130,16 +123,15 @@ static struct option long_options[] = {\n #endif\n { \"fast-start\",      required_argument, NULL, 'f' },\n { \"config\",          required_argument, NULL, 'c' },\n-{ \"blind-send\",      no_argument,       NULL, 5 },\n { \"help\",            no_argument,       NULL, 'h' },\n { \"help-url\",        no_argument,       NULL, 'u' },\n #if HAVE_PROMETHEUS_SUPPORT\n { \"enable-metrics\",  no_argument,       NULL, 'M' },\n { \"metrics-tags\",    required_argument, NULL, 1 },\n-{ \"metrics-multipoint\",no_argument,     &prometheus_multipoint, 1 },\n-{ \"metrics-nocreated\",no_argument,      &prometheus_nocreated, 1 },\n+{ \"metrics-multipoint\",no_argument,     (int*)&prometheus_multipoint, true },\n+{ \"metrics-nocreated\",no_argument,      (int*)&prometheus_nocreated, true },\n #if HAVE_LIBMICROHTTPD\n-{ \"metrics-http\",    no_argument,      &prometheus_httpd, 1 },\n+{ \"metrics-http\",    no_argument,      (int*)&prometheus_httpd, true },\n { \"metrics-port\",    required_argument, NULL, 2 },\n { \"metrics-ip\",      required_argument, NULL, 3 },\n #endif //HAVE_LIBMICROHTTPD\n@@ -174,11 +166,7 @@ const char help_str[] = \"Usage: %s [OPTIONS] \\nWhere OPTIONS are:\\n\"\n \"                                                 | 1 = only non udp data is accepted (default)              |\\n\"\n \"                                                 | 2 = no data goes into or out of oob channel              |\\n\"\n \"       -f | --fast-start value                   | Controls data output flow before handshake is completed  |\\n\"\n-\"       -c | --config name.yaml                   | YAML config file                                         |\\n\";\n-// Split into two string literals: a single literal must stay under the\n-// C99 translation limit of 4095 chars (5.2.4.1).  The two parts are\n-// printed back-to-back, so the help output is unchanged.\n-const char help_str2[] =\n+\"       -c | --config name.yaml                   | YAML config file                                         |\\n\"\n //\"                                                 | -1 = hold data out and igmp source joins                 |\\n\"\n \"                                                 |  0 = hold data out                                       |\\n\"\n \"                                                 |  1 = start to send data immediately                      |\\n\"\n@@ -197,7 +185,6 @@ const char help_str2[] =\n \"          | --metrics-unix                       | Unix socket to expose metrics on                         |\\n\"\n #endif //HAVE_SOCK_UN_H\n #endif //HAVE_PROMETHEUS_SUPPORT\n-\"          | --blind-send                         | Stream regardless of peer state (fire-and-forget mode)   |\\n\"\n \"       -h | --help                               | Show this help                                           |\\n\"\n \"       -u | --help-url                           | Show all the possible url options                        |\\n\"\n \"   * == mandatory value \\n\"\n@@ -206,14 +193,16 @@ const char help_str2[] =\n \"       --statsinterval 1000      \\\\\\n\"\n \"       --verbose-level 6         \\n\";\n \n+/*\n static uint64_t risttools_convertRTPtoNTP(uint32_t i_rtp)\n {\n \tuint64_t i_ntp;\n-\tint32_t clock = 90000;\n-\ti_ntp = (uint64_t)i_rtp << 32;\n-\ti_ntp /= clock;\n+    int32_t clock = 90000;\n+    i_ntp = (uint64_t)i_rtp << 32;\n+    i_ntp /= clock;\n \treturn i_ntp;\n }\n+*/\n \n #if HAVE_SRP_SUPPORT\n \tchar *srpfile = NULL;\n@@ -254,21 +243,22 @@ static void input_udp_recv(struct evsocket_ctx *evctx, int fd, short revents, vo\n \t\t// If we wanted to be more accurate, we could use the kernel nic capture timestamp (linux)\n \t\tdata_block.ts_ntp = 0;\n \t\tdata_block.flags = 0;\n-\t\tuint8_t *rtp_hdr = recv_buf + ipheader_bytes;\n \t\tif (callback_object->udp_config->rtp_timestamp && recv_bufsize > 12)\n \t\t{\n-\t\t\tuint32_t rtp_time = ((uint32_t)rtp_hdr[4] << 24) |\n-\t\t\t                    ((uint32_t)rtp_hdr[5] << 16) |\n-\t\t\t                    ((uint32_t)rtp_hdr[6] << 8)  |\n-\t\t\t                    (uint32_t)rtp_hdr[7];\n-\t\t\tdata_block.ts_ntp = risttools_convertRTPtoNTP(rtp_time);\n+\t\t\t// Extract timestamp from rtp header\n+\t\t\t//uint32_t rtp_time = (recv_buf[4] << 24) | (recv_buf[5] << 16) | (recv_buf[6] << 8) | recv_buf[7];\n+\t\t\t// Convert to NTP (assumes 90Khz)\n+\t\t\t//data_block.ts_ntp = risttools_convertRTPtoNTP(rtp_time);\n+\t\t\t// TODO: Figure out why this does not work (commenting out for now)\n \t\t}\n \t\tif (callback_object->udp_config->rtp_sequence && recv_bufsize > 12)\n \t\t{\n-\t\t\tdata_block.seq = (uint64_t)(((uint16_t)rtp_hdr[2] << 8) | rtp_hdr[3]);\n-\t\t\tdata_block.flags = RIST_DATA_FLAGS_USE_SEQ;\n+\t\t\t// Extract sequence number from rtp header\n+\t\t\t//data_block.seq = (uint64_t)((recv_buf[2] << 8) | recv_buf[3]);\n+\t\t\t//data_block.flags = RIST_DATA_FLAGS_USE_SEQ;\n+\t\t\t// TODO: Figure out why this does not work (commenting out for now)\n \t\t}\n-\t\tif (callback_object->udp_config->version >= 1 && callback_object->udp_config->multiplex_mode == LIBRIST_MULTIPLEX_MODE_IPV4) {\n+\t\tif (callback_object->udp_config->version == 1 && callback_object->udp_config->multiplex_mode == LIBRIST_MULTIPLEX_MODE_IPV4) {\n \t\t\tdata_block.virt_src_port = UINT16_MAX;\n \t\t\tdata_block.payload = recv_buf + offset;\n \t\t\tdata_block.payload_len = recv_bufsize - offset + ipheader_bytes;\n@@ -278,13 +268,13 @@ static void input_udp_recv(struct evsocket_ctx *evctx, int fd, short revents, vo\n \t\t\t// rtp header will not be stripped out in IPV4 mux mode\n \t\t\tif (callback_object->udp_config->rtp && recv_bufsize > 12)\n \t\t\t\toffset = 12; // TODO: check for header extensions and remove them as well\n-\t\t\tif (callback_object->udp_config->version >= 1 && callback_object->udp_config->multiplex_mode == LIBRIST_MULTIPLEX_MODE_VIRT_SOURCE_PORT) {\n+\t\t\tif (callback_object->udp_config->version == 1 && callback_object->udp_config->multiplex_mode == LIBRIST_MULTIPLEX_MODE_VIRT_SOURCE_PORT) {\n \t\t\t\tdata_block.virt_src_port = callback_object->udp_config->stream_id;\n \t\t\t}\n \t\t\tdata_block.payload = recv_buf + offset + ipheader_bytes;\n \t\t\tdata_block.payload_len = recv_bufsize - offset;\n \t\t}\n-\t\tif (peer_connected_count || blind_send) {\n+\t\tif (peer_connected_count) {\n \t\t\tif (rist_sender_data_write(callback_object->sender_ctx->ctx, &data_block) < 0)\n \t\t\t\trist_log(&logging_settings, RIST_LOG_ERROR, \"Error writing data in input_udp_recv, socket=%d\\n\", callback_object->sd);\n \t\t}\n@@ -308,39 +298,17 @@ static void input_udp_sockerr(struct evsocket_ctx *evctx, int fd, short revents,\n \n static void usage(char *cmd)\n {\n-\trist_log(&logging_settings, RIST_LOG_INFO, \"%s\\n%s%s version %s libRIST library: %s API version: %s\\n\", cmd, help_str, help_str2, RISTSENDER_VERSION, librist_version(), librist_api_version());\n+\trist_log(&logging_settings, RIST_LOG_INFO, \"%s\\n%s version %s libRIST library: %s API version: %s\\n\", cmd, help_str, RISTSENDER_VERSION, librist_version(), librist_api_version());\n \texit(1);\n }\n \n static void connection_status_callback(void *arg, struct rist_peer *peer, enum rist_connection_status peer_connection_status)\n {\n \t(void)arg;\n-\tif (peer_connection_status == RIST_CONNECTION_ESTABLISHED || peer_connection_status == RIST_CLIENT_CONNECTED) {\n+\tif (peer_connection_status == RIST_CONNECTION_ESTABLISHED || peer_connection_status == RIST_CLIENT_CONNECTED)\n \t\tpeer_connected_count++;\n-\t\tfor (int j = 0; j < g_callback_object_count; j++) {\n-\t\t\tif (g_callback_objects[j].mcast_deferred) {\n-\t\t\t\tstruct sockaddr_in6 sa;\n-\t\t\t\tmemset(&sa, 0, sizeof(sa));\n-\t\t\t\tif (udpsocket_resolve_host(g_callback_objects[j].mcast_host,\n-\t\t\t\t    g_callback_objects[j].mcast_port, (struct sockaddr *)&sa) == 0) {\n-\t\t\t\tif (udpsocket_join_mcast_group(g_callback_objects[j].sd,\n-\t\t\t\t    g_callback_objects[j].mcast_miface,\n-\t\t\t\t    (struct sockaddr *)&sa, sa.sin6_family, NULL) == 0) {\n-\t\t\t\t\t\trist_log(&logging_settings, RIST_LOG_INFO,\n-\t\t\t\t\t\t\t\"Deferred multicast join for %s succeeded\\n\",\n-\t\t\t\t\t\t\tg_callback_objects[j].mcast_host);\n-\t\t\t\t\t} else {\n-\t\t\t\t\t\trist_log(&logging_settings, RIST_LOG_ERROR,\n-\t\t\t\t\t\t\t\"Deferred multicast join for %s failed\\n\",\n-\t\t\t\t\t\t\tg_callback_objects[j].mcast_host);\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t\tg_callback_objects[j].mcast_deferred = false;\n-\t\t\t}\n-\t\t}\n-\t} else {\n+\telse\n \t\tpeer_connected_count--;\n-\t}\n \trist_log(&logging_settings, RIST_LOG_INFO,\"Connection Status changed for Peer %\"PRIu64\", new status is %d, peer connected count is %d\\n\",\n \t\t\t\tpeer, peer_connection_status, peer_connected_count);\n }\n@@ -391,8 +359,8 @@ static int rist_validate_tun_data(uint8_t *buffer, ssize_t buffer_len)\n \t\tprotocol = (int) ip->iph_protocol;\n \t\tpayload_len = (ssize_t)be16toh(ip->iph_len);\n \t\tif (payload_len != buffer_len) {\n-\t\t\trist_log(&logging_settings, RIST_LOG_INFO, \"Malformed ipv4 packet %zd != %zd\\n\",\n-\t\t\t\tpayload_len, buffer_len);\n+\t\t\trist_log(&logging_settings, RIST_LOG_INFO, \"Malformed ipv4 packet %d != %d\\n\",\n+\t\t\t\tpayload_len != buffer_len);\n \t\t\treturn -1;\n \t\t}\n \t}\n@@ -478,6 +446,11 @@ static struct rist_peer* setup_rist_peer(struct rist_ctx_wrap *w, struct rist_se\n \t\t}\n \t}\n \n+\tif (rist_sender_stats_callback_set(ctx, setup->statsinterval, sender_stats_callback, NULL) == -1) {\n+\t\trist_log(&logging_settings, RIST_LOG_ERROR, \"Could not enable stats callback\\n\");\n+\t\treturn NULL;\n+\t}\n+\n \t// Rely on the library to parse the url\n \tstruct rist_peer_config *peer_config_link = NULL;\n \tif (rist_parse_address2(setup->token, (void *)&peer_config_link))\n@@ -714,7 +687,7 @@ int main(int argc, char *argv[])\n \tint buffer_size = 0;\n \tint encryption_type = 0;\n \tint statsinterval = 1000;\n-\tenum rist_profile profile = RIST_DEFAULT_PROFILE;\n+\tenum rist_profile profile = RIST_PROFILE_MAIN;\n \tenum rist_log_level loglevel = RIST_LOG_INFO;\n \tbool npd = false;\n \tint faststart = 0;\n@@ -883,9 +856,6 @@ int main(int argc, char *argv[])\n #endif\n \t\t\tcleanup_tools_config(yaml_config);\n \t\t\tbreak;\n-\t\tcase 5:\n-\t\t\tblind_send = true;\n-\t\t\tbreak;\n \t\tcase 'h':\n \t\t\t/* Fall through */\n \t\tdefault:\n@@ -990,7 +960,7 @@ int main(int argc, char *argv[])\n \n \t\t// Setup the output rist objects\n \t\tif (rist_listens && i > 0) {\n-\t\t\tif (callback_object[0].udp_config->version >= 1 && (callback_object[0].udp_config->multiplex_mode == LIBRIST_MULTIPLEX_MODE_VIRT_DESTINATION_PORT || udp_config->multiplex_mode == LIBRIST_MULTIPLEX_MODE_VIRT_DESTINATION_PORT)) {\n+\t\t\tif (callback_object[0].udp_config->version == 1 && (callback_object[0].udp_config->multiplex_mode == LIBRIST_MULTIPLEX_MODE_VIRT_DESTINATION_PORT || udp_config->multiplex_mode == LIBRIST_MULTIPLEX_MODE_VIRT_DESTINATION_PORT)) {\n \t\t\t\trist_log(&logging_settings, RIST_LOG_ERROR, \"Multiplexing is not allowed when any peer is in listening mode unless you enable non standard muxing on all inputs\\n\");\n \t\t\t\tgoto shutdown;\n \t\t\t}\n@@ -1039,100 +1009,22 @@ int main(int argc, char *argv[])\n \t\t\t// This is a udp input, i.e. 127.0.0.1:5000\n \t\t\tchar hostname[200] = {0};\n \t\t\tint inputlisten;\n-\t\t\tuint16_t inputport = 0;\n+\t\t\tuint16_t inputport;\n \t\t\tif (udpsocket_parse_url((void *)udp_config->address, hostname, 200, &inputport, &inputlisten) || !inputport || strlen(hostname) == 0) {\n \t\t\t\trist_log(&logging_settings, RIST_LOG_ERROR, \"Could not parse input url %s\\n\", inputtoken);\n \t\t\t\tgoto next;\n \t\t\t}\n \t\t\trist_log(&logging_settings, RIST_LOG_INFO, \"URL parsed successfully: Host %s, Port %d\\n\", (char *) hostname, inputport);\n \n-\t\t\tint is_mcast = 0;\n-\t\t\t{\n-\t\t\t\tstruct sockaddr_in6 probe;\n-\t\t\t\tmemset(&probe, 0, sizeof(probe));\n-\t\t\t\tif (udpsocket_resolve_host(hostname, inputport, (struct sockaddr *)&probe) == 0) {\n-\t\t\t\t\tif (probe.sin6_family == AF_INET6)\n-\t\t\t\t\t\tis_mcast = IN6_IS_ADDR_MULTICAST(&probe.sin6_addr);\n-\t\t\t\t\telse\n-\t\t\t\t\t\tis_mcast = IN_MULTICAST(ntohl(((struct sockaddr_in *)&probe)->sin_addr.s_addr));\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tif (is_mcast) {\n-\t\t\t\tstruct sockaddr_in6 mcast_sa;\n-\t\t\t\tmemset(&mcast_sa, 0, sizeof(mcast_sa));\n-\t\t\t\tif (udpsocket_resolve_host(hostname, inputport, (struct sockaddr *)&mcast_sa) < 0) {\n-\t\t\t\t\trist_log(&logging_settings, RIST_LOG_ERROR, \"Could not resolve %s\\n\", hostname);\n-\t\t\t\t\tgoto next;\n-\t\t\t\t}\n-\t\t\t\tuint16_t af = mcast_sa.sin6_family;\n-\t\t\t\tint sd = udpsocket_open(af);\n-\t\t\t\tif (sd < 0) {\n-\t\t\t\t\trist_log(&logging_settings, RIST_LOG_ERROR, \"Could not open socket for %s\\n\", hostname);\n-\t\t\t\t\tgoto next;\n-\t\t\t\t}\n-\t\t\t\tint yes_val = 1;\n-\t\t\t\tsetsockopt(sd, SOL_SOCKET, SO_REUSEADDR, (const char *)&yes_val, sizeof(yes_val));\n-\t\t\t\tstruct sockaddr_storage bind_addr;\n-\t\t\t\tsocklen_t bind_len;\n-\t\t\t\tmemset(&bind_addr, 0, sizeof(bind_addr));\n-\t\t\t\tif (af == AF_INET6) {\n-\t\t\t\t\tstruct sockaddr_in6 *a6 = (struct sockaddr_in6 *)&bind_addr;\n-\t\t\t\t\ta6->sin6_family = AF_INET6;\n-\t\t\t\t\ta6->sin6_port = htons(inputport);\n-\t\t\t\t\ta6->sin6_addr = in6addr_any;\n-\t\t\t\t\tbind_len = sizeof(struct sockaddr_in6);\n-\t\t\t\t} else {\n-\t\t\t\t\tstruct sockaddr_in *a4 = (struct sockaddr_in *)&bind_addr;\n-\t\t\t\t\ta4->sin_family = AF_INET;\n-\t\t\t\t\ta4->sin_port = htons(inputport);\n-\t\t\t\t\ta4->sin_addr.s_addr = INADDR_ANY;\n-\t\t\t\t\tbind_len = sizeof(struct sockaddr_in);\n-\t\t\t\t}\n-\t\t\t\tif (bind(sd, (struct sockaddr *)&bind_addr, bind_len) < 0) {\n-#ifdef _WIN32\n-\t\t\t\t\trist_log(&logging_settings, RIST_LOG_ERROR, \"Could not bind to port %d: WSAGetLastError=%d\\n\", inputport, WSAGetLastError());\n-#else\n-\t\t\t\t\trist_log(&logging_settings, RIST_LOG_ERROR, \"Could not bind to port %d: %s\\n\", inputport, strerror(errno));\n-#endif\n-\t\t\t\t\tudpsocket_close(sd);\n-\t\t\t\t\tgoto next;\n-\t\t\t\t}\n-\t\t\t\tcallback_object[i].sd = sd;\n-\t\t\t\tif (blind_send) {\n-\t\t\t\t\tif (udpsocket_join_mcast_group(sd, udp_config->miface,\n-\t\t\t\t\t    (struct sockaddr *)&mcast_sa, af, NULL) != 0) {\n-\t\t\t\t\t\trist_log(&logging_settings, RIST_LOG_ERROR,\n-\t\t\t\t\t\t\t\"Could not join multicast group %s on %s\\n\",\n-\t\t\t\t\t\t\thostname, udp_config->miface[0] ? udp_config->miface : \"default\");\n-\t\t\t\t\t\tudpsocket_close(sd);\n-\t\t\t\t\t\tgoto next;\n-\t\t\t\t\t}\n-\t\t\t\t\trist_log(&logging_settings, RIST_LOG_INFO,\n-\t\t\t\t\t\t\"Multicast input %s:%d joined immediately (--blind-send)\\n\",\n-\t\t\t\t\t\thostname, inputport);\n-\t\t\t\t} else {\n-\t\t\t\t\tcallback_object[i].mcast_deferred = true;\n-\t\t\t\t\tsnprintf(callback_object[i].mcast_host, sizeof(callback_object[i].mcast_host), \"%s\", hostname);\n-\t\t\t\t\tcallback_object[i].mcast_port = inputport;\n-\t\t\t\t\tsnprintf(callback_object[i].mcast_miface, sizeof(callback_object[i].mcast_miface), \"%s\", udp_config->miface);\n-\t\t\t\t\trist_log(&logging_settings, RIST_LOG_INFO,\n-\t\t\t\t\t\t\"Multicast input %s:%d bound, join deferred until peer handshake\\n\",\n-\t\t\t\t\t\thostname, inputport);\n-\t\t\t\t}\n+\t\t\tcallback_object[i].sd = udpsocket_open_bind(hostname, inputport, udp_config->miface);\n+\t\t\tif (callback_object[i].sd < 0) {\n+\t\t\t\trist_log(&logging_settings, RIST_LOG_ERROR, \"Could not bind to: Host %s, Port %d, miface %s.\\n\",\n+\t\t\t\t\t(char *) hostname, inputport, udp_config->miface);\n+\t\t\t\tgoto next;\n+\t\t\t} else {\n \t\t\t\tudpsocket_set_nonblocking(callback_object[i].sd);\n+\t\t\t\trist_log(&logging_settings, RIST_LOG_INFO, \"Input socket is open and bound %s:%d\\n\", (char *) hostname, inputport);\n \t\t\t\tatleast_one_socket_opened = true;\n-\t\t\t} else {\n-\t\t\t\tcallback_object[i].sd = udpsocket_open_bind(hostname, inputport, udp_config->miface);\n-\t\t\t\tif (callback_object[i].sd < 0) {\n-\t\t\t\t\trist_log(&logging_settings, RIST_LOG_ERROR, \"Could not bind to: Host %s, Port %d, miface %s.\\n\",\n-\t\t\t\t\t\t(char *) hostname, inputport, udp_config->miface);\n-\t\t\t\t\tgoto next;\n-\t\t\t\t} else {\n-\t\t\t\t\tudpsocket_set_nonblocking(callback_object[i].sd);\n-\t\t\t\t\trist_log(&logging_settings, RIST_LOG_INFO, \"Input socket is open and bound %s:%d\\n\", (char *) hostname, inputport);\n-\t\t\t\t\tatleast_one_socket_opened = true;\n-\t\t\t\t}\n \t\t\t}\n \t\t\tcallback_object[i].udp_config = udp_config;\n \t\t\tudp_config = NULL;\n@@ -1149,9 +1041,6 @@ next:\n \t\tgoto shutdown;\n \t}\n \n-\tg_callback_objects = callback_object;\n-\tg_callback_object_count = MAX_INPUT_COUNT;\n-\n \tif (evctx && pthread_create(&thread_main_loop[0], NULL, input_loop, (void *)callback_object) != 0)\n \t{\n \t\trist_log(&logging_settings, RIST_LOG_ERROR, \"Could not start udp receiver thread\\n\");\ndiff --git a/tools/risttunnel.c b/tools/risttunnel.c\nindex 11971df..5d9f355 100644\n--- a/tools/risttunnel.c\n+++ b/tools/risttunnel.c\n@@ -12,8 +12,6 @@\n  */\n \n #include <librist/librist.h>\n-#include <librist/oob.h>\n-#include <librist/tun.h>\n #include \"librist/version.h\"\n #include \"config.h\"\n #if HAVE_SRP_SUPPORT\n@@ -22,19 +20,16 @@\n #endif\n #include \"vcs_version.h\"\n #include \"risturlhelp.h\"\n-#include \"pthread-shim.h\"\n \n #include <stdio.h>\n #include <stdlib.h>\n #include <string.h>\n-#include <stdint.h>\n #include <signal.h>\n #include <stdbool.h>\n #include \"getopt-shim.h\"\n \n #if defined(__unix) || defined(__APPLE__)\n #include <unistd.h>\n-#include <poll.h>\n #endif\n #ifdef _WIN32\n #include <windows.h>\n@@ -62,89 +57,6 @@ static int cb_stats(void *arg, const struct rist_stats *stats_container)\n \treturn 0;\n }\n \n-/* ---- single-port mode (one UDP port, bidirectional) -------------------\n- * A single RIST connection carries both directions on one socket:\n- *   - forward  rides the RTP data_fd path  -> ARQ-protected\n- *   - reverse  rides the RIST OOB channel  -> best-effort (no ARQ)\n- *\n- * Roles are asymmetric (one connection only has one sender + one receiver):\n- *   caller   (sender side):   data_fd reads TUN -> RTP (forward out);\n- *                             OOB callback writes inbound OOB -> TUN (reverse in)\n- *   listener (receiver side): data_fd writes inbound RTP -> TUN (forward in);\n- *                             a reader thread reads TUN -> OOB (reverse out)\n- *\n- * The heavy direction of a support tunnel (device -> concentrator: shell\n- * output, log/file pulls) is the forward/RTP/ARQ-protected path; the light\n- * reverse direction (keystrokes/commands) tolerates best-effort OOB.\n- */\n-struct sp_reverse {\n-\tstruct rist_ctx *ctx;            /* context that owns the OOB channel */\n-\tstruct rist_peer *volatile peer; /* connected remote peer (listener side) */\n-\tint tun_fd;\n-\tint mtu;\n-};\n-\n-/* OOB received from the peer -> inject into the local TUN. */\n-static int sp_oob_to_tun(void *arg, const struct rist_oob_block *oob_block)\n-{\n-\tint tun_fd = *(int *)arg;\n-\tif (oob_block && oob_block->payload && oob_block->payload_len > 0)\n-\t\trist_tun_write(tun_fd, (const uint8_t *)oob_block->payload,\n-\t\t               oob_block->payload_len);\n-\treturn 0;\n-}\n-\n-/* Listener side: librist only auto-learns oob_current_peer from *received*\n- * OOB. The caller sends only forward RTP, so capture the connecting peer\n- * here and hand it explicitly to rist_oob_write() for the reverse path. */\n-static int sp_conn_cb(void *arg, const char *connecting_ip, uint16_t connecting_port,\n-                      const char *local_ip, uint16_t local_port, struct rist_peer *peer)\n-{\n-\t(void)connecting_ip; (void)connecting_port; (void)local_ip; (void)local_port;\n-\tstruct sp_reverse *r = (struct sp_reverse *)arg;\n-\tif (r)\n-\t\tr->peer = peer;\n-\treturn 0;\n-}\n-\n-static int sp_disconn_cb(void *arg, struct rist_peer *peer)\n-{\n-\tstruct sp_reverse *r = (struct sp_reverse *)arg;\n-\tif (r && r->peer == peer)\n-\t\tr->peer = NULL;\n-\treturn 0;\n-}\n-\n-/* Read packets off the local TUN -> send to the peer over OOB. */\n-static PTHREAD_START_FUNC(sp_tun_to_oob, arg)\n-{\n-\tstruct sp_reverse *r = (struct sp_reverse *)arg;\n-\tsize_t bufsize = (r->mtu > 0 ? (size_t)r->mtu : 1500) + 64;\n-\tuint8_t *buf = malloc(bufsize);\n-\tif (!buf)\n-\t\treturn 0;\n-\twhile (keep_running) {\n-#if defined(__unix) || defined(__APPLE__)\n-\t\tstruct pollfd pfd = { .fd = r->tun_fd, .events = POLLIN, .revents = 0 };\n-\t\tif (poll(&pfd, 1, 200) <= 0)\n-\t\t\tcontinue;   /* timeout -> re-check keep_running */\n-#endif\n-\t\tint n = rist_tun_read(r->tun_fd, buf, bufsize);\n-\t\tif (n <= 0)\n-\t\t\tcontinue;\n-\t\tif (!r->peer)\n-\t\t\tcontinue;            /* no peer connected yet -> drop */\n-\t\tstruct rist_oob_block ob;\n-\t\tmemset(&ob, 0, sizeof(ob));\n-\t\tob.peer = r->peer;         /* explicit reverse destination */\n-\t\tob.payload = buf;\n-\t\tob.payload_len = (size_t)n;\n-\t\trist_oob_write(r->ctx, &ob);\n-\t}\n-\tfree(buf);\n-\treturn 0;\n-}\n-\n #if HAVE_SRP_SUPPORT\n static int cb_auth_connect(void *arg, const char *connecting_ip,\n                            uint16_t connecting_port, const char *local_ip,\n@@ -171,7 +83,7 @@ static void usage(const char *name)\n \t\t\"IP tunnel over RIST with ARQ packet recovery\\n\\n\"\n \t\t\"Usage: %s [options]\\n\\n\"\n \t\t\"Required:\\n\"\n-\t\t\"  -l, --local-ip IP/PREFIX    Local tunnel IP (e.g. 10.0.0.1/24 or fd00::1/64)\\n\"\n+\t\t\"  -l, --local-ip IP/PREFIX    Local tunnel IP (e.g. 10.0.0.1/24)\\n\"\n \t\t\"  -o, --output-url URL        RIST URL for sending\\n\"\n \t\t\"  -b, --input-url URL         RIST URL for receiving\\n\\n\"\n \t\t\"Optional:\\n\"\n@@ -182,29 +94,43 @@ static void usage(const char *name)\n \t\t\"  -e, --encryption-type INT   AES type: 128 or 256 (default: 128)\\n\"\n \t\t\"  -S, --statsinterval INT     Stats interval in ms (default: %d, 0=off)\\n\"\n \t\t\"  -v, --verbose-level INT     Log level 0-6 (default: 3)\\n\"\n-\t\t\"  -p, --profile INT           RIST profile: 0=simple, 1=main, 2=advanced (default: 2)\\n\"\n-\t\t\"  -1, --single-port           One UDP port (forward=RTP/ARQ, reverse=OOB).\\n\"\n-\t\t\"                              Give exactly one of -o (caller) or -b (listener).\\n\"\n+\t\t\"  -p, --profile INT           RIST profile: 0=simple, 1=main (default: 1)\\n\"\n #if HAVE_SRP_SUPPORT\n \t\t\"  -F, --srpfile PATH          EAP-SRP verifier file (server side)\\n\"\n #endif\n \t\t\"  -h, --help                  Show this help\\n\\n\"\n-\t\t\"Example (server / listener side, two ports):\\n\"\n+\t\t\"Example (server / listener side):\\n\"\n \t\t\"  sudo %s -l 10.0.0.1/24 -b rist://@:5000 -o rist://@:5001\\n\\n\"\n-\t\t\"Example (client / caller side, two ports):\\n\"\n+\t\t\"Example (client / caller side):\\n\"\n \t\t\"  sudo %s -l 10.0.0.2/24 -b rist://server:5001 -o rist://server:5000\\n\\n\"\n-\t\t\"Example (single port, listener):  sudo %s -1 -l 10.0.0.1/24 -b rist://@:443\\n\"\n-\t\t\"Example (single port, caller):    sudo %s -1 -l 10.0.0.2/24 -o rist://server:443\\n\\n\"\n \t\t\"%s\",\n \t\tRISTTUNNEL_VERSION, librist_version(), librist_api_version(),\n \t\tname, DEFAULT_MTU, DEFAULT_RTT_MULTIPLIER, DEFAULT_STATS_INTERVAL,\n-\t\tname, name, name, name, help_urlstr);\n+\t\tname, name, help_urlstr);\n \texit(1);\n }\n \n static int parse_ip_prefix(const char *cidr, char *ip_out, size_t ip_len, int *prefix_out)\n {\n-\treturn rist_tun_parse_cidr(cidr, ip_out, ip_len, prefix_out);\n+\tconst char *slash = strchr(cidr, '/');\n+\tif (!slash) {\n+\t\tstrncpy(ip_out, cidr, ip_len - 1);\n+\t\t*prefix_out = 32;\n+\t\treturn 0;\n+\t}\n+\n+\tsize_t ip_part_len = (size_t)(slash - cidr);\n+\tif (ip_part_len >= ip_len)\n+\t\treturn -1;\n+\n+\tmemcpy(ip_out, cidr, ip_part_len);\n+\tip_out[ip_part_len] = '\\0';\n+\t*prefix_out = atoi(slash + 1);\n+\n+\tif (*prefix_out < 0 || *prefix_out > 32)\n+\t\treturn -1;\n+\n+\treturn 0;\n }\n \n int main(int argc, char *argv[])\n@@ -223,14 +149,10 @@ int main(int argc, char *argv[])\n \tint encryption_type = 0;\n \tint statsinterval = DEFAULT_STATS_INTERVAL;\n \tenum rist_log_level loglevel = RIST_LOG_WARN;\n-\tenum rist_profile profile = RIST_DEFAULT_PROFILE;\n+\tenum rist_profile profile = RIST_PROFILE_MAIN;\n \tstruct rist_ctx *sender_ctx = NULL;\n \tstruct rist_ctx *receiver_ctx = NULL;\n \tint tun_fd = -1;\n-\tint single_port = 0;\n-\tpthread_t sp_thread = 0;\n-\tint sp_thread_started = 0;\n-\tstruct sp_reverse sp_rev = { 0 };\n \n \tstatic struct option long_options[] = {\n \t\t{ \"local-ip\",        required_argument, NULL, 'l' },\n@@ -244,7 +166,6 @@ int main(int argc, char *argv[])\n \t\t{ \"statsinterval\",   required_argument, NULL, 'S' },\n \t\t{ \"verbose-level\",   required_argument, NULL, 'v' },\n \t\t{ \"profile\",         required_argument, NULL, 'p' },\n-\t\t{ \"single-port\",     no_argument,       NULL, '1' },\n #if HAVE_SRP_SUPPORT\n \t\t{ \"srpfile\",         required_argument, NULL, 'F' },\n #endif\n@@ -253,9 +174,9 @@ int main(int argc, char *argv[])\n \t};\n \n #if HAVE_SRP_SUPPORT\n-\tconst char *optstring = \"l:i:o:b:m:M:s:e:S:v:p:1F:h\";\n+\tconst char *optstring = \"l:i:o:b:m:M:s:e:S:v:p:F:h\";\n #else\n-\tconst char *optstring = \"l:i:o:b:m:M:s:e:S:v:p:1h\";\n+\tconst char *optstring = \"l:i:o:b:m:M:s:e:S:v:p:h\";\n #endif\n \n \tint opt;\n@@ -272,7 +193,6 @@ int main(int argc, char *argv[])\n \t\tcase 'S': statsinterval = atoi(optarg); break;\n \t\tcase 'v': loglevel = (enum rist_log_level)atoi(optarg); break;\n \t\tcase 'p': profile = (enum rist_profile)atoi(optarg); break;\n-\t\tcase '1': single_port = 1; break;\n #if HAVE_SRP_SUPPORT\n \t\tcase 'F': srpfile = strdup(optarg); break;\n #endif\n@@ -282,54 +202,32 @@ int main(int argc, char *argv[])\n \t\t}\n \t}\n \n-\tif (!local_ip_cidr)\n+\tif (!local_ip_cidr || !output_url || !input_url)\n \t\tusage(argv[0]);\n-\tif (single_port) {\n-\t\t/* one connection only: exactly one of -o (caller) / -b (listener) */\n-\t\tif ((!output_url && !input_url) || (output_url && input_url)) {\n-\t\t\tfprintf(stderr, \"single-port mode: specify exactly one of \"\n-\t\t\t        \"-o (caller) or -b (listener)\\n\");\n-\t\t\tusage(argv[0]);\n-\t\t}\n-\t\tif (profile == RIST_PROFILE_SIMPLE) {\n-\t\t\tfprintf(stderr, \"single-port mode requires main or advanced \"\n-\t\t\t        \"profile (OOB is unavailable in simple profile)\\n\");\n-\t\t\tusage(argv[0]);\n-\t\t}\n-\t} else if (!output_url || !input_url) {\n-\t\tusage(argv[0]);\n-\t}\n \n-\t/* Append secret/encryption to whichever RIST URLs are present */\n+\t/* Append secret/encryption to RIST URLs if specified */\n \tif (secret) {\n \t\tchar url_suffix[512];\n \t\tint enc = encryption_type > 0 ? encryption_type : 128;\n-\t\tif (output_url) {\n-\t\t\tsnprintf(url_suffix, sizeof(url_suffix), \"%csecret=%s&aes-type=%d\",\n-\t\t\t         strchr(output_url, '?') ? '&' : '?', secret, enc);\n-\t\t\tchar *new_out = malloc(strlen(output_url) + strlen(url_suffix) + 1);\n-\t\t\tif (!new_out) {\n-\t\t\t\tfprintf(stderr, \"Out of memory\\n\");\n-\t\t\t\texitcode = 1;\n-\t\t\t\tgoto cleanup;\n-\t\t\t}\n-\t\t\tsprintf(new_out, \"%s%s\", output_url, url_suffix);\n-\t\t\tfree(output_url);\n-\t\t\toutput_url = new_out;\n-\t\t}\n-\t\tif (input_url) {\n-\t\t\tsnprintf(url_suffix, sizeof(url_suffix), \"%csecret=%s&aes-type=%d\",\n-\t\t\t         strchr(input_url, '?') ? '&' : '?', secret, enc);\n-\t\t\tchar *new_in = malloc(strlen(input_url) + strlen(url_suffix) + 1);\n-\t\t\tif (!new_in) {\n-\t\t\t\tfprintf(stderr, \"Out of memory\\n\");\n-\t\t\t\texitcode = 1;\n-\t\t\t\tgoto cleanup;\n-\t\t\t}\n-\t\t\tsprintf(new_in, \"%s%s\", input_url, url_suffix);\n-\t\t\tfree(input_url);\n-\t\t\tinput_url = new_in;\n+\t\tsnprintf(url_suffix, sizeof(url_suffix), \"%csecret=%s&aes-type=%d\",\n+\t\t         strchr(output_url, '?') ? '&' : '?', secret, enc);\n+\n+\t\tchar *new_out = malloc(strlen(output_url) + strlen(url_suffix) + 1);\n+\t\tchar *new_in = malloc(strlen(input_url) + strlen(url_suffix) + 1);\n+\t\tif (!new_out || !new_in) {\n+\t\t\tfprintf(stderr, \"Out of memory\\n\");\n+\t\t\texitcode = 1;\n+\t\t\tgoto cleanup;\n \t\t}\n+\t\tsprintf(new_out, \"%s%s\", output_url, url_suffix);\n+\t\tfree(output_url);\n+\t\toutput_url = new_out;\n+\n+\t\tsnprintf(url_suffix, sizeof(url_suffix), \"%csecret=%s&aes-type=%d\",\n+\t\t         strchr(input_url, '?') ? '&' : '?', secret, enc);\n+\t\tsprintf(new_in, \"%s%s\", input_url, url_suffix);\n+\t\tfree(input_url);\n+\t\tinput_url = new_in;\n \t}\n \n \t/* Parse IP/prefix */\n@@ -348,12 +246,8 @@ int main(int argc, char *argv[])\n \tsignal(SIGPIPE, SIG_IGN);\n #endif\n \n-\t/* Logging.  Point log_ptr at the static settings like the other tools:\n-\t * rist_logging_set() only allocates a new struct when *log_ptr is NULL,\n-\t * otherwise it configures the pointed-to settings in place.  Leaving it\n-\t * uninitialised let a garbage non-NULL value through and caused a write\n-\t * to an invalid address (intermittent SIGSEGV at startup). */\n-\tstruct rist_logging_settings *log_ptr = &logging_settings;\n+\t/* Logging */\n+\tstruct rist_logging_settings *log_ptr;\n \tif (rist_logging_set(&log_ptr, loglevel, NULL, NULL, NULL, stderr) != 0) {\n \t\tfprintf(stderr, \"Failed to setup logging\\n\");\n \t\texitcode = 1;\n@@ -380,181 +274,91 @@ int main(int argc, char *argv[])\n \trist_log(&logging_settings, RIST_LOG_INFO,\n \t         \"TUN %s: %s/%d MTU %d\\n\", actual_ifname, ip_addr, prefix_len, mtu);\n \n-\t/* RIST sender (TUN → network): hand tun_fd to librist.\n-\t * Built when -o is present: always in two-port mode, and for the\n-\t * caller role in single-port mode. */\n-\tif (output_url) {\n-\t\tif (rist_sender_create(&sender_ctx, profile, 0, log_ptr) != 0) {\n-\t\t\tfprintf(stderr, \"Failed to create RIST sender\\n\");\n-\t\t\texitcode = 1;\n-\t\t\tgoto cleanup;\n-\t\t}\n-\t\tif (rtt_multiplier > 0)\n-\t\t\trist_recovery_rtt_multiplier_set(sender_ctx, rtt_multiplier);\n+\t/* RIST sender (TUN → network): hand tun_fd to librist */\n+\tif (rist_sender_create(&sender_ctx, profile, 0, log_ptr) != 0) {\n+\t\tfprintf(stderr, \"Failed to create RIST sender\\n\");\n+\t\texitcode = 1;\n+\t\tgoto cleanup;\n+\t}\n+\tif (rtt_multiplier > 0)\n+\t\trist_recovery_rtt_multiplier_set(sender_ctx, rtt_multiplier);\n \n-\t\trist_sender_data_fd_set(sender_ctx, tun_fd, (size_t)mtu, RIST_DATA_FD_FLAG_TUN);\n+\trist_sender_data_fd_set(sender_ctx, tun_fd, (size_t)mtu, RIST_DATA_FD_FLAG_TUN);\n \n-\t\tstruct rist_peer_config *sender_peer_cfg = NULL;\n-\t\tif (rist_parse_address2(output_url, &sender_peer_cfg) != 0) {\n-\t\t\tfprintf(stderr, \"Failed to parse sender URL: %s\\n\", output_url);\n-\t\t\texitcode = 1;\n-\t\t\tgoto cleanup;\n-\t\t}\n-\t\tif (mtu < 1400 && sender_peer_cfg->split_mode == LIBRIST_SPLIT_MODE_OFF) {\n-\t\t\trist_log(&logging_settings, RIST_LOG_INFO,\n-\t\t\t         \"MTU %d < 1400: auto-enabling split=half on sender\\n\", mtu);\n-\t\t\tsender_peer_cfg->split_mode = LIBRIST_SPLIT_MODE_HALF;\n-\t\t}\n-\t\tstruct rist_peer *sender_peer = NULL;\n-\t\tif (rist_peer_create(sender_ctx, &sender_peer, sender_peer_cfg) != 0) {\n-\t\t\tfprintf(stderr, \"Failed to create sender peer\\n\");\n-\t\t\texitcode = 1;\n-\t\t\tgoto cleanup;\n-\t\t}\n-#if HAVE_SRP_SUPPORT\n-\t\t/* Present SRP credentials supplied on the sender URL\n-\t\t * (?username=...&password=...), the same way ristsender does.\n-\t\t * Without this the credentials are parsed into the peer config but\n-\t\t * never handed to EAP, so authentication to an SRP-protected\n-\t\t * listener never starts. */\n-\t\tif (strlen(sender_peer_cfg->srp_username) > 0 &&\n-\t\t    strlen(sender_peer_cfg->srp_password) > 0) {\n-\t\t\tif (rist_enable_eap_srp_2(sender_peer, sender_peer_cfg->srp_username,\n-\t\t\t                          sender_peer_cfg->srp_password, NULL, NULL) != 0) {\n-\t\t\t\tfprintf(stderr, \"Failed to enable EAP-SRP on sender\\n\");\n-\t\t\t\texitcode = 1;\n-\t\t\t\tgoto cleanup;\n-\t\t\t}\n-\t\t}\n-#endif\n-\t\tif (statsinterval > 0)\n-\t\t\trist_stats_callback_set(sender_ctx, statsinterval, cb_stats, NULL);\n+\tstruct rist_peer_config *sender_peer_cfg = NULL;\n+\tif (rist_parse_address2(output_url, &sender_peer_cfg) != 0) {\n+\t\tfprintf(stderr, \"Failed to parse sender URL: %s\\n\", output_url);\n+\t\texitcode = 1;\n+\t\tgoto cleanup;\n+\t}\n+\tstruct rist_peer *sender_peer = NULL;\n+\tif (rist_peer_create(sender_ctx, &sender_peer, sender_peer_cfg) != 0) {\n+\t\tfprintf(stderr, \"Failed to create sender peer\\n\");\n+\t\texitcode = 1;\n+\t\tgoto cleanup;\n \t}\n+\tif (statsinterval > 0)\n+\t\trist_stats_callback_set(sender_ctx, statsinterval, cb_stats, NULL);\n \n-\t/* RIST receiver (network → TUN): hand tun_fd to librist.\n-\t * Built when -b is present: always in two-port mode, and for the\n-\t * listener role in single-port mode. */\n-\tif (input_url) {\n-\t\tif (rist_receiver_create(&receiver_ctx, profile, log_ptr) != 0) {\n-\t\t\tfprintf(stderr, \"Failed to create RIST receiver\\n\");\n-\t\t\texitcode = 1;\n-\t\t\tgoto cleanup;\n-\t\t}\n-\t\tif (rtt_multiplier > 0)\n-\t\t\trist_recovery_rtt_multiplier_set(receiver_ctx, rtt_multiplier);\n+\t/* RIST receiver (network → TUN): hand tun_fd to librist */\n+\tif (rist_receiver_create(&receiver_ctx, profile, log_ptr) != 0) {\n+\t\tfprintf(stderr, \"Failed to create RIST receiver\\n\");\n+\t\texitcode = 1;\n+\t\tgoto cleanup;\n+\t}\n+\tif (rtt_multiplier > 0)\n+\t\trist_recovery_rtt_multiplier_set(receiver_ctx, rtt_multiplier);\n \n-\t\trist_receiver_data_fd_set(receiver_ctx, tun_fd, RIST_DATA_FD_FLAG_TUN);\n+\trist_receiver_data_fd_set(receiver_ctx, tun_fd, RIST_DATA_FD_FLAG_TUN);\n \n #if HAVE_SRP_SUPPORT\n-\t\tif (srpfile) {\n-\t\t\tif (rist_auth_handler_set(receiver_ctx, cb_auth_connect, cb_auth_disconnect, receiver_ctx) != 0) {\n-\t\t\t\tfprintf(stderr, \"Failed to set auth handler\\n\");\n-\t\t\t\texitcode = 1;\n-\t\t\t\tgoto cleanup;\n-\t\t\t}\n-\t\t}\n-#endif\n-\n-\t\tstruct rist_peer_config *recv_peer_cfg = NULL;\n-\t\tif (rist_parse_address2(input_url, &recv_peer_cfg) != 0) {\n-\t\t\tfprintf(stderr, \"Failed to parse receiver URL: %s\\n\", input_url);\n+\tif (srpfile) {\n+\t\tif (rist_auth_handler_set(receiver_ctx, cb_auth_connect, cb_auth_disconnect, receiver_ctx) != 0) {\n+\t\t\tfprintf(stderr, \"Failed to set auth handler\\n\");\n \t\t\texitcode = 1;\n \t\t\tgoto cleanup;\n \t\t}\n-\t\tif (mtu < 1400 && recv_peer_cfg->merge_mode == LIBRIST_MERGE_MODE_OFF) {\n-\t\t\trist_log(&logging_settings, RIST_LOG_INFO,\n-\t\t\t         \"MTU %d < 1400: auto-enabling merge=auto on receiver\\n\", mtu);\n-\t\t\trecv_peer_cfg->merge_mode = LIBRIST_MERGE_MODE_AUTO;\n-\t\t}\n-\t\tstruct rist_peer *recv_peer = NULL;\n-\t\tif (rist_peer_create(receiver_ctx, &recv_peer, recv_peer_cfg) != 0) {\n-\t\t\tfprintf(stderr, \"Failed to create receiver peer\\n\");\n-\t\t\texitcode = 1;\n-\t\t\tgoto cleanup;\n-\t\t}\n-\n-#if HAVE_SRP_SUPPORT\n-\t\t/* Receiver leg acts as an SRP client when credentials are supplied on\n-\t\t * the input URL (a two-port client connecting out to a listener);\n-\t\t * otherwise it is the server that verifies callers against -F. */\n-\t\tif (strlen(recv_peer_cfg->srp_username) > 0 &&\n-\t\t    strlen(recv_peer_cfg->srp_password) > 0) {\n-\t\t\tif (rist_enable_eap_srp_2(recv_peer, recv_peer_cfg->srp_username,\n-\t\t\t                          recv_peer_cfg->srp_password, NULL, NULL) != 0) {\n-\t\t\t\tfprintf(stderr, \"Failed to enable EAP-SRP (receiver credentials)\\n\");\n-\t\t\t\texitcode = 1;\n-\t\t\t\tgoto cleanup;\n-\t\t\t}\n-\t\t} else if (srpfile) {\n-\t\t\tif (rist_enable_eap_srp_2(recv_peer, NULL, NULL, user_verifier_lookup, (void *)srpfile) != 0) {\n-\t\t\t\tfprintf(stderr, \"Failed to enable EAP-SRP\\n\");\n-\t\t\t\texitcode = 1;\n-\t\t\t\tgoto cleanup;\n-\t\t\t}\n-\t\t}\n+\t}\n #endif\n \n-\t\tif (statsinterval > 0)\n-\t\t\trist_stats_callback_set(receiver_ctx, statsinterval, cb_stats, NULL);\n+\tstruct rist_peer_config *recv_peer_cfg = NULL;\n+\tif (rist_parse_address2(input_url, &recv_peer_cfg) != 0) {\n+\t\tfprintf(stderr, \"Failed to parse receiver URL: %s\\n\", input_url);\n+\t\texitcode = 1;\n+\t\tgoto cleanup;\n+\t}\n+\tstruct rist_peer *recv_peer = NULL;\n+\tif (rist_peer_create(receiver_ctx, &recv_peer, recv_peer_cfg) != 0) {\n+\t\tfprintf(stderr, \"Failed to create receiver peer\\n\");\n+\t\texitcode = 1;\n+\t\tgoto cleanup;\n \t}\n \n-\t/* single-port: enable the OOB channel that carries the reverse\n-\t * direction on the same socket. The caller installs a callback to\n-\t * inject inbound OOB into the TUN; the listener only needs OOB\n-\t * enabled so its reader thread can rist_oob_write() back. */\n-\tif (single_port) {\n-\t\tstruct rist_ctx *oob_ctx = sender_ctx ? sender_ctx : receiver_ctx;\n-\t\toob_callback_func_t oob_cb = sender_ctx ? sp_oob_to_tun : NULL;\n-\t\tif (rist_oob_callback_set(oob_ctx, oob_cb, &tun_fd) != 0) {\n-\t\t\tfprintf(stderr, \"Failed to enable OOB channel for single-port mode\\n\");\n+#if HAVE_SRP_SUPPORT\n+\tif (srpfile) {\n+\t\tif (rist_enable_eap_srp_2(recv_peer, NULL, NULL, user_verifier_lookup, (void *)srpfile) != 0) {\n+\t\t\tfprintf(stderr, \"Failed to enable EAP-SRP\\n\");\n \t\t\texitcode = 1;\n \t\t\tgoto cleanup;\n \t\t}\n-\t\tif (receiver_ctx) {\n-\t\t\t/* listener: prep the reverse path and capture the peer on connect */\n-\t\t\tsp_rev.ctx = receiver_ctx;\n-\t\t\tsp_rev.tun_fd = tun_fd;\n-\t\t\tsp_rev.mtu = mtu;\n-\t\t\tsp_rev.peer = NULL;\n-\t\t\tif (rist_auth_handler_set(receiver_ctx, sp_conn_cb, sp_disconn_cb, &sp_rev) != 0) {\n-\t\t\t\tfprintf(stderr, \"Failed to set connect handler for single-port listener\\n\");\n-\t\t\t\texitcode = 1;\n-\t\t\t\tgoto cleanup;\n-\t\t\t}\n-\t\t}\n \t}\n+#endif\n+\n+\tif (statsinterval > 0)\n+\t\trist_stats_callback_set(receiver_ctx, statsinterval, cb_stats, NULL);\n \n-\t/* Start whichever contexts exist — librist forwards internally */\n-\tif ((sender_ctx && rist_start(sender_ctx) != 0) ||\n-\t    (receiver_ctx && rist_start(receiver_ctx) != 0)) {\n+\t/* Start both — librist handles all forwarding internally */\n+\tif (rist_start(sender_ctx) != 0 || rist_start(receiver_ctx) != 0) {\n \t\tfprintf(stderr, \"Failed to start RIST\\n\");\n \t\texitcode = 1;\n \t\tgoto cleanup;\n \t}\n \n-\tif (single_port)\n-\t\trist_log(&logging_settings, RIST_LOG_INFO,\n-\t\t         \"Tunnel active (single-port %s): %s/%d via RIST %s\\n\",\n-\t\t         sender_ctx ? \"caller\" : \"listener\", ip_addr, prefix_len,\n-\t\t         output_url ? output_url : input_url);\n-\telse\n-\t\trist_log(&logging_settings, RIST_LOG_INFO,\n-\t\t         \"Tunnel active: %s/%d via RIST (%s -> %s)\\n\",\n-\t\t         ip_addr, prefix_len, input_url, output_url);\n-\n-\t/* single-port listener: pump the local TUN -> OOB (reverse direction).\n-\t * The caller's reverse path is handled by the OOB receive callback. */\n-\tif (single_port && receiver_ctx) {\n-\t\t/* sp_rev was populated above; conn_cb fills sp_rev.peer on connect */\n-\t\tif (pthread_create(&sp_thread, NULL, sp_tun_to_oob, &sp_rev) != 0) {\n-\t\t\tfprintf(stderr, \"Failed to start reverse OOB thread\\n\");\n-\t\t\texitcode = 1;\n-\t\t\tgoto cleanup;\n-\t\t}\n-\t\tsp_thread_started = 1;\n-\t}\n+\trist_log(&logging_settings, RIST_LOG_INFO,\n+\t         \"Tunnel active: %s/%d via RIST (%s -> %s)\\n\",\n+\t         ip_addr, prefix_len, input_url, output_url);\n \n-\t/* Wait for signal — librist handles all forwarding internally */\n+\t/* Wait for signal — no forwarding loop needed */\n \twhile (keep_running) {\n #if defined(__unix) || defined(__APPLE__)\n \t\tsleep(1);\n@@ -563,32 +367,16 @@ int main(int argc, char *argv[])\n #endif\n \t}\n \n-\tif (sp_thread_started) {\n-\t\tpthread_join(sp_thread, NULL);\n-\t\tsp_thread_started = 0;\n-\t}\n-\n-\t/* Print stats (only for contexts that exist) */\n-\tif (sender_ctx) {\n-\t\tstruct rist_data_fd_stats tx_stats;\n-\t\trist_data_fd_stats_get(sender_ctx, &tx_stats);\n-\t\trist_log(&logging_settings, RIST_LOG_INFO,\n-\t\t         \"Shutting down. TX: %lu pkts / %lu bytes\\n\",\n-\t\t         (unsigned long)tx_stats.tx_packets,\n-\t\t         (unsigned long)tx_stats.tx_bytes);\n-\t}\n-\tif (receiver_ctx) {\n-\t\tstruct rist_data_fd_stats rx_stats;\n-\t\trist_data_fd_stats_get(receiver_ctx, &rx_stats);\n-\t\trist_log(&logging_settings, RIST_LOG_INFO,\n-\t\t         \"Shutting down. RX: %lu pkts / %lu bytes\\n\",\n-\t\t         (unsigned long)rx_stats.rx_packets,\n-\t\t         (unsigned long)rx_stats.rx_bytes);\n-\t}\n+\t/* Print stats */\n+\tstruct rist_data_fd_stats tx_stats, rx_stats;\n+\trist_data_fd_stats_get(sender_ctx, &tx_stats);\n+\trist_data_fd_stats_get(receiver_ctx, &rx_stats);\n+\trist_log(&logging_settings, RIST_LOG_INFO,\n+\t         \"Shutting down. TX: %lu pkts / %lu bytes, RX: %lu pkts / %lu bytes\\n\",\n+\t         (unsigned long)tx_stats.tx_packets, (unsigned long)tx_stats.tx_bytes,\n+\t         (unsigned long)rx_stats.rx_packets, (unsigned long)rx_stats.rx_bytes);\n \n cleanup:\n-\tif (sp_thread_started)\n-\t\tpthread_join(sp_thread, NULL);\n \tif (receiver_ctx)\n \t\trist_destroy(receiver_ctx);\n \tif (sender_ctx)\ndiff --git a/tools/risturlhelp.h b/tools/risturlhelp.h\nindex 14baedd..2a61bb2 100644\n--- a/tools/risturlhelp.h\n+++ b/tools/risturlhelp.h\n@@ -31,22 +31,14 @@ const char help_urlstr[] = \"\\nUsage: append to end of individual rist:// url(s)\n \"    param min-retries=##  min retries count before congestion control kicks in\\n\"\n \"    param max-retries=##  max retries count\\n\"\n \"    param weight=#  default weight for multi-path load balancing. Use 0 for duplicate paths.\\n\"\n-\"    param rtt-drop=###  smoothed-RTT ceiling in ms; a bonded leg above it is muted (0=disabled)\\n\"\n-\"    param rtt-restore=###  RTT in ms to rejoin the bond (0=derive 80% of rtt-drop)\\n\"\n-\"    param rtt-drop-settle=###  ms a leg must hold over the ceiling before muting (rejoin uses a longer, conservative dwell)\\n\"\n-\"    param rtt-drop-trickle=#  send 1-in-N redundant packets on a muted leg to keep it measured (default 100 ~1%, 0=hard mute)\\n\"\n \"    param username=abcde  Username to identify this peer during authentication\\n\"\n \"    param password=abcde  Password corresponding to this peer's Username\\n\"\n-\"    param reflector=1|0  enable transparent one-to-many reflector mode (receiver listener only)\\n\"\n \"  Advanced Profile\\n\"\n \"    param compression=1|0  enable lz4 levels\\n\"\n \"\\n\"\n \"Usage: append to end of individual udp:// or rtp:// url(s) as ?param1=value1&param2=value2...\\n\"\n \"  param miface=(device)  device name (e.g. eth0) for multicast\\n\"\n \"  param stream-id=#  ID number (arbitrary) for multiplex/demultiplexing steam in peer connector\\n\"\n-\"  param cbr-output=1|0  space receiver output at the stream's measured rate\\n\"\n-\"                        (assumes CBR; applies to the whole context, so every\\n\"\n-\"                        output url must agree)\\n\"\n \"  param multiplex-mode=#  Controls how rist payload is muxed/demuxed\\n\"\n \"                        (-1=auto-detect, 0=rist/raw, 1=vrt_src_port, 2=ipv4)\\n\"\n //\"  param multiplex-filter=#  When using mux-mode=ipv4, this is the string to be used for data filter.\\n\"\ndiff --git a/tools/srp_shared.c b/tools/srp_shared.c\nindex 0954de8..d3dbed5 100644\n--- a/tools/srp_shared.c\n+++ b/tools/srp_shared.c\n@@ -86,10 +86,9 @@ int parse_line(const char *line, size_t line_len, uint8_t **decoded_verifier, si\n \tuser_verifier_state_e state = IN_VERIFIER;\n \tint ret = -1;\n \tsize_t read_verifier_len = 0;\n-\t/* +8: room for the inline base64 padding (up to 3) + terminator writes */\n-\tchar *read_verifier = calloc(READ_VERIFIER_LEN + 8, 1);\n+\tchar *read_verifier = calloc(READ_VERIFIER_LEN, 1);\n \tsize_t read_salt_len = 0;\n-\tchar *read_salt = calloc(READ_SALT_LEN + 8, 1);\n+\tchar *read_salt = calloc(READ_SALT_LEN, 1);\n \tif (!read_salt || !read_verifier)\n \t\tgoto out;\n \tfor (size_t i=0; i < line_len; i++) {\n@@ -189,7 +188,7 @@ void user_verifier_lookup(char * username,\n \n \tsize_t username_offset = 0;\n \n-\tchar read_hashver[4] = {0}; /* 3 digits + NUL for atoi */\n+\tchar read_hashver[3] = {0};\n \tint read_hashver_len = 0;\n \tuser_verifier_state_e state = IN_USERNAME;\n \tbool skipnextline = false;\n@@ -232,11 +231,8 @@ void user_verifier_lookup(char * username,\n \t\t\tbreak;\n \t\t} else if (read == ':')\n \t\t{\n-\t\t\tif (state == IN_VERIFIER) {\n-\t\t\t\tif (line_len >= MAX_LINE_LEN)\n-\t\t\t\t\tgoto out;\n+\t\t\tif (state == IN_VERIFIER)\n \t\t\t\tline[line_len++] = read;\n-\t\t\t}\n \t\t\tif (state == IN_USERNAME && username_offset != username_len) {\n \t\t\t\tif (line_one_done) {\n \t\t\t\t\tbreak;\ndiff --git a/tools/string-shim.h b/tools/string-shim.h\ndeleted file mode 100644\nindex 80c8729..0000000\n--- a/tools/string-shim.h\n+++ /dev/null\n@@ -1,18 +0,0 @@\n-/* librist. Copyright © 2026 SipRadius LLC. All right reserved.\n- * Author: Sergio Ammirata, Ph.D. <sergio@ammirata.net>\n- *\n- * SPDX-License-Identifier: BSD-2-Clause\n- */\n-\n-#ifndef LIBRIST_TOOLS_STRING_SHIM_H\n-#define LIBRIST_TOOLS_STRING_SHIM_H\n-\n-#include <string.h>\n-\n-#if defined(_WIN32) || defined(_WIN64)\n-/* Microsoft's CRT does not ship the C99 strtok_r; the equivalent is\n- * strtok_s. Same signature, same semantics. */\n-#define strtok_r strtok_s\n-#endif\n-\n-#endif\ndiff --git a/tools/udp2udp.c b/tools/udp2udp.c\nindex 816d66a..a303f23 100644\n--- a/tools/udp2udp.c\n+++ b/tools/udp2udp.c\n@@ -53,9 +53,9 @@ struct rist_callback_object {\n \n #if HAVE_PROMETHEUS_SUPPORT\n struct rist_prometheus_stats *prom_stats_ctx;\n-int prometheus_multipoint = 0;\n-int prometheus_nocreated = 0;\n-int prometheus_httpd = 0;\n+bool prometheus_multipoint = false;\n+bool prometheus_nocreated = false;\n+bool prometheus_httpd = false;\n bool enable_prometheus = false;\n char *prometheus_tags = NULL;\n uint16_t prometheus_port = 9100;\n@@ -74,10 +74,10 @@ static struct option long_options[] = {\n #if HAVE_PROMETHEUS_SUPPORT\n { \"enable-metrics\",  no_argument,       NULL, 'M' },\n { \"metrics-tags\",    required_argument, NULL, 1 },\n-{ \"metrics-multipoint\",no_argument,     &prometheus_multipoint, 1 },\n-{ \"metrics-nocreated\",no_argument,      &prometheus_nocreated, 1 },\n+{ \"metrics-multipoint\",no_argument,     (int*)&prometheus_multipoint, true },\n+{ \"metrics-nocreated\",no_argument,      (int*)&prometheus_nocreated, true },\n #if HAVE_LIBMICROHTTPD\n-{ \"metrics-http\",    no_argument,      &prometheus_httpd, 1 },\n+{ \"metrics-http\",    no_argument,      (int*)&prometheus_httpd, true },\n { \"metrics-port\",    required_argument, NULL, 2 },\n { \"metrics-ip\",      required_argument, NULL, 3 },\n #endif //HAVE_LIBMICROHTTPD\n@@ -342,13 +342,6 @@ int main(int argc, char *argv[])\n \t\t\trist_log(log_ptr, RIST_LOG_ERROR, \"Failed to setup Metrics output\\n\");\n \t\t\texit(1);\n \t\t}\n-\t\t/* udp2udp is a plain UDP relay; it doesn't run any RIST\n-\t\t * sender or receiver and therefore never feeds the Prometheus\n-\t\t * exporter. Make that clear so users don't expect rist_*\n-\t\t * series to show up at the scrape endpoint. */\n-\t\trist_log(log_ptr, RIST_LOG_WARN,\n-\t\t\t\"udp2udp does not produce RIST traffic, so no rist_* metrics will be exported. \"\n-\t\t\t\"Use ristsender/ristreceiver for end-to-end RIST stats.\\n\");\n \t}\n #endif\n \n@@ -367,7 +360,7 @@ int main(int argc, char *argv[])\n \t\tevctx = evsocket_create();\n \tchar hostname[200] = {0};\n \tint inputlisten;\n-\tuint16_t inputport = 0;\n+\tuint16_t inputport;\n \t// First parse extra parameters (?miface=lo) and separate the address\n \t// We are using the rist_parse_address function to create a config object that does not really\n \t// belong to the udp output. We do this only to avoid writing another parser for the two url\n@@ -432,7 +425,7 @@ int main(int argc, char *argv[])\n \t\tmemset(&hostname, 0, sizeof(hostname));\n \n \t\tint outputlisten;\n-\t\tuint16_t outputport = 0;\n+\t\tuint16_t outputport;\n \t\tif (udpsocket_parse_url((void *)output_udp_config->address, hostname, 200, &outputport, &outputlisten) || !outputport || strlen(hostname) == 0) {\n \t\t\trist_log(&logging_settings, RIST_LOG_ERROR, \"Could not parse output url %s\\n\", outputtoken);\n \t\t\tgoto next;\ndiff --git a/tools/yamlparse.c b/tools/yamlparse.c\nindex 9d6c1dc..daef6c0 100644\n--- a/tools/yamlparse.c\n+++ b/tools/yamlparse.c\n@@ -12,7 +12,6 @@\n #include <ctype.h>\n #include <string.h>\n #include \"headers.h\"\n-#include <librist/peer.h>\n #include \"logging.h\"\n \n #ifndef strndup\n@@ -70,11 +69,11 @@ yaml_node *parse_yaml_value(char *str, size_t *index, size_t *max) {\n \t\tnode->key = strndup(str + *index + start_pos, cr_pos - 1);\n \t\t//printf(\"Key =%s=\\n\", node->key);\n \t}\n-\telse if (space_pos > 0 && space_pos < cr_pos && str[*index + start_pos + space_pos - 1] == ':') // Key Value pairs\n+\telse if (space_pos < cr_pos && str[*index + start_pos + space_pos - 1] == ':') // Key Value pairs\n \t{\n \t\tnode->key = strndup(str + *index + start_pos, space_pos - 1);\n-\t\t// Remove any additional spaces before the value (bounded at end of line)\n-\t\twhile (space_pos < cr_pos && isspace(str[*index + start_pos + space_pos])) (space_pos)++;\n+\t\t// Remove any additional spaces before the value\n+\t\twhile (isspace(str[*index + start_pos + space_pos])) (space_pos)++;\n \t\tnode->value = strndup(str + *index + start_pos + space_pos, cr_pos - space_pos);\n \t\t//printf(\"Key =%s= Value =%s=\\n\", node->key, node->value);\n \t}\n@@ -82,7 +81,7 @@ yaml_node *parse_yaml_value(char *str, size_t *index, size_t *max) {\n \t{\n \t\t// If this is a list, remove the dash and space from it\n \t\tsize_t skip_chars = 0;\n-\t\tif (str[*index + start_pos] == '-' && cr_pos >= 2)\n+\t\tif (str[*index + start_pos] == '-')\n \t\t\tskip_chars = 2;\n \t\tnode->value = strndup(str + *index + start_pos + skip_chars, cr_pos - skip_chars);\n \t\t//printf(\"value =%s=\\n\", node->value);\n@@ -114,11 +113,7 @@ yaml_node *parse_yaml_string(char *yaml_str) {\n \n void print_yaml_node(yaml_node *node) {\n     if (node->key && node->value) {\n-        /* Never echo secrets into terminal/log output */\n-        if (strcmp(node->key, \"secret\") == 0)\n-            printf(\"%s: (redacted)\\n\", node->key);\n-        else\n-            printf(\"%s: %s\\n\", node->key, node->value);\n+        printf(\"%s: %s\\n\", node->key, node->value);\n     } else if (node->key) {\n         printf(\"%s:\\n\", node->key);\n     } else {\n@@ -246,7 +241,7 @@ rist_tools_config_object *parse_yaml(char * file){\n \n \t// Initialize rist_tools_config_object (non-zero values)\n \tconfig = calloc(1, sizeof(rist_tools_config_object));\n-\tconfig->profile = RIST_DEFAULT_PROFILE;\n+\tconfig->profile = RIST_PROFILE_MAIN;\n \tconfig->stats_interval = 1000;\n \tconfig->verbose_level = RIST_LOG_INFO;\n #ifdef HAVE_PROMETHEUS_SUPPORT\n@@ -259,33 +254,20 @@ rist_tools_config_object *parse_yaml(char * file){\n \tfseek(f, 0, SEEK_END);\n \tlong fsize = ftell(f);\n \tfseek(f, 0, SEEK_SET);  /* same as rewind(f); */\n-\tif (fsize < 0) {\n-\t\tfclose(f);\n-\t\treturn config;\n-\t}\n-\tchar *yaml_str = malloc((size_t)fsize + 1);\n-\tif (!yaml_str) {\n-\t\tfclose(f);\n-\t\treturn config;\n-\t}\n-\tsize_t got = fread(yaml_str, 1, (size_t)fsize, f);\n-\tyaml_str[got] = '\\0';\n+\tchar *yaml_str = malloc(fsize + 1);\n+\tsize_t read = fread(yaml_str, fsize, 1, f);\n+\t(void)read;\n     yaml_node *root = parse_yaml_string(yaml_str);\n \t// print entire config file (debug)\n-    if (root)\n-\t    print_yaml_node(root);\n+    print_yaml_node(root);\n \t// transfer the data to the config structure\n-\tif (root)\n-\t\tparse_config_file(config, current_key, root);\n-\tfree(yaml_str);\n+\tparse_config_file(config, current_key, root);\n \tfclose(f);\n \treturn config;\n }\n \n void cleanup_tools_config(rist_tools_config_object * config)\n {\n-\tif (!config)\n-\t\treturn;\n \tif (config->remote_log_address)\n         free(config->remote_log_address);\n \tif (config->secret)\ndiff --git a/wasm32-emscripten.txt b/wasm32-emscripten.txt\ndeleted file mode 100644\nindex bac0a5d..0000000\n--- a/wasm32-emscripten.txt\n+++ /dev/null\n@@ -1,21 +0,0 @@\n-[binaries]\n-c       = 'emcc'\n-cpp     = 'em++'\n-ar      = 'emar'\n-ranlib  = 'emranlib'\n-strip   = '/usr/bin/true'\n-pkgconfig = '/usr/bin/false'\n-\n-[built-in options]\n-c_args      = ['-pthread']\n-c_link_args = ['-pthread']\n-\n-[properties]\n-needs_exe_wrapper = true\n-sys_root = ''\n-\n-[host_machine]\n-system     = 'emscripten'\n-cpu_family = 'wasm32'\n-cpu        = 'wasm32'\n-endian     = 'little'\n","content_type":"text/plain","diff_type":"vcs","new_revision":"git-v1:d085be202949b3c064e48701f2853a41e61ddd81"}