Skip to main content

futu_net/
icmp_probe.rs

1//! Independent ICMP echo adapter used by C++-aligned delay calibration.
2//!
3//! This path deliberately does not reuse the Platform TCP channel or public
4//! CMD1316. Ref: `FTBasis/Src/ftbasis/ping.cpp` and
5//! `NNProtoCenter/Other/NNBiz_SvrTime.cpp:41-78`.
6
7use std::net::IpAddr;
8use std::time::Duration;
9
10use async_trait::async_trait;
11use surge_ping::{Client, Config, ICMP, PingIdentifier, PingSequence};
12use thiserror::Error;
13
14// Ref: `FTBasis/Src/ftbasis/protocol/ping/impl/ping.cpp:211-216,315-318`.
15// Windows C++ passes `sizeof(req_data)`, including the terminating NUL; the
16// ASIO path sends the `std::string` bytes without a terminator.
17#[cfg(windows)]
18const ICMP_PAYLOAD: &[u8] = b"\"Hello!\" from ftnet ping.......\0";
19#[cfg(not(windows))]
20const ICMP_PAYLOAD: &[u8] = b"\"Hello!\" from Futu ping.";
21
22#[derive(Debug, Error)]
23pub enum IcmpProbeError {
24    #[error("failed to create ICMP client: {0}")]
25    Client(String),
26    #[error("ICMP echo failed: {0}")]
27    Echo(String),
28}
29
30#[async_trait]
31pub trait IcmpProbe: Send + Sync + std::fmt::Debug {
32    async fn probe(&self, target: IpAddr, timeout: Duration) -> Result<Duration, IcmpProbeError>;
33}
34
35/// Production ICMP adapter.
36///
37/// C++ starts an independent one-host ping task every calibration cycle. Rust
38/// likewise creates a short-lived client for the target address family, sends
39/// exactly one echo request, and returns the measured RTT. Permission, socket,
40/// timeout, and receive errors remain explicit non-fatal probe failures.
41#[derive(Debug, Default)]
42pub struct SurgeIcmpProbe;
43
44#[async_trait]
45impl IcmpProbe for SurgeIcmpProbe {
46    async fn probe(&self, target: IpAddr, timeout: Duration) -> Result<Duration, IcmpProbeError> {
47        let kind = match target {
48            IpAddr::V4(_) => ICMP::V4,
49            IpAddr::V6(_) => ICMP::V6,
50        };
51        let config = Config::builder().kind(kind).build();
52        let client =
53            Client::new(&config).map_err(|error| IcmpProbeError::Client(error.to_string()))?;
54        let mut pinger = client.pinger(target, PingIdentifier(0)).await;
55        pinger.timeout(timeout);
56        let (_, round_trip) = pinger
57            .ping(PingSequence(0), ICMP_PAYLOAD)
58            .await
59            .map_err(|error| IcmpProbeError::Echo(error.to_string()))?;
60        Ok(round_trip)
61    }
62}