ts_netstack_smoltcp_socket/
ping.rs1#[cfg(any(feature = "tokio", test))]
15use alloc::vec;
16#[cfg(any(feature = "tokio", test))]
17use core::net::Ipv4Addr;
18#[cfg(feature = "tokio")]
19use core::{net::IpAddr, time::Duration};
20
21#[cfg(any(feature = "tokio", test))]
22use netcore::smoltcp::{
23 phy::ChecksumCapabilities,
24 wire::{IPV4_HEADER_LEN, Icmpv4Packet, Icmpv4Repr, IpProtocol, Ipv4Packet, Ipv4Repr},
25};
26
27#[cfg(feature = "tokio")]
28use crate::CreateSocket;
29
30#[derive(Debug)]
32pub enum PingError {
33 Timeout,
35 Ipv6Unsupported,
37 Net(netcore::Error),
39}
40
41impl core::fmt::Display for PingError {
42 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
43 match self {
44 Self::Timeout => f.write_str("ping timed out"),
45 Self::Ipv6Unsupported => f.write_str("ICMPv6 ping is unsupported (IPv6 is off)"),
46 Self::Net(e) => write!(f, "netstack error: {e}"),
47 }
48 }
49}
50
51impl core::error::Error for PingError {
52 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
53 match self {
54 Self::Net(e) => Some(e),
55 _ => None,
56 }
57 }
58}
59
60impl From<netcore::Error> for PingError {
61 fn from(e: netcore::Error) -> Self {
62 Self::Net(e)
63 }
64}
65
66#[cfg(any(feature = "tokio", test))]
69const PING_PAYLOAD: &[u8] = b"ts_netstack_smoltcp ping";
70
71#[cfg(feature = "tokio")]
83static PING_IDENT_COUNTER: core::sync::atomic::AtomicU16 = core::sync::atomic::AtomicU16::new(0);
84
85#[cfg(feature = "tokio")]
88fn next_ident() -> u16 {
89 let counter = PING_IDENT_COUNTER.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
90 let seed = (std::process::id() as u16) & 0xFF00;
93 seed ^ counter
94}
95
96#[cfg(feature = "tokio")]
106pub async fn ping<C: CreateSocket + Sync>(
107 chan: &C,
108 src: Ipv4Addr,
109 dst: IpAddr,
110 timeout: Duration,
111) -> Result<Duration, PingError> {
112 let dst = match dst {
113 IpAddr::V4(v4) => v4,
114 IpAddr::V6(_) => return Err(PingError::Ipv6Unsupported),
115 };
116
117 let ident: u16 = next_ident();
121 let seq_no: u16 = 1;
122
123 let sock = chan.raw_open(true, IpProtocol::Icmp).await?;
124
125 let request = build_echo_request(src, dst, ident, seq_no, PING_PAYLOAD);
126
127 let start = tokio::time::Instant::now();
128 sock.send(&request).await?;
129
130 let deadline = start + timeout;
131
132 loop {
133 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
134 if remaining.is_zero() {
135 return Err(PingError::Timeout);
136 }
137
138 let recv = tokio::time::timeout(remaining, sock.recv_bytes()).await;
139 let bytes = match recv {
140 Err(_elapsed) => return Err(PingError::Timeout),
141 Ok(Ok(b)) => b,
142 Ok(Err(e)) => return Err(PingError::Net(e)),
143 };
144
145 if matches_reply(&bytes, src, dst, ident, seq_no) {
146 return Ok(start.elapsed());
147 }
148 }
150}
151
152#[cfg(any(feature = "tokio", test))]
158fn build_echo_request(
159 src: Ipv4Addr,
160 dst: Ipv4Addr,
161 ident: u16,
162 seq_no: u16,
163 payload: &[u8],
164) -> vec::Vec<u8> {
165 let checksum_caps = ChecksumCapabilities::default();
166
167 let icmp_repr = Icmpv4Repr::EchoRequest {
168 ident,
169 seq_no,
170 data: payload,
171 };
172
173 let ipv4_repr = Ipv4Repr {
174 src_addr: src,
175 dst_addr: dst,
176 next_header: IpProtocol::Icmp,
177 payload_len: icmp_repr.buffer_len(),
178 hop_limit: 64,
179 };
180
181 let total = IPV4_HEADER_LEN + icmp_repr.buffer_len();
182 let mut buf = vec![0u8; total];
183
184 {
185 let mut ip_packet = Ipv4Packet::new_unchecked(&mut buf[..]);
186 ipv4_repr.emit(&mut ip_packet, &checksum_caps);
187 }
188
189 {
190 let mut icmp_packet = Icmpv4Packet::new_unchecked(&mut buf[IPV4_HEADER_LEN..]);
191 icmp_repr.emit(&mut icmp_packet, &checksum_caps);
192 }
193
194 buf
195}
196
197#[cfg(any(feature = "tokio", test))]
199fn matches_reply(
200 bytes: &[u8],
201 expect_src: Ipv4Addr,
202 expect_dst: Ipv4Addr,
203 ident: u16,
204 seq_no: u16,
205) -> bool {
206 let checksum_caps = ChecksumCapabilities::default();
207
208 let Ok(ip_packet) = Ipv4Packet::new_checked(bytes) else {
209 return false;
210 };
211 let Ok(ipv4_repr) = Ipv4Repr::parse(&ip_packet, &checksum_caps) else {
212 return false;
213 };
214 if ipv4_repr.next_header != IpProtocol::Icmp {
215 return false;
216 }
217 if ipv4_repr.src_addr != expect_dst || ipv4_repr.dst_addr != expect_src {
219 return false;
220 }
221
222 let Ok(icmp_packet) = Icmpv4Packet::new_checked(ip_packet.payload()) else {
223 return false;
224 };
225 let Ok(icmp_repr) = Icmpv4Repr::parse(&icmp_packet, &checksum_caps) else {
226 return false;
227 };
228
229 matches!(
230 icmp_repr,
231 Icmpv4Repr::EchoReply { ident: i, seq_no: s, .. } if i == ident && s == seq_no
232 )
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 const SRC: Ipv4Addr = Ipv4Addr::new(100, 64, 0, 1);
240 const DST: Ipv4Addr = Ipv4Addr::new(100, 64, 0, 2);
241
242 fn build_echo_reply(from: Ipv4Addr, to: Ipv4Addr, ident: u16, seq_no: u16) -> vec::Vec<u8> {
245 let checksum_caps = ChecksumCapabilities::default();
246 let icmp_repr = Icmpv4Repr::EchoReply {
247 ident,
248 seq_no,
249 data: PING_PAYLOAD,
250 };
251 let ipv4_repr = Ipv4Repr {
252 src_addr: from,
253 dst_addr: to,
254 next_header: IpProtocol::Icmp,
255 payload_len: icmp_repr.buffer_len(),
256 hop_limit: 64,
257 };
258 let mut buf = vec![0u8; IPV4_HEADER_LEN + icmp_repr.buffer_len()];
259 {
260 let mut p = Ipv4Packet::new_unchecked(&mut buf[..]);
261 ipv4_repr.emit(&mut p, &checksum_caps);
262 }
263 {
264 let mut p = Icmpv4Packet::new_unchecked(&mut buf[IPV4_HEADER_LEN..]);
265 icmp_repr.emit(&mut p, &checksum_caps);
266 }
267 buf
268 }
269
270 #[test]
271 fn matches_reply_accepts_matching_ident_and_seq() {
272 let reply = build_echo_reply(DST, SRC, 0xABCD, 7);
273 assert!(matches_reply(&reply, SRC, DST, 0xABCD, 7));
274 }
275
276 #[test]
277 fn matches_reply_rejects_foreign_ident() {
278 let foreign = build_echo_reply(DST, SRC, 0x1111, 7);
281 assert!(!matches_reply(&foreign, SRC, DST, 0xABCD, 7));
282 }
283
284 #[test]
285 fn matches_reply_rejects_foreign_seq() {
286 let foreign = build_echo_reply(DST, SRC, 0xABCD, 99);
287 assert!(!matches_reply(&foreign, SRC, DST, 0xABCD, 7));
288 }
289
290 #[test]
291 fn matches_reply_rejects_non_echo_reply() {
292 let request = build_echo_request(DST, SRC, 0xABCD, 7, PING_PAYLOAD);
294 assert!(!matches_reply(&request, SRC, DST, 0xABCD, 7));
295 }
296
297 #[cfg(feature = "tokio")]
298 #[test]
299 fn next_ident_is_unique_for_concurrent_calls() {
300 let a = next_ident();
303 let b = next_ident();
304 let c = next_ident();
305 assert_ne!(a, b);
306 assert_ne!(b, c);
307 assert_ne!(a, c);
308 }
309}