Skip to content

chore(release): promote rc-2026.4.4 - #77

Merged
jacderida merged 15 commits into
mainfrom
rc-2026.4.4
May 5, 2026
Merged

chore(release): promote rc-2026.4.4#77
jacderida merged 15 commits into
mainfrom
rc-2026.4.4

Conversation

@jacderida

@jacderida jacderida commented May 5, 2026

Copy link
Copy Markdown
Member

Promotes rc-2026.4.4 to release version(s): 0.34.1.

  • strips -rc.* from [package].version
  • rewrites internal git+branch deps to crates.io version pins
  • regenerates Cargo.lock

Once merged, the release tag will be pushed to fire the publish workflow.

Greptile Summary

This release PR promotes rc-2026.4.4 to version 0.34.1, stripping RC suffixes and regenerating Cargo.lock. Despite the chore label, the diff contains a substantial set of substantive changes across the transport, congestion, NAT traversal, and relay layers — all of which appear to be feature/fix work bundled into the RC that is now being released.

  • Congestion control: BBR2 initial window corrected from 200 to 10 packets (RFC 9002 §7.2 compliance); unacked_packets switched from a monotonic scalar to a BTreeSet for correct out-of-order ACK tracking; lost MTU probes now call on_packet_abandoned to avoid spurious congestion reactions; congestion window check changed from >= to > with overflow safety via saturating_add.
  • MASQUE relay PMTU feedback: New tunnel_control.rs module introduces an out-of-band control-frame protocol; relay-server now sets IP_PMTUDISC_DO/IP_DONTFRAG on egress sockets and emits PmtuUpdate frames on EMSGSIZE; relay-client's MasqueRelaySocket::try_send silently drops oversized packets to drive Quinn DPLPMTUD downward.
  • Happy Eyeballs / connection strategy: AttemptHandles RAII wrapper ensures in-flight tasks are aborted when the outer future is dropped; connection timeout split into direct_connect_timeout and direct_handshake_timeout; new first_peer_response oneshot enables two-phase timeout; socket-address normalization applied throughout to eliminate IPv4 vs. IPv4-mapped-IPv6 lookup mismatches.

Confidence Score: 3/5

Hold on the release tag until the datagram-loop EMSGSIZE gap in relay_server.rs is resolved; all other changes are sound.

The relay PMTU feedback mechanism is partially wired: stream-based relay sessions benefit from the new PmtuUpdate control frames, but datagram-loop relay sessions now have DF=1 set on their egress socket without any corresponding feedback path, meaning MTU-exceeded datagrams will be silently dropped for the life of those sessions. Every other change — BBR2 BTreeSet tracking, AttemptHandles RAII, socket normalization, first_peer_response two-phase timeout — looks correct and well-tested.

src/masque/relay_server.rs (datagram-loop EMSGSIZE handling); src/p2p_endpoint.rs (Ok(0) write branch)

Important Files Changed

