├── .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 | [](https://github.com/bornacvitanic/fumble/actions/workflows/rust.yml)
2 | [](https://deps.rs/repo/github/bornacvitanic/fumble)
3 | [](https://opensource.org/licenses/MIT)
4 | [](https://crates.io/crates/fumble)
5 | [](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 | 
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 | }
--------------------------------------------------------------------------------