├── .config └── config.json5 ├── .envrc ├── .github ├── FUNDING.yml └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── FUNDING.json ├── LICENSE ├── README.md ├── build.rs ├── debian ├── changelog └── netscanner.manpage ├── demo.gif ├── demo.tape ├── examples ├── chunks.rs ├── cidr.rs ├── env_csv.rs ├── ip_lookup.rs └── port.rs ├── rust-toolchain.toml ├── src ├── action.rs ├── app.rs ├── cli.rs ├── components.rs ├── components │ ├── discovery.rs │ ├── export.rs │ ├── interfaces.rs │ ├── packetdump.rs │ ├── ports.rs │ ├── sniff.rs │ ├── tabs.rs │ ├── title.rs │ ├── wifi_chart.rs │ ├── wifi_interface.rs │ └── wifi_scan.rs ├── config.rs ├── enums.rs ├── layout.rs ├── main.rs ├── mode.rs ├── tui.rs ├── utils.rs ├── widgets.rs └── widgets │ └── scroll_traffic.rs └── traffic.png /.config/config.json5: -------------------------------------------------------------------------------- 1 | { 2 | "keybindings": { 3 | "Normal": { 4 | "": "Quit", // Quit the application 5 | "": "Quit", // Another way to quit 6 | "": "Quit", // Yet another way to quit 7 | "": "Suspend", // Suspend the application 8 | "": "InputMode", 9 | "": "Graph", 10 | "": "Dump", 11 | "": "Interface", 12 | "": "Clear", 13 | "": "Scan", 14 | "": "Export", 15 | "": "Up", 16 | "": "Down", 17 | "": "Left", 18 | "": "Right", 19 | "": "Tab", 20 | "<1>": "JumpDiscovery", 21 | "<2>": "JumpPackets", 22 | "<3>": "JumpPorts", 23 | "<4>": "JumpSniffer", 24 | }, 25 | "Input": { 26 | "": "NormalMode" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | export NETSCANNER_CONFIG=`pwd`/.config 2 | export NETSCANNER_DATA=`pwd`/.data 3 | export NETSCANNER_LOG_LEVEL=debug 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Chleba] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: chlebikn # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.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 | RELEASE_TAG: 0.5.1-1 # This is the version of the package, it should be updated with every release 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Build 21 | run: cargo build --verbose --release 22 | - name: Run tests 23 | run: cargo test --verbose 24 | 25 | - name: Create Debian package 26 | uses: dominicorsi/cargo-deb-ubuntu@1.0.0 27 | 28 | - name: Upload Netscanner Debian Package (amd64) 29 | uses: actions/upload-artifact@v4 30 | with: 31 | name: netscanner_${{ env.RELEASE_TAG }}_amd64.deb 32 | path: netscanner_${{ env.RELEASE_TAG }}_amd64.deb 33 | 34 | - name: Release 35 | uses: softprops/action-gh-release@v2 36 | if: startsWith(github.ref, 'refs/tags/') 37 | with: 38 | files: netscanner_${{ env.RELEASE_TAG }}_amd64.deb -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "netscanner" 3 | version = "0.6.3" 4 | edition = "2021" 5 | description = "Network Scanner" 6 | license = "MIT" 7 | authors = ["Chleba "] 8 | repository = "https://github.com/Chleba/netscanner" 9 | homepage = "https://github.com/Chleba/netscanner" 10 | 11 | [profile.release] 12 | strip = true 13 | opt-level = "z" 14 | lto = true 15 | 16 | [package.metadata.deb] 17 | maintainer = "Dominic Orsi " 18 | depends = "iw" 19 | section = "utils" 20 | priority = "optional" 21 | changelog = "debian/changelog" 22 | license-file = ["LICENSE", "4"] 23 | extended-description = """\ 24 | Terminal Network scanner & diagnostic tool with modern TUI (terminal user interface). \n 25 | GitHub: https://github.com/Chleba/netscanner""" 26 | assets = [ 27 | [ 28 | "target/release/netscanner", 29 | "usr/bin/", 30 | "4755", 31 | ], 32 | [ 33 | "README.md", 34 | "usr/share/doc/netscanner/README", 35 | "644", 36 | ], 37 | [ 38 | "debian/netscanner.manpage", 39 | "usr/share/man/man1/netscanner.1", 40 | "644", 41 | ], 42 | ] 43 | 44 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 45 | 46 | [dependencies] 47 | better-panic = "0.3.0" 48 | chrono = "0.4.31" 49 | cidr = "0.2.2" 50 | clap = { version = "4.4.5", features = [ 51 | "derive", 52 | "cargo", 53 | "wrap_help", 54 | "unicode", 55 | "string", 56 | "unstable-styles", 57 | ] } 58 | color-eyre = "0.6.3" 59 | config = "0.14.0" 60 | crossterm = { version = "0.28.1", features = ["serde", "event-stream"] } 61 | csv = "1.3.0" 62 | derive_deref = "1.1.1" 63 | directories = "5.0.1" 64 | dns-lookup = "2.0.4" 65 | fastping-rs = "0.2.4" 66 | futures = "0.3.30" 67 | human-panic = "2.0.1" 68 | ipnetwork = "0.20.0" 69 | itertools = "0.13.0" 70 | json5 = "0.4.1" 71 | lazy_static = "1.5.0" 72 | libc = "0.2.158" 73 | log = "0.4.22" 74 | mac_oui = { version = "0.4.11", features = ["with-db"] } 75 | pnet = "0.35.0" 76 | port-desc = "0.1.1" 77 | pretty_assertions = "1.4.0" 78 | rand = "0.8.5" 79 | ratatui = { version = "0.28.1", features = ["serde", "macros"] } 80 | regex = "1.10.3" 81 | serde = { version = "1.0.210", features = ["derive"] } 82 | serde_json = "1.0.128" 83 | signal-hook = "0.3.17" 84 | strip-ansi-escapes = "0.2.0" 85 | strum = "0.26.3" 86 | surge-ping = "0.8.1" 87 | tokio = { version = "1.40.0", features = ["full"] } 88 | tokio-util = "0.7.12" 89 | tokio-wifiscanner = "0.2.2" 90 | tracing = "0.1.40" 91 | tracing-error = "0.2.0" 92 | tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] } 93 | tui-input = { version = "0.10.1", features = ["serde"] } 94 | tui-scrollview = "0.4.0" 95 | 96 | [target.'cfg(target_os = "windows")'.build-dependencies] 97 | anyhow = "1.0.86" 98 | http_req = "0.13.3" 99 | zip = "2.1.6" 100 | clap = { version = "4.5.13", features = ["derive"] } 101 | clap-verbosity-flag = "2.2.1" 102 | clap_complete = "4.5.12" 103 | clap_mangen = "0.2.23" 104 | -------------------------------------------------------------------------------- /FUNDING.json: -------------------------------------------------------------------------------- 1 | { 2 | "drips": { 3 | "ethereum": { 4 | "ownedBy": "0x341d98043b03B63B30732ae981503659CcCbE760" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Chleba 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | _ 3 | | | 4 | _ __ ___| |_ ___ ___ __ _ _ __ _ __ ___ _ __ 5 | | '_ \ / _ \ __/ __|/ __/ _` | '_ \| '_ \ / _ \ '__| 6 | | | | | __/ |_\__ \ (_| (_| | | | | | | | __/ | 7 | |_| |_|\___|\__|___/\___\__,_|_| |_|_| |_|\___|_| 8 | ``` 9 | *** 10 | 11 | 12 | [![Arch package](https://repology.org/badge/version-for-repo/arch/netscanner.svg)](https://repology.org/project/netscanner/versions) 13 | [![nixpkgs unstable package](https://repology.org/badge/version-for-repo/nix_unstable/netscanner.svg)](https://repology.org/project/netscanner/versions) 14 | [![Manjaro Stable package](https://repology.org/badge/version-for-repo/manjaro_stable/netscanner.svg)](https://repology.org/project/netscanner/versions) 15 | [![Kali Linux Rolling package](https://repology.org/badge/version-for-repo/kali_rolling/netscanner.svg)](https://repology.org/project/netscanner/versions) 16 | 17 | `netscanner` - Network scanner & diagnostic tool. 18 | 19 | **FEATURES:** 20 | - [x] List HW Interfaces 21 | - [x] Switching active Interface for scanning & packet-dumping 22 | - [x] WiFi networks scanning 23 | - [x] WiFi signals strength (with charts) 24 | - [x] (IPv4) Pinging CIDR with hostname, oui & mac address 25 | - [x] (IPv4) Packetdump (TCP, UDP, ICMP, ARP) 26 | - [x] (IPv6) Packetdump (ICMP6) 27 | - [x] start/pause packetdump 28 | - [x] scanning open ports (TCP) 29 | - [x] packet logs filter 30 | - [x] export scanned ips, ports, packets into csv 31 | - [x] traffic counting + DNS records 32 | 33 | **TODO:** 34 | - [ ] ipv6 scanning & dumping 35 | 36 | ## *Notes*: 37 | - Must be run with root privileges. 38 | - After `cargo install` You may try to change binary file chown & chmod 39 | - Export default path is in the user's `$HOME` directory (linux & macos) 40 | ``` 41 | sudo chown root:user /home/user/.cargo/bin/netscanner 42 | sudo chmod u+s /home/user/.cargo/bin/netscanner 43 | ``` 44 | 45 | ## Install on `Arch Linux` 46 | ``` 47 | pacman -S netscanner 48 | ``` 49 | 50 | ## Install on `Alpine(edge) Linux` 51 | ``` 52 | apk add netscanner --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/ 53 | ``` 54 | 55 | ## Install with `cargo` 56 | ``` 57 | cargo install netscanner 58 | ``` 59 | 60 | ## Windows Installation 61 | 62 | To use `netscanner` on Windows, you need to install [Npcap](https://npcap.com/dist/npcap-1.80.exe), otherwise you may encounter packet.dll error. Npcap is a packet capture library required for network scanning and packet analysis. 63 | 64 | ## Appreciation 65 | `netscanner` has been made thanks to some awesome libraries that can be found in [Cargo.toml](./Cargo.toml) file. 66 | But mostly I would like to link these two libraries that help me the most: 67 | - Ratatui: [https://github.com/ratatui-org/ratatui](https://github.com/ratatui-org/ratatui) 68 | - libpnet: [https://github.com/libpnet/libpnet](https://github.com/libpnet/libpnet) 69 | 70 | > Created by: Lukas Chleba 71 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(target_os = "windows")] 3 | download_windows_npcap_sdk().unwrap(); 4 | 5 | let git_output = std::process::Command::new("git") 6 | .args(["rev-parse", "--git-dir"]) 7 | .output() 8 | .ok(); 9 | let git_dir = git_output.as_ref().and_then(|output| { 10 | std::str::from_utf8(&output.stdout) 11 | .ok() 12 | .and_then(|s| s.strip_suffix('\n').or_else(|| s.strip_suffix("\r\n"))) 13 | }); 14 | 15 | // Tell cargo to rebuild if the head or any relevant refs change. 16 | if let Some(git_dir) = git_dir { 17 | let git_path = std::path::Path::new(git_dir); 18 | let refs_path = git_path.join("refs"); 19 | if git_path.join("HEAD").exists() { 20 | println!("cargo:rerun-if-changed={}/HEAD", git_dir); 21 | } 22 | if git_path.join("packed-refs").exists() { 23 | println!("cargo:rerun-if-changed={}/packed-refs", git_dir); 24 | } 25 | if refs_path.join("heads").exists() { 26 | println!("cargo:rerun-if-changed={}/refs/heads", git_dir); 27 | } 28 | if refs_path.join("tags").exists() { 29 | println!("cargo:rerun-if-changed={}/refs/tags", git_dir); 30 | } 31 | } 32 | 33 | let git_output = std::process::Command::new("git") 34 | .args(["describe", "--always", "--tags", "--long", "--dirty"]) 35 | .output() 36 | .ok(); 37 | let git_info = git_output 38 | .as_ref() 39 | .and_then(|output| std::str::from_utf8(&output.stdout).ok().map(str::trim)); 40 | let cargo_pkg_version = env!("CARGO_PKG_VERSION"); 41 | 42 | // Default git_describe to cargo_pkg_version 43 | let mut git_describe = String::from(cargo_pkg_version); 44 | 45 | if let Some(git_info) = git_info { 46 | // If the `git_info` contains `CARGO_PKG_VERSION`, we simply use `git_info` as it is. 47 | // Otherwise, prepend `CARGO_PKG_VERSION` to `git_info`. 48 | if git_info.contains(cargo_pkg_version) { 49 | // Remove the 'g' before the commit sha 50 | let git_info = &git_info.replace('g', ""); 51 | git_describe = git_info.to_string(); 52 | } else { 53 | git_describe = format!("v{}-{}", cargo_pkg_version, git_info); 54 | } 55 | } 56 | 57 | println!("cargo:rustc-env=_GIT_INFO={}", git_describe); 58 | } 59 | 60 | // -- unfortunately netscanner need to download sdk because of Packet.lib for build locally 61 | #[cfg(target_os = "windows")] 62 | fn download_windows_npcap_sdk() -> anyhow::Result<()> { 63 | use anyhow::anyhow; 64 | 65 | use std::{ 66 | env, fs, 67 | io::{self, Write}, 68 | path::PathBuf, 69 | }; 70 | 71 | use http_req::request; 72 | use zip::ZipArchive; 73 | 74 | println!("cargo:rerun-if-changed=build.rs"); 75 | 76 | // get npcap SDK 77 | const NPCAP_SDK: &str = "npcap-sdk-1.13.zip"; 78 | 79 | let npcap_sdk_download_url = format!("https://npcap.com/dist/{NPCAP_SDK}"); 80 | let cache_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?).join("target"); 81 | let npcap_sdk_cache_path = cache_dir.join(NPCAP_SDK); 82 | 83 | let npcap_zip = match fs::read(&npcap_sdk_cache_path) { 84 | // use cached 85 | Ok(zip_data) => { 86 | eprintln!("Found cached npcap SDK"); 87 | zip_data 88 | } 89 | // download SDK 90 | Err(_) => { 91 | eprintln!("Downloading npcap SDK"); 92 | 93 | // download 94 | let mut zip_data = vec![]; 95 | let _res = request::get(npcap_sdk_download_url, &mut zip_data)?; 96 | 97 | // write cache 98 | fs::create_dir_all(cache_dir)?; 99 | let mut cache = fs::File::create(npcap_sdk_cache_path)?; 100 | cache.write_all(&zip_data)?; 101 | 102 | zip_data 103 | } 104 | }; 105 | 106 | // extract DLL 107 | let lib_path = if cfg!(target_arch = "aarch64") { 108 | "Lib/ARM64/Packet.lib" 109 | } else if cfg!(target_arch = "x86_64") { 110 | "Lib/x64/Packet.lib" 111 | } else if cfg!(target_arch = "x86") { 112 | "Lib/Packet.lib" 113 | } else { 114 | panic!("Unsupported target!") 115 | }; 116 | let mut archive = ZipArchive::new(io::Cursor::new(npcap_zip))?; 117 | let mut npcap_lib = archive.by_name(lib_path)?; 118 | 119 | // write DLL 120 | let lib_dir = PathBuf::from(env::var("OUT_DIR")?).join("npcap_sdk"); 121 | let lib_path = lib_dir.join("Packet.lib"); 122 | fs::create_dir_all(&lib_dir)?; 123 | let mut lib_file = fs::File::create(lib_path)?; 124 | io::copy(&mut npcap_lib, &mut lib_file)?; 125 | 126 | println!( 127 | "cargo:rustc-link-search=native={}", 128 | lib_dir 129 | .to_str() 130 | .ok_or(anyhow!("{lib_dir:?} is not valid UTF-8"))? 131 | ); 132 | 133 | Ok(()) 134 | } 135 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | netscanner (0.5.1-1) stable; urgency=medium 2 | 3 | * Initial release. 4 | 5 | -- Dominic Orsi Sun, 23 Jun 2024 17:25:30 -0700 -------------------------------------------------------------------------------- /debian/netscanner.manpage: -------------------------------------------------------------------------------- 1 | .\" Manpage for netscanner. 2 | .\" Contact dominic.orsi@gmail.com to correct errors or typos. 3 | .TH man 1 "22 June 2024" "1.0" "netscanner man page" 4 | .SH NAME 5 | netscanner \- view and scan connected networks 6 | .SH SYNOPSIS 7 | .B netscanner 8 | [\fIOPTION\fR]... 9 | .SH DESCRIPTION 10 | .B netscanner 11 | is a terminal network scanner & diagnostic tool with modern TUI. 12 | .SH OPTIONS 13 | .TP 14 | .BR \-t ", " \-\-tick\-rate " " \fIFLOAT\fR 15 | Tick rate, i.e. number of ticks per second [default: 1] 16 | .TP 17 | .BR \-f ", " \-\-frame\-rate " " \fIFLOAT\fR 18 | Frame rate, i.e. number of frames per second [default: 10] 19 | .TP 20 | .BR \-h ", " \-\-help 21 | Print help 22 | .TP 23 | .BR \-V ", " \-\-version 24 | Print version 25 | .SH BUGS 26 | See GitHub issues: 27 | https://github.com/chleba/netscanner/issues 28 | .SH AUTHOR 29 | Dominic Orsi -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chleba/netscanner/f06f6aaf125dfcf0bc7ee82022c05fa70748c190/demo.gif -------------------------------------------------------------------------------- /demo.tape: -------------------------------------------------------------------------------- 1 | 2 | Output demo.gif 3 | 4 | Set FontSize 11 5 | Set Width 800 6 | Set Height 600 7 | 8 | Type "netscanner" 9 | Enter 10 | Sleep 800ms 11 | 12 | Type "s" 13 | Sleep 1s 14 | 15 | Type "2" 16 | Sleep 3s 17 | Type "g" 18 | Sleep 1s 19 | 20 | Type "3" 21 | 22 | Type "s" 23 | Sleep 1s 24 | 25 | Down 26 | Type "s" 27 | Sleep 1s 28 | 29 | Down 30 | Type "s" 31 | Sleep 1s 32 | 33 | Type "4" 34 | Sleep 4 35 | 36 | -------------------------------------------------------------------------------- /examples/chunks.rs: -------------------------------------------------------------------------------- 1 | use cidr::Ipv4Cidr; 2 | use std::net::Ipv4Addr; 3 | use tokio::task; 4 | use std::time::Duration; 5 | 6 | fn ping(ip: Ipv4Addr) -> Result { 7 | // println!("pinging ip - {}", ip); 8 | Ok(ip.to_string()) 9 | } 10 | 11 | #[tokio::main] 12 | async fn main() { 13 | 14 | let pool_size = 8; 15 | let mut ips: Vec = Vec::new(); 16 | 17 | let cidr_range = "192.168.1.0/24"; // Replace with your CIDR range 18 | match cidr_range.parse::() { 19 | Ok(ip_cidr) => { 20 | for ip in ip_cidr.iter() { 21 | ips.push(ip.address()); 22 | } 23 | } 24 | Err(e) => { 25 | eprintln!("Error parsing CIDR range: {}", e); 26 | } 27 | } 28 | 29 | // println!("{:?}", ips); 30 | 31 | let ip_chunks: Vec<_> = ips.chunks(pool_size).collect(); 32 | for chunk in ip_chunks { 33 | // println!("{:?}", chunk); 34 | let tasks: Vec<_> = chunk.iter().map(|&ip| { 35 | let closure = || async move { 36 | tokio::time::sleep(Duration::from_secs(1)).await; 37 | match ping(ip) { 38 | Ok(res) => { 39 | println!("{} ping done", res); 40 | } 41 | Err(err) => { 42 | println!("{:?} error", err); 43 | } 44 | } 45 | }; 46 | task::spawn(closure()) 47 | }).collect(); 48 | 49 | for task in tasks { 50 | task.await.unwrap(); 51 | // match task.await { 52 | // Ok(_) => {} 53 | // Err(_) => {} 54 | // }; 55 | } 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /examples/cidr.rs: -------------------------------------------------------------------------------- 1 | use cidr::Ipv4Cidr; 2 | 3 | fn main() { 4 | let cidr_range = "192.168.1.0/24"; // Replace with your CIDR range 5 | match cidr_range.parse::() { 6 | Ok(ip_cidr) => { 7 | for ip in ip_cidr.iter() { 8 | println!("IP Address: {}", ip); 9 | } 10 | } 11 | Err(e) => { 12 | eprintln!("Error parsing CIDR range: {}", e); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/env_csv.rs: -------------------------------------------------------------------------------- 1 | use std::{env, error::Error, io, process}; 2 | 3 | fn write_csv() -> Result<(), Box> { 4 | let mut csv_wtr = csv::Writer::from_writer(io::stdout()); 5 | csv_wtr.write_record(["maslo", "maslo1", "maslo2", "maslo3"])?; 6 | csv_wtr.write_record(["1", "12", "maslo12", "maslo13"])?; 7 | csv_wtr.write_record(["maslo", "maslo1", "maslo2", "maslo3"])?; 8 | 9 | csv_wtr.flush()?; 10 | 11 | Ok(()) 12 | } 13 | 14 | fn main() { 15 | println!("{:?}", env::vars_os()); 16 | 17 | match env::var_os("SUDO_USER") { 18 | Some(val) => println!("SUDO_USER: {val:?}"), 19 | None => println!("SUDO_USER was not found") 20 | } 21 | 22 | match env::var_os("HOME") { 23 | Some(val) => println!("HOME: {val:?}"), 24 | None => println!("HOME was not found") 25 | } 26 | 27 | if let Err(err) = write_csv() { 28 | println!("{}", err); 29 | process::exit(1); 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /examples/ip_lookup.rs: -------------------------------------------------------------------------------- 1 | // use tokio::net; 2 | // use std::io; 3 | use dns_lookup::getnameinfo; 4 | use std::net::{IpAddr, SocketAddr}; 5 | 6 | #[tokio::main] 7 | async fn main() { 8 | // let ip: IpAddr = "192.168.1.237".parse().unwrap(); 9 | let ip: IpAddr = "192.168.1.50".parse().unwrap(); 10 | let port = 22; 11 | let mut socket: SocketAddr = (ip, port).into(); 12 | let ports = vec![1, 5, 10, 22, 80, 3000, 8080, 5000, 5432, 7000]; 13 | 14 | for p in ports { 15 | socket.set_port(p); 16 | let (name, service) = match getnameinfo(&socket, 0) { 17 | Ok((n, s)) => (n, s), 18 | Err(e) => panic!("fail lookup {:?}", e), 19 | }; 20 | println!("{:?}, {:?}", name, service); 21 | } 22 | 23 | // let (name, service) = match getnameinfo(&socket, 0) { 24 | // Ok((n, s)) => (n, s), 25 | // Err(e) => panic!("fail lookup {:?}", e), 26 | // }; 27 | 28 | // println!("{:?}, {:?}", name, service); 29 | } 30 | -------------------------------------------------------------------------------- /examples/port.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::net::TcpListener; 3 | 4 | fn main() { 5 | get_available_port(); 6 | // if let Some(available_port) = get_available_port() { 7 | // println!("port `{}` is available", available_port); 8 | // } 9 | } 10 | 11 | // fn get_available_port() -> Option { 12 | fn get_available_port() { 13 | for p in 20..90 { 14 | println!("{}: {}", p, port_is_available(p)); 15 | // if port_is_available(p) == false { 16 | // println!("{} avail", p); 17 | // } 18 | } 19 | // (6000..9000) 20 | // .find(|port| port_is_available(*port)) 21 | // (6000..9000).into_iter().map(|p| { 22 | // println!("{}: avail", p); 23 | // }) 24 | } 25 | 26 | fn port_is_available(port: u16) -> bool { 27 | // match TcpListener::bind(("127.0.0.1", port)) { 28 | TcpListener::bind(("192.168.1.97", port)).is_ok() 29 | // match TcpListener::bind(("192.168.1.97", port)) { 30 | // Ok(_) => true, 31 | // Err(_) => false, 32 | // } 33 | } 34 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | -------------------------------------------------------------------------------- /src/action.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Local}; 2 | use pnet::datalink::NetworkInterface; 3 | use pnet::util::MacAddr; 4 | use ratatui::text::Line; 5 | use serde::{ 6 | de::{self, Deserializer, Visitor}, 7 | Deserialize, Serialize, 8 | }; 9 | use std::{fmt, net::Ipv4Addr}; 10 | 11 | use crate::{ 12 | components::{packetdump::ArpPacketData, wifi_scan::WifiInfo}, 13 | enums::{ExportData, PacketTypeEnum, PacketsInfoTypesEnum, TabsEnum}, 14 | mode::Mode, 15 | }; 16 | 17 | #[derive(Debug, Clone, PartialEq)] 18 | pub enum Action { 19 | Tick, 20 | Render, 21 | Resize(u16, u16), 22 | Suspend, 23 | Resume, 24 | Quit, 25 | Refresh, 26 | Error(String), 27 | Help, 28 | 29 | // -- custom actions 30 | Up, 31 | Down, 32 | Left, 33 | Right, 34 | Tab, 35 | TabChange(TabsEnum), 36 | GraphToggle, 37 | DumpToggle, 38 | InterfaceSwitch, 39 | ScanCidr, 40 | ActiveInterface(NetworkInterface), 41 | ArpRecieve(ArpPacketData), 42 | Scan(Vec), 43 | AppModeChange(Mode), 44 | ModeChange(Mode), 45 | PingIp(String), 46 | CountIp, 47 | CidrError, 48 | PacketDump(DateTime, PacketsInfoTypesEnum, PacketTypeEnum), 49 | PortScan(usize, u16), 50 | PortScanDone(usize), 51 | Clear, 52 | Export, 53 | ExportData(ExportData), 54 | } 55 | 56 | impl<'de> Deserialize<'de> for Action { 57 | fn deserialize(deserializer: D) -> Result 58 | where 59 | D: Deserializer<'de>, 60 | { 61 | struct ActionVisitor; 62 | 63 | impl<'de> Visitor<'de> for ActionVisitor { 64 | type Value = Action; 65 | 66 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 67 | formatter.write_str("a valid string representation of Action") 68 | } 69 | 70 | fn visit_str(self, value: &str) -> Result 71 | where 72 | E: de::Error, 73 | { 74 | match value { 75 | // -- custom actions 76 | "InputMode" => Ok(Action::ModeChange(Mode::Input)), 77 | "NormalMode" => Ok(Action::ModeChange(Mode::Normal)), 78 | "Graph" => Ok(Action::GraphToggle), 79 | "Dump" => Ok(Action::DumpToggle), 80 | "Interface" => Ok(Action::InterfaceSwitch), 81 | "Scan" => Ok(Action::ScanCidr), 82 | "Clear" => Ok(Action::Clear), 83 | "Up" => Ok(Action::Up), 84 | "Down" => Ok(Action::Down), 85 | "Left" => Ok(Action::Left), 86 | "Right" => Ok(Action::Right), 87 | "Tab" => Ok(Action::Tab), 88 | "Export" => Ok(Action::Export), 89 | "JumpDiscovery" => Ok(Action::TabChange(TabsEnum::Discovery)), 90 | "JumpPackets" => Ok(Action::TabChange(TabsEnum::Packets)), 91 | "JumpPorts" => Ok(Action::TabChange(TabsEnum::Ports)), 92 | "JumpSniffer" => Ok(Action::TabChange(TabsEnum::Traffic)), 93 | 94 | // -- default actions 95 | "Tick" => Ok(Action::Tick), 96 | "Render" => Ok(Action::Render), 97 | "Suspend" => Ok(Action::Suspend), 98 | "Resume" => Ok(Action::Resume), 99 | "Quit" => Ok(Action::Quit), 100 | "Refresh" => Ok(Action::Refresh), 101 | "Help" => Ok(Action::Help), 102 | data if data.starts_with("Error(") => { 103 | let error_msg = data.trim_start_matches("Error(").trim_end_matches(')'); 104 | Ok(Action::Error(error_msg.to_string())) 105 | } 106 | data if data.starts_with("Resize(") => { 107 | let parts: Vec<&str> = data 108 | .trim_start_matches("Resize(") 109 | .trim_end_matches(')') 110 | .split(',') 111 | .collect(); 112 | if parts.len() == 2 { 113 | let width: u16 = parts[0].trim().parse().map_err(E::custom)?; 114 | let height: u16 = parts[1].trim().parse().map_err(E::custom)?; 115 | Ok(Action::Resize(width, height)) 116 | } else { 117 | Err(E::custom(format!("Invalid Resize format: {}", value))) 118 | } 119 | } 120 | _ => Err(E::custom(format!("Unknown Action variant: {}", value))), 121 | } 122 | } 123 | } 124 | 125 | deserializer.deserialize_str(ActionVisitor) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Local}; 2 | use color_eyre::eyre::Result; 3 | use crossterm::event::KeyEvent; 4 | use ratatui::prelude::Rect; 5 | use serde::{Deserialize, Serialize}; 6 | use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; 7 | 8 | use crate::{ 9 | action::Action, 10 | components::{ 11 | discovery::{self, Discovery, ScannedIp}, 12 | export::Export, 13 | interfaces::Interfaces, 14 | packetdump::PacketDump, 15 | ports::{Ports, ScannedIpPorts}, 16 | tabs::Tabs, 17 | title::Title, 18 | wifi_chart::WifiChart, 19 | wifi_interface::WifiInterface, 20 | wifi_scan::WifiScan, 21 | sniff::Sniffer, 22 | Component, 23 | }, 24 | config::Config, 25 | enums::{ExportData, PacketTypeEnum, PacketsInfoTypesEnum}, 26 | mode::Mode, 27 | tui, 28 | }; 29 | 30 | pub struct App { 31 | pub config: Config, 32 | pub tick_rate: f64, 33 | pub frame_rate: f64, 34 | pub components: Vec>, 35 | pub should_quit: bool, 36 | pub should_suspend: bool, 37 | pub mode: Mode, 38 | pub last_tick_key_events: Vec, 39 | pub action_tx: UnboundedSender, 40 | pub action_rx: UnboundedReceiver, 41 | pub post_exist_msg: Option, 42 | } 43 | 44 | impl App { 45 | pub fn new(tick_rate: f64, frame_rate: f64) -> Result { 46 | let title = Title::new(); 47 | let interfaces = Interfaces::default(); 48 | let wifiscan = WifiScan::default(); 49 | let wifi_interface = WifiInterface::default(); 50 | let wifi_chart = WifiChart::default(); 51 | let tabs = Tabs::default(); 52 | let discovery = Discovery::default(); 53 | let packetdump = PacketDump::default(); 54 | let ports = Ports::default(); 55 | let sniff = Sniffer::default(); 56 | let export = Export::default(); 57 | let config = Config::new()?; 58 | 59 | let mode = Mode::Normal; 60 | let (action_tx, action_rx) = mpsc::unbounded_channel(); 61 | 62 | Ok(Self { 63 | tick_rate: 1.0, 64 | frame_rate: 10.0, 65 | components: vec![ 66 | Box::new(title), 67 | Box::new(interfaces), 68 | Box::new(wifiscan), 69 | Box::new(wifi_interface), 70 | Box::new(wifi_chart), 71 | Box::new(tabs), 72 | Box::new(discovery), 73 | Box::new(packetdump), 74 | Box::new(ports), 75 | Box::new(sniff), 76 | Box::new(export), 77 | ], 78 | should_quit: false, 79 | should_suspend: false, 80 | config, 81 | mode, 82 | last_tick_key_events: Vec::new(), 83 | action_tx, 84 | action_rx, 85 | post_exist_msg: None, 86 | }) 87 | } 88 | 89 | pub async fn run(&mut self) -> Result<()> { 90 | // let (action: action_rx_tx, mut action_rx) = mpsc::unbounded_channel(); 91 | let action_tx = &self.action_tx; 92 | let action_rx = &mut self.action_rx; 93 | 94 | let mut tui = tui::Tui::new()? 95 | .tick_rate(self.tick_rate) 96 | .frame_rate(self.frame_rate); 97 | // tui.mouse(true); 98 | tui.enter()?; 99 | 100 | for component in self.components.iter_mut() { 101 | component.register_action_handler(action_tx.clone())?; 102 | } 103 | 104 | for component in self.components.iter_mut() { 105 | component.register_config_handler(self.config.clone())?; 106 | } 107 | 108 | for component in self.components.iter_mut() { 109 | component.init(tui.size()?)?; 110 | } 111 | 112 | loop { 113 | if let Some(e) = tui.next().await { 114 | match e { 115 | tui::Event::Quit => action_tx.send(Action::Quit)?, 116 | tui::Event::Tick => action_tx.send(Action::Tick)?, 117 | tui::Event::Render => action_tx.send(Action::Render)?, 118 | tui::Event::Resize(x, y) => action_tx.send(Action::Resize(x, y))?, 119 | tui::Event::Key(key) => { 120 | if let Some(keymap) = self.config.keybindings.get(&self.mode) { 121 | if let Some(action) = keymap.get(&vec![key]) { 122 | log::info!("Got action: {action:?}"); 123 | action_tx.send(action.clone())?; 124 | } else { 125 | // If the key was not handled as a single key action, 126 | // then consider it for multi-key combinations. 127 | self.last_tick_key_events.push(key); 128 | 129 | // Check for multi-key combinations 130 | if let Some(action) = keymap.get(&self.last_tick_key_events) { 131 | log::info!("Got action: {action:?}"); 132 | action_tx.send(action.clone())?; 133 | } 134 | } 135 | }; 136 | } 137 | _ => {} 138 | } 139 | for component in self.components.iter_mut() { 140 | if let Some(action) = component.handle_events(Some(e.clone()))? { 141 | action_tx.send(action)?; 142 | } 143 | } 144 | } 145 | 146 | while let Ok(action) = action_rx.try_recv() { 147 | if action != Action::Tick && action != Action::Render { 148 | log::debug!("{action:?}"); 149 | } 150 | match action { 151 | Action::AppModeChange(mode) => { 152 | self.mode = mode; 153 | } 154 | 155 | Action::Error(ref err_msg) => { 156 | self.post_exist_msg = Some(err_msg.to_string()); 157 | self.should_quit = true; 158 | } 159 | 160 | Action::Export => { 161 | // get data from specific components by downcasting them and then try to 162 | // comvert into specific struct 163 | let mut scanned_ips: Vec = Vec::new(); 164 | let mut scanned_ports: Vec = Vec::new(); 165 | let mut arp_packets: Vec<(DateTime, PacketsInfoTypesEnum)> = Vec::new(); 166 | let mut udp_packets = Vec::new(); 167 | let mut tcp_packets = Vec::new(); 168 | let mut icmp_packets = Vec::new(); 169 | let mut icmp6_packets = Vec::new(); 170 | 171 | for component in &self.components { 172 | if let Some(d) = component.as_any().downcast_ref::() { 173 | scanned_ips = d.get_scanned_ips().to_vec(); 174 | } else if let Some(pd) = component.as_any().downcast_ref::() { 175 | arp_packets = pd.clone_array_by_packet_type(PacketTypeEnum::Arp); 176 | udp_packets = pd.clone_array_by_packet_type(PacketTypeEnum::Udp); 177 | tcp_packets = pd.clone_array_by_packet_type(PacketTypeEnum::Tcp); 178 | icmp_packets = pd.clone_array_by_packet_type(PacketTypeEnum::Icmp); 179 | icmp6_packets = pd.clone_array_by_packet_type(PacketTypeEnum::Icmp6); 180 | } else if let Some(p) = component.as_any().downcast_ref::() { 181 | scanned_ports = p.get_scanned_ports().to_vec(); 182 | } 183 | } 184 | action_tx 185 | .send(Action::ExportData(ExportData { 186 | scanned_ips, 187 | scanned_ports, 188 | arp_packets, 189 | udp_packets, 190 | tcp_packets, 191 | icmp_packets, 192 | icmp6_packets, 193 | })) 194 | .unwrap(); 195 | } 196 | 197 | Action::Tick => { 198 | self.last_tick_key_events.drain(..); 199 | } 200 | Action::Quit => self.should_quit = true, 201 | Action::Suspend => self.should_suspend = true, 202 | Action::Resume => self.should_suspend = false, 203 | Action::Resize(w, h) => { 204 | tui.resize(Rect::new(0, 0, w, h))?; 205 | tui.draw(|f| { 206 | for component in self.components.iter_mut() { 207 | let r = component.draw(f, f.area()); 208 | if let Err(e) = r { 209 | action_tx 210 | .send(Action::Error(format!("Failed to draw: {:?}", e))) 211 | .unwrap(); 212 | } 213 | } 214 | })?; 215 | } 216 | Action::Render => { 217 | tui.draw(|f| { 218 | for component in self.components.iter_mut() { 219 | let r = component.draw(f, f.area()); 220 | if let Err(e) = r { 221 | action_tx 222 | .send(Action::Error(format!("Failed to draw: {:?}", e))) 223 | .unwrap(); 224 | } 225 | } 226 | })?; 227 | } 228 | _ => {} 229 | } 230 | for component in self.components.iter_mut() { 231 | if let Some(action) = component.update(action.clone())? { 232 | action_tx.send(action)? 233 | }; 234 | } 235 | } 236 | if self.should_suspend { 237 | tui.suspend()?; 238 | action_tx.send(Action::Resume)?; 239 | tui = tui::Tui::new()? 240 | .tick_rate(self.tick_rate) 241 | .frame_rate(self.frame_rate); 242 | // tui.mouse(true); 243 | tui.enter()?; 244 | } else if self.should_quit { 245 | tui.stop()?; 246 | break; 247 | } 248 | } 249 | tui.exit()?; 250 | 251 | if let Some(ref s) = self.post_exist_msg { 252 | println!("`netscanner` failed with Error:"); 253 | println!("{}", s); 254 | } 255 | 256 | Ok(()) 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::Parser; 4 | 5 | use crate::utils::version; 6 | 7 | #[derive(Parser, Debug)] 8 | #[command(author, version = version(), about)] 9 | pub struct Cli { 10 | #[arg( 11 | short, 12 | long, 13 | value_name = "FLOAT", 14 | help = "Tick rate, i.e. number of ticks per second", 15 | default_value_t = 1.0 16 | )] 17 | pub tick_rate: f64, 18 | 19 | #[arg( 20 | short, 21 | long, 22 | value_name = "FLOAT", 23 | help = "Frame rate, i.e. number of frames per second", 24 | default_value_t = 10.0 25 | )] 26 | pub frame_rate: f64, 27 | } 28 | -------------------------------------------------------------------------------- /src/components.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::eyre::Result; 2 | use crossterm::event::{KeyEvent, MouseEvent}; 3 | use ratatui::layout::{Rect, Size}; 4 | use std::any::Any; 5 | use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; 6 | 7 | use crate::{ 8 | action::Action, 9 | config::Config, 10 | enums::TabsEnum, 11 | tui::{Event, Frame}, 12 | }; 13 | 14 | pub mod discovery; 15 | pub mod export; 16 | pub mod interfaces; 17 | pub mod packetdump; 18 | pub mod ports; 19 | pub mod sniff; 20 | pub mod tabs; 21 | pub mod title; 22 | pub mod wifi_chart; 23 | pub mod wifi_interface; 24 | pub mod wifi_scan; 25 | 26 | /// `Component` is a trait that represents a visual and interactive element of the user interface. 27 | /// Implementors of this trait can be registered with the main application loop and will be able to receive events, 28 | /// update state, and be rendered on the screen. 29 | pub trait Component: Any { 30 | /// Register an action handler that can send actions for processing if necessary. 31 | /// # Arguments 32 | /// * `tx` - An unbounded sender that can send actions. 33 | /// # Returns 34 | /// * `Result<()>` - An Ok result or an error. 35 | #[allow(unused_variables)] 36 | fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { 37 | Ok(()) 38 | } 39 | 40 | #[allow(unused_variables)] 41 | fn as_any(&self) -> &dyn Any; 42 | 43 | #[allow(unused_variables)] 44 | fn tab_changed(&mut self, tab: TabsEnum) -> Result<()> { 45 | Ok(()) 46 | } 47 | 48 | /// Register a configuration handler that provides configuration settings if necessary. 49 | /// # Arguments 50 | /// * `config` - Configuration settings. 51 | /// # Returns 52 | /// * `Result<()>` - An Ok result or an error. 53 | #[allow(unused_variables)] 54 | fn register_config_handler(&mut self, config: Config) -> Result<()> { 55 | Ok(()) 56 | } 57 | 58 | /// Initialize the component with a specified area if necessary. 59 | /// # Arguments 60 | /// * `area` - Rectangular area to initialize the component within. 61 | /// # Returns 62 | /// * `Result<()>` - An Ok result or an error. 63 | fn init(&mut self, _area: Size) -> Result<()> { 64 | Ok(()) 65 | } 66 | 67 | /// Handle incoming events and produce actions if necessary. 68 | /// # Arguments 69 | /// * `event` - An optional event to be processed. 70 | /// # Returns 71 | /// * `Result>` - An action to be processed or none. 72 | fn handle_events(&mut self, event: Option) -> Result> { 73 | let r = match event { 74 | Some(Event::Key(key_event)) => self.handle_key_events(key_event)?, 75 | Some(Event::Mouse(mouse_event)) => self.handle_mouse_events(mouse_event)?, 76 | _ => None, 77 | }; 78 | Ok(r) 79 | } 80 | 81 | /// Handle key events and produce actions if necessary. 82 | /// # Arguments 83 | /// * `key` - A key event to be processed. 84 | /// # Returns 85 | /// * `Result>` - An action to be processed or none. 86 | #[allow(unused_variables)] 87 | fn handle_key_events(&mut self, key: KeyEvent) -> Result> { 88 | Ok(None) 89 | } 90 | 91 | /// Handle mouse events and produce actions if necessary. 92 | /// # Arguments 93 | /// * `mouse` - A mouse event to be processed. 94 | /// # Returns 95 | /// * `Result>` - An action to be processed or none. 96 | #[allow(unused_variables)] 97 | fn handle_mouse_events(&mut self, mouse: MouseEvent) -> Result> { 98 | Ok(None) 99 | } 100 | 101 | /// Update the state of the component based on a received action. (REQUIRED) 102 | /// # Arguments 103 | /// * `action` - An action that may modify the state of the component. 104 | /// # Returns 105 | /// * `Result>` - An action to be processed or none. 106 | #[allow(unused_variables)] 107 | fn update(&mut self, action: Action) -> Result> { 108 | Ok(None) 109 | } 110 | 111 | /// Render the component on the screen. (REQUIRED) 112 | /// # Arguments 113 | /// * `f` - A frame used for rendering. 114 | /// * `area` - The area in which the component should be drawn. 115 | /// # Returns 116 | /// * `Result<()>` - An Ok result or an error. 117 | fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()>; 118 | } 119 | -------------------------------------------------------------------------------- /src/components/discovery.rs: -------------------------------------------------------------------------------- 1 | use cidr::Ipv4Cidr; 2 | use color_eyre::eyre::Result; 3 | use color_eyre::owo_colors::OwoColorize; 4 | use dns_lookup::{lookup_addr, lookup_host}; 5 | use futures::future::join_all; 6 | 7 | use pnet::datalink::{Channel, NetworkInterface}; 8 | use pnet::packet::{ 9 | arp::{ArpHardwareTypes, ArpOperations, ArpPacket, MutableArpPacket}, 10 | ethernet::{EtherTypes, MutableEthernetPacket}, 11 | MutablePacket, Packet, 12 | }; 13 | use pnet::util::MacAddr; 14 | use tokio::sync::Semaphore; 15 | 16 | use core::str; 17 | use ratatui::layout::Position; 18 | use ratatui::{prelude::*, widgets::*}; 19 | use std::net::{IpAddr, Ipv4Addr}; 20 | use std::string; 21 | use std::sync::Arc; 22 | use std::time::{Duration, Instant}; 23 | use surge_ping::{Client, Config, IcmpPacket, PingIdentifier, PingSequence, ICMP}; 24 | use tokio::{ 25 | sync::mpsc::{self, UnboundedSender}, 26 | task::{self, JoinHandle}, 27 | }; 28 | 29 | use super::Component; 30 | use crate::{ 31 | action::Action, 32 | components::packetdump::ArpPacketData, 33 | config::DEFAULT_BORDER_STYLE, 34 | enums::TabsEnum, 35 | layout::get_vertical_layout, 36 | mode::Mode, 37 | tui::Frame, 38 | utils::{count_ipv4_net_length, get_ips4_from_cidr}, 39 | }; 40 | use crossterm::event::Event; 41 | use crossterm::event::{KeyCode, KeyEvent}; 42 | use mac_oui::Oui; 43 | use rand::random; 44 | use tui_input::backend::crossterm::EventHandler; 45 | use tui_input::Input; 46 | 47 | static POOL_SIZE: usize = 32; 48 | static INPUT_SIZE: usize = 30; 49 | static DEFAULT_IP: &str = "192.168.1.0/24"; 50 | const SPINNER_SYMBOLS: [&str; 6] = ["⠷", "⠯", "⠟", "⠻", "⠽", "⠾"]; 51 | 52 | #[derive(Clone, Debug, PartialEq)] 53 | pub struct ScannedIp { 54 | pub ip: String, 55 | pub mac: String, 56 | pub hostname: String, 57 | pub vendor: String, 58 | } 59 | 60 | pub struct Discovery { 61 | active_tab: TabsEnum, 62 | active_interface: Option, 63 | action_tx: Option>, 64 | scanned_ips: Vec, 65 | ip_num: i32, 66 | input: Input, 67 | cidr: Option, 68 | cidr_error: bool, 69 | is_scanning: bool, 70 | mode: Mode, 71 | task: JoinHandle<()>, 72 | oui: Option, 73 | table_state: TableState, 74 | scrollbar_state: ScrollbarState, 75 | spinner_index: usize, 76 | } 77 | 78 | impl Default for Discovery { 79 | fn default() -> Self { 80 | Self::new() 81 | } 82 | } 83 | 84 | impl Discovery { 85 | pub fn new() -> Self { 86 | Self { 87 | active_tab: TabsEnum::Discovery, 88 | active_interface: None, 89 | task: tokio::spawn(async {}), 90 | action_tx: None, 91 | scanned_ips: Vec::new(), 92 | ip_num: 0, 93 | input: Input::default().with_value(String::from(DEFAULT_IP)), 94 | cidr: None, 95 | cidr_error: false, 96 | is_scanning: false, 97 | mode: Mode::Normal, 98 | oui: None, 99 | table_state: TableState::default().with_selected(0), 100 | scrollbar_state: ScrollbarState::new(0), 101 | spinner_index: 0, 102 | } 103 | } 104 | 105 | pub fn get_scanned_ips(&self) -> &Vec { 106 | &self.scanned_ips 107 | } 108 | 109 | fn set_cidr(&mut self, cidr_str: String, scan: bool) { 110 | match cidr_str.parse::() { 111 | Ok(ip_cidr) => { 112 | self.cidr = Some(ip_cidr); 113 | if scan { 114 | self.scan(); 115 | } 116 | } 117 | Err(e) => { 118 | if let Some(tx) = &self.action_tx { 119 | tx.clone().send(Action::CidrError).unwrap(); 120 | } 121 | } 122 | } 123 | } 124 | 125 | fn reset_scan(&mut self) { 126 | self.scanned_ips.clear(); 127 | self.ip_num = 0; 128 | } 129 | 130 | fn send_arp(&mut self, target_ip: Ipv4Addr) { 131 | if let Some(active_interface) = &self.active_interface { 132 | if let Some(active_interface_mac) = active_interface.mac { 133 | let ipv4 = active_interface.ips.iter().find(|f| f.is_ipv4()).unwrap(); 134 | let source_ip: Ipv4Addr = ipv4.ip().to_string().parse().unwrap(); 135 | 136 | let (mut sender, _) = 137 | match pnet::datalink::channel(active_interface, Default::default()) { 138 | Ok(Channel::Ethernet(tx, rx)) => (tx, rx), 139 | Ok(_) => { 140 | if let Some(tx_action) = &self.action_tx { 141 | tx_action 142 | .clone() 143 | .send(Action::Error( 144 | "Unknown or unsupported channel type".into(), 145 | )) 146 | .unwrap(); 147 | } 148 | return; 149 | } 150 | Err(e) => { 151 | if let Some(tx_action) = &self.action_tx { 152 | tx_action 153 | .clone() 154 | .send(Action::Error(format!( 155 | "Unable to create datalink channel: {e}" 156 | ))) 157 | .unwrap(); 158 | } 159 | return; 160 | } 161 | }; 162 | 163 | let mut ethernet_buffer = [0u8; 42]; 164 | let mut ethernet_packet = MutableEthernetPacket::new(&mut ethernet_buffer).unwrap(); 165 | 166 | ethernet_packet.set_destination(MacAddr::broadcast()); 167 | ethernet_packet.set_source(active_interface_mac); 168 | ethernet_packet.set_ethertype(EtherTypes::Arp); 169 | 170 | let mut arp_buffer = [0u8; 28]; 171 | let mut arp_packet = MutableArpPacket::new(&mut arp_buffer).unwrap(); 172 | 173 | arp_packet.set_hardware_type(ArpHardwareTypes::Ethernet); 174 | arp_packet.set_protocol_type(EtherTypes::Ipv4); 175 | arp_packet.set_hw_addr_len(6); 176 | arp_packet.set_proto_addr_len(4); 177 | arp_packet.set_operation(ArpOperations::Request); 178 | arp_packet.set_sender_hw_addr(active_interface_mac); 179 | arp_packet.set_sender_proto_addr(source_ip); 180 | arp_packet.set_target_hw_addr(MacAddr::zero()); 181 | arp_packet.set_target_proto_addr(target_ip); 182 | 183 | ethernet_packet.set_payload(arp_packet.packet_mut()); 184 | 185 | sender 186 | .send_to(ethernet_packet.packet(), None) 187 | .unwrap() 188 | .unwrap(); 189 | } 190 | } 191 | } 192 | 193 | // fn scan(&mut self) { 194 | // self.reset_scan(); 195 | 196 | // if let Some(cidr) = self.cidr { 197 | // self.is_scanning = true; 198 | // let tx = self.action_tx.as_ref().unwrap().clone(); 199 | // self.task = tokio::spawn(async move { 200 | // let ips = get_ips4_from_cidr(cidr); 201 | // let chunks: Vec<_> = ips.chunks(POOL_SIZE).collect(); 202 | // for chunk in chunks { 203 | // let tasks: Vec<_> = chunk 204 | // .iter() 205 | // .map(|&ip| { 206 | // let tx = tx.clone(); 207 | // let closure = || async move { 208 | // let client = 209 | // Client::new(&Config::default()).expect("Cannot create client"); 210 | // let payload = [0; 56]; 211 | // let mut pinger = client 212 | // .pinger(IpAddr::V4(ip), PingIdentifier(random())) 213 | // .await; 214 | // pinger.timeout(Duration::from_secs(2)); 215 | 216 | // match pinger.ping(PingSequence(2), &payload).await { 217 | // Ok((IcmpPacket::V4(packet), dur)) => { 218 | // tx.send(Action::PingIp(packet.get_real_dest().to_string())) 219 | // .unwrap_or_default(); 220 | // tx.send(Action::CountIp).unwrap_or_default(); 221 | // } 222 | // Ok(_) => { 223 | // tx.send(Action::CountIp).unwrap_or_default(); 224 | // } 225 | // Err(_) => { 226 | // tx.send(Action::CountIp).unwrap_or_default(); 227 | // } 228 | // } 229 | // }; 230 | // task::spawn(closure()) 231 | // }) 232 | // .collect(); 233 | 234 | // let _ = join_all(tasks).await; 235 | // } 236 | // }); 237 | // }; 238 | // } 239 | 240 | fn scan(&mut self) { 241 | self.reset_scan(); 242 | 243 | if let Some(cidr) = self.cidr { 244 | self.is_scanning = true; 245 | 246 | let tx = self.action_tx.clone().unwrap(); 247 | let semaphore = Arc::new(Semaphore::new(POOL_SIZE)); 248 | 249 | self.task = tokio::spawn(async move { 250 | let ips = get_ips4_from_cidr(cidr); 251 | let tasks: Vec<_> = ips 252 | .iter() 253 | .map(|&ip| { 254 | let s = semaphore.clone(); 255 | let tx = tx.clone(); 256 | let c = || async move { 257 | let _permit = s.acquire().await.unwrap(); 258 | let client = 259 | Client::new(&Config::default()).expect("Cannot create client"); 260 | let payload = [0; 56]; 261 | let mut pinger = client 262 | .pinger(IpAddr::V4(ip), PingIdentifier(random())) 263 | .await; 264 | pinger.timeout(Duration::from_secs(2)); 265 | 266 | match pinger.ping(PingSequence(2), &payload).await { 267 | Ok((IcmpPacket::V4(packet), dur)) => { 268 | tx.send(Action::PingIp(packet.get_real_dest().to_string())) 269 | .unwrap_or_default(); 270 | tx.send(Action::CountIp).unwrap_or_default(); 271 | } 272 | Ok(_) => { 273 | tx.send(Action::CountIp).unwrap_or_default(); 274 | } 275 | Err(_) => { 276 | tx.send(Action::CountIp).unwrap_or_default(); 277 | } 278 | } 279 | }; 280 | tokio::spawn(c()) 281 | }) 282 | .collect(); 283 | for t in tasks { 284 | let _ = t.await; 285 | } 286 | // let _ = join_all(tasks).await; 287 | }); 288 | }; 289 | } 290 | 291 | fn process_mac(&mut self, arp_data: ArpPacketData) { 292 | if let Some(n) = self 293 | .scanned_ips 294 | .iter_mut() 295 | .find(|item| item.ip == arp_data.sender_ip.to_string()) 296 | { 297 | n.mac = arp_data.sender_mac.to_string(); 298 | 299 | if let Some(oui) = &self.oui { 300 | let oui_res = oui.lookup_by_mac(&n.mac); 301 | if let Ok(Some(oui_res)) = oui_res { 302 | let cn = oui_res.company_name.clone(); 303 | n.vendor = cn; 304 | } 305 | } 306 | } 307 | } 308 | 309 | fn process_ip(&mut self, ip: &str) { 310 | let tx = self.action_tx.as_ref().unwrap(); 311 | let ipv4: Ipv4Addr = ip.parse().unwrap(); 312 | // self.send_arp(ipv4); 313 | 314 | if let Some(n) = self.scanned_ips.iter_mut().find(|item| item.ip == ip) { 315 | let hip: IpAddr = ip.parse().unwrap(); 316 | let host = lookup_addr(&hip).unwrap_or_default(); 317 | n.hostname = host; 318 | n.ip = ip.to_string(); 319 | } else { 320 | let hip: IpAddr = ip.parse().unwrap(); 321 | let host = lookup_addr(&hip).unwrap_or_default(); 322 | self.scanned_ips.push(ScannedIp { 323 | ip: ip.to_string(), 324 | mac: String::new(), 325 | hostname: host, 326 | vendor: String::new(), 327 | }); 328 | 329 | self.scanned_ips.sort_by(|a, b| { 330 | let a_ip: Ipv4Addr = a.ip.parse::().unwrap(); 331 | let b_ip: Ipv4Addr = b.ip.parse::().unwrap(); 332 | a_ip.partial_cmp(&b_ip).unwrap() 333 | }); 334 | } 335 | 336 | self.set_scrollbar_height(); 337 | } 338 | 339 | fn set_active_subnet(&mut self, intf: &NetworkInterface) { 340 | let a_ip = intf.ips[0].ip().to_string(); 341 | let ip: Vec<&str> = a_ip.split('.').collect(); 342 | if ip.len() > 1 { 343 | let new_a_ip = format!("{}.{}.{}.0/24", ip[0], ip[1], ip[2]); 344 | self.input = Input::default().with_value(new_a_ip); 345 | 346 | self.set_cidr(self.input.value().to_string(), false); 347 | } 348 | } 349 | 350 | fn set_scrollbar_height(&mut self) { 351 | let mut ip_len = 0; 352 | if !self.scanned_ips.is_empty() { 353 | ip_len = self.scanned_ips.len() - 1; 354 | } 355 | self.scrollbar_state = self.scrollbar_state.content_length(ip_len); 356 | } 357 | 358 | fn previous_in_table(&mut self) { 359 | let index = match self.table_state.selected() { 360 | Some(index) => { 361 | if index == 0 { 362 | if self.scanned_ips.is_empty() { 363 | 0 364 | } else { 365 | self.scanned_ips.len() - 1 366 | } 367 | } else { 368 | index - 1 369 | } 370 | } 371 | None => 0, 372 | }; 373 | self.table_state.select(Some(index)); 374 | self.scrollbar_state = self.scrollbar_state.position(index); 375 | } 376 | 377 | fn next_in_table(&mut self) { 378 | let index = match self.table_state.selected() { 379 | Some(index) => { 380 | let mut s_ip_len = 0; 381 | if !self.scanned_ips.is_empty() { 382 | s_ip_len = self.scanned_ips.len() - 1; 383 | } 384 | if index >= s_ip_len { 385 | 0 386 | } else { 387 | index + 1 388 | } 389 | } 390 | None => 0, 391 | }; 392 | self.table_state.select(Some(index)); 393 | self.scrollbar_state = self.scrollbar_state.position(index); 394 | } 395 | 396 | fn make_table( 397 | scanned_ips: &Vec, 398 | cidr: Option, 399 | ip_num: i32, 400 | is_scanning: bool, 401 | ) -> Table { 402 | let header = Row::new(vec!["ip", "mac", "hostname", "vendor"]) 403 | .style(Style::default().fg(Color::Yellow)) 404 | .top_margin(1) 405 | .bottom_margin(1); 406 | let mut rows = Vec::new(); 407 | let cidr_length = match cidr { 408 | Some(c) => count_ipv4_net_length(c.network_length() as u32), 409 | None => 0, 410 | }; 411 | 412 | for sip in scanned_ips { 413 | let ip = &sip.ip; 414 | rows.push(Row::new(vec![ 415 | Cell::from(Span::styled( 416 | format!("{ip:<2}"), 417 | Style::default().fg(Color::Blue), 418 | )), 419 | Cell::from(sip.mac.as_str().green()), 420 | Cell::from(sip.hostname.as_str()), 421 | Cell::from(sip.vendor.as_str().yellow()), 422 | ])); 423 | } 424 | 425 | let mut scan_title = vec![ 426 | Span::styled("|", Style::default().fg(Color::Yellow)), 427 | "◉ ".green(), 428 | Span::styled( 429 | format!("{}", scanned_ips.len()), 430 | Style::default().fg(Color::Red), 431 | ), 432 | Span::styled("|", Style::default().fg(Color::Yellow)), 433 | ]; 434 | if is_scanning { 435 | scan_title.push(" ⣿(".yellow()); 436 | scan_title.push(format!("{}", ip_num).red()); 437 | scan_title.push(format!("/{}", cidr_length).green()); 438 | scan_title.push(")".yellow()); 439 | } 440 | 441 | let table = Table::new( 442 | rows, 443 | [ 444 | Constraint::Length(16), 445 | Constraint::Length(19), 446 | Constraint::Fill(1), 447 | Constraint::Fill(1), 448 | ], 449 | ) 450 | .header(header) 451 | .block( 452 | Block::new() 453 | .title( 454 | ratatui::widgets::block::Title::from("|Discovery|".yellow()) 455 | .position(ratatui::widgets::block::Position::Top) 456 | .alignment(Alignment::Right), 457 | ) 458 | .title( 459 | ratatui::widgets::block::Title::from(Line::from(vec![ 460 | Span::styled("|", Style::default().fg(Color::Yellow)), 461 | Span::styled( 462 | "e", 463 | Style::default().add_modifier(Modifier::BOLD).fg(Color::Red), 464 | ), 465 | Span::styled("xport data", Style::default().fg(Color::Yellow)), 466 | Span::styled("|", Style::default().fg(Color::Yellow)), 467 | ])) 468 | .alignment(Alignment::Left) 469 | .position(ratatui::widgets::block::Position::Bottom), 470 | ) 471 | .title( 472 | ratatui::widgets::block::Title::from(Line::from(scan_title)) 473 | .position(ratatui::widgets::block::Position::Top) 474 | .alignment(Alignment::Left), 475 | ) 476 | .title( 477 | ratatui::widgets::block::Title::from(Line::from(vec![ 478 | Span::styled("|", Style::default().fg(Color::Yellow)), 479 | String::from(char::from_u32(0x25b2).unwrap_or('>')).red(), 480 | String::from(char::from_u32(0x25bc).unwrap_or('>')).red(), 481 | Span::styled("select|", Style::default().fg(Color::Yellow)), 482 | ])) 483 | .position(ratatui::widgets::block::Position::Bottom) 484 | .alignment(Alignment::Right), 485 | ) 486 | .border_style(Style::default().fg(Color::Rgb(100, 100, 100))) 487 | .borders(Borders::ALL) 488 | .border_type(DEFAULT_BORDER_STYLE), 489 | ) 490 | .highlight_symbol(String::from(char::from_u32(0x25b6).unwrap_or('>')).red()) 491 | .column_spacing(1); 492 | table 493 | } 494 | 495 | pub fn make_scrollbar<'a>() -> Scrollbar<'a> { 496 | let scrollbar = Scrollbar::default() 497 | .orientation(ScrollbarOrientation::VerticalRight) 498 | .style(Style::default().fg(Color::Rgb(100, 100, 100))) 499 | .begin_symbol(None) 500 | .end_symbol(None); 501 | scrollbar 502 | } 503 | 504 | fn make_input(&self, scroll: usize) -> Paragraph { 505 | let input = Paragraph::new(self.input.value()) 506 | .style(Style::default().fg(Color::Green)) 507 | .scroll((0, scroll as u16)) 508 | .block( 509 | Block::default() 510 | .borders(Borders::ALL) 511 | .border_style(match self.mode { 512 | Mode::Input => Style::default().fg(Color::Green), 513 | Mode::Normal => Style::default().fg(Color::Rgb(100, 100, 100)), 514 | }) 515 | .border_type(DEFAULT_BORDER_STYLE) 516 | .title( 517 | ratatui::widgets::block::Title::from(Line::from(vec![ 518 | Span::raw("|"), 519 | Span::styled( 520 | "i", 521 | Style::default().add_modifier(Modifier::BOLD).fg(Color::Red), 522 | ), 523 | Span::styled("nput", Style::default().fg(Color::Yellow)), 524 | Span::raw("/"), 525 | Span::styled( 526 | "ESC", 527 | Style::default().add_modifier(Modifier::BOLD).fg(Color::Red), 528 | ), 529 | Span::raw("|"), 530 | ])) 531 | .alignment(Alignment::Right) 532 | .position(ratatui::widgets::block::Position::Bottom), 533 | ) 534 | .title( 535 | ratatui::widgets::block::Title::from(Line::from(vec![ 536 | Span::raw("|"), 537 | Span::styled( 538 | "s", 539 | Style::default().add_modifier(Modifier::BOLD).fg(Color::Red), 540 | ), 541 | Span::styled("can", Style::default().fg(Color::Yellow)), 542 | Span::raw("|"), 543 | ])) 544 | .alignment(Alignment::Left) 545 | .position(ratatui::widgets::block::Position::Bottom), 546 | ), 547 | ); 548 | input 549 | } 550 | 551 | fn make_error(&mut self) -> Paragraph { 552 | let error = Paragraph::new("CIDR parse error") 553 | .style(Style::default().fg(Color::Red)) 554 | .block( 555 | Block::default() 556 | .borders(Borders::ALL) 557 | .border_type(BorderType::Double) 558 | .border_style(Style::default().fg(Color::Red)), 559 | ); 560 | error 561 | } 562 | 563 | fn make_spinner(&self) -> Span { 564 | let spinner = SPINNER_SYMBOLS[self.spinner_index]; 565 | Span::styled( 566 | format!("{spinner}scanning.."), 567 | Style::default().fg(Color::Yellow), 568 | ) 569 | } 570 | } 571 | 572 | impl Component for Discovery { 573 | fn init(&mut self, area: Size) -> Result<()> { 574 | if self.cidr.is_none() { 575 | self.set_cidr(String::from(DEFAULT_IP), false); 576 | } 577 | // -- init oui 578 | match Oui::default() { 579 | Ok(s) => self.oui = Some(s), 580 | Err(_) => self.oui = None, 581 | } 582 | Ok(()) 583 | } 584 | 585 | fn as_any(&self) -> &dyn std::any::Any { 586 | self 587 | } 588 | 589 | fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { 590 | self.action_tx = Some(tx); 591 | Ok(()) 592 | } 593 | 594 | fn handle_key_events(&mut self, key: KeyEvent) -> Result> { 595 | if self.active_tab == TabsEnum::Discovery { 596 | let action = match self.mode { 597 | Mode::Normal => return Ok(None), 598 | Mode::Input => match key.code { 599 | KeyCode::Enter => { 600 | if let Some(sender) = &self.action_tx { 601 | self.set_cidr(self.input.value().to_string(), true); 602 | } 603 | Action::ModeChange(Mode::Normal) 604 | } 605 | _ => { 606 | self.input.handle_event(&Event::Key(key)); 607 | return Ok(None); 608 | } 609 | }, 610 | }; 611 | Ok(Some(action)) 612 | } else { 613 | Ok(None) 614 | } 615 | } 616 | 617 | fn update(&mut self, action: Action) -> Result> { 618 | if self.is_scanning { 619 | if let Action::Tick = action { 620 | let mut s_index = self.spinner_index + 1; 621 | s_index %= SPINNER_SYMBOLS.len() - 1; 622 | self.spinner_index = s_index; 623 | } 624 | } 625 | 626 | // -- custom actions 627 | if let Action::PingIp(ref ip) = action { 628 | self.process_ip(ip); 629 | } 630 | // -- count IPs 631 | if let Action::CountIp = action { 632 | self.ip_num += 1; 633 | 634 | let ip_count = match self.cidr { 635 | Some(cidr) => count_ipv4_net_length(cidr.network_length() as u32) as i32, 636 | None => 0, 637 | }; 638 | 639 | if self.ip_num == ip_count { 640 | self.is_scanning = false; 641 | } 642 | } 643 | // -- CIDR error 644 | if let Action::CidrError = action { 645 | self.cidr_error = true; 646 | } 647 | // -- ARP packet recieved 648 | if let Action::ArpRecieve(ref arp_data) = action { 649 | self.process_mac(arp_data.clone()); 650 | } 651 | // -- Scan CIDR 652 | if let Action::ScanCidr = action { 653 | if self.active_interface.is_some() 654 | && !self.is_scanning 655 | && self.active_tab == TabsEnum::Discovery 656 | { 657 | self.scan(); 658 | } 659 | } 660 | // -- active interface 661 | if let Action::ActiveInterface(ref interface) = action { 662 | let intf = interface.clone(); 663 | // -- first time scan after setting of interface 664 | if self.active_interface.is_none() { 665 | self.set_active_subnet(&intf); 666 | } 667 | self.active_interface = Some(intf); 668 | } 669 | 670 | if self.active_tab == TabsEnum::Discovery { 671 | // -- prev & next select item in table 672 | if let Action::Down = action { 673 | self.next_in_table(); 674 | } 675 | if let Action::Up = action { 676 | self.previous_in_table(); 677 | } 678 | 679 | // -- MODE CHANGE 680 | if let Action::ModeChange(mode) = action { 681 | // -- when scanning don't switch to input mode 682 | if self.is_scanning && mode == Mode::Input { 683 | self.action_tx 684 | .clone() 685 | .unwrap() 686 | .send(Action::ModeChange(Mode::Normal)) 687 | .unwrap(); 688 | return Ok(None); 689 | } 690 | 691 | if mode == Mode::Input { 692 | // self.input.reset(); 693 | self.cidr_error = false; 694 | } 695 | self.action_tx 696 | .clone() 697 | .unwrap() 698 | .send(Action::AppModeChange(mode)) 699 | .unwrap(); 700 | self.mode = mode; 701 | } 702 | } 703 | 704 | // -- tab change 705 | if let Action::TabChange(tab) = action { 706 | self.tab_changed(tab).unwrap(); 707 | } 708 | 709 | Ok(None) 710 | } 711 | 712 | fn tab_changed(&mut self, tab: TabsEnum) -> Result<()> { 713 | self.active_tab = tab; 714 | Ok(()) 715 | } 716 | 717 | fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> { 718 | if self.active_tab == TabsEnum::Discovery { 719 | let layout = get_vertical_layout(area); 720 | 721 | // -- TABLE 722 | let mut table_rect = layout.bottom; 723 | table_rect.y += 1; 724 | table_rect.height -= 1; 725 | 726 | let table = 727 | Self::make_table(&self.scanned_ips, self.cidr, self.ip_num, self.is_scanning); 728 | f.render_stateful_widget(table, table_rect, &mut self.table_state); 729 | 730 | // -- SCROLLBAR 731 | let scrollbar = Self::make_scrollbar(); 732 | let mut scroll_rect = table_rect; 733 | scroll_rect.y += 3; 734 | scroll_rect.height -= 3; 735 | f.render_stateful_widget( 736 | scrollbar, 737 | scroll_rect.inner(Margin { 738 | vertical: 1, 739 | horizontal: 1, 740 | }), 741 | &mut self.scrollbar_state, 742 | ); 743 | 744 | // -- ERROR 745 | if self.cidr_error { 746 | let error_rect = Rect::new(table_rect.width - (19 + 41), table_rect.y + 1, 18, 3); 747 | let block = self.make_error(); 748 | f.render_widget(block, error_rect); 749 | } 750 | 751 | // -- INPUT 752 | let input_size: u16 = INPUT_SIZE as u16; 753 | let input_rect = Rect::new( 754 | table_rect.width - (input_size + 1), 755 | table_rect.y + 1, 756 | input_size, 757 | 3, 758 | ); 759 | 760 | // -- INPUT_SIZE - 3 is offset for border + 1char for cursor 761 | let scroll = self.input.visual_scroll(INPUT_SIZE - 3); 762 | let mut block = self.make_input(scroll); 763 | if self.is_scanning { 764 | block = block.add_modifier(Modifier::DIM); 765 | } 766 | f.render_widget(block, input_rect); 767 | 768 | // -- cursor 769 | match self.mode { 770 | Mode::Input => { 771 | f.set_cursor_position(Position { 772 | x: input_rect.x 773 | + ((self.input.visual_cursor()).max(scroll) - scroll) as u16 774 | + 1, 775 | y: input_rect.y + 1, 776 | }); 777 | } 778 | Mode::Normal => {} 779 | } 780 | 781 | // -- THROBBER 782 | if self.is_scanning { 783 | let throbber = self.make_spinner(); 784 | let throbber_rect = Rect::new(input_rect.x + 1, input_rect.y, 12, 1); 785 | f.render_widget(throbber, throbber_rect); 786 | } 787 | } 788 | 789 | Ok(()) 790 | } 791 | } 792 | -------------------------------------------------------------------------------- /src/components/export.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Local}; 2 | use color_eyre::{eyre::Result, owo_colors::OwoColorize}; 3 | use crossterm::style::Stylize; 4 | use csv::Writer; 5 | use ratatui::{prelude::*, widgets::*}; 6 | use std::env; 7 | use tokio::sync::mpsc::UnboundedSender; 8 | 9 | use super::{discovery::ScannedIp, ports::ScannedIpPorts, Component, Frame}; 10 | use crate::{action::Action, enums::PacketsInfoTypesEnum}; 11 | 12 | #[derive(Default)] 13 | pub struct Export { 14 | action_tx: Option>, 15 | home_dir: String, 16 | export_done: bool, 17 | export_failed: bool, 18 | } 19 | 20 | impl Export { 21 | pub fn new() -> Self { 22 | Self { 23 | action_tx: None, 24 | home_dir: String::new(), 25 | export_done: false, 26 | export_failed: false, 27 | } 28 | } 29 | 30 | #[cfg(target_os = "linux")] 31 | fn get_user_home_dir(&mut self) { 32 | let mut home_dir = String::from("/root"); 33 | if let Some(h_dir) = env::var_os("HOME") { 34 | home_dir = String::from(h_dir.to_str().unwrap()); 35 | } 36 | if let Some(sudo_user) = env::var_os("SUDO_USER") { 37 | home_dir = format!("/home/{}", sudo_user.to_str().unwrap()); 38 | } 39 | self.home_dir = format!("{}/.netscanner", home_dir); 40 | 41 | // -- create dot folder 42 | if std::fs::metadata(self.home_dir.clone()).is_err() 43 | && std::fs::create_dir_all(self.home_dir.clone()).is_err() 44 | { 45 | self.export_failed = true; 46 | } 47 | } 48 | 49 | #[cfg(target_os = "macos")] 50 | fn get_user_home_dir(&mut self) { 51 | let mut home_dir = String::from("/root"); 52 | if let Some(h_dir) = env::var_os("HOME") { 53 | home_dir = String::from(h_dir.to_str().unwrap()); 54 | } 55 | if let Some(sudo_user) = env::var_os("SUDO_USER") { 56 | home_dir = format!("/Users/{}", sudo_user.to_str().unwrap()); 57 | } 58 | self.home_dir = format!("{}/.netscanner", home_dir); 59 | 60 | // -- create dot folder 61 | if std::fs::metadata(self.home_dir.clone()).is_err() { 62 | if std::fs::create_dir_all(self.home_dir.clone()).is_err() { 63 | println!("Failed to create export dir"); 64 | } 65 | } 66 | } 67 | 68 | #[cfg(target_os = "windows")] 69 | fn get_user_home_dir(&mut self) { 70 | let mut home_dir = String::from("C:\\Users\\Administrator"); 71 | if let Some(h_dir) = env::var_os("USERPROFILE") { 72 | home_dir = String::from(h_dir.to_str().unwrap()); 73 | } 74 | if let Some(sudo_user) = env::var_os("SUDO_USER") { 75 | home_dir = format!("C:\\Users\\{}", sudo_user.to_str().unwrap()); 76 | } 77 | self.home_dir = format!("{}\\.netscanner", home_dir); 78 | 79 | // -- create .netscanner folder if it doesn't exist 80 | if std::fs::metadata(self.home_dir.clone()).is_err() { 81 | if std::fs::create_dir_all(self.home_dir.clone()).is_err() { 82 | self.export_failed = true; 83 | } 84 | } 85 | } 86 | 87 | 88 | pub fn write_discovery(&mut self, data: Vec, timestamp: &String) -> Result<()> { 89 | let mut w = Writer::from_path(format!("{}/scanned_ips.{}.csv", self.home_dir, timestamp))?; 90 | 91 | // -- header 92 | w.write_record(["ip", "mac", "hostname", "vendor"])?; 93 | for s_ip in data { 94 | w.write_record([s_ip.ip, s_ip.mac, s_ip.hostname, s_ip.vendor])?; 95 | } 96 | w.flush()?; 97 | 98 | Ok(()) 99 | } 100 | 101 | pub fn write_ports(&mut self, data: Vec, timestamp: &String) -> Result<()> { 102 | let mut w = 103 | Writer::from_path(format!("{}/scanned_ports.{}.csv", self.home_dir, timestamp))?; 104 | 105 | // -- header 106 | w.write_record(["ip", "ports"])?; 107 | for s_ip in data { 108 | let ports: String = s_ip 109 | .ports 110 | .iter() 111 | .map(|n| n.to_string()) 112 | .collect::>() 113 | .join(":"); 114 | w.write_record([s_ip.ip, ports])?; 115 | } 116 | w.flush()?; 117 | 118 | Ok(()) 119 | } 120 | 121 | pub fn write_packets( 122 | &mut self, 123 | data: Vec<(DateTime, PacketsInfoTypesEnum)>, 124 | timestamp: &String, 125 | name: &str, 126 | ) -> Result<()> { 127 | let mut w = Writer::from_path(format!( 128 | "{}/{}_packets.{}.csv", 129 | self.home_dir, name, timestamp 130 | ))?; 131 | 132 | // -- header 133 | w.write_record(["time", "log"])?; 134 | for (t, p) in data { 135 | let log_str = match p { 136 | PacketsInfoTypesEnum::Icmp(log) => log.raw_str, 137 | PacketsInfoTypesEnum::Arp(log) => log.raw_str, 138 | PacketsInfoTypesEnum::Icmp6(log) => log.raw_str, 139 | PacketsInfoTypesEnum::Udp(log) => log.raw_str, 140 | PacketsInfoTypesEnum::Tcp(log) => log.raw_str, 141 | }; 142 | w.write_record([t.to_string(), log_str])?; 143 | } 144 | w.flush()?; 145 | 146 | Ok(()) 147 | } 148 | } 149 | 150 | impl Component for Export { 151 | fn init(&mut self, area: Size) -> Result<()> { 152 | self.get_user_home_dir(); 153 | Ok(()) 154 | } 155 | 156 | fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { 157 | self.action_tx = Some(tx); 158 | Ok(()) 159 | } 160 | 161 | fn as_any(&self) -> &dyn std::any::Any { 162 | self 163 | } 164 | 165 | fn update(&mut self, action: Action) -> Result> { 166 | match action { 167 | Action::Export => {} 168 | Action::ExportData(data) => { 169 | let now = Local::now(); 170 | // let now_str = now.format("%Y-%m-%d-%H-%M-%S").to_string(); 171 | let now_str = now.timestamp().to_string(); 172 | let _ = self.write_discovery(data.scanned_ips, &now_str); 173 | let _ = self.write_ports(data.scanned_ports, &now_str); 174 | let _ = self.write_packets(data.arp_packets, &now_str, "arp"); 175 | let _ = self.write_packets(data.tcp_packets, &now_str, "tcp"); 176 | let _ = self.write_packets(data.udp_packets, &now_str, "udp"); 177 | let _ = self.write_packets(data.icmp_packets, &now_str, "icmp"); 178 | let _ = self.write_packets(data.icmp6_packets, &now_str, "icmp6"); 179 | 180 | self.export_done = true; 181 | } 182 | _ => {} 183 | } 184 | Ok(None) 185 | } 186 | 187 | fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> { 188 | if self.export_done { 189 | let l_area = Rect { 190 | x: 15, 191 | y: area.height - 1, 192 | width: area.width - 15, 193 | height: 1, 194 | }; 195 | let line = Line::from(vec![ 196 | Span::styled("|", Style::default().fg(Color::Yellow)), 197 | Span::styled("exported: ", Style::default().fg(Color::Yellow)), 198 | Span::styled( 199 | format!("{}/*", self.home_dir), 200 | Style::default().fg(Color::Green), 201 | ), 202 | Span::styled("|", Style::default().fg(Color::Yellow)), 203 | ]); 204 | f.render_widget(line, l_area); 205 | } 206 | 207 | Ok(()) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/components/interfaces.rs: -------------------------------------------------------------------------------- 1 | use ipnetwork::IpNetwork; 2 | use pnet::{ 3 | datalink::{self, NetworkInterface}, 4 | util::MacAddr, 5 | }; 6 | use std::net::IpAddr; 7 | use std::time::Instant; 8 | 9 | use color_eyre::eyre::Result; 10 | use ratatui::{prelude::*, widgets::*}; 11 | use tokio::sync::mpsc::UnboundedSender; 12 | 13 | use super::Component; 14 | use crate::{ 15 | action::Action, 16 | config::DEFAULT_BORDER_STYLE, 17 | layout::{get_horizontal_layout, get_vertical_layout}, 18 | mode::Mode, 19 | tui::Frame, 20 | }; 21 | 22 | pub struct Interfaces { 23 | action_tx: Option>, 24 | interfaces: Vec, 25 | last_update_time: Instant, 26 | active_interfaces: Vec, 27 | active_interface_index: usize, 28 | } 29 | 30 | impl Default for Interfaces { 31 | fn default() -> Self { 32 | Self::new() 33 | } 34 | } 35 | 36 | impl Interfaces { 37 | pub fn new() -> Self { 38 | Self { 39 | action_tx: None, 40 | interfaces: Vec::new(), 41 | last_update_time: Instant::now(), 42 | active_interfaces: Vec::new(), 43 | active_interface_index: 0, 44 | } 45 | } 46 | 47 | fn get_interfaces(&mut self) { 48 | self.interfaces.clear(); 49 | self.active_interfaces.clear(); 50 | 51 | let interfaces = datalink::interfaces(); 52 | for intf in &interfaces { 53 | // -- get active interface with non-local IP 54 | if (cfg!(windows) || intf.is_up()) && !intf.ips.is_empty() { 55 | // Windows doesn't have the is_up() method 56 | for ip in &intf.ips { 57 | if let IpAddr::V4(ipv4) = ip.ip() { 58 | if ipv4.is_private() && !ipv4.is_loopback() && !ipv4.is_unspecified() { 59 | self.active_interfaces.push(intf.clone()); 60 | break; 61 | } 62 | } 63 | } 64 | } 65 | // -- store interfaces into a vec 66 | self.interfaces.push(intf.clone()); 67 | } 68 | // -- sort interfaces 69 | self.interfaces.sort_by(|a, b| a.name.cmp(&b.name)); 70 | } 71 | 72 | fn next_active_interface(&mut self) { 73 | let mut new_index = self.active_interface_index + 1; 74 | if new_index >= self.active_interfaces.len() { 75 | new_index = 0; 76 | } 77 | if new_index != self.active_interface_index { 78 | self.active_interface_index = new_index; 79 | self.send_active_interface(); 80 | } 81 | } 82 | 83 | fn send_active_interface(&mut self) { 84 | if !self.active_interfaces.is_empty() { 85 | let tx = self.action_tx.clone().unwrap(); 86 | let active_interface = &self.active_interfaces[self.active_interface_index]; 87 | tx.send(Action::ActiveInterface(active_interface.clone())) 88 | .unwrap(); 89 | } 90 | } 91 | 92 | fn app_tick(&mut self) -> Result<()> { 93 | let now = Instant::now(); 94 | let elapsed = (now - self.last_update_time).as_secs_f64(); 95 | if elapsed > 5.0 { 96 | self.last_update_time = now; 97 | self.get_interfaces(); 98 | } 99 | Ok(()) 100 | } 101 | 102 | fn make_table(&mut self) -> Table { 103 | let mut active_interface: Option<&NetworkInterface> = None; 104 | if !self.active_interfaces.is_empty() { 105 | active_interface = Some(&self.active_interfaces[self.active_interface_index]); 106 | } 107 | let header = Row::new(vec!["", "name", "mac", "ipv4", "ipv6"]) 108 | .style(Style::default().fg(Color::Yellow)) 109 | .height(1); 110 | let mut rows = Vec::new(); 111 | for w in &self.interfaces { 112 | let mut active = String::from(""); 113 | if active_interface.is_some() && active_interface.unwrap() == w { 114 | active = String::from("*"); 115 | } 116 | let name = if cfg!(windows) { 117 | w.description.clone() 118 | } else { 119 | w.name.clone() 120 | }; let mac = w.mac.unwrap_or(MacAddr::default()).to_string(); 121 | let ipv4: Vec = w 122 | .ips 123 | .iter() 124 | .filter(|f| f.is_ipv4()) 125 | .cloned() 126 | .map(|ip| { 127 | let ip_str = ip.ip().to_string(); 128 | Line::from(vec![Span::styled( 129 | format!("{ip_str:<2}"), 130 | Style::default().fg(Color::Blue), 131 | )]) 132 | }) 133 | .collect(); 134 | let ipv6: Vec = w 135 | .ips 136 | .iter() 137 | .filter(|f| f.is_ipv6()) 138 | .cloned() 139 | .map(|ip| Span::from(ip.ip().to_string())) 140 | .collect(); 141 | 142 | let mut row_height = 1; 143 | if ipv4.len() > 1 { 144 | row_height = ipv4.clone().len() as u16; 145 | } 146 | rows.push( 147 | Row::new(vec![ 148 | Cell::from(Span::styled( 149 | format!("{active:<1}"), 150 | Style::default().fg(Color::Red), 151 | )), 152 | Cell::from(Span::styled( 153 | format!("{name:<2}"), 154 | Style::default().fg(Color::Green), 155 | )), 156 | Cell::from(mac), 157 | Cell::from(ipv4.clone()), 158 | Cell::from(vec![Line::from(ipv6)]), 159 | ]) 160 | .height(row_height), // .bottom_margin((ipv4.len()) as u16) 161 | ); 162 | } 163 | 164 | let table = Table::new( 165 | rows, 166 | [ 167 | Constraint::Length(1), 168 | Constraint::Length(8), 169 | Constraint::Length(18), 170 | Constraint::Length(14), 171 | Constraint::Length(25), 172 | ], 173 | ) 174 | .header(header) 175 | .block( 176 | Block::default() 177 | .title(Line::from(vec![ 178 | Span::styled("|Inter", Style::default().fg(Color::Yellow)), 179 | Span::styled("f", Style::default().fg(Color::Red)), 180 | Span::styled("aces|", Style::default().fg(Color::Yellow)), 181 | ])) 182 | .border_style(Style::default().fg(Color::Rgb(100, 100, 100))) 183 | .title_style(Style::default().fg(Color::Yellow)) 184 | .title_alignment(Alignment::Right) 185 | .borders(Borders::ALL) 186 | .border_type(DEFAULT_BORDER_STYLE) 187 | .padding(Padding::new(0, 0, 1, 0)), 188 | ) 189 | .column_spacing(1); 190 | table 191 | } 192 | } 193 | 194 | impl Component for Interfaces { 195 | fn init(&mut self, area: Size) -> Result<()> { 196 | self.get_interfaces(); 197 | self.send_active_interface(); 198 | Ok(()) 199 | } 200 | 201 | fn as_any(&self) -> &dyn std::any::Any { 202 | self 203 | } 204 | 205 | fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { 206 | self.action_tx = Some(tx); 207 | Ok(()) 208 | } 209 | 210 | fn update(&mut self, action: Action) -> Result> { 211 | if let Action::Tick = action { 212 | self.app_tick()? 213 | } 214 | if let Action::InterfaceSwitch = action { 215 | self.next_active_interface(); 216 | } 217 | 218 | Ok(None) 219 | } 220 | 221 | fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> { 222 | let v_layout = get_vertical_layout(area); 223 | let h_layout = get_horizontal_layout(area); 224 | 225 | let table_rect = Rect::new( 226 | h_layout.right.x, 227 | 1, 228 | h_layout.right.width, 229 | v_layout.top.height, 230 | ); 231 | 232 | let block = self.make_table(); 233 | f.render_widget(block, table_rect); 234 | 235 | Ok(()) 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/components/ports.rs: -------------------------------------------------------------------------------- 1 | use cidr::Ipv4Cidr; 2 | use color_eyre::eyre::Result; 3 | use color_eyre::owo_colors::OwoColorize; 4 | use dns_lookup::{lookup_addr, lookup_host}; 5 | use futures::StreamExt; 6 | use futures::{future::join_all, stream}; 7 | 8 | use ratatui::style::Stylize; 9 | 10 | use core::str; 11 | use port_desc::{PortDescription, TransportProtocol}; 12 | use ratatui::{prelude::*, widgets::*}; 13 | use std::net::{IpAddr, Ipv4Addr, SocketAddr}; 14 | use std::{string, time::Duration}; 15 | use tokio::{ 16 | net::TcpStream, 17 | sync::mpsc::{self, UnboundedSender}, 18 | task::{self, JoinHandle}, 19 | }; 20 | 21 | use super::Component; 22 | use crate::enums::COMMON_PORTS; 23 | use crate::{ 24 | action::Action, 25 | config::DEFAULT_BORDER_STYLE, 26 | enums::{PortsScanState, TabsEnum}, 27 | layout::get_vertical_layout, 28 | tui::Frame, 29 | }; 30 | 31 | static POOL_SIZE: usize = 64; 32 | const SPINNER_SYMBOLS: [&str; 6] = ["⠷", "⠯", "⠟", "⠻", "⠽", "⠾"]; 33 | 34 | #[derive(Debug, Clone, PartialEq)] 35 | pub struct ScannedIpPorts { 36 | pub ip: String, 37 | state: PortsScanState, 38 | hostname: String, 39 | pub ports: Vec, 40 | } 41 | 42 | pub struct Ports { 43 | active_tab: TabsEnum, 44 | action_tx: Option>, 45 | ip_ports: Vec, 46 | list_state: ListState, 47 | scrollbar_state: ScrollbarState, 48 | spinner_index: usize, 49 | port_desc: Option, 50 | } 51 | 52 | impl Default for Ports { 53 | fn default() -> Self { 54 | Self::new() 55 | } 56 | } 57 | 58 | impl Ports { 59 | pub fn new() -> Self { 60 | let mut port_desc = None; 61 | if let Ok(pd) = PortDescription::default() { 62 | port_desc = Some(pd); 63 | } 64 | 65 | Self { 66 | active_tab: TabsEnum::Discovery, 67 | action_tx: None, 68 | ip_ports: Vec::new(), 69 | list_state: ListState::default().with_selected(Some(0)), 70 | scrollbar_state: ScrollbarState::new(0), 71 | spinner_index: 0, 72 | port_desc, 73 | } 74 | } 75 | 76 | pub fn get_scanned_ports(&self) -> &Vec { 77 | &self.ip_ports 78 | } 79 | 80 | fn process_ip(&mut self, ip: &str) { 81 | let ipv4: Ipv4Addr = ip.parse().unwrap(); 82 | let hostname = lookup_addr(&ipv4.into()).unwrap_or_default(); 83 | 84 | if let Some(n) = self.ip_ports.iter_mut().find(|item| item.ip == ip) { 85 | n.ip = ip.to_string(); 86 | } else { 87 | self.ip_ports.push(ScannedIpPorts { 88 | ip: ip.to_string(), 89 | hostname, 90 | state: PortsScanState::Waiting, 91 | ports: Vec::new(), 92 | }); 93 | 94 | self.ip_ports.sort_by(|a, b| { 95 | let a_ip: Ipv4Addr = a.ip.parse::().unwrap(); 96 | let b_ip: Ipv4Addr = b.ip.parse::().unwrap(); 97 | a_ip.partial_cmp(&b_ip).unwrap() 98 | }); 99 | } 100 | 101 | self.set_scrollbar_height(); 102 | } 103 | 104 | fn set_scrollbar_height(&mut self) { 105 | let mut ip_len = 0; 106 | if !self.ip_ports.is_empty() { 107 | ip_len = self.ip_ports.len() - 1; 108 | } 109 | self.scrollbar_state = self.scrollbar_state.content_length(ip_len); 110 | } 111 | 112 | pub fn make_scrollbar<'a>() -> Scrollbar<'a> { 113 | let scrollbar = Scrollbar::default() 114 | .orientation(ScrollbarOrientation::VerticalRight) 115 | .style(Style::default().fg(Color::Rgb(100, 100, 100))) 116 | .begin_symbol(None) 117 | .end_symbol(None); 118 | scrollbar 119 | } 120 | 121 | fn previous_in_list(&mut self) { 122 | let index = match self.list_state.selected() { 123 | Some(index) => { 124 | if index == 0 { 125 | if self.ip_ports.is_empty() { 126 | 0 127 | } else { 128 | self.ip_ports.len() - 1 129 | } 130 | } else { 131 | index - 1 132 | } 133 | } 134 | None => 0, 135 | }; 136 | self.list_state.select(Some(index)); 137 | self.scrollbar_state = self.scrollbar_state.position(index); 138 | } 139 | 140 | fn next_in_list(&mut self) { 141 | let index = match self.list_state.selected() { 142 | Some(index) => { 143 | let mut s_ip_len = 0; 144 | if !self.ip_ports.is_empty() { 145 | s_ip_len = self.ip_ports.len() - 1; 146 | } 147 | if index >= s_ip_len { 148 | 0 149 | } else { 150 | index + 1 151 | } 152 | } 153 | None => 0, 154 | }; 155 | self.list_state.select(Some(index)); 156 | self.scrollbar_state = self.scrollbar_state.position(index); 157 | } 158 | 159 | fn scan_ports(&mut self, index: usize) { 160 | if index >= self.ip_ports.len() { 161 | return; // -- index out of bounds 162 | } 163 | 164 | self.ip_ports[index].state = PortsScanState::Scanning; 165 | 166 | let tx = self.action_tx.clone().unwrap(); 167 | let ip: IpAddr = self.ip_ports[index].ip.parse().unwrap(); 168 | let ports_box = Box::new(COMMON_PORTS.iter()); 169 | 170 | let h = tokio::spawn(async move { 171 | let ports = stream::iter(ports_box); 172 | ports 173 | .for_each_concurrent(POOL_SIZE, |port| { 174 | Self::scan(tx.clone(), index, ip, port.to_owned(), 2) 175 | }) 176 | .await; 177 | tx.send(Action::PortScanDone(index)).unwrap(); 178 | }); 179 | } 180 | 181 | async fn scan(tx: UnboundedSender, index: usize, ip: IpAddr, port: u16, timeout: u64) { 182 | let timeout = Duration::from_secs(2); 183 | let soc_addr = SocketAddr::new(ip, port); 184 | if let Ok(Ok(_)) = tokio::time::timeout(timeout, TcpStream::connect(&soc_addr)).await { 185 | tx.send(Action::PortScan(index, port)).unwrap(); 186 | } 187 | } 188 | 189 | fn scan_selected(&mut self) { 190 | if let Some(index) = self.list_state.selected() { 191 | self.scan_ports(index); 192 | } 193 | } 194 | 195 | fn store_scanned_port(&mut self, index: usize, port: u16) { 196 | let ip_ports = &mut self.ip_ports[index]; 197 | if !ip_ports.ports.contains(&port) { 198 | ip_ports.ports.push(port); 199 | } 200 | } 201 | 202 | fn make_list(&self, rect: Rect) -> List { 203 | let mut items = Vec::new(); 204 | for ip in &self.ip_ports { 205 | let mut lines = Vec::new(); 206 | 207 | let mut ip_line_vec = vec![ 208 | "IP: ".yellow(), 209 | ip.ip.clone().blue(), 210 | ]; 211 | if !ip.hostname.is_empty() { 212 | ip_line_vec.push(" (".into()); 213 | ip_line_vec.push(ip.hostname.clone().cyan()); 214 | ip_line_vec.push(")".into()); 215 | } 216 | lines.push(Line::from(ip_line_vec)); 217 | 218 | let mut ports_spans = vec!["PORTS: ".yellow()]; 219 | if ip.state == PortsScanState::Waiting { 220 | ports_spans.push("?".red()); 221 | } else if ip.state == PortsScanState::Scanning { 222 | let spinner = SPINNER_SYMBOLS[self.spinner_index]; 223 | ports_spans.push(spinner.magenta()); 224 | } else { 225 | let mut line_size = 0; 226 | 227 | for p in &ip.ports { 228 | let port = p.to_string(); 229 | line_size += port.len(); 230 | 231 | ports_spans.push(port.green()); 232 | 233 | if let Some(pd) = &self.port_desc { 234 | let p_type = pd.get_port_service_name(p.to_owned(), TransportProtocol::Tcp); 235 | let p_type_str = format!("({})", p_type).to_string(); 236 | ports_spans.push(p_type_str.clone().light_magenta()); 237 | line_size += p_type_str.len(); 238 | } 239 | 240 | ports_spans.push(", ".yellow()); 241 | 242 | let t_width: usize = (rect.width as usize) - 8; 243 | if line_size >= t_width { 244 | line_size = 0; 245 | lines.push(Line::from(ports_spans.clone())); 246 | ports_spans.clear(); 247 | ports_spans.push(" ".gray()); 248 | } 249 | } 250 | } 251 | lines.push(Line::from(ports_spans.clone())); 252 | 253 | let t = Text::from(lines); 254 | items.push(t); 255 | } 256 | 257 | List::new(items) 258 | .block( 259 | Block::new() 260 | .title( 261 | ratatui::widgets::block::Title::from(Line::from(vec![ 262 | Span::styled("|", Style::default().fg(Color::Yellow)), 263 | Span::styled( 264 | "s", 265 | Style::default().add_modifier(Modifier::BOLD).fg(Color::Red), 266 | ), 267 | Span::styled("can selected", Style::default().fg(Color::Yellow)), 268 | Span::styled("|", Style::default().fg(Color::Yellow)), 269 | ])) 270 | .alignment(Alignment::Right), // .position(ratatui::widgets::block::Position::Bottom), 271 | ) 272 | .title( 273 | ratatui::widgets::block::Title::from("|Ports|".yellow()) 274 | .position(ratatui::widgets::block::Position::Top) 275 | .alignment(Alignment::Right), 276 | ) 277 | .title( 278 | ratatui::widgets::block::Title::from(Line::from(vec![ 279 | Span::styled("|", Style::default().fg(Color::Yellow)), 280 | String::from(char::from_u32(0x25b2).unwrap_or('>')).red(), 281 | String::from(char::from_u32(0x25bc).unwrap_or('>')).red(), 282 | Span::styled("select|", Style::default().fg(Color::Yellow)), 283 | ])) 284 | .position(ratatui::widgets::block::Position::Bottom) 285 | .alignment(Alignment::Right), 286 | ) 287 | .border_style(Style::default().fg(Color::Rgb(100, 100, 100))) 288 | .borders(Borders::ALL) 289 | .border_type(DEFAULT_BORDER_STYLE) 290 | .padding(Padding::new(1, 3, 1, 1)), 291 | ) 292 | .highlight_symbol("*") 293 | .highlight_style( 294 | Style::default() 295 | .add_modifier(Modifier::BOLD) 296 | .bg(Color::Rgb(100, 100, 100)), 297 | ) 298 | } 299 | } 300 | 301 | impl Component for Ports { 302 | fn init(&mut self, area: Size) -> Result<()> { 303 | Ok(()) 304 | } 305 | 306 | fn as_any(&self) -> &dyn std::any::Any { 307 | self 308 | } 309 | 310 | fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { 311 | self.action_tx = Some(tx); 312 | Ok(()) 313 | } 314 | 315 | fn tab_changed(&mut self, tab: TabsEnum) -> Result<()> { 316 | self.active_tab = tab; 317 | Ok(()) 318 | } 319 | 320 | fn update(&mut self, action: Action) -> Result> { 321 | if let Action::Tick = action { 322 | let mut s_index = self.spinner_index + 1; 323 | s_index %= SPINNER_SYMBOLS.len() - 1; 324 | self.spinner_index = s_index; 325 | } 326 | 327 | // -- tab change 328 | if let Action::TabChange(tab) = action { 329 | self.tab_changed(tab).unwrap(); 330 | } 331 | 332 | if self.active_tab == TabsEnum::Ports { 333 | // -- prev & next select item in list 334 | if let Action::Down = action { 335 | self.next_in_list(); 336 | } 337 | if let Action::Up = action { 338 | self.previous_in_list(); 339 | } 340 | 341 | if let Action::ScanCidr = action { 342 | self.scan_selected(); 343 | } 344 | } 345 | 346 | if let Action::PortScan(index, port) = action { 347 | self.store_scanned_port(index, port); 348 | } 349 | 350 | if let Action::PortScanDone(index) = action { 351 | self.ip_ports[index].state = PortsScanState::Done; 352 | } 353 | 354 | // -- PING IP 355 | if let Action::PingIp(ref ip) = action { 356 | self.process_ip(ip); 357 | } 358 | 359 | Ok(None) 360 | } 361 | 362 | fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> { 363 | if self.active_tab == TabsEnum::Ports { 364 | let layout = get_vertical_layout(area); 365 | 366 | let mut list_rect = layout.bottom; 367 | list_rect.y += 1; 368 | list_rect.height -= 1; 369 | 370 | // -- LIST 371 | let list = self.make_list(list_rect); 372 | f.render_stateful_widget(list, list_rect, &mut self.list_state.clone()); 373 | 374 | // -- SCROLLBAR 375 | let scrollbar = Self::make_scrollbar(); 376 | let mut scroll_rect = list_rect; 377 | scroll_rect.y += 1; 378 | scroll_rect.height -= 2; 379 | f.render_stateful_widget( 380 | scrollbar, 381 | scroll_rect.inner(Margin { 382 | vertical: 1, 383 | horizontal: 1, 384 | }), 385 | &mut self.scrollbar_state, 386 | ); 387 | } 388 | 389 | Ok(()) 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /src/components/sniff.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::eyre::Result; 2 | use color_eyre::owo_colors::OwoColorize; 3 | use dns_lookup::{lookup_addr, lookup_host}; 4 | 5 | use ipnetwork::IpNetwork; 6 | use pnet::{ 7 | datalink::NetworkInterface, 8 | packet::{ 9 | arp::{ArpHardwareTypes, ArpOperations, ArpPacket, MutableArpPacket}, 10 | ethernet::{EtherTypes, MutableEthernetPacket}, 11 | MutablePacket, Packet, 12 | }, 13 | }; 14 | use ratatui::style::Stylize; 15 | 16 | use ratatui::{prelude::*, widgets::*}; 17 | use std::net::IpAddr; 18 | use tokio::sync::mpsc::{self, UnboundedSender}; 19 | use tui_scrollview::{ScrollView, ScrollViewState}; 20 | 21 | use super::Component; 22 | use crate::{ 23 | action::Action, 24 | config::DEFAULT_BORDER_STYLE, 25 | enums::{PacketTypeEnum, PacketsInfoTypesEnum, TabsEnum}, 26 | layout::{get_vertical_layout, HORIZONTAL_CONSTRAINTS}, 27 | tui::Frame, 28 | utils::bytes_convert, 29 | widgets::scroll_traffic::TrafficScroll, 30 | }; 31 | 32 | #[derive(Clone, Debug)] 33 | pub struct IPTraffic { 34 | pub ip: IpAddr, 35 | pub download: f64, 36 | pub upload: f64, 37 | pub hostname: String, 38 | } 39 | 40 | pub struct Sniffer { 41 | active_tab: TabsEnum, 42 | action_tx: Option>, 43 | list_state: ListState, 44 | scrollbar_state: ScrollbarState, 45 | traffic_ips: Vec, 46 | scrollview_state: ScrollViewState, 47 | udp_sum: f64, 48 | tcp_sum: f64, 49 | active_inft_ips: Vec, 50 | } 51 | 52 | impl Default for Sniffer { 53 | fn default() -> Self { 54 | Self::new() 55 | } 56 | } 57 | 58 | impl Sniffer { 59 | pub fn new() -> Self { 60 | Self { 61 | active_tab: TabsEnum::Discovery, 62 | action_tx: None, 63 | list_state: ListState::default().with_selected(Some(0)), 64 | scrollbar_state: ScrollbarState::new(0), 65 | traffic_ips: Vec::new(), 66 | scrollview_state: ScrollViewState::new(), 67 | udp_sum: 0.0, 68 | tcp_sum: 0.0, 69 | active_inft_ips: Vec::new(), 70 | } 71 | } 72 | 73 | fn scroll_down(&mut self) { 74 | self.scrollview_state.scroll_down(); 75 | } 76 | 77 | fn scroll_up(&mut self) { 78 | self.scrollview_state.scroll_up(); 79 | } 80 | 81 | fn traffic_contains_ip(&self, ip: &IpAddr) -> bool { 82 | self.traffic_ips 83 | .iter() 84 | .any(|traffic| traffic.ip == ip.clone()) 85 | } 86 | 87 | fn count_traffic_packet(&mut self, source: IpAddr, destination: IpAddr, length: usize) { 88 | // -- destination 89 | if self.traffic_contains_ip(&destination) { 90 | if let Some(ip_entry) = self.traffic_ips.iter_mut().find(|ie| ie.ip == destination) { 91 | ip_entry.download += length as f64; 92 | } 93 | } else { 94 | self.traffic_ips.push(IPTraffic { 95 | ip: destination, 96 | download: length as f64, 97 | upload: 0.0, 98 | hostname: lookup_addr(&destination).unwrap_or(String::from("unknown")), 99 | }); 100 | } 101 | 102 | // -- source 103 | if self.traffic_contains_ip(&source) { 104 | if let Some(ip_entry) = self.traffic_ips.iter_mut().find(|ie| ie.ip == source) { 105 | ip_entry.upload += length as f64; 106 | } 107 | } else { 108 | self.traffic_ips.push(IPTraffic { 109 | ip: source, 110 | download: 0.0, 111 | upload: length as f64, 112 | hostname: lookup_addr(&source).unwrap_or(String::from("unknown")), 113 | }); 114 | } 115 | 116 | self.traffic_ips.sort_by(|a, b| { 117 | let a_sum = a.download + a.upload; 118 | let b_sum = b.download + b.upload; 119 | b_sum.partial_cmp(&a_sum).unwrap() 120 | }); 121 | } 122 | 123 | fn process_packet(&mut self, packet: PacketsInfoTypesEnum) { 124 | match packet { 125 | PacketsInfoTypesEnum::Tcp(p) => { 126 | self.count_traffic_packet(p.source, p.destination, p.length); 127 | self.tcp_sum += p.length as f64; 128 | } 129 | PacketsInfoTypesEnum::Udp(p) => { 130 | self.count_traffic_packet(p.source, p.destination, p.length); 131 | self.udp_sum += p.length as f64; 132 | } 133 | _ => {} 134 | } 135 | } 136 | 137 | fn make_charts(&self) -> BarChart { 138 | BarChart::default() 139 | .direction(Direction::Vertical) 140 | .bar_width(12) 141 | .bar_gap(4) 142 | .data( 143 | BarGroup::default().bars(&[ 144 | Bar::default() 145 | .value(self.udp_sum as u64) 146 | .text_value(bytes_convert(self.udp_sum)) 147 | .label("UDP".into()) 148 | .style(Color::Yellow), 149 | Bar::default() 150 | .value(self.tcp_sum as u64) 151 | .text_value(bytes_convert(self.tcp_sum)) 152 | .label("TCP".into()) 153 | .style(Color::Green), 154 | ]), 155 | ) 156 | } 157 | 158 | fn make_ips_block(&self) -> Block { 159 | let ips_block = Block::default() 160 | .title( 161 | ratatui::widgets::block::Title::from(Line::from(vec![ 162 | Span::styled("|", Style::default().fg(Color::Yellow)), 163 | Span::styled( 164 | String::from(char::from_u32(0x25b2).unwrap_or('<')), 165 | Style::default().fg(Color::Red), 166 | ), 167 | Span::styled( 168 | String::from(char::from_u32(0x25bc).unwrap_or('>')), 169 | Style::default().fg(Color::Red), 170 | ), 171 | Span::styled("scroll|", Style::default().fg(Color::Yellow)), 172 | ])) 173 | .position(ratatui::widgets::block::Position::Bottom) 174 | .alignment(Alignment::Right), 175 | ) 176 | .title( 177 | ratatui::widgets::block::Title::from(Span::styled( 178 | "|Download/Upload|", 179 | Style::default().fg(Color::Yellow), 180 | )) 181 | .position(ratatui::widgets::block::Position::Top) 182 | .alignment(Alignment::Right), 183 | ) 184 | .borders(Borders::ALL) 185 | .border_style(Color::Rgb(100, 100, 100)) 186 | .border_type(BorderType::Rounded); 187 | ips_block 188 | } 189 | 190 | fn make_sum_block(&self) -> Block { 191 | let ips_block = Block::default() 192 | .title( 193 | ratatui::widgets::block::Title::from(Span::styled( 194 | "|Summary|", 195 | Style::default().fg(Color::Yellow), 196 | )) 197 | .position(ratatui::widgets::block::Position::Top) 198 | .alignment(Alignment::Right), 199 | ) 200 | .borders(Borders::ALL) 201 | .border_style(Color::Rgb(100, 100, 100)) 202 | .border_type(BorderType::Rounded); 203 | ips_block 204 | } 205 | 206 | fn make_charts_block(&self) -> Block { 207 | Block::default() 208 | .title( 209 | ratatui::widgets::block::Title::from(Span::styled( 210 | "|Protocols sum|", 211 | Style::default().fg(Color::Yellow), 212 | )) 213 | .position(ratatui::widgets::block::Position::Top) 214 | .alignment(Alignment::Right), 215 | ) 216 | .borders(Borders::ALL) 217 | .border_style(Color::Rgb(100, 100, 100)) 218 | .border_type(BorderType::Rounded) 219 | } 220 | 221 | fn render_summary(&mut self, f: &mut Frame<'_>, area: Rect) { 222 | if !self.traffic_ips.is_empty() { 223 | let total_download = Line::from(vec![ 224 | "Total download: ".into(), 225 | bytes_convert(self.traffic_ips[0].download).green(), 226 | ]); 227 | f.render_widget( 228 | total_download, 229 | Rect { 230 | x: area.x + 2, 231 | y: area.y + 2, 232 | width: area.width, 233 | height: 1, 234 | }, 235 | ); 236 | 237 | let total_upload = Line::from(vec![ 238 | "Total upload: ".into(), 239 | bytes_convert(self.traffic_ips[0].upload).red(), 240 | ]); 241 | f.render_widget( 242 | total_upload, 243 | Rect { 244 | x: area.x + 2, 245 | y: area.y + 3, 246 | width: area.width, 247 | height: 1, 248 | }, 249 | ); 250 | 251 | let a_intfs = &self.active_inft_ips; 252 | let tu = self 253 | .traffic_ips 254 | .iter() 255 | .filter(|item| { 256 | let t_ip = item.ip.to_string(); 257 | for i_ip in a_intfs { 258 | if i_ip.ip().to_string() == t_ip { 259 | return false; 260 | } 261 | } 262 | true 263 | }) 264 | .max_by_key(|t| t.upload as u64); 265 | 266 | let mut tu_ip = String::from(""); 267 | let mut tu_name = String::from(""); 268 | if let Some(tu) = tu { 269 | tu_ip = tu.ip.to_string(); 270 | tu_name = format!(" ({})", tu.hostname); 271 | } 272 | let top_uploader = Line::from(vec![ 273 | "Top uploader: ".into(), 274 | tu_ip.blue(), 275 | tu_name.magenta(), 276 | ]); 277 | f.render_widget( 278 | top_uploader, 279 | Rect { 280 | x: area.x + 2, 281 | y: area.y + 5, 282 | width: area.width, 283 | height: 1, 284 | }, 285 | ); 286 | 287 | let td = self 288 | .traffic_ips 289 | .iter() 290 | .filter(|item| { 291 | let t_ip = item.ip.to_string(); 292 | for i_ip in a_intfs { 293 | if i_ip.ip().to_string() == t_ip { 294 | return false; 295 | } 296 | } 297 | true 298 | }) 299 | .max_by_key(|t| t.download as u64); 300 | 301 | let mut td_ip = String::from(""); 302 | let mut td_name = String::from(""); 303 | if let Some(td) = td { 304 | td_ip = td.ip.to_string(); 305 | td_name = format!(" ({})", td.hostname); 306 | } 307 | let top_downloader = Line::from(vec![ 308 | "Top downloader: ".into(), 309 | td_ip.blue(), 310 | td_name.magenta(), 311 | ]); 312 | f.render_widget( 313 | top_downloader, 314 | Rect { 315 | x: area.x + 2, 316 | y: area.y + 6, 317 | width: area.width, 318 | height: 1, 319 | }, 320 | ); 321 | } 322 | } 323 | 324 | fn tab_changed(&mut self, tab: TabsEnum) -> Result<()> { 325 | self.active_tab = tab; 326 | Ok(()) 327 | } 328 | } 329 | 330 | impl Component for Sniffer { 331 | fn as_any(&self) -> &dyn std::any::Any { 332 | self 333 | } 334 | 335 | fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { 336 | self.action_tx = Some(tx); 337 | Ok(()) 338 | } 339 | 340 | fn update(&mut self, action: Action) -> Result> { 341 | // -- tab change 342 | if let Action::TabChange(tab) = action { 343 | self.tab_changed(tab).unwrap(); 344 | } 345 | 346 | if self.active_tab == TabsEnum::Traffic { 347 | if let Action::Down = action { 348 | self.scroll_down(); 349 | } 350 | 351 | if let Action::Up = action { 352 | self.scroll_up(); 353 | } 354 | } 355 | 356 | if let Action::ActiveInterface(ref interface) = action { 357 | self.active_inft_ips = interface.ips.clone(); 358 | } 359 | 360 | if let Action::PacketDump(time, packet, packet_type) = action { 361 | match packet_type { 362 | PacketTypeEnum::Tcp => self.process_packet(packet), 363 | PacketTypeEnum::Udp => self.process_packet(packet), 364 | _ => {} 365 | } 366 | } 367 | 368 | Ok(None) 369 | } 370 | 371 | fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> { 372 | if self.active_tab == TabsEnum::Traffic { 373 | let layout = get_vertical_layout(area); 374 | 375 | // -- IPs block 376 | let mut ips_block_rect = layout.bottom; 377 | ips_block_rect.y += 1; 378 | ips_block_rect.height -= 1; 379 | let ips_layout = Layout::horizontal(HORIZONTAL_CONSTRAINTS).split(ips_block_rect); 380 | let b = self.make_ips_block(); 381 | f.render_widget(b, ips_layout[0]); 382 | 383 | // -- scrollview 384 | let ips_rect = Rect { 385 | x: ips_layout[0].x + 1, 386 | y: ips_layout[0].y + 1, 387 | width: ips_layout[0].width - 2, 388 | height: ips_layout[0].height - 2, 389 | }; 390 | let ips_scroll = TrafficScroll { 391 | traffic_ips: self.traffic_ips.clone(), 392 | }; 393 | f.render_stateful_widget(ips_scroll, ips_rect, &mut self.scrollview_state); 394 | 395 | // -- summary 396 | let sum_layout = 397 | Layout::vertical([Constraint::Percentage(30), Constraint::Percentage(70)]) 398 | .split(ips_layout[1]); 399 | let sum_block = self.make_sum_block(); 400 | f.render_widget(sum_block, sum_layout[0]); 401 | 402 | self.render_summary(f, sum_layout[0]); 403 | 404 | // -- charts 405 | let charts_block = self.make_charts_block(); 406 | f.render_widget(charts_block, sum_layout[1]); 407 | 408 | let charts = self.make_charts(); 409 | let charts_rect = Rect { 410 | x: sum_layout[1].x + 2, 411 | y: sum_layout[1].y + 2, 412 | width: sum_layout[1].width - 5, 413 | height: sum_layout[1].height - 3, 414 | }; 415 | f.render_widget(charts, charts_rect); 416 | } 417 | 418 | Ok(()) 419 | } 420 | } 421 | -------------------------------------------------------------------------------- /src/components/tabs.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::eyre::Result; 2 | use color_eyre::owo_colors::OwoColorize; 3 | use crossterm::event::{KeyCode, KeyEvent}; 4 | use ratatui::style::Stylize; 5 | use ratatui::{prelude::*, widgets::*}; 6 | use ratatui::{ 7 | text::{Line, Span}, 8 | widgets::{block::Title, Paragraph}, 9 | }; 10 | use serde::{Deserialize, Serialize}; 11 | use strum::{EnumCount, IntoEnumIterator}; 12 | use tokio::sync::mpsc::UnboundedSender; 13 | 14 | use super::{Component, Frame}; 15 | use crate::{ 16 | action::Action, 17 | config::DEFAULT_BORDER_STYLE, 18 | config::{Config, KeyBindings}, 19 | enums::TabsEnum, 20 | layout::get_vertical_layout, 21 | }; 22 | 23 | #[derive(Default)] 24 | pub struct Tabs { 25 | action_tx: Option>, 26 | config: Config, 27 | tab_index: usize, 28 | } 29 | 30 | impl Tabs { 31 | pub fn new() -> Self { 32 | Self { 33 | action_tx: None, 34 | config: Config::default(), 35 | tab_index: 0, 36 | } 37 | } 38 | 39 | fn make_tabs(&self) -> Paragraph { 40 | let enum_titles: Vec = 41 | TabsEnum::iter() 42 | .enumerate() 43 | .fold(Vec::new(), |mut title_spans, (idx, p)| { 44 | let index_str = idx + 1; 45 | 46 | let s1 = "(".yellow(); 47 | let s2 = format!("{}", index_str).red(); 48 | let s3 = ")".yellow(); 49 | let mut s4 = format!("{} ", p).dark_gray().bold(); 50 | if idx == self.tab_index { 51 | s4 = format!("{} ", p).green().bold(); 52 | } 53 | 54 | title_spans.push(s1); 55 | title_spans.push(s2); 56 | title_spans.push(s3); 57 | title_spans.push(s4); 58 | title_spans 59 | }); 60 | 61 | let title = Title::from(Line::from(vec![ 62 | "|".yellow(), 63 | "Tab".red().bold(), 64 | "s|".yellow(), 65 | ])) 66 | .alignment(Alignment::Right); 67 | 68 | let arrrow = String::from(char::from_u32(0x25bc).unwrap_or('>')); 69 | let b = Block::default() 70 | .title(title) 71 | .title( 72 | Title::from(Line::from(vec!["|".yellow(), arrrow.green(), "|".yellow()])) 73 | .alignment(Alignment::Center) 74 | .position(block::Position::Bottom), 75 | ) 76 | .borders(Borders::ALL) 77 | .border_type(DEFAULT_BORDER_STYLE) 78 | .padding(Padding::new(1, 0, 0, 0)) 79 | .border_style(Style::default().fg(Color::Rgb(100, 100, 100))); 80 | 81 | Paragraph::new(Line::from(enum_titles)).block(b) 82 | } 83 | 84 | fn next_tab(&mut self) { 85 | self.tab_index = (self.tab_index + 1) % TabsEnum::COUNT; 86 | if let Some(ref action_tx) = self.action_tx { 87 | let tab_enum = TabsEnum::iter().nth(self.tab_index).unwrap(); 88 | action_tx.send(Action::TabChange(tab_enum)).unwrap(); 89 | } 90 | } 91 | } 92 | 93 | impl Component for Tabs { 94 | fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { 95 | self.action_tx = Some(tx); 96 | Ok(()) 97 | } 98 | 99 | fn as_any(&self) -> &dyn std::any::Any { 100 | self 101 | } 102 | 103 | fn register_config_handler(&mut self, config: Config) -> Result<()> { 104 | self.config = config; 105 | Ok(()) 106 | } 107 | 108 | fn update(&mut self, action: Action) -> Result> { 109 | match action { 110 | Action::Tab => { 111 | self.next_tab(); 112 | } 113 | 114 | Action::TabChange(tab_enum) => TabsEnum::iter().enumerate().for_each(|(idx, t)| { 115 | if tab_enum == t { 116 | self.tab_index = idx; 117 | } 118 | }), 119 | 120 | _ => {} 121 | } 122 | Ok(None) 123 | } 124 | 125 | fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> { 126 | let layout = get_vertical_layout(area); 127 | let mut rect = layout.tabs; 128 | rect.y += 1; 129 | 130 | let tabs = self.make_tabs(); 131 | f.render_widget(tabs, rect); 132 | 133 | Ok(()) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/components/title.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, time::Duration}; 2 | 3 | use color_eyre::eyre::Result; 4 | use crossterm::event::{KeyCode, KeyEvent}; 5 | use ratatui::{prelude::*, widgets::*}; 6 | use serde::{Deserialize, Serialize}; 7 | use tokio::sync::mpsc::UnboundedSender; 8 | 9 | use super::{Component, Frame}; 10 | use crate::{ 11 | action::Action, 12 | config::{Config, KeyBindings}, 13 | }; 14 | 15 | #[derive(Default)] 16 | pub struct Title { 17 | command_tx: Option>, 18 | config: Config, 19 | } 20 | 21 | impl Title { 22 | pub fn new() -> Self { 23 | Self { 24 | command_tx: None, 25 | config: Config::default(), 26 | } 27 | } 28 | } 29 | 30 | impl Component for Title { 31 | fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { 32 | self.command_tx = Some(tx); 33 | Ok(()) 34 | } 35 | 36 | fn as_any(&self) -> &dyn std::any::Any { 37 | self 38 | } 39 | 40 | fn register_config_handler(&mut self, config: Config) -> Result<()> { 41 | self.config = config; 42 | Ok(()) 43 | } 44 | 45 | fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> { 46 | let rect = Rect::new(0, 0, f.area().width, 1); 47 | let version: &str = env!("CARGO_PKG_VERSION"); 48 | let title = format!(" Network Scanner (v{})", version); 49 | f.render_widget(Paragraph::new(title), rect); 50 | Ok(()) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/components/wifi_chart.rs: -------------------------------------------------------------------------------- 1 | use crate::components::wifi_scan::WifiInfo; 2 | use crate::utils::MaxSizeVec; 3 | use chrono::Timelike; 4 | use color_eyre::eyre::Result; 5 | use pnet::datalink::{self, NetworkInterface}; 6 | use ratatui::{prelude::*, widgets::*}; 7 | use std::collections::HashMap; 8 | use std::process::{Command, Output}; 9 | use std::time::Instant; 10 | use tokio::sync::mpsc::UnboundedSender; 11 | 12 | use super::Component; 13 | use crate::{ 14 | action::Action, 15 | config::DEFAULT_BORDER_STYLE, 16 | layout::{get_horizontal_layout, get_vertical_layout}, 17 | tui::Frame, 18 | }; 19 | 20 | #[derive(Debug)] 21 | pub struct WifiDataset { 22 | ssid: String, 23 | data: MaxSizeVec<(f64, f64)>, 24 | color: Color, 25 | } 26 | 27 | pub struct WifiChart { 28 | action_tx: Option>, 29 | last_update_time: Instant, 30 | wifi_datasets: Vec, 31 | signal_tick: [f64; 2], 32 | show_graph: bool, 33 | } 34 | 35 | impl Default for WifiChart { 36 | fn default() -> Self { 37 | Self::new() 38 | } 39 | } 40 | 41 | impl WifiChart { 42 | pub fn new() -> Self { 43 | Self { 44 | show_graph: false, 45 | action_tx: None, 46 | last_update_time: Instant::now(), 47 | wifi_datasets: Vec::new(), 48 | signal_tick: [0.0, 40.0], 49 | } 50 | } 51 | 52 | fn app_tick(&mut self) -> Result<()> { 53 | Ok(()) 54 | } 55 | 56 | fn parse_char_data(&mut self, nets: &[WifiInfo]) { 57 | for w in nets { 58 | let seconds: f64 = w.time.second() as f64; 59 | if let Some(p) = self 60 | .wifi_datasets 61 | .iter_mut() 62 | .position(|item| item.ssid == w.ssid) 63 | { 64 | let n = &mut self.wifi_datasets[p]; 65 | let signal: f64 = w.signal as f64; 66 | n.data.push((self.signal_tick[1], signal * -1.0)); 67 | } else { 68 | self.wifi_datasets.push(WifiDataset { 69 | ssid: w.ssid.clone(), 70 | data: MaxSizeVec::new(100), 71 | color: w.color, 72 | }); 73 | } 74 | } 75 | self.signal_tick[0] += 1.0; 76 | self.signal_tick[1] += 1.0; 77 | } 78 | 79 | pub fn make_chart(&self) -> Chart { 80 | let mut datasets = Vec::new(); 81 | for d in &self.wifi_datasets { 82 | let d_data = &d.data.get_vec(); 83 | let dataset = Dataset::default() 84 | .name(&*d.ssid) 85 | .marker(symbols::Marker::Dot) 86 | .style(Style::default().fg(d.color)) 87 | .graph_type(GraphType::Line) 88 | .data(d_data); 89 | datasets.push(dataset); 90 | } 91 | 92 | let x_labels: Vec = [ 93 | self.signal_tick[0].to_string(), 94 | (((self.signal_tick[1] - self.signal_tick[0]) / 2.0) + self.signal_tick[0]).to_string(), 95 | self.signal_tick[1].to_string(), 96 | ] 97 | .iter() 98 | .cloned() 99 | .map(Span::from) 100 | .collect(); 101 | 102 | let chart = Chart::new(datasets) 103 | .block( 104 | Block::new() 105 | .title( 106 | ratatui::widgets::block::Title::from("|WiFi signals|".yellow()) 107 | .position(ratatui::widgets::block::Position::Top) 108 | .alignment(Alignment::Right), 109 | ) 110 | .title( 111 | ratatui::widgets::block::Title::from(Line::from(vec![ 112 | Span::styled("|hide ", Style::default().fg(Color::Yellow)), 113 | Span::styled("g", Style::default().fg(Color::Red)), 114 | Span::styled("raph|", Style::default().fg(Color::Yellow)), 115 | ])) 116 | .position(ratatui::widgets::block::Position::Bottom) 117 | .alignment(Alignment::Right), 118 | ) 119 | .border_style(Style::default().fg(Color::Rgb(100, 100, 100))) 120 | .borders(Borders::ALL) 121 | .border_type(DEFAULT_BORDER_STYLE) 122 | .padding(Padding::new(1, 1, 1, 1)), 123 | ) 124 | .y_axis( 125 | Axis::default() 126 | .bounds([25.0, 100.0]) 127 | .title("[signal(dbm)]") 128 | .labels( 129 | ["-25.0", "-52.0", "-100.0"] 130 | .iter() 131 | .cloned() 132 | .map(Span::from) 133 | .collect::>(), 134 | ) 135 | .style(Style::default().fg(Color::Yellow)), 136 | ) 137 | .x_axis( 138 | Axis::default() 139 | .bounds(self.signal_tick) 140 | .title("[scans]") 141 | .labels(x_labels) 142 | .style(Style::default().fg(Color::Yellow)), 143 | ) 144 | .legend_position(Some(LegendPosition::TopLeft)) 145 | .hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2))); 146 | chart 147 | } 148 | } 149 | 150 | impl Component for WifiChart { 151 | fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { 152 | self.action_tx = Some(tx); 153 | Ok(()) 154 | } 155 | 156 | fn as_any(&self) -> &dyn std::any::Any { 157 | self 158 | } 159 | 160 | fn update(&mut self, action: Action) -> Result> { 161 | if let Action::Tick = action { 162 | self.app_tick()? 163 | } 164 | // -- custom actions 165 | if let Action::Scan(ref nets) = action { 166 | self.parse_char_data(nets); 167 | } 168 | 169 | if let Action::GraphToggle = action { 170 | self.show_graph = !self.show_graph; 171 | } 172 | 173 | Ok(None) 174 | } 175 | 176 | fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> { 177 | if self.show_graph { 178 | let v_layout = get_vertical_layout(area); 179 | let h_layout = get_horizontal_layout(area); 180 | 181 | let rect = Rect::new(h_layout.left.x, 1, h_layout.left.width, v_layout.top.height); 182 | 183 | let block = self.make_chart(); 184 | f.render_widget(block, rect); 185 | } 186 | Ok(()) 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/components/wifi_interface.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::eyre::Result; 2 | use pnet::datalink::{self, NetworkInterface}; 3 | use ratatui::{prelude::*, widgets::*}; 4 | use std::collections::HashMap; 5 | use std::process::{Command, Output}; 6 | use std::time::Instant; 7 | use tokio::sync::mpsc::UnboundedSender; 8 | 9 | use super::Component; 10 | use crate::{ 11 | action::Action, 12 | layout::{get_horizontal_layout, get_vertical_layout}, 13 | mode::Mode, 14 | tui::Frame, 15 | }; 16 | 17 | #[derive(Debug, PartialEq)] 18 | struct WifiConn { 19 | interface: String, 20 | ifindex: u8, 21 | mac: String, 22 | ssid: String, 23 | channel: String, 24 | txpower: String, 25 | } 26 | 27 | struct CommandError { 28 | desc: String, 29 | } 30 | 31 | pub struct WifiInterface { 32 | action_tx: Option>, 33 | last_update: Instant, 34 | wifi_info: Option, 35 | } 36 | 37 | impl Default for WifiInterface { 38 | fn default() -> Self { 39 | Self::new() 40 | } 41 | } 42 | 43 | impl WifiInterface { 44 | pub fn new() -> Self { 45 | Self { 46 | action_tx: None, 47 | last_update: Instant::now(), 48 | wifi_info: None, 49 | } 50 | } 51 | 52 | fn app_tick(&mut self) -> Result<()> { 53 | let now = Instant::now(); 54 | let elapsed = (now - self.last_update).as_secs_f64(); 55 | 56 | if self.wifi_info.is_none() || elapsed > 5.0 { 57 | self.last_update = now; 58 | self.get_connected_wifi_info(); 59 | } 60 | Ok(()) 61 | } 62 | 63 | fn iw_command(&mut self, intf_name: String) -> Result { 64 | let iw_output = Command::new("iw") 65 | .arg("dev") 66 | .arg(intf_name) 67 | .arg("info") 68 | .output() 69 | .map_err(|e| CommandError { 70 | desc: format!("command failed: {}", e), 71 | })?; 72 | if iw_output.status.success() { 73 | Ok(iw_output) 74 | } else { 75 | Err(CommandError { 76 | desc: "command failed".to_string(), 77 | }) 78 | } 79 | } 80 | 81 | fn parse_iw_command(&mut self, output: String) -> WifiConn { 82 | let lines = output.lines(); 83 | let mut hash = HashMap::new(); 84 | for l in lines { 85 | let split = l.trim().split(" ").collect::>(); 86 | if split.len() > 1 { 87 | hash.insert(split[0], split[1].trim()); 88 | } 89 | } 90 | WifiConn { 91 | interface: hash 92 | .get("Interface") 93 | .unwrap_or(&"") 94 | .parse::() 95 | .unwrap_or(String::from("")), 96 | ssid: hash 97 | .get("ssid") 98 | .unwrap_or(&"") 99 | .parse::() 100 | .unwrap_or(String::from("")), 101 | ifindex: hash 102 | .get("ifindex") 103 | .unwrap_or(&"") 104 | .parse::() 105 | .unwrap_or(0), 106 | mac: hash 107 | .get("addr") 108 | .unwrap_or(&"") 109 | .parse::() 110 | .unwrap_or(String::from("")), 111 | channel: hash 112 | .get("channel") 113 | .unwrap_or(&"") 114 | .parse::() 115 | .unwrap_or(String::from("")), 116 | txpower: hash 117 | .get("txpower") 118 | .unwrap_or(&"") 119 | .parse::() 120 | .unwrap_or(String::from("")), 121 | } 122 | } 123 | 124 | fn get_connected_wifi_info(&mut self) { 125 | let interfaces = datalink::interfaces(); 126 | for i in interfaces { 127 | if let Ok(output) = self.iw_command(i.name) { 128 | let o = String::from_utf8(output.stdout).unwrap_or(String::from("")); 129 | self.wifi_info = Some(self.parse_iw_command(o)); 130 | } 131 | } 132 | } 133 | 134 | fn make_list(&mut self) -> List { 135 | if let Some(wifi_info) = &self.wifi_info { 136 | let interface = &wifi_info.interface; 137 | let interface_label = "Interface:"; 138 | let ssid = &wifi_info.ssid; 139 | let ssid_label = "SSID:"; 140 | let ifindex = &wifi_info.ifindex; 141 | let ifindex_label = "Intf index:"; 142 | let channel = &wifi_info.channel; 143 | let channel_label = "Channel:"; 144 | let txpower = &wifi_info.txpower; 145 | let txpower_label = "TxPower:"; 146 | let mac = &wifi_info.mac; 147 | let mac_label = "Mac addr:"; 148 | 149 | let mut items: Vec = Vec::new(); 150 | 151 | items.push(ListItem::new(vec![ 152 | Line::from(vec![ 153 | Span::styled( 154 | format!("{ssid_label:<12}"), 155 | Style::default().fg(Color::White), 156 | ), 157 | Span::styled(format!("{ssid:<12}"), Style::default().fg(Color::Green)), 158 | ]), 159 | Line::from(vec![ 160 | Span::styled( 161 | format!("{channel_label:<12}"), 162 | Style::default().fg(Color::White), 163 | ), 164 | Span::styled(format!("{channel:<12}"), Style::default().fg(Color::Green)), 165 | ]), 166 | ])); 167 | 168 | List::new(items).block( 169 | Block::default() 170 | .borders(Borders::TOP) 171 | .title("|WiFi Interface|") 172 | .border_style(Style::default().fg(Color::Rgb(100, 100, 100))) 173 | .title_style(Style::default().fg(Color::Yellow)) 174 | .padding(Padding::new(2, 0, 0, 0)) 175 | .title_alignment(Alignment::Right), 176 | ) 177 | } else { 178 | let items: Vec = Vec::new(); 179 | List::new(items) 180 | } 181 | } 182 | } 183 | 184 | impl Component for WifiInterface { 185 | fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { 186 | self.action_tx = Some(tx); 187 | Ok(()) 188 | } 189 | 190 | fn as_any(&self) -> &dyn std::any::Any { 191 | self 192 | } 193 | 194 | fn update(&mut self, action: Action) -> Result> { 195 | if let Action::Tick = action { 196 | self.app_tick()? 197 | } 198 | Ok(None) 199 | } 200 | 201 | fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> { 202 | let v_layout = get_vertical_layout(area); 203 | let h_layout = get_horizontal_layout(area); 204 | 205 | let rect = Rect::new( 206 | h_layout.right.x + 1, 207 | (v_layout.top.y + v_layout.top.height) - 3, 208 | h_layout.right.width - 2, 209 | 4, 210 | ); 211 | 212 | let block = self.make_list(); 213 | f.render_widget(block, rect); 214 | 215 | Ok(()) 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/components/wifi_scan.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Local, Timelike}; 2 | use config::Source; 3 | use std::time::Instant; 4 | use tokio::sync::mpsc::UnboundedSender; 5 | use tokio_wifiscanner::Wifi; 6 | 7 | use color_eyre::eyre::Result; 8 | use ratatui::{prelude::*, widgets::*}; 9 | 10 | use super::Component; 11 | use crate::{ 12 | action::Action, 13 | config::DEFAULT_BORDER_STYLE, 14 | layout::{get_horizontal_layout, get_vertical_layout}, 15 | mode::Mode, 16 | tui::Frame, 17 | }; 18 | 19 | #[derive(Debug, PartialEq, Clone)] 20 | pub struct WifiInfo { 21 | pub time: DateTime, 22 | pub ssid: String, 23 | pub channel: u8, 24 | pub signal: f32, 25 | pub mac: String, 26 | pub color: Color, 27 | } 28 | 29 | impl WifiInfo { 30 | fn copy_values(&mut self, net: WifiInfo) { 31 | self.time = net.time; 32 | self.ssid = net.ssid; 33 | self.channel = net.channel; 34 | self.signal = net.signal; 35 | self.mac = net.mac; 36 | } 37 | } 38 | 39 | pub struct WifiScan { 40 | pub action_tx: Option>, 41 | pub scan_start_time: Instant, 42 | pub wifis: Vec, 43 | pub signal_tick: [f64; 2], 44 | show_graph: bool, 45 | } 46 | 47 | impl Default for WifiScan { 48 | fn default() -> Self { 49 | Self::new() 50 | } 51 | } 52 | 53 | const COLORS_SIGNAL: [Color; 7] = [ 54 | Color::Red, 55 | Color::LightRed, 56 | Color::LightMagenta, 57 | Color::Magenta, 58 | Color::Yellow, 59 | Color::LightGreen, 60 | Color::Green, 61 | ]; 62 | 63 | const COLORS_NAMES: [Color; 14] = [ 64 | Color::Rgb(244, 67, 54), 65 | Color::Rgb(233, 30, 99), 66 | Color::Rgb(156, 39, 176), 67 | Color::Rgb(103, 58, 183), 68 | Color::Rgb(63, 81, 184), 69 | Color::Rgb(3, 169, 244), 70 | Color::Rgb(0, 150, 136), 71 | Color::Rgb(255, 235, 59), 72 | Color::Rgb(255, 152, 0), 73 | Color::Rgb(255, 87, 34), 74 | Color::Rgb(121, 85, 72), 75 | Color::Rgb(158, 158, 158), 76 | Color::Rgb(96, 125, 139), 77 | Color::White, 78 | ]; 79 | 80 | static MIN_DBM: f32 = -100.0; 81 | static MAX_DBM: f32 = -1.0; 82 | 83 | impl WifiScan { 84 | pub fn new() -> Self { 85 | Self { 86 | show_graph: false, 87 | scan_start_time: Instant::now(), 88 | wifis: Vec::new(), 89 | action_tx: None, 90 | signal_tick: [0.0, 40.0], 91 | } 92 | } 93 | 94 | fn make_table(&mut self) -> Table { 95 | let header = Row::new(vec!["time", "ssid", "ch", "mac", "signal"]) 96 | .style(Style::default().fg(Color::Yellow)); 97 | // .bottom_margin(1); 98 | let mut rows = Vec::new(); 99 | for w in &self.wifis { 100 | let s_clamp = w.signal.max(MIN_DBM).min(MAX_DBM); 101 | let percent = ((s_clamp - MIN_DBM) / (MAX_DBM - MIN_DBM)).clamp(0.0, 1.0); 102 | 103 | let p = (percent * 10.0) as usize; 104 | let gauge: String = std::iter::repeat(char::from_u32(0x25a8).unwrap_or('#')) 105 | .take(p) 106 | .collect(); 107 | 108 | let signal = format!("({}){}", w.signal, gauge); 109 | let color = (percent * ((COLORS_SIGNAL.len() - 1) as f32)) as usize; 110 | let ssid = w.ssid.clone(); 111 | let mut signal_span = Span::from(""); 112 | if w.signal < 0.0 { 113 | signal_span = Span::styled( 114 | format!("{signal:<2}"), 115 | Style::default().fg(COLORS_SIGNAL[color]), 116 | ); 117 | } 118 | 119 | rows.push(Row::new(vec![ 120 | Cell::from(w.time.format("%H:%M:%S").to_string()), 121 | Cell::from(Span::styled( 122 | format!("{ssid:<2}"), 123 | Style::default().fg(w.color), 124 | )), 125 | Cell::from(w.channel.to_string()), 126 | Cell::from(w.mac.clone()), 127 | Cell::from(signal_span), 128 | ])); 129 | } 130 | 131 | let table = Table::new( 132 | rows, 133 | [ 134 | Constraint::Length(9), 135 | Constraint::Length(11), 136 | Constraint::Length(4), 137 | Constraint::Length(17), 138 | Constraint::Length(18), 139 | ], 140 | ) 141 | .header(header) 142 | .block( 143 | Block::new() 144 | .title( 145 | ratatui::widgets::block::Title::from("|WiFi Networks|".yellow()) 146 | .position(ratatui::widgets::block::Position::Top) 147 | .alignment(Alignment::Right), 148 | ) 149 | .title( 150 | ratatui::widgets::block::Title::from(Line::from(vec![ 151 | Span::styled("|show ", Style::default().fg(Color::Yellow)), 152 | Span::styled("g", Style::default().fg(Color::Red)), 153 | Span::styled("raph|", Style::default().fg(Color::Yellow)), 154 | ])) 155 | .position(ratatui::widgets::block::Position::Bottom) 156 | .alignment(Alignment::Right), 157 | ) 158 | .border_style(Style::default().fg(Color::Rgb(100, 100, 100))) 159 | .borders(Borders::ALL) 160 | .border_type(DEFAULT_BORDER_STYLE) 161 | .padding(Padding::new(1, 0, 1, 0)), 162 | ) 163 | .column_spacing(1); 164 | table 165 | } 166 | 167 | pub fn scan(&mut self) { 168 | let tx = self.action_tx.clone().unwrap(); 169 | tokio::spawn(async move { 170 | let networks = tokio_wifiscanner::scan().await; 171 | match networks { 172 | Ok(nets) => { 173 | let mut wifi_nets: Vec = Vec::new(); 174 | let now = Local::now(); 175 | for w in nets { 176 | if let Some(n) = wifi_nets.iter_mut().find(|item| item.ssid == w.ssid) { 177 | let signal: f32 = w.signal_level.parse().unwrap_or(-100.00); 178 | if n.signal < signal { 179 | n.signal = signal; 180 | n.mac = w.mac; 181 | let channel = w.channel.parse::().unwrap_or(0); 182 | n.channel = channel; 183 | } 184 | } else { 185 | wifi_nets.push(WifiInfo { 186 | time: now, 187 | ssid: w.ssid, 188 | channel: w.channel.parse::().unwrap_or(0), 189 | signal: w.signal_level.parse::().unwrap_or(-100.00), 190 | mac: w.mac, 191 | color: COLORS_NAMES[wifi_nets.len() % COLORS_NAMES.len()], 192 | }); 193 | } 194 | } 195 | 196 | let t_send = tx.send(Action::Scan(wifi_nets)); 197 | match t_send { 198 | Ok(n) => (), 199 | Err(e) => (), 200 | } 201 | } 202 | Err(_e) => (), 203 | }; 204 | }); 205 | } 206 | 207 | fn parse_networks_data(&mut self, nets: &Vec) { 208 | // -- clear signal values 209 | self.wifis.iter_mut().for_each(|item| { 210 | item.signal = 0.0; 211 | }); 212 | // -- add or update wifi info 213 | for w in nets { 214 | if let Some(n) = self.wifis.iter_mut().find(|item| item.ssid == w.ssid) { 215 | n.copy_values(w.clone()); 216 | } else { 217 | self.wifis.push(w.clone()); 218 | } 219 | } 220 | // -- sort wifi networks by it's signal strength 221 | self.wifis 222 | .sort_by(|a, b| b.signal.partial_cmp(&a.signal).unwrap()); 223 | } 224 | 225 | fn app_tick(&mut self) -> Result<()> { 226 | let now = Instant::now(); 227 | let elapsed = (now - self.scan_start_time).as_secs_f64(); 228 | 229 | if elapsed > 1.5 { 230 | self.scan_start_time = now; 231 | self.scan(); 232 | } 233 | Ok(()) 234 | } 235 | 236 | fn render_tick(&mut self) -> Result<()> { 237 | Ok(()) 238 | } 239 | } 240 | 241 | impl Component for WifiScan { 242 | fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { 243 | self.action_tx = Some(tx); 244 | Ok(()) 245 | } 246 | 247 | fn as_any(&self) -> &dyn std::any::Any { 248 | self 249 | } 250 | 251 | fn update(&mut self, action: Action) -> Result> { 252 | if let Action::Tick = action { 253 | self.app_tick()? 254 | }; 255 | if let Action::Render = action { 256 | self.render_tick()? 257 | }; 258 | // -- custom actions 259 | if let Action::Scan(ref nets) = action { 260 | self.parse_networks_data(nets); 261 | } 262 | 263 | if let Action::GraphToggle = action { 264 | self.show_graph = !self.show_graph; 265 | } 266 | 267 | Ok(None) 268 | } 269 | 270 | fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> { 271 | if !self.show_graph { 272 | let v_layout = get_vertical_layout(area); 273 | let h_layout = get_horizontal_layout(area); 274 | 275 | let table_rect = 276 | Rect::new(h_layout.left.x, 1, h_layout.left.width, v_layout.top.height); 277 | 278 | let block = self.make_table(); 279 | f.render_widget(block, table_rect); 280 | } 281 | 282 | Ok(()) 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, fmt, path::PathBuf}; 2 | 3 | use color_eyre::eyre::Result; 4 | use config::Value; 5 | use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; 6 | use derive_deref::{Deref, DerefMut}; 7 | use ratatui::{style::{Color, Modifier, Style}, widgets::{BorderType, Borders}}; 8 | use serde::{ 9 | de::{self, Deserializer, MapAccess, Visitor}, 10 | Deserialize, Serialize, 11 | }; 12 | use serde_json::Value as JsonValue; 13 | 14 | use crate::{action::Action, mode::Mode}; 15 | 16 | pub const DEFAULT_BORDER_STYLE: BorderType = BorderType::Rounded; 17 | 18 | const CONFIG: &str = include_str!("../.config/config.json5"); 19 | 20 | #[derive(Clone, Debug, Deserialize, Default)] 21 | pub struct AppConfig { 22 | #[serde(default)] 23 | pub _data_dir: PathBuf, 24 | #[serde(default)] 25 | pub _config_dir: PathBuf, 26 | } 27 | 28 | #[derive(Clone, Debug, Default, Deserialize)] 29 | pub struct Config { 30 | #[serde(default, flatten)] 31 | pub config: AppConfig, 32 | #[serde(default)] 33 | pub keybindings: KeyBindings, 34 | #[serde(default)] 35 | pub styles: Styles, 36 | } 37 | 38 | impl Config { 39 | pub fn new() -> Result { 40 | let default_config: Config = json5::from_str(CONFIG).unwrap(); 41 | let data_dir = crate::utils::get_data_dir(); 42 | let config_dir = crate::utils::get_config_dir(); 43 | let mut builder = config::Config::builder() 44 | .set_default("_data_dir", data_dir.to_str().unwrap())? 45 | .set_default("_config_dir", config_dir.to_str().unwrap())?; 46 | 47 | let config_files = [ 48 | ("config.json5", config::FileFormat::Json5), 49 | ("config.json", config::FileFormat::Json), 50 | ("config.yaml", config::FileFormat::Yaml), 51 | ("config.toml", config::FileFormat::Toml), 52 | ("config.ini", config::FileFormat::Ini), 53 | ]; 54 | let mut found_config = false; 55 | for (file, format) in &config_files { 56 | builder = builder.add_source(config::File::from(config_dir.join(file)).format(*format).required(false)); 57 | if config_dir.join(file).exists() { 58 | found_config = true 59 | } 60 | } 61 | if !found_config { 62 | log::error!("No configuration file found. Application may not behave as expected"); 63 | } 64 | 65 | let mut cfg: Self = builder.build()?.try_deserialize()?; 66 | 67 | for (mode, default_bindings) in default_config.keybindings.iter() { 68 | let user_bindings = cfg.keybindings.entry(*mode).or_default(); 69 | for (key, cmd) in default_bindings.iter() { 70 | user_bindings.entry(key.clone()).or_insert_with(|| cmd.clone()); 71 | } 72 | } 73 | for (mode, default_styles) in default_config.styles.iter() { 74 | let user_styles = cfg.styles.entry(*mode).or_default(); 75 | for (style_key, style) in default_styles.iter() { 76 | user_styles.entry(style_key.clone()).or_insert_with(|| style.clone()); 77 | } 78 | } 79 | 80 | Ok(cfg) 81 | } 82 | } 83 | 84 | #[derive(Clone, Debug, Default, Deref, DerefMut)] 85 | pub struct KeyBindings(pub HashMap, Action>>); 86 | 87 | impl<'de> Deserialize<'de> for KeyBindings { 88 | fn deserialize(deserializer: D) -> Result 89 | where 90 | D: Deserializer<'de>, 91 | { 92 | let parsed_map = HashMap::>::deserialize(deserializer)?; 93 | 94 | let keybindings = parsed_map 95 | .into_iter() 96 | .map(|(mode, inner_map)| { 97 | let converted_inner_map = 98 | inner_map.into_iter().map(|(key_str, cmd)| (parse_key_sequence(&key_str).unwrap(), cmd)).collect(); 99 | (mode, converted_inner_map) 100 | }) 101 | .collect(); 102 | 103 | Ok(KeyBindings(keybindings)) 104 | } 105 | } 106 | 107 | fn parse_key_event(raw: &str) -> Result { 108 | let raw_lower = raw.to_ascii_lowercase(); 109 | let (remaining, modifiers) = extract_modifiers(&raw_lower); 110 | parse_key_code_with_modifiers(remaining, modifiers) 111 | } 112 | 113 | fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) { 114 | let mut modifiers = KeyModifiers::empty(); 115 | let mut current = raw; 116 | 117 | loop { 118 | match current { 119 | rest if rest.starts_with("ctrl-") => { 120 | modifiers.insert(KeyModifiers::CONTROL); 121 | current = &rest[5..]; 122 | }, 123 | rest if rest.starts_with("alt-") => { 124 | modifiers.insert(KeyModifiers::ALT); 125 | current = &rest[4..]; 126 | }, 127 | rest if rest.starts_with("shift-") => { 128 | modifiers.insert(KeyModifiers::SHIFT); 129 | current = &rest[6..]; 130 | }, 131 | _ => break, // break out of the loop if no known prefix is detected 132 | }; 133 | } 134 | 135 | (current, modifiers) 136 | } 137 | 138 | fn parse_key_code_with_modifiers(raw: &str, mut modifiers: KeyModifiers) -> Result { 139 | let c = match raw { 140 | "esc" => KeyCode::Esc, 141 | "enter" => KeyCode::Enter, 142 | "left" => KeyCode::Left, 143 | "right" => KeyCode::Right, 144 | "up" => KeyCode::Up, 145 | "down" => KeyCode::Down, 146 | "home" => KeyCode::Home, 147 | "end" => KeyCode::End, 148 | "pageup" => KeyCode::PageUp, 149 | "pagedown" => KeyCode::PageDown, 150 | "backtab" => { 151 | modifiers.insert(KeyModifiers::SHIFT); 152 | KeyCode::BackTab 153 | }, 154 | "backspace" => KeyCode::Backspace, 155 | "delete" => KeyCode::Delete, 156 | "insert" => KeyCode::Insert, 157 | "f1" => KeyCode::F(1), 158 | "f2" => KeyCode::F(2), 159 | "f3" => KeyCode::F(3), 160 | "f4" => KeyCode::F(4), 161 | "f5" => KeyCode::F(5), 162 | "f6" => KeyCode::F(6), 163 | "f7" => KeyCode::F(7), 164 | "f8" => KeyCode::F(8), 165 | "f9" => KeyCode::F(9), 166 | "f10" => KeyCode::F(10), 167 | "f11" => KeyCode::F(11), 168 | "f12" => KeyCode::F(12), 169 | "space" => KeyCode::Char(' '), 170 | "hyphen" => KeyCode::Char('-'), 171 | "minus" => KeyCode::Char('-'), 172 | "tab" => KeyCode::Tab, 173 | c if c.len() == 1 => { 174 | let mut c = c.chars().next().unwrap(); 175 | if modifiers.contains(KeyModifiers::SHIFT) { 176 | c = c.to_ascii_uppercase(); 177 | } 178 | KeyCode::Char(c) 179 | }, 180 | _ => return Err(format!("Unable to parse {raw}")), 181 | }; 182 | Ok(KeyEvent::new(c, modifiers)) 183 | } 184 | 185 | pub fn key_event_to_string(key_event: &KeyEvent) -> String { 186 | let char; 187 | let key_code = match key_event.code { 188 | KeyCode::Backspace => "backspace", 189 | KeyCode::Enter => "enter", 190 | KeyCode::Left => "left", 191 | KeyCode::Right => "right", 192 | KeyCode::Up => "up", 193 | KeyCode::Down => "down", 194 | KeyCode::Home => "home", 195 | KeyCode::End => "end", 196 | KeyCode::PageUp => "pageup", 197 | KeyCode::PageDown => "pagedown", 198 | KeyCode::Tab => "tab", 199 | KeyCode::BackTab => "backtab", 200 | KeyCode::Delete => "delete", 201 | KeyCode::Insert => "insert", 202 | KeyCode::F(c) => { 203 | char = format!("f({c})"); 204 | &char 205 | }, 206 | KeyCode::Char(c) if c == ' ' => "space", 207 | KeyCode::Char(c) => { 208 | char = c.to_string(); 209 | &char 210 | }, 211 | KeyCode::Esc => "esc", 212 | KeyCode::Null => "", 213 | KeyCode::CapsLock => "", 214 | KeyCode::Menu => "", 215 | KeyCode::ScrollLock => "", 216 | KeyCode::Media(_) => "", 217 | KeyCode::NumLock => "", 218 | KeyCode::PrintScreen => "", 219 | KeyCode::Pause => "", 220 | KeyCode::KeypadBegin => "", 221 | KeyCode::Modifier(_) => "", 222 | }; 223 | 224 | let mut modifiers = Vec::with_capacity(3); 225 | 226 | if key_event.modifiers.intersects(KeyModifiers::CONTROL) { 227 | modifiers.push("ctrl"); 228 | } 229 | 230 | if key_event.modifiers.intersects(KeyModifiers::SHIFT) { 231 | modifiers.push("shift"); 232 | } 233 | 234 | if key_event.modifiers.intersects(KeyModifiers::ALT) { 235 | modifiers.push("alt"); 236 | } 237 | 238 | let mut key = modifiers.join("-"); 239 | 240 | if !key.is_empty() { 241 | key.push('-'); 242 | } 243 | key.push_str(key_code); 244 | 245 | key 246 | } 247 | 248 | pub fn parse_key_sequence(raw: &str) -> Result, String> { 249 | if raw.chars().filter(|c| *c == '>').count() != raw.chars().filter(|c| *c == '<').count() { 250 | return Err(format!("Unable to parse `{}`", raw)); 251 | } 252 | let raw = if !raw.contains("><") { 253 | let raw = raw.strip_prefix('<').unwrap_or(raw); 254 | let raw = raw.strip_prefix('>').unwrap_or(raw); 255 | raw 256 | } else { 257 | raw 258 | }; 259 | let sequences = raw 260 | .split("><") 261 | .map(|seq| { 262 | if let Some(s) = seq.strip_prefix('<') { 263 | s 264 | } else if let Some(s) = seq.strip_suffix('>') { 265 | s 266 | } else { 267 | seq 268 | } 269 | }) 270 | .collect::>(); 271 | 272 | sequences.into_iter().map(parse_key_event).collect() 273 | } 274 | 275 | #[derive(Clone, Debug, Default, Deref, DerefMut)] 276 | pub struct Styles(pub HashMap>); 277 | 278 | impl<'de> Deserialize<'de> for Styles { 279 | fn deserialize(deserializer: D) -> Result 280 | where 281 | D: Deserializer<'de>, 282 | { 283 | let parsed_map = HashMap::>::deserialize(deserializer)?; 284 | 285 | let styles = parsed_map 286 | .into_iter() 287 | .map(|(mode, inner_map)| { 288 | let converted_inner_map = inner_map.into_iter().map(|(str, style)| (str, parse_style(&style))).collect(); 289 | (mode, converted_inner_map) 290 | }) 291 | .collect(); 292 | 293 | Ok(Styles(styles)) 294 | } 295 | } 296 | 297 | pub fn parse_style(line: &str) -> Style { 298 | let (foreground, background) = line.split_at(line.to_lowercase().find("on ").unwrap_or(line.len())); 299 | let foreground = process_color_string(foreground); 300 | let background = process_color_string(&background.replace("on ", "")); 301 | 302 | let mut style = Style::default(); 303 | if let Some(fg) = parse_color(&foreground.0) { 304 | style = style.fg(fg); 305 | } 306 | if let Some(bg) = parse_color(&background.0) { 307 | style = style.bg(bg); 308 | } 309 | style = style.add_modifier(foreground.1 | background.1); 310 | style 311 | } 312 | 313 | fn process_color_string(color_str: &str) -> (String, Modifier) { 314 | let color = color_str 315 | .replace("grey", "gray") 316 | .replace("bright ", "") 317 | .replace("bold ", "") 318 | .replace("underline ", "") 319 | .replace("inverse ", ""); 320 | 321 | let mut modifiers = Modifier::empty(); 322 | if color_str.contains("underline") { 323 | modifiers |= Modifier::UNDERLINED; 324 | } 325 | if color_str.contains("bold") { 326 | modifiers |= Modifier::BOLD; 327 | } 328 | if color_str.contains("inverse") { 329 | modifiers |= Modifier::REVERSED; 330 | } 331 | 332 | (color, modifiers) 333 | } 334 | 335 | fn parse_color(s: &str) -> Option { 336 | let s = s.trim_start(); 337 | let s = s.trim_end(); 338 | if s.contains("bright color") { 339 | let s = s.trim_start_matches("bright "); 340 | let c = s.trim_start_matches("color").parse::().unwrap_or_default(); 341 | Some(Color::Indexed(c.wrapping_shl(8))) 342 | } else if s.contains("color") { 343 | let c = s.trim_start_matches("color").parse::().unwrap_or_default(); 344 | Some(Color::Indexed(c)) 345 | } else if s.contains("gray") { 346 | let c = 232 + s.trim_start_matches("gray").parse::().unwrap_or_default(); 347 | Some(Color::Indexed(c)) 348 | } else if s.contains("rgb") { 349 | let red = (s.as_bytes()[3] as char).to_digit(10).unwrap_or_default() as u8; 350 | let green = (s.as_bytes()[4] as char).to_digit(10).unwrap_or_default() as u8; 351 | let blue = (s.as_bytes()[5] as char).to_digit(10).unwrap_or_default() as u8; 352 | let c = 16 + red * 36 + green * 6 + blue; 353 | Some(Color::Indexed(c)) 354 | } else if s == "bold black" { 355 | Some(Color::Indexed(8)) 356 | } else if s == "bold red" { 357 | Some(Color::Indexed(9)) 358 | } else if s == "bold green" { 359 | Some(Color::Indexed(10)) 360 | } else if s == "bold yellow" { 361 | Some(Color::Indexed(11)) 362 | } else if s == "bold blue" { 363 | Some(Color::Indexed(12)) 364 | } else if s == "bold magenta" { 365 | Some(Color::Indexed(13)) 366 | } else if s == "bold cyan" { 367 | Some(Color::Indexed(14)) 368 | } else if s == "bold white" { 369 | Some(Color::Indexed(15)) 370 | } else if s == "black" { 371 | Some(Color::Indexed(0)) 372 | } else if s == "red" { 373 | Some(Color::Indexed(1)) 374 | } else if s == "green" { 375 | Some(Color::Indexed(2)) 376 | } else if s == "yellow" { 377 | Some(Color::Indexed(3)) 378 | } else if s == "blue" { 379 | Some(Color::Indexed(4)) 380 | } else if s == "magenta" { 381 | Some(Color::Indexed(5)) 382 | } else if s == "cyan" { 383 | Some(Color::Indexed(6)) 384 | } else if s == "white" { 385 | Some(Color::Indexed(7)) 386 | } else { 387 | None 388 | } 389 | } 390 | 391 | #[cfg(test)] 392 | mod tests { 393 | use pretty_assertions::assert_eq; 394 | 395 | use super::*; 396 | 397 | #[test] 398 | fn test_parse_style_default() { 399 | let style = parse_style(""); 400 | assert_eq!(style, Style::default()); 401 | } 402 | 403 | #[test] 404 | fn test_parse_style_foreground() { 405 | let style = parse_style("red"); 406 | assert_eq!(style.fg, Some(Color::Indexed(1))); 407 | } 408 | 409 | #[test] 410 | fn test_parse_style_background() { 411 | let style = parse_style("on blue"); 412 | assert_eq!(style.bg, Some(Color::Indexed(4))); 413 | } 414 | 415 | #[test] 416 | fn test_parse_style_modifiers() { 417 | let style = parse_style("underline red on blue"); 418 | assert_eq!(style.fg, Some(Color::Indexed(1))); 419 | assert_eq!(style.bg, Some(Color::Indexed(4))); 420 | } 421 | 422 | #[test] 423 | fn test_process_color_string() { 424 | let (color, modifiers) = process_color_string("underline bold inverse gray"); 425 | assert_eq!(color, "gray"); 426 | assert!(modifiers.contains(Modifier::UNDERLINED)); 427 | assert!(modifiers.contains(Modifier::BOLD)); 428 | assert!(modifiers.contains(Modifier::REVERSED)); 429 | } 430 | 431 | #[test] 432 | fn test_parse_color_rgb() { 433 | let color = parse_color("rgb123"); 434 | let expected = 16 + 1 * 36 + 2 * 6 + 3; 435 | assert_eq!(color, Some(Color::Indexed(expected))); 436 | } 437 | 438 | #[test] 439 | fn test_parse_color_unknown() { 440 | let color = parse_color("unknown"); 441 | assert_eq!(color, None); 442 | } 443 | 444 | // #[test] 445 | // fn test_config() -> Result<()> { 446 | // let c = Config::new()?; 447 | // assert_eq!( 448 | // c.keybindings.get(&Mode::Home).unwrap().get(&parse_key_sequence("").unwrap_or_default()).unwrap(), 449 | // &Action::Quit 450 | // ); 451 | // Ok(()) 452 | // } 453 | 454 | #[test] 455 | fn test_simple_keys() { 456 | assert_eq!(parse_key_event("a").unwrap(), KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())); 457 | 458 | assert_eq!(parse_key_event("enter").unwrap(), KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())); 459 | 460 | assert_eq!(parse_key_event("esc").unwrap(), KeyEvent::new(KeyCode::Esc, KeyModifiers::empty())); 461 | } 462 | 463 | #[test] 464 | fn test_with_modifiers() { 465 | assert_eq!(parse_key_event("ctrl-a").unwrap(), KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)); 466 | 467 | assert_eq!(parse_key_event("alt-enter").unwrap(), KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT)); 468 | 469 | assert_eq!(parse_key_event("shift-esc").unwrap(), KeyEvent::new(KeyCode::Esc, KeyModifiers::SHIFT)); 470 | } 471 | 472 | #[test] 473 | fn test_multiple_modifiers() { 474 | assert_eq!( 475 | parse_key_event("ctrl-alt-a").unwrap(), 476 | KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL | KeyModifiers::ALT) 477 | ); 478 | 479 | assert_eq!( 480 | parse_key_event("ctrl-shift-enter").unwrap(), 481 | KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL | KeyModifiers::SHIFT) 482 | ); 483 | } 484 | 485 | #[test] 486 | fn test_reverse_multiple_modifiers() { 487 | assert_eq!( 488 | key_event_to_string(&KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL | KeyModifiers::ALT)), 489 | "ctrl-alt-a".to_string() 490 | ); 491 | } 492 | 493 | #[test] 494 | fn test_invalid_keys() { 495 | assert!(parse_key_event("invalid-key").is_err()); 496 | assert!(parse_key_event("ctrl-invalid-key").is_err()); 497 | } 498 | 499 | #[test] 500 | fn test_case_insensitivity() { 501 | assert_eq!(parse_key_event("CTRL-a").unwrap(), KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)); 502 | 503 | assert_eq!(parse_key_event("AlT-eNtEr").unwrap(), KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT)); 504 | } 505 | } 506 | -------------------------------------------------------------------------------- /src/enums.rs: -------------------------------------------------------------------------------- 1 | use crate::components::{discovery::ScannedIp, ports::ScannedIpPorts}; 2 | use chrono::{DateTime, Local}; 3 | use pnet::{ 4 | packet::{ 5 | arp::{ArpOperation, ArpOperations}, 6 | icmp::IcmpType, 7 | icmpv6::Icmpv6Type, 8 | }, 9 | util::MacAddr, 10 | }; 11 | use std::net::{IpAddr, Ipv4Addr}; 12 | use strum::{Display, EnumCount, EnumIter, FromRepr}; 13 | 14 | #[derive(Debug, Clone, PartialEq)] 15 | pub struct ExportData { 16 | pub scanned_ips: Vec, 17 | pub scanned_ports: Vec, 18 | pub arp_packets: Vec<(DateTime, PacketsInfoTypesEnum)>, 19 | pub udp_packets: Vec<(DateTime, PacketsInfoTypesEnum)>, 20 | pub tcp_packets: Vec<(DateTime, PacketsInfoTypesEnum)>, 21 | pub icmp_packets: Vec<(DateTime, PacketsInfoTypesEnum)>, 22 | pub icmp6_packets: Vec<(DateTime, PacketsInfoTypesEnum)>, 23 | } 24 | 25 | #[derive(Debug, Clone, PartialEq)] 26 | pub struct UDPPacketInfo { 27 | pub interface_name: String, 28 | pub source: IpAddr, 29 | pub source_port: u16, 30 | pub destination: IpAddr, 31 | pub destination_port: u16, 32 | pub length: usize, 33 | pub raw_str: String, 34 | } 35 | 36 | #[derive(Debug, Clone, PartialEq)] 37 | pub struct TCPPacketInfo { 38 | pub interface_name: String, 39 | pub source: IpAddr, 40 | pub source_port: u16, 41 | pub destination: IpAddr, 42 | pub destination_port: u16, 43 | pub length: usize, 44 | pub raw_str: String, 45 | } 46 | 47 | #[derive(Debug, Clone, PartialEq)] 48 | pub struct ARPPacketInfo { 49 | pub interface_name: String, 50 | pub source_mac: MacAddr, 51 | pub source_ip: Ipv4Addr, 52 | pub destination_mac: MacAddr, 53 | pub destination_ip: Ipv4Addr, 54 | pub operation: ArpOperation, 55 | pub raw_str: String, 56 | } 57 | 58 | #[derive(Debug, Clone, PartialEq)] 59 | pub struct ICMPPacketInfo { 60 | pub interface_name: String, 61 | pub source: IpAddr, 62 | pub destination: IpAddr, 63 | pub seq: u16, 64 | pub id: u16, 65 | pub icmp_type: IcmpType, 66 | pub raw_str: String, 67 | } 68 | 69 | #[derive(Debug, Clone, PartialEq)] 70 | pub struct ICMP6PacketInfo { 71 | pub interface_name: String, 72 | pub source: IpAddr, 73 | pub destination: IpAddr, 74 | pub icmp_type: Icmpv6Type, 75 | pub raw_str: String, 76 | } 77 | 78 | #[derive(Debug, Clone, PartialEq)] 79 | pub enum PacketsInfoTypesEnum { 80 | Arp(ARPPacketInfo), 81 | Tcp(TCPPacketInfo), 82 | Udp(UDPPacketInfo), 83 | Icmp(ICMPPacketInfo), 84 | Icmp6(ICMP6PacketInfo), 85 | } 86 | 87 | #[derive(Default, Clone, Copy, Display, FromRepr, EnumIter, EnumCount, PartialEq, Debug)] 88 | pub enum TabsEnum { 89 | #[default] 90 | #[strum(to_string = "Discovery")] 91 | Discovery, 92 | #[strum(to_string = "Packets")] 93 | Packets, 94 | #[strum(to_string = "Ports")] 95 | Ports, 96 | #[strum(to_string = "Traffic")] 97 | Traffic, 98 | } 99 | 100 | #[derive(Default, Clone, Copy, Display, FromRepr, EnumIter, EnumCount, PartialEq, Debug)] 101 | pub enum PacketTypeEnum { 102 | #[default] 103 | #[strum(to_string = "All")] 104 | All, 105 | #[strum(to_string = "ARP")] 106 | Arp, 107 | #[strum(to_string = "TCP")] 108 | Tcp, 109 | #[strum(to_string = "UDP")] 110 | Udp, 111 | #[strum(to_string = "ICMP")] 112 | Icmp, 113 | #[strum(to_string = "ICMP6")] 114 | Icmp6, 115 | } 116 | 117 | #[derive(Clone, Debug, PartialEq)] 118 | pub enum PortsScanState { 119 | Waiting, 120 | Scanning, 121 | Done, 122 | } 123 | 124 | impl PacketTypeEnum { 125 | pub fn previous(&self) -> Self { 126 | let current_index: usize = *self as usize; 127 | let previous_index = current_index.saturating_sub(1); 128 | Self::from_repr(previous_index).unwrap_or(*self) 129 | } 130 | 131 | pub fn next(&self) -> Self { 132 | let current_index = *self as usize; 133 | let next_index = current_index.saturating_add(1); 134 | Self::from_repr(next_index).unwrap_or(*self) 135 | } 136 | } 137 | 138 | pub const COMMON_PORTS: &[u16] = &[ 139 | 5601, 9300, 80, 23, 443, 21, 22, 25, 3389, 110, 445, 139, 143, 53, 135, 3306, 8080, 1723, 111, 140 | 995, 993, 5900, 1025, 587, 8888, 199, 1720, 465, 548, 113, 81, 6001, 10000, 514, 5060, 179, 141 | 1026, 2000, 8443, 8000, 32768, 554, 26, 1433, 49152, 2001, 515, 8008, 49154, 1027, 5666, 646, 142 | 5000, 5631, 631, 49153, 8081, 2049, 88, 79, 5800, 106, 2121, 1110, 49155, 6000, 513, 990, 5357, 143 | 427, 49156, 543, 544, 5101, 144, 7, 389, 8009, 3128, 444, 9999, 5009, 7070, 5190, 3000, 5432, 144 | 1900, 3986, 13, 1029, 9, 5051, 6646, 49157, 1028, 873, 1755, 2717, 4899, 9100, 119, 37, 1000, 145 | 3001, 5001, 82, 10010, 1030, 9090, 2107, 1024, 2103, 6004, 1801, 5050, 19, 8031, 1041, 255, 146 | 2967, 1049, 1048, 1053, 3703, 1056, 1065, 1064, 1054, 17, 808, 3689, 1031, 1044, 1071, 5901, 147 | 9102, 100, 8010, 2869, 1039, 5120, 4001, 9000, 2105, 636, 1038, 2601, 7000, 1, 1066, 1069, 625, 148 | 311, 280, 254, 4000, 5003, 1761, 2002, 2005, 1998, 1032, 1050, 6112, 3690, 1521, 2161, 6002, 149 | 1080, 2401, 4045, 902, 7937, 787, 1058, 2383, 32771, 1033, 1040, 1059, 50000, 5555, 10001, 150 | 1494, 2301, 593, 3, 3268, 7938, 1234, 1022, 1035, 9001, 1074, 8002, 1036, 1037, 464, 1935, 151 | 6666, 2003, 497, 6543, 1352, 24, 3269, 1111, 407, 500, 20, 2006, 3260, 1034, 15000, 1218, 4444, 152 | 264, 2004, 33, 1042, 42510, 999, 3052, 1023, 1068, 222, 888, 7100, 563, 1717, 2008, 992, 32770, 153 | 32772, 7001, 8082, 2007, 5550, 2009, 1043, 512, 5801, 7019, 2701, 50001, 1700, 4662, 2065, 154 | 2010, 42, 9535, 2602, 3333, 161, 5100, 2604, 4002, 5002, 8192, 6789, 8194, 6059, 1047, 8193, 155 | 2702, 9595, 1051, 9594, 9593, 16993, 16992, 5226, 5225, 32769, 1052, 1055, 3283, 1062, 9415, 156 | 8701, 8652, 8651, 8089, 65389, 65000, 64680, 64623, 55600, 55555, 52869, 35500, 33354, 23502, 157 | 20828, 1311, 1060, 4443, 1067, 13782, 5902, 366, 9050, 1002, 85, 5500, 1864, 5431, 1863, 8085, 158 | 51103, 49999, 45100, 10243, 49, 6667, 90, 27000, 1503, 6881, 8021, 1500, 340, 5566, 8088, 2222, 159 | 9071, 8899, 1501, 5102, 32774, 32773, 9101, 6005, 9876, 5679, 163, 648, 146, 1666, 901, 83, 160 | 9207, 8001, 8083, 5004, 3476, 8084, 5214, 14238, 12345, 912, 30, 2605, 2030, 6, 541, 8007, 161 | 3005, 4, 1248, 2500, 880, 306, 4242, 1097, 9009, 2525, 1086, 1088, 8291, 52822, 6101, 900, 162 | 7200, 2809, 800, 32775, 12000, 1083, 211, 987, 705, 20005, 711, 13783, 6969, 3071, 3801, 3017, 163 | 8873, 5269, 5222, 1046, 1085, 5987, 5989, 5988, 2190, 11967, 8600, 8087, 30000, 9010, 7741, 164 | 3367, 3766, 7627, 14000, 3031, 1099, 1098, 6580, 2718, 15002, 4129, 6901, 3827, 3580, 2144, 165 | 8181, 9900, 1718, 9080, 2135, 2811, 1045, 2399, 1148, 10002, 9002, 8086, 3998, 2607, 11110, 166 | 4126, 2875, 5718, 9011, 5911, 5910, 9618, 2381, 1096, 3300, 3351, 1073, 8333, 15660, 6123, 167 | 3784, 5633, 3211, 1078, 3659, 3551, 2100, 16001, 3325, 3323, 2260, 2160, 1104, 9968, 9503, 168 | 9502, 9485, 9290, 9220, 8994, 8649, 8222, 7911, 7625, 7106, 65129, 63331, 6156, 6129, 60020, 169 | 5962, 5961, 5960, 5959, 5925, 5877, 5825, 5810, 58080, 57294, 50800, 50006, 50003, 49160, 170 | 49159, 49158, 48080, 40193, 34573, 34572, 34571, 3404, 33899, 3301, 32782, 32781, 31038, 30718, 171 | 28201, 27715, 25734, 24800, 22939, 21571, 20221, 20031, 19842, 19801, 19101, 17988, 1783, 172 | 16018, 16016, 15003, 14442, 13456, 10629, 10628, 10626, 10621, 10617, 10616, 10566, 10025, 173 | 10024, 10012, 1169, 5030, 5414, 1057, 6788, 1947, 1094, 1075, 1108, 4003, 1081, 1093, 4449, 174 | 1687, 1840, 1100, 1063, 1061, 1107, 1106, 9500, 20222, 7778, 1077, 1310, 2119, 2492, 1070, 175 | 20000, 8400, 1272, 6389, 7777, 1072, 1079, 1082, 8402, 691, 89, 32776, 1999, 1001, 212, 2020, 176 | 7002, 2998, 6003, 50002, 3372, 898, 5510, 32, 2033, 5903, 99, 749, 425, 43, 5405, 6106, 13722, 177 | 6502, 7007, 458, 1580, 9666, 8100, 3737, 5298, 1152, 8090, 2191, 3011, 5200, 3851, 3371, 3370, 178 | 3369, 7402, 5054, 3918, 3077, 7443, 3493, 3828, 1186, 2179, 1183, 19315, 19283, 5963, 3995, 179 | 1124, 8500, 1089, 10004, 2251, 1087, 5280, 3871, 3030, 62078, 9091, 4111, 1334, 3261, 2522, 180 | 5859, 1247, 9944, 9943, 9877, 9110, 8654, 8254, 8180, 8011, 7512, 7435, 7103, 61900, 61532, 181 | 5922, 5915, 5904, 5822, 56738, 55055, 51493, 50636, 50389, 49175, 49165, 49163, 3546, 32784, 182 | 27355, 27353, 27352, 24444, 19780, 18988, 16012, 15742, 10778, 4006, 2126, 4446, 3880, 1782, 183 | 1296, 9998, 32777, 9040, 32779, 1021, 2021, 666, 32778, 616, 700, 1524, 1112, 5802, 4321, 545, 184 | 49400, 84, 38292, 2040, 3006, 2111, 32780, 1084, 1600, 2048, 2638, 9111, 6547, 6699, 16080, 185 | 2106, 667, 6007, 1533, 5560, 1443, 720, 2034, 555, 801, 3826, 3814, 7676, 3869, 1138, 6567, 186 | 10003, 3221, 6025, 2608, 9200, 7025, 11111, 4279, 3527, 1151, 8300, 6689, 9878, 8200, 10009, 187 | 8800, 5730, 2394, 2393, 2725, 5061, 6566, 9081, 5678, 3800, 4550, 5080, 1201, 3168, 1862, 1114, 188 | 3905, 6510, 8383, 3914, 3971, 3809, 5033, 3517, 4900, 9418, 2909, 3878, 8042, 1091, 1090, 3920, 189 | 3945, 1175, 3390, 3889, 1131, 8292, 1119, 5087, 7800, 4848, 16000, 3324, 3322, 1117, 5221, 190 | 4445, 9917, 9575, 9099, 9003, 8290, 8099, 8093, 8045, 7921, 7920, 7496, 6839, 6792, 6779, 6692, 191 | 6565, 60443, 5952, 5950, 5907, 5906, 5862, 5850, 5815, 5811, 57797, 56737, 5544, 55056, 5440, 192 | 54328, 54045, 52848, 52673, 50500, 50300, 49176, 49167, 49161, 44501, 44176, 41511, 40911, 193 | 32785, 32783, 30951, 27356, 26214, 25735, 19350, 18101, 18040, 17877, 16113, 15004, 14441, 194 | 12265, 12174, 10215, 10180, 4567, 6100, 4004, 4005, 8022, 9898, 7999, 1271, 1199, 3003, 1122, 195 | 2323, 2022, 4224, 617, 777, 417, 714, 6346, 981, 722, 1009, 4998, 70, 1076, 5999, 10082, 765, 196 | 301, 524, 668, 2041, 259, 1984, 2068, 6009, 1417, 1434, 44443, 7004, 1007, 4343, 416, 2038, 197 | 4125, 1461, 9103, 6006, 109, 911, 726, 1010, 2046, 2035, 7201, 687, 2013, 481, 903, 125, 6669, 198 | 6668, 1455, 683, 1011, 2043, 2047, 256, 31337, 9929, 5998, 406, 44442, 783, 843, 2042, 2045, 199 | 1875, 1556, 5938, 8675, 1277, 3972, 3968, 3870, 6068, 3050, 5151, 3792, 8889, 5063, 1198, 1192, 200 | 4040, 1145, 6060, 6051, 3916, 7272, 9443, 9444, 7024, 13724, 4252, 4200, 1141, 1233, 8765, 201 | 3963, 1137, 9191, 3808, 8686, 3981, 9988, 1163, 4164, 3820, 6481, 3731, 40000, 2710, 3852, 202 | 3849, 3853, 5081, 8097, 3944, 1287, 3863, 4555, 4430, 7744, 1812, 7913, 1166, 1164, 1165, 203 | 10160, 8019, 4658, 7878, 1259, 1092, 10008, 3304, 3307, 204 | ]; 205 | -------------------------------------------------------------------------------- /src/layout.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{prelude::*, widgets::*}; 2 | 3 | const VERTICAL_TOP_PERCENT: u16 = 40; 4 | const VERTICAL_BOTTOM_PERCENT: u16 = 60; 5 | 6 | const HORIZONTAL_SPLIT: u16 = 50; 7 | 8 | const VERTICAL_CONSTRAINTS: [Constraint; 3] = [ 9 | Constraint::Percentage(VERTICAL_TOP_PERCENT), 10 | Constraint::Length(3), 11 | Constraint::Percentage(VERTICAL_BOTTOM_PERCENT), 12 | ]; 13 | 14 | pub const HORIZONTAL_CONSTRAINTS: [Constraint; 2] = [ 15 | Constraint::Percentage(HORIZONTAL_SPLIT), 16 | Constraint::Percentage(HORIZONTAL_SPLIT), 17 | ]; 18 | 19 | pub struct VerticalLayoutRects { 20 | pub top: Rect, 21 | pub tabs: Rect, 22 | pub bottom: Rect, 23 | } 24 | 25 | pub struct HorizontalLayoutRects { 26 | pub left: Rect, 27 | pub right: Rect, 28 | } 29 | 30 | pub fn get_vertical_layout(area: Rect) -> VerticalLayoutRects { 31 | let layout = Layout::vertical(VERTICAL_CONSTRAINTS).split(area); 32 | VerticalLayoutRects { 33 | top: layout[0], 34 | tabs: layout[1], 35 | bottom: layout[2], 36 | } 37 | } 38 | 39 | pub fn get_horizontal_layout(area: Rect) -> HorizontalLayoutRects { 40 | let layout = Layout::horizontal(HORIZONTAL_CONSTRAINTS).split(area); 41 | HorizontalLayoutRects { 42 | left: layout[0], 43 | right: layout[1], 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![allow(unused_imports)] 3 | #![allow(unused_variables)] 4 | 5 | pub mod action; 6 | pub mod app; 7 | pub mod cli; 8 | pub mod components; 9 | pub mod config; 10 | pub mod mode; 11 | pub mod tui; 12 | pub mod utils; 13 | pub mod enums; 14 | pub mod layout; 15 | pub mod widgets; 16 | 17 | use clap::Parser; 18 | use cli::Cli; 19 | use color_eyre::eyre::Result; 20 | 21 | use crate::{ 22 | app::App, 23 | utils::{initialize_logging, initialize_panic_handler, version}, 24 | }; 25 | 26 | async fn tokio_main() -> Result<()> { 27 | initialize_logging()?; 28 | 29 | initialize_panic_handler()?; 30 | 31 | let args = Cli::parse(); 32 | let mut app = App::new(args.tick_rate, args.frame_rate)?; 33 | app.run().await?; 34 | 35 | Ok(()) 36 | } 37 | 38 | #[tokio::main] 39 | async fn main() -> Result<()> { 40 | if let Err(e) = tokio_main().await { 41 | eprintln!("{} error: Something went wrong", env!("CARGO_PKG_NAME")); 42 | Err(e) 43 | } else { 44 | Ok(()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/mode.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use ratatui::style::Color; 3 | 4 | #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 5 | pub enum Mode { 6 | #[default] 7 | Normal, 8 | Input, 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/tui.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ops::{Deref, DerefMut}, 3 | time::Duration, 4 | }; 5 | 6 | use color_eyre::eyre::Result; 7 | use crossterm::{ 8 | cursor, 9 | event::{ 10 | DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, Event as CrosstermEvent, 11 | KeyEvent, KeyEventKind, MouseEvent, 12 | }, 13 | terminal::{EnterAlternateScreen, LeaveAlternateScreen}, 14 | }; 15 | use futures::{FutureExt, StreamExt}; 16 | use ratatui::backend::CrosstermBackend as Backend; 17 | use serde::{Deserialize, Serialize}; 18 | use tokio::{ 19 | sync::mpsc::{self, UnboundedReceiver, UnboundedSender}, 20 | task::JoinHandle, 21 | }; 22 | use tokio_util::sync::CancellationToken; 23 | 24 | pub type IO = std::io::Stdout; 25 | pub fn io() -> IO { 26 | std::io::stdout() 27 | } 28 | pub type Frame<'a> = ratatui::Frame<'a>; 29 | 30 | #[derive(Clone, Debug, Serialize, Deserialize)] 31 | pub enum Event { 32 | Init, 33 | Quit, 34 | Error, 35 | Closed, 36 | Tick, 37 | Render, 38 | FocusGained, 39 | FocusLost, 40 | Paste(String), 41 | Key(KeyEvent), 42 | Mouse(MouseEvent), 43 | Resize(u16, u16), 44 | } 45 | 46 | pub struct Tui { 47 | pub terminal: ratatui::Terminal>, 48 | pub task: JoinHandle<()>, 49 | pub cancellation_token: CancellationToken, 50 | pub event_rx: UnboundedReceiver, 51 | pub event_tx: UnboundedSender, 52 | pub frame_rate: f64, 53 | pub tick_rate: f64, 54 | pub mouse: bool, 55 | pub paste: bool, 56 | } 57 | 58 | impl Tui { 59 | pub fn new() -> Result { 60 | let tick_rate = 4.0; 61 | let frame_rate = 60.0; 62 | let terminal = ratatui::Terminal::new(Backend::new(io()))?; 63 | let (event_tx, event_rx) = mpsc::unbounded_channel(); 64 | let cancellation_token = CancellationToken::new(); 65 | let task = tokio::spawn(async {}); 66 | let mouse = false; 67 | let paste = false; 68 | Ok(Self { terminal, task, cancellation_token, event_rx, event_tx, frame_rate, tick_rate, mouse, paste }) 69 | } 70 | 71 | pub fn tick_rate(mut self, tick_rate: f64) -> Self { 72 | self.tick_rate = tick_rate; 73 | self 74 | } 75 | 76 | pub fn frame_rate(mut self, frame_rate: f64) -> Self { 77 | self.frame_rate = frame_rate; 78 | self 79 | } 80 | 81 | pub fn mouse(mut self, mouse: bool) -> Self { 82 | self.mouse = mouse; 83 | self 84 | } 85 | 86 | pub fn paste(mut self, paste: bool) -> Self { 87 | self.paste = paste; 88 | self 89 | } 90 | 91 | pub fn start(&mut self) { 92 | let tick_delay = std::time::Duration::from_secs_f64(1.0 / self.tick_rate); 93 | let render_delay = std::time::Duration::from_secs_f64(1.0 / self.frame_rate); 94 | self.cancel(); 95 | self.cancellation_token = CancellationToken::new(); 96 | let _cancellation_token = self.cancellation_token.clone(); 97 | let _event_tx = self.event_tx.clone(); 98 | self.task = tokio::spawn(async move { 99 | let mut reader = crossterm::event::EventStream::new(); 100 | let mut tick_interval = tokio::time::interval(tick_delay); 101 | let mut render_interval = tokio::time::interval(render_delay); 102 | _event_tx.send(Event::Init).unwrap(); 103 | loop { 104 | let tick_delay = tick_interval.tick(); 105 | let render_delay = render_interval.tick(); 106 | let crossterm_event = reader.next().fuse(); 107 | tokio::select! { 108 | _ = _cancellation_token.cancelled() => { 109 | break; 110 | } 111 | maybe_event = crossterm_event => { 112 | match maybe_event { 113 | Some(Ok(evt)) => { 114 | match evt { 115 | CrosstermEvent::Key(key) => { 116 | if key.kind == KeyEventKind::Press { 117 | _event_tx.send(Event::Key(key)).unwrap(); 118 | } 119 | }, 120 | CrosstermEvent::Mouse(mouse) => { 121 | _event_tx.send(Event::Mouse(mouse)).unwrap(); 122 | }, 123 | CrosstermEvent::Resize(x, y) => { 124 | _event_tx.send(Event::Resize(x, y)).unwrap(); 125 | }, 126 | CrosstermEvent::FocusLost => { 127 | _event_tx.send(Event::FocusLost).unwrap(); 128 | }, 129 | CrosstermEvent::FocusGained => { 130 | _event_tx.send(Event::FocusGained).unwrap(); 131 | }, 132 | CrosstermEvent::Paste(s) => { 133 | _event_tx.send(Event::Paste(s)).unwrap(); 134 | }, 135 | } 136 | } 137 | Some(Err(_)) => { 138 | _event_tx.send(Event::Error).unwrap(); 139 | } 140 | None => {}, 141 | } 142 | }, 143 | _ = tick_delay => { 144 | _event_tx.send(Event::Tick).unwrap(); 145 | }, 146 | _ = render_delay => { 147 | _event_tx.send(Event::Render).unwrap(); 148 | }, 149 | } 150 | } 151 | }); 152 | } 153 | 154 | pub fn stop(&self) -> Result<()> { 155 | self.cancel(); 156 | let mut counter = 0; 157 | while !self.task.is_finished() { 158 | std::thread::sleep(Duration::from_millis(1)); 159 | counter += 1; 160 | if counter > 50 { 161 | self.task.abort(); 162 | } 163 | if counter > 100 { 164 | log::error!("Failed to abort task in 100 milliseconds for unknown reason"); 165 | break; 166 | } 167 | } 168 | Ok(()) 169 | } 170 | 171 | pub fn enter(&mut self) -> Result<()> { 172 | crossterm::terminal::enable_raw_mode()?; 173 | crossterm::execute!(io(), EnterAlternateScreen, cursor::Hide)?; 174 | if self.mouse { 175 | crossterm::execute!(io(), EnableMouseCapture)?; 176 | } 177 | if self.paste { 178 | crossterm::execute!(io(), EnableBracketedPaste)?; 179 | } 180 | self.start(); 181 | Ok(()) 182 | } 183 | 184 | pub fn exit(&mut self) -> Result<()> { 185 | self.stop()?; 186 | if crossterm::terminal::is_raw_mode_enabled()? { 187 | self.flush()?; 188 | if self.paste { 189 | crossterm::execute!(io(), DisableBracketedPaste)?; 190 | } 191 | if self.mouse { 192 | crossterm::execute!(io(), DisableMouseCapture)?; 193 | } 194 | crossterm::execute!(io(), LeaveAlternateScreen, cursor::Show)?; 195 | crossterm::terminal::disable_raw_mode()?; 196 | } 197 | Ok(()) 198 | } 199 | 200 | pub fn cancel(&self) { 201 | self.cancellation_token.cancel(); 202 | } 203 | 204 | pub fn suspend(&mut self) -> Result<()> { 205 | self.exit()?; 206 | #[cfg(not(windows))] 207 | signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?; 208 | Ok(()) 209 | } 210 | 211 | pub fn resume(&mut self) -> Result<()> { 212 | self.enter()?; 213 | Ok(()) 214 | } 215 | 216 | pub async fn next(&mut self) -> Option { 217 | self.event_rx.recv().await 218 | } 219 | } 220 | 221 | impl Deref for Tui { 222 | type Target = ratatui::Terminal>; 223 | 224 | fn deref(&self) -> &Self::Target { 225 | &self.terminal 226 | } 227 | } 228 | 229 | impl DerefMut for Tui { 230 | fn deref_mut(&mut self) -> &mut Self::Target { 231 | &mut self.terminal 232 | } 233 | } 234 | 235 | impl Drop for Tui { 236 | fn drop(&mut self) { 237 | self.exit().unwrap(); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | use std::path::PathBuf; 3 | 4 | use cidr::Ipv4Cidr; 5 | use color_eyre::eyre::Result; 6 | use directories::ProjectDirs; 7 | use human_panic::metadata; 8 | use lazy_static::lazy_static; 9 | use std::net::Ipv4Addr; 10 | use tracing::error; 11 | use tracing_error::ErrorLayer; 12 | use tracing_subscriber::{ 13 | self, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, Layer, 14 | }; 15 | 16 | use crate::components::sniff::IPTraffic; 17 | 18 | pub static GIT_COMMIT_HASH: &'static str = env!("_GIT_INFO"); 19 | 20 | lazy_static! { 21 | pub static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase().to_string(); 22 | pub static ref DATA_FOLDER: Option = 23 | std::env::var(format!("{}_DATA", PROJECT_NAME.clone())) 24 | .ok() 25 | .map(PathBuf::from); 26 | pub static ref CONFIG_FOLDER: Option = 27 | std::env::var(format!("{}_CONFIG", PROJECT_NAME.clone())) 28 | .ok() 29 | .map(PathBuf::from); 30 | pub static ref LOG_ENV: String = format!("{}_LOGLEVEL", PROJECT_NAME.clone()); 31 | pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME")); 32 | } 33 | 34 | fn project_directory() -> Option { 35 | ProjectDirs::from("com", "kdheepak", env!("CARGO_PKG_NAME")) 36 | } 37 | 38 | pub fn get_ips4_from_cidr(cidr: Ipv4Cidr) -> Vec { 39 | let mut ips = Vec::new(); 40 | for ip in cidr.iter() { 41 | ips.push(ip.address()); 42 | } 43 | ips 44 | } 45 | 46 | pub fn count_ipv4_net_length(net_length: u32) -> u32 { 47 | 2u32.pow(32 - net_length) 48 | } 49 | 50 | pub fn count_traffic_total(traffic: &[IPTraffic]) -> (f64, f64) { 51 | let mut download = 0.0; 52 | let mut upload = 0.0; 53 | for ip in traffic.iter() { 54 | download += ip.download; 55 | upload += ip.upload; 56 | } 57 | (download, upload) 58 | } 59 | 60 | #[derive(Clone, Debug)] 61 | pub struct MaxSizeVec { 62 | p_vec: Vec, 63 | max_len: usize, 64 | } 65 | 66 | impl MaxSizeVec { 67 | pub fn new(max_len: usize) -> Self { 68 | Self { 69 | p_vec: Vec::with_capacity(max_len), 70 | max_len, 71 | } 72 | } 73 | 74 | pub fn push(&mut self, item: T) { 75 | if self.p_vec.len() >= self.max_len { 76 | self.p_vec.pop(); 77 | } 78 | self.p_vec.insert(0, item); 79 | } 80 | 81 | pub fn get_vec(&self) -> &Vec { 82 | &self.p_vec 83 | } 84 | } 85 | 86 | pub fn bytes_convert(num: f64) -> String { 87 | let num = num.abs(); 88 | let units = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; 89 | if num < 1_f64 { 90 | return format!("{}{}", num, "B"); 91 | } 92 | let delimiter = 1000_f64; 93 | let exponent = cmp::min( 94 | (num.ln() / delimiter.ln()).floor() as i32, 95 | (units.len() - 1) as i32, 96 | ); 97 | let pretty_bytes = format!("{:.2}", num / delimiter.powi(exponent)) 98 | .parse::() 99 | .unwrap() 100 | * 1_f64; 101 | let unit = units[exponent as usize]; 102 | format!("{}{}", pretty_bytes, unit) 103 | } 104 | 105 | pub fn initialize_panic_handler() -> Result<()> { 106 | let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default() 107 | .panic_section(format!( 108 | "This is a bug. Consider reporting it at {}", 109 | env!("CARGO_PKG_REPOSITORY") 110 | )) 111 | .capture_span_trace_by_default(false) 112 | .display_location_section(false) 113 | .display_env_section(false) 114 | .into_hooks(); 115 | eyre_hook.install()?; 116 | std::panic::set_hook(Box::new(move |panic_info| { 117 | if let Ok(mut t) = crate::tui::Tui::new() { 118 | if let Err(r) = t.exit() { 119 | error!("Unable to exit Terminal: {:?}", r); 120 | } 121 | } 122 | 123 | #[cfg(not(debug_assertions))] 124 | { 125 | use human_panic::{handle_dump, print_msg, Metadata}; 126 | let meta = metadata!() 127 | .authors("Chleba ") 128 | .homepage("https://github.com/Chleba/netscanner") 129 | .support("https://github.com/Chleba/netscanner/issues"); 130 | 131 | let file_path = handle_dump(&meta, panic_info); 132 | // prints human-panic message 133 | print_msg(file_path, &meta) 134 | .expect("human-panic: printing error message to console failed"); 135 | eprintln!("{}", panic_hook.panic_report(panic_info)); // prints color-eyre stack trace to stderr 136 | } 137 | let msg = format!("{}", panic_hook.panic_report(panic_info)); 138 | log::error!("Error: {}", strip_ansi_escapes::strip_str(msg)); 139 | 140 | #[cfg(debug_assertions)] 141 | { 142 | // Better Panic stacktrace that is only enabled when debugging. 143 | better_panic::Settings::auto() 144 | .most_recent_first(false) 145 | .lineno_suffix(true) 146 | .verbosity(better_panic::Verbosity::Full) 147 | .create_panic_handler()(panic_info); 148 | } 149 | 150 | std::process::exit(libc::EXIT_FAILURE); 151 | })); 152 | Ok(()) 153 | } 154 | 155 | pub fn get_data_dir() -> PathBuf { 156 | let directory = if let Some(s) = DATA_FOLDER.clone() { 157 | s 158 | } else if let Some(proj_dirs) = project_directory() { 159 | proj_dirs.data_local_dir().to_path_buf() 160 | } else { 161 | PathBuf::from(".").join(".data") 162 | }; 163 | directory 164 | } 165 | 166 | pub fn get_config_dir() -> PathBuf { 167 | let directory = if let Some(s) = CONFIG_FOLDER.clone() { 168 | s 169 | } else if let Some(proj_dirs) = project_directory() { 170 | proj_dirs.config_local_dir().to_path_buf() 171 | } else { 172 | PathBuf::from(".").join(".config") 173 | }; 174 | directory 175 | } 176 | 177 | pub fn initialize_logging() -> Result<()> { 178 | let directory = get_data_dir(); 179 | std::fs::create_dir_all(directory.clone())?; 180 | let log_path = directory.join(LOG_FILE.clone()); 181 | let log_file = std::fs::File::create(log_path)?; 182 | std::env::set_var( 183 | "RUST_LOG", 184 | std::env::var("RUST_LOG") 185 | .or_else(|_| std::env::var(LOG_ENV.clone())) 186 | .unwrap_or_else(|_| format!("{}=info", env!("CARGO_CRATE_NAME"))), 187 | ); 188 | let file_subscriber = tracing_subscriber::fmt::layer() 189 | .with_file(true) 190 | .with_line_number(true) 191 | .with_writer(log_file) 192 | .with_target(false) 193 | .with_ansi(false) 194 | .with_filter(tracing_subscriber::filter::EnvFilter::from_default_env()); 195 | tracing_subscriber::registry() 196 | .with(file_subscriber) 197 | .with(ErrorLayer::default()) 198 | .init(); 199 | Ok(()) 200 | } 201 | 202 | /// Similar to the `std::dbg!` macro, but generates `tracing` events rather 203 | /// than printing to stdout. 204 | /// 205 | /// By default, the verbosity level for the generated events is `DEBUG`, but 206 | /// this can be customized. 207 | #[macro_export] 208 | macro_rules! trace_dbg { 209 | (target: $target:expr, level: $level:expr, $ex:expr) => {{ 210 | match $ex { 211 | value => { 212 | tracing::event!(target: $target, $level, ?value, stringify!($ex)); 213 | value 214 | } 215 | } 216 | }}; 217 | (level: $level:expr, $ex:expr) => { 218 | trace_dbg!(target: module_path!(), level: $level, $ex) 219 | }; 220 | (target: $target:expr, $ex:expr) => { 221 | trace_dbg!(target: $target, level: tracing::Level::DEBUG, $ex) 222 | }; 223 | ($ex:expr) => { 224 | trace_dbg!(level: tracing::Level::DEBUG, $ex) 225 | }; 226 | } 227 | 228 | pub fn version() -> String { 229 | let author = clap::crate_authors!(); 230 | 231 | let commit_hash = GIT_COMMIT_HASH; 232 | 233 | // let current_exe_path = PathBuf::from(clap::crate_name!()).display().to_string(); 234 | let config_dir_path = get_config_dir().display().to_string(); 235 | let data_dir_path = get_data_dir().display().to_string(); 236 | 237 | format!( 238 | "\ 239 | {commit_hash} 240 | 241 | Authors: {author} 242 | 243 | Config directory: {config_dir_path} 244 | Data directory: {data_dir_path}" 245 | ) 246 | } 247 | -------------------------------------------------------------------------------- /src/widgets.rs: -------------------------------------------------------------------------------- 1 | 2 | pub mod scroll_traffic; 3 | 4 | -------------------------------------------------------------------------------- /src/widgets/scroll_traffic.rs: -------------------------------------------------------------------------------- 1 | use crate::components::sniff::IPTraffic; 2 | use crate::utils::{bytes_convert, count_traffic_total}; 3 | use color_eyre::owo_colors::OwoColorize; 4 | use ratatui::style::Stylize; 5 | use ratatui::{layout::Size, prelude::*, widgets::*}; 6 | use tui_scrollview::{ScrollView, ScrollViewState}; 7 | 8 | #[derive(Debug)] 9 | pub struct TrafficScroll { 10 | pub traffic_ips: Vec, 11 | } 12 | 13 | impl StatefulWidget for TrafficScroll { 14 | type State = ScrollViewState; 15 | 16 | fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { 17 | let c_size = Size::new(area.width-1, (3 * self.traffic_ips.len()) as u16); 18 | let mut scrollview = ScrollView::new(c_size); 19 | let total = count_traffic_total(&self.traffic_ips); 20 | 21 | for (index, item) in self.traffic_ips.iter().enumerate() { 22 | // -- title 23 | let b_rect = Rect { 24 | x: 1, 25 | y: (index * 3) as u16, 26 | width: area.width - 3, 27 | height: 3, 28 | }; 29 | let b = Block::default() 30 | .borders(Borders::NONE) 31 | .border_style(Style::default().fg(Color::Rgb(100, 100, 100))) 32 | .title_style(Style::default().fg(Color::Blue)) 33 | .title(Line::from(vec![ 34 | format!("{}", item.ip).blue(), 35 | format!(" ({})", item.hostname.clone()).magenta(), 36 | ])); 37 | scrollview.render_widget(b, b_rect); 38 | 39 | // -- download gauge 40 | let gd = LineGauge::default() 41 | .label(Line::from(vec![ 42 | "D:".yellow(), 43 | bytes_convert(item.download).green(), 44 | ])) 45 | .ratio(item.download / total.0) 46 | .filled_style(Style::default().fg(Color::Green)) 47 | // .unfilled_style(Style::default().fg(Color::Rgb(100, 100, 100))); 48 | .unfilled_style(Style::default().fg(Color::Rgb(60, 60, 60))); 49 | let gd_rect = Rect { 50 | x: 1, 51 | y: ((index * 3) + 1) as u16, 52 | width: ((area.width - 2) / 2) - 2, 53 | height: 1, 54 | }; 55 | scrollview.render_widget(gd, gd_rect); 56 | 57 | // -- upload gauge 58 | let gu = LineGauge::default() 59 | .label(Line::from(vec![ 60 | "U:".yellow(), 61 | bytes_convert(item.upload).red(), 62 | ])) 63 | .ratio(item.upload / total.1) 64 | .filled_style(Style::default().fg(Color::Red)) 65 | .unfilled_style(Style::default().fg(Color::Rgb(60, 60, 60))); 66 | let gu_rect = Rect { 67 | x: (area.width - 2) / 2, 68 | y: ((index * 3) + 1) as u16, 69 | width: (area.width - 2) / 2, 70 | height: 1, 71 | }; 72 | scrollview.render_widget(gu, gu_rect); 73 | } 74 | 75 | scrollview.render(area, buf, state); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /traffic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chleba/netscanner/f06f6aaf125dfcf0bc7ee82022c05fa70748c190/traffic.png --------------------------------------------------------------------------------