Filename Overview
src/masque/relay_server.rs DF bit set on relay-allocated sockets; stream-loop now emits PmtuUpdate on EMSGSIZE; datagram-loop path still silently swallows EMSGSIZE without emitting PMTU feedback.
src/p2p_endpoint.rs Large-send backpressure semaphore added; write_stream_with_progress_timeout replaces fire-and-forget writes; send_ack_timeout removed; socket normalization applied throughout.
src/congestion/bbr2/adapter.rs BTreeSet replaces scalar least_unacked (correctness fix for out-of-order ACKs); initial window reduced 200→10 packets (RFC 9002 compliance); on_packet_abandoned added for MTU probe accounting.
src/connection/mod.rs Congestion window check fixed with saturating_add + strict inequality; PATH_CHALLENGE packets now tracked by congestion controller via account_udp_transmit; min_initial_size replaced by path-MTU-aware variant throughout.
src/high_level/connection.rs New first_peer_response oneshot and peer_response_received flag for two-phase connection timeout; notify_first_peer_response called on HandshakeDataReady, Connected, and Proto events; terminate() always unblocks the receiver.
src/happy_eyeballs.rs AttemptHandles RAII wrapper correctly aborts in-flight tasks on drop; test verifies cancellation; old abort_all free function removed.
src/masque/relay_socket.rs Client-side PmtuUpdate decoding and per-target MTU cap via DashMap; oversized segments silently dropped in try_send to drive DPLPMTUD; logic is sound for matching address forms.
src/masque/tunnel_control.rs New file implementing out-of-band tunnel control frames (PmtuUpdate); wire format well-specified, sentinel value chosen above data-frame range, round-trip tests included.
src/nat_traversal_api.rs New PeerObservedExternal event; socket normalization applied at accept, connect, and event emission sites; socket_addr_variants used for dedup checks.
src/connection_strategy.rs Breaking API change: ipv4_timeout/ipv6_timeout replaced by direct_connect_timeout (1s) + direct_handshake_timeout (4s); builder methods and tests updated accordingly.
src/shared.rs New socket_addr_variants helper returns all lookup forms for a SocketAddr (normalized, original, dual-stack alternate) with a fixed MAX_ADDR_VARIANTS capacity bound.
src/endpoint.rs connection_handle_for_addr now normalizes both the query address and stored remote address, removing the dual-stack alternate fallback that was needed when insertions weren't normalized.

Sequence Diagram

sequenceDiagram
    participant Quinn as Quinn (relay-client)
    participant RS as MasqueRelaySocket
    participant RServer as MasqueRelayServer
    participant Target as Remote Target

    Note over Quinn,Target: Stream-based relay with PMTU feedback (new)

    Quinn->>RS: try_send(Transmit { destination: Target })
    RS->>RS: check target_mtu[Target]?
    alt segment > MTU cap
        RS-->>Quinn: Ok(()) drop silently, DPLPMTUD sees loss
    else within cap
        RS->>RServer: UncompressedDatagram frame (stream)
        RServer->>Target: UDP send_to(payload)
        alt EMSGSIZE
            RServer->>RServer: encode PmtuUpdate{target, mtu=1200}
            RServer-->>RS: WriterItem::Control(PmtuUpdate)
            RS->>RS: target_mtu.insert(Target, 1200)
        else OK
            Target-->>RServer: UDP reply
            RServer-->>RS: UncompressedDatagram (Direction 1)
            RS-->>Quinn: recv datagram
        end
    end

    Note over Quinn,Target: Happy Eyeballs with split timeouts (new)

    participant P2P as P2pEndpoint
    participant Peer as Remote Peer

    P2P->>Peer: QUIC connect(addr)
    P2P->>P2P: timeout(connect_timeout=1s, first_peer_response())
    Peer-->>P2P: first authenticated QUIC packet
    P2P->>P2P: peer_response_received = true
    P2P->>P2P: timeout(handshake_timeout=4s, connecting.await)
    Peer-->>P2P: handshake complete
    P2P->>P2P: ConnectionEstablished
Loading

