Files
sniffnet/src/thread_write_report_functions.rs
2022-09-03 00:30:19 +02:00

543 lines
23 KiB
Rust

//! Module containing functions executed by the thread in charge of updating the output report
//! every ```interval``` seconds, with ```interval``` specified by the user through the
//! ```-i``` command line option.
//!
//! If the ```-i``` option is not specified, the report is updated every 5 seconds.
use std::cmp::Ordering;
use std::cmp::Ordering::Equal;
use std::collections::HashMap;
use std::fs::File;
use std::sync::{Arc, Condvar, Mutex};
use std::time::{Duration};
use std::thread;
use chrono::Local;
use std::io::{BufWriter, Write};
use colored::Colorize;
use thousands::Separable;
use crate::{AddressPort, AppProtocol, ReportInfo, Status};
#[cfg(feature = "unknown_ports")]
use std::collections::HashSet;
#[cfg(feature = "elapsed_time")]
use std::time::{Instant};
#[cfg(feature = "draw_graph")]
use charts::{Chart, ScaleLinear, MarkerType, LineSeriesView};
/// The calling thread enters in a loop in which it waits for ```interval``` seconds and then re-write
/// from scratch the output report file, with updated values.
///
/// # Arguments
///
/// * `lowest_port` - The lowest port number to be considered in the report. Specified by the user
/// through the ```-l``` option.
///
/// * `highest_port` - The highest port number to be considered in the report. Specified by the user
/// through the ```-h``` option.
///
/// * `interval` - Frequency of report updates (value in seconds). Specified by the user through the
/// ```-i``` option.
///
/// * `min_packets` - Minimum number of packets for an address:port pair to be considered in the report.
/// Specified by the user through the ```-m``` option.
///
/// * `device_name` - A String representing the name of th network adapter to be sniffed. Specified by the user through the
/// ```-a``` option.
///
/// * `network_layer` - A String representing the IP version to be filtered. Specified by the user through the
/// ```-n``` option.
///
/// * `transport_layer` - A String representing the transport protocol to be filtered. Specified by the user through the
/// ```-t``` option.
///
/// * `app_layer` - An AppProtocol representing the application protocol to be filtered. Specified by the user through the
/// ```--app``` option.
///
/// * `output_file` - A String representing the output report file name. Specified by the user through the
/// ```-o``` option.
///
/// * `mutex_map` - Mutex to permit exclusive access to the shared tuple containing the parsed packets,
/// the total number of sniffed packets and the number of filtered packets.
///
/// * `status_pair` - Shared variable to check the application current status.
pub fn sleep_and_write_report_loop(lowest_port: u16, highest_port: u16, interval: u64, min_packets: u128,
device_name: String, network_layer: String, transport_layer: String, app_layer: AppProtocol,
output_file: String,
mutex_map: Arc<Mutex<(HashMap<AddressPort,ReportInfo>, u128, u128, HashMap<AppProtocol, u128>)>>,
status_pair: Arc<(Mutex<Status>, Condvar)>) {
let mut times_report_updated: u128 = 0;
let mut last_report_updated_console: u128 = 0;
let cvar = &status_pair.1;
let first_timestamp = Local::now().format("%d/%m/%Y %H:%M:%S").to_string();
#[cfg(feature = "unknown_ports")]
let mut set_unknown = HashSet::new();
#[cfg(feature = "elapsed_time")]
let mut last_10_write_times = vec![];
#[cfg(feature = "draw_graph")]
let mut tot_packets_graph = vec![(0.0,0.0)];
#[cfg(feature = "draw_graph")]
let mut filtered_packets_graph = vec![(0.0,0.0)];
loop {
thread::sleep(Duration::from_secs(interval));
times_report_updated += 1;
let mut output = BufWriter::new(File::create(output_file.clone()).expect("Error creating output file\n\r"));
#[cfg(feature = "unknown_ports")]
let mut output2 = File::create("unknown_ports.txt").expect("Error creating output file\n\r");
let map_sniffed_filtered_app = mutex_map.lock().expect("Error acquiring mutex\n\r");
let tot_packets = map_sniffed_filtered_app.1;
let filtered_packets = map_sniffed_filtered_app.2;
#[cfg(feature = "draw_graph")]
{
tot_packets_graph.push((interval as f32 *times_report_updated as f32,tot_packets as f32));
filtered_packets_graph.push((interval as f32 *times_report_updated as f32,filtered_packets as f32));
}
#[cfg(feature = "elapsed_time")]
let start = Instant::now();
write_report_file_header(output.get_mut().try_clone().expect("Error cloning file handler\n\r"),
device_name.clone(), first_timestamp.clone(),
times_report_updated, lowest_port, highest_port, min_packets,
network_layer.clone(), transport_layer.clone(), app_layer.clone(),
map_sniffed_filtered_app.0.len(), tot_packets,
filtered_packets, map_sniffed_filtered_app.3.clone());
#[cfg(feature = "elapsed_time")]
let time_header = start.elapsed().as_millis();
let mut sorted_vec: Vec<(&AddressPort, &ReportInfo)> = map_sniffed_filtered_app.0.iter().collect();
sorted_vec.sort_by(|&(_, a), &(_, b)|
(b.received_packets + b.transmitted_packets).cmp(&(a.received_packets + a.transmitted_packets)));
#[cfg(feature = "elapsed_time")]
let time_header_sort = start.elapsed().as_millis();
for (key, val) in sorted_vec.iter() {
if val.transmitted_packets + val.received_packets >= min_packets {
write!(output, "{}\n{}\n\n", key, val).expect("Error writing output file\n\r");
#[cfg(feature = "unknown_ports")]
if val.app_protocols.len() == 0 && key.port < 49152{
set_unknown.insert(key.port);
}
}
}
#[cfg(feature = "unknown_ports")]
{
let mut sorted_set: Vec<&u16> = set_unknown.iter().collect();
sorted_set.sort();
write!(output2, "{:?}\n",sorted_set).unwrap();
}
drop(map_sniffed_filtered_app);
#[cfg(feature = "elapsed_time")]
{
let time_header_sort_print = start.elapsed().as_millis();
last_10_write_times.push(time_header_sort_print);
write!(output, "---------------------------------------------------------\n\n\
\t\tTimings (last report write):\n\
\t\t\tPrint header: {}ms\n\
\t\t\tSort map: {}ms\n\
\t\t\tPrint map: {}ms\n\
\t\t\tTot time mutex held: {}ms\n\n",
time_header, time_header_sort-time_header,
time_header_sort_print-time_header_sort,
time_header_sort_print).expect("Error writing output file\n\r");
if times_report_updated >= 10 {
write!(output, "\t\tTimings (average on the last 10 report writes):\n\
\t\t\tTot time mutex held: {}ms\n",
last_10_write_times.iter().sum::<u128>()/10).expect("Error writing output file\n\r");
last_10_write_times.remove(0);
}
}
output.flush().expect("Error writing output file\n\r");
//experimental: plot received packets in a line series chart
#[cfg(feature = "draw_graph")]
{
let width = 1120;
let height = 700;
let (top, right, bottom, left) = (90, 40, 50, 60);
let x = ScaleLinear::new()
.set_domain(vec![0_f32, interval as f32 * times_report_updated as f32])
.set_range(vec![0, width - left - right]);
let y = ScaleLinear::new()
.set_domain(vec![0_f32, 1.5*tot_packets as f32])
.set_range(vec![height - top - bottom, 0]);
let tot_packets_view = LineSeriesView::new()
.set_x_scale(&x)
.set_y_scale(&y)
.set_marker_type(MarkerType::Square)
.set_label_visibility(false)
.load_data(&tot_packets_graph).unwrap();
let filtered_packets_view = LineSeriesView::new()
.set_x_scale(&x)
.set_y_scale(&y)
.set_marker_type(MarkerType::Circle)
.set_label_visibility(false)
.load_data(&filtered_packets_graph).unwrap();
Chart::new()
.set_width(width)
.set_height(height)
.set_margins(top, right, bottom, left)
.add_title(String::from("Total number of sniffed packets"))
.add_view(&tot_packets_view)
.add_view(&filtered_packets_view)
.add_axis_bottom(&x)
.add_axis_left(&y)
.add_bottom_axis_label("Time (s)")
.save("sniffnet_graph.svg")
.unwrap();
// if tot_packets > filtered_packets {
// chart.add_view(&filtered_packets_view);
// }
}
let mut status = status_pair.0.lock().expect("Error acquiring mutex\n\r");
if *status == Status::Running {
if times_report_updated - last_report_updated_console != 1 {
println!("{}{}{}{}\r", "\tReport updated (".cyan().italic(),
times_report_updated.to_string().cyan().italic(), ")".cyan().italic(),
" - report has also been updated once during pause".cyan().italic());
}
else {
println!("{}{}{}\r", "\tReport updated (".cyan().italic(),
times_report_updated.to_string().cyan().italic(), ")".cyan().italic());
}
last_report_updated_console = times_report_updated;
}
status = cvar.wait_while(status, |s| *s == Status::Pause).expect("Error acquiring mutex\n\r");
if *status == Status::Stop {
println!("{}{}{}\r", "\tThe final report is available in the file '".cyan().italic(),
output_file.clone().cyan().bold(), "'\n\n\r".cyan().italic());
return;
}
}
}
/// Given the lowest and highest port numbers, the function generates the corresponding String
/// to be used in the output report file header.
///
/// # Arguments
///
/// * `lowest_port` - The lowest port number to be considered in the report. Specified by the user
/// through the ```-l``` option.
///
/// * `highest_port` - The highest port number to be considered in the report. Specified by the user
/// through the ```-h``` option.
fn get_ports_string(lowest_port: u16, highest_port: u16) -> String {
if lowest_port == highest_port {
format!("<><>\t\t\t[x] Considering only port number {}\n", lowest_port)
}
else if lowest_port != u16::MIN || highest_port != u16::MAX {
format!("<><>\t\t\t[x] Considering only port numbers from {} to {}\n", lowest_port, highest_port)
}
else {
format!("<><>\t\t\t[ ] Considering all port numbers (from {} to {})\n", lowest_port, highest_port)
}
}
/// Given the minimum packets number, the function generates the corresponding String
/// to be used in the output report file header.
///
/// # Arguments
///
/// * `min_packets` - Minimum number of packets for an address:port pair to be considered in the report.
/// Specified by the user through the ```-m``` option.
fn get_min_packets_string(min_packets: u128) -> String {
format!("<><>\t\tShowing only [address:port] pairs featured by more than {} packets\n", min_packets)
}
/// Given the network layer textual filter, the function generates the corresponding String
/// to be used in the output report file header.
///
/// # Arguments
///
/// * `network_layer` - A String representing the IP version to be filtered. Specified by the user through the
/// ```-n``` option.
fn get_network_layer_string (network_layer: String) -> String {
if network_layer.cmp(&"ipv4".to_string()) == Equal {
format!("<><>\t\t\t[x] Considering only IPv4 packets\n")
}
else if network_layer.cmp(&"ipv6".to_string()) == Equal {
format!("<><>\t\t\t[x] Considering only IPv6 packets\n")
}
else {
format!("<><>\t\t\t[ ] Considering both IPv4 and IPv6 packets\n")
}
}
/// Given the transport layer textual filter, the function generates the corresponding String
/// to be used in the output report file header.
///
/// # Arguments
///
/// * `transport_layer` - A String representing the transport protocol to be filtered. Specified by the user through the
/// ```-t``` option.
fn get_transport_layer_string(transport_layer: String) -> String {
if transport_layer.cmp(&"tcp".to_string()) == Equal {
format!("<><>\t\t\t[x] Considering only packets exchanged with TCP\n")
}
else if transport_layer.cmp(&"udp".to_string()) == Equal {
format!("<><>\t\t\t[x] Considering only packets exchanged with UDP\n")
}
else {
format!("<><>\t\t\t[ ] Considering packets exchanged both with TCP and/or UDP\n")
}
}
/// Given the application layer filter, the function generates the corresponding String
/// to be used in the output report file header.
///
/// # Arguments
///
/// * `app_layer` - A String representing the application layer protocol to be filtered. Specified by the user through the
/// ```--app``` option.
fn get_app_layer_string(app_layer: AppProtocol) -> String {
if app_layer.eq(&AppProtocol::Other) {
format!("<><>\t\t\t[ ] Considering all application layer protocols\n")
}
else {
format!("<><>\t\t\t[x] Considering only {:?} packets\n", app_layer)
}
}
/// Given the numbers of sniffed packets and filtered packets, the function generates the corresponding String
/// to be used in the output report file header.
///
/// # Arguments
///
/// * `sniffed` - Number of sniffed packets.
///
/// * `filtered` - Number of filtered packets
fn get_filtered_packets_string(sniffed: u128, filtered: u128) -> String {
if sniffed != 0 {
format!("<><>\t\t\tConsidered packets: {} ({:.1}%)\n",
filtered.separate_with_underscores(), 100.0*filtered as f32/sniffed as f32)
}
else {
format!("<><>\t\t\tConsidered packets: {}\n",
filtered.separate_with_underscores())
}
}
/// Given the map of app layer protocols with the relative sniffed packets count,
/// the function generates the corresponding String
/// to be used in the output report file header.
///
/// # Arguments
///
/// * `app_count` - Map of app layer protocols with the relative sniffed packets count
///
/// * `tot_packets` - Total number of sniffed packets
fn get_app_count_string(app_count: HashMap<AppProtocol, u128>, tot_packets: u128) -> String {
let mut ret_val = "".to_string();
let mut sorted_app_count: Vec<(&AppProtocol, &u128)> = app_count.iter().collect();
sorted_app_count.sort_by(|&(p1, a), &(p2, b)| {
if p1.eq(&AppProtocol::Other) {
Ordering::Greater
}
else if p2.eq(&AppProtocol::Other) {
Ordering::Less
}
else {
b.cmp(a)
}
});
//compute the length of the longest packet count string, used to align text
let mut longest_num;
longest_num = sorted_app_count.get(0).unwrap().1.separate_with_underscores().len();
match app_count.get(&AppProtocol::Other) {
None => {}
Some(x) => {
if x.separate_with_underscores().len() > longest_num {
longest_num = x.separate_with_underscores().len();
}
}
}
for entry in sorted_app_count {
let app_proto_string = format!("{:?}", entry.0);
let num_string = format!("{}", entry.1.separate_with_underscores());
let percentage_string =
if format!("{:.2}", 100.0*(*entry.1) as f32/tot_packets as f32).eq("0.00") {
"(<0.01%)".to_string()
}
else {
format!("({:.2}%)", 100.0*(*entry.1) as f32/tot_packets as f32)
};
//to align digits
let spaces_string_1 = " ".to_string()
.repeat(9+longest_num-num_string.len()-app_proto_string.len());
let spaces_string_2 = " ".to_string()
.repeat(11-percentage_string.len());
ret_val.push_str(&format!("<><>\t\t\t-{}:{}{}{}{}\n",
app_proto_string,
spaces_string_1,
num_string,
spaces_string_2,
percentage_string));
}
ret_val
}
/// Writes the output report file header, which contains useful info about the sniffing process.
///
/// # Arguments
///
/// * `output_file` - A String representing the output report file name. Specified by the user through the
/// ```-o``` option.
///
/// * `device_name` - A String representing the name of th network adapter to be sniffed. Specified by the user through the
/// ```-a``` option.
///
/// * `first_timestamp` - A not formatted String representing the initial timestamp of the sniffing process.
///
/// * `times_report_updated` - An integer representing the amount of times the report has been updated.
///
/// * `lowest_port` - The lowest port number to be considered in the report. Specified by the user
/// through the ```-l``` option.
///
/// * `highest_port` - The highest port number to be considered in the report. Specified by the user
/// through the ```-h``` option.
///
/// * `min_packets` - Minimum number of packets for an address:port pair to be considered in the report.
/// Specified by the user through the ```-m``` option.
///
/// * `network_layer` - A String representing the IP version to be filtered. Specified by the user through the
/// ```-n``` option.
///
/// * `transport_layer` - A String representing the transport protocol to be filtered. Specified by the user through the
/// ```-t``` option.
///
/// * `app_layer` - An AppProtocol representing the application protocol to be filtered. Specified by the user through the
/// ```--app``` option.
///
/// * `num_pairs` - Total numbers of address:port pairs considered in the report.
///
/// * `num_sniffed_packets` - Total numbers of sniffed packets.
/// # Examples
/// An example of output report file header generated by this function is reported below.
///
/// ```<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
/// <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
/// <><>
/// <><> Packets are sniffed from adapter 'en0'
/// <><>
/// <><> Report updates info
/// <><> Report start time: 15/08/2022 15:39:07
/// <><> Report last update: 15/08/2022 15:39:52
/// <><> Report update frequency: every 5 seconds
/// <><> Number of times report was updated: 9
/// <><>
/// <><> Filters
/// <><> Considering only address:port pairs featured by more than 500 packets
/// <><> Considering both IPv4 and IPv6 packets
/// <><> Considering only packets exchanged with TCP
/// <><> Considering only port number 443
/// <><>
/// <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
/// <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
/// ```
fn write_report_file_header(mut output: File, device_name: String, first_timestamp: String,
times_report_updated: u128, lowest_port: u16, highest_port: u16,
min_packets: u128, network_layer: String, transport_layer: String, app_layer: AppProtocol,
num_pairs: usize, num_sniffed_packets: u128, num_filtered_packets: u128,
app_count: HashMap<AppProtocol, u128>) {
let cornice_string = "<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>\n".to_string();
let adapter_string = format!("<><>\t\tPackets are sniffed from adapter '{}'\n", device_name);
let first_timestamp_string = format!("<><>\t\t\tReport start time: {}\n", first_timestamp);
let last_timestamp_string = format!("<><>\t\t\tReport last update: {}\n", Local::now().format("%d/%m/%Y %H:%M:%S").to_string());
let number_updates_string = format!("<><>\t\t\tNumber of times report was updated: {}\n", times_report_updated.separate_with_underscores());
let ports_string = get_ports_string(lowest_port,highest_port);
let network_layer_string = get_network_layer_string(network_layer);
let transport_layer_string = get_transport_layer_string(transport_layer);
let app_layer_string = get_app_layer_string(app_layer);
let filtered_packets_string = get_filtered_packets_string(num_sniffed_packets, num_filtered_packets);
write!(output, "{}", cornice_string).expect("Error writing output file\n");
write!(output, "{}", cornice_string).expect("Error writing output file\n");
write!(output, "<><>\n").expect("Error writing output file\n");
write!(output, "{}", adapter_string).expect("Error writing output file\n");
write!(output, "<><>\n").expect("Error writing output file\n");
write!(output, "<><>\t\tReport updates info\n").expect("Error writing output file\n");
write!(output, "{}", first_timestamp_string).expect("Error writing output file\n");
write!(output, "{}", last_timestamp_string).expect("Error writing output file\n");
write!(output, "{}", number_updates_string).expect("Error writing output file\n");
write!(output, "<><>\n").expect("Error writing output file\n");
write!(output, "<><>\t\tFilters\n").expect("Error writing output file\n");
write!(output, "{}", network_layer_string).expect("Error writing output file\n");
write!(output, "{}", transport_layer_string).expect("Error writing output file\n");
write!(output, "{}", ports_string).expect("Error writing output file\n");
write!(output, "{}", app_layer_string).expect("Error writing output file\n");
write!(output, "<><>\n").expect("Error writing output file\n");
write!(output, "<><>\t\tOverall statistics\n").expect("Error writing output file\n");
write!(output, "<><>\t\t\tConsidered [address:port] pairs: {}\n", num_pairs.separate_with_underscores()).expect("Error writing output file\n");
write!(output, "<><>\t\t\tTotal packets: {}\n", num_sniffed_packets.separate_with_underscores()).expect("Error writing output file\n");
write!(output, "{}", filtered_packets_string).expect("Error writing output file\n");
write!(output, "<><>\n").expect("Error writing output file\n");
if num_sniffed_packets > 0 {
let app_count_string = get_app_count_string(app_count, num_sniffed_packets);
write!(output, "<><>\t\tTotal packets divided by app layer protocol\n").expect("Error writing output file\n");
write!(output, "{}", app_count_string).expect("Error writing output file\n");
write!(output, "<><>\n").expect("Error writing output file\n");
}
if min_packets > 1 {
let min_packets_string = get_min_packets_string(min_packets);
write!(output, "{}", min_packets_string).expect("Error writing output file\n");
write!(output, "<><>\n").expect("Error writing output file\n");
}
write!(output,"{}", cornice_string).expect("Error writing output file\n");
write!(output,"{}\n\n\n", cornice_string).expect("Error writing output file\n");
}