feat: Add connection extension framework with symmetric client-server hooks#316
feat: Add connection extension framework with symmetric client-server hooks#316jiangliu wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a symmetric connection extension framework to ttrpc’s async client/server stack, enabling applications to attach per-connection metadata and optionally transform payload bytes (e.g., for encryption/auth) via hooks and a PayloadTransform.
Changes:
- Introduces a new
extensionmodule with hook/transform traits, connection context, and typed connection metadata propagation. - Wires the connection context through async server/client/stream pipelines, applying transforms at defined injection points and adding a
skip_transforminternal flag for synthetic stream DATA. - Extends async transport
Socketto capture Unixraw_fdso hooks can inspect peers and perform handshake I/O.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/hook_integration.rs | Adds end-to-end integration tests for hooks/transforms over Unix sockets. |
| src/extension.rs | New core extension framework types (hooks, transform trait, connection context) plus unit tests. |
| src/lib.rs | Exposes the new extension module and re-exports key extension APIs. |
| src/proto.rs | Adds non-wire internal_flags to GenMessage and helper constructors for DATA/RESPONSE/close. |
| src/asynchronous/utils.rs | Adds connection_data to TtrpcContext for handler access. |
| src/asynchronous/server.rs | Installs accept hook, propagates connection context, and applies transforms in server pipeline. |
| src/asynchronous/client.rs | Adds Client::with_hook, runs connect hook, and applies transforms in client pipeline. |
| src/asynchronous/stream.rs | Applies transforms for streaming DATA send/recv with skip-transform support. |
| src/asynchronous/transport/mod.rs | Refactors Socket to store Unix raw_fd and exposes as_raw_fd(). |
| src/asynchronous/transport/unix.rs | Ensures accepted/connected Unix sockets capture raw_fd. |
| src/asynchronous/transport/tcp.rs | Ensures accepted/connected TCP sockets capture raw_fd. |
| src/asynchronous/transport/vsock.rs | Ensures accepted/connected vsock sockets capture raw_fd. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
e1496d5 to
8ebbffb
Compare
ce6ccfd to
cf31d6b
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (1)
src/asynchronous/server.rs:692
respond()appliestransform_outbound, but it doesn't enforceMESSAGE_LENGTH_MAXon the transformed payload. A transform that expands data can cause oversized responses to be sent (especially for error/status responses that go throughrespond_with_status). Addcheck_oversizeafter the transform.
let payload = self
.conn_ctx
.transform_outbound(payload)
.map_err(|e| Error::Others(format!("transform_outbound failed: {}", e)))?;
let msg = GenMessage::new_response(stream_id, payload);
cf31d6b to
0194e89
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
src/asynchronous/server.rs:694
respond()appliestransform_outboundbut never enforcesMESSAGE_LENGTH_MAX. Other outbound paths (e.g. the normal unary response path) check size after transform; without a check here, an oversized (or expansion-prone) transform can cause the peer to reject/discard the message and the caller to hang until timeout.
let payload = self
.conn_ctx
.transform_outbound(payload)
.map_err(|e| Error::Others(format!("transform_outbound failed: {}", e)))?;
let msg = GenMessage::new_response(stream_id, payload);
0194e89 to
14655b1
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
src/asynchronous/server.rs:694
respond()appliestransform_outboundbut does not enforceMESSAGE_LENGTH_MAXafter the transform. A transform (e.g., encryption) can expand the payload beyond the limit, leading to oversized frames being written and then rejected by the peer during read/oversize checks.
let payload = self
.conn_ctx
.transform_outbound(payload)
.map_err(|e| Error::Others(format!("transform_outbound failed: {}", e)))?;
let msg = GenMessage::new_response(stream_id, payload);
14655b1 to
1f6a055
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (2)
src/asynchronous/server.rs:695
respond()appliestransform_outboundbut does not re-check the transformed payload size. A transform can expand data, so this can violateMESSAGE_LENGTH_MAXand produce frames the peer will reject or that can break framing.
let payload = self
.conn_ctx
.transform_outbound(payload)
.map_err(|e| Error::Others(format!("transform_outbound failed: {}", e)))?;
let msg = GenMessage::new_response(stream_id, payload);
src/asynchronous/server.rs:455
- When
transform_outboundfails, this path tries to send an INTERNAL status viarespond_with_status(), which will calltransform_outboundagain and likely fail the same way—leaving the client waiting until timeout. Consider closing the stream/connection to unblock the client when the transform is unusable.
self.respond_with_status(
stream_id,
get_status(Code::INTERNAL, format!("transform_outbound: {}", e)),
)
.await;
1f6a055 to
0325c7b
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
src/asynchronous/server.rs:695
respond()appliestransform_outbound, but it does not enforceMESSAGE_LENGTH_MAX(or callGenMessage::check()) after the transform. A transform can expand payload size (e.g., AEAD tag, framing, compression expansion), which would cause the peer to reject/discard the message and potentially hang waiting for a response. Add a post-transform size check before sending.
let payload = self
.conn_ctx
.transform_outbound(payload)
.map_err(|e| Error::Others(format!("transform_outbound failed: {}", e)))?;
let msg = GenMessage::new_response(stream_id, payload);
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (1)
src/proto.rs:197
- Making
GenMessage#[non_exhaustive]and adding apub(crate)field is a breaking API change for any downstream crate that previously constructed/destructuredttrpc::proto::GenMessage. If the crate intends to preserve semver compatibility, consider keepingGenMessageout of the public API surface (e.g.,pub(crate)/#[doc(hidden)]) or making the new field publicly settable with a documented default, and call out the breaking change explicitly in release notes.
/// This struct is `#[non_exhaustive]` to allow adding internal fields without
/// breaking downstream construction. Use the constructor methods
/// ([`new_data`](Self::new_data), [`new_data_decrypted`](Self::new_data_decrypted),
/// [`new_response`](Self::new_response), [`new_close`](Self::new_close)) instead of
/// struct literals.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
src/asynchronous/server.rs:702
respond()appliestransform_outbound, but it never re-validates the transformed payload size. If a transform expands the payload beyondMESSAGE_LENGTH_MAX, the server will attempt to send an oversized frame (and the peer will reject it). Add an explicit size check (ormsg.check()?) after the transform, consistent with the other response path.
.conn_ctx
.transform_outbound(payload)
.map_err(|e| Error::Others(format!("transform_outbound failed: {}", e)))?;
let msg = GenMessage::new_response(stream_id, payload);
self.tx
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (2)
src/proto.rs:197
- The doc comment for
GenMessageis internally inconsistent: adding#[non_exhaustive]and apub(crate)field does break downstream struct-literal construction, and the recommended constructor methods arepub(crate)+#[cfg(feature = "async")], so downstream users can’t follow this guidance (and sync builds won’t have them). Consider updating the docs to reflect thatGenMessageis not intended to be constructed downstream (or make the constructors public and available without theasynccfg).
/// Generic message of ttrpc.
///
/// This struct is `#[non_exhaustive]` to allow adding internal fields without
/// breaking downstream construction. Use the constructor methods
/// ([`new_data`](Self::new_data), [`new_data_decrypted`](Self::new_data_decrypted),
/// [`new_response`](Self::new_response), [`new_close`](Self::new_close)) instead of
/// struct literals.
src/asynchronous/server.rs:702
HandlerContext::respond()appliestransform_outbound()but never re-checks the post-transform payload size. A transform that expands data (e.g. decompression or header-wrapping) can exceedMESSAGE_LENGTH_MAXand lead to the peer rejecting/discarding the message. The main response path performs this check, but error/status responses sent viarespond()currently do not.
async fn respond(&self, stream_id: u32, resp: Response) -> Result<()> {
let payload = resp
.encode()
.map_err(err_to_others_err!(e, "Encode Response failed."))?;
let payload = self
.conn_ctx
.transform_outbound(payload)
.map_err(|e| Error::Others(format!("transform_outbound failed: {}", e)))?;
let msg = GenMessage::new_response(stream_id, payload);
self.tx
377e22c to
edf3f6a
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
src/asynchronous/server.rs:429
check_oversize(resp.compute_size())runs beforetransform_outbound(). If aPayloadTransformshrinks payloads (e.g., compression), this will incorrectly reject responses that would be under the limit on the wire after transformation. Size enforcement should be based on the bytes actually sent (post-transform).
Some(mut resp) => {
// Server: check size before sending to client
if let Err(e) = check_oversize(resp.compute_size() as usize, true) {
resp = e.into();
}
src/asynchronous/utils.rs:264
- This adds a new public field to
TtrpcContext. SinceTtrpcContextis a public struct, adding a field is a Rust semver-breaking change for any downstream code that constructs it with a struct literal—contradicting the PR description's “no breakage” claim. Consider documenting this as a breaking change and/or providing a stable construction/access pattern (e.g., constructors/getters) to reduce future churn.
/// Opaque per-connection data from [`AcceptHook`](crate::extension::AcceptHook). Immutable after accept.
/// See [`ConnectionData`](crate::extension::ConnectionData) for full contract.
pub connection_data: Arc<ConnectionData>,
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (6)
src/extension.rs:383
transform_inbound()matches onself.payload_transformby value, which attempts to move theOption<Arc<..>>out of&selfand will not compile. Match on a borrow (e.g.,.as_ref()or&self.payload_transform) instead.
match self.payload_transform {
Some(ref xform) => xform.transform_inbound(payload),
None => Ok(payload),
}
src/extension.rs:392
transform_outbound()matches onself.payload_transformby value, which attempts to move theOption<Arc<..>>out of&selfand will not compile. Match on a borrow (e.g.,.as_ref()or&self.payload_transform) instead.
match self.payload_transform {
Some(ref xform) => xform.transform_outbound(payload),
None => Ok(payload),
}
src/extension.rs:402
transform_inbound_msg()usesif let Some(ref xform) = self.payload_transform, which tries to movepayload_transformout of&selfand will not compile. Borrow the option (e.g.,if let Some(xform) = self.payload_transform.as_ref()).
if let Some(ref xform) = self.payload_transform {
msg.payload = xform.transform_inbound(std::mem::take(&mut msg.payload))?;
// Update header length to match decrypted payload
msg.header.length = msg.payload.len() as u32;
}
src/extension.rs:413
transform_outbound_msg()usesif let Some(ref xform) = self.payload_transform, which tries to movepayload_transformout of&selfand will not compile. Borrow the option (e.g.,if let Some(xform) = self.payload_transform.as_ref()).
if let Some(ref xform) = self.payload_transform {
msg.payload = xform.transform_outbound(std::mem::take(&mut msg.payload))?;
// Update header length to match transformed payload
msg.header.length = msg.payload.len() as u32;
}
src/asynchronous/stream.rs:474
StreamSender::transform_outbound()matches onself.payload_transformby value, which attempts to move theOption<Arc<..>>out of&selfand will not compile. Match on a borrow (e.g.,.as_ref()) instead.
match self.payload_transform {
Some(ref xform) => xform
.transform_outbound(buf)
.map_err(|e| Error::Others(format!("transform_outbound failed: {}", e))),
None => Ok(buf),
src/asynchronous/stream.rs:520
StreamReceiver::transform_inbound()matches onself.payload_transformby value, which attempts to move theOption<Arc<..>>out of&selfand will not compile. Match on a borrow (e.g.,.as_ref()) instead.
match self.payload_transform {
Some(ref xform) => xform
.transform_inbound(payload)
.map_err(|e| Error::Others(format!("transform_inbound failed: {}", e))),
None => Ok(payload),
26858c4 to
5529d4e
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
src/proto.rs:196
- The doc comment says
#[non_exhaustive]allows adding internal fields "without breaking downstream compatibility", but#[non_exhaustive]itself is a (source) breaking change for downstream code that does exhaustive matches or struct literals. The comment should be reworded to avoid promising compatibility that isn’t guaranteed.
/// This struct is `#[non_exhaustive]` to allow adding internal fields
/// (such as `internal_flags`) without breaking downstream compatibility.
/// `GenMessage` is constructed internally by the ttrpc framework and is
/// not intended to be built directly by downstream code.
5529d4e to
2d55cee
Compare
…r hooks
ttrpc currently provides no mechanism for applications to inject
connection-level policies such as encryption, authentication, or
file-descriptor passthrough. Each RPC message passes through
serialization and transport layers with no opportunity for the
application to inspect or transform the wire-format bytes, leaving
security-critical concerns to be handled out-of-band or not at all.
Introduce a symmetric extension framework that exposes 10 injection
points across the client and server lifecycle:
1. AcceptHook::on_accept — server-side connection acceptance
2. Unary REQUEST transform_inbound — decrypt before dispatch
3. Streaming DATA route-only — no transform in router
4. Unary RESPONSE transform_outbound — encrypt after handler
5. Streaming DATA send transform_outbound — encrypt each chunk
6. Streaming DATA recv transform_inbound — decrypt each chunk
7. ConnectHook::on_connect — client-side connection establishment
8. Client REQUEST transform_outbound — encrypt before send
9. Client RESPONSE transform_inbound — decrypt after recv
10. GenMessage internal_flags — skip double-transform
The framework is symmetric: a server that registers hooks must pair
with a client that registers the matching hooks, and both sides share
the same PayloadTransform trait for consistent wire-format transforms.
New types in src/extension.rs:
- PayloadTransform — trait for wire-format byte transforms
- AcceptHook / ConnectHook — trait for connection lifecycle hooks
- ConnectionContext — holds hook references, immutable after accept
Wire-format changes:
- GenMessage gains a pub(crate) internal_flags field (never serialized)
with a skip_transform bit to prevent double-decryption when the
streaming_client=false path creates a faked DATA message from an
already-decrypted REQUEST payload.
- Helper constructors (new_data, new_data_decrypted, new_response,
new_close) encapsulate internal_flags to keep call sites clean.
Error handling:
- ConnectHook::on_connect errors are propagated through
Client::with_hook, which now returns Result<Client>. This prevents
a rejected or failed hook from silently downgrading the connection
to an untransformed, default context.
- AcceptHook::on_accept errors are propagated through
ConnectionContext::on_accept as HookError so callers can log the
specific rejection reason before dropping the connection.
- Server error/status responses sent via respond() / respond_with_status()
now apply the connection's transform_outbound, so error paths on
transformed connections still produce decryptable responses.
Platform support:
- AcceptHook, ConnectHook, and all RawFd-dependent APIs are gated
with #[cfg(unix)] and compile out cleanly on Windows.
- Client::with_hook and the ConnectHook import are gated so the
async client builds on Windows without the Unix-only hook.
- Server base_ctx construction uses functional struct update syntax
so the optional accept_hook field is initialized conditionally
without a mutability warning on non-Unix targets.
Compatibility (source-level breaking changes):
- TtrpcContext gains a `connection_data` field. This breaks
downstream code that constructs TtrpcContext with a struct literal.
TtrpcContext now derives Default, so downstream code can use
`..Default::default()` for forward-compatible construction.
- GenMessage is marked #[non_exhaustive] and gains a pub(crate)
internal_flags field, breaking downstream struct-literal
construction. GenMessage is intended for internal framework use;
downstream code should not construct it directly.
- Client::with_hook now returns Result<Client> instead of Client,
breaking downstream code that constructs clients with connect hooks.
- Size validation for unary responses is now performed after
transform_outbound (post-transform), not before. A pre-transform
check would incorrectly reject responses that fit on the wire after
a compression transform.
No changes to the wire-protocol header format; the transform operates
on the payload bytes after MessageHeader framing.
Signed-off-by: Jiang Liu <gerry@linux.alibaba.com>
The extension framework introduces 10 injection points for wire-format transforms across the client-server lifecycle. Without dedicated tests, regressions in transform ordering (e.g., double-transform on the streaming_client=false path) or missing injection points would silently produce incorrect payload bytes. Add comprehensive test coverage for all 10 injection points: Unit tests (src/extension.rs, 20 tests): - PayloadTransform::transform_inbound / transform_outbound with XOR-0xA5A5 - transform_inbound_msg / transform_outbound_msg on GenMessage - AcceptHook::on_accept with mock TCP socket - ConnectHook::on_connect with mock TCP connection - ConnectionContext helpers (transform_*_msg delegation) - File descriptor passthrough via ConnectionContext - Asymmetric transform (distinct inbound/outbound keys) - Empty payload edge case Integration tests (tests/hook_integration_unix.rs, 19 tests): - Symmetric client/server hooks with data and transform - XOR transform on the wire (end-to-end encryption) - Streaming with XOR transform (chunked encrypt/decrypt) - streaming_client=false path (server creates faked DATA) - streaming_server=false path (rejects DATA, unary only) - Multiple concurrent streams on one connection - Multiple concurrent connections with transform - Unary request timeout - Server-initiated stream close - Server shutdown during active stream - Connect hook called and receives valid raw fd - Connect hook rejection fails connection - Accept hook called on connection - Accept hook rejects connection - Connection data propagated to handler - No-hook plaintext passthrough (baseline) Also adds a unit test in src/asynchronous/client.rs verifying that Client::with_hook fails when the Socket has no raw fd, preventing a silent downgrade to an untransformed connection. Signed-off-by: Jiang Liu <gerry@linux.alibaba.com>
2d55cee to
d318cda
Compare
| /// Generic message of ttrpc. | ||
| /// | ||
| /// This struct is `#[non_exhaustive]` to allow adding internal fields | ||
| /// (such as `internal_flags`) without further breaking changes. | ||
| /// `GenMessage` is constructed internally by the ttrpc framework and is | ||
| /// not intended to be built directly by downstream code. | ||
| #[non_exhaustive] | ||
| #[derive(Default, Debug, Clone, PartialEq, Eq)] | ||
| pub struct GenMessage { | ||
| pub header: MessageHeader, | ||
| pub payload: Vec<u8>, | ||
| /// Internal flags, never serialized to the wire. | ||
| /// Bit 0: skip_transform — payload is already decrypted, do not apply | ||
| /// `transform_inbound` in `StreamReceiver::recv()`. | ||
| pub(crate) internal_flags: u8, | ||
| } |
| // Sleep longer than any reasonable client timeout | ||
| sleep(Duration::from_secs(10)).await; |
| server.start().await.unwrap(); | ||
| sleep(Duration::from_millis(100)).await; |
| Some(resp) => { | ||
| // Encode response to protobuf, then apply transform. | ||
| // | ||
| // Note: size validation is performed *after* transform_outbound | ||
| // (below), not before. A transform may compress the payload, | ||
| // so checking the pre-transform protobuf size would incorrectly | ||
| // reject responses that fit on the wire after compression. | ||
| match resp.encode() { | ||
| Ok(payload) => { | ||
| // ── Injection Point 4/10: unary response transform_outbound ── | ||
| match self.conn_ctx.transform_outbound(payload) { | ||
| Ok(payload) => { | ||
| if check_oversize(payload.len(), true).is_err() { | ||
| self.respond_with_status( | ||
| stream_id, | ||
| get_status( | ||
| Code::INVALID_ARGUMENT, | ||
| "transformed response exceeds maximum message size", | ||
| ), | ||
| ) | ||
| .await; | ||
| return; | ||
| } | ||
| let msg = GenMessage::new_response(stream_id, payload); | ||
| self.tx | ||
| .send(SendingMessage::new(msg)) | ||
| .await | ||
| .map_err(|e| error!("Send packet error {:?}", e)) | ||
| .ok(); | ||
| } | ||
| Err(e) => { | ||
| error!("transform_outbound failed: {}", e); | ||
| self.respond_with_status( | ||
| stream_id, | ||
| get_status(Code::INTERNAL, format!("transform_outbound: {}", e)), | ||
| ) | ||
| .await; | ||
| } | ||
| } | ||
| } |
| match resp.encode() { | ||
| Ok(payload) => { | ||
| // ── Injection Point 4/10: unary response transform_outbound ── | ||
| match self.conn_ctx.transform_outbound(payload) { | ||
| Ok(payload) => { | ||
| if check_oversize(payload.len(), true).is_err() { | ||
| self.respond_with_status( | ||
| stream_id, | ||
| get_status( | ||
| Code::INVALID_ARGUMENT, | ||
| "transformed response exceeds maximum message size", | ||
| ), | ||
| ) | ||
| .await; | ||
| return; | ||
| } |
Summary
ttrpc currently provides no mechanism for applications to inject connection-level policies such as encryption, authentication, or file-descriptor passthrough. Each RPC message passes through serialization and transport layers with no opportunity for the application to inspect or transform the wire-format bytes, leaving security-critical concerns to be handled out-of-band or not at all.
This PR introduces a symmetric extension framework that exposes 10 injection points across the client and server lifecycle, enabling applications to implement composable security policies (e.g., AES-GCM encryption, Ed25519 authentication) without modifying ttrpc core.
Design Principles
AcceptHook(server) andConnectHook(client) share the sameHookOutput/HookErrortypes andPayloadTransformtrait.payload_transform = None→ plaintext pass-through.10 Injection Points
do_start()acceptAcceptHook::on_accepthandle_request()transform_inboundhandle_msg()DATAhandle_method()responsetransform_outboundnew_inner()connectConnectHook::on_connectrequest()sendtransform_outboundhandle_msg()recvtransform_inboundnew_stream()sendtransform_outboundStreamSender::send()transform_outboundStreamReceiver::recv()transform_inboundKey design decision: Streaming DATA messages are routed (not transformed) in
handle_msg()(#3). The transform is applied exclusively inStreamSender::send()/StreamReceiver::recv()(#9/#10) to avoid double-transform.streaming_client=falsewith initial payloadWhen
streaming_client=falseand the stream-init REQUEST carries a payload,handle_stream()creates a synthetic DATA message from the already-decrypted REQUEST payload (decrypted at #2). Apub(crate) internal_flagsfield onGenMessagecarries askip_transformbit soStreamReceiver::recv()(#10) passes it through without re-applyingtransform_inbound. This works for anyPayloadTransform, including asymmetric transforms.New Public API
Server registration
Client registration
Socket raw_fd Capture
Socketnow stores the underlyingRawFd(Unix only) so hooks can callgetpeername()for peer identity inspection (e.g., vsock CID) and perform bidirectional handshake I/O. Platform-specificFromimpls (TcpStream,UnixStream,VsockStream) capture the fd viaas_raw_fd(); the genericSocket::new()setsraw_fd = None.Files Changed
src/extension.rsConnectionContext, 20 unit teststests/hook_integration.rssrc/proto.rsGenMessage::internal_flags+ helper constructorssrc/lib.rsHookError,HookOutput,AcceptHook,ConnectHooksrc/asynchronous/server.rsset_accept_hook(),on_accept()in accept loop, injection points #2/#3/#4src/asynchronous/client.rswith_hook(),ConnectHookinnew_inner(), injection points #6/#7/#8src/asynchronous/stream.rspayload_transformfield on sender/receiver, injection points #9/#10src/asynchronous/transport/mod.rsSocketgainsraw_fd: Option<RawFd>,Socket::from()captures fdsrc/asynchronous/transport/{tcp,unix,vsock}.rsFromimpls captureas_raw_fd()src/asynchronous/utils.rsTtrpcContext::connection_datafieldTest Coverage
87 tests (67 unit + 19 integration + 1 example):
ConnectionContextconstruction, Arc sharing, transform pass-throughstreaming_client=falsewith initial payload (skip_transform path)streaming_server=falseDATA message rejectionBackward Compatibility
payload_transform = Noneconnection_datais empty HashMap — no breakage