Comments Outside Diff (2)

  1. src/masque/relay_server.rs, line 1116-1151 (link)

    P1 EMSGSIZE not handled in the datagram-loop forward path

    The stream-forwarding loop (run_stream_forwarding_loop) now catches EMSGSIZE from send_to and emits a PmtuUpdate control frame back to the relay-client. The datagram-loop in handle_session_inner received the same set_dont_fragment treatment on the egress socket, so it will also start seeing EMSGSIZE failures — but the only response here is a warn! log and a silent packet drop. Because ctrl_tx is not threaded into this path, no PmtuUpdate frame is sent, and the relay-client's target_mtu map is never populated for sessions that go through this code path. Packets will keep hitting the MTU cap silently for the lifetime of the session.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/masque/relay_server.rs
    Line: 1116-1151
    
    Comment:
    **EMSGSIZE not handled in the datagram-loop forward path**
    
    The stream-forwarding loop (`run_stream_forwarding_loop`) now catches `EMSGSIZE` from `send_to` and emits a `PmtuUpdate` control frame back to the relay-client. The datagram-loop in `handle_session_inner` received the same `set_dont_fragment` treatment on the egress socket, so it will also start seeing `EMSGSIZE` failures — but the only response here is a `warn!` log and a silent packet drop. Because `ctrl_tx` is not threaded into this path, no `PmtuUpdate` frame is sent, and the relay-client's `target_mtu` map is never populated for sessions that go through this code path. Packets will keep hitting the MTU cap silently for the lifetime of the session.
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. src/p2p_endpoint.rs, line 637-720 (link)

    P2 Ok(0) from SendStream::write() treated as a hard fatal error

    write_stream_with_progress_timeout returns Err(SendFailed { stage: Write, ... }) whenever Quinn's write() yields Ok(0). Quinn's SendStream::write() is documented to return Ok(0) when the stream has been stopped (i.e. the peer already reset it with STOP_SENDING). In that case the correct response is to surface the stop error — but write() can also transiently return Ok(0) on an empty-capacity flow-control window before the async machinery has a chance to park. Treating this as a fatal error causes the stream to be reset with code 0 and the send to fail even though the connection may be healthy. Matching Ok(0) separately and checking send_stream.stopped() for a STOP_SENDING code, or just retrying the write, would be safer.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/p2p_endpoint.rs
    Line: 637-720
    
    Comment:
    **`Ok(0)` from `SendStream::write()` treated as a hard fatal error**
    
    `write_stream_with_progress_timeout` returns `Err(SendFailed { stage: Write, ... })` whenever Quinn's `write()` yields `Ok(0)`. Quinn's `SendStream::write()` is documented to return `Ok(0)` when the stream has been stopped (i.e. the peer already reset it with `STOP_SENDING`). In that case the correct response is to surface the stop error — but `write()` can also transiently return `Ok(0)` on an empty-capacity flow-control window before the async machinery has a chance to park. Treating this as a fatal error causes the stream to be reset with code 0 and the send to fail even though the connection may be healthy. Matching `Ok(0)` separately and checking `send_stream.stopped()` for a `STOP_SENDING` code, or just retrying the write, would be safer.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
src/masque/relay_server.rs:1116-1151
**EMSGSIZE not handled in the datagram-loop forward path**

The stream-forwarding loop (`run_stream_forwarding_loop`) now catches `EMSGSIZE` from `send_to` and emits a `PmtuUpdate` control frame back to the relay-client. The datagram-loop in `handle_session_inner` received the same `set_dont_fragment` treatment on the egress socket, so it will also start seeing `EMSGSIZE` failures — but the only response here is a `warn!` log and a silent packet drop. Because `ctrl_tx` is not threaded into this path, no `PmtuUpdate` frame is sent, and the relay-client's `target_mtu` map is never populated for sessions that go through this code path. Packets will keep hitting the MTU cap silently for the lifetime of the session.

### Issue 2 of 4
src/congestion/bbr2/adapter.rs:30-35
**Initial window reduced 20× — bulk throughput impact**

`K_INITIAL_WINDOW_PACKETS` drops from 200 to 10. The old value (200 × ~1 450 B ≈ 290 KB) was far above the RFC 9002 §7.2 limit of 10 packets and the change is correct for compliance. At the same time this is effectively a 20× reduction in the burst the sender is allowed before the first ACK arrives. On high-bandwidth / high-RTT paths (e.g. the relay-tunnelled cross-continent scenarios described in `connection_strategy.rs`) this will substantially extend the slow-start ramp-up. Worth confirming this was measured against the ant-rc testnet numbers mentioned elsewhere in the codebase before shipping.

### Issue 3 of 4
src/p2p_endpoint.rs:637-720
**`Ok(0)` from `SendStream::write()` treated as a hard fatal error**

