Repository: GyulyVGC/sniffnet (language: rust)

Issue:
Packet PDUs Export
Are you planning to provide support for exporting the packets to either PCAP or PCAPng format?


Discussion / hints:
I have other plans in the short term, but in the long term I’ll consider adding support for PCAP format

Fix patch:
diff --git a/.gitignore b/.gitignore
index 2b0ebcd5b..05386a8fd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,4 +56,5 @@ $RECYCLE.BIN/
 
 ### Custom... ###
 Dockerfile
-lcov.info
\ No newline at end of file
+lcov.info
+*.pcap
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9caf22406..d534982b3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ All Sniffnet releases with the relative changes are documented in this file.
 ## [UNRELEASED]
 - Added support for ICMP connections and messages ([#417](https://github.com/GyulyVGC/sniffnet/pull/417) — fixes [#288](https://github.com/GyulyVGC/sniffnet/issues/288))
 - Added capability to identify 6000+ upper layer services, protocols, trojans, and worms ([#450](https://github.com/GyulyVGC/sniffnet/pull/450) — fixes [#374](https://github.com/GyulyVGC/sniffnet/issues/374))
+- Added feature to optionally export the analysis as a PCAP file with a custom path ([#473](https://github.com/GyulyVGC/sniffnet/pull/473) — fixes [#162](https://github.com/GyulyVGC/sniffnet/issues/162) and [#291](https://github.com/GyulyVGC/sniffnet/issues/291))
 - Introduced new filtering capabilities to allow users specify custom values of ports and IP addresses ([#414](https://github.com/GyulyVGC/sniffnet/pull/414))
 - The size of text and widgets can now be customised by setting a proper zoom value (fixes [#202](https://github.com/GyulyVGC/sniffnet/issues/202) and [#344](https://github.com/GyulyVGC/sniffnet/issues/344))
 - Added possibility to totally customize the app's theme via styles defined in TOML files ([#286](https://github.com/GyulyVGC/sniffnet/pull/286) and [#419](https://github.com/GyulyVGC/sniffnet/pull/419))
diff --git a/Cargo.toml b/Cargo.toml
index 6bcf5d2f0..c155c94dd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -39,13 +39,13 @@ strip = true
 [dependencies]
 pcap = "1.2.0"
 etherparse = "0.14.2"
-chrono = { version = "0.4.33", default_features = false, features = ["clock"] }
+chrono = { version = "0.4.35", default_features = false, features = ["clock"] }
 plotters = { version = "0.3.5", default_features = false, features = ["area_series"] }
 iced = { version = "0.12.1", features = ["tokio", "svg", "advanced", "lazy"] }
 plotters-iced = "0.10.0"
 maxminddb = "0.24.0"
-confy = "0.6.0"
-serde = { version = "1.0.196", default_features = false, features = ["derive"] }
+confy = "0.6.1"
+serde = { version = "1.0.197", default_features = false, features = ["derive"] }
 rodio = { version = "0.17.3", default_features = false, features = ["mp3"] }
 dns-lookup = "2.0.4"
 toml = "0.8.10"
diff --git a/src/configs/types/config_device.rs b/src/configs/types/config_device.rs
index d8e69f174..6c7249585 100644
--- a/src/configs/types/config_device.rs
+++ b/src/configs/types/config_device.rs
@@ -58,7 +58,7 @@ impl ConfigDevice {
                     name: device.name,
                     desc: device.desc,
                     addresses: Arc::new(Mutex::new(device.addresses)),
-                    link_type: MyLinkType::NotYetAssigned,
+                    link_type: MyLinkType::default(),
                 };
             }
         }
@@ -72,7 +72,7 @@ impl ConfigDevice {
             name: standard_device.name,
             desc: standard_device.desc,
             addresses: Arc::new(Mutex::new(standard_device.addresses)),
-            link_type: MyLinkType::NotYetAssigned,
+            link_type: MyLinkType::default(),
         }
     }
 }
diff --git a/src/gui/pages/initial_page.rs b/src/gui/pages/initial_page.rs
index c100a9270..6d86ffdcf 100644
--- a/src/gui/pages/initial_page.rs
+++ b/src/gui/pages/initial_page.rs
@@ -8,12 +8,14 @@ use iced::alignment::{Horizontal, Vertical};
 use iced::widget::scrollable::Direction;
 use iced::widget::tooltip::Position;
 use iced::widget::{
-    button, Button, Column, Container, Row, Rule, Scrollable, Space, Text, TextInput, Tooltip,
+    button, Button, Checkbox, Column, Container, Row, Rule, Scrollable, Space, Text, TextInput,
+    Tooltip,
 };
 use iced::Length::FillPortion;
-use iced::{alignment, Font, Length};
+use iced::{alignment, Alignment, Font, Length};
 use pcap::Device;
 
+use crate::gui::components::button::button_open_file;
 use crate::gui::styles::button::ButtonType;
 use crate::gui::styles::container::ContainerType;
 use crate::gui::styles::scrollbar::ScrollbarType;
@@ -21,6 +23,7 @@ use crate::gui::styles::style_constants::{FONT_SIZE_SUBTITLE, FONT_SIZE_TITLE};
 use crate::gui::styles::text::TextType;
 use crate::gui::styles::text_input::TextInputType;
 use crate::gui::styles::types::gradient_type::GradientType;
+use crate::gui::types::export_pcap::ExportPcap;
 use crate::gui::types::message::Message;
 use crate::gui::types::sniffer::Sniffer;
 use crate::networking::types::filters::Filters;
@@ -30,8 +33,11 @@ use crate::translations::translations::{
     address_translation, addresses_translation, choose_adapters_translation,
     ip_version_translation, protocol_translation, select_filters_translation, start_translation,
 };
-use crate::translations::translations_3::port_translation;
-use crate::utils::formatted_strings::get_invalid_filters_string;
+use crate::translations::translations_3::{
+    directory_translation, export_capture_translation, file_name_translation, port_translation,
+};
+use crate::utils::formatted_strings::{get_invalid_filters_string, get_path_termination_string};
+use crate::utils::types::file_info::FileInfo;
 use crate::utils::types::icon::Icon;
 use crate::{ConfigSettings, IpVersion, Language, Protocol, StyleType};
 
@@ -82,6 +88,11 @@ pub fn initial_page(sniffer: &Sniffer) -> Container<Message, StyleType> {
                 .push(col_port_filter),
         )
         .push(Rule::horizontal(40))
+        .push(
+            Container::new(get_export_pcap_group(&sniffer.export_pcap, language, font))
+                .height(Length::Fill)
+                .align_y(Vertical::Top),
+        )
         .push(
             Container::new(button_start(
                 font,
@@ -91,7 +102,7 @@ pub fn initial_page(sniffer: &Sniffer) -> Container<Message, StyleType> {
             ))
             .width(Length::Fill)
             .height(Length::Fill)
-            .align_y(Vertical::Center)
+            .align_y(Vertical::Top)
             .align_x(Horizontal::Center),
         );
 
@@ -355,3 +366,65 @@ fn get_col_adapter(sniffer: &Sniffer, font: Font) -> Column<Message, StyleType>
             .direction(Direction::Vertical(ScrollbarType::properties())),
         )
 }
+
+fn get_export_pcap_group(
+    export_pcap: &ExportPcap,
+    language: Language,
+    font: Font,
+) -> Container<'static, Message, StyleType> {
+    let enabled = export_pcap.enabled();
+    let file_name = export_pcap.file_name();
+    let directory = export_pcap.directory();
+
+    let caption = export_capture_translation(language);
+    let checkbox = Checkbox::new(caption, enabled)
+        .on_toggle(move |_| Message::ToggleExportPcap)
+        .size(18)
+        .font(font);
+
+    let mut ret_val = Column::new().spacing(10).push(checkbox);
+
+    if enabled {
+        let inner_col = Column::new()
+            .spacing(10)
+            .padding([0, 0, 0, 45])
+            .push(
+                Row::new()
+                    .align_items(Alignment::Center)
+                    .spacing(5)
+                    .push(Text::new(format!("{}:", file_name_translation(language))).font(font))
+                    .push(
+                        TextInput::new(ExportPcap::DEFAULT_FILE_NAME, file_name)
+                            .on_input(Message::OutputPcapFile)
+                            .padding([2, 5])
+                            .font(font)
+                            .width(200),
+                    ),
+            )
+            .push(
+                Row::new()
+                    .align_items(Alignment::Center)
+                    .spacing(5)
+                    .push(Text::new(format!("{}:", directory_translation(language))).font(font))
+                    .push(Text::new(get_path_termination_string(directory, 25)).font(font))
+                    .push(button_open_file(
+                        directory.to_owned(),
+                        FileInfo::Directory,
+                        language,
+                        font,
+                        true,
+                        Message::OutputPcapDir,
+                    )),
+            );
+        ret_val = ret_val.push(inner_col);
+        Container::new(ret_val)
+            .padding(10)
+            .width(Length::Fill)
+            .style(ContainerType::BorderedRound)
+    } else {
+        Container::new(ret_val)
+            .padding(10)
+            .width(Length::Fill)
+            .style(ContainerType::BorderedRound)
+    }
+}
diff --git a/src/gui/pages/settings_general_page.rs b/src/gui/pages/settings_general_page.rs
index b1350296d..3d3b71d05 100644
--- a/src/gui/pages/settings_general_page.rs
+++ b/src/gui/pages/settings_general_page.rs
@@ -291,7 +291,8 @@ fn button_clear_mmdb(
             .font(font)
             .vertical_alignment(Vertical::Center)
             .horizontal_alignment(Horizontal::Center)
-            .size(15),
+            .size(15)
+            .line_height(LineHeight::Relative(1.0)),
     )
     .padding(2)
     .height(20)
diff --git a/src/gui/pages/settings_notifications_page.rs b/src/gui/pages/settings_notifications_page.rs
index f629c146a..52b512ff2 100644
--- a/src/gui/pages/settings_notifications_page.rs
+++ b/src/gui/pages/settings_notifications_page.rs
@@ -279,7 +279,7 @@ fn input_group_packets(
                     false,
                 )
             })
-            .padding([3, 5])
+            .padding([2, 5])
             .font(font)
             .width(100),
         )
@@ -326,7 +326,7 @@ fn input_group_bytes(
                 let bytes_notification = BytesNotification::from(&value, Some(bytes_notification));
                 Message::UpdateNotificationSettings(Notification::Bytes(bytes_notification), false)
             })
