Skip to content

Commit 3c425ec

Browse files
committed
feat: add TCP connection latency (RTT) measurement
Measure Round-Trip Time from TCP SYN/SYN-ACK handshake timestamps. Display latency in milliseconds on the connection details page. Closes #845
1 parent 7f139ec commit 3c425ec

5 files changed

Lines changed: 156 additions & 7 deletions

File tree

src/gui/pages/connection_details_page.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ use crate::translations::translations_2::{
3434
use crate::translations::translations_3::{
3535
copy_translation, messages_translation, service_translation,
3636
};
37-
use crate::translations::translations_5::program_translation;
37+
use crate::translations::translations_5::{latency_translation, program_translation};
3838
use crate::utils::formatted_strings::{get_formatted_timestamp, get_socket_address};
3939
use crate::utils::types::icon::Icon;
4040
use crate::{Language, Protocol, Sniffer, StyleType};
@@ -201,6 +201,13 @@ fn col_info<'a>(
201201
));
202202
}
203203

204+
if let Some(lat) = val.latency {
205+
ret_val = ret_val.push(TextType::highlighted_subtitle_with_desc(
206+
latency_translation(language),
207+
&format!("{:.1} ms", lat.as_secs_f64() * 1000.0),
208+
));
209+
}
210+
204211
ret_val = ret_val.push(TextType::highlighted_subtitle_with_desc(
205212
&format!(
206213
"{} ({})",

src/networking/manage_packets.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use crate::networking::types::service_query::ServiceQuery;
2121
use crate::networking::types::traffic_direction::TrafficDirection;
2222
use crate::networking::types::traffic_type::TrafficType;
2323
use std::fmt::Write;
24-
use std::time::Instant;
24+
use std::time::{Duration, Instant};
2525

2626
include!(concat!(env!("OUT_DIR"), "/services.rs"));
2727

@@ -266,6 +266,7 @@ pub fn modify_or_insert_in_map(
266266
arp_type: ArpType,
267267
exchanged_bytes: u128,
268268
ip_blacklist: &IpBlacklist,
269+
latency: Option<Duration>,
269270
) -> (TrafficDirection, Service) {
270271
let mut traffic_direction = TrafficDirection::default();
271272
let mut service = Service::Unknown;
@@ -313,6 +314,9 @@ pub fn modify_or_insert_in_map(
313314
.and_modify(|n| *n += 1)
314315
.or_insert(1);
315316
}
317+
if latency.is_some() {
318+
info.latency = latency;
319+
}
316320
})
317321
.or_insert_with(|| InfoAddressPortPair {
318322
mac_address1: mac_addresses.0,
@@ -336,6 +340,7 @@ pub fn modify_or_insert_in_map(
336340
},
337341
is_blacklisted,
338342
program: Program::NotApplicable,
343+
latency,
339344
});
340345

341346
(new_info.traffic_direction, new_info.service)

src/networking/parse_packets.rs

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use crate::utils::formatted_strings::get_domain_from_r_dns;
2626
use crate::utils::types::timestamp::Timestamp;
2727
use async_channel::Sender;
2828
use dns_lookup::lookup_addr;
29-
use etherparse::{EtherType, LaxPacketHeaders};
29+
use etherparse::{EtherType, LaxPacketHeaders, TransportHeader};
3030
use pcap::{Address, Packet, PacketHeader};
3131
use std::collections::HashMap;
3232
use std::net::IpAddr;
@@ -59,6 +59,8 @@ pub fn parse_packets(
5959

6060
let mut info_traffic_msg = InfoTraffic::default();
6161

62+
let mut pending_syns: HashMap<(IpAddr, u16, IpAddr, u16), Timestamp> = HashMap::new();
63+
6264
let (lookup_request_tx, lookup_request_rx) = std::sync::mpsc::channel();
6365
let (lookup_result_tx, lookup_result_rx) = std::sync::mpsc::channel();
6466
let mut resolutions_state = AddressesResolutionState::new(lookup_request_tx, lookup_result_rx);
@@ -130,6 +132,7 @@ pub fn parse_packets(
130132
}
131133
Ok(packet) => {
132134
if let Some(headers) = get_sniffable_headers(&packet.data, my_link_type) {
135+
let headers_clone = headers.clone();
133136
#[allow(clippy::useless_conversion)]
134137
let secs = i64::from(packet.header.ts.tv_sec);
135138
#[allow(clippy::useless_conversion)]
@@ -167,14 +170,32 @@ pub fn parse_packets(
167170
continue;
168171
};
169172

170-
// save this packet to PCAP file
173+
let mut latency = None;
174+
if key.protocol == crate::Protocol::TCP {
175+
if let (Some(sport), Some(dport)) = (key.sport, key.dport) {
176+
if let Some(TransportHeader::Tcp(tcp)) = headers_clone.transport {
177+
if tcp.syn && !tcp.ack {
178+
pending_syns.insert(
179+
(key.source, sport, key.dest, dport),
180+
next_packet_timestamp,
181+
);
182+
} else if tcp.syn && tcp.ack {
183+
let syn_key = (key.dest, dport, key.source, sport);
184+
if let Some(syn_ts) = pending_syns.get(&syn_key).copied() {
185+
latency = compute_rtt(syn_ts, next_packet_timestamp);
186+
pending_syns.remove(&syn_key);
187+
}
188+
}
189+
}
190+
}
191+
}
192+
171193
if let Some(file) = savefile.as_mut() {
172194
file.write(&Packet {
173195
header: &packet.header,
174196
data: &packet.data,
175197
});
176198
}
177-
// update the map
178199
let (traffic_direction, service) = modify_or_insert_in_map(
179200
&mut info_traffic_msg,
180201
&key,
@@ -184,6 +205,7 @@ pub fn parse_packets(
184205
arp_type,
185206
exchanged_bytes,
186207
ip_blacklist,
208+
latency,
187209
);
188210

189211
info_traffic_msg
@@ -553,3 +575,68 @@ struct PacketOwned {
553575
header: PacketHeader,
554576
data: Box<[u8]>,
555577
}
578+
579+
fn compute_rtt(syn_ts: Timestamp, synack_ts: Timestamp) -> Option<Duration> {
580+
let syn_us = syn_ts.to_usecs()?;
581+
let ack_us = synack_ts.to_usecs()?;
582+
let diff = ack_us - syn_us;
583+
if diff >= 0 {
584+
Some(Duration::from_micros(diff as u64))
585+
} else {
586+
None
587+
}
588+
}
589+
590+
#[cfg(test)]
591+
mod tests {
592+
use super::*;
593+
594+
#[test]
595+
fn test_compute_rtt_basic() {
596+
let syn = Timestamp::new(100, 0);
597+
let synack = Timestamp::new(100, 5000);
598+
let rtt = compute_rtt(syn, synack);
599+
assert_eq!(rtt, Some(Duration::from_micros(5000)));
600+
}
601+
602+
#[test]
603+
fn test_compute_rtt_crossing_second() {
604+
let syn = Timestamp::new(100, 999999);
605+
let synack = Timestamp::new(101, 1);
606+
let rtt = compute_rtt(syn, synack);
607+
assert_eq!(rtt, Some(Duration::from_micros(2)));
608+
}
609+
610+
#[test]
611+
fn test_compute_rtt_large_gap() {
612+
let syn = Timestamp::new(100, 0);
613+
let synack = Timestamp::new(105, 500000);
614+
let rtt = compute_rtt(syn, synack);
615+
assert_eq!(rtt, Some(Duration::from_millis(5500)));
616+
}
617+
618+
#[test]
619+
fn test_compute_rtt_zero() {
620+
let syn = Timestamp::new(100, 500);
621+
let synack = Timestamp::new(100, 500);
622+
let rtt = compute_rtt(syn, synack);
623+
assert_eq!(rtt, Some(Duration::from_micros(0)));
624+
}
625+
626+
#[test]
627+
fn test_compute_rtt_negative_returns_none() {
628+
let syn = Timestamp::new(101, 0);
629+
let synack = Timestamp::new(100, 0);
630+
let rtt = compute_rtt(syn, synack);
631+
assert_eq!(rtt, None);
632+
}
633+
634+
#[test]
635+
fn test_compute_rtt_display_ms() {
636+
let syn = Timestamp::new(0, 0);
637+
let synack = Timestamp::new(0, 25000);
638+
let rtt = compute_rtt(syn, synack).unwrap();
639+
let display = format!("{:.1} ms", rtt.as_secs_f64() * 1000.0);
640+
assert_eq!(display, "25.0 ms");
641+
}
642+
}

src/networking/types/info_address_port_pair.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::report::types::sort_type::SortType;
1212
use crate::utils::types::timestamp::Timestamp;
1313
use std::cmp::Ordering;
1414
use std::collections::HashMap;
15-
use std::time::Instant;
15+
use std::time::{Duration, Instant};
1616

1717
/// Struct useful to format the output report file and to keep track of statistics about the sniffed traffic.
1818
///
@@ -43,8 +43,8 @@ pub struct InfoAddressPortPair {
4343
pub arp_types: HashMap<ArpType, usize>,
4444
/// Whether the remote address is blacklisted
4545
pub is_blacklisted: bool,
46-
/// The program associated to this pair
4746
pub program: Program,
47+
pub latency: Option<Duration>,
4848
}
4949

5050
impl InfoAddressPortPair {
@@ -57,6 +57,9 @@ impl InfoAddressPortPair {
5757
self.service = other.service;
5858
self.is_blacklisted = other.is_blacklisted;
5959
self.traffic_direction = other.traffic_direction;
60+
if other.latency.is_some() {
61+
self.latency = other.latency;
62+
}
6063
for (icmp_type, count) in &other.icmp_types {
6164
self.icmp_types
6265
.entry(*icmp_type)
@@ -119,6 +122,7 @@ impl Default for InfoAddressPortPair {
119122
arp_types: HashMap::new(),
120123
is_blacklisted: false,
121124
program: Program::default(),
125+
latency: None,
122126
}
123127
}
124128
}
@@ -191,4 +195,29 @@ mod tests {
191195
Ordering::Greater
192196
);
193197
}
198+
199+
#[test]
200+
fn test_latency_default_is_none() {
201+
let pair = InfoAddressPortPair::default();
202+
assert!(pair.latency.is_none());
203+
}
204+
205+
#[test]
206+
fn test_latency_refresh_overwrites_when_some() {
207+
let mut base = InfoAddressPortPair::default();
208+
base.latency = Some(Duration::from_millis(50));
209+
let mut new = InfoAddressPortPair::default();
210+
new.latency = Some(Duration::from_millis(20));
211+
base.refresh(&new);
212+
assert_eq!(base.latency, Some(Duration::from_millis(20)));
213+
}
214+
215+
#[test]
216+
fn test_latency_refresh_preserves_when_other_none() {
217+
let mut base = InfoAddressPortPair::default();
218+
base.latency = Some(Duration::from_millis(50));
219+
let new = InfoAddressPortPair::default();
220+
base.refresh(&new);
221+
assert_eq!(base.latency, Some(Duration::from_millis(50)));
222+
}
194223
}

src/translations/translations_5.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,24 @@ pub fn no_favorites_saved_translation(language: Language) -> &'static str {
178178
_ => "No favorites saved yet",
179179
}
180180
}
181+
182+
pub fn latency_translation(language: Language) -> &'static str {
183+
match language {
184+
Language::EN => "Latency",
185+
Language::IT => "Latenza",
186+
Language::DE => "Latenz",
187+
Language::ZH => "延迟",
188+
Language::ZH_TW => "延遲",
189+
Language::TR => "Gecikme",
190+
Language::JA => "レイテンシ",
191+
Language::ES => "Latencia",
192+
Language::RO => "Latență",
193+
Language::ID => "Latensi",
194+
Language::FR => "Latence",
195+
Language::UK => "Затримка",
196+
Language::SV => "Latens",
197+
Language::EL => "Α latencia",
198+
Language::HU => "Késleltetés",
199+
_ => "Latency",
200+
}
201+
}

0 commit comments

Comments
 (0)