Skip to main content

fips_core/transport/
handle.rs

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