`write_stream_with_progress_timeout` returns `Err(SendFailed { stage: Write, ... })` whenever Quinn's `write()` yields `Ok(0)`. Quinn's `SendStream::write()` is documented to return `Ok(0)` when the stream has been stopped (i.e. the peer already reset it with `STOP_SENDING`). In that case the correct response is to surface the stop error — but `write()` can also transiently return `Ok(0)` on an empty-capacity flow-control window before the async machinery has a chance to park. Treating this as a fatal error causes the stream to be reset with code 0 and the send to fail even though the connection may be healthy. Matching `Ok(0)` separately and checking `send_stream.stopped()` for a `STOP_SENDING` code, or just retrying the write, would be safer.

### Issue 4 of 4
src/high_level/connection.rs:190-230
**`first_peer_response` receiver dropped silently if called after connection close**

If `Connecting::first_peer_response()` is called after the connection has already terminated (so `self.conn` is `None`), `has_peer_response()` returns `Err(LocallyClosed)`. This is propagated by `?` — correct. However, if `self.conn` is still `Some` but `peer_response_received` is false AND `on_first_peer_response` has already been taken (e.g., a second call), the function silently skips the oneshot wait and falls through to the state lock. At that point `peer_response_received` is still false and `error` may not yet be set, so the function hits the `unwrap_or_else` branch and logs a spurious `error!`. Consider guarding against multiple calls more explicitly or documenting that the method is single-use.

Reviews (1): Last reviewed commit: "chore(release): promote rc-2026.4.4 to 0..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

mickvandijke and others added 15 commits April 30, 2026 13:16
- Enhanced relay diagnostic logs for clarity during inbound and outbound traffic handling.
- Refined UDP relay logging paths to include detailed source, target, and payload information.
- connection: bound PQC `min_initial_size` by the current path MTU so
  coalesced Initial+Handshake packets never exceed what the path has
  proven; restart MTUD from the established MTU instead of jumping to
  the PQC-preferred floor.
- masque: add out-of-band `TunnelControlFrame::PmtuUpdate` emitted by
  the relay-server on egress `EMSGSIZE` and enforced by the
  relay-client `MasqueRelaySocket` as a per-target send cap that
  drives Quinn's DPLPMTUD downward.
- masque/relay_server: enable `IP_DONTFRAG` / `IP_PMTUDISC_DO` on the
  bound socket so oversized egress fails fast instead of silently
  fragmenting and getting dropped by intermediate NATs.
- nat_traversal_api, p2p_endpoint: clamp the inner endpoint's
  transport config to a 1200-byte initial/min MTU on both
  accept- and dial-through-relay paths, with DPLPMTUD enabled to
  probe upward.
- p2p_endpoint, config/nat_timeouts: treat `send_ack_timeout` as a
  best-effort observation window rather than a hard failure — data is
  already queued to QUIC; later connection state or application-level
  timeouts handle retries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ervedExternal

Add `NatTraversalEvent::PeerObservedExternal { peer_addr, observed_external }`
and the corresponding `P2pEvent::PeerObservedExternal`, fired on every distinct
`(peer, observed-address)` pair from the OBSERVED_ADDRESS path — both the
regular discovery loop and the bootstrap reflexive-candidate path. The signal
is emitted regardless of whether saorsa-transport's pinning quorum
(`MIN_OBSERVERS_FOR_QUORUM = 2`) has been reached.

Upper layers can now do per-address attribution of cold-dialability proof:
the peer's frame is their own statement that they reached us at a specific
external. Saorsa-core consumes this in its passive reachability classifier
(separate PR) to credit only the externals each inbound peer actually
reported observing — closing a per-family-leak in the previous design where
two source-disjoint inbounds promoted every same-family pinned external,
including stale NAT rebindings and unrelated externals.

Additive on the public enums; `Node::convert_event` adds the new variant to
its already-ignored arm. Other matchers in saorsa-transport already used
catch-all arms.

