diff --git a/oryx-tui/src/alert/alert.rs b/oryx-tui/src/alert/alert.rs new file mode 100644 index 0000000..ac217dc --- /dev/null +++ b/oryx-tui/src/alert/alert.rs @@ -0,0 +1,111 @@ +use ratatui::{ + layout::{Alignment, Constraint, Direction, Layout, Margin, Rect}, + style::{Color, Style, Stylize}, + text::{Line, Span}, + widgets::{Block, BorderType, Borders, Padding}, + Frame, +}; +use std::sync::{atomic::Ordering, Arc, Mutex}; + +use crate::packets::packet::AppPacket; + +use super::syn_flood::SynFlood; + +#[derive(Debug)] +pub struct Alert { + syn_flood: SynFlood, + pub flash_count: usize, + pub detected: bool, +} + +impl Alert { + pub fn new(packets: Arc>>) -> Self { + Self { + syn_flood: SynFlood::new(packets), + flash_count: 1, + detected: false, + } + } + + pub fn check(&mut self) { + if self.syn_flood.detected.load(Ordering::Relaxed) { + self.detected = true; + self.flash_count += 1; + } else { + self.detected = false; + self.flash_count = 1; + } + } + + pub fn render(&self, frame: &mut Frame, block: Rect) { + frame.render_widget( + Block::default() + .title({ + Line::from(vec![ + Span::from(" Packet ").fg(Color::DarkGray), + Span::from(" Stats ").fg(Color::DarkGray), + { + if self.detected { + if self.flash_count % 12 == 0 { + Span::from(" Alert 󰐼 ").fg(Color::White).bg(Color::Red) + } else { + Span::from(" Alert 󰐼 ").bg(Color::Red) + } + } else { + Span::styled( + " Alert ", + Style::default().bg(Color::Green).fg(Color::White).bold(), + ) + } + }, + ]) + }) + .title_alignment(Alignment::Left) + .padding(Padding::top(1)) + .borders(Borders::ALL) + .style(Style::default()) + .border_type(BorderType::default()) + .border_style(Style::default().green()), + block.inner(Margin { + horizontal: 1, + vertical: 0, + }), + ); + + if !self.detected { + return; + } + + let syn_flood_block = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(10), Constraint::Fill(1)]) + .flex(ratatui::layout::Flex::SpaceBetween) + .margin(2) + .split(block)[0]; + + let syn_flood_block = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), + Constraint::Max(60), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .margin(2) + .split(syn_flood_block)[1]; + + self.syn_flood.render(frame, syn_flood_block); + } + + pub fn title_span(&self) -> Span<'_> { + if self.detected { + if self.flash_count % 12 == 0 { + Span::from(" Alert 󰐼 ").fg(Color::White).bg(Color::Red) + } else { + Span::from(" Alert 󰐼 ").fg(Color::Red) + } + } else { + Span::from(" Alert ").fg(Color::DarkGray) + } + } +} diff --git a/oryx-tui/src/alert/syn_flood.rs b/oryx-tui/src/alert/syn_flood.rs new file mode 100644 index 0000000..b50c1f1 --- /dev/null +++ b/oryx-tui/src/alert/syn_flood.rs @@ -0,0 +1,146 @@ +use std::{ + collections::HashMap, + net::IpAddr, + sync::{atomic::AtomicBool, Arc, Mutex}, + thread, + time::Duration, +}; + +use ratatui::{ + layout::{Alignment, Constraint, Flex, Rect}, + style::{Style, Stylize}, + text::Line, + widgets::{Block, Borders, Row, Table}, + Frame, +}; + +use crate::packets::{ + network::{IpPacket, IpProto}, + packet::AppPacket, +}; + +const WIN_SIZE: usize = 100_000; + +#[derive(Debug)] +pub struct SynFlood { + pub detected: Arc, + pub map: Arc>>, +} + +impl SynFlood { + pub fn new(packets: Arc>>) -> Self { + let map: Arc>> = Arc::new(Mutex::new(HashMap::new())); + + let detected = Arc::new(AtomicBool::new(false)); + + thread::spawn({ + let packets = packets.clone(); + let map = map.clone(); + let detected = detected.clone(); + move || loop { + let start_index = { + let packets = packets.lock().unwrap(); + packets.len().saturating_sub(1) + }; + thread::sleep(Duration::from_secs(5)); + let app_packets = { + let packets = packets.lock().unwrap(); + packets.clone() + }; + + let mut map = map.lock().unwrap(); + map.clear(); + + if app_packets.len() < WIN_SIZE { + continue; + } + + let mut nb_syn_packets = 0; + + app_packets[start_index..app_packets.len().saturating_sub(1)] + .iter() + .for_each(|packet| { + if let AppPacket::Ip(ip_packet) = packet { + if let IpPacket::V4(ipv4_packet) = ip_packet { + if let IpProto::Tcp(tcp_packet) = ipv4_packet.proto { + if tcp_packet.syn == 1 { + nb_syn_packets += 1; + if let Some(count) = + map.get_mut(&IpAddr::V4(ipv4_packet.src_ip)) + { + *count += 1; + } else { + map.insert(IpAddr::V4(ipv4_packet.src_ip), 1); + } + } + } + } + if let IpPacket::V6(ipv6_packet) = ip_packet { + if let IpProto::Tcp(tcp_packet) = ipv6_packet.proto { + if tcp_packet.syn == 1 { + nb_syn_packets += 1; + if let Some(count) = + map.get_mut(&IpAddr::V6(ipv6_packet.src_ip)) + { + *count += 1; + } else { + map.insert(IpAddr::V6(ipv6_packet.src_ip), 1); + } + } + } + } + } + }); + + if (nb_syn_packets as f64 / WIN_SIZE as f64) > 0.45 { + detected.store(true, std::sync::atomic::Ordering::Relaxed); + } else { + detected.store(false, std::sync::atomic::Ordering::Relaxed); + } + } + }); + + Self { map, detected } + } + + pub fn render(&self, frame: &mut Frame, block: Rect) { + let mut ips: Vec<(IpAddr, usize)> = { + let map = self.map.lock().unwrap(); + map.clone().into_iter().collect() + }; + ips.sort_by(|a, b| b.1.cmp(&a.1)); + + ips.retain(|(_, count)| *count > 10_000); + + let top_3_ips = ips.into_iter().take(3); + + let widths = [Constraint::Min(30), Constraint::Min(20)]; + + let rows = top_3_ips.map(|(ip, count)| { + Row::new(vec![ + Line::from(ip.to_string()).centered().bold(), + Line::from(count.to_string()).centered(), + ]) + }); + let table = Table::new(rows, widths) + .column_spacing(2) + .flex(Flex::SpaceBetween) + .header( + Row::new(vec![ + Line::from("IP Address").centered(), + Line::from("Number of SYN packets").centered(), + ]) + .style(Style::new().bold()) + .bottom_margin(1), + ) + .block( + Block::new() + .title(" SYN Flood Attack ") + .borders(Borders::all()) + .border_style(Style::new().yellow()) + .title_alignment(Alignment::Center), + ); + + frame.render_widget(table, block); + } +} diff --git a/oryx-tui/src/app.rs b/oryx-tui/src/app.rs index 0ac0f46..7180e7b 100644 --- a/oryx-tui/src/app.rs +++ b/oryx-tui/src/app.rs @@ -1,23 +1,20 @@ use oryx_common::RawPacket; -use ratatui::layout::{Alignment, Constraint, Direction, Flex, Layout, Margin, Rect}; -use ratatui::style::{Color, Style, Stylize}; -use ratatui::text::{Line, Span}; -use ratatui::widgets::Clear; use ratatui::{ + layout::{Alignment, Constraint, Direction, Flex, Layout, Margin, Rect}, + style::{Color, Style, Stylize}, + text::{Line, Span}, widgets::{ - Block, BorderType, Borders, Cell, HighlightSpacing, Padding, Paragraph, Row, Scrollbar, - ScrollbarOrientation, ScrollbarState, Table, TableState, + Block, BorderType, Borders, Cell, Clear, HighlightSpacing, Padding, Paragraph, Row, + Scrollbar, ScrollbarOrientation, ScrollbarState, Table, TableState, }, Frame, }; -use std::collections::HashMap; -use std::net::IpAddr; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::Duration; use std::{error, thread}; use tui_big_text::{BigText, PixelSize}; +use crate::alert::alert::Alert; use crate::bandwidth::Bandwidth; use crate::filters::filter::Filter; use crate::filters::fuzzy::{self, Fuzzy}; @@ -81,9 +78,7 @@ pub struct App { pub bandwidth: Arc>>, pub show_packet_infos_popup: bool, pub packet_index: Option, - pub syn_flood_map: Arc>>, - pub syn_flood_attck_detected: Arc, - pub alert_flash_count: usize, + pub alert: Alert, } impl Default for App { @@ -97,10 +92,6 @@ impl App { let packets = Arc::new(Mutex::new(Vec::with_capacity(AppPacket::LEN * 1024 * 1024))); let stats = Arc::new(Mutex::new(Stats::default())); let fuzzy = Arc::new(Mutex::new(Fuzzy::default())); - let syn_flood_map: Arc>> = - Arc::new(Mutex::new(HashMap::new())); - - let syn_flood_attck_detected = Arc::new(AtomicBool::new(false)); let (sender, receiver) = kanal::unbounded(); @@ -156,74 +147,6 @@ impl App { } }); - thread::spawn({ - let packets = packets.clone(); - let syn_flood_map = syn_flood_map.clone(); - let syn_flood_attck_detected = syn_flood_attck_detected.clone(); - let win_size = 100_000; - move || loop { - let start_index = { - let packets = packets.lock().unwrap(); - packets.len().saturating_sub(1) - }; - thread::sleep(Duration::from_secs(5)); - let app_packets = { - let packets = packets.lock().unwrap(); - packets.clone() - }; - - let mut map = syn_flood_map.lock().unwrap(); - map.clear(); - - if app_packets.len() < win_size { - continue; - } - - let mut nb_syn_packets = 0; - - app_packets[start_index..app_packets.len().saturating_sub(1)] - .iter() - .for_each(|packet| { - if let AppPacket::Ip(ip_packet) = packet { - if let IpPacket::V4(ipv4_packet) = ip_packet { - if let IpProto::Tcp(tcp_packet) = ipv4_packet.proto { - if tcp_packet.syn == 1 { - nb_syn_packets += 1; - if let Some(count) = - map.get_mut(&IpAddr::V4(ipv4_packet.src_ip)) - { - *count += 1; - } else { - map.insert(IpAddr::V4(ipv4_packet.src_ip), 1); - } - } - } - } - if let IpPacket::V6(ipv6_packet) = ip_packet { - if let IpProto::Tcp(tcp_packet) = ipv6_packet.proto { - if tcp_packet.syn == 1 { - nb_syn_packets += 1; - if let Some(count) = - map.get_mut(&IpAddr::V6(ipv6_packet.src_ip)) - { - *count += 1; - } else { - map.insert(IpAddr::V6(ipv6_packet.src_ip), 1); - } - } - } - } - } - }); - - if (nb_syn_packets as f64 / win_size as f64) > 0.45 { - syn_flood_attck_detected.store(true, std::sync::atomic::Ordering::Relaxed); - } else { - syn_flood_attck_detected.store(false, std::sync::atomic::Ordering::Relaxed); - } - } - }); - Self { running: true, help: Help::new(), @@ -232,7 +155,7 @@ impl App { interface: Interface::default(), filter: Filter::new(), start_sniffing: false, - packets, + packets: packets.clone(), packets_table_state: TableState::default(), fuzzy, notifications: Vec::new(), @@ -246,9 +169,7 @@ impl App { bandwidth, show_packet_infos_popup: false, packet_index: None, - syn_flood_map, - syn_flood_attck_detected, - alert_flash_count: 1, + alert: Alert::new(packets.clone()), } } @@ -323,7 +244,7 @@ impl App { } } Mode::Stats => self.render_stats_mode(frame, mode_block), - Mode::Alerts => self.render_alerts_mode(frame, mode_block), + Mode::Alerts => self.alert.render(frame, mode_block), } // Update filters @@ -647,17 +568,7 @@ impl App { Style::default().bg(Color::Green).fg(Color::White).bold(), ), Span::from(" Stats ").fg(Color::DarkGray), - { - if self.syn_flood_attck_detected.load(Ordering::Relaxed) { - if self.alert_flash_count % 12 == 0 { - Span::from(" Alert 󰐼 ").fg(Color::White).bg(Color::Red) - } else { - Span::from(" Alert 󰐼 ").fg(Color::Red) - } - } else { - Span::from(" Alert ").fg(Color::DarkGray) - } - }, + self.alert.title_span(), ]) }) .title_alignment(Alignment::Left) @@ -767,17 +678,7 @@ impl App { " Stats ", Style::default().bg(Color::Green).fg(Color::White).bold(), ), - { - if self.syn_flood_attck_detected.load(Ordering::Relaxed) { - if self.alert_flash_count % 12 == 0 { - Span::from(" Alert 󰐼 ").fg(Color::White).bg(Color::Red) - } else { - Span::from(" Alert 󰐼 ").fg(Color::Red) - } - } else { - Span::from(" Alert ").fg(Color::DarkGray) - } - }, + self.alert.title_span(), ]) }) .title_alignment(Alignment::Left) @@ -803,102 +704,6 @@ impl App { } } - fn render_alerts_mode(&self, frame: &mut Frame, block: Rect) { - frame.render_widget( - Block::default() - .title({ - Line::from(vec![ - Span::from(" Packet ").fg(Color::DarkGray), - Span::from(" Stats ").fg(Color::DarkGray), - { - if self.syn_flood_attck_detected.load(Ordering::Relaxed) { - if self.alert_flash_count % 12 == 0 { - Span::from(" Alert 󰐼 ").fg(Color::White).bg(Color::Red) - } else { - Span::from(" Alert 󰐼 ").bg(Color::Red) - } - } else { - Span::styled( - " Alert ", - Style::default().bg(Color::Green).fg(Color::White).bold(), - ) - } - }, - ]) - }) - .title_alignment(Alignment::Left) - .padding(Padding::top(1)) - .borders(Borders::ALL) - .style(Style::default()) - .border_type(BorderType::default()) - .border_style(Style::default().green()), - block.inner(Margin { - horizontal: 1, - vertical: 0, - }), - ); - - if !self.syn_flood_attck_detected.load(Ordering::Relaxed) { - return; - } - let syn_flood_block = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(10), Constraint::Fill(1)]) - .flex(ratatui::layout::Flex::SpaceBetween) - .margin(2) - .split(block)[0]; - - let syn_flood_block = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Fill(1), - Constraint::Max(60), - Constraint::Fill(1), - ]) - .flex(ratatui::layout::Flex::SpaceBetween) - .margin(2) - .split(syn_flood_block)[1]; - - let mut attacker_ips: Vec<(IpAddr, usize)> = { - let map = self.syn_flood_map.lock().unwrap(); - map.clone().into_iter().collect() - }; - attacker_ips.sort_by(|a, b| b.1.cmp(&a.1)); - - attacker_ips.retain(|(_, count)| *count > 10_000); - - let top_3 = attacker_ips.into_iter().take(3); - - let widths = [Constraint::Min(30), Constraint::Min(20)]; - - let rows = top_3.map(|(ip, count)| { - Row::new(vec![ - Line::from(ip.to_string()).centered().bold(), - Line::from(count.to_string()).centered(), - ]) - }); - let table = Table::new(rows, widths) - .column_spacing(2) - .flex(Flex::SpaceBetween) - .header( - Row::new(vec![ - Line::from("IP Address").centered(), - Line::from("Number of SYN packets").centered(), - ]) - .style(Style::new().bold()) - .bottom_margin(1), - ) - .block( - Block::new() - .title(" SYN Flood Attack ") - .borders(Borders::all()) - .border_style(Style::new().yellow()) - .title_alignment(Alignment::Center), - ); - - frame.render_widget(table, syn_flood_block); - } - fn render_packet_infos_popup(&self, frame: &mut Frame) { let layout = Layout::default() .direction(Direction::Vertical) @@ -967,11 +772,7 @@ impl App { self.notifications.iter_mut().for_each(|n| n.ttl -= 1); self.notifications.retain(|n| n.ttl > 0); - if self.syn_flood_attck_detected.load(Ordering::Relaxed) { - self.alert_flash_count += 1; - } else { - self.alert_flash_count = 1; - } + self.alert.check(); } pub fn quit(&mut self) { diff --git a/oryx-tui/src/lib.rs b/oryx-tui/src/lib.rs index 68d7fc6..d4e3ba0 100644 --- a/oryx-tui/src/lib.rs +++ b/oryx-tui/src/lib.rs @@ -37,3 +37,8 @@ pub mod packets { pub mod packet; pub mod transport; } + +pub mod alert { + pub mod alert; + pub mod syn_flood; +}