From 53d165b2a033615e2cce7ab5cc42ff2bf85e50da Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Tue, 5 Nov 2019 20:33:12 +0100 Subject: feat(raw): machine friendly output --- Cargo.lock | 2 + Cargo.toml | 2 + src/display/components/table.rs | 50 +- src/display/mod.rs | 2 + src/display/raw_terminal_backend.rs | 52 + src/display/ui.rs | 58 +- src/display/ui_state.rs | 14 +- src/main.rs | 141 +- src/network/connection.rs | 23 + src/network/sniffer.rs | 4 +- src/network/utilization.rs | 4 +- src/os/linux.rs | 23 +- src/tests/cases/mod.rs | 2 + src/tests/cases/raw_mode.rs | 1115 +++++++++++++ .../cases/snapshots/raw_mode__basic_startup.snap | 5 + .../raw_mode__bi_directional_traffic.snap | 8 + ...__multiple_connections_from_remote_address.snap | 10 + ...kets_of_traffic_from_different_connections.snap | 11 + ..._packets_of_traffic_from_single_connection.snap | 8 + ...ltiple_processes_with_multiple_connections.snap | 17 + .../cases/snapshots/raw_mode__no_resolve_mode.snap | 17 + .../snapshots/raw_mode__one_packet_of_traffic.snap | 8 + ...ode__one_process_with_multiple_connections.snap | 11 + ..._sustained_traffic_from_multiple_processes.snap | 17 + ...fic_from_multiple_processes_bi_directional.snap | 17 + ...w_mode__sustained_traffic_from_one_process.snap | 11 + .../raw_mode__traffic_with_host_names.snap | 17 + src/tests/cases/snapshots/ui__basic_startup.snap | 55 + .../snapshots/ui__bi_directional_traffic-2.snap | 55 + .../snapshots/ui__bi_directional_traffic.snap | 55 + .../ui__layout_full_width_under_30_height-2.snap | 34 + .../ui__layout_full_width_under_30_height.snap | 34 + .../ui__layout_under_120_width_full_height-2.snap | 55 + .../ui__layout_under_120_width_full_height.snap | 55 + ...__layout_under_120_width_under_30_height-2.snap | 34 + ...ui__layout_under_120_width_under_30_height.snap | 34 + ...layout_under_120_width_under_full_height-2.snap | 55 + ...__layout_under_120_width_under_full_height.snap | 55 + .../ui__layout_under_150_width_full_height-2.snap | 55 + .../ui__layout_under_150_width_full_height.snap | 55 + ...__layout_under_150_width_under_30_height-2.snap | 34 + ...ui__layout_under_150_width_under_30_height.snap | 34 + ...multiple_connections_from_remote_address-2.snap | 55 + ...__multiple_connections_from_remote_address.snap | 55 + .../ui__multiple_connections_from_remote_ip-2.snap | 55 + .../ui__multiple_connections_from_remote_ip.snap | 55 + ...ts_of_traffic_from_different_connections-2.snap | 55 + ...kets_of_traffic_from_different_connections.snap | 55 + ...ackets_of_traffic_from_single_connection-2.snap | 55 + ..._packets_of_traffic_from_single_connection.snap | 55 + ...iple_processes_with_multiple_connections-2.snap | 55 + ...ltiple_processes_with_multiple_connections.snap | 55 + .../cases/snapshots/ui__no_resolve_mode-2.snap | 55 + src/tests/cases/snapshots/ui__no_resolve_mode.snap | 55 + .../snapshots/ui__one_packet_of_traffic-2.snap | 55 + .../cases/snapshots/ui__one_packet_of_traffic.snap | 55 + ...i__one_process_with_multiple_connections-2.snap | 55 + .../ui__one_process_with_multiple_connections.snap | 55 + ...ustained_traffic_from_multiple_processes-2.snap | 55 + ..._sustained_traffic_from_multiple_processes.snap | 55 + ...c_from_multiple_processes_bi_directional-2.snap | 55 + ...fic_from_multiple_processes_bi_directional.snap | 55 + .../ui__sustained_traffic_from_one_process-2.snap | 55 + .../ui__sustained_traffic_from_one_process.snap | 55 + .../snapshots/ui__traffic_with_host_names-2.snap | 55 + .../snapshots/ui__traffic_with_host_names.snap | 55 + .../snapshots/ui__traffic_with_winch_event-2.snap | 55 + .../snapshots/ui__traffic_with_winch_event-3.snap | 55 + .../snapshots/ui__traffic_with_winch_event.snap | 55 + src/tests/cases/ui.rs | 1682 ++++++++++++++++++++ src/tests/fakes/fake_input.rs | 2 +- src/tests/mod.rs | 1373 +--------------- src/tests/snapshots/tests__basic_startup.snap | 2 +- .../snapshots/tests__bi_directional_traffic.snap | 2 +- .../tests__layout_under_150_width_full_height.snap | 2 +- ...multiple_connections_from_remote_address-2.snap | 55 + ...__multiple_connections_from_remote_address.snap | 55 + ...kets_of_traffic_from_different_connections.snap | 2 +- ..._packets_of_traffic_from_single_connection.snap | 2 +- ...ltiple_processes_with_multiple_connections.snap | 2 +- src/tests/snapshots/tests__no_resolve_mode-2.snap | 55 + src/tests/snapshots/tests__no_resolve_mode.snap | 55 + .../snapshots/tests__one_packet_of_traffic.snap | 2 +- ...sts__one_process_with_multiple_connections.snap | 2 +- .../snapshots/tests__traffic_with_winch_event.snap | 2 +- 85 files changed, 5638 insertions(+), 1490 deletions(-) create mode 100644 src/display/raw_terminal_backend.rs create mode 100644 src/tests/cases/mod.rs create mode 100644 src/tests/cases/raw_mode.rs create mode 100644 src/tests/cases/snapshots/raw_mode__basic_startup.snap create mode 100644 src/tests/cases/snapshots/raw_mode__bi_directional_traffic.snap create mode 100644 src/tests/cases/snapshots/raw_mode__multiple_connections_from_remote_address.snap create mode 100644 src/tests/cases/snapshots/raw_mode__multiple_packets_of_traffic_from_different_connections.snap create mode 100644 src/tests/cases/snapshots/raw_mode__multiple_packets_of_traffic_from_single_connection.snap create mode 100644 src/tests/cases/snapshots/raw_mode__multiple_processes_with_multiple_connections.snap create mode 100644 src/tests/cases/snapshots/raw_mode__no_resolve_mode.snap create mode 100644 src/tests/cases/snapshots/raw_mode__one_packet_of_traffic.snap create mode 100644 src/tests/cases/snapshots/raw_mode__one_process_with_multiple_connections.snap create mode 100644 src/tests/cases/snapshots/raw_mode__sustained_traffic_from_multiple_processes.snap create mode 100644 src/tests/cases/snapshots/raw_mode__sustained_traffic_from_multiple_processes_bi_directional.snap create mode 100644 src/tests/cases/snapshots/raw_mode__sustained_traffic_from_one_process.snap create mode 100644 src/tests/cases/snapshots/raw_mode__traffic_with_host_names.snap create mode 100644 src/tests/cases/snapshots/ui__basic_startup.snap create mode 100644 src/tests/cases/snapshots/ui__bi_directional_traffic-2.snap create mode 100644 src/tests/cases/snapshots/ui__bi_directional_traffic.snap create mode 100644 src/tests/cases/snapshots/ui__layout_full_width_under_30_height-2.snap create mode 100644 src/tests/cases/snapshots/ui__layout_full_width_under_30_height.snap create mode 100644 src/tests/cases/snapshots/ui__layout_under_120_width_full_height-2.snap create mode 100644 src/tests/cases/snapshots/ui__layout_under_120_width_full_height.snap create mode 100644 src/tests/cases/snapshots/ui__layout_under_120_width_under_30_height-2.snap create mode 100644 src/tests/cases/snapshots/ui__layout_under_120_width_under_30_height.snap create mode 100644 src/tests/cases/snapshots/ui__layout_under_120_width_under_full_height-2.snap create mode 100644 src/tests/cases/snapshots/ui__layout_under_120_width_under_full_height.snap create mode 100644 src/tests/cases/snapshots/ui__layout_under_150_width_full_height-2.snap create mode 100644 src/tests/cases/snapshots/ui__layout_under_150_width_full_height.snap create mode 100644 src/tests/cases/snapshots/ui__layout_under_150_width_under_30_height-2.snap create mode 100644 src/tests/cases/snapshots/ui__layout_under_150_width_under_30_height.snap create mode 100644 src/tests/cases/snapshots/ui__multiple_connections_from_remote_address-2.snap create mode 100644 src/tests/cases/snapshots/ui__multiple_connections_from_remote_address.snap create mode 100644 src/tests/cases/snapshots/ui__multiple_connections_from_remote_ip-2.snap create mode 100644 src/tests/cases/snapshots/ui__multiple_connections_from_remote_ip.snap create mode 100644 src/tests/cases/snapshots/ui__multiple_packets_of_traffic_from_different_connections-2.snap create mode 100644 src/tests/cases/snapshots/ui__multiple_packets_of_traffic_from_different_connections.snap create mode 100644 src/tests/cases/snapshots/ui__multiple_packets_of_traffic_from_single_connection-2.snap create mode 100644 src/tests/cases/snapshots/ui__multiple_packets_of_traffic_from_single_connection.snap create mode 100644 src/tests/cases/snapshots/ui__multiple_processes_with_multiple_connections-2.snap create mode 100644 src/tests/cases/snapshots/ui__multiple_processes_with_multiple_connections.snap create mode 100644 src/tests/cases/snapshots/ui__no_resolve_mode-2.snap create mode 100644 src/tests/cases/snapshots/ui__no_resolve_mode.snap create mode 100644 src/tests/cases/snapshots/ui__one_packet_of_traffic-2.snap create mode 100644 src/tests/cases/snapshots/ui__one_packet_of_traffic.snap create mode 100644 src/tests/cases/snapshots/ui__one_process_with_multiple_connections-2.snap create mode 100644 src/tests/cases/snapshots/ui__one_process_with_multiple_connections.snap create mode 100644 src/tests/cases/snapshots/ui__sustained_traffic_from_multiple_processes-2.snap create mode 100644 src/tests/cases/snapshots/ui__sustained_traffic_from_multiple_processes.snap create mode 100644 src/tests/cases/snapshots/ui__sustained_traffic_from_multiple_processes_bi_directional-2.snap create mode 100644 src/tests/cases/snapshots/ui__sustained_traffic_from_multiple_processes_bi_directional.snap create mode 100644 src/tests/cases/snapshots/ui__sustained_traffic_from_one_process-2.snap create mode 100644 src/tests/cases/snapshots/ui__sustained_traffic_from_one_process.snap create mode 100644 src/tests/cases/snapshots/ui__traffic_with_host_names-2.snap create mode 100644 src/tests/cases/snapshots/ui__traffic_with_host_names.snap create mode 100644 src/tests/cases/snapshots/ui__traffic_with_winch_event-2.snap create mode 100644 src/tests/cases/snapshots/ui__traffic_with_winch_event-3.snap create mode 100644 src/tests/cases/snapshots/ui__traffic_with_winch_event.snap create mode 100644 src/tests/cases/ui.rs create mode 100644 src/tests/snapshots/tests__multiple_connections_from_remote_address-2.snap create mode 100644 src/tests/snapshots/tests__multiple_connections_from_remote_address.snap create mode 100644 src/tests/snapshots/tests__no_resolve_mode-2.snap create mode 100644 src/tests/snapshots/tests__no_resolve_mode.snap diff --git a/Cargo.lock b/Cargo.lock index 7f4168c..09fb45a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1217,6 +1217,7 @@ name = "what" version = "0.1.0" dependencies = [ "cargo-insta 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", "dns-lookup 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "insta 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1225,6 +1226,7 @@ dependencies = [ "pnet 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", "pnet_base 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", "procfs 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "signal-hook 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "structopt 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "termion 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index c1f04e2..66facc2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,9 +22,11 @@ structopt = "0.3" dns-lookup = "1.0.1" signal-hook = "0.1.10" failure = "0.1.6" +chrono = "0.4" [dev-dependencies] insta = "0.11.0" cargo-insta = "0.11.0" packet-builder = "0.3.0" pnet_base = "0.22.0" +regex = "1" diff --git a/src/display/components/table.rs b/src/display/components/table.rs index 6f22ea5..f59536e 100644 --- a/src/display/components/table.rs +++ b/src/display/components/table.rs @@ -7,7 +7,7 @@ use ::tui::terminal::Frame; use ::tui::widgets::{Block, Borders, Row, Widget}; use crate::display::{Bandwidth, DisplayBandwidth, UIState}; -use crate::network::Connection; +use crate::network::{display_connection_string, display_ip_or_host}; use ::std::net::Ipv4Addr; use std::iter::FromIterator; @@ -28,13 +28,6 @@ fn display_upload_and_download(bandwidth: &impl Bandwidth) -> String { ) } -fn display_ip_or_host(ip: Ipv4Addr, ip_to_host: &HashMap) -> String { - match ip_to_host.get(&ip) { - Some(host) => host.clone(), - None => ip.to_string(), - } -} - fn sort_by_bandwidth<'a, T>( list: &'a mut Vec<(T, &impl Bandwidth)>, ) -> &'a Vec<(T, &'a impl Bandwidth)> { @@ -54,19 +47,6 @@ fn sort_by_bandwidth<'a, T>( list } -fn display_connection_string( - connection: &Connection, - ip_to_host: &HashMap, -) -> String { - format!( - ":{} => {}:{} ({})", - connection.local_port, - display_ip_or_host(connection.remote_socket.ip, ip_to_host), - connection.remote_socket.port, - connection.protocol, - ) -} - pub struct Table<'a> { title: &'a str, column_names: &'a [&'a str], @@ -119,29 +99,29 @@ impl<'a> Table<'a> { rows: processes_rows, } } - pub fn create_remote_ips_table( + pub fn create_remote_addresses_table( state: &UIState, ip_to_host: &HashMap, ) -> Self { - let mut remote_ips_list = Vec::from_iter(&state.remote_ips); - sort_by_bandwidth(&mut remote_ips_list); - let remote_ips_rows = remote_ips_list + let mut remote_addresses_list = Vec::from_iter(&state.remote_addresses); + sort_by_bandwidth(&mut remote_addresses_list); + let remote_addresses_rows = remote_addresses_list .iter() - .map(|(remote_ip, data_for_remote_ip)| { - let remote_ip = display_ip_or_host(**remote_ip, &ip_to_host); + .map(|(remote_address, data_for_remote_address)| { + let remote_address = display_ip_or_host(**remote_address, &ip_to_host); vec![ - remote_ip, - data_for_remote_ip.connection_count.to_string(), - display_upload_and_download(*data_for_remote_ip), + remote_address, + data_for_remote_address.connection_count.to_string(), + display_upload_and_download(*data_for_remote_address), ] }) .collect(); - let remote_ips_title = "Utilization by remote ip"; - let remote_ips_column_names = &["Remote Address", "Connection Count", "Rate Up/Down"]; + let remote_addresses_title = "Utilization by remote address"; + let remote_addresses_column_names = &["Remote Address", "Connection Count", "Rate Up/Down"]; Table { - title: remote_ips_title, - column_names: remote_ips_column_names, - rows: remote_ips_rows, + title: remote_addresses_title, + column_names: remote_addresses_column_names, + rows: remote_addresses_rows, } } pub fn render(&self, frame: &mut Frame, rect: Rect) { diff --git a/src/display/mod.rs b/src/display/mod.rs index 8d24159..396b5d9 100644 --- a/src/display/mod.rs +++ b/src/display/mod.rs @@ -1,7 +1,9 @@ mod components; mod ui; mod ui_state; +mod raw_terminal_backend; pub use components::*; pub use ui::*; pub use ui_state::*; +pub use raw_terminal_backend::*; diff --git a/src/display/raw_terminal_backend.rs b/src/display/raw_terminal_backend.rs new file mode 100644 index 0000000..3ae299a --- /dev/null +++ b/src/display/raw_terminal_backend.rs @@ -0,0 +1,52 @@ +// this is a bit of a hack: +// the TUI backend used by this app changes stdout to raw byte mode. +// this is not desired when we do not use it (in our --raw mode), +// since it makes writing to stdout overly complex +// +// so what we do here is provide a fake backend (RawTerminalBackend) +// that implements the Backend TUI trait, but does nothing +// this way, we don't need to create the TermionBackend +// and thus skew our stdout when we don't need it +use ::std::io; +use ::tui::backend::Backend; +use ::tui::buffer::Cell; +use ::tui::layout::Rect; + +pub struct RawTerminalBackend {} + +impl Backend for RawTerminalBackend { + fn clear(&mut self) -> io::Result<()> { + Ok(()) + } + + fn hide_cursor(&mut self) -> io::Result<()> { + Ok(()) + } + + fn show_cursor(&mut self) -> io::Result<()> { + Ok(()) + } + + fn get_cursor(&mut self) -> io::Result<(u16, u16)> { + Ok((0, 0)) + } + + fn set_cursor(&mut self, _x: u16, _y: u16) -> io::Result<()> { + Ok(()) + } + + fn draw<'a, I>(&mut self, _content: I) -> io::Result<()> + where + I: Iterator, + { + Ok(()) + } + + fn size(&self) -> io::Result { + Ok(Rect::new(0, 0, 0, 0)) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} diff --git a/src/display/ui.rs b/src/display/ui.rs index feacc5e..e23843e 100644 --- a/src/display/ui.rs +++ b/src/display/ui.rs @@ -5,10 +5,12 @@ use ::tui::Terminal; use crate::display::components::{Layout, Table, TotalBandwidth}; use crate::display::UIState; -use crate::network::{Connection, Utilization}; +use crate::network::{Connection, Utilization, display_connection_string, display_ip_or_host}; use ::std::net::Ipv4Addr; +use chrono::prelude::*; + pub struct Ui where B: Backend, @@ -26,10 +28,52 @@ where let mut terminal = Terminal::new(terminal_backend).unwrap(); terminal.clear().unwrap(); terminal.hide_cursor().unwrap(); - Ui { - terminal, - state: Default::default(), - ip_to_host: Default::default(), + Ui { + terminal: terminal, + state: Default::default(), + ip_to_host: Default::default(), + } + } + pub fn output_text(&mut self, write_to_stdout: &mut Box) { + let state = &self.state; + let ip_to_host = &self.ip_to_host; + let local_time: DateTime = Local::now(); + let timestamp = local_time.timestamp(); + for (process, process_network_data) in &state.processes { + write_to_stdout( + format!( + "process: <{}> \"{}\" up/down Bps: {}/{} connections: {}", + timestamp, + process, + process_network_data.total_bytes_uploaded, + process_network_data.total_bytes_downloaded, + process_network_data.connection_count + ) + ); + } + for (connection, connection_network_data) in &state.connections { + write_to_stdout( + format!( + "connection: <{}> {} up/down Bps: {}/{} process: \"{}\"", + timestamp, + display_connection_string(connection, ip_to_host), + connection_network_data.total_bytes_uploaded, + connection_network_data.total_bytes_downloaded, + connection_network_data.process_name + ) + ); + } + for (remote_address, remote_address_network_data) in &state.remote_addresses { + write_to_stdout( + format!( + "remote_address: <{}> {} up/down Bps: {}/{} connections: {}", + timestamp, + display_ip_or_host(*remote_address, ip_to_host), + remote_address_network_data.total_bytes_uploaded, + remote_address_network_data.total_bytes_downloaded, + remote_address_network_data.connection_count + ) + ); } } pub fn draw(&mut self) { @@ -40,11 +84,11 @@ where let size = frame.size(); let connections = Table::create_connections_table(&state, &ip_to_host); let processes = Table::create_processes_table(&state); - let remote_ips = Table::create_remote_ips_table(&state, &ip_to_host); + let remote_addresses = Table::create_remote_addresses_table(&state, &ip_to_host); let total_bandwidth = TotalBandwidth { state: &state }; let layout = Layout { header: total_bandwidth, - children: vec![processes, connections, remote_ips], + children: vec![processes, connections, remote_addresses], }; layout.render(&mut frame, size); }) diff --git a/src/display/ui_state.rs b/src/display/ui_state.rs index ac60933..1bf235a 100644 --- a/src/display/ui_state.rs +++ b/src/display/ui_state.rs @@ -43,7 +43,7 @@ impl Bandwidth for NetworkData { #[derive(Default)] pub struct UIState { pub processes: BTreeMap, - pub remote_ips: BTreeMap, + pub remote_addresses: BTreeMap, pub connections: BTreeMap, pub total_bytes_downloaded: u128, pub total_bytes_uploaded: u128, @@ -55,7 +55,7 @@ impl UIState { network_utilization: Utilization, ) -> Self { let mut processes: BTreeMap = BTreeMap::new(); - let mut remote_ips: BTreeMap = BTreeMap::new(); + let mut remote_addresses: BTreeMap = BTreeMap::new(); let mut connections: BTreeMap = BTreeMap::new(); let mut total_bytes_downloaded: u128 = 0; let mut total_bytes_uploaded: u128 = 0; @@ -63,7 +63,7 @@ impl UIState { if let Some(connection_bandwidth_utilization) = network_utilization.connections.get(&connection) { - let data_for_remote_ip = remote_ips.entry(connection.remote_socket.ip).or_default(); + let data_for_remote_address = remote_addresses.entry(connection.remote_socket.ip).or_default(); let connection_data = connections.entry(connection).or_default(); let data_for_process = processes.entry(process_name.clone()).or_default(); @@ -77,18 +77,18 @@ impl UIState { connection_data.total_bytes_uploaded += &connection_bandwidth_utilization.total_bytes_uploaded; connection_data.process_name = process_name; - data_for_remote_ip.total_bytes_downloaded += + data_for_remote_address.total_bytes_downloaded += connection_bandwidth_utilization.total_bytes_downloaded; - data_for_remote_ip.total_bytes_uploaded += + data_for_remote_address.total_bytes_uploaded += connection_bandwidth_utilization.total_bytes_uploaded; - data_for_remote_ip.connection_count += 1; + data_for_remote_address.connection_count += 1; total_bytes_downloaded += connection_bandwidth_utilization.total_bytes_downloaded; total_bytes_uploaded += connection_bandwidth_utilization.total_bytes_uploaded; } } UIState { processes, - remote_ips, + remote_addresses, connections, total_bytes_downloaded, total_bytes_uploaded, diff --git a/src/main.rs b/src/main.rs index 86abf5d..a28b2a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ mod os; #[cfg(test)] mod tests; -use display::Ui; +use display::{Ui, RawTerminalBackend}; use network::{Connection, DnsQueue, Sniffer, Utilization}; use ::std::net::IpAddr; @@ -30,7 +30,14 @@ use structopt::StructOpt; #[structopt(name = "what")] pub struct Opt { #[structopt(short, long)] + /// The network interface to listen on, eg. eth0 interface: String, + #[structopt(short, long)] + /// Machine friendlier output + raw: bool, + #[structopt(short, long)] + /// Do not attempt to resolve IPs to their hostnames + no_resolve: bool, } fn main() { @@ -47,20 +54,27 @@ fn try_main() -> Result<(), failure::Error> { ); use os::get_input; - let opt = Opt::from_args(); - let os_input = get_input(opt)?; - let stdout = match io::stdout().into_raw_mode() { - Ok(stdout) => stdout, - Err(_) => failure::bail!( - "Failed to get stdout: 'what' does not (yet) support piping, is it being piped?" - ), + let opts = Opt::from_args(); + let os_input = get_input(&opts.interface)?; + let raw_mode = opts.raw; + if raw_mode { + let terminal_backend = RawTerminalBackend {}; + start(terminal_backend, os_input, opts); + } else { + match io::stdout().into_raw_mode() { + Ok(stdout) => { + let terminal_backend = TermionBackend::new(stdout); + start(terminal_backend, os_input, opts); + }, + Err(_) => failure::bail!( + "Failed to get stdout: 'what' does not (yet) support piping, is it being piped?" + ), + } }; - let terminal_backend = TermionBackend::new(stdout); - start(terminal_backend, os_input); Ok(()) } -pub struct OsInput { +pub struct OsInputOutput { pub network_interface: NetworkInterface, pub network_frames: Box, pub get_open_sockets: fn() -> HashMap, @@ -68,51 +82,69 @@ pub struct OsInput { pub lookup_addr: Box Option + Send>, pub on_winch: Box) + Send>, pub cleanup: Box, + pub write_to_stdout: Box, } -pub fn start(terminal_backend: B, os_input: OsInput) +pub fn start<'a, B>(terminal_backend: B, os_input: OsInputOutput, opts: Opt) where B: Backend + Send + 'static, { let running = Arc::new(AtomicBool::new(true)); + let mut active_threads = vec![]; + let keyboard_events = os_input.keyboard_events; let get_open_sockets = os_input.get_open_sockets; let lookup_addr = os_input.lookup_addr; + let mut write_to_stdout = os_input.write_to_stdout; let on_winch = os_input.on_winch; let cleanup = os_input.cleanup; + let raw_mode = opts.raw; + let no_resolve = opts.no_resolve; + let mut sniffer = Sniffer::new(os_input.network_interface, os_input.network_frames); let network_utilization = Arc::new(Mutex::new(Utilization::new())); let ui = Arc::new(Mutex::new(Ui::new(terminal_backend))); - let dns_queue = Arc::new(DnsQueue::new()); + + let dns_queue = if no_resolve { + Arc::new(None) + } else { + Arc::new(Some(DnsQueue::new())) + }; let ip_to_host = Arc::new(Mutex::new(HashMap::new())); - let dns_handler = thread::spawn({ - let dns_queue = dns_queue.clone(); - let ip_to_host = ip_to_host.clone(); - move || { - while let Some(ip) = dns_queue.wait_for_job() { - if let Some(addr) = lookup_addr(&IpAddr::V4(ip)) { - ip_to_host.lock().unwrap().insert(ip, addr); + if !no_resolve { + active_threads.push(thread::Builder::new().name("dns_resolver".to_string()).spawn({ + let dns_queue = dns_queue.clone(); + let ip_to_host = ip_to_host.clone(); + move || { + if let Some(dns_queue) = Option::as_ref(&dns_queue) { + while let Some(ip) = dns_queue.wait_for_job() { + if let Some(addr) = lookup_addr(&IpAddr::V4(ip)) { + ip_to_host.lock().unwrap().insert(ip, addr); + } + } } } - } - }); + }).unwrap()); + } - let resize_handler = thread::spawn({ - let ui = ui.clone(); - move || { - on_winch({ - Box::new(move || { - let mut ui = ui.lock().unwrap(); - ui.draw(); - }) - }); - } - }); + if !raw_mode { + active_threads.push(thread::Builder::new().name("resize_handler".to_string()).spawn({ + let ui = ui.clone(); + move || { + on_winch({ + Box::new(move || { + let mut ui = ui.lock().unwrap(); + ui.draw(); + }) + }); + } + }).unwrap()); + } - let display_handler = thread::spawn({ + let display_handler = thread::Builder::new().name("display_handler".to_string()).spawn({ let running = running.clone(); let network_utilization = network_utilization.clone(); let ip_to_host = ip_to_host.clone(); @@ -132,23 +164,33 @@ where unresolved_ips.push(connection.remote_socket.ip); } } - if !unresolved_ips.is_empty() { - dns_queue.resolve_ips(unresolved_ips); + if let Some(dns_queue) = Option::as_ref(&dns_queue) { + if !unresolved_ips.is_empty() { + dns_queue.resolve_ips(unresolved_ips); + } } { let mut ui = ui.lock().unwrap(); ui.update_state(connections_to_procs, utilization, ip_to_host); - ui.draw(); + if raw_mode { + ui.output_text(&mut write_to_stdout); + } else { + ui.draw(); + } } park_timeout(time::Duration::from_secs(1)); } - let mut ui = ui.lock().unwrap(); - ui.end(); - dns_queue.end(); + if !raw_mode { + let mut ui = ui.lock().unwrap(); + ui.end(); + } + if let Some(dns_queue) = Option::as_ref(&dns_queue) { + dns_queue.end(); + } } - }); + }).unwrap(); - let stdin_handler = thread::spawn({ + active_threads.push(thread::Builder::new().name("stdin_handler".to_string()).spawn({ let running = running.clone(); let display_handler = display_handler.thread().clone(); move || { @@ -164,18 +206,17 @@ where }; } } - }); + }).unwrap()); + active_threads.push(display_handler); - let sniffing_handler = thread::spawn(move || { + active_threads.push(thread::Builder::new().name("sniffing_handler".to_string()).spawn(move || { while running.load(Ordering::Acquire) { if let Some(segment) = sniffer.next() { network_utilization.lock().unwrap().update(&segment) } } - }); - display_handler.join().unwrap(); - sniffing_handler.join().unwrap(); - stdin_handler.join().unwrap(); - dns_handler.join().unwrap(); - resize_handler.join().unwrap(); + }).unwrap()); + for thread_handler in active_threads { + thread_handler.join().unwrap() + }; } diff --git a/src/network/connection.rs b/src/network/connection.rs index 9a47fb0..e22e4eb 100644 --- a/src/network/connection.rs +++ b/src/network/connection.rs @@ -1,3 +1,4 @@ +use ::std::collections::HashMap; use ::std::fmt; use ::std::net::Ipv4Addr; @@ -31,6 +32,28 @@ pub struct Connection { pub local_port: u16, } +pub fn display_ip_or_host(ip: Ipv4Addr, ip_to_host: &HashMap) -> String { + match ip_to_host.get(&ip) { + Some(host) => host.clone(), + None => ip.to_string(), + } +} + + +pub fn display_connection_string( + connection: &Connection, + ip_to_host: &HashMap, +) -> String { + format!( + ":{} => {}:{} ({})", + connection.local_port, + display_ip_or_host(connection.remote_socket.ip, ip_to_host), + connection.remote_socket.port, + connection.protocol, + ) +} + + impl Connection { pub fn new(remote_socket: SocketAddr, local_port: u16, protocol: Protocol) -> Option { match remote_socket { diff --git a/src/network/sniffer.rs b/src/network/sniffer.rs index e527df3..51bcef2 100644 --- a/src/network/sniffer.rs +++ b/src/network/sniffer.rs @@ -64,7 +64,7 @@ impl Sniffer { Protocol::Tcp, message.get_source(), message.get_destination(), - message.payload().len() as u128, + ip_packet.payload().len() as u128, ) } IpNextHeaderProtocol(17) => { @@ -73,7 +73,7 @@ impl Sniffer { Protocol::Udp, datagram.get_source(), datagram.get_destination(), - datagram.payload().len() as u128, + ip_packet.payload().len() as u128, ) } _ => return None, diff --git a/src/network/utilization.rs b/src/network/utilization.rs index 54537a1..bb3f581 100644 --- a/src/network/utilization.rs +++ b/src/network/utilization.rs @@ -56,10 +56,10 @@ impl Utilization { }); match seg.direction { Direction::Download => { - total_bandwidth.increment_bytes_downloaded(seg.data_length, &self.reset_time) + total_bandwidth.increment_bytes_downloaded(seg.data_length, &self.reset_time); } Direction::Upload => { - total_bandwidth.increment_bytes_uploaded(seg.data_length, &self.reset_time) + total_bandwidth.increment_bytes_uploaded(seg.data_length, &self.reset_time); } } } diff --git a/src/os/linux.rs b/src/os/linux.rs index a7cbe56..f7db89b 100644 --- a/src/os/linux.rs +++ b/src/os/linux.rs @@ -1,7 +1,7 @@ use ::pnet::datalink::Channel::Ethernet; use ::pnet::datalink::DataLinkReceiver; use ::pnet::datalink::{self, Config, NetworkInterface}; -use ::std::io::stdin; +use ::std::io::{self, stdin, Write}; use ::termion::event::Event; use ::termion::input::TermRead; @@ -13,7 +13,7 @@ use ::procfs::FDTarget; use signal_hook::iterator::Signals; use crate::network::{Connection, Protocol}; -use crate::{Opt, OsInput}; +use crate::OsInputOutput; struct KeyboardEvents; @@ -108,19 +108,29 @@ fn sigwinch() -> (Box) + Send>, Box) { (Box::new(on_winch), Box::new(cleanup)) } -pub fn get_input(opt: Opt) -> Result { +pub fn create_write_to_stdout () -> Box { + Box::new({ + let mut stdout = io::stdout(); + move |output: String| { + writeln!(stdout, "{}", output).unwrap(); + } + }) +} + +pub fn get_input(interface_name: &str) -> Result { let keyboard_events = Box::new(KeyboardEvents); - let network_interface = match get_interface(&opt.interface) { + let network_interface = match get_interface(interface_name) { Some(interface) => interface, None => { - failure::bail!("Cannot find interface {}", opt.interface); + failure::bail!("Cannot find interface {}", interface_name); } }; let network_frames = get_datalink_channel(&network_interface)?; let lookup_addr = Box::new(lookup_addr); + let write_to_stdout = create_write_to_stdout(); let (on_winch, cleanup) = sigwinch(); - Ok(OsInput { + Ok(OsInputOutput { network_interface, network_frames, get_open_sockets, @@ -128,5 +138,6 @@ pub fn get_input(opt: Opt) -> Result { lookup_addr, on_winch, cleanup, + write_to_stdout, }) } diff --git a/src/tests/cases/mod.rs b/src/tests/cases/mod.rs new file mode 100644 index 0000000..c917d49 --- /dev/null +++ b/src/tests/cases/mod.rs @@ -0,0 +1,2 @@ +pub mod ui; +pub mod raw_mode; diff --git a/src/tests/cases/raw_mode.rs b/src/tests/cases/raw_mode.rs new file mode 100644 index 0000000..4e05c3f --- /dev/null +++ b/src/tests/cases/raw_mode.rs @@ -0,0 +1,1115 @@ +use crate::tests::fakes::{ + create_fake_lookup_addr, create_fake_on_winch, get_interface, get_open_sockets, KeyboardEvents, + NetworkFrames, TestBackend, +}; + +use ::insta::assert_snapshot; +use ::std::sync::{Arc, Mutex}; +use ::termion::event::{Event, Key}; + +use ::std::collections::HashMap; +use ::std::net::IpAddr; + +use packet_builder::payload::PayloadData; +use packet_builder::*; +use pnet::packet::Packet; +use pnet_base::MacAddr; + +use ::std::io::Write; + +use crate::{start, Opt, OsInputOutput}; + +fn build_tcp_packet( + source_ip: &str, + destination_ip: &str, + source_port: u16, + destination_port: u16, + payload: &'static [u8], +) -> Vec { + let mut pkt_buf = [0u8; 1500]; + let pkt = packet_builder!( + pkt_buf, + ether({set_destination => MacAddr(0,0,0,0,0,0), set_source => MacAddr(0,0,0,0,0,0)}) / + ipv4({set_source => ipv4addr!(source_ip), set_destination => ipv4addr!(destination_ip) }) / + tcp({set_source => source_port, set_destination => destination_port }) / + payload(payload) + ); + pkt.packet().to_vec() +} + +fn format_raw_output<'t> (output: Vec) -> String { + let stdout_utf8 = String::from_utf8(output).unwrap(); + use regex::Regex; + let timestamp = Regex::new(r"<\d+>").unwrap(); + let replaced = timestamp.replace_all(&stdout_utf8, ""); + format!("{}", replaced) +} + +struct LogWithMirror { + pub write: Arc>, + pub mirror: Arc>, +} + +impl LogWithMirror { + pub fn new(log: T) -> Self { + let write = Arc::new(Mutex::new(log)); + let mirror = write.clone(); + LogWithMirror { write, mirror } + } +} + +// #[test] +// fn basic_startup() { +// let keyboard_events = Box::new(KeyboardEvents::new(vec![ +// None, // sleep +// Some(Event::Key(Key::Ctrl('c'))), +// ])); +// let network_frames = NetworkFrames::new(vec![ +// None, // sleep +// ]); +// +// let terminal_width = Arc::new(Mutex::new(190)); +// let terminal_height = Arc::new(Mutex::new(50)); +// let terminal_events = LogWithMirror::new(Vec::new()); +// let terminal_draw_events = LogWithMirror::new(Vec::new()); +// +// let backend = TestBackend::new( +// terminal_events.write, +// terminal_draw_events.write, +// terminal_width, +// terminal_height, +// ); +// let network_interface = get_interface(); +// let lookup_addr = create_fake_lookup_addr(HashMap::new()); +// let on_winch = create_fake_on_winch(false); +// let cleanup = Box::new(|| {}); +// +// let os_input = OsInputOutput { +// network_interface, +// network_frames, +// get_open_sockets, +// keyboard_events, +// lookup_addr, +// on_winch, +// cleanup, +// }; +// let opts = Opt { +// interface: String::from("interface_name"), +// raw: true, +// no_resolve: false, +// }; +// let fake_stdout = Box::new(Vec::new()); +// let clone_stdout = fake_stdout.clone(); +// start(backend, os_input, Box::new(fake_stdout), opts); +// +// assert_snapshot!(String::from_utf8(*clone_stdout).unwrap()); +// } + +#[test] +fn one_packet_of_traffic() { + let keyboard_events = Box::new(KeyboardEvents::new(vec![ + None, // sleep + None, // sleep + Some(Event::Key(Key::Ctrl('c'))), + ])); + let network_frames = NetworkFrames::new(vec![Some(build_tcp_packet( + "10.0.0.2", + "1.1.1.1", + 443, + 12345, + b"I am a fake tcp packet", + ))]); + + let terminal_width = Arc::new(Mutex::new(190)); + let terminal_height = Arc::new(Mutex::new(50)); + let terminal_events = LogWithMirror::new(Vec::new()); + let terminal_draw_events = LogWithMirror::new(Vec::new()); + + let backend = TestBackend::new( + terminal_events.write, + terminal_draw_events.write, + terminal_width, + terminal_height, + ); + let network_interface = get_interface(); + let lookup_addr = create_fake_lookup_addr(HashMap::new()); + let on_winch = create_fake_on_winch(false); + let cleanup = Box::new(|| {}); + let stdout = Arc::new(Mutex::new(Vec::new())); + let write_to_stdout = Box::new({ + let stdout = stdout.clone(); + move |output: String| { + let mut stdout = stdout.lock().unwrap(); + writeln!(&mut stdout, "{}", output).unwrap(); + } + }); + + let os_input = OsInputOutput { + network_interface, + network_frames, + get_open_sockets, + keyboard_events, + lookup_addr, + on_winch, + cleanup, + write_to_stdout, + }; + let opts = Opt { + interface: String::from("interface_name"), + raw: true, + no_resolve: false, + }; + start(backend, os_input, opts); + let stdout = Arc::try_unwrap(stdout).unwrap().into_inner().unwrap(); + let formatted = format_raw_output(stdout); + assert_snapshot!(formatted); +} + +#[test] +fn bi_directional_traffic() { + let keyboard_events = Box::new(KeyboardEvents::new(vec![ + None, // sleep + None, // sleep + Some(Event::Key(Key::Ctrl('c'))), + ])); + let network_frames = NetworkFrames::new(vec![ + Some(build_tcp_packet( + "10.0.0.2", + "1.1.1.1", + 443, + 12345, + b"I am a fake tcp upload packet", + )), + Some(build_tcp_packet( + "1.1.1.1", + "10.0.0.2", + 12345, + 443, + b"I am a fake tcp download packet", + )), + ]); + + let terminal_width = Arc::new(Mutex::new(190)); + let terminal_height = Arc::new(Mutex::new(50)); + let terminal_events = LogWithMirror::new(Vec::new()); + let terminal_draw_events = LogWithMirror::new(Vec::new()); + + let backend = TestBackend::new( + terminal_events.write, + terminal_draw_events.write, + terminal_width, + terminal_height, + ); + let network_interface = get_interface(); + let lookup_addr = create_fake_lookup_addr(HashMap::new()); + let on_winch = create_fake_on_winch(false); + let cleanup = Box::new(|| {}); + let stdout = Arc::new(Mutex::new(Vec::new())); + let write_to_stdout = Box::new({ + let stdout = stdout.clone(); + move |output: String| { + let mut stdout = stdout.lock().unwrap(); + writeln!(&mut stdout, "{}", output).unwrap(); + } + }); + + let os_input = OsInputOutput { + network_interface, + network_frames, + get_open_sockets, + keyboard_events, + lookup_addr, + on_winch, + cleanup, + write_to_stdout, + }; + let opts = Opt { + interface: String::from("interface_name"), + raw: true, + no_resolve: false, + }; + start(backend, os_input, opts); + let stdout = Arc::try_unwrap(stdout).unwrap().into_inner().unwrap(); + let formatted = format_raw_output(stdout); + assert_snapshot!(formatted); +} + +#[test] +fn multiple_packets_of_traffic_from_different_connections() { + let keyboard_events = Box::new(KeyboardEvents::new(vec![ + None, // sleep + None, // sleep + Some(Event::Key(Key::Ctrl('c'))), + ])); + let network_frames = NetworkFrames::new(vec![ + Some(build_tcp_packet( + "1.1.1.1", + "10.0.0.2", + 12345, + 443, + b"I have come from 1.1.1.1", + )), + Some(build_tcp_packet( + "2.2.2.2", + "10.0.0.2", + 54321, + 443, + b"I come from 2.2.2.2", + )), + ]); + + let terminal_width = Arc::new(Mutex::new(190)); + let terminal_height = Arc::new(Mutex::new(50)); + let terminal_events = LogWithMirror::new(Vec::new()); + let terminal_draw_events = LogWithMirror::new(Vec::new()); + + let backend = TestBackend::new( + terminal_events.write, + terminal_draw_events.write, + terminal_width, + terminal_height, + ); + let on_winch = create_fake_on_winch(false); + let cleanup = Box::new(|| {}); + let network_interface = get_interface(); + let lookup_addr = create_fake_lookup_addr(HashMap::new()); + let stdout = Arc::new(Mutex::new(Vec::new())); + let write_to_stdout = Box::new({ + let stdout = stdout.clone(); + move |output: String| { + let mut stdout = stdout.lock().unwrap(); + writeln!(&mut stdout, "{}", output).unwrap(); + } + }); + + let os_input = OsInputOutput { + network_interface, + network_frames, + get_open_sockets, + on_winch, + cleanup, + keyboard_events, + lookup_addr, + write_to_stdout, + }; + let opts = Opt { + interface: String::from("interface_name"), + raw: true, + no_resolve: false, + }; + start(backend, os_input, opts); + let stdout = Arc::try_unwrap(stdout).unwrap().into_inner().unwrap(); + let formatted = format_raw_output(stdout); + assert_snapshot!(formatted); +} + +#[test] +fn multiple_packets_of_traffic_from_single_connection() { + let keyboard_events = Box::new(KeyboardEvents::new(vec![ + None, // sleep + None, // sleep + Some(Event::Key(Key::Ctrl('c'))), + ])); + let network_frames = NetworkFrames::new(vec![ + Some(build_tcp_packet( + "1.1.1.1", + "10.0.0.2", + 12345, + 443, + b"I have come from 1.1.1.1", + )), + Some(build_tcp_packet( + "1.1.1.1", + "10.0.0.2", + 12345, + 443, + b"I've come from 1.1.1.1 too!", + )), + ]); + + let terminal_width = Arc::new(Mutex::new(190)); + let terminal_height = Arc::new(Mutex::new(50)); + let terminal_events = LogWithMirror::new(Vec::new()); + let terminal_draw_events = LogWithMirror::new(Vec::new()); + + let backend = TestBackend::new( + terminal_events.write, + terminal_draw_events.write, + terminal_width, + terminal_height, + ); + let network_interface = get_interface(); + let lookup_addr = create_fake_lookup_addr(HashMap::new()); + let on_winch = create_fake_on_winch(false); + let cleanup = Box::new(|| {}); + let stdout = Arc::new(Mutex::new(Vec::new())); + let write_to_stdout = Box::new({ + let stdout = stdout.clone(); + move |output: String| { + let mut stdout = stdout.lock().unwrap(); + writeln!(&mut stdout, "{}", output).unwrap(); + } + }); + + let os_input = OsInputOutput { + network_interface, + network_frames, + get_open_sockets, + keyboard_events, + lookup_addr, + on_winch, + cleanup, + write_to_stdout, + }; + let opts = Opt { + interface: String::from("interface_name"), + raw: true, + no_resolve: false, + }; + start(backend, os_input, opts); + let stdout = Arc::try_unwrap(stdout).unwrap().into_inner().unwrap(); + let formatted = format_raw_output(stdout); + assert_snapshot!(formatted); +} + +#[test] +fn one_process_with_multiple_connections() { + let keyboard_events = Box::new(KeyboardEvents::new(vec![ + None, // sleep + None, // sleep + Some(Event::Key(Key::Ctrl('c'))), + ])); + let network_frames = NetworkFrames::new(vec![ + Some(build_tcp_packet( + "1.1.1.1", + "10.0.0.2", + 12345, + 443, + b"I have come from 1.1.1.1", + )), + Some(build_tcp_packet( + "3.3.3.3", + "10.0.0.2", + 1337, + 443, + b"Funny that, I'm from 3.3.3.3", + )), + ]); + + let terminal_width = Arc::new(Mutex::new(190)); + let terminal_height = Arc::new(Mutex::new(50)); + let terminal_events = LogWithMirror::new(Vec::new()); + let terminal_draw_events = LogWithMirror::new(Vec::new()); + + let backend = TestBackend::new( + terminal_events.write, + terminal_draw_events.write, + terminal_width, + terminal_height, + ); + let network_interface = get_interface(); + let lookup_addr = create_fake_lookup_addr(HashMap::new()); + let on_winch = create_fake_on_winch(false); + let cleanup = Box::new(|| {}); + let stdout = Arc::new(Mutex::new(Vec::new())); + let write_to_stdout = Box::new({ + let stdout = stdout.clone(); + move |output: String| { + let mut stdout = stdout.lock().unwrap(); + writeln!(&mut stdout, "{}", output).unwrap(); + } + }); + + let os_input = OsInputOutput { + network_interface, + network_frames, + get_open_sockets, + keyboard_events, + lookup_addr, + on_winch, + cleanup, + write_to_stdout, + }; + let opts = Opt { + interface: String::from("interface_name"), + raw: true, + no_resolve: false, + }; + start(backend, os_input, opts); + let stdout = Arc::try_unwrap(stdout).unwrap().into_inner().unwrap(); + let formatted = format_raw_output(stdout); + assert_snapshot!(formatted); +} + +#[test] +fn multiple_processes_with_multiple_connections() { + let keyboard_events = Box::new(KeyboardEvents::new(vec![ + None, // sleep + None, // sleep + Some(Event::Key(Key::Ctrl('c'))), + ])); + let network_frames = NetworkFrames::new(vec![ + Some(build_tcp_packet( + "1.1.1.1", + "10.0.0.2", + 12345, + 443, + b"I have come from 1.1.1.1", + )), + Some(build_tcp_packet( + "3.3.3.3", + "10.0.0.2", + 1337, + 443, + b"Awesome, I'm from 3.3.3.3", + )), + Some(build_tcp_packet( + "2.2.2.2", + "10.0.0.2", + 54321, + 443, + b"You know, 2.2.2.2 is really nice!", + )), + Some(build_tcp_packet( + "4.4.4.4", + "10.0.0.2", + 1337, + 443, + b"I'm partial to 4.4.4.4", + )), + ]); + + let terminal_width = Arc::new(Mutex::new(190)); + let terminal_height = Arc::new(Mutex::new(50)); + let terminal_events = LogWithMirror::new(Vec::new()); + let terminal_draw_events = LogWithMirror::new(Vec::new()); + + let backend = TestBackend::new( + terminal_events.write, + terminal_draw_events.write, + terminal_width, + terminal_height, + ); + let network_interface = get_interface(); + let lookup_addr = create_fake_lookup_addr(HashMap::new()); + let on_winch = create_fake_on_winch(false); + let cleanup = Box::new(|| {}); + let stdout = Arc::new(Mutex::new(Vec::new())); + let write_to_stdout = Box::new({ + let stdout = stdout.clone(); + move |output: String| { + let mut stdout = stdout.lock().unwrap(); + writeln!(&mut stdout, "{}", output).unwrap(); + } + }); + + let os_input = OsInputOutput { + network_interface, + network_frames, + get_open_sockets, + keyboard_events, + lookup_addr, + on_winch, + cleanup, + write_to_stdout, + }; + let opts = Opt { + interface: String::from("interface_name"), + raw: true, + no_resolve: false, + }; + start(backend, os_input, opts); + let stdout = Arc::try_unwrap(stdout).unwrap().into_inner().unwrap(); + let formatted = format_raw_output(stdout); + assert_snapshot!(formatted); +} + +#[test] +fn multiple_connections_from_remote_address() { + let keyboard_events = Box::new(KeyboardEvents::new(vec![ + None, // sleep + None, // sleep + Some(Event::Key(Key::Ctrl('c'))), + ])); + let network_frames = NetworkFrames::new(vec![ + Some(build_tcp_packet( + "1.1.1.1", + "10.0.0.2", + 12345, + 443, + b"I have come from 1.1.1.1", + )), + Some(build_tcp_packet( + "1.1.1.1", + "10.0.0.2", + 12346, + 443, + b"Me too, but on a different port", + )), + ]); + + let terminal_width = Arc::new(Mutex::new(190)); + let terminal_height = Arc::new(Mutex::new(50)); + let terminal_events = LogWithMirror::new(Vec::new()); + let terminal_draw_events = LogWithMirror::new(Vec::new()); + + let backend = TestBackend::new( + terminal_events.write, + terminal_draw_events.write, + terminal_width, + terminal_height, + ); + let network_interface = get_interface(); + let lookup_addr = create_fake_lookup_addr(HashMap::new()); + let on_winch = create_fake_on_winch(false); + let cleanup = Box::new(|| {}); + let stdout = Arc::new(Mutex::new(Vec::new())); + let write_to_stdout = Box::new({ + let stdout = stdout.clone(); + move |output: String| { + let mut stdout = stdout.lock().unwrap(); + writeln!(&mut stdout, "{}", output).unwrap(); + } + }); + + let os_input = OsInputOutput { + network_interface, + network_frames, + get_open_sockets, + keyboard_events, + lookup_addr, + on_winch, + cleanup, + write_to_stdout, + }; + let opts = Opt { + interface: String::from("interface_name"), + raw: true, + no_resolve: false, + }; + start(backend, os_input, opts); + let stdout = Arc::try_unwrap(stdout).unwrap().into_inner().unwrap(); + let formatted = format_raw_output(stdout); + assert_snapshot!(formatted); +} + +#[test] +fn sustained_traffic_from_one_process() { + let keyboard_events = Box::new(KeyboardEvents::new(vec![ + None, // sleep + None, // sleep + None, // sleep + Some(Event::Key(Key::Ctrl('c'))), + ])); + let network_frames = NetworkFrames::new(vec![ + Some(build_tcp_packet( + "1.1.1.1", + "10.0.0.2", + 12345, + 443, + b"I have come from 1.1.1.1", + )), + None, // sleep + Some(build_tcp_packet( + "1.1.1.1", + "10.0.0.2", + 12345, + 443, + b"Same here, but one second later", + )), + ]); + + let terminal_width = Arc::new(Mutex::new(190)); + let terminal_height = Arc::new(Mutex::new(50)); + let terminal_events = LogWithMirror::new(Vec::new()); + let terminal_draw_events = LogWithMirror::new(Vec::new()); + + let backend = TestBackend::new( + terminal_events.write, + terminal_draw_events.write, + terminal_width, + terminal_height, + ); + let network_interface = get_interface(); + let lookup_addr = create_fake_lookup_addr(HashMap::new()); + let on_winch = create_fake_on_winch(false); + let cleanup = Box::new(|| {}); + let stdout = Arc::new(Mutex::new(Vec::new())); + let write_to_stdout = Box::new({ + let stdout = stdout.clone(); + move |output: String| { + let mut stdout = stdout.lock().unwrap(); + writeln!(&mut stdout, "{}", output).unwrap(); + } + }); + + let os_input = OsInputOutput { + network_interface, + network_frames, + get_open_sockets, + keyboard_events, + lookup_addr, + on_winch, + cleanup, + write_to_stdout, + }; + let opts = Opt { + interface: String::from("interface_name"), + raw: true, + no_resolve: false, + }; + start(backend, os_input, opts); + let stdout = Arc::try_unwrap(stdout).unwrap().into_inner().unwrap(); + let formatted = format_raw_output(stdout); + assert_snapshot!(formatted); +} + +#[test] +fn sustained_traffic_from_multiple_processes() { + let keyboard_events = Box::new(KeyboardEvents::new(vec![ + None, // sleep + None, // sleep + None, // sleep + Some(Event::Key(Key::Ctrl('c'))), + ])); + let network_frames = NetworkFrames::new(vec![ + Some(build_tcp_packet( + "1.1.1.1", + "10.0.0.2", + 12345, + 443, + b"I have come from 1.1.1.1", + )), + Some(build_tcp_packet( + "3.3.3.3", + "10.0.0.2", + 1337, + 443, + b"I come from 3.3.3.3", + )), + None, // sleep + Some(build_tcp_packet( + "1.1.1.1", + "10.0.0.2", + 12345, + 443, + b"I have come from 1.1.1.1 one second later", + )), + Some(build_tcp_packet( + "3.3.3.3", + "10.0.0.2", + 1337, + 443, + b"I come 3.3.3.3 one second later", + )), + ]); + + let terminal_width = Arc::new(Mutex::new(190)); + let terminal_height = Arc::new(Mutex::new(50)); + let terminal_events = LogWithMirror::new(Vec::new()); + let terminal_draw_events = LogWithMirror::new(Vec::new()); + + let backend = TestBackend::new( + terminal_events.write, + terminal_draw_events.write, + terminal_width, + terminal_height, + ); + let network_interface = get_interface(); + let lookup_addr = create_fake_lookup_addr(HashMap::new()); + let on_winch = create_fake_on_winch(false); + let cleanup = Box::new(|| {}); + let stdout = Arc::new(Mutex::new(Vec::new())); + let write_to_stdout = Box::new({ + let stdout = stdout.clone(); + move |output: String| { + let mut stdout = stdout.lock().unwrap(); + writeln!(&mut stdout, "{}", output).unwrap(); + } + }); + + let os_input = OsInputOutput { + network_interface, + network_frames, + get_open_sockets, + keyboard_events, + lookup_addr, + on_winch, + cleanup, + write_to_stdout, + }; + let opts = Opt { + interface: String::from("interface_name"), + raw: true, + no_resolve: false, + }; + start(backend, os_input, opts); + let stdout = Arc::try_unwrap(stdout).unwrap().into_inner().unwrap(); + let formatted = format_raw_output(stdout); + assert_snapshot!(formatted); +} + +#[test] +fn sustained_traffic_from_multiple_processes_bi_directional() { + let keyboard_events = Box::new(KeyboardEvents::new(vec![ + None, // sleep + None, // sleep + None, // sleep + Some(Event::Key(Key::Ctrl('c'))), + ])); + let network_frames = NetworkFrames::new(vec![ + Some(build_tcp_packet( + "10.0.0.2", + "3.3.3.3", + 443, + 1337, + b"omw to 3.3.3.3", + )), + Some(build_tcp_packet( + "3.3.3.3", + "10.0.0.2", + 1337, + 443, + b"I was just there!", + )), + Some(build_tcp_packet( + "1.1.1.1", + "10.0.0.2", + 12345, + 443, + b"Is it nice there? I think 1.1.1.1 is dull", + )), + Some(build_tcp_packet( + "10.0.0.2", + "1.1.1.1", + 443, + 12345, + b"Well, I heard 1.1.1.1 is all the rage", + )), + None, // sleep + Some(build_tcp_packet( + "10.0.0.2", + "3.3.3.3", + 443, + 1337, + b"Wait for me!", + )), + Some(build_tcp_packet( + "3.3.3.3", + "10.0.0.2", + 1337, + 443, + b"They're waiting for you...", + )), + Some(build_tcp_packet( + "1.1.1.1", + "10.0.0.2", + 12345, + 443, + b"1.1.1.1 forever!", + )), + Some(build_tcp_packet( + "10.0.0.2", + "1.1.1.1", + 443, + 12345, + b"10.0.0.2 forever!", + )), + ]); + + let terminal_width = Arc::new(Mutex::new(190)); + let terminal_height = Arc::new(Mutex::new(50)); + let terminal_events = LogWithMirror::new(Vec::new()); + let terminal_draw_events = LogWithMirror::new(Vec::new()); + + let backend = TestBackend::new( + terminal_events.write, + terminal_draw_events.write, + terminal_width, + terminal_height, + ); + let network_interface = get_interface(); + let lookup_addr = create_fake_lookup_addr(HashMap::new()); + let on_winch = create_fake_on_winch(false); + let cleanup = Box::new(|| {}); + let stdout = Arc::new(Mutex::new(Vec::new())); + let write_to_stdout = Box::new({ + let stdout = stdout.clone(); + move |output: String| { + let mut stdout = stdout.lock().unwrap(); + writeln!(&mut stdout, "{}", output).unwrap(); + } + }); + + let os_input = OsInputOutput { + network_interface, + network_frames, + get_open_sockets, + keyboard_events, + lookup_addr, + on_winch, + cleanup, + write_to_stdout, + }; + let opts = Opt { + interface: String::from("interface_name"), + raw: true, + no_resolve: false, + }; + start(backend, os_input, opts); + let stdout = Arc::try_unwrap(stdout).unwrap().into_inner().unwrap(); + let formatted = format_raw_output(stdout); + assert_snapshot!(formatted); +} + +#[test] +fn traffic_with_host_names() { + let keyboard_events = Box::new(KeyboardEvents::new(vec![ + None, // sleep + None, // sleep + None, // sleep + Some(Event::Key(Key::Ctrl('c'))), + ])); + let network_frames = NetworkFrames::new(vec![ + Some(build_tcp_packet( + "10.0.0.2", + "3.3.3.3", + 443, + 1337, + b"omw to 3.3.3.3", + )), + Some(build_tcp_packet( + "3.3.3.3", + "10.0.0.2", + 1337, + 443, + b"I was just there!", + )), + Some(build_tcp_packet( + "1.1.1.1", + "10.0.0.2", + 12345, + 443, + b"Is it nice there? I think 1.1.1.1 is dull", + )), + Some(build_tcp_packet( + "10.0.0.2", + "1.1.1.1", + 443, + 12345, + b"Well, I heard 1.1.1.1 is all the rage", + )), + None, // sleep + Some(build_tcp_packet( + "10.0.0.2", + "3.3.3.3", + 443, + 1337, + b"Wait for me!", + )), + Some(build_tcp_packet( + "3.3.3.3", + "10.0.0.2", + 1337, + 443, + b"They're waiting for you...", + )), + Some(build_tcp_packet( + "1.1.1.1", + "10.0.0.2", + 12345, + 443, + b"1.1.1.1 forever!", + )), + Some(build_tcp_packet( + "10.0.0.2", + "1.1.1.1", + 443, + 12345, + b"10.0.0.2 forever!", + )), + ]); + + let terminal_width = Arc::new(Mutex::new(190)); + let terminal_height = Arc::new(Mutex::new(50)); + let termina