Skip to content

Commit 8cc0745

Browse files
authored
Merge pull request #992 from GyulyVGC/freeze
Pause and resume packet captures
2 parents fe31e8f + 5d25164 commit 8cc0745

File tree

8 files changed

+144
-7
lines changed

8 files changed

+144
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ All Sniffnet releases with the relative changes are documented in this file.
44

55
## [UNRELEASED]
66
- Send remote notifications via webhook ([#991](https://github.com/GyulyVGC/sniffnet/pull/991) — fixes [#841](https://github.com/GyulyVGC/sniffnet/issues/841))
7+
- Pause and resume packet captures ([#992](https://github.com/GyulyVGC/sniffnet/pull/992) — fixes [#551](https://github.com/GyulyVGC/sniffnet/issues/551))
78
- Added Czech translation 🇨🇿 ([#960](https://github.com/GyulyVGC/sniffnet/pull/960))
89
- Improve update checks using `semver` ([#891](https://github.com/GyulyVGC/sniffnet/pull/891))
910
- Build the app also for Windows ARM64 (fixes [#988](https://github.com/GyulyVGC/sniffnet/issues/988))

resources/fonts/subset/icons.ttf

204 Bytes
Binary file not shown.

src/gui/components/header.rs

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::gui::types::message::Message;
1515
use crate::gui::types::settings::Settings;
1616
use crate::translations::translations::{quit_analysis_translation, settings_translation};
1717
use crate::translations::translations_3::thumbnail_mode_translation;
18+
use crate::translations::translations_4::{pause_translation, resume_translation};
1819
use crate::utils::types::icon::Icon;
1920
use crate::{Language, SNIFFNET_TITLECASE, StyleType};
2021

@@ -37,6 +38,7 @@ pub fn header(sniffer: &Sniffer) -> Container<'_, Message, StyleType> {
3738
language,
3839
color_gradient,
3940
unread_notifications,
41+
sniffer.frozen,
4042
);
4143
}
4244

@@ -60,10 +62,15 @@ pub fn header(sniffer: &Sniffer) -> Container<'_, Message, StyleType> {
6062
Container::new(Space::with_width(60))
6163
})
6264
.push(horizontal_space())
63-
.push(Container::new(Space::with_width(40)))
65+
.push(Container::new(Space::with_width(80)))
6466
.push(Space::with_width(20))
6567
.push(logo)
6668
.push(Space::with_width(20))
69+
.push(if is_running {
70+
Container::new(get_button_freeze(font, language, sniffer.frozen, false))
71+
} else {
72+
Container::new(Space::with_width(40))
73+
})
6774
.push(if is_running {
6875
Container::new(get_button_minimize(font, language, false))
6976
} else {
@@ -161,9 +168,56 @@ pub fn get_button_minimize<'a>(
161168
.class(ButtonType::Thumbnail)
162169
.on_press(Message::ToggleThumbnail(false));
163170

164-
Tooltip::new(content, Text::new(tooltip).font(font), Position::Right)
165-
.gap(0)
166-
.class(tooltip_style)
171+
Tooltip::new(
172+
content,
173+
Text::new(tooltip).font(font),
174+
Position::FollowCursor,
175+
)
176+
.gap(0)
177+
.class(tooltip_style)
178+
}
179+
180+
pub fn get_button_freeze<'a>(
181+
font: Font,
182+
language: Language,
183+
frozen: bool,
184+
thumbnail: bool,
185+
) -> Tooltip<'a, Message, StyleType> {
186+
let size = if thumbnail { 19 } else { 23 };
187+
let button_size = if thumbnail { 30 } else { 40 };
188+
let icon = if frozen { Icon::Resume } else { Icon::Pause };
189+
let tooltip = if thumbnail {
190+
""
191+
} else if frozen {
192+
resume_translation(language)
193+
} else {
194+
pause_translation(language)
195+
};
196+
let tooltip_style = if thumbnail {
197+
ContainerType::Standard
198+
} else {
199+
ContainerType::Tooltip
200+
};
201+
202+
let content = button(
203+
icon.to_text()
204+
.size(size)
205+
.align_x(Alignment::Center)
206+
.align_y(Alignment::Center),
207+
)
208+
.padding(0)
209+
.height(button_size)
210+
.width(button_size)
211+
.class(ButtonType::Thumbnail)
212+
.on_press(Message::Freeze);
213+
214+
Tooltip::new(
215+
content,
216+
Text::new(tooltip).font(font),
217+
Position::FollowCursor,
218+
)
219+
.gap(0)
220+
.class(tooltip_style)
167221
}
168222

169223
fn thumbnail_header<'a>(
@@ -172,14 +226,16 @@ fn thumbnail_header<'a>(
172226
language: Language,
173227
color_gradient: GradientType,
174228
unread_notifications: usize,
229+
frozen: bool,
175230
) -> Container<'a, Message, StyleType> {
176231
Container::new(
177232
Row::new()
178233
.align_y(Alignment::Center)
179234
.push(horizontal_space())
180-
.push(Space::with_width(80))
235+
.push(Space::with_width(110))
181236
.push(Text::new(SNIFFNET_TITLECASE).font(font_headers))
182237
.push(Space::with_width(10))
238+
.push(get_button_freeze(font, language, frozen, true))
183239
.push(get_button_minimize(font, language, true))
184240
.push(horizontal_space())
185241
.push(if unread_notifications > 0 {

src/gui/sniffer.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ pub struct Sniffer {
121121
pub id: Option<Id>,
122122
/// Host data for filter dropdowns (comboboxes)
123123
pub host_data_states: HostDataStates,
124+
/// Flag reporting whether the packet capture is frozen
125+
pub frozen: bool,
126+
/// Sender to freeze the packet capture
127+
freeze_tx: Option<tokio::sync::broadcast::Sender<()>>,
124128
}
125129

126130
impl Sniffer {
@@ -160,6 +164,8 @@ impl Sniffer {
160164
thumbnail: false,
161165
id: None,
162166
host_data_states: HostDataStates::default(),
167+
frozen: false,
168+
freeze_tx: None,
163169
}
164170
}
165171

@@ -175,6 +181,7 @@ impl Sniffer {
175181
}) => match key.as_ref() {
176182
Key::Character("q") => Some(Message::QuitWrapper),
177183
Key::Character("t") => Some(Message::CtrlTPressed),
184+
Key::Named(Named::Space) => Some(Message::CtrlSpacePressed),
178185
_ => None,
179186
},
180187
_ => None,
@@ -185,6 +192,7 @@ impl Sniffer {
185192
Modifiers::COMMAND => match key.as_ref() {
186193
Key::Character("q") => Some(Message::QuitWrapper),
187194
Key::Character("t") => Some(Message::CtrlTPressed),
195+
Key::Named(Named::Space) => Some(Message::CtrlSpacePressed),
188196
Key::Character(",") => Some(Message::OpenLastSettings),
189197
Key::Named(Named::Backspace) => Some(Message::ResetButtonPressed),
190198
Key::Character("d") => Some(Message::CtrlDPressed),
@@ -500,6 +508,14 @@ impl Sniffer {
500508
return Task::done(Message::ToggleThumbnail(false));
501509
}
502510
}
511+
Message::CtrlSpacePressed => {
512+
if self.running_page.is_some()
513+
&& self.settings_page.is_none()
514+
&& self.modal.is_none()
515+
{
516+
return Task::done(Message::Freeze);
517+
}
518+
}
503519
Message::ScaleFactorShortcut(increase) => {
504520
let scale_factor = self.conf.settings.scale_factor;
505521
if !(scale_factor > 2.99 && increase || scale_factor < 0.31 && !increase) {
@@ -555,6 +571,12 @@ impl Sniffer {
555571
.remote_notifications
556572
.set_url(&url);
557573
}
574+
Message::Freeze => {
575+
self.frozen = !self.frozen;
576+
if let Some(tx) = &self.freeze_tx {
577+
let _ = tx.send(());
578+
}
579+
}
558580
}
559581
Task::none()
560582
}
@@ -750,6 +772,9 @@ impl Sniffer {
750772
self.traffic_chart
751773
.change_capture_source(matches!(capture_source, CaptureSource::Device(_)));
752774
let (tx, rx) = async_channel::unbounded();
775+
let (freeze_tx, freeze_rx) = tokio::sync::broadcast::channel(1_048_575);
776+
let freeze_rx2 = freeze_tx.subscribe();
777+
let filters = self.conf.filters.clone();
753778
let _ = thread::Builder::new()
754779
.name("thread_parse_packets".to_string())
755780
.spawn(move || {
@@ -758,11 +783,14 @@ impl Sniffer {
758783
capture_source,
759784
&mmdb_readers,
760785
capture_context,
786+
filters,
761787
&tx,
788+
(freeze_rx, freeze_rx2),
762789
);
763790
})
764791
.log_err(location!());
765792
self.current_capture_rx.1 = Some(rx.clone());
793+
self.freeze_tx = Some(freeze_tx);
766794
return Task::run(rx, |backend_msg| match backend_msg {
767795
BackendTrafficMessage::TickRun(cap_id, msg, host_msg, no_more_packets) => {
768796
Message::TickRun(cap_id, msg, host_msg, no_more_packets)
@@ -800,6 +828,8 @@ impl Sniffer {
800828
self.page_number = 1;
801829
self.thumbnail = false;
802830
self.host_data_states = HostDataStates::default();
831+
self.frozen = false;
832+
self.freeze_tx = None;
803833
}
804834

805835
fn set_device(&mut self, name: &str) {

src/gui/types/message.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ pub enum Message {
121121
Drag,
122122
/// Ctrl+T keys have been pressed
123123
CtrlTPressed,
124+
/// Ctrl+Space keys have been pressed
125+
CtrlSpacePressed,
124126
/// Edit scale factor via keyboard shortcut
125127
ScaleFactorShortcut(bool),
126128
/// Set new release status
@@ -139,4 +141,6 @@ pub enum Message {
139141
ToggleRemoteNotifications,
140142
/// The remote notifications URL has been updated
141143
RemoteNotificationsUrl(String),
144+
/// Pause or resume live capture
145+
Freeze,
142146
}

src/networking/parse_packets.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! Module containing functions executed by the thread in charge of parsing sniffed packets
22
3+
use crate::gui::types::filters::Filters;
34
use crate::location;
45
use crate::mmdb::asn::get_asn;
56
use crate::mmdb::country::get_country;
@@ -32,15 +33,20 @@ use std::net::IpAddr;
3233
use std::sync::{Arc, Mutex};
3334
use std::thread;
3435
use std::time::{Duration, Instant};
36+
use tokio::sync::broadcast::Receiver;
3537

3638
/// The calling thread enters a loop in which it waits for network packets
3739
pub fn parse_packets(
3840
cap_id: usize,
3941
mut cs: CaptureSource,
4042
mmdb_readers: &MmdbReaders,
4143
capture_context: CaptureContext,
44+
filters: Filters,
4245
tx: &Sender<BackendTrafficMessage>,
46+
freeze_rxs: (Receiver<()>, Receiver<()>),
4347
) {
48+
let (mut freeze_rx, mut freeze_rx_2) = freeze_rxs;
49+
4450
let my_link_type = capture_context.my_link_type();
4551
let (cap, mut savefile) = capture_context.consume();
4652

@@ -55,10 +61,18 @@ pub fn parse_packets(
5561
let (pcap_tx, pcap_rx) = std::sync::mpsc::sync_channel(10_000);
5662
let _ = thread::Builder::new()
5763
.name("thread_packet_stream".to_string())
58-
.spawn(move || packet_stream(cap, &pcap_tx))
64+
.spawn(move || packet_stream(cap, &pcap_tx, &mut freeze_rx_2, &filters))
5965
.log_err(location!());
6066

6167
loop {
68+
// check if we need to freeze the parsing
69+
if freeze_rx.try_recv().is_ok() {
70+
// wait until unfreeze
71+
let _ = freeze_rx.blocking_recv();
72+
// reset the first packet ticks
73+
first_packet_ticks = Some(Instant::now());
74+
}
75+
6276
let (packet_res, cap_stats) = pcap_rx
6377
.recv_timeout(Duration::from_millis(150))
6478
.unwrap_or((Err(pcap::Error::TimeoutExpired), None));
@@ -486,8 +500,20 @@ fn maybe_send_tick_run_offline(
486500
fn packet_stream(
487501
mut cap: CaptureType,
488502
tx: &std::sync::mpsc::SyncSender<(Result<PacketOwned, pcap::Error>, Option<pcap::Stat>)>,
503+
freeze_rx: &mut Receiver<()>,
504+
filters: &Filters,
489505
) {
490506
loop {
507+
// check if we need to freeze the parsing
508+
if freeze_rx.try_recv().is_ok() {
509+
// pause the capture
510+
cap.pause();
511+
// wait until unfreeze
512+
let _ = freeze_rx.blocking_recv();
513+
// resume the capture
514+
cap.resume(filters);
515+
}
516+
491517
let packet_res = cap.next_packet();
492518
let packet_owned = packet_res.map(|p| PacketOwned {
493519
header: *p.header,

src/networking/types/capture_context.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ impl CaptureType {
128128
CaptureSource::Device(device) => {
129129
let inactive = Capture::from_device(device.to_pcap_device())?;
130130
let cap = inactive
131-
.promisc(true)
131+
.promisc(false)
132132
.buffer_size(2_000_000) // 2MB buffer -> 10k packets of 200 bytes
133133
.snaplen(if pcap_out_path.is_some() {
134134
i32::from(u16::MAX)
@@ -150,6 +150,22 @@ impl CaptureType {
150150
Self::Offline(cap) => cap.filter(bpf, true),
151151
}
152152
}
153+
154+
pub fn pause(&mut self) {
155+
if let Self::Live(cap) = self {
156+
let _ = cap.filter("less 2", true).log_err(location!());
157+
}
158+
}
159+
160+
pub fn resume(&mut self, filters: &Filters) {
161+
if let Self::Live(cap) = self {
162+
if filters.is_some_filter_active() {
163+
let _ = cap.filter(filters.bpf(), true).log_err(location!());
164+
} else if cap.filter("", true).log_err(location!()).is_err() {
165+
let _ = cap.filter("greater 0", true).log_err(location!());
166+
}
167+
}
168+
}
153169
}
154170

155171
#[derive(Clone)]

src/utils/types/icon.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ pub enum Icon {
5252
Update,
5353
Warning,
5454
Waves,
55+
Pause,
56+
Resume,
5557
}
5658

5759
impl Icon {
@@ -105,6 +107,8 @@ impl Icon {
105107
Icon::Update => '<',
106108
// Icon::Expand => 'p',
107109
// Icon::Collapse => 'q',
110+
Icon::Pause => '-',
111+
Icon::Resume => '+',
108112
}
109113
}
110114

0 commit comments

Comments
 (0)