├── .gitignore ├── src ├── network │ ├── core │ │ ├── mod.rs │ │ └── packet_data.rs │ ├── utils │ │ ├── mod.rs │ │ └── filter.rs │ ├── modules │ │ ├── stats │ │ │ ├── util │ │ │ │ ├── mod.rs │ │ │ │ └── ewma.rs │ │ │ ├── throttle_stats.rs │ │ │ ├── delay_stats.rs │ │ │ ├── tamper_stats.rs │ │ │ ├── drop_stats.rs │ │ │ ├── duplicate_stats.rs │ │ │ ├── reorder_stats.rs │ │ │ ├── bandwidth_stats.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── delay.rs │ │ ├── throttle.rs │ │ ├── drop.rs │ │ ├── reorder.rs │ │ ├── duplicate.rs │ │ ├── tamper.rs │ │ └── bandwidth.rs │ ├── types │ │ ├── mod.rs │ │ ├── delayed_packet.rs │ │ └── probability.rs │ ├── mod.rs │ └── processing │ │ ├── mod.rs │ │ ├── packet_processing_state.rs │ │ ├── packet_receiving.rs │ │ └── packet_processing.rs ├── cli │ ├── config │ │ ├── mod.rs │ │ └── config_options.rs │ ├── utils │ │ ├── mod.rs │ │ ├── serialization.rs │ │ └── logging.rs │ ├── settings │ │ ├── mod.rs │ │ ├── bandwidth.rs │ │ ├── delay.rs │ │ ├── drop.rs │ │ ├── duplicate.rs │ │ ├── reorder.rs │ │ ├── throttle.rs │ │ ├── tamper.rs │ │ └── packet_manipulation.rs │ ├── tui │ │ ├── mod.rs │ │ ├── widgets │ │ │ ├── mod.rs │ │ │ ├── utils │ │ │ │ ├── textarea_parsing.rs │ │ │ │ ├── textarea_ext.rs │ │ │ │ ├── mod.rs │ │ │ │ └── block_ext.rs │ │ │ ├── custom_widget.rs │ │ │ ├── filter_widget.rs │ │ │ ├── delay_widget.rs │ │ │ ├── bandwidth_widget.rs │ │ │ ├── drop_widget.rs │ │ │ ├── logs_widget.rs │ │ │ ├── duplicate_widget.rs │ │ │ ├── reorder_widget.rs │ │ │ ├── throttle_widget.rs │ │ │ └── tamper_widget.rs │ │ ├── logging_util.rs │ │ ├── traits.rs │ │ ├── terminal.rs │ │ ├── state.rs │ │ ├── input.rs │ │ ├── custom_logger.rs │ │ ├── ui.rs │ │ └── cli_ext.rs │ └── mod.rs ├── lib.rs ├── utils.rs └── main.rs ├── .idea ├── .gitignore ├── vcs.xml ├── modules.xml └── fumble.iml ├── LICENSE.md ├── .github └── workflows │ └── rust.yml ├── Cargo.toml ├── cliff.toml ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /src/network/core/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod packet_data; 2 | -------------------------------------------------------------------------------- /src/network/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod filter; 2 | -------------------------------------------------------------------------------- /src/cli/config/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config_options; 2 | -------------------------------------------------------------------------------- /src/network/modules/stats/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ewma; 2 | -------------------------------------------------------------------------------- /src/cli/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod logging; 2 | pub mod serialization; 3 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod cli; 2 | pub mod network; 3 | pub mod utils; 4 | -------------------------------------------------------------------------------- /src/network/types/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod delayed_packet; 2 | pub mod probability; 3 | -------------------------------------------------------------------------------- /src/network/mod.rs: -------------------------------------------------------------------------------- 1 | mod core; 2 | pub mod modules; 3 | pub mod processing; 4 | pub mod types; 5 | pub(crate) mod utils; 6 | -------------------------------------------------------------------------------- /src/network/processing/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod packet_processing; 2 | pub mod packet_processing_state; 3 | pub mod packet_receiving; 4 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /src/network/modules/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bandwidth; 2 | pub mod delay; 3 | pub mod drop; 4 | pub mod duplicate; 5 | pub mod reorder; 6 | pub mod stats; 7 | pub mod tamper; 8 | pub mod throttle; 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/cli/settings/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bandwidth; 2 | pub mod delay; 3 | pub mod drop; 4 | pub mod duplicate; 5 | pub mod packet_manipulation; 6 | pub mod reorder; 7 | pub mod tamper; 8 | pub mod throttle; 9 | -------------------------------------------------------------------------------- /src/cli/tui/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cli_ext; 2 | pub mod custom_logger; 3 | pub mod input; 4 | pub mod logging_util; 5 | pub mod state; 6 | pub mod terminal; 7 | pub mod traits; 8 | pub mod ui; 9 | pub mod widgets; 10 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/cli/tui/widgets/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bandwidth_widget; 2 | pub mod custom_widget; 3 | pub mod delay_widget; 4 | pub mod drop_widget; 5 | pub mod duplicate_widget; 6 | pub mod filter_widget; 7 | pub mod logs_widget; 8 | pub mod reorder_widget; 9 | pub mod tamper_widget; 10 | pub mod throttle_widget; 11 | pub mod utils; 12 | -------------------------------------------------------------------------------- /src/cli/settings/bandwidth.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Parser, Debug, Serialize, Deserialize, Default)] 5 | pub struct BandwidthOptions { 6 | /// Maximum bandwidth limit in KB/s 7 | #[arg(long = "bandwidth-limit", id = "bandwidth-limit", default_value_t = 0)] 8 | #[serde(default)] 9 | pub limit: usize, 10 | } 11 | -------------------------------------------------------------------------------- /src/cli/settings/delay.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Parser, Debug, Serialize, Deserialize, Default)] 5 | pub struct DelayOptions { 6 | /// Delay in milliseconds to introduce for each packet 7 | #[arg(long = "delay-duration", id = "delay-duration", default_value_t = 0)] 8 | #[serde(default)] 9 | pub duration: u64, 10 | } 11 | -------------------------------------------------------------------------------- /src/cli/utils/serialization.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize, Serializer}; 2 | 3 | pub fn serialize_option(value: &Option, serializer: S) -> Result 4 | where 5 | S: Serializer, 6 | T: Serialize + Default, 7 | { 8 | match value { 9 | Some(v) => serializer.serialize_some(v), 10 | None => serializer.serialize_some(&T::default()), 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/cli/tui/logging_util.rs: -------------------------------------------------------------------------------- 1 | use log::{debug, error, info, trace, warn}; 2 | 3 | pub fn make_trace() { 4 | trace!("Hey i am a trace!") 5 | } 6 | 7 | pub fn make_debug() { 8 | debug!("Hey i am a debug!") 9 | } 10 | 11 | pub fn make_log() { 12 | info!("Hey i am a log!") 13 | } 14 | 15 | pub fn make_warning() { 16 | warn!("Hey i am a warning!") 17 | } 18 | 19 | pub fn make_error() { 20 | error!("Hey i am a error!") 21 | } 22 | -------------------------------------------------------------------------------- /src/network/modules/stats/throttle_stats.rs: -------------------------------------------------------------------------------- 1 | pub struct ThrottleStats { 2 | pub(crate) is_throttling: bool, 3 | pub(crate) dropped_count: usize, 4 | } 5 | 6 | impl Default for ThrottleStats { 7 | fn default() -> Self { 8 | Self::new() 9 | } 10 | } 11 | 12 | impl ThrottleStats { 13 | pub fn new() -> Self { 14 | ThrottleStats { 15 | is_throttling: false, 16 | dropped_count: 0, 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.idea/fumble.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/cli/settings/drop.rs: -------------------------------------------------------------------------------- 1 | use crate::network::types::probability::Probability; 2 | use clap::Parser; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Parser, Debug, Serialize, Deserialize, Default)] 6 | pub struct DropOptions { 7 | /// Probability of dropping packets, ranging from 0.0 to 1.0 8 | #[arg(long = "drop-probability", id = "drop-probability", default_value_t = Probability::default())] 9 | #[serde(default)] 10 | pub probability: Probability, 11 | } 12 | -------------------------------------------------------------------------------- /src/cli/tui/traits.rs: -------------------------------------------------------------------------------- 1 | use ratatui::crossterm::event::KeyEvent; 2 | 3 | pub trait HandleInput { 4 | fn handle_input(&mut self, key: KeyEvent) -> bool; 5 | } 6 | 7 | pub trait DisplayName { 8 | fn name(&self) -> &str; 9 | } 10 | 11 | pub trait KeyBindings { 12 | fn key_bindings(&self) -> String; // or Vec if there are multiple bindings 13 | } 14 | 15 | pub trait IsActive { 16 | fn is_active(&self) -> bool; 17 | fn set_active(&mut self, state: bool); 18 | } 19 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use log::info; 2 | 3 | pub fn log_statistics(received: usize, sent: usize) { 4 | let dropped = received.saturating_sub(sent); // Number of dropped packets 5 | let dropped_percentage = if received > 0 { 6 | (dropped as f64 / received as f64) * 100.0 7 | } else { 8 | 0.0 9 | }; 10 | info!( 11 | "Received Packets: {}, Sent Packets: {}, Skipped Packets: {} - {:.2}%", 12 | received, sent, dropped, dropped_percentage 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/network/modules/stats/delay_stats.rs: -------------------------------------------------------------------------------- 1 | pub struct DelayStats { 2 | pub(crate) delayed_package_count: usize, 3 | } 4 | 5 | impl Default for DelayStats { 6 | fn default() -> Self { 7 | Self::new() 8 | } 9 | } 10 | 11 | impl DelayStats { 12 | pub fn new() -> Self { 13 | DelayStats { 14 | delayed_package_count: 0, 15 | } 16 | } 17 | 18 | pub fn delayed_package_count(&mut self, value: usize) { 19 | self.delayed_package_count = value; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/network/processing/packet_processing_state.rs: -------------------------------------------------------------------------------- 1 | use crate::network::core::packet_data::PacketData; 2 | use crate::network::types::delayed_packet::DelayedPacket; 3 | use std::collections::{BinaryHeap, VecDeque}; 4 | use std::time::Instant; 5 | 6 | pub struct PacketProcessingState<'a> { 7 | pub delay_storage: VecDeque>, 8 | pub reorder_storage: BinaryHeap>, 9 | pub bandwidth_limit_storage: VecDeque>, 10 | pub bandwidth_storage_total_size: usize, 11 | pub throttle_storage: VecDeque>, 12 | pub throttled_start_time: Instant, 13 | pub last_sent_package_time: Instant, 14 | } 15 | -------------------------------------------------------------------------------- /src/network/modules/delay.rs: -------------------------------------------------------------------------------- 1 | use crate::network::core::packet_data::PacketData; 2 | use crate::network::modules::stats::delay_stats::DelayStats; 3 | use std::collections::VecDeque; 4 | use std::time::Duration; 5 | 6 | pub fn delay_packets<'a>( 7 | packets: &mut Vec>, 8 | storage: &mut VecDeque>, 9 | delay: Duration, 10 | stats: &mut DelayStats, 11 | ) { 12 | storage.extend(packets.drain(..)); 13 | 14 | while let Some(packet_data) = storage.pop_front() { 15 | if packet_data.arrival_time.elapsed() >= delay { 16 | packets.push(packet_data); 17 | } else { 18 | storage.push_front(packet_data); 19 | break; 20 | } 21 | } 22 | stats.delayed_package_count(storage.len()) 23 | } 24 | -------------------------------------------------------------------------------- /src/cli/tui/widgets/utils/textarea_parsing.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | use std::str::FromStr; 3 | use tui_textarea::TextArea; 4 | 5 | pub trait ParseFromTextArea: Sized { 6 | fn from_text_area(widget: &TextArea) -> Option { 7 | Self::parse_from_text_area(widget).ok() 8 | } 9 | 10 | fn parse_from_text_area(widget: &TextArea) -> Result; 11 | } 12 | 13 | impl ParseFromTextArea for T 14 | where 15 | ::Err: Display, 16 | T: FromStr, 17 | { 18 | fn parse_from_text_area(widget: &TextArea) -> Result { 19 | widget 20 | .lines() 21 | .first() 22 | .ok_or_else(|| "No input found".to_string()) 23 | .and_then(|line| { 24 | line.parse::() 25 | .map_err(|e| format!("Failed to parse input: {}", e)) 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/cli/settings/duplicate.rs: -------------------------------------------------------------------------------- 1 | use crate::network::types::probability::Probability; 2 | use clap::Parser; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Parser, Debug, Serialize, Deserialize)] 6 | pub struct DuplicateOptions { 7 | /// Probability of duplicating packets, ranging from 0.0 to 1.0 8 | #[arg(long = "duplicate-probability", id = "duplicate-probability", default_value_t = Probability::default())] 9 | #[serde(default)] 10 | pub probability: Probability, 11 | 12 | /// Number of times to duplicate each packet 13 | #[arg(long = "duplicate-count", default_value_t = 1, id = "duplicate-count")] 14 | #[serde(default)] 15 | pub count: usize, 16 | } 17 | 18 | impl Default for DuplicateOptions { 19 | fn default() -> Self { 20 | DuplicateOptions { 21 | count: 1, 22 | probability: Probability::default(), 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/network/modules/stats/tamper_stats.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Sub; 2 | use std::time::{Duration, Instant}; 3 | 4 | pub struct TamperStats { 5 | pub(crate) data: Vec, 6 | pub(crate) tamper_flags: Vec, 7 | pub(crate) checksum_valid: bool, 8 | pub last_update: Instant, 9 | pub update_interval: Duration, 10 | } 11 | 12 | impl TamperStats { 13 | pub fn new(refresh_interval: Duration) -> Self { 14 | TamperStats { 15 | data: vec![], 16 | tamper_flags: vec![], 17 | checksum_valid: true, 18 | last_update: Instant::now().sub(refresh_interval), 19 | update_interval: refresh_interval, 20 | } 21 | } 22 | 23 | pub fn should_update(&mut self) -> bool { 24 | self.last_update.elapsed() >= self.update_interval 25 | } 26 | 27 | pub fn updated(&mut self) { 28 | self.last_update = Instant::now(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/cli/settings/reorder.rs: -------------------------------------------------------------------------------- 1 | use crate::network::types::probability::Probability; 2 | use clap::Parser; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Parser, Debug, Serialize, Deserialize)] 6 | pub struct ReorderOptions { 7 | /// Probability of reordering packets, ranging from 0.0 to 1.0 8 | #[arg(long = "reorder-probability", id = "reorder-probability", default_value_t = Probability::default())] 9 | #[serde(default)] 10 | pub probability: Probability, 11 | /// Maximum random delay in milliseconds to apply when reordering packets 12 | #[arg( 13 | long = "reorder-max-delay", 14 | id = "reorder-max-delay", 15 | default_value_t = 100 16 | )] 17 | #[serde(default)] 18 | pub max_delay: u64, 19 | } 20 | 21 | impl Default for ReorderOptions { 22 | fn default() -> Self { 23 | ReorderOptions { 24 | probability: Probability::default(), 25 | max_delay: 100, 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/cli/tui/widgets/utils/textarea_ext.rs: -------------------------------------------------------------------------------- 1 | use ratatui::prelude::{Color, Modifier, Style}; 2 | use tui_textarea::TextArea; 3 | 4 | pub trait TextAreaExt { 5 | fn set_text(&mut self, text: &str); 6 | fn set_cursor_visibility(&mut self, active: bool); 7 | fn set_dim_placeholder(&mut self, placeholder: impl Into); 8 | } 9 | 10 | impl<'a> TextAreaExt for TextArea<'a> { 11 | fn set_text(&mut self, text: &str) { 12 | self.set_yank_text(text); 13 | self.select_all(); 14 | self.paste(); 15 | } 16 | 17 | fn set_cursor_visibility(&mut self, active: bool) { 18 | self.set_cursor_style(if active { 19 | Style::default().add_modifier(Modifier::REVERSED) 20 | } else { 21 | Style::default().bg(Color::Black) 22 | }); 23 | } 24 | 25 | fn set_dim_placeholder(&mut self, placeholder: impl Into) { 26 | self.set_placeholder_text(placeholder); 27 | self.set_placeholder_style(Style::new().add_modifier(Modifier::DIM)); 28 | } 29 | } -------------------------------------------------------------------------------- /src/cli/tui/widgets/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod block_ext; 2 | pub mod textarea_ext; 3 | pub mod textarea_parsing; 4 | 5 | use ratatui::prelude::{Color, Style}; 6 | use ratatui::widgets::{Block}; 7 | use std::fmt::Display; 8 | use tui_textarea::TextArea; 9 | use crate::cli::tui::widgets::utils::block_ext::RoundedBlockExt; 10 | 11 | pub(crate) fn style_textarea_based_on_validation( 12 | textarea: &mut TextArea, 13 | res: &Result, 14 | ) -> bool 15 | where 16 | E: Display, 17 | { 18 | match res { 19 | Err(err) => { 20 | textarea.set_style(Style::default().fg(Color::LightRed)); 21 | let block = match textarea.block() { 22 | None => { Block::rounded() } 23 | Some(block) => { block.clone() } 24 | }; 25 | textarea.set_block(block.border_style(Style::default().fg(Color::LightRed)) 26 | .title_bottom(format!("ERROR: {}", err))); 27 | 28 | false 29 | } 30 | Ok(_) => { 31 | textarea.set_style(Style::default()); 32 | true 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Borna Cvitanić 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/cli/tui/widgets/utils/block_ext.rs: -------------------------------------------------------------------------------- 1 | use ratatui::prelude::{Color, Style}; 2 | use ratatui::widgets::{Block, BorderType}; 3 | 4 | pub trait RoundedBlockExt<'a> { 5 | fn rounded() -> Block<'a>; 6 | fn roundedt(title: &'a str) -> Block<'a>; 7 | fn invisible() -> Block<'a>; 8 | fn highlight_if(self, active: bool) -> Self; 9 | } 10 | 11 | impl<'a> RoundedBlockExt<'a> for Block<'a> { 12 | /// Creates a new block with all rounded borders 13 | fn rounded() -> Block<'a> { 14 | Block::bordered().border_type(BorderType::Rounded) 15 | } 16 | 17 | /// Creates a new block with all rounded borders and the specified title 18 | fn roundedt(title: &'a str) -> Block<'a> { 19 | Block::rounded().title(title) 20 | } 21 | 22 | /// Creates a new block with invisible borders 23 | fn invisible() -> Block<'a> { 24 | Block::bordered().border_style(Style::new().fg(Color::Black)) 25 | } 26 | 27 | fn highlight_if(self, condition: bool) -> Self { 28 | if condition { 29 | self.border_style(Style::default().fg(Color::Yellow)) 30 | } else { 31 | self 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/network/modules/stats/drop_stats.rs: -------------------------------------------------------------------------------- 1 | use crate::network::modules::stats::util::ewma::Ewma; 2 | 3 | pub struct DropStats { 4 | pub total_packets: usize, 5 | pub total_dropped: usize, 6 | ewma: Ewma, 7 | } 8 | 9 | impl DropStats { 10 | pub fn new(alpha: f64) -> Self { 11 | Self { 12 | total_packets: 0, 13 | total_dropped: 0, 14 | ewma: Ewma::new(alpha), 15 | } 16 | } 17 | 18 | pub fn record(&mut self, dropped: bool) { 19 | self.total_packets += 1; 20 | if dropped { 21 | self.total_dropped += 1; 22 | } 23 | 24 | // Update the EWMA with the new drop status (1.0 if dropped, 0.0 if not) 25 | let current_drop_rate = if dropped { 1.0 } else { 0.0 }; 26 | self.ewma.update(current_drop_rate); 27 | } 28 | 29 | pub fn total_drop_rate(&self) -> f64 { 30 | if self.total_packets == 0 { 31 | 0.0 32 | } else { 33 | self.total_dropped as f64 / self.total_packets as f64 34 | } 35 | } 36 | 37 | pub fn recent_drop_rate(&self) -> f64 { 38 | self.ewma.get().unwrap_or(0.0) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/cli/tui/terminal.rs: -------------------------------------------------------------------------------- 1 | use ratatui::backend::CrosstermBackend; 2 | use ratatui::crossterm::{ 3 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 4 | ExecutableCommand, 5 | }; 6 | use ratatui::{CompletedFrame, Frame, Terminal}; 7 | use std::io::{self, stdout}; 8 | 9 | pub struct TerminalManager { 10 | terminal: Terminal>, 11 | } 12 | 13 | impl TerminalManager { 14 | pub fn new() -> io::Result { 15 | enable_raw_mode()?; 16 | stdout().execute(EnterAlternateScreen)?; 17 | 18 | let backend = CrosstermBackend::new(stdout()); 19 | let terminal = Terminal::new(backend)?; 20 | 21 | Ok(TerminalManager { terminal }) 22 | } 23 | 24 | pub fn draw(&mut self, f: F) -> io::Result 25 | where 26 | F: FnOnce(&mut Frame), 27 | { 28 | self.terminal.draw(f) 29 | } 30 | } 31 | 32 | impl Drop for TerminalManager { 33 | fn drop(&mut self) { 34 | // Cleanup is guaranteed to be called even if the program panics. 35 | let _ = disable_raw_mode(); 36 | let _ = stdout().execute(LeaveAlternateScreen); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/network/modules/stats/duplicate_stats.rs: -------------------------------------------------------------------------------- 1 | use crate::network::modules::stats::util::ewma::Ewma; 2 | 3 | pub struct DuplicateStats { 4 | pub(crate) incoming_packet_count: usize, 5 | pub(crate) outgoing_packet_count: usize, 6 | ewma: Ewma, 7 | } 8 | 9 | impl DuplicateStats { 10 | pub fn new(alpha: f64) -> Self { 11 | DuplicateStats { 12 | incoming_packet_count: 0, 13 | outgoing_packet_count: 0, 14 | ewma: Ewma::new(alpha), 15 | } 16 | } 17 | 18 | pub fn record(&mut self, outgoing_count: usize) { 19 | self.incoming_packet_count += 1; 20 | self.outgoing_packet_count += outgoing_count; 21 | 22 | let current_duplication_multiplier = outgoing_count as f64; 23 | self.ewma.update(current_duplication_multiplier); 24 | } 25 | 26 | pub fn total_duplication_multiplier(&self) -> f64 { 27 | if self.outgoing_packet_count == 0 { 28 | 1.0 29 | } else { 30 | self.outgoing_packet_count as f64 / self.incoming_packet_count as f64 31 | } 32 | } 33 | 34 | pub fn recent_duplication_multiplier(&self) -> f64 { 35 | self.ewma.get().unwrap_or(1.0) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/cli/settings/throttle.rs: -------------------------------------------------------------------------------- 1 | use crate::network::types::probability::Probability; 2 | use clap::Parser; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Parser, Debug, Serialize, Deserialize)] 6 | pub struct ThrottleOptions { 7 | /// Probability of triggering a throttle event, ranging from 0.0 to 1.0 8 | #[arg(long = "throttle-probability", id = "throttle-probability", default_value_t = Probability::default())] 9 | #[serde(default)] 10 | pub probability: Probability, 11 | 12 | /// Duration in milliseconds for which throttling should be applied 13 | #[arg( 14 | long = "throttle-duration", 15 | default_value_t = 30, 16 | id = "throttle-duration" 17 | )] 18 | #[serde(default)] 19 | pub duration: u64, 20 | 21 | /// Indicates whether throttled packets should be dropped 22 | #[arg(long = "throttle-drop", default_value_t = false, id = "throttle-drop")] 23 | #[serde(default)] 24 | pub drop: bool, 25 | } 26 | 27 | impl Default for ThrottleOptions { 28 | fn default() -> Self { 29 | ThrottleOptions { 30 | probability: Probability::default(), 31 | duration: 30, 32 | drop: false, 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/network/modules/stats/reorder_stats.rs: -------------------------------------------------------------------------------- 1 | use crate::network::modules::stats::util::ewma::Ewma; 2 | 3 | pub struct ReorderStats { 4 | pub(crate) total_packets: usize, 5 | pub(crate) reordered_packets: usize, 6 | pub(crate) delayed_packets: usize, 7 | ewma: Ewma, 8 | } 9 | 10 | impl ReorderStats { 11 | pub fn new(alpha: f64) -> Self { 12 | ReorderStats { 13 | total_packets: 0, 14 | reordered_packets: 0, 15 | delayed_packets: 0, 16 | ewma: Ewma::new(alpha), 17 | } 18 | } 19 | 20 | pub fn record(&mut self, reordered: bool) { 21 | self.total_packets += 1; 22 | if reordered { 23 | self.reordered_packets += 1; 24 | } 25 | 26 | let current_reorder_rate = if reordered { 1.0 } else { 0.0 }; 27 | self.ewma.update(current_reorder_rate); 28 | } 29 | 30 | pub fn total_reorder_rate(&self) -> f64 { 31 | if self.total_packets == 0 { 32 | 0.0 33 | } else { 34 | self.reordered_packets as f64 / self.total_packets as f64 35 | } 36 | } 37 | 38 | pub fn recent_reorder_rate(&self) -> f64 { 39 | self.ewma.get().unwrap_or(0.0) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/network/types/delayed_packet.rs: -------------------------------------------------------------------------------- 1 | use crate::network::core::packet_data::PacketData; 2 | use std::cmp::Ordering; 3 | use std::time::{Duration, Instant}; 4 | 5 | pub struct DelayedPacket<'a> { 6 | pub packet: PacketData<'a>, 7 | pub delay_until: Instant, 8 | } 9 | 10 | impl<'a> PartialEq for DelayedPacket<'a> { 11 | fn eq(&self, other: &Self) -> bool { 12 | self.delay_until == other.delay_until 13 | } 14 | } 15 | 16 | impl<'a> Eq for DelayedPacket<'a> {} 17 | 18 | impl<'a> PartialOrd for DelayedPacket<'a> { 19 | fn partial_cmp(&self, other: &Self) -> Option { 20 | // Note: We flip the ordering here to turn BinaryHeap into a min-heap based on delay_until 21 | Some(other.delay_until.cmp(&self.delay_until)) 22 | } 23 | } 24 | 25 | impl<'a> Ord for DelayedPacket<'a> { 26 | fn cmp(&self, other: &Self) -> Ordering { 27 | // Note: We flip the ordering here to turn BinaryHeap into a min-heap based on delay_until 28 | other.delay_until.cmp(&self.delay_until) 29 | } 30 | } 31 | 32 | impl<'a> DelayedPacket<'a> { 33 | pub(crate) fn new(packet: PacketData<'a>, delay: Duration) -> Self { 34 | DelayedPacket { 35 | packet, 36 | delay_until: Instant::now() + delay, 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/network/types/probability.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::fmt; 3 | use std::fmt::Formatter; 4 | use std::str::FromStr; 5 | 6 | #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] 7 | pub struct Probability(f64); 8 | 9 | impl Probability { 10 | pub fn new(value: f64) -> Result { 11 | if !(0.0..=1.0).contains(&value) { 12 | Err(format!("{} is not in the range 0.0 to 1.0", value)) 13 | } else { 14 | Ok(Probability(value)) 15 | } 16 | } 17 | 18 | pub fn value(&self) -> f64 { 19 | self.0 20 | } 21 | } 22 | 23 | impl FromStr for Probability { 24 | type Err = String; 25 | 26 | fn from_str(s: &str) -> Result { 27 | let value: f64 = s 28 | .parse() 29 | .map_err(|_| format!("`{}` is not a valid number", s))?; 30 | Probability::new(value) 31 | } 32 | } 33 | 34 | impl From for f64 { 35 | fn from(prob: Probability) -> Self { 36 | prob.0 37 | } 38 | } 39 | 40 | impl Default for Probability { 41 | fn default() -> Self { 42 | Probability(0.0) 43 | } 44 | } 45 | 46 | impl fmt::Display for Probability { 47 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 48 | write!(f, "{}", self.0) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/network/modules/throttle.rs: -------------------------------------------------------------------------------- 1 | use crate::network::core::packet_data::PacketData; 2 | use crate::network::modules::stats::throttle_stats::ThrottleStats; 3 | use crate::network::types::probability::Probability; 4 | use rand::{thread_rng, Rng}; 5 | use std::collections::VecDeque; 6 | use std::time::{Duration, Instant}; 7 | 8 | pub fn throttle_packages<'a>( 9 | packets: &mut Vec>, 10 | storage: &mut VecDeque>, 11 | throttled_start_time: &mut Instant, 12 | throttle_probability: Probability, 13 | throttle_duration: Duration, 14 | drop: bool, 15 | stats: &mut ThrottleStats, 16 | ) { 17 | if is_throttled(throttle_duration, throttled_start_time) { 18 | if drop { 19 | stats.dropped_count += packets.len(); 20 | packets.clear(); 21 | } else { 22 | storage.extend(packets.drain(..)); 23 | } 24 | stats.is_throttling = true; 25 | } else { 26 | packets.extend(storage.drain(..)); 27 | if thread_rng().gen_bool(throttle_probability.value()) { 28 | *throttled_start_time = Instant::now(); 29 | } 30 | stats.is_throttling = false; 31 | } 32 | } 33 | 34 | fn is_throttled(throttle_duration: Duration, throttled_start_time: &mut Instant) -> bool { 35 | throttled_start_time.elapsed() <= throttle_duration 36 | } 37 | -------------------------------------------------------------------------------- /src/cli/settings/tamper.rs: -------------------------------------------------------------------------------- 1 | use crate::network::types::probability::Probability; 2 | use clap::Parser; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Parser, Debug, Serialize, Deserialize)] 6 | pub struct TamperOptions { 7 | /// Probability of tampering packets, ranging from 0.0 to 1.0 8 | #[arg(long = "tamper-probability", id = "tamper-probability", default_value_t = Probability::default())] 9 | #[serde(default)] 10 | pub probability: Probability, 11 | 12 | /// Amount of tampering that should be applied, ranging from 0.0 to 1.0 13 | #[arg(long = "tamper-amount", default_value_t = Probability::new(0.1).unwrap(), id = "tamper-amount")] 14 | #[serde(default)] 15 | pub amount: Probability, 16 | 17 | /// Whether tampered packets should have their checksums recalculated to mask the tampering and avoid the packets getting automatically dropped 18 | #[arg( 19 | long = "tamper-recalculate-checksums", 20 | id = "tamper-recalculate-checksums" 21 | )] 22 | #[serde(default)] 23 | pub recalculate_checksums: Option, 24 | } 25 | 26 | impl Default for TamperOptions { 27 | fn default() -> Self { 28 | TamperOptions { 29 | probability: Probability::default(), 30 | amount: Probability::new(0.1).unwrap(), 31 | recalculate_checksums: Some(true), 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/network/modules/stats/bandwidth_stats.rs: -------------------------------------------------------------------------------- 1 | use crate::network::modules::stats::util::ewma::Ewma; 2 | use std::time::{Duration, Instant}; 3 | 4 | pub struct BandwidthStats { 5 | pub(crate) storage_packet_count: usize, 6 | pub(crate) total_byte_count: usize, 7 | ewma: Ewma, 8 | recent_byte_sent: usize, 9 | recent_timer: Instant, 10 | update_interval: Duration, 11 | } 12 | 13 | impl BandwidthStats { 14 | pub fn new(alpha: f64) -> Self { 15 | BandwidthStats { 16 | storage_packet_count: 0, 17 | total_byte_count: 0, 18 | ewma: Ewma::new(alpha), 19 | recent_byte_sent: 0, 20 | recent_timer: Instant::now(), 21 | update_interval: Duration::from_millis(100), 22 | } 23 | } 24 | 25 | pub fn record(&mut self, bytes_sent: usize) { 26 | self.total_byte_count += bytes_sent; 27 | self.recent_byte_sent += bytes_sent; 28 | if self.recent_timer.elapsed() >= self.update_interval { 29 | self.ewma.update( 30 | (self.recent_byte_sent as f64 / 1024f64) / self.update_interval.as_secs_f64(), 31 | ); 32 | self.recent_byte_sent = 0; 33 | self.recent_timer = Instant::now(); 34 | } 35 | } 36 | 37 | pub fn recent_throughput(&self) -> f64 { 38 | self.ewma.get().unwrap_or(0.0) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::config::config_options::ConfigOptions; 2 | use crate::cli::settings::packet_manipulation::PacketManipulationSettings; 3 | use crate::network::utils::filter::validate_filter_with_docs; 4 | use clap::Parser; 5 | 6 | pub mod config; 7 | pub mod settings; 8 | pub mod tui; 9 | pub mod utils; 10 | 11 | #[derive(Parser, Debug)] 12 | #[command( 13 | name = "fumble", 14 | about = "A network manipulation tool for simulating various network conditions.", 15 | long_about = "fumble is a network manipulation tool that can introduce packet drops, delays, throttling, reordering, duplication, and bandwidth limitations.\n\n\ 16 | ## Logging\n\ 17 | The tool uses the `env_logger` crate for logging. By default, informational messages are displayed.\n\n\ 18 | To customize the verbosity of logs, set the `RUST_LOG` environment variable before running `fumble`.\n\n\ 19 | Example: RUST_LOG=debug fumble --filter 'tcp.DstPort == 80'" 20 | )] 21 | #[derive(Default)] 22 | pub struct Cli { 23 | /// Filter expression for capturing packets 24 | #[arg(short, long, value_parser = validate_filter_with_docs)] 25 | pub filter: Option, 26 | 27 | #[command(flatten)] 28 | pub config: ConfigOptions, 29 | 30 | #[command(flatten)] 31 | pub packet_manipulation_settings: PacketManipulationSettings, 32 | 33 | #[arg(short, long, default_value_t = false)] 34 | pub tui: bool, 35 | } -------------------------------------------------------------------------------- /src/network/core/packet_data.rs: -------------------------------------------------------------------------------- 1 | use std::time::Instant; 2 | use windivert::layer::NetworkLayer; 3 | use windivert::packet::WinDivertPacket; 4 | 5 | #[derive(Clone)] 6 | pub struct PacketData<'a> { 7 | pub packet: WinDivertPacket<'a, NetworkLayer>, 8 | pub arrival_time: Instant, 9 | } 10 | 11 | impl<'a> From> for PacketData<'a> { 12 | fn from(packet: WinDivertPacket<'a, NetworkLayer>) -> Self { 13 | PacketData { 14 | packet, 15 | arrival_time: Instant::now(), 16 | } 17 | } 18 | } 19 | 20 | #[cfg(test)] 21 | mod tests { 22 | use crate::network::core::packet_data::PacketData; 23 | use windivert::layer::NetworkLayer; 24 | use windivert::packet::WinDivertPacket; 25 | 26 | #[test] 27 | fn test_packet_data_creation() { 28 | unsafe { 29 | let dummy_packet = WinDivertPacket::::new(vec![1, 2, 3, 4]); 30 | let packet_data = PacketData::from(dummy_packet); 31 | // Assert that the packet data is correctly assigned 32 | assert_eq!(packet_data.packet.data.len(), 4); 33 | assert_eq!(packet_data.packet.data[..], [1, 2, 3, 4]); 34 | 35 | // Optionally, check if the arrival time is set (not empty, but correctness might need specific methods) 36 | assert!(packet_data.arrival_time.elapsed().as_secs() < 1); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build-windows: 14 | runs-on: windows-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Set up Rust 19 | uses: actions-rs/toolchain@v1 20 | with: 21 | toolchain: stable 22 | target: x86_64-pc-windows-msvc 23 | override: true 24 | 25 | - name: Download WinDivert 26 | run: | 27 | Invoke-WebRequest -Uri "https://reqrypt.org/download/WinDivert-2.2.2-A.zip" -OutFile "WinDivert.zip" 28 | Expand-Archive -Path "WinDivert.zip" -DestinationPath "WinDivert" 29 | 30 | - name: List extracted files (Debug) 31 | run: | 32 | Get-ChildItem -Recurse "WinDivert" 33 | 34 | - name: Copy WinDivert Files 35 | run: | 36 | $dllPath = (Get-ChildItem -Path "WinDivert" -Recurse -Filter "WinDivert.dll").FullName 37 | $libPath = (Get-ChildItem -Path "WinDivert" -Recurse -Filter "WinDivert.lib").FullName 38 | Copy-Item $dllPath "$env:USERPROFILE\.cargo\bin\WinDivert.dll" 39 | Copy-Item $libPath "$env:USERPROFILE\.cargo\bin\WinDivert.lib" 40 | 41 | - name: Build (Windows) 42 | run: cargo build --verbose --all-targets 43 | 44 | - name: Run tests (Windows) 45 | run: cargo test --verbose --all-targets -------------------------------------------------------------------------------- /src/network/modules/drop.rs: -------------------------------------------------------------------------------- 1 | use crate::network::core::packet_data::PacketData; 2 | use crate::network::modules::stats::drop_stats::DropStats; 3 | use crate::network::types::probability::Probability; 4 | use rand::Rng; 5 | 6 | pub fn drop_packets( 7 | packets: &mut Vec, 8 | drop_probability: Probability, 9 | stats: &mut DropStats, 10 | ) { 11 | let mut rng = rand::thread_rng(); 12 | 13 | // We use retain with a side effect: recording the drop stats 14 | packets.retain(|_| { 15 | let drop = rng.random::() < drop_probability.value(); 16 | stats.record(drop); 17 | !drop 18 | }); 19 | } 20 | 21 | #[cfg(test)] 22 | mod tests { 23 | use crate::network::core::packet_data::PacketData; 24 | use crate::network::modules::drop::drop_packets; 25 | use crate::network::modules::stats::drop_stats::DropStats; 26 | use crate::network::types::probability::Probability; 27 | use windivert::layer::NetworkLayer; 28 | use windivert::packet::WinDivertPacket; 29 | 30 | #[test] 31 | fn test_drop_packets() { 32 | unsafe { 33 | let mut packets = vec![PacketData::from(WinDivertPacket::::new( 34 | vec![1, 2, 3], 35 | ))]; 36 | let mut drop_stats = DropStats::new(0.3); 37 | drop_packets( 38 | &mut packets, 39 | Probability::new(1.0).unwrap(), 40 | &mut drop_stats, 41 | ); 42 | assert!(packets.is_empty()) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fumble" 3 | version = "0.6.1" 4 | authors = ["Borna Cvitanić borna.cvitanic@gmail.com"] 5 | description = "an oxidized implementation of the original clumsy tool, designed to simulate adverse network conditions on Windows systems." 6 | repository = "https://github.com/bornacvitanic/fumble" 7 | license-file = "LICENSE.md" 8 | edition = "2021" 9 | keywords = ["clumsy", "network", "packet", "manipulation", "simulation"] 10 | categories = ["command-line-utilities", "development-tools::testing", "simulation", "network-programming"] 11 | 12 | [lib] 13 | name = "fumble" 14 | path = "src/lib.rs" 15 | 16 | [[bin]] 17 | name = "fumble" 18 | path = "src/main.rs" 19 | 20 | [dependencies] 21 | # Rust bindings for the WinDivert library, enabling packet capture and modification on Windows. 22 | windivert = { version = "0.6", features = ["vendored"] } 23 | windivert-sys = "0.10.0" 24 | # Library for generating random numbers, used for probabilistic operations. 25 | rand = "0.9.0-alpha.2" 26 | # CLI argument parsing library 27 | clap = { version = "4.5.11", features = ["derive"]} 28 | # TUI (Terminal User Interface) 29 | ratatui = { version = "0.28.0"} 30 | tui-textarea = "0.6.1" 31 | lazy_static = "1.5.0" 32 | # Ctrl-C signals handling library to more gracefully shut down and be able to stop threads cleanly 33 | ctrlc = "3.2" 34 | # For matching strings 35 | regex = "1.10.5" 36 | # For configuration file serialization and deserialization 37 | serde = { version = "1.0", features = ["derive"] } 38 | toml = "0.8.19" 39 | dirs = "5.0.1" 40 | # Libraries for better logging 41 | env_logger = "0.11.5" 42 | log = "0.4.22" 43 | thiserror = "1.0.63" -------------------------------------------------------------------------------- /src/network/modules/stats/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::network::modules::stats::bandwidth_stats::BandwidthStats; 2 | use crate::network::modules::stats::delay_stats::DelayStats; 3 | use crate::network::modules::stats::drop_stats::DropStats; 4 | use crate::network::modules::stats::duplicate_stats::DuplicateStats; 5 | use crate::network::modules::stats::reorder_stats::ReorderStats; 6 | use crate::network::modules::stats::tamper_stats::TamperStats; 7 | use crate::network::modules::stats::throttle_stats::ThrottleStats; 8 | use std::sync::{Arc, RwLock}; 9 | use std::time::Duration; 10 | 11 | pub mod bandwidth_stats; 12 | pub mod delay_stats; 13 | pub mod drop_stats; 14 | pub mod duplicate_stats; 15 | pub mod reorder_stats; 16 | pub mod tamper_stats; 17 | pub mod throttle_stats; 18 | pub mod util; 19 | 20 | pub struct PacketProcessingStatistics { 21 | pub drop_stats: DropStats, 22 | pub delay_stats: DelayStats, 23 | pub throttle_stats: ThrottleStats, 24 | pub reorder_stats: ReorderStats, 25 | pub tamper_stats: TamperStats, 26 | pub duplicate_stats: DuplicateStats, 27 | pub bandwidth_stats: BandwidthStats, 28 | } 29 | 30 | // Function to initialize the statistics 31 | pub fn initialize_statistics() -> Arc> { 32 | Arc::new(RwLock::new(PacketProcessingStatistics { 33 | drop_stats: DropStats::new(0.005), 34 | delay_stats: DelayStats::new(), 35 | throttle_stats: ThrottleStats::new(), 36 | reorder_stats: ReorderStats::new(0.005), 37 | tamper_stats: TamperStats::new(Duration::from_millis(500)), 38 | duplicate_stats: DuplicateStats::new(0.005), 39 | bandwidth_stats: BandwidthStats::new(0.005), 40 | })) 41 | } 42 | -------------------------------------------------------------------------------- /src/cli/utils/logging.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::settings::packet_manipulation::PacketManipulationSettings; 2 | use log::info; 3 | 4 | pub fn log_initialization_info(filter: &Option, settings: &PacketManipulationSettings) { 5 | if let Some(traffic_filter) = &filter { 6 | info!("Traffic filer: {}", traffic_filter); 7 | } 8 | if let Some(drop) = &settings.drop { 9 | info!("Dropping packets with probability: {}", drop.probability); 10 | } 11 | if let Some(delay) = &settings.delay { 12 | info!("Delaying packets for: {} ms", delay.duration) 13 | } 14 | if let Some(throttle) = &settings.throttle { 15 | info!( 16 | "Throttling packets with probability of {} ms with a throttle duration of {}. \ 17 | Throttle packet dropping: {}", 18 | throttle.probability, throttle.duration, throttle.drop 19 | ) 20 | } 21 | if let Some(reorder) = &settings.reorder { 22 | info!( 23 | "Reordering packets with probability {} and maximum random delay of: {} ms", 24 | reorder.probability, reorder.max_delay 25 | ) 26 | } 27 | if let Some(tamper) = &settings.tamper { 28 | info!( 29 | "Tampering packets with probability {} and amount {}. Recalculating checksums: {}", 30 | tamper.probability, 31 | tamper.amount, 32 | tamper.recalculate_checksums.unwrap_or(true) 33 | ) 34 | } 35 | if let Some(duplicate) = &settings.duplicate { 36 | if duplicate.count > 1usize && duplicate.probability.value() > 0.0 { 37 | info!( 38 | "Duplicating packets {} times with probability: {}", 39 | duplicate.count, duplicate.probability 40 | ); 41 | } 42 | } 43 | if let Some(bandwidth) = &settings.bandwidth { 44 | info!("Limiting bandwidth to: {} KB/s", bandwidth.limit) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/cli/tui/state.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::tui::ui::LayoutSection; 2 | use crate::cli::tui::widgets::bandwidth_widget::BandwidthWidget; 3 | use crate::cli::tui::widgets::custom_widget::CustomWidget; 4 | use crate::cli::tui::widgets::delay_widget::DelayWidget; 5 | use crate::cli::tui::widgets::drop_widget::DropWidget; 6 | use crate::cli::tui::widgets::duplicate_widget::DuplicateWidget; 7 | use crate::cli::tui::widgets::filter_widget::FilterWidget; 8 | use crate::cli::tui::widgets::logs_widget::LogsWidget; 9 | use crate::cli::tui::widgets::reorder_widget::ReorderWidget; 10 | use crate::cli::tui::widgets::tamper_widget::TamperWidget; 11 | use crate::cli::tui::widgets::throttle_widget::ThrottleWidget; 12 | 13 | pub struct TuiState<'a> { 14 | pub processing: bool, 15 | pub filter_widget: FilterWidget<'a>, 16 | pub sections: Vec>, 17 | pub logs_widget: LogsWidget, 18 | pub selected: usize, 19 | pub interacting: Option, 20 | pub focused: LayoutSection, 21 | } 22 | 23 | impl<'a> Default for TuiState<'a> { 24 | fn default() -> Self { 25 | Self::new() 26 | } 27 | } 28 | 29 | impl<'a> TuiState<'a> { 30 | pub fn new() -> Self { 31 | TuiState { 32 | processing: true, 33 | filter_widget: FilterWidget::new(), 34 | sections: vec![ 35 | CustomWidget::Drop(DropWidget::new()), 36 | CustomWidget::Delay(DelayWidget::new()), 37 | CustomWidget::Throttle(ThrottleWidget::new()), 38 | CustomWidget::Reorder(ReorderWidget::new()), 39 | CustomWidget::Tamper(TamperWidget::new()), 40 | CustomWidget::Duplicate(DuplicateWidget::new()), 41 | CustomWidget::Bandwidth(BandwidthWidget::new()), 42 | ], 43 | selected: 0, 44 | interacting: None, 45 | logs_widget: LogsWidget::new(), 46 | focused: LayoutSection::Main, 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/network/modules/reorder.rs: -------------------------------------------------------------------------------- 1 | use crate::network::core::packet_data::PacketData; 2 | use crate::network::modules::stats::reorder_stats::ReorderStats; 3 | use crate::network::types::delayed_packet::DelayedPacket; 4 | use crate::network::types::probability::Probability; 5 | use log::{error, warn}; 6 | use std::collections::BinaryHeap; 7 | use std::time::{Duration, Instant}; 8 | 9 | pub fn reorder_packets<'a>( 10 | packets: &mut Vec>, 11 | storage: &mut BinaryHeap>, 12 | reorder_probability: Probability, 13 | max_delay: Duration, 14 | stats: &mut ReorderStats, 15 | ) { 16 | if max_delay.as_millis() == 0 { 17 | warn!("Max delay cannot be zero. Skipping packet reordering."); 18 | return; 19 | } 20 | 21 | let mut skipped_packets = Vec::new(); // Temporary storage for packets to be skipped 22 | 23 | for packet in packets.drain(..) { 24 | if rand::random::() >= reorder_probability.value() { 25 | skipped_packets.push(packet); // Store skipped packets 26 | stats.record(false); 27 | continue; 28 | } 29 | 30 | let delay = Duration::from_millis((rand::random::() % max_delay.as_millis()) as u64); 31 | let delayed_packet = DelayedPacket::new(packet, delay); 32 | storage.push(delayed_packet); 33 | stats.record(true); 34 | } 35 | stats.delayed_packets = storage.len(); 36 | 37 | packets.append(&mut skipped_packets); // Append skipped packets back to the original packets vector 38 | 39 | let now = Instant::now(); 40 | while let Some(delayed_packet) = storage.peek() { 41 | if delayed_packet.delay_until <= now { 42 | if let Some(delayed_packet) = storage.pop() { 43 | packets.push(delayed_packet.packet); 44 | } else { 45 | error!("Expected a delayed packet, but none was found in storage."); 46 | break; 47 | } 48 | } else { 49 | break; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/network/utils/filter.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use thiserror::Error; 3 | use windivert::layer::NetworkLayer; 4 | use windivert::prelude::WinDivertFlags; 5 | use windivert::{CloseAction, WinDivert}; 6 | 7 | #[derive(Debug, Error, Clone)] 8 | pub enum FilterError { 9 | #[error("Invalid filter syntax: {0}")] 10 | InvalidSyntax(String), 11 | #[error("Invalid port number detected in filter: {0}")] 12 | InvalidPort(String), 13 | } 14 | 15 | pub fn validate_filter_with_docs(filter: &str) -> Result { 16 | match validate_filter(filter) { 17 | Err(FilterError::InvalidSyntax(msg)) => { 18 | let detailed_msg = format!( 19 | "{}\n\nFor more details about the filter syntax, see the filter language documentation: https://reqrypt.org/windivert-doc.html#filter_language", 20 | msg 21 | ); 22 | Err(FilterError::InvalidSyntax(detailed_msg)) 23 | } 24 | other => other, 25 | } 26 | } 27 | 28 | pub fn validate_filter(filter: &str) -> Result { 29 | // Attempt to open a handle to validate the filter string syntax 30 | let mut win_divert = WinDivert::::network(filter, 0, WinDivertFlags::new().set_sniff()) 31 | .map_err(|e| FilterError::InvalidSyntax(format!("{}", e.to_string())))?; 32 | 33 | win_divert 34 | .close(CloseAction::Nothing) 35 | .map_err(|_| FilterError::InvalidSyntax("Failed to close handle.".into()))?; 36 | 37 | // Additional check: ensure any provided port numbers are valid 38 | let port_pattern = Regex::new(r"(tcp|udp)\.(SrcPort|DstPort)\s*==\s*(\d+)(?:$|\s)").unwrap(); 39 | for cap in port_pattern.captures_iter(filter) { 40 | if let Some(port_str) = cap.get(3) { 41 | port_str.as_str().parse::().map_err(|_| { 42 | FilterError::InvalidPort(format!( 43 | "Port number {} is out of range (0-65535)", 44 | port_str.as_str() 45 | )) 46 | })?; 47 | } 48 | } 49 | 50 | Ok(filter.to_string()) 51 | } -------------------------------------------------------------------------------- /src/network/modules/duplicate.rs: -------------------------------------------------------------------------------- 1 | use crate::network::core::packet_data::PacketData; 2 | use crate::network::modules::stats::duplicate_stats::DuplicateStats; 3 | use crate::network::types::probability::Probability; 4 | use rand::Rng; 5 | use std::vec::Vec; 6 | 7 | pub fn duplicate_packets( 8 | packets: &mut Vec, 9 | count: usize, 10 | probability: Probability, 11 | stats: &mut DuplicateStats, 12 | ) { 13 | let mut rng = rand::thread_rng(); 14 | let mut duplicate_packets = Vec::with_capacity(packets.len() * count); 15 | 16 | for packet_data in packets.iter() { 17 | if rng.random::() < probability.value() { 18 | for _ in 1..=count { 19 | duplicate_packets.push(PacketData::from(packet_data.packet.clone())); 20 | } 21 | stats.record(1 + count); 22 | } else { 23 | stats.record(1); 24 | } 25 | } 26 | packets.extend(duplicate_packets); 27 | } 28 | 29 | #[cfg(test)] 30 | mod tests { 31 | use crate::network::core::packet_data::PacketData; 32 | use crate::network::modules::duplicate::duplicate_packets; 33 | use crate::network::modules::stats::duplicate_stats::DuplicateStats; 34 | use crate::network::types::probability::Probability; 35 | use windivert::layer::NetworkLayer; 36 | use windivert::packet::WinDivertPacket; 37 | 38 | #[test] 39 | fn test_packet_duplication() { 40 | unsafe { 41 | let original_packets = vec![PacketData::from(WinDivertPacket::::new( 42 | vec![1, 2, 3], 43 | ))]; 44 | let original_len = original_packets.len(); 45 | let mut packets = original_packets.clone(); 46 | let mut stats = DuplicateStats::new(0.05); 47 | 48 | duplicate_packets(&mut packets, 3, Probability::new(1.0).unwrap(), &mut stats); 49 | 50 | // Ensure three times as many packets 51 | assert_eq!(packets.len(), original_len * 4); 52 | 53 | // Ensure data consistency 54 | for chunk in packets.chunks(original_len) { 55 | for packet_data in chunk.iter() { 56 | assert_eq!(packet_data.packet.data[..], [1, 2, 3]); 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/cli/tui/widgets/custom_widget.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::tui::traits::{DisplayName, HandleInput, IsActive, KeyBindings}; 2 | use crate::cli::tui::widgets::bandwidth_widget::BandwidthWidget; 3 | use crate::cli::tui::widgets::delay_widget::DelayWidget; 4 | use crate::cli::tui::widgets::drop_widget::DropWidget; 5 | use crate::cli::tui::widgets::duplicate_widget::DuplicateWidget; 6 | use crate::cli::tui::widgets::reorder_widget::ReorderWidget; 7 | use crate::cli::tui::widgets::tamper_widget::TamperWidget; 8 | use crate::cli::tui::widgets::throttle_widget::ThrottleWidget; 9 | use ratatui::buffer::Buffer; 10 | use ratatui::crossterm::event::KeyEvent; 11 | use ratatui::layout::Rect; 12 | use ratatui::widgets::Widget; 13 | 14 | pub enum CustomWidget<'a> { 15 | Drop(DropWidget<'a>), 16 | Delay(DelayWidget<'a>), 17 | Throttle(ThrottleWidget<'a>), 18 | Reorder(ReorderWidget<'a>), 19 | Tamper(TamperWidget<'a>), 20 | Duplicate(DuplicateWidget<'a>), 21 | Bandwidth(BandwidthWidget<'a>), 22 | } 23 | 24 | macro_rules! impl_widget_traits_for_enum { 25 | ($enum_name:ident, $($variant:ident),+) => { 26 | impl<'a> Widget for &mut $enum_name<'a> { 27 | fn render(mut self, area: Rect, buf: &mut Buffer) { 28 | match &mut self { 29 | $( $enum_name::$variant(ref mut widget) => widget.render(area, buf), )+ 30 | } 31 | } 32 | } 33 | 34 | impl<'a> HandleInput for $enum_name<'a> { 35 | fn handle_input(&mut self, key: KeyEvent) -> bool { 36 | match self { 37 | $( $enum_name::$variant(widget) => widget.handle_input(key), )+ 38 | } 39 | } 40 | } 41 | 42 | impl<'a> DisplayName for $enum_name<'a> { 43 | fn name(&self) -> &str { 44 | match self { 45 | $( $enum_name::$variant(ref widget) => widget.name(), )+ 46 | } 47 | } 48 | } 49 | 50 | impl<'a> KeyBindings for $enum_name<'a> { 51 | fn key_bindings(&self) -> String { 52 | match self { 53 | $( $enum_name::$variant(ref widget) => widget.key_bindings(), )+ 54 | } 55 | } 56 | } 57 | 58 | impl<'a> IsActive for $enum_name<'a> { 59 | fn is_active(&self) -> bool { 60 | match self { 61 | $( $enum_name::$variant(ref widget) => widget.is_active(), )+ 62 | } 63 | } 64 | 65 | fn set_active(&mut self, state: bool) { 66 | match self { 67 | $( $enum_name::$variant(ref mut widget) => widget.set_active(state), )+ 68 | } 69 | } 70 | } 71 | }; 72 | } 73 | 74 | impl_widget_traits_for_enum!( 75 | CustomWidget, 76 | Drop, 77 | Delay, 78 | Throttle, 79 | Reorder, 80 | Tamper, 81 | Duplicate, 82 | Bandwidth 83 | ); 84 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # configuration for https://github.com/orhun/git-cliff 2 | 3 | [changelog] 4 | # changelog header 5 | header = """ 6 | # Changelog\n 7 | All notable changes to this project will be documented in this file.\n 8 | """ 9 | # template for the changelog body 10 | # https://keats.github.io/tera/docs/#introduction 11 | body = """ 12 | {% if version %}\ 13 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 14 | {% else %}\ 15 | ## [unreleased] 16 | {% endif %}\ 17 | {% for group, commits in commits | group_by(attribute="group") %} 18 | ### {{ group | striptags | trim | upper_first }} 19 | {% for commit in commits %} 20 | - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ 21 | {% if commit.breaking %}[**breaking**] {% endif %}\ 22 | {{ commit.message | upper_first }}\ 23 | {% endfor %} 24 | {% endfor %}\n 25 | """ 26 | # remove the leading and trailing whitespace from the template 27 | trim = true 28 | # postprocessors 29 | postprocessors = [ 30 | { pattern = '', replace = "https://github.com/bornacvitanic/dev_environment_launcher" }, 31 | ] 32 | 33 | [git] 34 | # parse the commits based on your custom prefixes 35 | conventional_commits = false 36 | # filter out the commits that are not conventional 37 | filter_unconventional = false 38 | # process each line of a commit as an individual commit 39 | split_commits = false 40 | # regex for preprocessing the commit messages 41 | commit_preprocessors = [ 42 | { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))" }, 43 | ] 44 | # regex for parsing and grouping commits 45 | commit_parsers = [ 46 | { message = "(?i)\\bdocs?|documentation|\\.md\\b", group = "Documentation" }, 47 | { message = "(?i)\\bfix(ed|es|ing|s)?\\b", group = "Bug Fixes" }, 48 | { message = "(?i)test(s?)\\b", group = "Testing" }, 49 | { message = "(?i)^revert", group = "Revert" }, 50 | { message = "(?i)\\badd(ed|s)?\\b|(?i)\\bimplement(ed|s)?\\b", group = "Features" }, 51 | { message = "(?i)^remove", group = "Removals" }, 52 | { message = "(?i)\\brefactor(ed|s)?\\b", group = "Refactors" }, 53 | { message = "(?i)\\bmove(d|s)?\\b", group = "Moves" }, 54 | { message = "(?i)\\brename(d|s)?\\b", group = "Renames" }, 55 | { message = "(?i)^style|(?i)\\b(clean|clippy|fmt)\\b", group = "Styling" }, 56 | { message = "(?i)^chore|(?i)^ci", group = "Miscellaneous Tasks" }, 57 | { message = "(?i)^update|(?i)^improve", group = "Updates" }, 58 | ] 59 | # protect breaking changes from being skipped due to matching a skipping commit_parser 60 | protect_breaking_commits = false 61 | # filter out the commits that are not matched by commit parsers 62 | filter_commits = true 63 | # glob pattern for matching git tags 64 | tag_pattern = "v[0-9]*" 65 | # regex for skipping tags 66 | skip_tags = "beta|alpha" 67 | # regex for ignoring tags 68 | ignore_tags = "rc|lib" 69 | # sort the tags topologically 70 | topo_order = false 71 | # sort the commits inside sections by oldest/newest order 72 | sort_commits = "newest" -------------------------------------------------------------------------------- /src/cli/tui/widgets/filter_widget.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::tui::traits::KeyBindings; 2 | use crate::cli::tui::widgets::utils::block_ext::RoundedBlockExt; 3 | use crate::cli::tui::widgets::utils::style_textarea_based_on_validation; 4 | use crate::cli::tui::widgets::utils::textarea_ext::TextAreaExt; 5 | use crate::network::utils::filter::{validate_filter, FilterError}; 6 | use ratatui::buffer::Buffer; 7 | use ratatui::crossterm::event::{KeyCode, KeyEvent}; 8 | use ratatui::layout::Rect; 9 | use ratatui::style::Style; 10 | use ratatui::widgets::{Block, Widget}; 11 | use tui_textarea::TextArea; 12 | 13 | pub struct FilterWidget<'a> { 14 | textarea: TextArea<'a>, 15 | pub inputting: bool, 16 | pub filter: Result, 17 | validation_filter: Result, 18 | } 19 | 20 | impl Default for FilterWidget<'_> { 21 | fn default() -> Self { 22 | Self::new() 23 | } 24 | } 25 | 26 | impl FilterWidget<'_> { 27 | pub fn new() -> Self { 28 | FilterWidget { 29 | textarea: TextArea::default(), 30 | inputting: false, 31 | filter: Err(FilterError::InvalidSyntax("No filter provided".to_string())), 32 | validation_filter: Err(FilterError::InvalidSyntax("No filter provided".to_string())), 33 | } 34 | } 35 | 36 | pub fn set_filter(&mut self, filter: &str) { 37 | self.filter = validate_filter(filter); 38 | self.validation_filter = self.filter.clone(); 39 | if self.filter.is_ok() { 40 | self.textarea.set_text(filter) 41 | } 42 | } 43 | 44 | pub fn input(&mut self, key: KeyEvent) { 45 | if !self.inputting { 46 | if key.code == KeyCode::Char('f') { 47 | self.inputting = true; 48 | } 49 | } else { 50 | if let KeyCode::Esc = key.code { 51 | self.inputting = false; 52 | if let Ok(filter) = &self.filter { 53 | self.set_filter(&filter.to_string()); 54 | } 55 | 56 | return; 57 | } 58 | if let KeyCode::Enter = key.code { 59 | if let Ok(filter) = &self.validation_filter { 60 | self.set_filter(&filter.to_string()); 61 | } 62 | if self.validation_filter.is_ok() { 63 | self.inputting = false; 64 | } 65 | 66 | return; 67 | } 68 | if self.textarea.input(key) { 69 | self.validation_filter = validate_filter(&self.textarea.lines()[0]); 70 | } 71 | } 72 | } 73 | } 74 | 75 | impl KeyBindings for FilterWidget<'_> { 76 | fn key_bindings(&self) -> String { 77 | "Exit: Esc | Confirm: Enter".to_string() 78 | } 79 | } 80 | 81 | impl Widget for &mut FilterWidget<'_> { 82 | fn render(self, area: Rect, buf: &mut Buffer) 83 | where 84 | Self: Sized, 85 | { 86 | self.textarea.set_cursor_visibility(self.inputting); 87 | self.textarea.set_cursor_line_style(Style::default()); 88 | let mut text_area_block = Block::roundedt("[F]-Filter"); 89 | text_area_block = text_area_block.highlight_if(self.inputting); 90 | self.textarea.set_block(text_area_block); 91 | style_textarea_based_on_validation(&mut self.textarea, &self.validation_filter); 92 | self.textarea.render(area, buf); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/cli/config/config_options.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::settings::packet_manipulation::PacketManipulationSettings; 2 | use clap::Parser; 3 | use dirs::config_dir; 4 | use std::path::PathBuf; 5 | use std::{fs, io}; 6 | 7 | /// Manage configurations for fumble. 8 | #[derive(Parser, Debug, Default)] 9 | pub struct ConfigOptions { 10 | /// Command to create a default configuration file with the specified name. 11 | #[arg(long, help_heading = "Configuration Management")] 12 | pub create_default: Option, 13 | 14 | /// Command to use an existing configuration file based on specified name. 15 | #[arg(long, help_heading = "Configuration Management")] 16 | pub use_config: Option, 17 | 18 | /// Command to list all available configuration files. 19 | #[arg(long, help_heading = "Configuration Management")] 20 | pub list_configs: bool, 21 | } 22 | 23 | impl ConfigOptions { 24 | /// Create a default configuration file with all fields commented out. 25 | pub fn create_default_config(file_name: &str) -> io::Result<()> { 26 | ensure_config_dir_exists().unwrap(); 27 | PacketManipulationSettings::create_default_config_file( 28 | get_config_dir().join(ensure_toml_extension(file_name)), 29 | ) 30 | } 31 | 32 | /// List all configuration files in the specified directory. 33 | pub fn list_all_configs() -> io::Result> { 34 | let mut config_files = Vec::new(); 35 | ensure_config_dir_exists().unwrap(); 36 | for entry in fs::read_dir(get_config_dir())? { 37 | let entry = entry?; 38 | let path = entry.path(); 39 | if path.is_file() { 40 | if let Some(filename) = path.file_name() { 41 | if let Some(filename_str) = filename.to_str() { 42 | config_files.push( 43 | filename_str 44 | .strip_suffix(".toml") 45 | .unwrap_or(filename_str) 46 | .to_string(), 47 | ); 48 | } 49 | } 50 | } 51 | } 52 | Ok(config_files) 53 | } 54 | 55 | /// Load an existing configuration file. 56 | pub fn load_existing_config(file_name: &str) -> io::Result { 57 | ensure_config_dir_exists().unwrap(); 58 | PacketManipulationSettings::load_from_file( 59 | get_config_dir().join(ensure_toml_extension(file_name)), 60 | ) 61 | } 62 | } 63 | 64 | pub fn get_config_dir() -> PathBuf { 65 | let mut config_path = config_dir().unwrap_or_else(|| { 66 | // Fallback to home directory if config_dir() fails 67 | let mut home_dir = dirs::home_dir().expect("Could not find home directory"); 68 | home_dir.push(".fumble"); 69 | home_dir 70 | }); 71 | 72 | config_path.push("fumble"); 73 | config_path 74 | } 75 | 76 | pub fn ensure_config_dir_exists() -> io::Result<()> { 77 | let config_path = get_config_dir(); 78 | if !config_path.exists() { 79 | fs::create_dir_all(&config_path)?; 80 | } 81 | Ok(()) 82 | } 83 | 84 | pub fn ensure_toml_extension(file_name: &str) -> String { 85 | let mut path = PathBuf::from(file_name); 86 | if path.extension().map_or(true, |ext| ext != "toml") { 87 | path.set_extension("toml"); 88 | } 89 | path.to_string_lossy().to_string() 90 | } 91 | -------------------------------------------------------------------------------- /src/cli/settings/packet_manipulation.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::settings::bandwidth::BandwidthOptions; 2 | use crate::cli::settings::delay::DelayOptions; 3 | use crate::cli::settings::drop::DropOptions; 4 | use crate::cli::settings::duplicate::DuplicateOptions; 5 | use crate::cli::settings::reorder::ReorderOptions; 6 | use crate::cli::settings::tamper::TamperOptions; 7 | use crate::cli::settings::throttle::ThrottleOptions; 8 | use crate::cli::utils::serialization::serialize_option; 9 | use clap::Parser; 10 | use serde::{Deserialize, Serialize}; 11 | use std::io::Write; 12 | use std::path::Path; 13 | use std::{fs, io}; 14 | 15 | #[derive(Parser, Debug, Serialize, Deserialize, Default)] 16 | pub struct PacketManipulationSettings { 17 | #[command(flatten)] 18 | #[serde(serialize_with = "serialize_option")] 19 | pub drop: Option, 20 | 21 | #[command(flatten)] 22 | #[serde(default, serialize_with = "serialize_option")] 23 | pub delay: Option, 24 | 25 | #[command(flatten)] 26 | #[serde(serialize_with = "serialize_option")] 27 | pub throttle: Option, 28 | 29 | #[command(flatten)] 30 | #[serde(serialize_with = "serialize_option")] 31 | pub reorder: Option, 32 | 33 | #[command(flatten)] 34 | #[serde(serialize_with = "serialize_option")] 35 | pub tamper: Option, 36 | 37 | #[command(flatten)] 38 | #[serde(serialize_with = "serialize_option")] 39 | pub duplicate: Option, 40 | 41 | #[command(flatten)] 42 | #[serde(serialize_with = "serialize_option")] 43 | pub bandwidth: Option, 44 | } 45 | 46 | impl PacketManipulationSettings { 47 | /// Load configuration from a TOML file 48 | pub fn load_from_file>(path: P) -> io::Result { 49 | let content = fs::read_to_string(path)?; 50 | let config = 51 | toml::from_str(&content).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; 52 | Ok(config) 53 | } 54 | 55 | /// Save current configuration to a TOML file 56 | pub fn save_to_file>(&self, path: P) -> io::Result<()> { 57 | let content = toml::to_string_pretty(self) 58 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; 59 | let mut file = fs::File::create(path)?; 60 | file.write_all(content.as_bytes())?; 61 | Ok(()) 62 | } 63 | 64 | /// Create a default configuration file with all fields set to default values 65 | /// but commented out 66 | pub fn create_default_config_file>(path: P) -> io::Result<()> { 67 | let default_cli = Self::default(); 68 | 69 | // Serialize the default configuration to TOML 70 | let serialized = toml::to_string_pretty(&default_cli) 71 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; 72 | 73 | // Comment out all lines 74 | let commented_out = serialized 75 | .lines() 76 | .map(|line| { 77 | if line.trim().is_empty() || line.starts_with('[') { 78 | line.to_string() 79 | } else { 80 | format!("# {}", line) 81 | } 82 | }) 83 | .collect::>() 84 | .join("\n"); 85 | 86 | let mut file = fs::File::create(path)?; 87 | file.write_all(commented_out.as_bytes())?; 88 | Ok(()) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/cli/tui/input.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::tui::state::TuiState; 2 | use crate::cli::tui::traits::{HandleInput, IsActive}; 3 | use crate::cli::tui::ui::LayoutSection; 4 | use ratatui::crossterm::event; 5 | use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind}; 6 | use std::io; 7 | 8 | // Main input handler function 9 | pub fn handle_input(state: &mut TuiState) -> io::Result { 10 | if event::poll(std::time::Duration::from_millis(50))? { 11 | if let Event::Key(key) = event::read()? { 12 | if key.kind != KeyEventKind::Press { 13 | return Ok(false); 14 | } 15 | 16 | match state.focused { 17 | LayoutSection::Filter | LayoutSection::Logging => { 18 | if handle_widget_input(state, key) { 19 | return Ok(false); 20 | } 21 | } 22 | LayoutSection::Main => { 23 | if key.code == KeyCode::Char('p') { 24 | state.processing = !state.processing; 25 | return Ok(false); 26 | } 27 | // Handle section input 28 | if handle_section_input(state, key) { 29 | return Ok(false); 30 | } 31 | 32 | if handle_main_menu_input(state, key) { 33 | return Ok(true); 34 | } 35 | } 36 | } 37 | } 38 | } 39 | Ok(false) 40 | } 41 | 42 | // Function to handle input for sections 43 | fn handle_section_input(state: &mut TuiState, key: KeyEvent) -> bool { 44 | for (i, section) in state.sections.iter_mut().enumerate() { 45 | if i != state.selected { 46 | continue; 47 | } 48 | let handled = section.handle_input(key); 49 | if handled { 50 | state.interacting = Some(i); 51 | return true; 52 | } 53 | state.interacting = None; 54 | } 55 | false 56 | } 57 | 58 | // Function to handle input for widgets (filter and logs) 59 | fn handle_widget_input(state: &mut TuiState, key: KeyEvent) -> bool { 60 | if key.kind == KeyEventKind::Press { 61 | if state.filter_widget.inputting { 62 | state.filter_widget.input(key); 63 | return true; 64 | } else if state.logs_widget.focused { 65 | state.logs_widget.input(key); 66 | return true; 67 | } 68 | } 69 | false 70 | } 71 | 72 | // Function to handle main menu navigation and commands 73 | fn handle_main_menu_input(state: &mut TuiState, key: KeyEvent) -> bool { 74 | match key.code { 75 | KeyCode::Char('q') => return true, 76 | KeyCode::Up => { 77 | if state.selected > 0 { 78 | state.selected -= 1; 79 | } 80 | } 81 | KeyCode::Down => { 82 | if state.selected < state.sections.len() - 1 { 83 | state.selected += 1; 84 | } 85 | } 86 | KeyCode::Char(' ') => { 87 | let active_state = state.sections[state.selected].is_active(); 88 | state.sections[state.selected].set_active(!active_state); 89 | } 90 | KeyCode::Char(c) if c.is_numeric() => { 91 | for (i, _) in state.sections.iter().enumerate() { 92 | if c == char::from_digit((i + 1) as u32, 10).unwrap() { 93 | state.selected = i; 94 | break; 95 | } 96 | } 97 | } 98 | _ => {} 99 | } 100 | 101 | // Pass the key event to widgets if it's not handled by menu navigation 102 | state.filter_widget.input(key); 103 | state.logs_widget.input(key); 104 | false 105 | } -------------------------------------------------------------------------------- /src/network/modules/stats/util/ewma.rs: -------------------------------------------------------------------------------- 1 | /// A structure that computes the Exponentially Weighted Moving Average (EWMA) of a sequence of values. 2 | /// 3 | /// EWMA is a type of infinite impulse response filter that applies weighting factors which 4 | /// decrease exponentially. The weighting for each older datum decreases exponentially, never reaching zero. 5 | /// This is useful for smoothing out time series data and giving more weight to recent observations. 6 | /// 7 | /// # Fields 8 | /// 9 | /// * `alpha` - The smoothing factor, between 0 and 1. A higher value discounts older observations faster. 10 | /// * `current_value` - The current value of the EWMA after processing the latest input. 11 | /// Initially, this will be `None` until the first value is processed. 12 | /// 13 | /// # Example 14 | /// 15 | /// ```rust 16 | /// use fumble::network::modules::stats::util::ewma::Ewma; 17 | /// let mut ewma = Ewma::new(0.5); 18 | /// ewma.update(10.0); 19 | /// assert_eq!(ewma.get(), Some(10.0)); 20 | /// ewma.update(20.0); 21 | /// assert_eq!(ewma.get(), Some(15.0)); // 0.5 * 10.0 + 0.5 * 20.0 = 15.0 22 | /// ``` 23 | pub struct Ewma { 24 | alpha: f64, 25 | current_value: Option, 26 | } 27 | 28 | impl Ewma { 29 | /// Creates a new `Ewma` instance with the specified smoothing factor `alpha`. 30 | /// 31 | /// # Parameters 32 | /// 33 | /// * `alpha` - A smoothing factor between 0.0 (exclusive) and 1.0 (inclusive). 34 | /// Higher values give more weight to recent observations. 35 | /// 36 | /// # Panics 37 | /// 38 | /// This function will panic if `alpha` is not in the range `(0, 1]`. 39 | /// 40 | /// # Example 41 | /// 42 | /// ```rust 43 | /// use fumble::network::modules::stats::util::ewma::Ewma; 44 | /// let ewma = Ewma::new(0.3); 45 | /// ``` 46 | pub fn new(alpha: f64) -> Self { 47 | assert!( 48 | alpha > 0.0 && alpha <= 1.0, 49 | "Alpha should be between 0 and 1" 50 | ); 51 | Ewma { 52 | alpha, 53 | current_value: None, 54 | } 55 | } 56 | 57 | /// Updates the EWMA with a new value and returns the updated EWMA value. 58 | /// 59 | /// # Parameters 60 | /// 61 | /// * `new_value` - The new data point to be incorporated into the EWMA. 62 | /// 63 | /// # Returns 64 | /// 65 | /// The updated EWMA value after incorporating the `new_value`. 66 | /// 67 | /// # Example 68 | /// 69 | /// ```rust 70 | /// use fumble::network::modules::stats::util::ewma::Ewma; 71 | /// let mut ewma = Ewma::new(0.5); 72 | /// ewma.update(10.0); 73 | /// assert_eq!(ewma.get(), Some(10.0)); 74 | /// ewma.update(20.0); 75 | /// assert_eq!(ewma.get(), Some(15.0)); // 0.5 * 10.0 + 0.5 * 20.0 = 15.0 76 | /// ``` 77 | pub fn update(&mut self, new_value: f64) -> f64 { 78 | self.current_value = Some(match self.current_value { 79 | Some(current) => current * (1.0 - self.alpha) + new_value * self.alpha, 80 | None => new_value, // If no previous value exists, just set to new_value 81 | }); 82 | self.current_value.unwrap() 83 | } 84 | 85 | /// Retrieves the current EWMA value. 86 | /// 87 | /// # Returns 88 | /// 89 | /// An `Option` representing the current EWMA value. 90 | /// This will be `None` if `update` has not yet been called. 91 | /// 92 | /// # Example 93 | /// 94 | /// ```rust 95 | /// use fumble::network::modules::stats::util::ewma::Ewma; 96 | /// let mut ewma = Ewma::new(0.5); 97 | /// assert_eq!(ewma.get(), None); 98 | /// ewma.update(10.0); 99 | /// assert_eq!(ewma.get(), Some(10.0)); 100 | /// ``` 101 | pub fn get(&self) -> Option { 102 | self.current_value 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/network/processing/packet_receiving.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::Cli; 2 | use crate::network::core::packet_data::PacketData; 3 | use log::{debug, error}; 4 | use std::sync::atomic::{AtomicBool, Ordering}; 5 | use std::sync::{mpsc, Arc, Mutex}; 6 | use windivert::error::WinDivertError; 7 | use windivert::layer::NetworkLayer; 8 | use windivert::{CloseAction, WinDivert}; 9 | use windivert_sys::WinDivertFlags; 10 | 11 | pub fn receive_packets( 12 | packet_sender: mpsc::Sender>, 13 | running: Arc, 14 | cli: Arc>, 15 | ) -> Result<(), WinDivertError> { 16 | let mut buffer = vec![0u8; 1500]; 17 | let mut last_filter = String::new(); 18 | let mut wd: Option> = None; 19 | let mut logged_missing_handle = false; 20 | 21 | while running.load(Ordering::SeqCst) { 22 | // Check for filter updates 23 | let current_filter = match cli.lock() { 24 | Ok(cli) => cli.filter.clone().unwrap_or_default(), 25 | Err(_) => { 26 | error!("Failed to lock CLI for reading"); 27 | continue; 28 | } 29 | }; 30 | 31 | if current_filter != last_filter { 32 | // Filter changed, close the existing handle if it exists 33 | if let Some(ref mut wd_handle) = wd { 34 | if let Err(e) = wd_handle.close(CloseAction::Nothing) { 35 | error!("Failed to close existing WinDivert handle: {}", e); 36 | } else { 37 | debug!("Closed existing WinDivert handle"); 38 | } 39 | } 40 | 41 | // Open a new WinDivert handle with the new filter 42 | last_filter = current_filter.clone(); 43 | wd = match WinDivert::::network( 44 | ¤t_filter, 45 | 1, 46 | WinDivertFlags::set_recv_only(WinDivertFlags::new()), 47 | ) { 48 | Ok(handle) => { 49 | debug!( 50 | "WinDivert handle re-opened with new filter: {}", 51 | current_filter 52 | ); 53 | Some(handle) 54 | } 55 | Err(e) => { 56 | error!("Failed to initialize WinDivert: {}", e); 57 | None 58 | } 59 | }; 60 | } 61 | 62 | if let Some(ref wd_handle) = wd { 63 | logged_missing_handle = false; 64 | match wd_handle.recv(Some(&mut buffer)) { 65 | Ok(packet) => { 66 | let packet_data = PacketData::from(packet.into_owned()); 67 | if packet_sender.send(packet_data).is_err() { 68 | if should_shutdown(&running) { 69 | break; 70 | } else { 71 | error!("Failed to send packet data to main thread"); 72 | } 73 | } 74 | } 75 | Err(e) => { 76 | error!("Failed to receive packet: {}", e); 77 | if should_shutdown(&running) { 78 | break; 79 | } 80 | } 81 | } 82 | } else { 83 | if !logged_missing_handle { 84 | error!("WinDivert handle is not initialized. Skipping packet reception."); 85 | logged_missing_handle = true; 86 | } 87 | } 88 | } 89 | 90 | debug!("Shutting down packet receiving thread"); 91 | Ok(()) 92 | } 93 | 94 | fn should_shutdown(running: &Arc) -> bool { 95 | if !running.load(Ordering::SeqCst) { 96 | debug!("Packet receiving thread exiting due to shutdown signal."); 97 | return true; 98 | } 99 | false 100 | } -------------------------------------------------------------------------------- /src/cli/tui/widgets/delay_widget.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::tui::traits::{DisplayName, HandleInput, IsActive, KeyBindings}; 2 | use crate::cli::tui::widgets::utils::block_ext::RoundedBlockExt; 3 | use crate::cli::tui::widgets::utils::style_textarea_based_on_validation; 4 | use crate::cli::tui::widgets::utils::textarea_ext::TextAreaExt; 5 | use crate::cli::tui::widgets::utils::textarea_parsing::ParseFromTextArea; 6 | use crate::network::modules::stats::delay_stats::DelayStats; 7 | use ratatui::buffer::Buffer; 8 | use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; 9 | use ratatui::layout::{Constraint, Layout, Margin, Rect}; 10 | use ratatui::style::Style; 11 | use ratatui::widgets::{Block, Paragraph, Widget}; 12 | use tui_textarea::TextArea; 13 | 14 | pub struct DelayWidget<'a> { 15 | title: String, 16 | delay_duration: TextArea<'a>, 17 | is_active: bool, 18 | interacting: bool, 19 | pub delay: Result, 20 | delayed_packet_count: usize, 21 | } 22 | 23 | impl Default for DelayWidget<'_> { 24 | fn default() -> Self { 25 | Self::new() 26 | } 27 | } 28 | 29 | impl DelayWidget<'_> { 30 | pub fn new() -> Self { 31 | DelayWidget { 32 | title: "Delay".to_string(), 33 | delay_duration: TextArea::default(), 34 | is_active: false, 35 | interacting: false, 36 | delay: Ok(0), 37 | delayed_packet_count: 0, 38 | } 39 | } 40 | 41 | pub fn set_delay(&mut self, duration_ms: u64) { 42 | self.delay_duration.set_text(&duration_ms.to_string()); 43 | self.delay = Ok(duration_ms); 44 | } 45 | 46 | pub fn update_data(&mut self, stats: &DelayStats) { 47 | self.delayed_packet_count = stats.delayed_package_count; 48 | } 49 | } 50 | 51 | impl HandleInput for DelayWidget<'_> { 52 | fn handle_input(&mut self, key: KeyEvent) -> bool { 53 | if !self.interacting { 54 | if key.code == KeyCode::Enter && key.kind == KeyEventKind::Press { 55 | self.interacting = true; 56 | return true; 57 | } 58 | } else { 59 | if let KeyCode::Enter | KeyCode::Esc = key.code { 60 | self.interacting = false; 61 | return false; 62 | } 63 | if self.delay_duration.input(key) { 64 | self.delay = u64::parse_from_text_area(&self.delay_duration); 65 | } 66 | return true; 67 | } 68 | false 69 | } 70 | } 71 | 72 | impl DisplayName for DelayWidget<'_> { 73 | fn name(&self) -> &str { 74 | &self.title 75 | } 76 | } 77 | 78 | impl KeyBindings for DelayWidget<'_> { 79 | fn key_bindings(&self) -> String { 80 | "Exit: Esc".to_string() 81 | } 82 | } 83 | 84 | impl IsActive for DelayWidget<'_> { 85 | fn is_active(&self) -> bool { 86 | self.is_active 87 | } 88 | 89 | fn set_active(&mut self, state: bool) { 90 | self.is_active = state; 91 | } 92 | } 93 | 94 | impl Widget for &mut DelayWidget<'_> { 95 | fn render(self, area: Rect, buf: &mut Buffer) 96 | where 97 | Self: Sized, 98 | { 99 | let [delay_duration_area, info_area] = 100 | Layout::horizontal([Constraint::Max(10), Constraint::Min(25)]).areas(area.inner( 101 | Margin { 102 | horizontal: 1, 103 | vertical: 1, 104 | }, 105 | )); 106 | 107 | self.delay_duration.set_cursor_visibility(self.interacting); 108 | self.delay_duration.set_dim_placeholder("50"); 109 | self.delay_duration.set_cursor_line_style(Style::default()); 110 | self.delay_duration 111 | .set_block(Block::roundedt("Duration").highlight_if(self.interacting)); 112 | if !self.delay_duration.lines()[0].is_empty() { 113 | style_textarea_based_on_validation(&mut self.delay_duration, &self.delay); 114 | } 115 | self.delay_duration.render(delay_duration_area, buf); 116 | 117 | let [delay_count_info, _excess_info] = 118 | Layout::horizontal([Constraint::Max(30), Constraint::Fill(1)]).areas(info_area); 119 | Paragraph::new(format!("{} packets", self.delayed_packet_count)) 120 | .block(Block::bordered().title("Delayed packets")) 121 | .render(delay_count_info, buf); 122 | } 123 | } -------------------------------------------------------------------------------- /src/cli/tui/widgets/bandwidth_widget.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::tui::traits::{DisplayName, HandleInput, IsActive, KeyBindings}; 2 | use crate::cli::tui::widgets::utils::block_ext::RoundedBlockExt; 3 | use crate::cli::tui::widgets::utils::style_textarea_based_on_validation; 4 | use crate::cli::tui::widgets::utils::textarea_ext::TextAreaExt; 5 | use crate::cli::tui::widgets::utils::textarea_parsing::ParseFromTextArea; 6 | use crate::network::modules::stats::bandwidth_stats::BandwidthStats; 7 | use ratatui::buffer::Buffer; 8 | use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; 9 | use ratatui::layout::{Constraint, Layout, Margin, Rect}; 10 | use ratatui::style::Style; 11 | use ratatui::widgets::{Block, Paragraph, Widget}; 12 | use tui_textarea::TextArea; 13 | 14 | pub struct BandwidthWidget<'a> { 15 | title: String, 16 | limit_text_area: TextArea<'a>, 17 | is_active: bool, 18 | interacting: bool, 19 | pub limit: Result, 20 | throughput: f64, 21 | stored_packet_count: usize, 22 | } 23 | 24 | impl Default for BandwidthWidget<'_> { 25 | fn default() -> Self { 26 | Self::new() 27 | } 28 | } 29 | 30 | impl BandwidthWidget<'_> { 31 | pub fn new() -> Self { 32 | BandwidthWidget { 33 | title: "Bandwidth".to_string(), 34 | limit_text_area: TextArea::default(), 35 | is_active: false, 36 | interacting: false, 37 | limit: Ok(0), 38 | throughput: 0.0, 39 | stored_packet_count: 0, 40 | } 41 | } 42 | 43 | pub fn set_limit(&mut self, limit: usize) { 44 | self.limit_text_area.set_text(&limit.to_string()); 45 | self.limit = Ok(limit); 46 | } 47 | 48 | pub(crate) fn update_data(&mut self, stats: &BandwidthStats) { 49 | self.throughput = stats.recent_throughput(); 50 | self.stored_packet_count = stats.storage_packet_count; 51 | } 52 | } 53 | 54 | impl HandleInput for BandwidthWidget<'_> { 55 | fn handle_input(&mut self, key: KeyEvent) -> bool { 56 | if !self.interacting { 57 | if key.code == KeyCode::Enter && key.kind == KeyEventKind::Press { 58 | self.interacting = true; 59 | return true; 60 | } 61 | } else { 62 | if let KeyCode::Enter | KeyCode::Esc = key.code { 63 | self.interacting = false; 64 | return false; 65 | } 66 | if self.limit_text_area.input(key) { 67 | self.limit = usize::parse_from_text_area(&self.limit_text_area); 68 | } 69 | return true; 70 | } 71 | false 72 | } 73 | } 74 | 75 | impl DisplayName for BandwidthWidget<'_> { 76 | fn name(&self) -> &str { 77 | &self.title 78 | } 79 | } 80 | 81 | impl KeyBindings for BandwidthWidget<'_> { 82 | fn key_bindings(&self) -> String { 83 | "Exit: Esc".to_string() 84 | } 85 | } 86 | 87 | impl IsActive for BandwidthWidget<'_> { 88 | fn is_active(&self) -> bool { 89 | self.is_active 90 | } 91 | 92 | fn set_active(&mut self, state: bool) { 93 | self.is_active = state; 94 | } 95 | } 96 | 97 | impl Widget for &mut BandwidthWidget<'_> { 98 | fn render(self, area: Rect, buf: &mut Buffer) 99 | where 100 | Self: Sized, 101 | { 102 | let [delay_duration_area, info_area] = 103 | Layout::horizontal([Constraint::Max(15), Constraint::Min(25)]).areas(area.inner( 104 | Margin { 105 | horizontal: 1, 106 | vertical: 1, 107 | }, 108 | )); 109 | 110 | self.limit_text_area.set_cursor_visibility(self.interacting); 111 | self.limit_text_area.set_dim_placeholder("No limit"); 112 | self.limit_text_area.set_cursor_line_style(Style::default()); 113 | self.limit_text_area 114 | .set_block(Block::roundedt("KBps Limit").highlight_if(self.interacting)); 115 | if !self.limit_text_area.lines()[0].is_empty() { 116 | style_textarea_based_on_validation(&mut self.limit_text_area, &self.limit); 117 | } 118 | self.limit_text_area.render(delay_duration_area, buf); 119 | 120 | let [throughput_info, storage_packet_count_info, _excess_info] = Layout::horizontal([ 121 | Constraint::Max(15), 122 | Constraint::Max(15), 123 | Constraint::Fill(1), 124 | ]) 125 | .areas(info_area); 126 | Paragraph::new(format!("{:.2} KBps", self.throughput)) 127 | .block(Block::bordered().title("Throughput")) 128 | .render(throughput_info, buf); 129 | Paragraph::new(format!("{}", self.stored_packet_count)) 130 | .block(Block::bordered().title("Stored packets")) 131 | .render(storage_packet_count_info, buf); 132 | } 133 | } -------------------------------------------------------------------------------- /src/cli/tui/custom_logger.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use log::{Level, LevelFilter, Metadata, Record, SetLoggerError}; 3 | use std::sync::Mutex; 4 | use std::time::{SystemTime, UNIX_EPOCH}; 5 | 6 | #[derive(Clone)] 7 | pub struct LogEntry { 8 | pub level: Level, 9 | pub timestamp: String, 10 | pub module_path: Option, 11 | pub message: String, 12 | } 13 | 14 | impl LogEntry { 15 | fn new(level: Level, message: String, module_path: Option<&str>) -> Self { 16 | let timestamp = current_utc_timestamp(); 17 | 18 | Self { 19 | level, 20 | timestamp, 21 | module_path: module_path.map(|s| s.to_string()), 22 | message, 23 | } 24 | } 25 | 26 | fn colored_level(&self) -> String { 27 | match self.level { 28 | Level::Error => format!("\x1b[31m{}\x1b[0m", self.level), // Red 29 | Level::Warn => format!("\x1b[33m{}\x1b[0m", self.level), // Yellow 30 | Level::Info => format!("\x1b[32m{}\x1b[0m", self.level), // Green 31 | Level::Debug => format!("\x1b[34m{}\x1b[0m", self.level), // Blue 32 | Level::Trace => format!("\x1b[35m{}\x1b[0m", self.level), // Magenta 33 | } 34 | } 35 | 36 | fn formatted_log(&self) -> String { 37 | format!( 38 | "[{} {} {}] {}", 39 | self.timestamp, 40 | self.colored_level(), 41 | self.module_path.as_deref().unwrap_or("unknown"), 42 | self.message 43 | ) 44 | } 45 | } 46 | 47 | // Function to generate a UTC timestamp in the desired format 48 | fn current_utc_timestamp() -> String { 49 | let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); 50 | let seconds = now.as_secs(); 51 | let nanos = now.subsec_nanos(); 52 | 53 | // Convert to a human-readable timestamp (ISO 8601-like) 54 | let datetime = seconds_to_datetime(seconds); 55 | 56 | format!("{}.{:02}Z", datetime, nanos / 10_000_000) 57 | } 58 | 59 | // Function to convert seconds since UNIX_EPOCH to a date-time string 60 | fn seconds_to_datetime(seconds: u64) -> String { 61 | let days = seconds / 86400; 62 | let seconds_in_day = seconds % 86400; 63 | 64 | let hours = seconds_in_day / 3600; 65 | let minutes = (seconds_in_day % 3600) / 60; 66 | let seconds = seconds_in_day % 60; 67 | 68 | let year = 1970 + days / 365; 69 | let day_of_year = days % 365; 70 | 71 | format!( 72 | "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}", 73 | year, 74 | day_of_year / 30 + 1, 75 | day_of_year % 30 + 1, 76 | hours, 77 | minutes, 78 | seconds 79 | ) 80 | } 81 | 82 | pub struct LogBuffer { 83 | buffer: Mutex>, 84 | console_logging: Mutex, 85 | } 86 | 87 | impl LogBuffer { 88 | fn new() -> LogBuffer { 89 | LogBuffer { 90 | buffer: Mutex::new(Vec::new()), 91 | console_logging: Mutex::new(false), 92 | } 93 | } 94 | 95 | fn log(&self, record: &Record) { 96 | let module_path = record.module_path(); 97 | let mut buffer = self.buffer.lock().unwrap(); 98 | let entry = LogEntry::new(record.level(), record.args().to_string(), module_path); 99 | 100 | buffer.push(entry.clone()); 101 | 102 | if *self.console_logging.lock().unwrap() { 103 | eprintln!("{}", entry.formatted_log()); 104 | } 105 | } 106 | 107 | pub(crate) fn get_logs(&self) -> Vec { 108 | let buffer = self.buffer.lock().unwrap(); 109 | buffer.clone() 110 | } 111 | } 112 | 113 | lazy_static! { 114 | pub static ref LOG_BUFFER: LogBuffer = LogBuffer::new(); 115 | } 116 | 117 | struct SimpleLogger; 118 | 119 | impl log::Log for SimpleLogger { 120 | fn enabled(&self, metadata: &Metadata) -> bool { 121 | metadata.level() <= log::max_level() 122 | } 123 | 124 | fn log(&self, record: &Record) { 125 | if self.enabled(record.metadata()) { 126 | LOG_BUFFER.log(record); 127 | } 128 | } 129 | 130 | fn flush(&self) {} 131 | } 132 | 133 | static LOGGER: SimpleLogger = SimpleLogger; 134 | 135 | pub fn init_logger() -> Result<(), SetLoggerError> { 136 | let env_logger = 137 | env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).build(); 138 | 139 | // Set our custom logger, which also uses env_logger under the hood 140 | log::set_logger(&LOGGER).map(|()| { 141 | // Sync the max level with env_logger 142 | log::set_max_level(env_logger.filter()); 143 | }) 144 | } 145 | 146 | pub fn set_logger_level_filter(level_filter: LevelFilter) { 147 | log::set_max_level(level_filter); 148 | } 149 | 150 | pub fn set_logger_console_state(state: bool) { 151 | let mut console_logging = LOG_BUFFER.console_logging.lock().unwrap(); 152 | *console_logging = state; 153 | } 154 | -------------------------------------------------------------------------------- /src/cli/tui/widgets/drop_widget.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::tui::traits::{DisplayName, HandleInput, IsActive, KeyBindings}; 2 | use crate::cli::tui::widgets::utils::block_ext::RoundedBlockExt; 3 | use crate::cli::tui::widgets::utils::style_textarea_based_on_validation; 4 | use crate::cli::tui::widgets::utils::textarea_ext::TextAreaExt; 5 | use crate::cli::tui::widgets::utils::textarea_parsing::ParseFromTextArea; 6 | use crate::network::modules::stats::drop_stats::DropStats; 7 | use crate::network::types::probability::Probability; 8 | use ratatui::buffer::Buffer; 9 | use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; 10 | use ratatui::layout::{Constraint, Layout, Margin, Rect}; 11 | use ratatui::style::Style; 12 | use ratatui::widgets::{Block, Paragraph, Widget}; 13 | use tui_textarea::TextArea; 14 | 15 | pub struct DropWidget<'a> { 16 | title: String, 17 | probability_text_area: TextArea<'a>, 18 | is_active: bool, 19 | interacting: bool, 20 | pub probability: Result, 21 | drop_rate: f64, 22 | dropped_packets: usize, 23 | total_packets: usize, 24 | } 25 | 26 | impl Default for DropWidget<'_> { 27 | fn default() -> Self { 28 | Self::new() 29 | } 30 | } 31 | 32 | impl DropWidget<'_> { 33 | pub fn new() -> Self { 34 | DropWidget { 35 | title: "Drop".to_string(), 36 | probability_text_area: TextArea::default(), 37 | is_active: false, 38 | interacting: false, 39 | probability: Ok(Probability::default()), 40 | drop_rate: 0.0, 41 | dropped_packets: 0, 42 | total_packets: 0, 43 | } 44 | } 45 | 46 | pub fn set_probability(&mut self, probability: Probability) { 47 | self.probability_text_area 48 | .set_text(&probability.to_string()); 49 | self.probability = Ok(probability); 50 | } 51 | 52 | pub fn update_data(&mut self, stats: &DropStats) { 53 | self.drop_rate = stats.recent_drop_rate(); 54 | self.dropped_packets = stats.total_dropped; 55 | self.total_packets = stats.total_packets; 56 | } 57 | } 58 | 59 | impl HandleInput for DropWidget<'_> { 60 | fn handle_input(&mut self, key: KeyEvent) -> bool { 61 | if !self.interacting { 62 | if key.code == KeyCode::Enter && key.kind == KeyEventKind::Press { 63 | self.interacting = true; 64 | return true; 65 | } 66 | } else { 67 | if let KeyCode::Enter | KeyCode::Esc = key.code { 68 | self.interacting = false; 69 | return false; 70 | } 71 | if self.probability_text_area.input(key) { 72 | self.probability = Probability::parse_from_text_area(&self.probability_text_area); 73 | } 74 | return true; 75 | } 76 | false 77 | } 78 | } 79 | 80 | impl DisplayName for DropWidget<'_> { 81 | fn name(&self) -> &str { 82 | &self.title 83 | } 84 | } 85 | 86 | impl KeyBindings for DropWidget<'_> { 87 | fn key_bindings(&self) -> String { 88 | "Exit: Esc".to_string() 89 | } 90 | } 91 | 92 | impl IsActive for DropWidget<'_> { 93 | fn is_active(&self) -> bool { 94 | self.is_active 95 | } 96 | 97 | fn set_active(&mut self, state: bool) { 98 | self.is_active = state; 99 | } 100 | } 101 | 102 | impl Widget for &mut DropWidget<'_> { 103 | fn render(self, area: Rect, buf: &mut Buffer) 104 | where 105 | Self: Sized, 106 | { 107 | let [drop_probability_area, info_area] = 108 | Layout::horizontal([Constraint::Max(10), Constraint::Min(25)]).areas(area.inner( 109 | Margin { 110 | horizontal: 1, 111 | vertical: 1, 112 | }, 113 | )); 114 | 115 | self.probability_text_area 116 | .set_cursor_visibility(self.interacting); 117 | self.probability_text_area.set_dim_placeholder("0.1"); 118 | self.probability_text_area 119 | .set_cursor_line_style(Style::default()); 120 | self.probability_text_area 121 | .set_block(Block::roundedt("Probability").highlight_if(self.interacting)); 122 | if !self.probability_text_area.lines()[0].is_empty() { 123 | style_textarea_based_on_validation(&mut self.probability_text_area, &self.probability); 124 | } 125 | self.probability_text_area 126 | .render(drop_probability_area, buf); 127 | 128 | let [drop_rate_info, drop_count_info, _excess_info] = Layout::horizontal([ 129 | Constraint::Max(12), 130 | Constraint::Max(18), 131 | Constraint::Fill(1), 132 | ]) 133 | .areas(info_area); 134 | Paragraph::new(format!("{:.2}%", self.drop_rate * 100.0)) 135 | .block(Block::bordered().title("Drop rate")) 136 | .render(drop_rate_info, buf); 137 | Paragraph::new(format!("{}/{}", self.dropped_packets, self.total_packets)) 138 | .right_aligned() 139 | .block(Block::bordered().title("Drop count")) 140 | .render(drop_count_info, buf); 141 | } 142 | } -------------------------------------------------------------------------------- /src/cli/tui/widgets/logs_widget.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::tui::custom_logger::{set_logger_level_filter, LogEntry, LOG_BUFFER}; 2 | use crate::cli::tui::traits::KeyBindings; 3 | use crate::cli::tui::widgets::utils::block_ext::RoundedBlockExt; 4 | use log::{debug, error, info, trace, warn, Level, LevelFilter}; 5 | use ratatui::buffer::Buffer; 6 | use ratatui::crossterm::event::{KeyCode, KeyEvent}; 7 | use ratatui::layout::Rect; 8 | use ratatui::prelude::{Line, Span, Style}; 9 | use ratatui::style::{Color, Modifier, Stylize}; 10 | use ratatui::widgets::{Block, Borders, List, ListItem, Widget}; 11 | 12 | pub struct LogsWidget { 13 | pub(crate) open: bool, 14 | pub(crate) focused: bool, 15 | } 16 | 17 | impl Default for LogsWidget { 18 | fn default() -> Self { 19 | Self::new() 20 | } 21 | } 22 | 23 | impl LogsWidget { 24 | pub fn new() -> Self { 25 | LogsWidget { 26 | open: false, 27 | focused: false, 28 | } 29 | } 30 | pub fn input(&mut self, key: KeyEvent) { 31 | if !self.focused { 32 | if KeyCode::Char('l') == key.code { 33 | self.open = true; 34 | self.focused = true; 35 | } 36 | } else { 37 | if KeyCode::Char('l') == key.code { 38 | self.open = false; 39 | self.focused = false; 40 | return; 41 | } 42 | if let KeyCode::Esc = key.code { 43 | self.focused = false; 44 | return; 45 | } 46 | Self::change_log_level(key); 47 | } 48 | } 49 | 50 | fn change_log_level(key: KeyEvent) { 51 | match key.code { 52 | KeyCode::Char('t') => { 53 | set_logger_level_filter(LevelFilter::Trace); 54 | trace!("Logging level set to trace.") 55 | } 56 | KeyCode::Char('d') => { 57 | set_logger_level_filter(LevelFilter::Debug); 58 | debug!("Logging level set to debug.") 59 | } 60 | KeyCode::Char('i') => { 61 | set_logger_level_filter(LevelFilter::Info); 62 | info!("Logging level set to info.") 63 | } 64 | KeyCode::Char('w') => { 65 | set_logger_level_filter(LevelFilter::Warn); 66 | warn!("Logging level set to warning.") 67 | } 68 | KeyCode::Char('e') => { 69 | set_logger_level_filter(LevelFilter::Error); 70 | error!("Logging level set to error.") 71 | } 72 | _ => {} 73 | } 74 | } 75 | } 76 | 77 | impl KeyBindings for LogsWidget { 78 | fn key_bindings(&self) -> String { 79 | "Exit: Esc | Trace: t | Debug: d | Info: i | Warn: w | Error: e".to_string() 80 | } 81 | } 82 | 83 | impl Widget for &mut LogsWidget { 84 | fn render(self, area: Rect, buf: &mut Buffer) 85 | where 86 | Self: Sized, 87 | { 88 | if self.open { 89 | let log_display_height = area.height.saturating_sub(2) as usize; 90 | let logs = LOG_BUFFER.get_logs(); 91 | let start = logs.len().saturating_sub(log_display_height); 92 | let recent_logs = &logs[start..]; 93 | let items: Vec = format_logs_for_tui(recent_logs); 94 | let mut logging_area_block = Block::bordered().title("[L]-Logs"); 95 | logging_area_block = logging_area_block.highlight_if(self.focused); 96 | let list = List::new(items) 97 | .add_modifier(Modifier::ITALIC) 98 | .block(logging_area_block); 99 | list.render(area, buf); 100 | } else { 101 | Block::bordered() 102 | .borders(Borders::TOP) 103 | .title("[L]-Logs") 104 | .render(area, buf) 105 | } 106 | } 107 | } 108 | 109 | fn format_logs_for_tui(logs: &[LogEntry]) -> Vec { 110 | logs.iter() 111 | .map(|log| { 112 | let color = match log.level { 113 | Level::Error => Color::Red, 114 | Level::Warn => Color::Yellow, 115 | Level::Info => Color::Green, 116 | Level::Debug => Color::Blue, 117 | Level::Trace => Color::Cyan, 118 | }; 119 | 120 | let timestamp_span = 121 | if matches!(log::max_level(), LevelFilter::Debug | LevelFilter::Trace) { 122 | Span::styled( 123 | format!("[{} ", log.timestamp), 124 | Style::default().fg(Color::DarkGray), 125 | ) 126 | } else { 127 | Span::raw(String::new()) 128 | }; 129 | 130 | let level_span = Span::styled(format!("{}", log.level), Style::default().fg(color)); 131 | 132 | let module_path_span = 133 | if matches!(log::max_level(), LevelFilter::Debug | LevelFilter::Trace) { 134 | log.module_path.as_ref().map_or(Span::raw(" "), |module| { 135 | Span::styled( 136 | format!(" {}] ", module), 137 | Style::default().fg(Color::DarkGray), 138 | ) 139 | }) 140 | } else { 141 | Span::raw(String::new()) 142 | }; 143 | 144 | let message_span = Span::raw(&log.message); 145 | 146 | let separator_span = Span::raw(" "); 147 | 148 | ListItem::new(Line::default().spans(vec![ 149 | timestamp_span, 150 | level_span, 151 | module_path_span, 152 | separator_span, 153 | message_span, 154 | ])) 155 | }) 156 | .collect() 157 | } 158 | -------------------------------------------------------------------------------- /src/network/processing/packet_processing.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::settings::packet_manipulation::PacketManipulationSettings; 2 | use crate::cli::Cli; 3 | use crate::network::core::packet_data::PacketData; 4 | use crate::network::modules::bandwidth::bandwidth_limiter; 5 | use crate::network::modules::delay::delay_packets; 6 | use crate::network::modules::drop::drop_packets; 7 | use crate::network::modules::duplicate::duplicate_packets; 8 | use crate::network::modules::reorder::reorder_packets; 9 | use crate::network::modules::stats::PacketProcessingStatistics; 10 | use crate::network::modules::tamper::tamper_packets; 11 | use crate::network::modules::throttle::throttle_packages; 12 | use crate::network::processing::packet_processing_state::PacketProcessingState; 13 | use crate::utils::log_statistics; 14 | use log::{error, info}; 15 | use std::collections::{BinaryHeap, VecDeque}; 16 | use std::sync::atomic::{AtomicBool, Ordering}; 17 | use std::sync::mpsc::Receiver; 18 | use std::sync::{Arc, Mutex, RwLock}; 19 | use std::time::{Duration, Instant}; 20 | use windivert::error::WinDivertError; 21 | use windivert::layer::NetworkLayer; 22 | use windivert::WinDivert; 23 | use windivert_sys::WinDivertFlags; 24 | 25 | pub fn start_packet_processing( 26 | cli: Arc>, 27 | packet_receiver: Receiver, 28 | running: Arc, 29 | statistics: Arc>, 30 | ) -> Result<(), WinDivertError> { 31 | let wd = WinDivert::::network( 32 | "false", 33 | 0, 34 | WinDivertFlags::set_send_only(WinDivertFlags::new()), 35 | ) 36 | .map_err(|e| { 37 | error!("Failed to initialize WinDiver: {}", e); 38 | e 39 | })?; 40 | 41 | let log_interval = Duration::from_secs(2); 42 | let mut last_log_time = Instant::now(); 43 | 44 | let mut received_packet_count = 0; 45 | let mut sent_packet_count = 0; 46 | 47 | let mut state = PacketProcessingState { 48 | delay_storage: VecDeque::new(), 49 | throttle_storage: VecDeque::new(), 50 | bandwidth_limit_storage: VecDeque::new(), 51 | bandwidth_storage_total_size: 0, 52 | reorder_storage: BinaryHeap::new(), 53 | throttled_start_time: Instant::now(), 54 | last_sent_package_time: Instant::now(), 55 | }; 56 | 57 | info!("Starting packet interception."); 58 | while running.load(Ordering::SeqCst) { 59 | let mut packets = Vec::new(); 60 | // Try to receive packets from the channel 61 | while let Ok(packet_data) = packet_receiver.try_recv() { 62 | packets.push(packet_data); 63 | received_packet_count += 1; 64 | } 65 | 66 | if let Ok(cli) = cli.lock() { 67 | process_packets( 68 | &cli.packet_manipulation_settings, 69 | &mut packets, 70 | &mut state, 71 | &statistics, 72 | ); 73 | } 74 | 75 | for packet_data in &packets { 76 | wd.send(&packet_data.packet).map_err(|e| { 77 | error!("Failed to send packet: {}", e); 78 | e 79 | })?; 80 | sent_packet_count += 1; 81 | } 82 | 83 | // Periodically log the statistics 84 | if last_log_time.elapsed() >= log_interval && cli.lock().unwrap().filter.is_some() { 85 | log_statistics(received_packet_count, sent_packet_count); 86 | received_packet_count = 0; 87 | sent_packet_count = 0; 88 | last_log_time = Instant::now(); // Reset the timer 89 | } 90 | } 91 | 92 | Ok(()) 93 | } 94 | 95 | pub fn process_packets<'a>( 96 | settings: &PacketManipulationSettings, 97 | packets: &mut Vec>, 98 | state: &mut PacketProcessingState<'a>, 99 | statistics: &Arc>, 100 | ) { 101 | if let Some(drop) = &settings.drop { 102 | drop_packets( 103 | packets, 104 | drop.probability, 105 | &mut statistics.write().unwrap().drop_stats, 106 | ); 107 | } 108 | 109 | if let Some(delay) = &settings.delay { 110 | delay_packets( 111 | packets, 112 | &mut state.delay_storage, 113 | Duration::from_millis(delay.duration), 114 | &mut statistics.write().unwrap().delay_stats, 115 | ); 116 | } 117 | 118 | if let Some(throttle) = &settings.throttle { 119 | throttle_packages( 120 | packets, 121 | &mut state.throttle_storage, 122 | &mut state.throttled_start_time, 123 | throttle.probability, 124 | Duration::from_millis(throttle.duration), 125 | throttle.drop, 126 | &mut statistics.write().unwrap().throttle_stats, 127 | ); 128 | } 129 | 130 | if let Some(reorder) = &settings.reorder { 131 | reorder_packets( 132 | packets, 133 | &mut state.reorder_storage, 134 | reorder.probability, 135 | Duration::from_millis(reorder.max_delay), 136 | &mut statistics.write().unwrap().reorder_stats, 137 | ); 138 | } 139 | 140 | if let Some(tamper) = &settings.tamper { 141 | tamper_packets( 142 | packets, 143 | tamper.probability, 144 | tamper.amount, 145 | tamper.recalculate_checksums.unwrap_or(true), 146 | &mut statistics.write().unwrap().tamper_stats, 147 | ); 148 | } 149 | 150 | if let Some(duplicate) = &settings.duplicate { 151 | if duplicate.count > 1 && duplicate.probability.value() > 0.0 { 152 | duplicate_packets( 153 | packets, 154 | duplicate.count, 155 | duplicate.probability, 156 | &mut statistics.write().unwrap().duplicate_stats, 157 | ); 158 | } 159 | } 160 | 161 | if let Some(bandwidth) = &settings.bandwidth { 162 | bandwidth_limiter( 163 | packets, 164 | &mut state.bandwidth_limit_storage, 165 | &mut state.bandwidth_storage_total_size, 166 | &mut state.last_sent_package_time, 167 | bandwidth.limit, 168 | &mut statistics.write().unwrap().bandwidth_stats, 169 | ); 170 | } 171 | } -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use env_logger::Env; 3 | use fumble::cli::config::config_options::ConfigOptions; 4 | use fumble::cli::tui::cli_ext::{CliExt, TuiStateExt}; 5 | use fumble::cli::tui::custom_logger::{init_logger, set_logger_console_state}; 6 | use fumble::cli::tui::state::TuiState; 7 | use fumble::cli::tui::terminal::TerminalManager; 8 | use fumble::cli::tui::{input, ui}; 9 | use fumble::cli::utils::logging::log_initialization_info; 10 | use fumble::cli::Cli; 11 | use fumble::network::modules::stats::{initialize_statistics, PacketProcessingStatistics}; 12 | use fumble::network::processing::packet_processing::start_packet_processing; 13 | use fumble::network::processing::packet_receiving::receive_packets; 14 | use log::{debug, error, info}; 15 | use std::process::exit; 16 | use std::sync::atomic::{AtomicBool, Ordering}; 17 | use std::sync::{mpsc, Arc, Mutex, RwLock}; 18 | use std::thread; 19 | use std::thread::JoinHandle; 20 | use windivert::error::WinDivertError; 21 | 22 | fn main() -> Result<(), WinDivertError> { 23 | let mut cli = Cli::parse(); 24 | 25 | let mut should_start_tui = false; 26 | if cli.tui { 27 | should_start_tui = true; 28 | init_logger().expect("Failed to init logger.") 29 | } else { 30 | initialize_logging(); 31 | } 32 | 33 | debug!("Parsed CLI arguments: {:?}", &cli); 34 | 35 | if let Some(file_name) = &cli.config.create_default { 36 | // Create a default config file and exit 37 | ConfigOptions::create_default_config(file_name)?; 38 | info!( 39 | "Default configuration file created with name {:?}", 40 | file_name 41 | ); 42 | return Ok(()); 43 | } 44 | 45 | if cli.config.list_configs { 46 | // List all config files in the current directory 47 | match ConfigOptions::list_all_configs() { 48 | Ok(configs) => { 49 | for config in configs { 50 | println!("{}", config); 51 | } 52 | } 53 | Err(e) => error!("Failed to list configs: {}", e), 54 | } 55 | return Ok(()); 56 | } 57 | 58 | // Load configuration from file if specified 59 | if let Some(file_name) = &cli.config.use_config { 60 | let loaded_settings = ConfigOptions::load_existing_config(file_name)?; 61 | cli.packet_manipulation_settings = loaded_settings; 62 | info!("Loaded configuration from {:?}", file_name); 63 | } 64 | 65 | log_initialization_info(&cli.filter, &cli.packet_manipulation_settings); 66 | 67 | let running = Arc::new(AtomicBool::new(true)); 68 | let shutdown_triggered = Arc::new(AtomicBool::new(false)); 69 | setup_ctrlc_handler(running.clone(), shutdown_triggered.clone()); 70 | 71 | let (packet_sender, packet_receiver) = mpsc::channel(); 72 | 73 | let cli_thread_safe = Arc::new(Mutex::new(cli)); 74 | 75 | // Start the packet receiving thread 76 | let cli_for_reading = cli_thread_safe.clone(); 77 | let packet_receiver_handle = thread::spawn({ 78 | let running = running.clone(); 79 | move || receive_packets(packet_sender, running, cli_for_reading) 80 | }); 81 | 82 | // Start packet processing thread 83 | let statistics = initialize_statistics(); 84 | 85 | // Clone the Arc for the packet processing thread 86 | let cli_for_processing = cli_thread_safe.clone(); 87 | let statistics_for_processing = statistics.clone(); 88 | let packet_sender_handle = thread::spawn({ 89 | let running = running.clone(); 90 | move || { 91 | start_packet_processing( 92 | cli_for_processing, 93 | packet_receiver, 94 | running, 95 | statistics_for_processing, 96 | ) 97 | } 98 | }); 99 | 100 | if should_start_tui { 101 | tui(cli_thread_safe, statistics, running, shutdown_triggered)?; 102 | } 103 | 104 | wait_for_thread(packet_sender_handle, "Packet sending"); 105 | debug!("Awaiting packet receiving thread termination..."); 106 | wait_for_thread(packet_receiver_handle, "Packet receiving"); 107 | 108 | info!("Application shutdown complete."); 109 | Ok(()) 110 | } 111 | 112 | fn tui( 113 | cli: Arc>, 114 | statistics: Arc>, 115 | running: Arc, 116 | shutdown_triggered: Arc, 117 | ) -> Result<(), WinDivertError> { 118 | { 119 | let mut terminal_manager = TerminalManager::new()?; 120 | 121 | let mut tui_state = TuiState::from_cli(&cli); 122 | 123 | while running.load(Ordering::SeqCst) { 124 | terminal_manager.draw(|f| ui::ui(f, &mut tui_state))?; 125 | let should_quit = input::handle_input(&mut tui_state)?; 126 | if should_quit { 127 | shutdown_triggered.store(true, Ordering::SeqCst); 128 | break; 129 | } else if tui_state.processing { 130 | cli.update_from(&mut tui_state); 131 | tui_state.update_from(&statistics); 132 | } else { 133 | cli.clear_state(); 134 | } 135 | } 136 | } 137 | set_logger_console_state(true); 138 | running.store(false, Ordering::SeqCst); 139 | info!("Initiating shutdown..."); 140 | Ok(()) 141 | } 142 | 143 | fn wait_for_thread(thread_handle: JoinHandle>, thread_name: &str) { 144 | match thread_handle.join() { 145 | Ok(Ok(())) => { 146 | debug!("{} thread completed successfully.", thread_name); 147 | } 148 | Ok(Err(e)) => { 149 | error!("{} thread encountered an error: {:?}", thread_name, e); 150 | } 151 | Err(e) => { 152 | error!("Failed to join {} thread: {:?}", thread_name, e); 153 | } 154 | } 155 | } 156 | 157 | fn setup_ctrlc_handler(running: Arc, shutdown_triggered: Arc) { 158 | ctrlc::set_handler(move || { 159 | if !shutdown_triggered.load(Ordering::SeqCst) { 160 | shutdown_triggered.store(true, Ordering::SeqCst); 161 | info!("Ctrl+C pressed; initiating shutdown..."); 162 | running.store(false, Ordering::SeqCst); 163 | } else { 164 | error!("Ctrl+C pressed again; forcing immediate exit."); 165 | exit(1); // Exit immediately without waiting for cleanup 166 | } 167 | }) 168 | .expect("Error setting Ctrl-C handler"); 169 | } 170 | 171 | fn initialize_logging() { 172 | env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); 173 | } -------------------------------------------------------------------------------- /src/network/modules/tamper.rs: -------------------------------------------------------------------------------- 1 | use crate::network::core::packet_data::PacketData; 2 | use crate::network::modules::stats::tamper_stats::TamperStats; 3 | use crate::network::types::probability::Probability; 4 | use log::error; 5 | use rand::Rng; 6 | use std::collections::HashSet; 7 | use windivert_sys::ChecksumFlags; 8 | 9 | pub fn tamper_packets( 10 | packets: &mut [PacketData], 11 | tamper_probability: Probability, 12 | tamper_amount: Probability, 13 | recalculate_checksums: bool, 14 | stats: &mut TamperStats, 15 | ) { 16 | let should_update_stats = stats.should_update(); 17 | for packet_data in packets.iter_mut() { 18 | let should_skip = rand::random::() >= tamper_probability.value(); 19 | 20 | if should_skip && !should_update_stats { 21 | continue; 22 | } 23 | 24 | let data = packet_data.packet.data.to_mut(); 25 | 26 | let (ip_header_len, protocol) = match get_ip_version(data) { 27 | Some((4, data)) => parse_ipv4_header(data), 28 | Some((6, data)) => parse_ipv6_header(data), 29 | _ => { 30 | error!("Unsupported IP version"); 31 | continue; 32 | } 33 | }; 34 | 35 | let total_header_len = match protocol { 36 | 17 => parse_udp_header(data, ip_header_len), // UDP 37 | 6 => parse_tcp_header(data, ip_header_len), // TCP 38 | _ => ip_header_len, // Unsupported protocols 39 | }; 40 | 41 | let payload_offset = total_header_len; 42 | let payload_length = data.len() - payload_offset; 43 | 44 | if should_skip { 45 | if should_update_stats { 46 | stats.data = data[payload_offset..].to_owned(); 47 | stats.tamper_flags = vec![false; stats.data.len()]; 48 | stats.checksum_valid = true; 49 | stats.updated(); 50 | } 51 | continue; 52 | } 53 | 54 | if payload_length > 0 { 55 | let bytes_to_tamper = (payload_length as f64 * tamper_amount.value()).ceil() as usize; 56 | let tampered_indices = apply_tampering(&mut data[payload_offset..], bytes_to_tamper); 57 | 58 | if should_update_stats { 59 | let tampered_flags = calculate_tampered_flags(data.len(), &tampered_indices); 60 | stats.tamper_flags = tampered_flags; 61 | stats.data = data[payload_offset..].to_owned(); 62 | stats.updated(); 63 | } 64 | } 65 | 66 | if recalculate_checksums { 67 | if let Err(e) = packet_data 68 | .packet 69 | .recalculate_checksums(ChecksumFlags::new()) 70 | { 71 | error!("Error recalculating checksums: {}", e); 72 | } 73 | } 74 | 75 | if should_update_stats { 76 | stats.checksum_valid = packet_data.packet.address.ip_checksum() 77 | && packet_data.packet.address.tcp_checksum() 78 | && packet_data.packet.address.udp_checksum(); 79 | stats.updated(); 80 | } 81 | } 82 | } 83 | 84 | fn apply_tampering(data: &mut [u8], bytes_to_tamper: usize) -> HashSet { 85 | let mut tampered_indices = HashSet::new(); 86 | let mut tampered_count = 0; 87 | let data_len = data.len(); 88 | let mut rng = rand::thread_rng(); 89 | 90 | while tampered_count < bytes_to_tamper && tampered_count < data_len { 91 | let index = rng.gen_range(0..data.len()); 92 | if tampered_indices.insert(index) { 93 | tampered_count += 1; 94 | let tamper_type = rng.gen_range(0..3); 95 | let modified_indices = match tamper_type { 96 | 0 => bit_manipulation(data, index, rng.gen_range(0..8), true), 97 | 1 => bit_flipping(data, index, rng.gen_range(0..8)), 98 | 2 => value_adjustment(data, index, rng.gen_range(-64..64)), 99 | _ => vec![], 100 | }; 101 | tampered_indices.extend(modified_indices); 102 | } 103 | } 104 | 105 | tampered_indices 106 | } 107 | 108 | fn calculate_tampered_flags(data_len: usize, tampered_indices: &HashSet) -> Vec { 109 | let mut tampered_flags = vec![false; data_len]; 110 | for &index in tampered_indices.iter() { 111 | if index < data_len { 112 | tampered_flags[index] = true; 113 | } 114 | } 115 | tampered_flags 116 | } 117 | 118 | fn get_ip_version(data: &[u8]) -> Option<(u8, &[u8])> { 119 | if data.is_empty() { 120 | return None; 121 | } 122 | let version = data[0] >> 4; 123 | Some((version, data)) 124 | } 125 | 126 | fn parse_ipv4_header(data: &[u8]) -> (usize, u8) { 127 | let header_length = ((data[0] & 0x0F) * 4) as usize; 128 | let protocol = data[9]; // Protocol field 129 | (header_length, protocol) 130 | } 131 | 132 | fn parse_ipv6_header(data: &[u8]) -> (usize, u8) { 133 | let header_length = 40; // IPv6 header is always 40 bytes 134 | let next_header = data[6]; // Next header field 135 | (header_length, next_header) 136 | } 137 | 138 | fn parse_udp_header(_data: &[u8], ip_header_len: usize) -> usize { 139 | let udp_header_len = 8; // UDP header is always 8 bytes 140 | ip_header_len + udp_header_len 141 | } 142 | 143 | fn parse_tcp_header(data: &[u8], ip_header_len: usize) -> usize { 144 | let tcp_data_offset = (data[ip_header_len + 12] >> 4) * 4; 145 | ip_header_len + tcp_data_offset as usize 146 | } 147 | 148 | fn bit_manipulation( 149 | data: &mut [u8], 150 | byte_index: usize, 151 | bit_position: usize, 152 | new_bit: bool, 153 | ) -> Vec { 154 | if byte_index < data.len() && bit_position < 8 { 155 | if new_bit { 156 | data[byte_index] |= 1 << bit_position; // Set the bit 157 | } else { 158 | data[byte_index] &= !(1 << bit_position); // Clear the bit 159 | } 160 | vec![byte_index] // Return the modified index 161 | } else { 162 | vec![] // No modification 163 | } 164 | } 165 | 166 | fn bit_flipping(data: &mut [u8], byte_index: usize, bit_position: usize) -> Vec { 167 | if byte_index < data.len() && bit_position < 8 { 168 | data[byte_index] ^= 1 << bit_position; // Flip the bit 169 | vec![byte_index] // Return the modified index 170 | } else { 171 | vec![] // No modification 172 | } 173 | } 174 | 175 | fn value_adjustment(data: &mut [u8], offset: usize, value: i8) -> Vec { 176 | if offset < data.len() { 177 | let adjusted_value = data[offset].wrapping_add(value as u8); 178 | data[offset] = adjusted_value; 179 | vec![offset] // Return the modified index 180 | } else { 181 | vec![] // No modification 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/cli/tui/widgets/duplicate_widget.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::tui::traits::{DisplayName, HandleInput, IsActive, KeyBindings}; 2 | use crate::cli::tui::widgets::utils::block_ext::RoundedBlockExt; 3 | use crate::cli::tui::widgets::utils::style_textarea_based_on_validation; 4 | use crate::cli::tui::widgets::utils::textarea_ext::TextAreaExt; 5 | use crate::cli::tui::widgets::utils::textarea_parsing::ParseFromTextArea; 6 | use crate::network::modules::stats::duplicate_stats::DuplicateStats; 7 | use crate::network::types::probability::Probability; 8 | use ratatui::buffer::Buffer; 9 | use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; 10 | use ratatui::layout::{Constraint, Layout, Margin, Rect}; 11 | use ratatui::style::Style; 12 | use ratatui::widgets::{Block, Paragraph, Widget}; 13 | use tui_textarea::TextArea; 14 | 15 | pub struct DuplicateWidget<'a> { 16 | title: String, 17 | probability_text_area: TextArea<'a>, 18 | duplicate_count_text_area: TextArea<'a>, 19 | is_active: bool, 20 | interacting: bool, 21 | pub probability: Result, 22 | pub duplicate_count: Result, 23 | selected: usize, 24 | duplication_multiplier: f64, 25 | } 26 | 27 | impl Default for DuplicateWidget<'_> { 28 | fn default() -> Self { 29 | Self::new() 30 | } 31 | } 32 | 33 | impl DuplicateWidget<'_> { 34 | pub fn new() -> Self { 35 | DuplicateWidget { 36 | title: "Duplicate".to_string(), 37 | probability_text_area: TextArea::default(), 38 | duplicate_count_text_area: TextArea::default(), 39 | is_active: false, 40 | interacting: false, 41 | probability: Ok(Probability::default()), 42 | duplicate_count: Ok(1), 43 | selected: 0, 44 | duplication_multiplier: 1.0, 45 | } 46 | } 47 | 48 | pub fn set_probability(&mut self, probability: Probability) { 49 | self.probability_text_area 50 | .set_text(&probability.to_string()); 51 | self.probability = Ok(probability); 52 | } 53 | 54 | pub fn set_duplicate_count(&mut self, duplicate_count: usize) { 55 | self.duplicate_count_text_area 56 | .set_text(&duplicate_count.to_string()); 57 | self.duplicate_count = Ok(duplicate_count); 58 | } 59 | 60 | pub(crate) fn update_data(&mut self, stats: &DuplicateStats) { 61 | self.duplication_multiplier = stats.recent_duplication_multiplier(); 62 | } 63 | } 64 | 65 | impl HandleInput for DuplicateWidget<'_> { 66 | fn handle_input(&mut self, key: KeyEvent) -> bool { 67 | if !self.interacting { 68 | if key.code == KeyCode::Enter && key.kind == KeyEventKind::Press { 69 | self.interacting = true; 70 | return true; 71 | } 72 | } else { 73 | if let KeyCode::Enter | KeyCode::Esc = key.code { 74 | self.interacting = false; 75 | return false; 76 | } 77 | if key.code == KeyCode::Down && self.selected < 1 { 78 | self.selected += 1; 79 | } 80 | if key.code == KeyCode::Up && self.selected > 0 { 81 | self.selected -= 1; 82 | } 83 | match self.selected { 84 | 0 => { 85 | if self.probability_text_area.input(key) { 86 | self.probability = 87 | Probability::parse_from_text_area(&self.probability_text_area); 88 | } 89 | } 90 | 1 => { 91 | if self.duplicate_count_text_area.input(key) { 92 | self.duplicate_count = 93 | usize::parse_from_text_area(&self.duplicate_count_text_area); 94 | } 95 | } 96 | _ => {} 97 | } 98 | 99 | return true; 100 | } 101 | false 102 | } 103 | } 104 | 105 | impl DisplayName for DuplicateWidget<'_> { 106 | fn name(&self) -> &str { 107 | &self.title 108 | } 109 | } 110 | 111 | impl KeyBindings for DuplicateWidget<'_> { 112 | fn key_bindings(&self) -> String { 113 | "Exit: Esc | Navigation: Up and Down".to_string() 114 | } 115 | } 116 | 117 | impl IsActive for DuplicateWidget<'_> { 118 | fn is_active(&self) -> bool { 119 | self.is_active 120 | } 121 | 122 | fn set_active(&mut self, state: bool) { 123 | self.is_active = state; 124 | } 125 | } 126 | 127 | impl Widget for &mut DuplicateWidget<'_> { 128 | fn render(self, area: Rect, buf: &mut Buffer) 129 | where 130 | Self: Sized, 131 | { 132 | let [probability_area, duration_area, info_area] = Layout::horizontal([ 133 | Constraint::Max(10), 134 | Constraint::Max(10), 135 | Constraint::Min(25), 136 | ]) 137 | .areas(area.inner(Margin { 138 | horizontal: 1, 139 | vertical: 1, 140 | })); 141 | 142 | self.probability_text_area 143 | .set_cursor_visibility(self.interacting && self.selected == 0); 144 | self.probability_text_area.set_dim_placeholder("0.1"); 145 | self.probability_text_area 146 | .set_cursor_line_style(Style::default()); 147 | self.probability_text_area.set_block( 148 | Block::roundedt("Probability").highlight_if(self.interacting && self.selected == 0), 149 | ); 150 | if !self.probability_text_area.lines()[0].is_empty() { 151 | style_textarea_based_on_validation(&mut self.probability_text_area, &self.probability); 152 | } 153 | self.probability_text_area.render(probability_area, buf); 154 | 155 | self.duplicate_count_text_area 156 | .set_cursor_visibility(self.interacting && self.selected == 1); 157 | self.duplicate_count_text_area.set_dim_placeholder("1"); 158 | self.duplicate_count_text_area 159 | .set_cursor_line_style(Style::default()); 160 | self.duplicate_count_text_area.set_block( 161 | Block::roundedt("Count").highlight_if(self.interacting && self.selected == 1), 162 | ); 163 | if !self.duplicate_count_text_area.lines()[0].is_empty() { 164 | style_textarea_based_on_validation( 165 | &mut self.duplicate_count_text_area, 166 | &self.duplicate_count, 167 | ); 168 | } 169 | self.duplicate_count_text_area.render(duration_area, buf); 170 | 171 | let [duplication_multiplier_info, _excess_info] = 172 | Layout::horizontal([Constraint::Max(20), Constraint::Fill(1)]).areas(info_area); 173 | Paragraph::new(format!("{:.2}x", self.duplication_multiplier)) 174 | .block(Block::bordered().title("I/O Multiplier")) 175 | .render(duplication_multiplier_info, buf); 176 | } 177 | } -------------------------------------------------------------------------------- /src/cli/tui/widgets/reorder_widget.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::tui::traits::{DisplayName, HandleInput, IsActive, KeyBindings}; 2 | use crate::cli::tui::widgets::utils::block_ext::RoundedBlockExt; 3 | use crate::cli::tui::widgets::utils::style_textarea_based_on_validation; 4 | use crate::cli::tui::widgets::utils::textarea_ext::TextAreaExt; 5 | use crate::cli::tui::widgets::utils::textarea_parsing::ParseFromTextArea; 6 | use crate::network::modules::stats::reorder_stats::ReorderStats; 7 | use crate::network::types::probability::Probability; 8 | use ratatui::buffer::Buffer; 9 | use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; 10 | use ratatui::layout::{Constraint, Layout, Margin, Rect}; 11 | use ratatui::style::Style; 12 | use ratatui::widgets::{Block, Paragraph, Widget}; 13 | use tui_textarea::TextArea; 14 | 15 | pub struct ReorderWidget<'a> { 16 | title: String, 17 | probability_text_area: TextArea<'a>, 18 | delay_duration_text_area: TextArea<'a>, 19 | is_active: bool, 20 | interacting: bool, 21 | pub probability: Result, 22 | pub delay_duration: Result, 23 | selected: usize, 24 | reorder_rate: f64, 25 | delayed_packets: usize, 26 | } 27 | 28 | impl Default for ReorderWidget<'_> { 29 | fn default() -> Self { 30 | Self::new() 31 | } 32 | } 33 | 34 | impl ReorderWidget<'_> { 35 | pub fn new() -> Self { 36 | ReorderWidget { 37 | title: "Reorder".to_string(), 38 | probability_text_area: TextArea::default(), 39 | delay_duration_text_area: TextArea::default(), 40 | is_active: false, 41 | interacting: false, 42 | probability: Ok(Probability::default()), 43 | delay_duration: Ok(0), 44 | selected: 0, 45 | reorder_rate: 0.0, 46 | delayed_packets: 0, 47 | } 48 | } 49 | 50 | pub fn set_probability(&mut self, probability: Probability) { 51 | self.probability_text_area 52 | .set_text(&probability.to_string()); 53 | self.probability = Ok(probability); 54 | } 55 | 56 | pub fn set_delay_duration(&mut self, delay_duration_ms: u64) { 57 | self.delay_duration_text_area 58 | .set_text(&delay_duration_ms.to_string()); 59 | self.delay_duration = Ok(delay_duration_ms); 60 | } 61 | 62 | pub(crate) fn update_data(&mut self, stats: &ReorderStats) { 63 | self.reorder_rate = stats.recent_reorder_rate(); 64 | self.delayed_packets = stats.delayed_packets; 65 | } 66 | } 67 | 68 | impl HandleInput for ReorderWidget<'_> { 69 | fn handle_input(&mut self, key: KeyEvent) -> bool { 70 | if !self.interacting { 71 | if key.code == KeyCode::Enter && key.kind == KeyEventKind::Press { 72 | self.interacting = true; 73 | return true; 74 | } 75 | } else { 76 | if let KeyCode::Enter | KeyCode::Esc = key.code { 77 | self.interacting = false; 78 | return false; 79 | } 80 | if key.code == KeyCode::Down && self.selected < 1 { 81 | self.selected += 1; 82 | } 83 | if key.code == KeyCode::Up && self.selected > 0 { 84 | self.selected -= 1; 85 | } 86 | match self.selected { 87 | 0 => { 88 | if self.probability_text_area.input(key) { 89 | self.probability = 90 | Probability::parse_from_text_area(&self.probability_text_area); 91 | } 92 | } 93 | 1 => { 94 | if self.delay_duration_text_area.input(key) { 95 | self.delay_duration = 96 | u64::parse_from_text_area(&self.delay_duration_text_area); 97 | } 98 | } 99 | _ => {} 100 | } 101 | 102 | return true; 103 | } 104 | false 105 | } 106 | } 107 | 108 | impl DisplayName for ReorderWidget<'_> { 109 | fn name(&self) -> &str { 110 | &self.title 111 | } 112 | } 113 | 114 | impl KeyBindings for ReorderWidget<'_> { 115 | fn key_bindings(&self) -> String { 116 | "Exit: Esc | Navigation: Up and Down".to_string() 117 | } 118 | } 119 | 120 | impl IsActive for ReorderWidget<'_> { 121 | fn is_active(&self) -> bool { 122 | self.is_active 123 | } 124 | 125 | fn set_active(&mut self, state: bool) { 126 | self.is_active = state; 127 | } 128 | } 129 | 130 | impl Widget for &mut ReorderWidget<'_> { 131 | fn render(self, area: Rect, buf: &mut Buffer) 132 | where 133 | Self: Sized, 134 | { 135 | let [probability_area, delay_duration_area, info_area] = Layout::horizontal([ 136 | Constraint::Max(12), 137 | Constraint::Max(10), 138 | Constraint::Min(25), 139 | ]) 140 | .areas(area.inner(Margin { 141 | horizontal: 1, 142 | vertical: 1, 143 | })); 144 | 145 | self.probability_text_area 146 | .set_cursor_visibility(self.interacting && self.selected == 0); 147 | self.probability_text_area.set_dim_placeholder("0.1"); 148 | self.probability_text_area 149 | .set_cursor_line_style(Style::default()); 150 | self.probability_text_area.set_block( 151 | Block::roundedt("Probability").highlight_if(self.interacting && self.selected == 0), 152 | ); 153 | if !self.probability_text_area.lines()[0].is_empty() { 154 | style_textarea_based_on_validation(&mut self.probability_text_area, &self.probability); 155 | } 156 | self.probability_text_area.render(probability_area, buf); 157 | 158 | self.delay_duration_text_area 159 | .set_cursor_visibility(self.interacting && self.selected == 1); 160 | self.delay_duration_text_area.set_dim_placeholder("30"); 161 | self.delay_duration_text_area 162 | .set_cursor_line_style(Style::default()); 163 | self.delay_duration_text_area.set_block( 164 | Block::roundedt("Duration").highlight_if(self.interacting && self.selected == 1), 165 | ); 166 | if !self.delay_duration_text_area.lines()[0].is_empty() { 167 | style_textarea_based_on_validation( 168 | &mut self.delay_duration_text_area, 169 | &self.delay_duration, 170 | ); 171 | } 172 | self.delay_duration_text_area 173 | .render(delay_duration_area, buf); 174 | 175 | let [reorder_percentage_info, delayed_count_info, _excess_info] = Layout::horizontal([ 176 | Constraint::Max(10), 177 | Constraint::Max(10), 178 | Constraint::Fill(1), 179 | ]) 180 | .areas(info_area); 181 | Paragraph::new(format!("{:.2}%", self.reorder_rate * 100.0)) 182 | .block(Block::bordered().title("Reorder rate")) 183 | .render(reorder_percentage_info, buf); 184 | Paragraph::new(format!("{}", self.delayed_packets)) 185 | .block(Block::bordered().title("Delayed")) 186 | .render(delayed_count_info, buf); 187 | } 188 | } -------------------------------------------------------------------------------- /src/cli/tui/ui.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::tui::state::TuiState; 2 | use crate::cli::tui::traits::{DisplayName, IsActive, KeyBindings}; 3 | use crate::cli::tui::widgets::utils::block_ext::RoundedBlockExt; 4 | use ratatui::layout::{Constraint, Layout, Margin, Rect}; 5 | use ratatui::prelude::{Color, Line, Style, Stylize}; 6 | use ratatui::style::Styled; 7 | use ratatui::widgets::{Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState}; 8 | use ratatui::Frame; 9 | use std::cmp::PartialEq; 10 | 11 | pub fn ui(frame: &mut Frame, state: &mut TuiState) { 12 | update_focus(state); 13 | let (header_area, middle_area, footer_area) = setup_layout(frame); 14 | let (main_area, log_area) = arrange_middle_area(state, middle_area); 15 | 16 | let [filter_area, start_stop_toggle_area] = 17 | Layout::horizontal([Constraint::Fill(1), Constraint::Max(8)]).areas(header_area); 18 | render_start_stop_toggle(frame, state, filter_area, start_stop_toggle_area); 19 | render_sections(frame, state, main_area); 20 | frame.render_widget(&mut state.logs_widget, log_area); 21 | render_keybindings(frame, state, footer_area); 22 | } 23 | 24 | #[derive(PartialEq)] 25 | pub enum LayoutSection { 26 | Filter, 27 | Main, 28 | Logging, 29 | } 30 | 31 | fn update_focus(state: &mut TuiState) { 32 | if state.filter_widget.inputting { 33 | state.focused = LayoutSection::Filter; 34 | } else if state.logs_widget.focused { 35 | state.focused = LayoutSection::Logging; 36 | } else { 37 | state.focused = LayoutSection::Main; 38 | } 39 | } 40 | 41 | fn setup_layout(frame: &mut Frame) -> (Rect, Rect, Rect) { 42 | let [header_area, middle_area, footer_area] = Layout::vertical([ 43 | Constraint::Max(3), 44 | Constraint::Min(0), 45 | Constraint::Length(1), 46 | ]) 47 | .areas(frame.area()); 48 | (header_area, middle_area, footer_area) 49 | } 50 | 51 | fn arrange_middle_area(state: &mut TuiState, middle_area: Rect) -> (Rect, Rect) { 52 | let [main_area, log_area] = 53 | if middle_area.height + 60 >= middle_area.width || !state.logs_widget.open { 54 | Layout::vertical([ 55 | Constraint::Max(500), 56 | Constraint::Max(if state.logs_widget.open { 10 } else { 1 }), 57 | ]) 58 | .areas(middle_area) 59 | } else { 60 | Layout::horizontal([ 61 | Constraint::Fill(1), 62 | Constraint::Fill(if state.logs_widget.open { 1 } else { 0 }), 63 | ]) 64 | .areas(middle_area) 65 | }; 66 | (main_area, log_area) 67 | } 68 | 69 | fn render_start_stop_toggle( 70 | frame: &mut Frame, 71 | state: &mut TuiState, 72 | filter_area: Rect, 73 | start_stop_toggle_area: Rect, 74 | ) { 75 | frame.render_widget(&mut state.filter_widget, filter_area); 76 | frame.render_widget( 77 | Paragraph::new(if state.processing { 78 | "Stop".to_string() 79 | } else { 80 | "Start".to_string() 81 | }) 82 | .block(Block::roundedt("[P]").set_style(if state.processing { 83 | Style::new().fg(Color::LightRed) 84 | } else { 85 | Style::new().fg(Color::LightGreen) 86 | })), 87 | start_stop_toggle_area, 88 | ); 89 | } 90 | 91 | fn render_sections(frame: &mut Frame, state: &mut TuiState, main_area: Rect) { 92 | let total_sections = state.sections.len(); 93 | let default_height = 5; 94 | let available_rect = main_area.inner(Margin { 95 | horizontal: 1, 96 | vertical: 1, 97 | }); 98 | let available_height = available_rect.height as usize; 99 | 100 | // Calculate how many sections can be displayed given the available height. 101 | let max_visible_sections = available_height / default_height; 102 | let half_visible = max_visible_sections / 2; 103 | 104 | // Ensure at least one section is visible 105 | let max_visible_sections = max_visible_sections.max(1); 106 | 107 | let (start_index, end_index) = if max_visible_sections >= total_sections { 108 | // If we have enough space to show all sections, just display all. 109 | (0, total_sections - 1) 110 | } else { 111 | // Center the selected section, adjusting for edges 112 | let start = state.selected.saturating_sub(half_visible); 113 | let end = (start + max_visible_sections - 1).min(total_sections - 1); 114 | 115 | // Adjust start if we're at the end of the list 116 | let start = if end == total_sections - 1 { 117 | end.saturating_sub(max_visible_sections - 1) 118 | } else { 119 | start 120 | }; 121 | (start, end) 122 | }; 123 | 124 | // Apply the constraints 125 | let constraints: Vec = (0..total_sections) 126 | .map(|i| { 127 | if i >= start_index && i <= end_index { 128 | Constraint::Length(default_height as u16) 129 | } else { 130 | Constraint::Length(0) 131 | } 132 | }) 133 | .collect(); 134 | 135 | let section_areas: [Rect; 7] = Layout::vertical(constraints).areas(available_rect); 136 | 137 | let mut main_block = 138 | Block::roundedt("Main").title_bottom(Line::from("This is the main area").right_aligned()); 139 | main_block = main_block.highlight_if(state.focused == LayoutSection::Main); 140 | frame.render_widget(main_block, main_area); 141 | 142 | for (i, option) in state.sections.iter_mut().enumerate() { 143 | let mut area_block = Block::rounded().title(format!("[{}]-{}", i + 1, option.name())); 144 | if !option.is_active() { 145 | area_block = area_block.fg(Color::DarkGray); 146 | } 147 | if state.selected == i { 148 | area_block = area_block.border_style(Style::default().fg(Color::Cyan)); 149 | } 150 | area_block = area_block.highlight_if(state.interacting == Some(i)); 151 | frame.render_widget(area_block, section_areas[i]); 152 | frame.render_widget(option, section_areas[i]); 153 | } 154 | 155 | if total_sections > max_visible_sections { 156 | // Calculate scrollbar state 157 | let scroll_position = state.selected; 158 | let mut scrollbar_state = ScrollbarState::new(total_sections) 159 | .viewport_content_length(max_visible_sections) 160 | .position(scroll_position); 161 | 162 | // Render the scrollbar on the right side 163 | let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) 164 | .begin_symbol(Some("↑")) 165 | .end_symbol(Some("↓")) 166 | .thumb_symbol("█") 167 | .thumb_style(Style::default().fg(Color::DarkGray)); 168 | 169 | frame.render_stateful_widget(scrollbar, available_rect, &mut scrollbar_state); 170 | } 171 | } 172 | 173 | fn render_keybindings(frame: &mut Frame, state: &mut TuiState, key_bind_area: Rect) { 174 | let mut keybinds = "Quit: q | Toggle: Space | Navigation: Up and Down".to_string(); 175 | match state.focused { 176 | LayoutSection::Filter => { 177 | keybinds = state.filter_widget.key_bindings(); 178 | } 179 | LayoutSection::Main => { 180 | if let Some(index) = state.interacting { 181 | keybinds = state.sections[index].key_bindings(); 182 | } 183 | } 184 | LayoutSection::Logging => { 185 | keybinds = state.logs_widget.key_bindings(); 186 | } 187 | } 188 | 189 | frame.render_widget(Paragraph::new(keybinds).style(Color::Cyan), key_bind_area) 190 | } 191 | -------------------------------------------------------------------------------- /src/cli/tui/widgets/throttle_widget.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::tui::traits::{DisplayName, HandleInput, IsActive, KeyBindings}; 2 | use crate::cli::tui::widgets::utils::block_ext::RoundedBlockExt; 3 | use crate::cli::tui::widgets::utils::style_textarea_based_on_validation; 4 | use crate::cli::tui::widgets::utils::textarea_ext::TextAreaExt; 5 | use crate::cli::tui::widgets::utils::textarea_parsing::ParseFromTextArea; 6 | use crate::network::modules::stats::throttle_stats::ThrottleStats; 7 | use crate::network::types::probability::Probability; 8 | use ratatui::buffer::Buffer; 9 | use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; 10 | use ratatui::layout::{Constraint, Layout, Margin, Rect}; 11 | use ratatui::style::{Modifier, Style, Stylize}; 12 | use ratatui::text::Span; 13 | use ratatui::widgets::{Block, Paragraph, Widget}; 14 | use tui_textarea::TextArea; 15 | 16 | pub struct ThrottleWidget<'a> { 17 | title: String, 18 | probability_text_area: TextArea<'a>, 19 | throttle_duration_text_area: TextArea<'a>, 20 | pub drop: bool, 21 | is_active: bool, 22 | interacting: bool, 23 | pub probability: Result, 24 | pub throttle_duration: Result, 25 | selected: usize, 26 | is_throttling: bool, 27 | dropped_count: usize, 28 | } 29 | 30 | impl Default for ThrottleWidget<'_> { 31 | fn default() -> Self { 32 | Self::new() 33 | } 34 | } 35 | 36 | impl ThrottleWidget<'_> { 37 | pub fn new() -> Self { 38 | ThrottleWidget { 39 | title: "Throttle".to_string(), 40 | probability_text_area: TextArea::default(), 41 | throttle_duration_text_area: TextArea::default(), 42 | drop: false, 43 | is_active: false, 44 | interacting: false, 45 | probability: Ok(Probability::default()), 46 | throttle_duration: Ok(0), 47 | selected: 0, 48 | is_throttling: false, 49 | dropped_count: 0, 50 | } 51 | } 52 | 53 | pub fn set_probability(&mut self, probability: Probability) { 54 | self.probability_text_area 55 | .set_text(&probability.to_string()); 56 | self.probability = Ok(probability); 57 | } 58 | 59 | pub fn set_throttle_duration(&mut self, throttle_duration_ms: u64) { 60 | self.throttle_duration_text_area 61 | .set_text(&throttle_duration_ms.to_string()); 62 | self.throttle_duration = Ok(throttle_duration_ms); 63 | } 64 | 65 | pub fn update_data(&mut self, stats: &ThrottleStats) { 66 | self.is_throttling = stats.is_throttling; 67 | self.dropped_count = stats.dropped_count; 68 | } 69 | } 70 | 71 | impl HandleInput for ThrottleWidget<'_> { 72 | fn handle_input(&mut self, key: KeyEvent) -> bool { 73 | if !self.interacting { 74 | if key.code == KeyCode::Enter && key.kind == KeyEventKind::Press { 75 | self.interacting = true; 76 | return true; 77 | } 78 | } else { 79 | if let KeyCode::Enter | KeyCode::Esc = key.code { 80 | self.interacting = false; 81 | return false; 82 | } 83 | if key.code == KeyCode::Down && self.selected < 2 { 84 | self.selected += 1; 85 | } 86 | if key.code == KeyCode::Up && self.selected > 0 { 87 | self.selected -= 1; 88 | } 89 | match self.selected { 90 | 0 => { 91 | if self.probability_text_area.input(key) { 92 | self.probability = 93 | Probability::parse_from_text_area(&self.probability_text_area); 94 | } 95 | } 96 | 1 => { 97 | if self.throttle_duration_text_area.input(key) { 98 | self.throttle_duration = 99 | u64::parse_from_text_area(&self.throttle_duration_text_area); 100 | } 101 | } 102 | 2 => { 103 | if key.code == KeyCode::Char(' ') && key.kind == KeyEventKind::Press { 104 | self.drop = !self.drop; 105 | } 106 | } 107 | _ => {} 108 | } 109 | 110 | return true; 111 | } 112 | false 113 | } 114 | } 115 | 116 | impl DisplayName for ThrottleWidget<'_> { 117 | fn name(&self) -> &str { 118 | &self.title 119 | } 120 | } 121 | 122 | impl KeyBindings for ThrottleWidget<'_> { 123 | fn key_bindings(&self) -> String { 124 | "Exit: Esc | Navigation: Up and Down".to_string() 125 | } 126 | } 127 | 128 | impl IsActive for ThrottleWidget<'_> { 129 | fn is_active(&self) -> bool { 130 | self.is_active 131 | } 132 | 133 | fn set_active(&mut self, state: bool) { 134 | self.is_active = state; 135 | } 136 | } 137 | 138 | impl Widget for &mut ThrottleWidget<'_> { 139 | fn render(self, area: Rect, buf: &mut Buffer) 140 | where 141 | Self: Sized, 142 | { 143 | let [probability_area, duration_area, drop_area, info_area] = Layout::horizontal([ 144 | Constraint::Max(12), 145 | Constraint::Max(10), 146 | Constraint::Max(8), 147 | Constraint::Min(25), 148 | ]) 149 | .areas(area.inner(Margin { 150 | horizontal: 1, 151 | vertical: 1, 152 | })); 153 | 154 | self.probability_text_area 155 | .set_cursor_visibility(self.interacting && self.selected == 0); 156 | self.probability_text_area.set_dim_placeholder("0.1"); 157 | self.probability_text_area 158 | .set_cursor_line_style(Style::default()); 159 | self.probability_text_area.set_block( 160 | Block::roundedt("Probability").highlight_if(self.interacting && self.selected == 0), 161 | ); 162 | if !self.probability_text_area.lines()[0].is_empty() { 163 | style_textarea_based_on_validation(&mut self.probability_text_area, &self.probability); 164 | } 165 | self.probability_text_area.render(probability_area, buf); 166 | 167 | self.throttle_duration_text_area 168 | .set_cursor_visibility(self.interacting && self.selected == 1); 169 | self.throttle_duration_text_area.set_dim_placeholder("30"); 170 | self.throttle_duration_text_area 171 | .set_cursor_line_style(Style::default()); 172 | self.throttle_duration_text_area.set_block( 173 | Block::roundedt("Duration").highlight_if(self.interacting && self.selected == 1), 174 | ); 175 | if !self.throttle_duration_text_area.lines()[0].is_empty() { 176 | style_textarea_based_on_validation( 177 | &mut self.throttle_duration_text_area, 178 | &self.throttle_duration, 179 | ); 180 | } 181 | self.throttle_duration_text_area.render(duration_area, buf); 182 | 183 | let mut drop_span = Span::from(self.drop.to_string()); 184 | if self.selected == 2 && self.interacting { 185 | drop_span = drop_span.add_modifier(Modifier::RAPID_BLINK); 186 | } 187 | let drop_paragraph = Paragraph::new(drop_span) 188 | .block(Block::roundedt("Drop").highlight_if(self.interacting && self.selected == 2)); 189 | drop_paragraph.render(drop_area, buf); 190 | 191 | let [is_throttling_info, drop_count, _excess_info] = Layout::horizontal([ 192 | Constraint::Max(10), 193 | Constraint::Max(10), 194 | Constraint::Fill(1), 195 | ]) 196 | .areas(info_area); 197 | Paragraph::new(format!("{}", self.is_throttling)) 198 | .block(Block::bordered().title("Throttling")) 199 | .render(is_throttling_info, buf); 200 | Paragraph::new(format!("{}", self.dropped_count)) 201 | .block(Block::bordered().title("Dropped")) 202 | .render(drop_count, buf); 203 | } 204 | } -------------------------------------------------------------------------------- /src/cli/tui/widgets/tamper_widget.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::tui::traits::{DisplayName, HandleInput, IsActive, KeyBindings}; 2 | use crate::cli::tui::widgets::utils::block_ext::RoundedBlockExt; 3 | use crate::cli::tui::widgets::utils::style_textarea_based_on_validation; 4 | use crate::cli::tui::widgets::utils::textarea_ext::TextAreaExt; 5 | use crate::cli::tui::widgets::utils::textarea_parsing::ParseFromTextArea; 6 | use crate::network::modules::stats::tamper_stats::TamperStats; 7 | use crate::network::types::probability::Probability; 8 | use ratatui::buffer::Buffer; 9 | use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; 10 | use ratatui::layout::{Constraint, Layout, Margin, Rect}; 11 | use ratatui::prelude::{Modifier, Span}; 12 | use ratatui::style::{Color, Style, Stylize}; 13 | use ratatui::text::Line; 14 | use ratatui::widgets::{Block, Paragraph, Widget}; 15 | use tui_textarea::TextArea; 16 | 17 | pub struct TamperWidget<'a> { 18 | title: String, 19 | probability_text_area: TextArea<'a>, 20 | tamper_amount_text_area: TextArea<'a>, 21 | pub recalculate_checksums: bool, 22 | is_active: bool, 23 | interacting: bool, 24 | selected: usize, 25 | pub probability: Result, 26 | pub tamper_amount: Result, 27 | data: Vec, 28 | tamper_flags: Vec, 29 | checksum_valid: bool, 30 | } 31 | 32 | impl Default for TamperWidget<'_> { 33 | fn default() -> Self { 34 | Self::new() 35 | } 36 | } 37 | 38 | impl TamperWidget<'_> { 39 | pub fn new() -> Self { 40 | TamperWidget { 41 | title: "Tamper".to_string(), 42 | probability_text_area: TextArea::default(), 43 | tamper_amount_text_area: TextArea::default(), 44 | recalculate_checksums: true, 45 | is_active: false, 46 | interacting: false, 47 | selected: 0, 48 | probability: Ok(Probability::default()), 49 | tamper_amount: Ok(Probability::default()), 50 | data: vec![], 51 | tamper_flags: vec![], 52 | checksum_valid: true, 53 | } 54 | } 55 | 56 | pub fn set_tamper_amount(&mut self, tamper_amount: Probability) { 57 | self.tamper_amount_text_area 58 | .set_text(&tamper_amount.to_string()); 59 | self.tamper_amount = Ok(tamper_amount); 60 | } 61 | 62 | pub fn set_probability(&mut self, probability: Probability) { 63 | self.probability_text_area 64 | .set_text(&probability.to_string()); 65 | self.probability = Ok(probability); 66 | } 67 | 68 | pub(crate) fn update_data(&mut self, stats: &TamperStats) { 69 | self.data = stats.data.clone(); 70 | self.tamper_flags = stats.tamper_flags.clone(); 71 | self.checksum_valid = stats.checksum_valid; 72 | } 73 | } 74 | 75 | impl HandleInput for TamperWidget<'_> { 76 | fn handle_input(&mut self, key: KeyEvent) -> bool { 77 | if !self.interacting { 78 | if key.code == KeyCode::Enter && key.kind == KeyEventKind::Press { 79 | self.interacting = true; 80 | return true; 81 | } 82 | } else { 83 | if let KeyCode::Enter | KeyCode::Esc = key.code { 84 | self.interacting = false; 85 | return false; 86 | } 87 | if key.code == KeyCode::Down && self.selected < 2 { 88 | self.selected += 1; 89 | } 90 | if key.code == KeyCode::Up && self.selected > 0 { 91 | self.selected -= 1; 92 | } 93 | match self.selected { 94 | 0 => { 95 | if self.probability_text_area.input(key) { 96 | self.probability = 97 | Probability::parse_from_text_area(&self.probability_text_area); 98 | } 99 | } 100 | 1 => { 101 | if self.tamper_amount_text_area.input(key) { 102 | self.tamper_amount = 103 | Probability::parse_from_text_area(&self.tamper_amount_text_area); 104 | } 105 | } 106 | 2 => { 107 | if key.code == KeyCode::Char(' ') && key.kind == KeyEventKind::Press { 108 | self.recalculate_checksums = !self.recalculate_checksums; 109 | } 110 | } 111 | _ => {} 112 | } 113 | 114 | return true; 115 | } 116 | false 117 | } 118 | } 119 | 120 | impl DisplayName for TamperWidget<'_> { 121 | fn name(&self) -> &str { 122 | &self.title 123 | } 124 | } 125 | 126 | impl KeyBindings for TamperWidget<'_> { 127 | fn key_bindings(&self) -> String { 128 | "Exit: Esc | Navigation: Up and Down".to_string() 129 | } 130 | } 131 | 132 | impl IsActive for TamperWidget<'_> { 133 | fn is_active(&self) -> bool { 134 | self.is_active 135 | } 136 | 137 | fn set_active(&mut self, state: bool) { 138 | self.is_active = state; 139 | } 140 | } 141 | 142 | impl Widget for &mut TamperWidget<'_> { 143 | fn render(self, area: Rect, buf: &mut Buffer) 144 | where 145 | Self: Sized, 146 | { 147 | let [probability_area, duration_area, checksum_area, info_area] = Layout::horizontal([ 148 | Constraint::Max(10), 149 | Constraint::Max(10), 150 | Constraint::Max(25), 151 | Constraint::Min(25), 152 | ]) 153 | .areas(area.inner(Margin { 154 | horizontal: 1, 155 | vertical: 1, 156 | })); 157 | 158 | self.probability_text_area 159 | .set_cursor_visibility(self.interacting && self.selected == 0); 160 | self.probability_text_area.set_dim_placeholder("0.1"); 161 | self.probability_text_area 162 | .set_cursor_line_style(Style::default()); 163 | self.probability_text_area.set_block( 164 | Block::roundedt("Probability").highlight_if(self.interacting && self.selected == 0), 165 | ); 166 | if !self.probability_text_area.lines()[0].is_empty() { 167 | style_textarea_based_on_validation(&mut self.probability_text_area, &self.probability); 168 | } 169 | self.probability_text_area.render(probability_area, buf); 170 | 171 | self.tamper_amount_text_area 172 | .set_cursor_visibility(self.interacting && self.selected == 1); 173 | self.tamper_amount_text_area.set_dim_placeholder("0.1"); 174 | self.tamper_amount_text_area 175 | .set_cursor_line_style(Style::default()); 176 | self.tamper_amount_text_area.set_block( 177 | Block::roundedt("Amount").highlight_if(self.interacting && self.selected == 1), 178 | ); 179 | if !self.tamper_amount_text_area.lines()[0].is_empty() { 180 | style_textarea_based_on_validation( 181 | &mut self.probability_text_area, 182 | &self.tamper_amount, 183 | ); 184 | } 185 | self.tamper_amount_text_area.render(duration_area, buf); 186 | 187 | let mut checksum_span = Span::from(self.recalculate_checksums.to_string()); 188 | if self.selected == 2 && self.interacting { 189 | checksum_span = checksum_span.add_modifier(Modifier::RAPID_BLINK); 190 | } 191 | let checksum_paragraph = 192 | Paragraph::new(checksum_span).block(Block::roundedt("Recalculate Checksums")); 193 | checksum_paragraph.render(checksum_area, buf); 194 | 195 | let mut info_block = Block::bordered(); 196 | if !self.checksum_valid { 197 | info_block = info_block.border_style(Style::new().fg(Color::LightRed)) 198 | }; 199 | Paragraph::new(Line::from(highlight_tampered_data( 200 | self.data.clone(), 201 | info_area.width, 202 | self.tamper_flags.clone(), 203 | ))) 204 | .block(info_block) 205 | .render(info_area, buf); 206 | } 207 | } 208 | 209 | fn highlight_tampered_data(data: Vec, width: u16, flags: Vec) -> Vec> { 210 | data.into_iter() 211 | .zip(flags) 212 | .take(width as usize) 213 | .map(|(byte, is_tampered)| { 214 | let symbol = char::try_from(byte); 215 | let symbol = match symbol { 216 | Ok(c) 217 | if c.is_ascii_alphanumeric() 218 | || [' ', '.', ',', '!', '?', ':', ';', '-'].contains(&c) => 219 | { 220 | c 221 | } 222 | _ => '�', 223 | }; 224 | if is_tampered { 225 | Span::styled(symbol.to_string(), Style::default().fg(Color::LightRed)) 226 | } else { 227 | Span::styled(symbol.to_string(), Style::default()) 228 | } 229 | }) 230 | .collect() 231 | } -------------------------------------------------------------------------------- /src/network/modules/bandwidth.rs: -------------------------------------------------------------------------------- 1 | use crate::network::core::packet_data::PacketData; 2 | use crate::network::modules::stats::bandwidth_stats::BandwidthStats; 3 | use log::trace; 4 | use std::collections::VecDeque; 5 | use std::time::Instant; 6 | 7 | const MAX_BUFFER_SIZE: usize = 10 * 1024 * 1024; // 10 MB in bytes 8 | 9 | pub fn bandwidth_limiter<'a>( 10 | packets: &mut Vec>, 11 | buffer: &mut VecDeque>, 12 | total_buffer_size: &mut usize, 13 | last_send_time: &mut Instant, 14 | bandwidth_limit_kbps: usize, 15 | stats: &mut BandwidthStats, 16 | ) { 17 | let incoming_packet_count = packets.len(); 18 | stats.storage_packet_count += incoming_packet_count; 19 | add_packets_to_buffer(buffer, packets, total_buffer_size); 20 | maintain_buffer_size(buffer, total_buffer_size, stats); 21 | 22 | let now = Instant::now(); 23 | let elapsed = now.duration_since(*last_send_time).as_secs_f64(); 24 | let bytes_allowed = (((bandwidth_limit_kbps * 1024) as f64) * elapsed) as usize; 25 | 26 | let mut bytes_sent = 0; 27 | let mut to_send = Vec::new(); 28 | 29 | while let Some(packet_data) = buffer.front() { 30 | let packet_size = packet_data.packet.data.len(); 31 | if bytes_sent + packet_size > bytes_allowed { 32 | break; 33 | } 34 | bytes_sent += packet_size; 35 | if let Some(packet) = remove_packet_from_buffer(buffer, total_buffer_size, stats) { 36 | to_send.push(packet); 37 | } 38 | } 39 | 40 | packets.extend(to_send); 41 | 42 | if bytes_sent > 0 { 43 | trace!("Limit: {}, Bytes Allowed {}, Incoming Packets: {}, Packets Sent: {}, Buffer Element Count: {}, Total Buffer Size: {}, Bytes Sent: {}", 44 | bandwidth_limit_kbps, bytes_allowed, incoming_packet_count, packets.len(), buffer.len(), total_buffer_size, bytes_sent); 45 | stats.record(bytes_sent); 46 | *last_send_time = now; 47 | } 48 | } 49 | 50 | fn add_packet_to_buffer<'a>( 51 | buffer: &mut VecDeque>, 52 | packet: PacketData<'a>, 53 | total_size: &mut usize, 54 | ) { 55 | *total_size += packet.packet.data.len(); 56 | buffer.push_back(packet); 57 | } 58 | 59 | fn add_packets_to_buffer<'a>( 60 | buffer: &mut VecDeque>, 61 | packets: &mut Vec>, 62 | total_size: &mut usize, 63 | ) { 64 | while let Some(packet) = packets.pop() { 65 | add_packet_to_buffer(buffer, packet, total_size); 66 | } 67 | } 68 | 69 | fn remove_packet_from_buffer<'a>( 70 | buffer: &mut VecDeque>, 71 | total_size: &mut usize, 72 | stats: &mut BandwidthStats, 73 | ) -> Option> { 74 | if let Some(packet) = buffer.pop_front() { 75 | *total_size -= packet.packet.data.len(); 76 | stats.storage_packet_count = stats.storage_packet_count.saturating_sub(1); 77 | Some(packet) 78 | } else { 79 | None 80 | } 81 | } 82 | 83 | fn maintain_buffer_size( 84 | buffer: &mut VecDeque>, 85 | total_size: &mut usize, 86 | stats: &mut BandwidthStats, 87 | ) { 88 | while *total_size > MAX_BUFFER_SIZE { 89 | if remove_packet_from_buffer(buffer, total_size, stats).is_some() { 90 | // Packet removed from buffer to maintain size limit 91 | } else { 92 | break; // No more packets to remove 93 | } 94 | } 95 | } 96 | 97 | #[cfg(test)] 98 | mod tests { 99 | use super::*; 100 | use crate::network::core::packet_data::PacketData; 101 | use crate::network::modules::bandwidth::{ 102 | add_packet_to_buffer, add_packets_to_buffer, bandwidth_limiter, remove_packet_from_buffer, 103 | MAX_BUFFER_SIZE, 104 | }; 105 | use std::collections::VecDeque; 106 | use std::time::Duration; 107 | use windivert::layer::NetworkLayer; 108 | use windivert::packet::WinDivertPacket; 109 | 110 | /// Safely creates a dummy packet with a specified length. 111 | /// Assumes the vector created with the specified length is valid for packet creation. 112 | fn create_dummy_packet<'a>(length: usize) -> WinDivertPacket<'a, NetworkLayer> { 113 | let data = vec![1; length]; 114 | unsafe { WinDivertPacket::::new(data) } 115 | } 116 | 117 | #[test] 118 | fn test_basic_bandwidth_limiting() { 119 | let mut packets = vec![ 120 | PacketData::from(create_dummy_packet(1000)), 121 | PacketData::from(create_dummy_packet(1000)), 122 | ]; 123 | let mut buffer = VecDeque::new(); 124 | let total_buffer_size: &mut usize = &mut 0usize; 125 | let mut last_send_time = Instant::now() - Duration::from_secs(1); 126 | let bandwidth_limit = 1; // 1 KB/s 127 | let mut stats = BandwidthStats::new(0.5); 128 | 129 | bandwidth_limiter( 130 | &mut packets, 131 | &mut buffer, 132 | total_buffer_size, 133 | &mut last_send_time, 134 | bandwidth_limit, 135 | &mut stats, 136 | ); 137 | 138 | assert!(packets.len() <= 1); 139 | } 140 | 141 | #[test] 142 | fn test_exceeding_buffer_size() { 143 | let mut packets = Vec::new(); 144 | let mut buffer = VecDeque::new(); 145 | let mut total_buffer_size = 0; 146 | 147 | // Fill the buffer with packets to exceed the max total size 148 | while total_buffer_size < MAX_BUFFER_SIZE + 10_000 { 149 | let packet = PacketData::from(create_dummy_packet(1000)); 150 | total_buffer_size += packet.packet.data.len(); 151 | buffer.push_back(packet); 152 | } 153 | let mut last_send_time = Instant::now(); 154 | let bandwidth_limit = 100; // High enough to not limit the test 155 | let mut stats = BandwidthStats::new(0.5); 156 | 157 | bandwidth_limiter( 158 | &mut packets, 159 | &mut buffer, 160 | &mut total_buffer_size, 161 | &mut last_send_time, 162 | bandwidth_limit, 163 | &mut stats, 164 | ); 165 | 166 | let actual_total_size: usize = buffer.iter().map(|p| p.packet.data.len()).sum(); 167 | assert!(actual_total_size <= MAX_BUFFER_SIZE); 168 | } 169 | 170 | #[test] 171 | fn test_no_bandwidth_limiting() { 172 | let mut packets = vec![ 173 | PacketData::from(create_dummy_packet(1000)), 174 | PacketData::from(create_dummy_packet(1000)), 175 | ]; 176 | let mut buffer = VecDeque::new(); 177 | let mut total_buffer_size = 0; 178 | let mut last_send_time = Instant::now() - Duration::from_secs(1); 179 | let bandwidth_limit = 10_000; // 10 MB/s 180 | let mut stats = BandwidthStats::new(0.5); 181 | 182 | bandwidth_limiter( 183 | &mut packets, 184 | &mut buffer, 185 | &mut total_buffer_size, 186 | &mut last_send_time, 187 | bandwidth_limit, 188 | &mut stats, 189 | ); 190 | 191 | assert_eq!(packets.len(), 2); 192 | } 193 | 194 | #[test] 195 | fn test_zero_bandwidth() { 196 | let mut packets = vec![ 197 | PacketData::from(create_dummy_packet(1000)), 198 | PacketData::from(create_dummy_packet(1000)), 199 | ]; 200 | let mut buffer = VecDeque::new(); 201 | let mut total_buffer_size = 0; 202 | let mut last_send_time = Instant::now(); 203 | let bandwidth_limit = 0; // 0 KB/s 204 | let mut stats = BandwidthStats::new(0.5); 205 | 206 | bandwidth_limiter( 207 | &mut packets, 208 | &mut buffer, 209 | &mut total_buffer_size, 210 | &mut last_send_time, 211 | bandwidth_limit, 212 | &mut stats, 213 | ); 214 | 215 | assert!(packets.is_empty()); 216 | assert_eq!(buffer.len(), 2); 217 | } 218 | 219 | #[test] 220 | fn test_empty_packet_vector() { 221 | let mut packets = Vec::new(); 222 | let mut buffer = VecDeque::new(); 223 | let mut total_buffer_size = 0; 224 | let mut last_send_time = Instant::now(); 225 | let bandwidth_limit = 10_000; // 10 MB/s 226 | let mut stats = BandwidthStats::new(0.5); 227 | 228 | bandwidth_limiter( 229 | &mut packets, 230 | &mut buffer, 231 | &mut total_buffer_size, 232 | &mut last_send_time, 233 | bandwidth_limit, 234 | &mut stats, 235 | ); 236 | 237 | // Since the packets vector was empty, buffer should remain empty and nothing should be sent 238 | assert!(packets.is_empty()); 239 | assert!(buffer.is_empty()); 240 | } 241 | 242 | #[test] 243 | fn test_add_packet_to_buffer() { 244 | let mut buffer = VecDeque::new(); 245 | let mut total_size = 0; 246 | let packet = PacketData::from(create_dummy_packet(1000)); 247 | 248 | add_packet_to_buffer(&mut buffer, packet.clone(), &mut total_size); 249 | 250 | assert_eq!(buffer.len(), 1); 251 | assert_eq!(total_size, 1000); 252 | assert_eq!(buffer.front().unwrap().packet.data.len(), 1000); 253 | } 254 | 255 | #[test] 256 | fn test_add_packets_to_buffer() { 257 | let mut buffer = VecDeque::new(); 258 | let mut total_size = 0; 259 | let mut packets = vec![ 260 | PacketData::from(create_dummy_packet(1000)), 261 | PacketData::from(create_dummy_packet(2000)), 262 | ]; 263 | 264 | add_packets_to_buffer(&mut buffer, &mut packets, &mut total_size); 265 | 266 | assert_eq!(buffer.len(), 2); 267 | assert_eq!(total_size, 3000); 268 | assert_eq!(buffer.pop_front().unwrap().packet.data.len(), 2000); 269 | assert_eq!(buffer.pop_front().unwrap().packet.data.len(), 1000); 270 | } 271 | 272 | #[test] 273 | fn test_remove_packet_from_buffer() { 274 | let mut buffer = VecDeque::new(); 275 | let mut total_size = 0; 276 | let packet = PacketData::from(create_dummy_packet(1000)); 277 | add_packet_to_buffer(&mut buffer, packet.clone(), &mut total_size); 278 | let mut stats = BandwidthStats::new(0.5); 279 | 280 | let removed_packet = remove_packet_from_buffer(&mut buffer, &mut total_size, &mut stats); 281 | 282 | assert_eq!(removed_packet.unwrap().packet.data.len(), 1000); 283 | assert_eq!(buffer.len(), 0); 284 | assert_eq!(total_size, 0); 285 | } 286 | 287 | #[test] 288 | fn test_remove_packet_from_empty_buffer() { 289 | let mut buffer = VecDeque::new(); 290 | let mut total_size = 0; 291 | let mut stats = BandwidthStats::new(0.5); 292 | 293 | let removed_packet = remove_packet_from_buffer(&mut buffer, &mut total_size, &mut stats); 294 | 295 | assert!(removed_packet.is_none()); 296 | assert_eq!(buffer.len(), 0); 297 | assert_eq!(total_size, 0); 298 | } 299 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [0.6.1] - 2024-08-21 6 | 7 | ### Bug Fixes 8 | 9 | - Fix bandwidth remove packet method statistics tracking not saturating subtraction 10 | 11 | 12 | ### Documentation 13 | 14 | - Update README.md to be up to date and more comprehensive 15 | 16 | - Update filter validation to make a wrapper method which adds the documentation url only for CLI use 17 | 18 | - Update filter validation logic to link to the filter docs 19 | 20 | - Update README.md to be up to date 21 | 22 | - Update README.md to add screenshot 23 | 24 | ### Features 25 | 26 | - Add text area extension method to draw dimmed placeholder text 27 | 28 | 29 | ### Updates 30 | 31 | - Update tui initialization logic to initialize values of fields if they weren't specified in the cli 32 | 33 | - Update packet processing to not log if there is no filter 34 | 35 | - Update filter validation to open the wd handle in packet sniffing mode 36 | 37 | - Update packet_receiving to only log the missing WinDivert hanld error once as to not freeze up the TUI 38 | 39 | - Update style_textarea_based_on_validation to not override the existing title 40 | 41 | 42 | ## [0.6.0] - 2024-08-20 43 | 44 | ### Bug Fixes 45 | 46 | - Fix start stop toggle input working while focus is on filter aswell 47 | 48 | - Fix TextArea set_text extension not overriding existing text 49 | 50 | - Fix sending and receiving windivert instances being ont he same priority causing one to randomly absorb all the packets 51 | 52 | 53 | ### Documentation 54 | 55 | - Add docstrings to trait methods for CLI and TUI extension traits 56 | 57 | 58 | ### Features 59 | 60 | - Add start and stop toggle to be able to toggle all processing 61 | 62 | - Add keybind info for filter and logs widgets 63 | 64 | - Add conditional block border highlight method to unify highlighting logic 65 | 66 | - Add BandwidthStats to track throughput and storage buffer packet count 67 | 68 | - Add DuplicateStats to track duplicate multiplier and display it in the tui 69 | 70 | - Add TamperStats to visualize the packet data being tampered 71 | 72 | - Add ReorderStats to keep track of reordering percentage and number of delayed packets 73 | 74 | - Add ThrottleStats to keep track of the throttling state and package drop count 75 | 76 | - Add DelayStats to keep track of the number of actively delayed packets 77 | 78 | - Add statistic tracking support 79 | 80 | - Add TUI support using ratatui 81 | 82 | - Update main to add a separate thread for packet processing 83 | 84 | 85 | ### Moves 86 | 87 | - Move EWMA to util subfolder 88 | 89 | 90 | ### Refactors 91 | 92 | - Refactor TUI widgets by modularazing utility functions and improving code organization 93 | 94 | - Refactor packet manipulation settings to use optional structs and improve code consistency 95 | 96 | This commit refactors various packet manipulation settings to 97 | encapsulate their fields within optional structs. This change improves 98 | code consistency by grouping related settings together and ensuring that 99 | they are either fully provided or omitted as a whole. Additionally, the 100 | code has been cleaned up to remove unnecessary unwraps and serialization 101 | logic, resulting in more robust and maintainable code. The update also 102 | includes improvements in how CLI state is initialized and updated, 103 | reducing redundancy and improving clarity. 104 | 105 | - Refactor tui cli methods to reduce nesting via early return unpacks 106 | 107 | - Refactor rest of tui cli methods to reduce repetition 108 | 109 | - Refactor tui updating from statistics method to reduce repetition 110 | 111 | - Refactor CLI and TUI state management: Rename AppState to TuiState, introduce extension traits for modular updates 112 | 113 | - Refactor main.rs to extract cli and tui state interaction to separate module 114 | 115 | - Refactor cli updating logic to extract text area parsing logic into extension methods 116 | 117 | 118 | ### Removals 119 | 120 | - Remove validate_probability method for text areas 121 | 122 | - Remove validate_usize method for text areas which was doing both validation and block rendering updates 123 | 124 | 125 | ### Styling 126 | 127 | - Style code using fmt 128 | 129 | - Clean up code using Clippy 130 | 131 | 132 | ### Updates 133 | 134 | - Update project to 0.6.0 135 | 136 | - Update TUI cli command to default to false 137 | 138 | - Improve console statistic logging 139 | 140 | - Improve custom logger to have the capabilities of the default logger 141 | 142 | - Update tui input to use tui state for better isolation 143 | 144 | - Improve FilterWidget validation logic by caching last valid filter state and reverting to it on escape 145 | 146 | - Update LogsWidget to give feedback upon chaning log level 147 | 148 | - Update Widgets to handle Enter the same as Esc as to prevent entering of new lines in text areas 149 | 150 | - Improve TamperWidget validation logic 151 | 152 | - Improve ReorderWidget validation logic 153 | 154 | - Improve DuplicateWidget validation logic 155 | 156 | - Improve ThrottleWidget validation logic 157 | 158 | - Improve DropWidget validation logic 159 | 160 | - Improve DelayWidget validation logic 161 | 162 | - Improve BandiwdthWidget validation logic 163 | 164 | - Update FilterWidget and packet receiving thread to be able to change the filter via the tui 165 | 166 | - Update sending and receiving windivert handles to set matching flags to send and read only 167 | 168 | - Update TamperWidget to change info block border color logic 169 | 170 | - Update main to unify thread joining methods 171 | 172 | 173 | ## [0.5.0] - 2024-08-07 174 | 175 | ### Bug Fixes 176 | 177 | - Fix packet manipulation module tests failing 178 | 179 | - Squash merge fixing-packet-capture into develop 180 | 181 | 182 | ### Documentation 183 | 184 | - Update CHANGELOG.md to contain 0.5.0 changes 185 | 186 | - Update README.md to be up to date with the latest changes 187 | 188 | - Update README.md to mention known issues 189 | 190 | 191 | ### Features 192 | 193 | - Add configuration file support and packet manipulation settings serialization 194 | 195 | 196 | ### Refactors 197 | 198 | - Refactor configuration management to use the users configuration directory 199 | 200 | - Refactor CLI architecture to modularize cli components 201 | 202 | - Squash merge cleanup-refactor into develop 203 | 204 | 205 | ### Removals 206 | 207 | - Remove unused import in tamper.rs 208 | 209 | 210 | ### Revert 211 | 212 | - Revert back to using standard threads instead of tokio async 213 | 214 | even though tokio async looked promising as a way to get around the 215 | WinDivert receive blocking call testing showed that it caused packages 216 | getting skipped when sent at low amounts. 217 | 218 | 219 | ### Styling 220 | 221 | - Style code using fmt 222 | 223 | - Clean up code using clippy 224 | 225 | 226 | ### Updates 227 | 228 | - Update duplicate packets method to use the count number as the duplicate count not the total count of outgoing packet copies 229 | 230 | - Update project to 0.4.2 231 | 232 | 233 | ## [0.4.0] - 2024-08-05 234 | 235 | ### Bug Fixes 236 | 237 | - Fix filter validation method not properly closing WinDivert handle 238 | 239 | - Fix tests to use new Probability type 240 | 241 | 242 | ### Documentation 243 | 244 | - Document packet tampering functionality in README.md 245 | 246 | 247 | ### Features 248 | 249 | - Add async packet receiving and processing with Tokio integration 250 | 251 | - Implement package tampering support and CLI commands 252 | 253 | 254 | ### Removals 255 | 256 | - Remove unused extract_payload method from tamper.rs 257 | 258 | 259 | ### Styling 260 | 261 | - Clean up comments and logs in capture logic 262 | 263 | - Style capture.rs using fmt 264 | 265 | - Clean up unused imports in capture file 266 | 267 | - Style scripts using fmt 268 | 269 | - Clean up tamper logic using clippy 270 | 271 | 272 | ### Updates 273 | 274 | - Update CHANGELOG to contain 0.4.0 changes 275 | 276 | - Update project version to 0.4.0 277 | 278 | - Update Cargo.toml to remove unused tokio features 279 | 280 | 281 | ## [0.3.0] - 2024-08-03 282 | 283 | ### Bug Fixes 284 | 285 | - Fix capture not clearing packets vector in between loop iterations 286 | 287 | - Fix start_packet_processing method usign the wrong packets vector 288 | 289 | 290 | ### Documentation 291 | 292 | - Update README.md with feature roadmap 293 | 294 | - Update README.md to increment version number in installation example 295 | 296 | - Update CHANGELOG.md with changes for v0.2.0 297 | 298 | 299 | ### Features 300 | 301 | - Implement forced exit on double Ctrl + C for immediate termination 302 | 303 | - Add filter port validation support 304 | 305 | - Add graceful shutdown and improved loggin with Ctrl-C signal handling 306 | 307 | - Refactor capture.rs to add a PacketProcessingState structs for easier state passing between methods 308 | 309 | 310 | ### Refactors 311 | 312 | - Refactor CLI argument parsing with custom comamnd and field names 313 | 314 | 315 | ### Renames 316 | 317 | - Update .idea meta files to rename project to fumble 318 | 319 | 320 | ### Styling 321 | 322 | - Clean up scripts using fmt 323 | 324 | - Clean up scripts using clippy 325 | 326 | 327 | ### Testing 328 | 329 | - Add unit tests for bandwidth limiting functionality 330 | 331 | 332 | ### Updates 333 | 334 | - Update project to 0.3.0 335 | 336 | - Update main method to extract Ctrl-C handing logic to submethod 337 | 338 | - Update bandwidth limiter to define the max buffer size using memory size and not package count to better handle high amounts of small packets 339 | 340 | - Update Cargo.toml package version to 0.2.0 341 | 342 | 343 | ## [0.2.0] - 2024-08-02 344 | 345 | ### Bug Fixes 346 | 347 | - Update github workflow action to try and fix WinDivert download 348 | 349 | 350 | ### Documentation 351 | 352 | - Update README.md to mention new throttling feature 353 | 354 | - Update README.md to mention new reorder and bandwidth limitin functionalities 355 | 356 | - Update CHANGELOG.md with changes for v0.1.0 357 | 358 | 359 | ### Features 360 | 361 | - Add throttling feature to temporarily hold or drop packets to simulate sporadic network throttling 362 | 363 | - Add bandwidth limiting feature to control package transmission rate 364 | 365 | - Implement package capture on a separate thread as to not block processing of delayed packets while there are no new packets 366 | 367 | - Add packet reordering feature with CLI support 368 | 369 | 370 | ### Refactors 371 | 372 | - Refactor main.rs to extract logic into methods and move them into the right files 373 | 374 | - Refactor delay packet storage to use VecDeque for FIFO order 375 | 376 | 377 | ### Styling 378 | 379 | - Style files using fmt 380 | 381 | 382 | ### Updates 383 | 384 | - Update github workflow action to set WinDivert download source and to remove linux build 385 | 386 | - Update github workflow action to handle windows builds 387 | 388 | 389 | ## [0.1.0] - 2024-07-31 390 | 391 | ### Documentation 392 | 393 | - Add README.md 394 | 395 | - Add LICENSE.md 396 | 397 | 398 | ### Features 399 | 400 | - Add github workflow for automatic building and testing when pushing to main 401 | 402 | - Implement automatic changelog generation using cliff config 403 | 404 | - Implement improved logging with env_logger setup and detailed CLI help 405 | 406 | - Add packet delay feature with CLI support for delay duration 407 | 408 | - Add packet duplication feature with CLI support for count and probability 409 | 410 | - Update capture.ts to add Clone implementation 411 | 412 | - Add drop probability value validation 413 | 414 | - Implement From trait for PacketData 415 | 416 | - Add better logging 417 | 418 | - Add better error handling for WinDivert initialization 419 | 420 | - Add library interface 421 | 422 | - Add CLI network filter specification support 423 | 424 | - Add CLI support using Clap 425 | 426 | - Add RustRover meta files 427 | 428 | - Add dependencies and implement basic packet dropping functionality with logging 429 | 430 | 431 | ### Refactors 432 | 433 | - Refactor project structure to modularize network and utility functions 434 | 435 | 436 | ### Renames 437 | 438 | - Rename the project to fumble 439 | 440 | 441 | ### Testing 442 | 443 | - Add Unit tests for capture and dropping logic -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Test](https://github.com/bornacvitanic/fumble/actions/workflows/rust.yml/badge.svg)](https://github.com/bornacvitanic/fumble/actions/workflows/rust.yml) 2 | [![dependency status](https://deps.rs/repo/github/bornacvitanic/fumble/status.svg)](https://deps.rs/repo/github/bornacvitanic/fumble) 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | [![Crates.io](https://img.shields.io/crates/v/fumble.svg)](https://crates.io/crates/fumble) 5 | [![Download](https://img.shields.io/badge/download-releases-blue.svg)](https://github.com/bornacvitanic/fumble/releases) 6 | 7 | # fumble 8 | 9 | fumble is an oxidized (Rust-based) implementation of the original clumsy tool, designed to simulate adverse network conditions on Windows systems. Utilizing the powerful capabilities of the WinDivert library, fumble intercepts live network packets and allows users to introduce controlled delays, drops, duplications, and modifications to these packets. This tool is invaluable for debugging network-related bugs, testing application resilience under poor network conditions, and evaluating performance in unreliable network environments. 10 | 11 | Just like its predecessor, fumble offers a user-friendly and interactive way to degrade network performance intentionally, making it easier to diagnose issues and improve the robustness of network-dependent applications. Whether you're a developer needing to simulate a flaky connection or a QA engineer stress-testing an application, fumble provides a versatile and reliable solution. 12 | 13 | ![image](https://github.com/user-attachments/assets/857b528f-8b0d-4c51-a777-c7fe84e9e4cb) 14 | 15 | ## Features 16 | ### Packet Manipulation Features 17 | - **Packet Filtering**: Use filter expressions to capture specific packets. 18 | - **Packet Dropping**: Drop packets with a specified probability. 19 | - **Packet Delay**: Introduce delays to simulate latency. 20 | - **Packet Throttling**: Temporarily hold or drop packets to simulate sporadic network throttling. 21 | - **Packet Reordering**: Reorder packets by applying a random delay to simulate out-of-order delivery. 22 | - **Packet Tampering:** Modify packet payloads by altering, flipping, or injecting data to simulate corrupted transmissions. 23 | - **Packet Duplication**: Duplicate packets to simulate packet duplication issues. 24 | - **Bandwidth Limiting**: Limit the bandwidth to simulate a constrained network environment. 25 | ### Binary Features 26 | - **General CLI Usage:** Utilize a comprehensive command-line interface for flexible and detailed control over network manipulation settings. Easily specify parameters for packet filtering, dropping, delaying, throttling, reordering, tampering, duplicating, and bandwidth limiting. 27 | - **Configuration Support:** Easily manage your settings through configuration files. Create, list, and use configuration files to save and load your preferred settings, simplifying the setup and ensuring consistent behavior across different runs. 28 | - **Text User Interface (TUI) Mode:** a Text User Interface (TUI) for users who prefer an interactive and visual interface over the command line. The TUI provides a more user-friendly way to configure and manage network manipulation settings in real-time. 29 | 30 | ## Roadmap 31 | 32 | - **TUI/CLI Enhancements:** Enhance the Text User Interface (TUI) to offer a graph visualization of the network traffic and all the modifications being applied by fumble. 33 | - **Graphical User Interface (GUI):** Implement a GUI to cater to users who prefer not to use the command line. 34 | 35 | ## Requirements 36 | 37 | **Important:** `fumble` requires `WinDivert.dll` and `WinDivert64.sys` to function properly. You can download them from the [official WinDivert releases page](https://github.com/basil00/Divert/releases). 38 | 39 |
40 | Installing WinDivert 41 | 42 | 1. Download the latest version of `WinDivert.dll` and `WinDivert64.sys`. 43 | 2. Place `WinDivert.dll` and `WinDivert64.sys` in the same directory as the `fumble` binary executable, or add the directory containing these files to your system's `PATH` environment variable. 44 | 45 |
46 | 47 | ## Installation 48 | ### From Source 49 | To build `fumble`, ensure you have Rust and Cargo installed.\ 50 | Clone the repository and build the project using Cargo: 51 | 52 | ```sh 53 | git clone https://github.com/bornacvitanic/fumble.git 54 | cd fumble 55 | cargo build --release 56 | ``` 57 | To ensure proper functionality, place WinDivert.dll and WinDivert64.sys in the same directory as the fumble binary (typically `./target/debug` or `./target/release`). Alternatively, you can add the directory containing these files to your system's `PATH` environment variable. 58 | 59 | ### From GitHub Releases 60 | You can download pre-built binaries from the [GitHub Releases](https://github.com/bornacvitanic/fumble/releases) page: 61 | 62 | 1. Download the appropriate release for your platform. 63 | 2. Extract the files from the release archive. 64 | 65 | The release archive already contains a copy of the `WinDivert.dll` and `WinDivert64.sys` files. 66 | 67 | ### From crates.io as a CLI 68 | 69 | To install fumble as a command-line tool globally, use: 70 | 71 | ```sh 72 | cargo install fumble 73 | ``` 74 | 75 | This installs the fumble binary, enabling you to use the CLI tool globally. 76 | After installation, ensure that `WinDivert.dll` and `WinDivert64.sys` are placed in the same directory as the fumble binary (typically located at `C:\Users\username\.cargo\bin` on Windows). Alternatively, you can add the directory containing these files to your system's `PATH` environment variable. 77 | 78 | ### Using fumble as a Library 79 | 80 | To include `fumble` as a dependency in your Rust project, add the following to your `Cargo.toml`: 81 | 82 | ```toml 83 | [dependencies] 84 | fumble = "0.6.0" 85 | ``` 86 | 87 | Run cargo build to download and compile the crate.\ 88 | To ensure proper functionality, place WinDivert.dll and WinDivert64.sys in the same directory as the fumble binary (typically `./target/debug` or `./target/release`). Alternatively, you can add the directory containing these files to your system's `PATH` environment variable. 89 | 90 | ## Usage 91 | 92 | Run the `fumble` executable with the desired options: 93 | 94 | ```sh 95 | fumble --filter "inbound and tcp" --delay-duration 500 --drop-probability 0.1 96 | ``` 97 | 98 | ## TUI Mode 99 | 100 | fumble offers a Text User Interface (TUI) mode for those who prefer a more interactive experience. The TUI allows you to view, configure, and manage network manipulation settings in a visual interface, making it easier to adjust settings on the fly. You can initialise the TUI via either a config or normal cli commands. 101 | 102 | ### Launching the TUI 103 | 104 | To start `fumble` in TUI mode, use the following command: 105 | 106 | ```sh 107 | fumble -t 108 | ``` 109 | Once in the TUI, you can navigate through different settings using your keyboard. The TUI provides real-time feedback and allows for quick adjustments to your configurations. 110 | 111 | You can initialize the TUI with default values from either individual commands of a config. You can also specify a initial filter: 112 | ```sh 113 | fumble --filter "outbound and udp" -t 114 | ``` 115 | ```sh 116 | fumble --filter "outbound and udp" --delay-duration 500 -t 117 | ``` 118 | ```sh 119 | fumble --filter "inbound and udp" --use-config config_name -t 120 | ``` 121 | 122 |
123 | Command-Line Options 124 | 125 | - `-f, --filter `: Filter expression for capturing packets. 126 | - `--drop-probability `: Probability of dropping packets, ranging from 0.0 to 1.0. 127 | - `--delay-duration `: Delay in milliseconds to introduce for each packet. 128 | - `--throttle-probability `: Probability of triggering a throttle event, ranging from 0.0 to 1.0. 129 | - `--throttle-duration `: Duration in milliseconds for which throttling should be applied. 130 | - **Default**: `30` 131 | - `--throttle-drop`: Indicates whether throttled packets should be dropped. 132 | - `--reorder-probability `: Probability of reordering packets, ranging from 0.0 to 1.0. 133 | - `--reorder-max-delay `: Maximum random delay in milliseconds to apply when reordering packets. 134 | - **Default**: `100` 135 | - `--tamper-probability `: Probability of tampering packets, ranging from 0.0 to 1.0. 136 | - `--tamper-amount `: Amount of tampering that should be applied, ranging from 0.0 to 1.0. 137 | - **Default**: `0.1` 138 | - `--tamper-recalculate-checksums `: Whether tampered packets should have their checksums recalculated to mask the tampering and avoid the packets getting automatically dropped. 139 | - **Possible values**: `true`, `false` 140 | - `--duplicate-probability `: Probability of duplicating packets, ranging from 0.0 to 1.0. 141 | - `--duplicate-count `: Number of times to duplicate each packet. 142 | - **Default**: `1` 143 | - `--bandwidth-limit `: Maximum bandwidth limit in KB/s. 144 | - `-t, --tui`: Launch the Text User Interface (TUI). 145 | - `-h, --help`: Print help (see a summary with `-h`). 146 | 147 | **Configuration Management:** 148 | 149 | - `--create-default `: Command to create a default configuration file with the specified name. 150 | - `--use-config `: Command to use an existing configuration file based on the specified name. 151 | - `--list-configs`: Command to list all available configuration files. 152 |
153 |
154 | Examples 155 | 156 | - Drop 10% of incoming TCP packets: 157 | 158 | ```sh 159 | fumble --filter "inbound and tcp" --drop-probability 0.1 160 | ``` 161 | 162 | - Delay packets by 500 milliseconds: 163 | 164 | ```sh 165 | fumble --filter "inbound and tcp" --delay-duration 500 166 | ``` 167 | 168 | - Throttle packets with a 10% probability for 30 milliseconds and drop them: 169 | 170 | ```sh 171 | fumble --filter "inbound and tcp" --throttle-probability 0.1 --throttle-duration 30 --throttle-drop 172 | ``` 173 | 174 | - Throttle packets with a 20% probability for 50 milliseconds and delay them: 175 | 176 | ```sh 177 | fumble --filter "inbound and tcp" --throttle-probability 0.2 --throttle-duration 50 178 | ``` 179 | 180 | - Reorder packets with a 10% probability and a maximum delay of 100 milliseconds: 181 | 182 | ```sh 183 | fumble --filter "inbound and tcp" --reorder-probability 0.1 --reorder-max-delay 100 184 | ``` 185 | 186 | - Tamper packets with a 25% probability and a tamper amount of 0.2, recalculating checksums: 187 | 188 | ```sh 189 | fumble --filter "inbound and tcp" --tamper-probability 0.25 --tamper-amount 0.2 --tamper-recalculate-checksums true 190 | ``` 191 | 192 | - Tamper packets with a 30% probability, and do not recalculate checksums: 193 | 194 | ```sh 195 | fumble --filter "inbound and tcp" --tamper-probability 0.3 --tamper-recalculate-checksums false 196 | ``` 197 | 198 | - Duplicate packets with a 50% chance: 199 | 200 | ```sh 201 | fumble --filter "inbound and tcp" --duplicate-probability 0.5 --duplicate-count 2 202 | ``` 203 | 204 | - Limit bandwidth to 100 KB/s: 205 | 206 | ```sh 207 | fumble --filter "inbound and tcp" --bandwidth-limit 100 208 | ``` 209 |
210 | 211 | ## Logging 212 | 213 | The tool uses the env_logger crate for logging. By default, informational messages are shown. 214 | 215 | ### Enabling Detailed Logs 216 | 217 | To see more detailed logs, set the `RUST_LOG` environment variable before running `fumble`. 218 | 219 | ## Contributing 220 | 221 | Contributions are welcome! Please open an issue or submit a pull request. 222 | 223 | ## License 224 | 225 | This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) file for details. 226 | 227 | ## Acknowledgements 228 | 229 | - [clap](https://crates.io/crates/clap) - A command-line argument parser for Rust that provides a simple and powerful API for defining complex CLI interfaces. 230 | - [windivert](https://crates.io/crates/windivert) - A Rust binding for the WinDivert library, used for network packet interception and manipulation. 231 | - [rand](https://crates.io/crates/rand) - A Rust library for generating random numbers, used for implementing random packet dropping and duplication. 232 | - [ratatui](https://crates.io/crates/ratatui) - A Rust library for building terminal user interfaces with an emphasis on simplicity and ease of use. 233 | - [tui-textarea](https://crates.io/crates/tui-textarea) - A Rust crate for managing text input within terminal user interfaces. 234 | - [lazy_static](https://crates.io/crates/lazy_static) - A Rust macro for defining statically initialized variables that are computed lazily. 235 | - [ctrlc](https://crates.io/crates/ctrlc) - A Rust library for handling Ctrl-C signals, enabling graceful shutdowns and clean thread termination. 236 | - [regex](https://crates.io/crates/regex) - A Rust library for regular expressions, used for string matching operations. 237 | - [env_logger](https://crates.io/crates/env_logger) - A simple logger for Rust applications that can be configured via environment variables. 238 | - [log](https://crates.io/crates/log) - A logging facade that provides a common interface for various log implementations. 239 | - [serde](https://crates.io/crates/serde) - For serialization and deserialization of configuration files. 240 | - [toml](https://crates.io/crates/toml) - For parsing and serializing TOML configuration files. 241 | - [dirs](https://crates.io/crates/dirs) - For handling configuration directories across different operating systems. 242 | - [thiserror](https://crates.io/crates/thiserror) - For ergonomic error handling. 243 | 244 | ## Contact 245 | 246 | - **Email**: [borna.cvitanic@gmail.com](mailto:borna.cvitanic@gmail.com) 247 | - **GitHub Issues**: [GitHub Issues Page](https://github.com/bornacvitanic/fumble/issues) -------------------------------------------------------------------------------- /src/cli/tui/cli_ext.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::settings::bandwidth::BandwidthOptions; 2 | use crate::cli::settings::delay::DelayOptions; 3 | use crate::cli::settings::drop::DropOptions; 4 | use crate::cli::settings::duplicate::DuplicateOptions; 5 | use crate::cli::settings::reorder::ReorderOptions; 6 | use crate::cli::settings::tamper::TamperOptions; 7 | use crate::cli::settings::throttle::ThrottleOptions; 8 | use crate::cli::tui::state::TuiState; 9 | use crate::cli::tui::traits::IsActive; 10 | use crate::cli::tui::widgets::custom_widget::CustomWidget; 11 | use crate::cli::Cli; 12 | use crate::network::modules::stats::PacketProcessingStatistics; 13 | use log::error; 14 | use std::sync::{Arc, Mutex, RwLock}; 15 | use crate::network::types::probability::Probability; 16 | 17 | pub trait TuiStateExt { 18 | /// Creates a `TuiState` instance from the current state of the `Cli` object. 19 | /// This function initializes the `TuiState` based on the settings stored in the `Cli`. 20 | fn from_cli(cli: &Arc>) -> Self; 21 | 22 | /// Updates the `TuiState` with the latest statistics from the packet processing. 23 | /// This function refreshes the widgets in the `TuiState` using data from the provided `PacketProcessingStatistics`. 24 | fn update_from(&mut self, statistics: &Arc>); 25 | } 26 | 27 | impl TuiStateExt for TuiState<'_> { 28 | fn from_cli(cli: &Arc>) -> Self { 29 | let mut state = TuiState::new(); 30 | init_tui_state_from_cli(&mut state, cli); 31 | state 32 | } 33 | 34 | fn update_from(&mut self, statistics: &Arc>) { 35 | update_tui_state_from_statistics(self, statistics); 36 | } 37 | } 38 | 39 | pub trait CliExt { 40 | /// Updates the `Cli` object based on the current state of the `TuiState`. 41 | /// This function applies the user inputs from the TUI to the `Cli`, synchronizing its settings with the interface state. 42 | fn update_from(&self, state: &mut TuiState); 43 | 44 | fn clear_state(&self); 45 | } 46 | 47 | impl CliExt for Arc> { 48 | fn update_from(&self, state: &mut TuiState) { 49 | update_cli_from_tui_state(state, self); 50 | } 51 | 52 | fn clear_state(&self) { 53 | let mut cli = match self.lock() { 54 | Ok(cli) => cli, 55 | Err(e) => { 56 | error!("Failed to lock CLI mutex. {}", e); 57 | return; 58 | } 59 | }; 60 | 61 | cli.packet_manipulation_settings.drop = None; 62 | cli.packet_manipulation_settings.delay = None; 63 | cli.packet_manipulation_settings.throttle = None; 64 | cli.packet_manipulation_settings.reorder = None; 65 | cli.packet_manipulation_settings.tamper = None; 66 | cli.packet_manipulation_settings.duplicate = None; 67 | cli.packet_manipulation_settings.bandwidth = None; 68 | } 69 | } 70 | 71 | fn init_tui_state_from_cli(state: &mut TuiState, cli: &Arc>) { 72 | let cli = match cli.lock() { 73 | Ok(cli) => cli, 74 | Err(e) => { 75 | error!("Failed to lock CLI mutex. {}", e); 76 | return; 77 | } 78 | }; 79 | 80 | if let Some(filter) = &cli.filter { 81 | state.filter_widget.set_filter(filter); 82 | } 83 | for section in state.sections.iter_mut() { 84 | match section { 85 | CustomWidget::Drop(ref mut drop_widget) => { 86 | if let Some(drop) = &cli.packet_manipulation_settings.drop { 87 | drop_widget.set_probability(drop.probability); 88 | drop_widget.set_active(true); 89 | } else { 90 | drop_widget.set_probability(Probability::new(0.1).unwrap()); 91 | } 92 | } 93 | CustomWidget::Delay(ref mut delay_widget) => { 94 | if let Some(delay) = &cli.packet_manipulation_settings.delay { 95 | delay_widget.set_delay(delay.duration); 96 | delay_widget.set_active(true); 97 | } else { 98 | delay_widget.set_delay(50); 99 | } 100 | } 101 | CustomWidget::Throttle(ref mut throttle_widget) => { 102 | if let Some(throttle) = &cli.packet_manipulation_settings.throttle { 103 | throttle_widget.set_probability(throttle.probability); 104 | throttle_widget.set_throttle_duration(throttle.duration); 105 | throttle_widget.drop = throttle.drop; 106 | throttle_widget.set_active(true); 107 | } else { 108 | throttle_widget.set_probability(Probability::new(0.1).unwrap()); 109 | throttle_widget.set_throttle_duration(30); 110 | } 111 | } 112 | CustomWidget::Reorder(ref mut reorder_widget) => { 113 | if let Some(reorder) = &cli.packet_manipulation_settings.reorder { 114 | reorder_widget.set_probability(reorder.probability); 115 | reorder_widget.set_delay_duration(reorder.max_delay); 116 | reorder_widget.set_active(true); 117 | } else { 118 | reorder_widget.set_probability(Probability::new(0.1).unwrap()); 119 | reorder_widget.set_delay_duration(30); 120 | } 121 | } 122 | CustomWidget::Tamper(ref mut tamper_widget) => { 123 | if let Some(tamper) = &cli.packet_manipulation_settings.tamper { 124 | tamper_widget.set_probability(tamper.probability); 125 | tamper_widget.set_tamper_amount(tamper.amount); 126 | if let Some(recalculate_checksums) = tamper.recalculate_checksums { 127 | tamper_widget.recalculate_checksums = recalculate_checksums; 128 | } 129 | tamper_widget.set_active(true); 130 | } else { 131 | tamper_widget.set_probability(Probability::new(0.1).unwrap()); 132 | tamper_widget.set_tamper_amount(Probability::new(0.1).unwrap()); 133 | } 134 | } 135 | CustomWidget::Duplicate(ref mut duplicate_widget) => { 136 | if let Some(duplicate) = &cli.packet_manipulation_settings.duplicate { 137 | duplicate_widget.set_probability(duplicate.probability); 138 | duplicate_widget.set_duplicate_count(duplicate.count); 139 | duplicate_widget.set_active(true); 140 | } else { 141 | duplicate_widget.set_probability(Probability::new(0.1).unwrap()); 142 | duplicate_widget.set_duplicate_count(1); 143 | } 144 | } 145 | CustomWidget::Bandwidth(ref mut bandwidth_widget) => { 146 | if let Some(bandwidth) = &cli.packet_manipulation_settings.bandwidth { 147 | bandwidth_widget.set_limit(bandwidth.limit); 148 | bandwidth_widget.set_active(true); 149 | } 150 | } 151 | } 152 | } 153 | } 154 | 155 | fn update_cli_from_tui_state(state: &mut TuiState, cli: &Arc>) { 156 | let mut cli = match cli.lock() { 157 | Ok(cli) => cli, 158 | Err(e) => { 159 | error!("Failed to lock CLI mutex. {}", e); 160 | return; 161 | } 162 | }; 163 | 164 | if let Ok(filter) = &state.filter_widget.filter { 165 | cli.filter = Some(filter.to_string()); 166 | } 167 | 168 | for section in state.sections.iter_mut() { 169 | match section { 170 | CustomWidget::Drop(ref mut drop_widget) => { 171 | cli.packet_manipulation_settings.drop = if !drop_widget.is_active() { 172 | None 173 | } else { 174 | match drop_widget.probability { 175 | Ok(probability) => Some(DropOptions { probability }), 176 | Err(_) => None, 177 | } 178 | } 179 | } 180 | CustomWidget::Delay(ref mut delay_widget) => { 181 | cli.packet_manipulation_settings.delay = if !delay_widget.is_active() { 182 | None 183 | } else { 184 | match delay_widget.delay { 185 | Ok(duration) => Some(DelayOptions { duration }), 186 | Err(_) => None, 187 | } 188 | } 189 | } 190 | CustomWidget::Throttle(ref mut throttle_widget) => { 191 | cli.packet_manipulation_settings.throttle = if !throttle_widget.is_active() { 192 | None 193 | } else { 194 | throttle_widget 195 | .probability 196 | .as_ref() 197 | .ok() 198 | .and_then(|probability| { 199 | throttle_widget 200 | .throttle_duration 201 | .as_ref() 202 | .ok() 203 | .map(|duration| ThrottleOptions { 204 | probability: *probability, 205 | duration: *duration, 206 | drop: throttle_widget.drop, 207 | }) 208 | }) 209 | } 210 | } 211 | CustomWidget::Reorder(ref reorder_widget) => { 212 | cli.packet_manipulation_settings.reorder = if !reorder_widget.is_active() { 213 | None 214 | } else { 215 | reorder_widget 216 | .probability 217 | .as_ref() 218 | .ok() 219 | .and_then(|probability| { 220 | reorder_widget 221 | .delay_duration 222 | .as_ref() 223 | .ok() 224 | .map(|max_delay| ReorderOptions { 225 | probability: *probability, 226 | max_delay: *max_delay, 227 | }) 228 | }) 229 | } 230 | } 231 | 232 | CustomWidget::Tamper(ref tamper_widget) => { 233 | cli.packet_manipulation_settings.tamper = 234 | if !tamper_widget.is_active() { 235 | None 236 | } else { 237 | tamper_widget 238 | .probability 239 | .as_ref() 240 | .ok() 241 | .and_then(|probability| { 242 | tamper_widget.tamper_amount.as_ref().ok().map(|amount| { 243 | TamperOptions { 244 | probability: *probability, 245 | amount: *amount, 246 | recalculate_checksums: Some( 247 | tamper_widget.recalculate_checksums, 248 | ), 249 | } 250 | }) 251 | }) 252 | } 253 | } 254 | CustomWidget::Duplicate(ref duplicate_widget) => { 255 | cli.packet_manipulation_settings.duplicate = if !duplicate_widget.is_active() { 256 | None 257 | } else { 258 | duplicate_widget 259 | .probability 260 | .as_ref() 261 | .ok() 262 | .and_then(|probability| { 263 | duplicate_widget.duplicate_count.as_ref().ok().map(|count| { 264 | DuplicateOptions { 265 | probability: *probability, 266 | count: *count, 267 | } 268 | }) 269 | }) 270 | } 271 | } 272 | CustomWidget::Bandwidth(ref bandwidth_widget) => { 273 | cli.packet_manipulation_settings.bandwidth = if !bandwidth_widget.is_active() { 274 | None 275 | } else { 276 | match bandwidth_widget.limit { 277 | Ok(limit) => Some(BandwidthOptions { limit }), 278 | Err(_) => None, 279 | } 280 | } 281 | } 282 | } 283 | } 284 | } 285 | 286 | fn update_tui_state_from_statistics( 287 | state: &mut TuiState, 288 | statistics: &Arc>, 289 | ) { 290 | let stats = match statistics.read() { 291 | Ok(stats) => stats, 292 | Err(e) => { 293 | error!("Failed to lock statistics read RwLock. {}", e); 294 | return; 295 | } 296 | }; 297 | 298 | for section in state.sections.iter_mut() { 299 | if section.is_active() { 300 | match section { 301 | CustomWidget::Drop(ref mut drop_widget) => { 302 | drop_widget.update_data(&stats.drop_stats); 303 | } 304 | CustomWidget::Delay(ref mut delay_widget) => { 305 | delay_widget.update_data(&stats.delay_stats); 306 | } 307 | CustomWidget::Throttle(ref mut throttle_widget) => { 308 | throttle_widget.update_data(&stats.throttle_stats); 309 | } 310 | CustomWidget::Reorder(ref mut reorder_widget) => { 311 | reorder_widget.update_data(&stats.reorder_stats) 312 | } 313 | CustomWidget::Tamper(ref mut tamper_widget) => { 314 | tamper_widget.update_data(&stats.tamper_stats) 315 | } 316 | CustomWidget::Duplicate(ref mut duplicate_widget) => { 317 | duplicate_widget.update_data(&stats.duplicate_stats) 318 | } 319 | CustomWidget::Bandwidth(ref mut bandwidth_widget) => { 320 | bandwidth_widget.update_data(&stats.bandwidth_stats) 321 | } 322 | } 323 | } 324 | } 325 | } --------------------------------------------------------------------------------