Tests: cargo fmt clean, cargo clippy --all-targets --all-features -- -D
warnings clean, 1506 lib tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous send path used `write_all` which could block indefinitely
on a stuck stream, and collapsed every failure into the opaque
`EndpointError::Connection(String)`. Upper layers had no way to tell
"open failed" from "stopped by peer" from "stalled mid-write" and so
could not pick a sensible retry strategy.

This introduces:
- `SendFailureStage` enum identifying where a send broke (open,
  open progress timeout, write, write progress timeout, finish,
  stopped, acknowledgement).
- `EndpointError::SendFailed { stage, bytes_written, reason }` so
  callers can branch on stage and know how many bytes the QUIC stream
  accepted before the failure.
- `write_stream_with_progress_timeout` helper that writes in 64 KiB
  chunks with a per-write progress timeout, returning the byte count
  for diagnostics and resetting the stream on failure.
- A wrapping `timeout` around `open_uni` to surface stuck stream
  opens the same way.

Constants `STREAM_WRITE_CHUNK_SIZE`, `STREAM_WRITE_PROGRESS_TIMEOUT`
and `STREAM_RESET_ABORT_CODE` are introduced rather than inlined.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nnections

- Introduced distinct timeouts for direct connection stages: progress and handshake.
- Enhanced `HappyEyeballs` logging to reflect both timeouts.
- Modified `race_connect` for explicit timeout handling at each stage and surface errors appropriately.
- Updated `StrategyConfig` to include configurable `direct_handshake_timeout`.
- Adjusted defaults and augmented tests to validate new timeout logic.
…cket

Add Connecting::first_peer_response() that resolves once the first QUIC
packet authenticated by the peer is received, distinct from full handshake
completion. P2pEndpoint's Happy Eyeballs direct race now uses it instead
of handshake_data() so direct_connect_timeout bounds basic peer
reachability while direct_handshake_timeout continues to govern full
handshake completion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The post-`finish()` `stopped()` wait was a 1s+payload best-effort window
that tied each send to a remote acknowledgement that may never come on
busy or torn-down connections. Per-step progress timeouts (open_uni and
write) plus close_reason short-circuiting already cover real failure
modes; the extra wait only added latency without delivery guarantees.

BREAKING CHANGE: removes the TimeoutConfig::send_ack_timeout field and
the SendFailureStage::Stopped/Acknowledgement variants that existed only
to drive that path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(p2p,relay): tunnel timeout, MTU, and connect timeout fixes
is_message_too_large referenced libc::EMSGSIZE unconditionally, but
the libc crate is declared as [target.'cfg(unix)'.dependencies] and so
is unavailable on Windows targets. The Windows build broke with
E0433: "cannot find module or crate `libc` in this scope".

Match the platform pattern set_dont_fragment already follows in this
module: keep the libc-backed implementation behind cfg(unix), and add
a non-Unix fallback that returns false. The PMTU control-frame loop
simply does not fire on those targets — same outcome as the existing
no-op set_dont_fragment fallback for non-Linux/BSD targets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment on lines 30 to +35

/// Default packet count for the initial congestion window — matches BBRv1
/// in this crate (200 × BASE_DATAGRAM_SIZE).
const K_INITIAL_WINDOW_PACKETS: u64 = 200;
/// Default packet count for the initial congestion window.
///
/// Keep this aligned with RFC 9002/quiche-style defaults; callers that want a
/// more aggressive startup can still override `Bbr2Config::initial_window`.
const K_INITIAL_WINDOW_PACKETS: u64 = 10;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Initial window reduced 20× — bulk throughput impact

K_INITIAL_WINDOW_PACKETS drops from 200 to 10. The old value (200 × ~1 450 B ≈ 290 KB) was far above the RFC 9002 §7.2 limit of 10 packets and the change is correct for compliance. At the same time this is effectively a 20× reduction in the burst the sender is allowed before the first ACK arrives. On high-bandwidth / high-RTT paths (e.g. the relay-tunnelled cross-continent scenarios described in connection_strategy.rs) this will substantially extend the slow-start ramp-up. Worth confirming this was measured against the ant-rc testnet numbers mentioned elsewhere in the codebase before shipping.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/congestion/bbr2/adapter.rs
Line: 30-35