-            .padding([3, 5])
+            .padding([2, 5])
             .font(font)
             .width(100),
         )
diff --git a/src/gui/types/export_pcap.rs b/src/gui/types/export_pcap.rs
new file mode 100644
index 000000000..9ca8caac9
--- /dev/null
+++ b/src/gui/types/export_pcap.rs
@@ -0,0 +1,154 @@
+use std::path::PathBuf;
+
+pub struct ExportPcap {
+    enabled: bool,
+    file_name: String,
+    directory: String,
+}
+
+impl ExportPcap {
+    pub const DEFAULT_FILE_NAME: &'static str = "sniffnet.pcap";
+
+    pub fn toggle(&mut self) {
+        self.enabled = !self.enabled;
+    }
+
+    pub fn set_file_name(&mut self, file_name: String) {
+        self.file_name = file_name;
+    }
+
+    pub fn set_directory(&mut self, directory: String) {
+        self.directory = directory;
+    }
+
+    pub fn enabled(&self) -> bool {
+        self.enabled
+    }
+
+    pub fn file_name(&self) -> &str {
+        &self.file_name
+    }
+
+    pub fn directory(&self) -> &str {
+        &self.directory
+    }
+
+    pub fn full_path(&self) -> Option<String> {
+        if self.enabled {
+            let mut full_path = PathBuf::from(&self.directory);
+            let file_name = if self.file_name.is_empty() {
+                Self::DEFAULT_FILE_NAME
+            } else {
+                &self.file_name
+            };
+            full_path.push(file_name);
+            Some(full_path.to_string_lossy().to_string())
+        } else {
+            None
+        }
+    }
+}
+
+impl Default for ExportPcap {
+    fn default() -> Self {
+        ExportPcap {
+            enabled: false,
+            file_name: String::from(Self::DEFAULT_FILE_NAME),
+            directory: std::env::var("HOME").unwrap_or_default(),
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_default() {
+        let export_pcap = ExportPcap::default();
+        assert_eq!(export_pcap.enabled(), false);
+        assert_eq!(export_pcap.file_name(), "sniffnet.pcap");
+        assert_eq!(
+            export_pcap.directory(),
+            std::env::var("HOME").unwrap_or_default()
+        );
+    }
+
+    #[test]
+    fn test_toggle() {
+        let mut export_pcap = ExportPcap::default();
+        assert_eq!(export_pcap.enabled(), false);
+
+        export_pcap.toggle();
+        assert_eq!(export_pcap.enabled(), true);
+
+        export_pcap.toggle();
+        assert_eq!(export_pcap.enabled(), false);
+    }
+
+    #[test]
+    fn test_set_file_name() {
+        let mut export_pcap = ExportPcap::default();
+        assert_eq!(export_pcap.file_name(), "sniffnet.pcap");
+
+        export_pcap.set_file_name("test.pcap".to_string());
+        assert_eq!(export_pcap.file_name(), "test.pcap");
+
+        export_pcap.set_file_name("".to_string());
+        assert_eq!(export_pcap.file_name(), "");
+    }
+
+    #[test]
+    fn test_set_directory() {
+        let mut export_pcap = ExportPcap::default();
+        assert_eq!(
+            export_pcap.directory(),
+            std::env::var("HOME").unwrap_or_default()
+        );
+
+        export_pcap.set_directory("/tmp".to_string());
+        assert_eq!(export_pcap.directory(), "/tmp");
+    }
+
+    #[test]
+    fn test_full_path() {
+        let mut export_pcap = ExportPcap::default();
+        assert_eq!(export_pcap.full_path(), None);
+
+        export_pcap.toggle();
+        assert_eq!(
+            export_pcap.full_path(),
+            Some(format!(
+                "{}/sniffnet.pcap",
+                std::env::var("HOME").unwrap_or_default(),
+            ))
+        );
+
+        export_pcap.set_file_name("test.pcap".to_string());
+        assert_eq!(
+            export_pcap.full_path(),
+            Some(format!(
+                "{}/test.pcap",
+                std::env::var("HOME").unwrap_or_default()
+            ))
+        );
+
+        export_pcap.set_directory("/tmp".to_string());
+        assert_eq!(export_pcap.full_path(), Some("/tmp/test.pcap".to_string()));
+
+        export_pcap.toggle();
+        assert_eq!(export_pcap.full_path(), None);
+
+        export_pcap.toggle();
+        assert_eq!(export_pcap.full_path(), Some("/tmp/test.pcap".to_string()));
+
+        export_pcap.set_file_name("".to_string());
+        assert_eq!(
+            export_pcap.full_path(),
+            Some("/tmp/sniffnet.pcap".to_string())
+        );
+
+        export_pcap.set_directory("".to_string());
+        assert_eq!(export_pcap.full_path(), Some("sniffnet.pcap".to_string()));
+    }
+}
diff --git a/src/gui/types/message.rs b/src/gui/types/message.rs
index f7ded251f..547c81ff0 100644
--- a/src/gui/types/message.rs
+++ b/src/gui/types/message.rs
@@ -99,12 +99,16 @@ pub enum Message {
     CustomCountryDb(String),
     /// The ASN MMDB custom path has been updated
     CustomAsnDb(String),
-    // /// The path for the output report has been updated
-    // CustomReport(String),
     /// Save the configurations of the app and quit
     CloseRequested,
     /// Copies the given string to clipboard
     CopyIp(String),
     /// Launch a new file dialog
     OpenFile(String, FileInfo, fn(String) -> Message),
+    /// Toggle export pcap file
+    ToggleExportPcap,
+    /// The output PCAP directory has been updated
+    OutputPcapDir(String),
+    /// The output PCAP file name has been updated
+    OutputPcapFile(String),
 }
diff --git a/src/gui/types/mod.rs b/src/gui/types/mod.rs
index 09c25589e..ac0577c9c 100644
--- a/src/gui/types/mod.rs
+++ b/src/gui/types/mod.rs
@@ -1,3 +1,4 @@
+pub mod export_pcap;
 pub mod message;
 pub mod runtime_data;
 pub mod sniffer;
diff --git a/src/gui/types/sniffer.rs b/src/gui/types/sniffer.rs
index 66db58f00..70495f99b 100644
--- a/src/gui/types/sniffer.rs
+++ b/src/gui/types/sniffer.rs
@@ -17,12 +17,13 @@ use crate::gui::pages::types::running_page::RunningPage;
 use crate::gui::pages::types::settings_page::SettingsPage;
 use crate::gui::styles::types::custom_palette::{CustomPalette, ExtraStyles};
 use crate::gui::styles::types::palette::Palette;
+use crate::gui::types::export_pcap::ExportPcap;
 use crate::gui::types::message::Message;
 use crate::gui::types::timing_events::TimingEvents;
 use crate::mmdb::asn::ASN_MMDB;
 use crate::mmdb::country::COUNTRY_MMDB;
 use crate::mmdb::types::mmdb_reader::MmdbReader;
-use crate::networking::manage_packets::get_capture_result;
+use crate::networking::types::capture_context::CaptureContext;
 use crate::networking::types::filters::Filters;
 use crate::networking::types::host::Host;
 use crate::networking::types::ip_collection::AddressCollection;
@@ -92,6 +93,8 @@ pub struct Sniffer {
     pub asn_mmdb_reader: Arc<MmdbReader>,
     /// Time-related events
     pub timing_events: TimingEvents,
+    /// Information about PCAP file export
+    pub export_pcap: ExportPcap,
 }
 
 impl Sniffer {
@@ -131,6 +134,7 @@ impl Sniffer {
             country_mmdb_reader: Arc::new(MmdbReader::from(&mmdb_country, COUNTRY_MMDB)),
             asn_mmdb_reader: Arc::new(MmdbReader::from(&mmdb_asn, ASN_MMDB)),
             timing_events: TimingEvents::default(),
+            export_pcap: ExportPcap::default(),
         }
     }
 
@@ -317,6 +321,15 @@ impl Sniffer {
             Message::ServiceSortSelection(sort_type) => {
                 self.service_sort_type = sort_type;
             }
+            Message::ToggleExportPcap => {
+                self.export_pcap.toggle();
+            }
+            Message::OutputPcapDir(path) => {
+                self.export_pcap.set_directory(path);
+            }
+            Message::OutputPcapFile(name) => {
+                self.export_pcap.set_file_name(name);
+            }
             Message::TickInit => {}
         }
         Command::none()
@@ -383,8 +396,9 @@ impl Sniffer {
         let current_device_name = &*self.device.name.clone();
         self.set_adapter(current_device_name);
         let device = self.device.clone();
-        let (pcap_error, cap_result) = get_capture_result(&device);
-        self.pcap_error = pcap_error.clone();
+        let pcap_path = self.export_pcap.full_path();
+        let capture_context = CaptureContext::new(&device, &pcap_path);
+        self.pcap_error = capture_context.error().map(ToString::to_string);
         let info_traffic_mutex = self.info_traffic.clone();
         *info_traffic_mutex.lock().unwrap() = InfoTraffic::new();
         self.runtime_data = RunTimeData::new();
@@ -394,25 +408,24 @@ impl Sniffer {
         self.traffic_chart = TrafficChart::new(style, language);
         self.running_page = RunningPage::Overview;
 
-        if pcap_error.is_none() {
+        if capture_context.error().is_none() {
             // no pcap error
-            let cap = cap_result.unwrap();
             let current_capture_id = self.current_capture_id.clone();
             let filters = self.filters.clone();
             let country_mmdb_reader = self.country_mmdb_reader.clone();
             let asn_mmdb_reader = self.asn_mmdb_reader.clone();
-            self.device.link_type = MyLinkType::from_pcap_link_type(cap.get_datalink());
+            self.device.link_type = capture_context.my_link_type();
             thread::Builder::new()
                 .name("thread_parse_packets".to_string())
                 .spawn(move || {
                     parse_packets(
                         &current_capture_id,
                         &device,
-                        cap,
                         &filters,
                         &info_traffic_mutex,
                         &country_mmdb_reader,
                         &asn_mmdb_reader,
+                        capture_context,
                     );
                 })
                 .unwrap();
@@ -440,7 +453,7 @@ impl Sniffer {
                     name: dev.name,
                     desc: dev.desc,
                     addresses: self.device.addresses.clone(),
-                    link_type: MyLinkType::NotYetAssigned,
+                    link_type: MyLinkType::default(),
                 };
                 break;
             }
@@ -595,20 +608,29 @@ impl Sniffer {
     async fn open_file(old_file: String, file_info: FileInfo, language: Language) -> String {
         let starting_directory = if old_file.is_empty() {
             std::env::var("HOME").unwrap_or_default()
+        } else if file_info == FileInfo::Directory {
+            old_file.clone()
         } else {
             let mut folder_path = PathBuf::from(&old_file);
             folder_path.pop();
             folder_path.to_string_lossy().to_string()
         };
-        let picked_file = rfd::AsyncFileDialog::new()
+
+        let dialog = rfd::AsyncFileDialog::new()
             .set_title(file_info.action_info(language))
-            .add_filter(file_info.get_extension(), &[file_info.get_extension()])
-            .set_directory(starting_directory)
-            .pick_file()
-            .await
-            .unwrap_or_else(|| FileHandle::from(PathBuf::from(&old_file)));
+            .set_directory(starting_directory);
+
+        let picked = if file_info == FileInfo::Directory {
+            dialog.pick_folder().await
+        } else {
+            dialog
+                .add_filter(file_info.get_extension(), &[file_info.get_extension()])
+                .pick_file()
+                .await
+        }
+        .unwrap_or_else(|| FileHandle::from(PathBuf::from(&old_file)));
 
-        picked_file.path().to_string_lossy().to_string()
+        picked.path().to_string_lossy().to_string()
     }
 }
 
diff --git a/src/networking/manage_packets.rs b/src/networking/manage_packets.rs
index 25f4c48f1..969f7157d 100644
--- a/src/networking/manage_packets.rs
+++ b/src/networking/manage_packets.rs
@@ -6,7 +6,7 @@ use std::sync::{Arc, Mutex};
 use chrono::Local;
 use dns_lookup::lookup_addr;
 use etherparse::{Ethernet2Header, LaxPacketHeaders, NetHeaders, TransportHeader};
-use pcap::{Active, Address, Capture, Device};
+use pcap::{Address, Device};
 
 use crate::mmdb::asn::get_asn;
 use crate::mmdb::country::get_country;
@@ -563,22 +563,6 @@ pub fn is_my_address(local_address: &String, my_interface_addresses: &Vec<Addres
     is_loopback(local_address)
 }
 
-/// Determines if the capture opening resolves into an Error
-pub fn get_capture_result(device: &MyDevice) -> (Option<String>, Option<Capture<Active>>) {
-    let cap_result = Capture::from_device(device.to_pcap_device())
-        .expect("Capture initialization error\n\r")
-        .promisc(true)
-        .snaplen(256) //limit stored packets slice dimension (to keep more in the buffer)
-        .immediate_mode(true) //parse packets ASAP!
-        .open();
-    if cap_result.is_err() {
-        let err_string = cap_result.err().unwrap().to_string();
-        (Some(err_string), None)
-    } else {
-        (None, cap_result.ok())
-    }
-}
-
 /// Converts a MAC address in its hexadecimal form
 fn mac_from_dec_to_hex(mac_dec: [u8; 6]) -> String {
     let mut mac_hex = String::new();
diff --git a/src/networking/types/capture_context.rs b/src/networking/types/capture_context.rs
new file mode 100644
index 000000000..994bb43fa
--- /dev/null
+++ b/src/networking/types/capture_context.rs
@@ -0,0 +1,86 @@
+use crate::networking::types::my_device::MyDevice;
+use crate::networking::types::my_link_type::MyLinkType;
+use pcap::{Active, Capture, Savefile};
+
+pub enum CaptureContext {
+    Online(Online),
+    OnlineWithSavefile(OnlineWithSavefile),
+    Error(String),
+}
+
+impl CaptureContext {
+    pub fn new(device: &MyDevice, pcap_path: &Option<String>) -> Self {
+        let cap_res = Capture::from_device(device.to_pcap_device())
+            .expect("Capture initialization error\n\r")
+            .promisc(true)
+            .snaplen(if pcap_path.is_some() {
+                i32::from(u16::MAX)
+            } else {
+                256 //limit stored packets slice dimension (to keep more in the buffer)
+            })
+            .immediate_mode(true) //parse packets ASAP!
+            .open();
+
+        if let Err(e) = &cap_res {
+            return Self::Error(e.to_string());
+        }
+
+        let cap = cap_res.unwrap();
+
+        if let Some(path) = pcap_path {
+            let savefile_res = cap.savefile(path);
+            if let Err(e) = savefile_res {
+                Self::Error(e.to_string())
+            } else {
+                Self::new_online_with_savefile(cap, savefile_res.unwrap())
+            }
+        } else {
+            Self::new_online(cap)
+        }
+    }
+
+    fn new_online(cap: Capture<Active>) -> Self {
+        Self::Online(Online { cap })
+    }
+
+    fn new_online_with_savefile(cap: Capture<Active>, savefile: Savefile) -> Self {
+        Self::OnlineWithSavefile(OnlineWithSavefile {
+            online: Online { cap },
+            savefile,
+        })
+    }
+
+    pub fn error(&self) -> Option<&str> {
+        match self {
+            Self::Error(e) => Some(e),
+            _ => None,
+        }
+    }
+
+    pub fn consume(self) -> (Capture<Active>, Option<Savefile>) {
+        match self {
+            Self::Online(o) => (o.cap, None),
+            Self::OnlineWithSavefile(ows) => (ows.online.cap, Some(ows.savefile)),
+            Self::Error(_) => panic!(),
+        }
+    }
+
+    pub fn my_link_type(&self) -> MyLinkType {
+        match self {
+            Self::Online(o) => MyLinkType::from_pcap_link_type(o.cap.get_datalink()),
+            Self::OnlineWithSavefile(ows) => {
+                MyLinkType::from_pcap_link_type(ows.online.cap.get_datalink())
+            }
+            Self::Error(_) => MyLinkType::default(),
+        }
+    }
+}
+
+pub struct Online {
+    cap: Capture<Active>,
+}
+
+pub struct OnlineWithSavefile {
+    online: Online,
+    savefile: Savefile,
+}
diff --git a/src/networking/types/mod.rs b/src/networking/types/mod.rs
index f4f2fee37..cb369e4e3 100644
--- a/src/networking/types/mod.rs
+++ b/src/networking/types/mod.rs
@@ -1,6 +1,7 @@
 pub mod address_port_pair;
 pub mod asn;
 pub mod byte_multiple;
+pub mod capture_context;
 pub mod data_info;
 pub mod data_info_host;
 pub mod filters;
diff --git a/src/networking/types/my_link_type.rs b/src/networking/types/my_link_type.rs
index 68a77fbc0..11164a454 100644
--- a/src/networking/types/my_link_type.rs
+++ b/src/networking/types/my_link_type.rs
@@ -8,7 +8,7 @@ use crate::translations::translations_3::link_type_translation;
 use crate::{Language, StyleType};
 
 /// Currently supported link types
-#[derive(Copy, Clone)]
+#[derive(Copy, Clone, Default)]
 pub enum MyLinkType {
     Null(Linktype),
     Ethernet(Linktype),
@@ -17,6 +17,7 @@ pub enum MyLinkType {
     IPv4(Linktype),
     IPv6(Linktype),
     Unsupported(Linktype),
+    #[default]
     NotYetAssigned,
 }
 
diff --git a/src/secondary_threads/parse_packets.rs b/src/secondary_threads/parse_packets.rs
index c087309fd..682708038 100644
--- a/src/secondary_threads/parse_packets.rs
+++ b/src/secondary_threads/parse_packets.rs
@@ -7,12 +7,13 @@ use std::thread;
 use etherparse::err::ip::{HeaderError, LaxHeaderSliceError};
 use etherparse::err::{Layer, LenError};
 use etherparse::{LaxPacketHeaders, LenSource};
-use pcap::{Active, Capture, Packet};
+use pcap::Packet;
 
 use crate::mmdb::types::mmdb_reader::MmdbReader;
 use crate::networking::manage_packets::{
     analyze_headers, get_address_to_lookup, modify_or_insert_in_map, reverse_dns_lookup,
 };
+use crate::networking::types::capture_context::CaptureContext;
 use crate::networking::types::data_info::DataInfo;
 use crate::networking::types::filters::Filters;
 use crate::networking::types::icmp_type::IcmpType;
@@ -22,20 +23,21 @@ use crate::networking::types::my_link_type::MyLinkType;
 use crate::networking::types::packet_filters_fields::PacketFiltersFields;
 use crate::InfoTraffic;
 
-/// The calling thread enters in a loop in which it waits for network packets, parses them according
+/// The calling thread enters a loop in which it waits for network packets, parses them according
 /// to the user specified filters, and inserts them into the shared map variable.
 pub fn parse_packets(
     current_capture_id: &Arc<Mutex<usize>>,
     device: &MyDevice,
-    mut cap: Capture<Active>,
     filters: &Filters,
     info_traffic_mutex: &Arc<Mutex<InfoTraffic>>,
     country_mmdb_reader: &Arc<MmdbReader>,
     asn_mmdb_reader: &Arc<MmdbReader>,
+    capture_context: CaptureContext,
 ) {
-    let capture_id = *current_capture_id.lock().unwrap();
+    let my_link_type = capture_context.my_link_type();
+    let (mut cap, mut savefile) = capture_context.consume();
 
-    let my_link_type = MyLinkType::from_pcap_link_type(cap.get_datalink());
+    let capture_id = *current_capture_id.lock().unwrap();
 
     loop {
         match cap.next_packet() {
@@ -71,6 +73,11 @@ pub fn parse_packets(
 
                     let passed_filters = filters.matches(&packet_filters_fields);
                     if passed_filters {
+                        // save this packet to PCAP file
+                        if let Some(file) = savefile.as_mut() {
+                            file.write(&packet);
+                        }
+                        // update the shared map
                         new_info = modify_or_insert_in_map(
                             info_traffic_mutex,
                             &key,
diff --git a/src/translations/translations_3.rs b/src/translations/translations_3.rs
index 2e84a6db3..b09cb596f 100644
--- a/src/translations/translations_3.rs
+++ b/src/translations/translations_3.rs
@@ -152,3 +152,36 @@ pub fn service_translation(language: Language) -> &'static str {
         _ => "Service",
     }
 }
+
+pub fn export_capture_translation(language: Language) -> &'static str {
+    match language {
+        Language::EN => "Export capture file",
+        Language::IT => "Esporta file di cattura",
+        _ => "Export capture file",
+    }
+}
+
+// (a filesystem directory)
+pub fn directory_translation(language: Language) -> &'static str {
+    match language {
+        Language::EN => "Directory",
+        Language::IT => "Cartella",
+        _ => "Directory",
+    }
+}
+
+pub fn select_directory_translation(language: Language) -> &'static str {
+    match language {
+        Language::EN => "Select destination directory",
+        Language::IT => "Seleziona cartella di destinazione",
+        _ => "Select destination directory",
+    }
+}
+
+pub fn file_name_translation(language: Language) -> &'static str {
+    match language {
+        Language::EN => "File name",
+        Language::IT => "Nome del file",
+        _ => "File name",
+    }
+}
diff --git a/src/utils/formatted_strings.rs b/src/utils/formatted_strings.rs
index 1395bfc77..df27cde68 100644
--- a/src/utils/formatted_strings.rs
+++ b/src/utils/formatted_strings.rs
@@ -11,8 +11,6 @@ use crate::Language;
 /// Application version number (to be displayed in gui footer)
 pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
 
-// pub const PCAP_FILE_NAME: &str = "sniffnet.pcap";
-
 /// Computes the String representing the percentage of filtered bytes/packets
 pub fn get_percentage_string(observed: u128, filtered: u128) -> String {
     #[allow(clippy::cast_precision_loss)]
@@ -77,39 +75,6 @@ pub fn get_active_filters_string(filters: &Filters, language: Language) -> Strin
     filters_string
 }
 
-// /// Returns the default report path
-// pub fn get_default_report_file_path() -> String {
-//     return if let Ok(mut config_path) = confy::get_configuration_file_path(SNIFFNET_LOWERCASE, "file") {
-//         config_path.pop();
-//         config_path.push(PCAP_FILE_NAME);
-//         config_path.to_string_lossy().to_string()
-//     } else {
-//         let mut path = PathBuf::from(std::env::var_os("HOME").unwrap());
-//         path.push(PCAP_FILE_NAME);
-//         path.to_string_lossy().to_string()
-//     };
-// }
-
-// /// Returns the file to use for the output PCAP report
-// /// It tries and fallbacks in the order: custom path, configs path, home directory path
-// // /// This function also updates the custom path text input TODO!
-// pub fn set_report_file_to_use(custom_path: &str) -> File {
-//     if let Ok(custom_file) = File::create(custom_path) {
-//         return custom_file;
-//     } else if let Ok(mut config_path) =
-//         confy::get_configuration_file_path(SNIFFNET_LOWERCASE, "file")
-//     {
-//         config_path.pop();
-//         config_path.push(PCAP_FILE_NAME);
-//         if let Ok(file) = File::create(config_path) {
-//             return file;
-//         }
-//     }
-//     let mut path = PathBuf::from(std::env::var_os("HOME").unwrap());
-//     path.push(PCAP_FILE_NAME);
-//     File::create(path).unwrap()
-// }
-
 pub fn print_cli_welcome_message() {
     print!(
         r"
diff --git a/src/utils/types/file_info.rs b/src/utils/types/file_info.rs
index a60fbc11b..644857eda 100644
--- a/src/utils/types/file_info.rs
+++ b/src/utils/types/file_info.rs
@@ -1,12 +1,13 @@
 use crate::translations::translations_3::{
-    database_from_file_translation, style_from_file_translation,
+    database_from_file_translation, select_directory_translation, style_from_file_translation,
 };
 use crate::translations::types::language::Language;
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
 pub enum FileInfo {
     Style,
     Database,
+    Directory,
 }
 
 impl FileInfo {
@@ -14,6 +15,7 @@ impl FileInfo {
         match self {
             FileInfo::Style => "toml",
             FileInfo::Database => "mmdb",
+            FileInfo::Directory => "",
         }
     }
 
@@ -21,6 +23,7 @@ impl FileInfo {
         match self {
             FileInfo::Style => style_from_file_translation(language),
             FileInfo::Database => database_from_file_translation(language),
+            FileInfo::Directory => select_directory_translation(language),
         }
     }
 }


Test patch: