Skip to main content

fips_core/transport/
handle.rs

1//! Enum wrapper for concrete transport implementations.
2
3#[cfg(target_os = "linux")]
4use super::ble::DefaultBleTransport;
5#[cfg(any(target_os = "linux", target_os = "macos"))]
6use super::ethernet::EthernetTransport;
7#[cfg(feature = "sim-transport")]
8use super::sim::SimTransport;
9use super::tcp::TcpTransport;
10use super::tor::TorTransport;
11use super::tor::control::TorMonitoringInfo;
12use super::udp::UdpTransport;
13#[cfg(feature = "webrtc-transport")]
14use super::webrtc::WebRtcTransport;
15use super::websocket::WebSocketTransport;
16use super::{
17    ConnectionState, DiscoveredPeer, Transport, TransportAddr, TransportCongestion, TransportError,
18    TransportId, TransportState, TransportType,
19};
20
21/// Wrapper enum for concrete transport implementations.
22///
23/// This enables polymorphic transport handling without trait objects,
24/// supporting async methods that the sync Transport trait cannot express.
25pub enum TransportHandle {
26    /// UDP/IP transport.
27    Udp(UdpTransport),
28    /// In-memory simulated packet transport.
29    #[cfg(feature = "sim-transport")]
30    Sim(SimTransport),
31    /// Raw Ethernet transport.
32    #[cfg(any(target_os = "linux", target_os = "macos"))]
33    Ethernet(EthernetTransport),
34    /// TCP/IP transport.
35    Tcp(TcpTransport),
36    /// Binary WebSocket physical transport.
37    WebSocket(Box<WebSocketTransport>),
38    /// Tor transport (via SOCKS5).
39    Tor(TorTransport),
40    /// WebRTC DataChannel transport.
41    #[cfg(feature = "webrtc-transport")]
42    WebRtc(Box<WebRtcTransport>),
43    /// BLE L2CAP transport.
44    #[cfg(target_os = "linux")]
45    Ble(DefaultBleTransport),
46}
47
48impl TransportHandle {
49    pub(crate) fn is_configured_websocket_adjacency(
50        &self,
51        addr: &TransportAddr,
52        handshake_is_initiator: bool,
53    ) -> bool {
54        match self {
55            TransportHandle::WebSocket(transport) => {
56                transport.is_configured_adjacency(addr, handshake_is_initiator)
57            }
58            _ => false,
59        }
60    }
61
62    /// Normalize transport-specific addresses before Node stores or compares
63    /// them. Most transports have one textual identity; WebRTC uses Nostr's
64    /// x-only identity and therefore collapses legacy 02/03 compressed forms.
65    pub(crate) fn canonical_addr(
66        &self,
67        addr: &TransportAddr,
68    ) -> Result<TransportAddr, TransportError> {
69        match self {
70            #[cfg(feature = "webrtc-transport")]
71            TransportHandle::WebRtc(_) => super::webrtc::canonical_webrtc_addr(addr),
72            _ => Ok(addr.clone()),
73        }
74    }
75
76    /// Drain adapter negotiations that must travel over the standard FSP
77    /// link-negotiation service.
78    #[cfg(feature = "webrtc-transport")]
79    pub(crate) fn drain_link_negotiations(
80        &mut self,
81        limit: usize,
82    ) -> Vec<super::link_negotiation::OutboundLinkNegotiation> {
83        match self {
84            TransportHandle::WebRtc(transport) => transport.drain_link_negotiations(limit),
85            _ => Vec::new(),
86        }
87    }
88
89    /// Deliver an authenticated negotiation to the enabled matching adapter.
90    #[cfg(feature = "webrtc-transport")]
91    pub(crate) fn ingest_link_negotiation(
92        &self,
93        source: secp256k1::PublicKey,
94        message: super::link_negotiation::LinkNegotiationMessage,
95    ) -> Result<(), TransportError> {
96        match self {
97            TransportHandle::WebRtc(transport) if message.link_type == "webrtc" => {
98                transport.ingest_link_negotiation(source, message)
99            }
100            _ => Err(TransportError::NotSupported(
101                "no enabled adapter accepts this link negotiation".into(),
102            )),
103        }
104    }
105
106    /// Start the transport asynchronously.
107    pub async fn start(&mut self) -> Result<(), TransportError> {
108        match self {
109            TransportHandle::Udp(t) => t.start_async().await,
110            #[cfg(feature = "sim-transport")]
111            TransportHandle::Sim(t) => t.start_async().await,
112            #[cfg(any(target_os = "linux", target_os = "macos"))]
113            TransportHandle::Ethernet(t) => t.start_async().await,
114            TransportHandle::Tcp(t) => t.start_async().await,
115            TransportHandle::WebSocket(t) => t.start_async().await,
116            TransportHandle::Tor(t) => t.start_async().await,
117            #[cfg(feature = "webrtc-transport")]
118            TransportHandle::WebRtc(t) => t.start_async().await,
119            #[cfg(target_os = "linux")]
120            TransportHandle::Ble(t) => t.start_async().await,
121        }
122    }
123
124    /// Stop the transport asynchronously.
125    pub async fn stop(&mut self) -> Result<(), TransportError> {
126        match self {
127            TransportHandle::Udp(t) => t.stop_async().await,
128            #[cfg(feature = "sim-transport")]
129            TransportHandle::Sim(t) => t.stop_async().await,
130            #[cfg(any(target_os = "linux", target_os = "macos"))]
131            TransportHandle::Ethernet(t) => t.stop_async().await,
132            TransportHandle::Tcp(t) => t.stop_async().await,
133            TransportHandle::WebSocket(t) => t.stop_async().await,
134            TransportHandle::Tor(t) => t.stop_async().await,
135            #[cfg(feature = "webrtc-transport")]
136            TransportHandle::WebRtc(t) => t.stop_async().await,
137            #[cfg(target_os = "linux")]
138            TransportHandle::Ble(t) => t.stop_async().await,
139        }
140    }
141
142    /// Send data to a remote address asynchronously.
143    pub async fn send(&self, addr: &TransportAddr, data: &[u8]) -> Result<usize, TransportError> {
144        match self {
145            TransportHandle::Udp(t) => t.send_async(addr, data).await,
146            #[cfg(feature = "sim-transport")]
147            TransportHandle::Sim(t) => t.send_async(addr, data).await,
148            #[cfg(any(target_os = "linux", target_os = "macos"))]
149            TransportHandle::Ethernet(t) => t.send_async(addr, data).await,
150            TransportHandle::Tcp(t) => t.send_async(addr, data).await,
151            TransportHandle::WebSocket(t) => t.send_async(addr, data).await,
152            TransportHandle::Tor(t) => t.send_async(addr, data).await,
153            #[cfg(feature = "webrtc-transport")]
154            // Keep this optional adapter's large future out of every transport send frame.
155            TransportHandle::WebRtc(t) => Box::pin(t.send_async(addr, data)).await,
156            #[cfg(target_os = "linux")]
157            TransportHandle::Ble(t) => t.send_async(addr, data).await,
158        }
159    }
160
161    pub(crate) async fn send_with_timeout(
162        &self,
163        addr: &TransportAddr,
164        data: &[u8],
165        timeout: std::time::Duration,
166    ) -> Result<usize, TransportError> {
167        match self {
168            TransportHandle::Tcp(transport) => {
169                transport.send_async_with_timeout(addr, data, timeout).await
170            }
171            _ => self.send(addr, data).await,
172        }
173    }
174
175    /// Get the transport ID.
176    pub fn transport_id(&self) -> TransportId {
177        match self {
178            TransportHandle::Udp(t) => t.transport_id(),
179            #[cfg(feature = "sim-transport")]
180            TransportHandle::Sim(t) => t.transport_id(),
181            #[cfg(any(target_os = "linux", target_os = "macos"))]
182            TransportHandle::Ethernet(t) => t.transport_id(),
183            TransportHandle::Tcp(t) => t.transport_id(),
184            TransportHandle::WebSocket(t) => t.transport_id(),
185            TransportHandle::Tor(t) => t.transport_id(),
186            #[cfg(feature = "webrtc-transport")]
187            TransportHandle::WebRtc(t) => t.transport_id(),
188            #[cfg(target_os = "linux")]
189            TransportHandle::Ble(t) => t.transport_id(),
190        }
191    }
192
193    /// Whether this carrier uses the shared FMP/FSP byte-stream record reader.
194    pub(crate) fn uses_fips_byte_stream_framing(&self) -> bool {
195        matches!(self, TransportHandle::Tcp(_) | TransportHandle::Tor(_))
196    }
197
198    /// Get the instance name (if configured as a named instance).
199    pub fn name(&self) -> Option<&str> {
200        match self {
201            TransportHandle::Udp(t) => t.name(),
202            #[cfg(feature = "sim-transport")]
203            TransportHandle::Sim(t) => t.name(),
204            #[cfg(any(target_os = "linux", target_os = "macos"))]
205            TransportHandle::Ethernet(t) => t.name(),
206            TransportHandle::Tcp(t) => t.name(),
207            TransportHandle::WebSocket(t) => t.name(),
208            TransportHandle::Tor(t) => t.name(),
209            #[cfg(feature = "webrtc-transport")]
210            TransportHandle::WebRtc(t) => t.name(),
211            #[cfg(target_os = "linux")]
212            TransportHandle::Ble(t) => t.name(),
213        }
214    }
215
216    /// Get the transport type metadata.
217    pub fn transport_type(&self) -> &TransportType {
218        match self {
219            TransportHandle::Udp(t) => t.transport_type(),
220            #[cfg(feature = "sim-transport")]
221            TransportHandle::Sim(t) => t.transport_type(),
222            #[cfg(any(target_os = "linux", target_os = "macos"))]
223            TransportHandle::Ethernet(t) => t.transport_type(),
224            TransportHandle::Tcp(t) => t.transport_type(),
225            TransportHandle::WebSocket(t) => t.transport_type(),
226            TransportHandle::Tor(t) => t.transport_type(),
227            #[cfg(feature = "webrtc-transport")]
228            TransportHandle::WebRtc(t) => t.transport_type(),
229            #[cfg(target_os = "linux")]
230            TransportHandle::Ble(t) => t.transport_type(),
231        }
232    }
233
234    /// Get current transport state.
235    pub fn state(&self) -> TransportState {
236        match self {
237            TransportHandle::Udp(t) => t.state(),
238            #[cfg(feature = "sim-transport")]
239            TransportHandle::Sim(t) => t.state(),
240            #[cfg(any(target_os = "linux", target_os = "macos"))]
241            TransportHandle::Ethernet(t) => t.state(),
242            TransportHandle::Tcp(t) => t.state(),
243            TransportHandle::WebSocket(t) => t.state(),
244            TransportHandle::Tor(t) => t.state(),
245            #[cfg(feature = "webrtc-transport")]
246            TransportHandle::WebRtc(t) => t.state(),
247            #[cfg(target_os = "linux")]
248            TransportHandle::Ble(t) => t.state(),
249        }
250    }
251
252    /// Get the transport MTU.
253    pub fn mtu(&self) -> u16 {
254        match self {
255            TransportHandle::Udp(t) => t.mtu(),
256            #[cfg(feature = "sim-transport")]
257            TransportHandle::Sim(t) => t.mtu(),
258            #[cfg(any(target_os = "linux", target_os = "macos"))]
259            TransportHandle::Ethernet(t) => t.mtu(),
260            TransportHandle::Tcp(t) => t.mtu(),
261            TransportHandle::WebSocket(t) => t.mtu(),
262            TransportHandle::Tor(t) => t.mtu(),
263            #[cfg(feature = "webrtc-transport")]
264            TransportHandle::WebRtc(t) => t.mtu(),
265            #[cfg(target_os = "linux")]
266            TransportHandle::Ble(t) => t.mtu(),
267        }
268    }
269
270    /// Get the MTU for a specific link address.
271    ///
272    /// Falls back to transport-wide MTU if the transport doesn't
273    /// support per-link MTU or the address is unknown.
274    pub fn link_mtu(&self, addr: &TransportAddr) -> u16 {
275        match self {
276            TransportHandle::Udp(t) => t.link_mtu(addr),
277            #[cfg(feature = "sim-transport")]
278            TransportHandle::Sim(t) => t.link_mtu(addr),
279            #[cfg(any(target_os = "linux", target_os = "macos"))]
280            TransportHandle::Ethernet(t) => t.link_mtu(addr),
281            TransportHandle::Tcp(t) => t.link_mtu(addr),
282            TransportHandle::WebSocket(t) => t.link_mtu(addr),
283            TransportHandle::Tor(t) => t.link_mtu(addr),
284            #[cfg(feature = "webrtc-transport")]
285            TransportHandle::WebRtc(t) => t.link_mtu(addr),
286            #[cfg(target_os = "linux")]
287            TransportHandle::Ble(t) => t.link_mtu(addr),
288        }
289    }
290
291    /// Get the local bound address (UDP/TCP only, returns None for other transports).
292    pub fn local_addr(&self) -> Option<std::net::SocketAddr> {
293        match self {
294            TransportHandle::Udp(t) => t.local_addr(),
295            #[cfg(feature = "sim-transport")]
296            TransportHandle::Sim(_) => None,
297            #[cfg(any(target_os = "linux", target_os = "macos"))]
298            TransportHandle::Ethernet(_) => None,
299            TransportHandle::Tcp(t) => t.local_addr(),
300            TransportHandle::WebSocket(t) => t.local_addr(),
301            TransportHandle::Tor(_) => None,
302            #[cfg(feature = "webrtc-transport")]
303            TransportHandle::WebRtc(_) => None,
304            #[cfg(target_os = "linux")]
305            TransportHandle::Ble(_) => None,
306        }
307    }
308
309    /// Resolve a UDP target only if doing so cannot block on DNS.
310    ///
311    /// Numeric UDP addresses are returned directly; hostnames are returned
312    /// only when the UDP transport already has a fresh cached resolution.
313    pub(crate) fn resolved_udp_socket_addr_if_cached(
314        &self,
315        addr: &TransportAddr,
316    ) -> Option<std::net::SocketAddr> {
317        match self {
318            TransportHandle::Udp(t) => t.resolved_socket_addr_if_cached(addr),
319            _ => None,
320        }
321    }
322
323    /// Get the interface name (Ethernet only, returns None for other transports).
324    pub fn interface_name(&self) -> Option<&str> {
325        match self {
326            TransportHandle::Udp(_) => None,
327            #[cfg(feature = "sim-transport")]
328            TransportHandle::Sim(_) => None,
329            #[cfg(any(target_os = "linux", target_os = "macos"))]
330            TransportHandle::Ethernet(t) => Some(t.interface_name()),
331            TransportHandle::Tcp(_) => None,
332            TransportHandle::WebSocket(_) => None,
333            TransportHandle::Tor(_) => None,
334            #[cfg(feature = "webrtc-transport")]
335            TransportHandle::WebRtc(_) => None,
336            #[cfg(target_os = "linux")]
337            TransportHandle::Ble(_) => None,
338        }
339    }
340
341    /// Get the onion service address (Tor only, returns None for other transports).
342    pub fn onion_address(&self) -> Option<&str> {
343        match self {
344            TransportHandle::Tor(t) => t.onion_address(),
345            _ => None,
346        }
347    }
348
349    /// Get cached Tor daemon monitoring info (Tor only).
350    pub fn tor_monitoring(&self) -> Option<TorMonitoringInfo> {
351        match self {
352            TransportHandle::Tor(t) => t.cached_monitoring(),
353            _ => None,
354        }
355    }
356
357    /// Get the Tor transport mode (Tor only).
358    pub fn tor_mode(&self) -> Option<&str> {
359        match self {
360            TransportHandle::Tor(t) => Some(t.mode()),
361            _ => None,
362        }
363    }
364
365    /// Drain discovered peers from this transport.
366    pub fn discover(&self) -> Result<Vec<DiscoveredPeer>, TransportError> {
367        match self {
368            TransportHandle::Udp(t) => t.discover(),
369            #[cfg(feature = "sim-transport")]
370            TransportHandle::Sim(t) => t.discover(),
371            #[cfg(any(target_os = "linux", target_os = "macos"))]
372            TransportHandle::Ethernet(t) => t.discover(),
373            TransportHandle::Tcp(t) => t.discover(),
374            TransportHandle::WebSocket(t) => t.discover(),
375            TransportHandle::Tor(t) => t.discover(),
376            #[cfg(feature = "webrtc-transport")]
377            TransportHandle::WebRtc(t) => t.discover(),
378            #[cfg(target_os = "linux")]
379            TransportHandle::Ble(t) => t.discover(),
380        }
381    }
382
383    /// Whether this transport auto-connects to discovered peers.
384    pub fn auto_connect(&self) -> bool {
385        match self {
386            TransportHandle::Udp(t) => t.auto_connect(),
387            #[cfg(feature = "sim-transport")]
388            TransportHandle::Sim(t) => t.auto_connect(),
389            #[cfg(any(target_os = "linux", target_os = "macos"))]
390            TransportHandle::Ethernet(t) => t.auto_connect(),
391            TransportHandle::Tcp(t) => t.auto_connect(),
392            TransportHandle::WebSocket(t) => t.auto_connect(),
393            TransportHandle::Tor(t) => t.auto_connect(),
394            #[cfg(feature = "webrtc-transport")]
395            TransportHandle::WebRtc(t) => t.auto_connect(),
396            #[cfg(target_os = "linux")]
397            TransportHandle::Ble(t) => t.auto_connect(),
398        }
399    }
400
401    /// Whether this transport accepts inbound connections.
402    pub fn accept_connections(&self) -> bool {
403        match self {
404            TransportHandle::Udp(t) => t.accept_connections(),
405            #[cfg(feature = "sim-transport")]
406            TransportHandle::Sim(t) => t.accept_connections(),
407            #[cfg(any(target_os = "linux", target_os = "macos"))]
408            TransportHandle::Ethernet(t) => t.accept_connections(),
409            TransportHandle::Tcp(t) => t.accept_connections(),
410            TransportHandle::WebSocket(t) => t.accept_connections(),
411            TransportHandle::Tor(t) => t.accept_connections(),
412            #[cfg(feature = "webrtc-transport")]
413            TransportHandle::WebRtc(t) => t.accept_connections(),
414            #[cfg(target_os = "linux")]
415            TransportHandle::Ble(t) => t.accept_connections(),
416        }
417    }
418
419    /// Initiate a non-blocking connection to a remote address.
420    ///
421    /// For connection-oriented transports (TCP, Tor), spawns a background
422    /// task to establish the connection. For connectionless transports
423    /// (UDP, Ethernet), this is a no-op that returns Ok immediately.
424    ///
425    /// Poll `connection_state()` to check when the connection is ready.
426    pub async fn connect(&self, addr: &TransportAddr) -> Result<(), TransportError> {
427        match self {
428            TransportHandle::Udp(_) => Ok(()), // connectionless
429            #[cfg(feature = "sim-transport")]
430            TransportHandle::Sim(_) => Ok(()), // connectionless
431            #[cfg(any(target_os = "linux", target_os = "macos"))]
432            TransportHandle::Ethernet(_) => Ok(()), // connectionless
433            TransportHandle::Tcp(t) => t.connect_async(addr).await,
434            TransportHandle::WebSocket(t) => t.connect_async(addr).await,
435            TransportHandle::Tor(t) => t.connect_async(addr).await,
436            #[cfg(feature = "webrtc-transport")]
437            TransportHandle::WebRtc(t) => t.connect_async(addr).await,
438            #[cfg(target_os = "linux")]
439            TransportHandle::Ble(t) => t.connect_async(addr).await,
440        }
441    }
442
443    /// Query the state of a connection attempt to a remote address.
444    ///
445    /// For connectionless transports, always returns `ConnectionState::Connected`
446    /// (they are always "connected"). For connection-oriented transports, returns
447    /// the current state of the background connection attempt.
448    pub fn connection_state(&self, addr: &TransportAddr) -> ConnectionState {
449        match self {
450            TransportHandle::Udp(_) => ConnectionState::Connected,
451            #[cfg(feature = "sim-transport")]
452            TransportHandle::Sim(_) => ConnectionState::Connected,
453            #[cfg(any(target_os = "linux", target_os = "macos"))]
454            TransportHandle::Ethernet(_) => ConnectionState::Connected,
455            TransportHandle::Tcp(t) => t.connection_state_sync(addr),
456            TransportHandle::WebSocket(t) => t.connection_state_sync(addr),
457            TransportHandle::Tor(t) => t.connection_state_sync(addr),
458            #[cfg(feature = "webrtc-transport")]
459            TransportHandle::WebRtc(t) => t.connection_state_sync(addr),
460            #[cfg(target_os = "linux")]
461            TransportHandle::Ble(t) => t.connection_state_sync(addr),
462        }
463    }
464
465    /// Close a specific connection on this transport.
466    ///
467    /// No-op for connectionless transports. For TCP/Tor, removes the
468    /// connection from the pool and drops the stream.
469    pub async fn close_connection(&self, addr: &TransportAddr) {
470        match self {
471            TransportHandle::Udp(t) => t.close_connection(addr),
472            #[cfg(feature = "sim-transport")]
473            TransportHandle::Sim(t) => t.close_connection(addr),
474            #[cfg(any(target_os = "linux", target_os = "macos"))]
475            TransportHandle::Ethernet(t) => t.close_connection(addr),
476            TransportHandle::Tcp(t) => t.close_connection_async(addr).await,
477            TransportHandle::WebSocket(t) => t.close_connection_async(addr).await,
478            TransportHandle::Tor(t) => t.close_connection_async(addr).await,
479            #[cfg(feature = "webrtc-transport")]
480            TransportHandle::WebRtc(t) => t.close_connection_async(addr).await,
481            #[cfg(target_os = "linux")]
482            TransportHandle::Ble(t) => t.close_connection_async(addr).await,
483        }
484    }
485
486    /// Schedule cleanup for a connection removed by a synchronous node path.
487    pub fn close_connection_detached(&self, _addr: &TransportAddr) {
488        #[cfg(feature = "webrtc-transport")]
489        if let TransportHandle::WebRtc(transport) = self {
490            transport.close_connection_detached(_addr);
491        }
492    }
493
494    /// Check if transport is operational.
495    pub fn is_operational(&self) -> bool {
496        self.state().is_operational()
497    }
498
499    /// Query transport-local congestion indicators.
500    ///
501    /// Returns a snapshot of congestion signals that the transport can
502    /// observe locally (e.g., kernel receive buffer drops). Fields are
503    /// `None` when the transport doesn't support that signal.
504    pub fn congestion(&self) -> TransportCongestion {
505        match self {
506            TransportHandle::Udp(t) => t.congestion(),
507            #[cfg(feature = "sim-transport")]
508            TransportHandle::Sim(_) => TransportCongestion::default(),
509            #[cfg(any(target_os = "linux", target_os = "macos"))]
510            TransportHandle::Ethernet(_) => TransportCongestion::default(),
511            TransportHandle::Tcp(_) => TransportCongestion::default(),
512            TransportHandle::WebSocket(_) => TransportCongestion::default(),
513            TransportHandle::Tor(_) => TransportCongestion::default(),
514            #[cfg(feature = "webrtc-transport")]
515            TransportHandle::WebRtc(_) => TransportCongestion::default(),
516            #[cfg(target_os = "linux")]
517            TransportHandle::Ble(_) => TransportCongestion::default(),
518        }
519    }
520
521    /// Get transport-specific stats as a JSON value.
522    ///
523    /// Returns a snapshot of counters for the specific transport type.
524    pub fn transport_stats(&self) -> serde_json::Value {
525        match self {
526            TransportHandle::Udp(t) => {
527                serde_json::to_value(t.stats().snapshot()).unwrap_or_default()
528            }
529            #[cfg(feature = "sim-transport")]
530            TransportHandle::Sim(t) => serde_json::to_value(t.stats()).unwrap_or_default(),
531            #[cfg(any(target_os = "linux", target_os = "macos"))]
532            TransportHandle::Ethernet(t) => {
533                let snap = t.stats().snapshot();
534                serde_json::json!({
535                    "frames_sent": snap.frames_sent,
536                    "frames_recv": snap.frames_recv,
537                    "bytes_sent": snap.bytes_sent,
538                    "bytes_recv": snap.bytes_recv,
539                    "send_errors": snap.send_errors,
540                    "recv_errors": snap.recv_errors,
541                    "beacons_sent": snap.beacons_sent,
542                    "beacons_recv": snap.beacons_recv,
543                    "frames_too_short": snap.frames_too_short,
544                    "frames_too_long": snap.frames_too_long,
545                })
546            }
547            TransportHandle::Tcp(t) => {
548                serde_json::to_value(t.stats().snapshot()).unwrap_or_default()
549            }
550            TransportHandle::WebSocket(t) => serde_json::to_value(t.stats()).unwrap_or_default(),
551            TransportHandle::Tor(t) => {
552                serde_json::to_value(t.stats().snapshot()).unwrap_or_default()
553            }
554            #[cfg(feature = "webrtc-transport")]
555            TransportHandle::WebRtc(t) => serde_json::json!({
556                "mtu": t.mtu(),
557                "state": t.state().to_string(),
558            }),
559            #[cfg(target_os = "linux")]
560            TransportHandle::Ble(t) => {
561                serde_json::to_value(t.stats().snapshot()).unwrap_or_default()
562            }
563        }
564    }
565}