Comment:
**Initial window reduced 20× — bulk throughput impact**

`K_INITIAL_WINDOW_PACKETS` drops from 200 to 10. The old value (200 × ~1 450 B ≈ 290 KB) was far above the RFC 9002 §7.2 limit of 10 packets and the change is correct for compliance. At the same time this is effectively a 20× reduction in the burst the sender is allowed before the first ACK arrives. On high-bandwidth / high-RTT paths (e.g. the relay-tunnelled cross-continent scenarios described in `connection_strategy.rs`) this will substantially extend the slow-start ramp-up. Worth confirming this was measured against the ant-rc testnet numbers mentioned elsewhere in the codebase before shipping.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 190 to +230
})
}

/// Wait for the first authenticated QUIC packet from the peer.
///
/// This completes before the full handshake and can be used to distinguish basic peer
/// reachability from handshake completion.
pub async fn first_peer_response(&mut self) -> Result<(), ConnectionError> {
if !self.has_peer_response()? {
if let Some(x) = self.first_peer_response.take() {
let _ = x.await;
}
}

let conn = self.conn.as_ref().ok_or_else(|| {
tracing::error!("Connection state missing while waiting for first peer response");
ConnectionError::LocallyClosed
})?;
let inner = conn.state.lock("first_peer_response");
if inner.peer_response_received {
Ok(())
} else {
Err(inner.error.clone().unwrap_or_else(|| {
error!("Spurious peer response notification with no error");
ConnectionError::TransportError(crate::transport_error::Error::INTERNAL_ERROR(
"Spurious peer response notification".to_string(),
))
}))
}
}

fn has_peer_response(&self) -> Result<bool, ConnectionError> {
let conn = self.conn.as_ref().ok_or_else(|| {
tracing::error!("Connection state missing while checking first peer response");
ConnectionError::LocallyClosed
})?;
Ok(conn
.state
.lock("first_peer_response")
.peer_response_received)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 first_peer_response receiver dropped silently if called after connection close

If Connecting::first_peer_response() is called after the connection has already terminated (so self.conn is None), has_peer_response() returns Err(LocallyClosed). This is propagated by ? — correct. However, if self.conn is still Some but peer_response_received is false AND on_first_peer_response has already been taken (e.g., a second call), the function silently skips the oneshot wait and falls through to the state lock. At that point peer_response_received is still false and error may not yet be set, so the function hits the unwrap_or_else branch and logs a spurious error!. Consider guarding against multiple calls more explicitly or documenting that the method is single-use.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/high_level/connection.rs
Line: 190-230

Comment:
**`first_peer_response` receiver dropped silently if called after connection close**

If `Connecting::first_peer_response()` is called after the connection has already terminated (so `self.conn` is `None`), `has_peer_response()` returns `Err(LocallyClosed)`. This is propagated by `?` — correct. However, if `self.conn` is still `Some` but `peer_response_received` is false AND `on_first_peer_response` has already been taken (e.g., a second call), the function silently skips the oneshot wait and falls through to the state lock. At that point `peer_response_received` is still false and `error` may not yet be set, so the function hits the `unwrap_or_else` branch and logs a spurious `error!`. Consider guarding against multiple calls more explicitly or documenting that the method is single-use.

How can I resolve this? If you propose a fix, please make it concise.

@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results

Performance Comparison

Benchmark Baseline Current Change Status

Summary

Configuration

  • Regression threshold: >10% slower
  • Improvement threshold: >10% faster
  • Measurements: Mean execution time

@jacderida
jacderida merged commit 5d2408e into main May 5, 2026
54 of 56 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants