Skip to content

Commit 841deb9

Browse files
authored
Merge pull request #1 from 0x6b/specify-timeout
feat: specify ping timeout
2 parents 740da00 + 2541433 commit 841deb9

File tree

7 files changed

+115
-49
lines changed

7 files changed

+115
-49
lines changed

Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rgtping"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
edition = "2021"
55
description = "ping(8) equivalent for GTPv1-U (3GPP TS 29.281)."
66
authors = ["kaoru <[email protected]>"]

README.md

+29-19
Original file line numberDiff line numberDiff line change
@@ -8,40 +8,50 @@ ping(8) equivalent for GTPv1-U (3GPP TS 29.281).
88
## Features
99

1010
- Send GTPv1-U Echo request to multiple endpoints simultaneously.
11-
- Output is an array of JSON objects, one for each endpoint e.g.
11+
- Output is an array of JSON objects, one for each endpoint e.g.
1212
```json5
1313
[
1414
{
15-
"target": "192.168.205.10:2152",
16-
"sent": 10, // Number of packets sent
17-
"received": 10, // Number of packets received
18-
"packet_loss_percentage": 0.0, // Packet loss percentage
19-
"duplicate_packets": 0, // Number of duplicate packets
20-
"refused_packets": 0, // Number of refused packets (maybe incorrect)
21-
"duration": 10164.467290999999, // Total duration in milliseconds
22-
"min": 0.0, // Minimum RTT in milliseconds
23-
"max": 53.558417, // Maximum RTT in milliseconds
24-
"avg": 13.0501917, // Average RTT in milliseconds
25-
"mdev": 14.24073989248037, // Standard deviation of RTT in milliseconds
26-
"epoch_ms": 1726323245070 // epoch time of the start of the operation, in milliseconds
15+
"target": "192.168.205.10:2152", /// Gtping target IP address
16+
"epoch_ms": 1726372276714, /// Epoch time of the start of the command, in milliseconds
17+
"duration": 3026.165084, /// Total duration in milliseconds
18+
"sent": 3, /// Number of sent packets (including lost)
19+
"received": 3, /// Number of received packets
20+
"packet_loss_percentage": 0.0, /// Packet loss percentage
21+
"duplicate_packets": 0, /// Number of duplicate packets
22+
"refused_packets": 0, /// Number of refused packets
23+
"min": 0.0, /// Minimum RTT in milliseconds
24+
"max": 14.750917000000001, /// Maximum RTT in milliseconds
25+
"avg": 5.607903, /// Average RTT in milliseconds
26+
"mdev": 6.4650903803150355 /// Mean deviation of RTT in milliseconds
2727
}
28+
// ...
2829
]
2930
```
3031

3132
## Usage
3233

3334
```console
34-
$ rgtping --help
35+
ping(8) equivalent for GTPv1-U (3GPP TS 29.281).
36+
3537
Usage: rgtping [OPTIONS] [TARGET_IPS]...
3638
3739
Arguments:
38-
[TARGET_IPS]... Array of IP address and port number (IP:port) to ping [default: 192.168.205.10:2152]
40+
[TARGET_IPS]... Array of IP address and port number (IP:port) to ping, delimited
41+
by a space
3942
4043
Options:
41-
-c, --count <COUNT> Number of pings to send [default: 5]
42-
-i, --interval-ms <INTERVAL_MS> Interval between pings in milliseconds [default: 1000]
43-
-h, --help Print help
44-
-V, --version Print version
44+
-c, --count <COUNT>
45+
Number of pings to send [default: 5]
46+
-i, --interval-ms <INTERVAL_MS>
47+
Interval between pings in milliseconds [default: 1000]
48+
-W, --timeout-ms <TIMEOUT_MS>
49+
Time to wait for a response, in milliseconds. 0 means wait indefinitely
50+
[default: 10000]
51+
-h, --help
52+
Print help
53+
-V, --version
54+
Print version
4555
```
4656

4757
## Acknowledgement

src/args.rs

+9-2
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,29 @@ use std::net::SocketAddr;
22

33
use clap::Parser;
44

5+
/// Command line arguments
56
#[derive(Debug, Parser)]
67
#[clap(author, version, about)]
78
pub struct Args {
89
/// Array of IP address and port number (IP:port) to ping, delimited by a space
9-
#[arg(default_value = "192.168.205.10:2152", value_delimiter = ' ')]
10+
#[arg(value_delimiter = ' ')]
1011
pub target_ips: Vec<SocketAddr>,
1112
/// Number of pings to send
1213
#[arg(short, long, default_value = "5")]
1314
pub count: u64,
1415
/// Interval between pings in milliseconds
1516
#[arg(short, long, default_value = "1000")]
1617
pub interval_ms: u64,
18+
/// Time to wait for a response, in milliseconds. 0 means wait indefinitely.
19+
#[arg(short = 'W', long, default_value = "10000")]
20+
pub timeout_ms: u64,
1721
}
1822

