diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b16a4e79..6f4806110 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All Sniffnet releases with the relative changes are documented in this file. ## [UNRELEASED] - Send remote notifications via webhook ([#991](https://github.com/GyulyVGC/sniffnet/pull/991) — fixes [#841](https://github.com/GyulyVGC/sniffnet/issues/841)) +- Pause and resume packet captures ([#992](https://github.com/GyulyVGC/sniffnet/pull/992) — fixes [#551](https://github.com/GyulyVGC/sniffnet/issues/551)) - Added Czech translation 🇨🇿 ([#960](https://github.com/GyulyVGC/sniffnet/pull/960)) - Improve update checks using `semver` ([#891](https://github.com/GyulyVGC/sniffnet/pull/891)) - Build the app also for Windows ARM64 (fixes [#988](https://github.com/GyulyVGC/sniffnet/issues/988)) diff --git a/resources/fonts/subset/icons.ttf b/resources/fonts/subset/icons.ttf index a77400c68..f577e3c15 100644 Binary files a/resources/fonts/subset/icons.ttf and b/resources/fonts/subset/icons.ttf differ diff --git a/src/gui/components/header.rs b/src/gui/components/header.rs index 5f0b980a6..85ca05649 100644 --- a/src/gui/components/header.rs +++ b/src/gui/components/header.rs @@ -15,6 +15,7 @@ use crate::gui::types::message::Message; use crate::gui::types::settings::Settings; use crate::translations::translations::{quit_analysis_translation, settings_translation}; use crate::translations::translations_3::thumbnail_mode_translation; +use crate::translations::translations_4::{pause_translation, resume_translation}; use crate::utils::types::icon::Icon; use crate::{Language, SNIFFNET_TITLECASE, StyleType}; @@ -37,6 +38,7 @@ pub fn header(sniffer: &Sniffer) -> Container<'_, Message, StyleType> { language, color_gradient, unread_notifications, + sniffer.frozen, ); } @@ -60,10 +62,15 @@ pub fn header(sniffer: &Sniffer) -> Container<'_, Message, StyleType> { Container::new(Space::with_width(60)) }) .push(horizontal_space()) - .push(Container::new(Space::with_width(40))) + .push(Container::new(Space::with_width(80))) .push(Space::with_width(20)) .push(logo) .push(Space::with_width(20)) + .push(if is_running { + Container::new(get_button_freeze(font, language, sniffer.frozen, false)) + } else { + Container::new(Space::with_width(40)) + }) .push(if is_running { Container::new(get_button_minimize(font, language, false)) } else { @@ -161,9 +168,56 @@ pub fn get_button_minimize<'a>( .class(ButtonType::Thumbnail) .on_press(Message::ToggleThumbnail(false)); - Tooltip::new(content, Text::new(tooltip).font(font), Position::Right) - .gap(0) - .class(tooltip_style) + Tooltip::new( + content, + Text::new(tooltip).font(font), + Position::FollowCursor, + ) + .gap(0) + .class(tooltip_style) +} + +pub fn get_button_freeze<'a>( + font: Font, + language: Language, + frozen: bool, + thumbnail: bool, +) -> Tooltip<'a, Message, StyleType> { + let size = if thumbnail { 19 } else { 23 }; + let button_size = if thumbnail { 30 } else { 40 }; + let icon = if frozen { Icon::Resume } else { Icon::Pause }; + let tooltip = if thumbnail { + "" + } else if frozen { + resume_translation(language) + } else { + pause_translation(language) + }; + let tooltip_style = if thumbnail { + ContainerType::Standard + } else { + ContainerType::Tooltip + }; + + let content = button( + icon.to_text() + .size(size) + .align_x(Alignment::Center) + .align_y(Alignment::Center), + ) + .padding(0) + .height(button_size) + .width(button_size) + .class(ButtonType::Thumbnail) + .on_press(Message::Freeze); + + Tooltip::new( + content, + Text::new(tooltip).font(font), + Position::FollowCursor, + ) + .gap(0) + .class(tooltip_style) } fn thumbnail_header<'a>( @@ -172,14 +226,16 @@ fn thumbnail_header<'a>( language: Language, color_gradient: GradientType, unread_notifications: usize, + frozen: bool, ) -> Container<'a, Message, StyleType> { Container::new( Row::new() .align_y(Alignment::Center) .push(horizontal_space()) - .push(Space::with_width(80)) + .push(Space::with_width(110)) .push(Text::new(SNIFFNET_TITLECASE).font(font_headers)) .push(Space::with_width(10)) + .push(get_button_freeze(font, language, frozen, true)) .push(get_button_minimize(font, language, true)) .push(horizontal_space()) .push(if unread_notifications > 0 { diff --git a/src/gui/sniffer.rs b/src/gui/sniffer.rs index 62030b339..9e5fb67d3 100644 --- a/src/gui/sniffer.rs +++ b/src/gui/sniffer.rs @@ -121,6 +121,10 @@ pub struct Sniffer { pub id: Option, /// Host data for filter dropdowns (comboboxes) pub host_data_states: HostDataStates, + /// Flag reporting whether the packet capture is frozen + pub frozen: bool, + /// Sender to freeze the packet capture + freeze_tx: Option>, } impl Sniffer { @@ -160,6 +164,8 @@ impl Sniffer { thumbnail: false, id: None, host_data_states: HostDataStates::default(), + frozen: false, + freeze_tx: None, } } @@ -175,6 +181,7 @@ impl Sniffer { }) => match key.as_ref() { Key::Character("q") => Some(Message::QuitWrapper), Key::Character("t") => Some(Message::CtrlTPressed), + Key::Named(Named::Space) => Some(Message::CtrlSpacePressed), _ => None, }, _ => None, @@ -185,6 +192,7 @@ impl Sniffer { Modifiers::COMMAND => match key.as_ref() { Key::Character("q") => Some(Message::QuitWrapper), Key::Character("t") => Some(Message::CtrlTPressed), + Key::Named(Named::Space) => Some(Message::CtrlSpacePressed), Key::Character(",") => Some(Message::OpenLastSettings), Key::Named(Named::Backspace) => Some(Message::ResetButtonPressed), Key::Character("d") => Some(Message::CtrlDPressed), @@ -500,6 +508,14 @@ impl Sniffer { return Task::done(Message::ToggleThumbnail(false)); } } + Message::CtrlSpacePressed => { + if self.running_page.is_some() + && self.settings_page.is_none() + && self.modal.is_none() + { + return Task::done(Message::Freeze); + } + } Message::ScaleFactorShortcut(increase) => { let scale_factor = self.conf.settings.scale_factor; if !(scale_factor > 2.99 && increase || scale_factor < 0.31 && !increase) { @@ -555,6 +571,12 @@ impl Sniffer { .remote_notifications .set_url(&url); } + Message::Freeze => { + self.frozen = !self.frozen; + if let Some(tx) = &self.freeze_tx { + let _ = tx.send(()); + } + } } Task::none() } @@ -750,6 +772,9 @@ impl Sniffer { self.traffic_chart .change_capture_source(matches!(capture_source, CaptureSource::Device(_))); let (tx, rx) = async_channel::unbounded(); + let (freeze_tx, freeze_rx) = tokio::sync::broadcast::channel(1_048_575); + let freeze_rx2 = freeze_tx.subscribe(); + let filters = self.conf.filters.clone(); let _ = thread::Builder::new() .name("thread_parse_packets".to_string()) .spawn(move || { @@ -758,11 +783,14 @@ impl Sniffer { capture_source, &mmdb_readers, capture_context, + filters, &tx, + (freeze_rx, freeze_rx2), ); }) .log_err(location!()); self.current_capture_rx.1 = Some(rx.clone()); + self.freeze_tx = Some(freeze_tx); return Task::run(rx, |backend_msg| match backend_msg { BackendTrafficMessage::TickRun(cap_id, msg, host_msg, no_more_packets) => { Message::TickRun(cap_id, msg, host_msg, no_more_packets) @@ -800,6 +828,8 @@ impl Sniffer { self.page_number = 1; self.thumbnail = false; self.host_data_states = HostDataStates::default(); + self.frozen = false; + self.freeze_tx = None; } fn set_device(&mut self, name: &str) { diff --git a/src/gui/types/message.rs b/src/gui/types/message.rs index 200e9cb66..bee2beabe 100644 --- a/src/gui/types/message.rs +++ b/src/gui/types/message.rs @@ -121,6 +121,8 @@ pub enum Message { Drag, /// Ctrl+T keys have been pressed CtrlTPressed, + /// Ctrl+Space keys have been pressed + CtrlSpacePressed, /// Edit scale factor via keyboard shortcut ScaleFactorShortcut(bool), /// Set new release status @@ -139,4 +141,6 @@ pub enum Message { ToggleRemoteNotifications, /// The remote notifications URL has been updated RemoteNotificationsUrl(String), + /// Pause or resume live capture + Freeze, } diff --git a/src/networking/parse_packets.rs b/src/networking/parse_packets.rs index f3236aebe..8fb1e6407 100644 --- a/src/networking/parse_packets.rs +++ b/src/networking/parse_packets.rs @@ -1,5 +1,6 @@ //! Module containing functions executed by the thread in charge of parsing sniffed packets +use crate::gui::types::filters::Filters; use crate::location; use crate::mmdb::asn::get_asn; use crate::mmdb::country::get_country; @@ -32,6 +33,7 @@ use std::net::IpAddr; use std::sync::{Arc, Mutex}; use std::thread; use std::time::{Duration, Instant}; +use tokio::sync::broadcast::Receiver; /// The calling thread enters a loop in which it waits for network packets pub fn parse_packets( @@ -39,8 +41,12 @@ pub fn parse_packets( mut cs: CaptureSource, mmdb_readers: &MmdbReaders, capture_context: CaptureContext, + filters: Filters, tx: &Sender, + freeze_rxs: (Receiver<()>, Receiver<()>), ) { + let (mut freeze_rx, mut freeze_rx_2) = freeze_rxs; + let my_link_type = capture_context.my_link_type(); let (cap, mut savefile) = capture_context.consume(); @@ -55,10 +61,18 @@ pub fn parse_packets( let (pcap_tx, pcap_rx) = std::sync::mpsc::sync_channel(10_000); let _ = thread::Builder::new() .name("thread_packet_stream".to_string()) - .spawn(move || packet_stream(cap, &pcap_tx)) + .spawn(move || packet_stream(cap, &pcap_tx, &mut freeze_rx_2, &filters)) .log_err(location!()); loop { + // check if we need to freeze the parsing + if freeze_rx.try_recv().is_ok() { + // wait until unfreeze + let _ = freeze_rx.blocking_recv(); + // reset the first packet ticks + first_packet_ticks = Some(Instant::now()); + } + let (packet_res, cap_stats) = pcap_rx .recv_timeout(Duration::from_millis(150)) .unwrap_or((Err(pcap::Error::TimeoutExpired), None)); @@ -486,8 +500,20 @@ fn maybe_send_tick_run_offline( fn packet_stream( mut cap: CaptureType, tx: &std::sync::mpsc::SyncSender<(Result, Option)>, + freeze_rx: &mut Receiver<()>, + filters: &Filters, ) { loop { + // check if we need to freeze the parsing + if freeze_rx.try_recv().is_ok() { + // pause the capture + cap.pause(); + // wait until unfreeze + let _ = freeze_rx.blocking_recv(); + // resume the capture + cap.resume(filters); + } + let packet_res = cap.next_packet(); let packet_owned = packet_res.map(|p| PacketOwned { header: *p.header, diff --git a/src/networking/types/capture_context.rs b/src/networking/types/capture_context.rs index b28d4d22a..12fc363c1 100644 --- a/src/networking/types/capture_context.rs +++ b/src/networking/types/capture_context.rs @@ -128,7 +128,7 @@ impl CaptureType { CaptureSource::Device(device) => { let inactive = Capture::from_device(device.to_pcap_device())?; let cap = inactive - .promisc(true) + .promisc(false) .buffer_size(2_000_000) // 2MB buffer -> 10k packets of 200 bytes .snaplen(if pcap_out_path.is_some() { i32::from(u16::MAX) @@ -150,6 +150,22 @@ impl CaptureType { Self::Offline(cap) => cap.filter(bpf, true), } } + + pub fn pause(&mut self) { + if let Self::Live(cap) = self { + let _ = cap.filter("less 2", true).log_err(location!()); + } + } + + pub fn resume(&mut self, filters: &Filters) { + if let Self::Live(cap) = self { + if filters.is_some_filter_active() { + let _ = cap.filter(filters.bpf(), true).log_err(location!()); + } else if cap.filter("", true).log_err(location!()).is_err() { + let _ = cap.filter("greater 0", true).log_err(location!()); + } + } + } } #[derive(Clone)] diff --git a/src/utils/types/icon.rs b/src/utils/types/icon.rs index 5208aa6a2..7ff3065ad 100644 --- a/src/utils/types/icon.rs +++ b/src/utils/types/icon.rs @@ -52,6 +52,8 @@ pub enum Icon { Update, Warning, Waves, + Pause, + Resume, } impl Icon { @@ -105,6 +107,8 @@ impl Icon { Icon::Update => '<', // Icon::Expand => 'p', // Icon::Collapse => 'q', + Icon::Pause => '-', + Icon::Resume => '+', } }