1923
impl Args {
20-
pub fn new() -> Self {
24+
/// Parse command line arguments and return the result. It's just a wrapper around
25+
/// `clap::Parser::parse()` so that we can call it from the `main()` function without importing
26+
/// `clap` at the top level.
27+
pub fn from_cli() -> Self {
2128
Args::parse()
2229
}
2330
}

src/main.rs

+11-4
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,30 @@
11
mod args;
22

3-
use anyhow::Error;
3+
use anyhow::{Error, Result};
44
use args::Args;
55
use env_logger::Env;
66
use rgtping::{Pinger, Stats};
77
use tokio::spawn;
88

99
#[tokio::main]
10-
async fn main() -> anyhow::Result<()> {
10+
async fn main() -> Result<()> {
1111
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
12-
let args = Args::new();
12+
let args = Args::from_cli();
1313

14+
// Holds the pinger instances
1415
let mut pingers = Vec::with_capacity(args.target_ips.len());
16+
// Holds the handles to the spawned threads which actually send the pings
1517
let mut handles = Vec::with_capacity(args.target_ips.len());
18+
// Holds the results of the pings from each handle
1619
let mut results = Vec::with_capacity(args.target_ips.len());
1720

21+
// Create a pinger for each target IP
1822
for target in args.target_ips {
19-
let pinger = Pinger::new(target, args.count, args.interval_ms).await?;
23+
let pinger = Pinger::new(target, args.count, args.interval_ms, args.timeout_ms).await?;
2024
pingers.push(pinger);
2125
}
2226

27+
// Spawn a thread for each pinger to send the pings
2328
for mut pinger in pingers {
2429
let handle = spawn(async move {
2530
pinger.ping().await?;
@@ -28,10 +33,12 @@ async fn main() -> anyhow::Result<()> {
2833
handles.push(handle);
2934
}
3035

36+
// Wait for all the threads to finish and collect the results
3137
for handle in handles {
3238
results.push(handle.await??);
3339
}
3440

41+
// Print the results in JSON format
3542
println!("{}", serde_json::to_string_pretty(&results)?);
3643

3744
Ok(())

src/pinger.rs

+48-20
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::{
2+
io,
23
net::SocketAddr,
34
ops::Add,
45
time::{Duration, SystemTime},
@@ -16,6 +17,8 @@ use crate::Stats;
1617

1718
const TRACK_PINGS_SIZE: usize = 1024;
1819

20+
/// Pinger struct to send and receive GTPv1-U packets. All fields are private, so you can only
21+
/// create a new instance using the `new` method.
1922
#[derive(Debug)]
2023
pub struct Pinger {
2124
// UDP socket to send and receive GTPv1-U packets
@@ -30,34 +33,50 @@ pub struct Pinger {
3033
send_buf: Vec<u8>,
3134
// Internal buffer to store received data
3235
recv_buf: [u8; 1024],
33-
// number of sent packets (including lost)
36+
// Number of sent packets (including lost)
3437
sent: u64,
35-
// number of received packets
38+
// Number of received packets
3639
received: u64,
37-
// track times needed to send packets for statistics
40+
// Array to track times needed to send packets for statistics
3841
send_times: [f64; TRACK_PINGS_SIZE],
39-
// track received packets to detect duplicates
42+
// Array to track received packets to detect duplicates
4043
received_packets: [u64; TRACK_PINGS_SIZE],
41-
// number of duplicate packets
44+
// Number of duplicate packets
4245
duplicate_packets: u64,
43-
// number of refused packets
46+
// Number of refused packets
4447
refused_packets: u64,
45-
// epoch time of the start of the operation, in milliseconds
48+
// Epoch time of the start of the operation, in milliseconds
4649
epoch_ms: u128,
47-
// start time of the command for statistics
50+
// Start time of the command for statistics
4851
start_time: Instant,
49-
// last ping time to calculate RTT
52+
// Last ping time to calculate RTT
5053
last_ping_time: Instant,
51-
// last receive time to calculate RTT
54+
// Last receive time to calculate RTT
5255
last_receive_time: Instant,
53-
// interval between pings in milliseconds
54-
interval_ms: Duration,
55-
// number of pings to send
56+
// Interval between pings in milliseconds
57+
interval: Duration,
58+
// Time to wait for a response in milliseconds
59+
timeout: Duration,
60+
// Number of pings to send
5661
count: u64,
5762
}
5863

5964
impl Pinger {
60-
pub async fn new(peer: SocketAddr, count: u64, interval_ms: u64) -> Result<Self> {
65+
/// Create a new Pinger instance.
66+
///
67+
/// # Arguments
68+
///
69+
/// - `peer`: [`SocketAddr`] of the peer to ping
70+
/// - `count`: Number of pings to send
71+
/// - `interval_ms`: Interval between pings in milliseconds
72+
/// - `timeout_ms`: Time to wait for a response, in milliseconds. 0 means wait (almost)
73+
/// indefinitely.
74+
pub async fn new(
75+
peer: SocketAddr,
76+
count: u64,
77+
interval_ms: u64,
78+
timeout_ms: u64,
79+
) -> Result<Self> {
6180
let mut packet = Gtpv1Header {
6281
msgtype: ECHO_REQUEST,
6382
sequence_number: Some(0),
@@ -73,8 +92,8 @@ impl Pinger {
7392
let pinger = Pinger {
7493
socket: UdpSocket::bind("0.0.0.0:0").await?,
7594
peer,
76-
seq: 0,
7795
packet,
96+
seq: 0,
7897
send_buf: Vec::with_capacity(header_size as usize),
7998
recv_buf: [0; 1024],
8099
sent: 0,
@@ -87,13 +106,17 @@ impl Pinger {
87106
start_time: now,
88107
last_ping_time: now,
89108
last_receive_time: now,
90-
interval_ms: Duration::from_millis(interval_ms),
109+
interval: Duration::from_millis(interval_ms),
110+
timeout: Duration::from_millis(if timeout_ms == 0 { u64::MAX } else { timeout_ms }),
91111
count,
92112
};
93113
debug!("Pinger created for {}", pinger.peer);
94114
Ok(pinger)
95115
}
96116

117+
/// Send `count` pings to the peer and wait for a response. This method will send a ping, wait
118+
/// for a response, and then sleep for `interval` milliseconds. If a response is not received
119+
/// within `timeout` milliseconds, the method will continue to the next ping.
97120
pub async fn ping(&mut self) -> Result<()> {
98121
debug!("Start pinging for {}", self.peer);
99122
for _ in 0..self.count {
@@ -114,7 +137,7 @@ impl Pinger {
114137
);
115138
}
116139
Err(e) => {
117-
if e.kind() == std::io::ErrorKind::ConnectionRefused {
140+
if e.kind() == io::ErrorKind::ConnectionRefused {
118141
error!("Connection refused");
119142
}
120143
self.refused_packets += 1;
@@ -127,8 +150,12 @@ impl Pinger {
127150
self.last_ping_time = Instant::now();
128151

129152
trace!("Waiting for response");
153+
// To implement the timeout, we use tokio::select! macro. This macro allows us to
154+
// wait for multiple futures to complete, and then execute the branch that completes
155+
// first. In this case, we wait for the timeout to expire or for a response to be
156+
// received.
130157
tokio::select! {
131-
_ = async { sleep_until(Instant::now().add(self.interval_ms)).await } => {
158+
_ = async { sleep_until(Instant::now().add(self.timeout)).await } => {
132159
debug!("Timed out");
133160
}
134161
result = self.socket.recv_from(&mut self.recv_buf) => {
@@ -174,12 +201,12 @@ impl Pinger {
174201
}
175202
self.received += 1;
176203

177-
sleep(self.interval_ms).await;
204+
sleep(self.interval).await;
178205
},
179206
Err(_) => {
180207
error!("Port closed");
181208
self.seq += 1;
182-
sleep(self.interval_ms).await;
209+
sleep(self.interval).await;
183210
continue;
184211
}
185212
}
@@ -201,6 +228,7 @@ impl Pinger {
201228
Ok(())
202229
}
203230

231+
/// Calculate statistics from the pings sent and received.
204232
pub fn calculate_stats(&self) -> Stats {
205233
let mut min = 0f64;
206234
let mut max = 0f64;

src/stats.rs

+16-2
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,37 @@ use std::fmt::Display;
22

33
use serde::Serialize;
44

5+
/// Holds results from a series of GTP ping commands
56
#[derive(Debug, Serialize)]
67
pub struct Stats {
8+
/// Gtping target IP address
79
pub target: String,
10+
/// Epoch time of the start of the command, in milliseconds
11+
pub epoch_ms: u128,
12+
/// Total duration in milliseconds
13+
pub duration: f64,
14+
/// Number of sent packets (including lost)
815
pub sent: u64,
16+
/// Number of received packets
917
pub received: u64,
18+
/// Packet loss percentage
1019
pub packet_loss_percentage: f64,
20+
/// Number of duplicate packets
1121
pub duplicate_packets: u64,
22+
/// Number of refused packets
1223
pub refused_packets: u64,
13-
pub duration: f64,
24+
/// Minimum RTT in milliseconds
1425
pub min: f64,
26+
/// Maximum RTT in milliseconds
1527
pub max: f64,
28+
/// Average RTT in milliseconds
1629
pub avg: f64,
30+
/// Mean deviation of RTT in milliseconds
1731
pub mdev: f64,
18-
pub epoch_ms: u128,
1932
}
2033

2134
impl Display for Stats {
35+
/// Formats the statistics in a human-readable format, as close as possible to ping
2236
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2337
let mut s = String::new();
2438
s.push('\n');

0 commit comments

Comments
 (0)