├── .dockerignore ├── .gitignore ├── .travis.yml ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── Sample ├── sample.pcap └── sample_parsed_data.json └── src ├── args ├── capture.rs ├── mod.rs └── parse.rs ├── lib ├── mod.rs ├── packet_capture.rs └── packet_parse.rs └── main.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | #Added by cargo 13 | # 14 | #already existing elements are commented out 15 | 16 | /target 17 | #**/*.rs.bk 18 | 19 | .idea/ 20 | 21 | Dockerfile -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | #Added by cargo 13 | # 14 | #already existing elements are commented out 15 | 16 | /target 17 | #**/*.rs.bk 18 | 19 | .idea/ 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | 3 | rust: 4 | - stable 5 | 6 | os: 7 | - linux 8 | - osx 9 | 10 | before_script: 11 | - if [ "$TRAVIS_OS_NAME" = 'windows' ]; then choco install windows-sdk-10.0; fi 12 | - rustup component add rustfmt-preview 13 | - rustup component add clippy-preview 14 | 15 | before_cache: | 16 | if [[ "$TRAVIS_OS_NAME" == linux && "$TRAVIS_RUST_VERSION" == nightly ]]; then 17 | RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install cargo-tarpaulin -f 18 | fi 19 | 20 | cache: 21 | directories: 22 | - $HOME/.cargo 23 | 24 | script: 25 | - export CARGO_TARGET_DIR=/tmp/target 26 | - export RUST_BACKTRACE=1 27 | - RUSTFLAGS="-D warnings" cargo check --all || exit 28 | - if [ "$TRAVIS_RUST_VERSION" = 'stable' ]; then cargo fmt --all -- --check; fi 29 | - if [ "$TRAVIS_RUST_VERSION" = 'stable' ]; then cargo clippy --all -- -D warnings; fi 30 | 31 | after_success: | 32 | if [[ "$TRAVIS_OS_NAME" == linux && "$TRAVIS_RUST_VERSION" == nightly ]]; then 33 | cd .. 34 | cargo tarpaulin -r rust -v --all --out Xml 35 | bash <(curl -s https://codecov.io/bash) 36 | fi -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "snoopy" 3 | version = "0.3.2" 4 | authors = ["kanishkarj "] 5 | edition = "2018" 6 | exclude = [ 7 | "Sample/**/*", 8 | ] 9 | description = "A highly configurable multi-threaded packet sniffer and parser." 10 | documentation = "https://github.com/kanishkarj/snoopy" 11 | homepage = "https://github.com/kanishkarj/snoopy" 12 | repository = "https://github.com/kanishkarj/snoopy" 13 | readme = "README.md" 14 | keywords = ["pcap-parser", "packet-capture", "packet-sniffer", "packet-parsing", "command-line-tool"] 15 | categories = ["command-line-utilities", "parsing"] 16 | license-file = "LICENSE" 17 | travis-ci = { repository = "kanishkarj/snoopy", branch = "master" } 18 | maintenance = { status = "actively-developed" } 19 | 20 | [dependencies] 21 | clap = "2.33.0" 22 | pcap = "0.7.0" 23 | pktparse = {version = "0.4.0", features = ["derive"]} 24 | dns-parser = "0.8" 25 | tls-parser = "0.7" 26 | hwaddr = "0.1.2" 27 | nom = "5.0.0" 28 | serde_json = "1.0" 29 | serde = { version = "1.0", features = ["derive"] } 30 | httparse = "1.3.0" 31 | threadpool = "1.0" 32 | num_cpus = "1.0" 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:latest 2 | 3 | RUN apt-get update \ 4 | && apt-get install -y libpcap-dev 5 | 6 | RUN mkdir /snoopy 7 | WORKDIR /snoopy 8 | COPY . /snoopy/ 9 | 10 | RUN cargo build --release; 11 | 12 | ENTRYPOINT ["./target/release/snoopy", "capture", "run"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kanishkar J 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 | # Snoopy 2 | [![Crates.io](https://img.shields.io/crates/v/snoopy.svg)](https://crates.io/crates/snoopy) 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | [![Build Status](https://travis-ci.com/kanishkarj/snoopy.svg?token=jy9kvPoUgCS7spyshyKq&branch=master)](https://travis-ci.com/kanishkarj/snoopy) 5 | 6 | A highly configurable multi-threaded packet sniffer and parser build in rust-lang. 7 | 8 | ## Features 9 | 10 | * Capturing packets and encoding them to Pcap files, or print them onto console. 11 | * While capturing packets, various configuration parameters can be specified. 12 | * Parse Pcap files and print them to console, or extract more verbose information from each packet and store them to JSON file. 13 | * Multi-threaded parsing of packets. 14 | * Filter packets while parsing and capturing. 15 | * Currently supports the following protocols : 16 | * Ethernet 17 | * Ipv4 18 | * Ipv6 19 | * Arp 20 | * Tcp 21 | * Udp 22 | * Dns 23 | * Tls 24 | 25 | the Json file is generated like given below : 26 | 27 | ```Json 28 | 29 | [{ 30 | "Ok": { 31 | "len": 11, 32 | "timestamp": "1234567890.123456", 33 | "headers": [{ 34 | "Tls": { 35 | ... 36 | } 37 | }, 38 | { 39 | "Tcp": { 40 | ... 41 | } 42 | }, { 43 | "Ipv4": { 44 | ... 45 | } 46 | }, { 47 | "Ether": { 48 | ... 49 | } 50 | } 51 | ], 52 | "remaining": [...] 53 | } 54 | }, 55 | ... 56 | ] 57 | 58 | ``` 59 | 60 | ## Installation 61 | 62 | Ensure that you have `libpcap-dev` (ubuntu) or the corresponding package installed on your system. 63 | Run the following commands in the command line inside the folder : 64 | 65 | ```zsh 66 | cargo install snoopy 67 | ``` 68 | 69 | ## Quick-Start 70 | 71 | To Capture packets and print them onto the console : 72 | ```zsh 73 | ➜ sudo snoopy capture run 74 | -------------------- 75 | Sniffing wlp3s0 76 | -------------------- 77 | 78 | 79 | Source IP | Source Port | Dest IP | Dest Port | Protocol | Length | Timestamp | 80 | ------------------------------------------------------------------------------------------------------------------------------------ 81 | 52.216.185.195 | 443 | 10.20.197.103 | 38522 | Tcp | 10078 | 1562310108.589373 82 | 10.20.197.103 | 38522 | 52.216.185.195 | 443 | Tcp | 54 | 1562310108.589468 83 | 52.216.185.195 | 443 | 10.20.197.103 | 38522 | Tcp | 10078 | 1562310108.890490 84 | 10.20.197.103 | 38522 | 52.216.185.195 | 443 | Tcp | 54 | 1562310108.890547 85 | 52.216.185.195 | 443 | 10.20.197.103 | 38522 | Tcp | 1486 | 1562310109.197739 86 | 10.20.197.103 | 38522 | 52.216.185.195 | 443 | Tcp | 54 | 1562310109.197795 87 | 52.216.185.195 | 443 | 10.20.197.103 | 38522 | Tcp | 1486 | 1562310109.197841 88 | 10.20.197.103 | 38522 | 52.216.185.195 | 443 | Tcp | 66 | 1562310109.197865 89 | 52.216.185.195 | 443 | 10.20.197.103 | 38522 | Tcp | 2918 | 1562310109.197887 90 | 10.20.197.103 | 38522 | 52.216.185.195 | 443 | Tcp | 74 | 1562310109.197906 91 | 52.216.185.195 | 443 | 10.20.197.103 | 38522 | Tcp | 1486 | 1562310109.197965 92 | 10.20.197.103 | 38522 | 52.216.185.195 | 443 | Tcp | 74 | 1562310109.197984 93 | 35.154.102.71 | 443 | 10.20.197.103 | 56572 | Tls | 160 | 1562310109.262324 94 | 10.20.197.103 | 56572 | 35.154.102.71 | 443 | Tcp | 66 | 1562310109.262383 95 | ``` 96 | 97 | Capture packets and save them to Pcap files : 98 | 99 | ```shell 100 | ➜ sudo snoopy capture run --timeout 10000 --savefile captured.pcap 101 | ``` 102 | 103 | > Note: For capturing packets the user needs root user permissions to capture network packets. 104 | 105 | Parse Pcap files and print to console: 106 | 107 | ```shell 108 | ➜ snoopy parse ./Sample/captured.pcap 109 | ``` 110 | 111 | Parse Pcap files and print to console (with filters): 112 | 113 | ```shell 114 | ➜ snoopy parse ./Sample/captured.pcap --filter "tcp port 443" 115 | ``` 116 | 117 | > The above command will print all TCP packets with source/destination port 443. 118 | 119 | 120 | Parse Pcap files and save to JSON file: 121 | 122 | ```shell 123 | ➜ snoopy parse ./Sample/captured.pcap --savefile ./parsed.json 124 | ``` 125 | 126 | ## Documentation 127 | 128 | All commands and sub-commands are listed below : 129 | 130 | ```zsh 131 | USAGE: 132 | snoopy [SUBCOMMAND] 133 | 134 | FLAGS: 135 | -h, --help Prints help information 136 | -V, --version Prints version information 137 | 138 | SUBCOMMANDS: 139 | capture Capture packets from interfaces. 140 | help Prints this message or the help of the given subcommand(s) 141 | parse Parse pcap files. 142 | 143 | ``` 144 | ```zsh 145 | USAGE: 146 | snoopy capture [SUBCOMMAND] 147 | 148 | FLAGS: 149 | -h, --help Prints help information 150 | -V, --version Prints version information 151 | 152 | SUBCOMMANDS: 153 | help Prints this message or the help of the given subcommand(s) 154 | list List all interfaces. 155 | run Start capturing packets. 156 | ``` 157 | ```zsh 158 | USAGE: 159 | snoopy capture run [FLAGS] [OPTIONS] 160 | 161 | FLAGS: 162 | -h, --help Prints help information 163 | -p, --promisc Set promiscuous mode on or off. By default, this is off. 164 | -r, --rfmon Set rfmon mode on or off. The default is maintained by pcap. 165 | -V, --version Prints version information 166 | 167 | OPTIONS: 168 | -b, --buffer_size Set the buffer size for incoming packet data. The default is 1000000. This should 169 | always be larger than the snaplen. 170 | --handle Specify the device interface 171 | -f, --filter Set filter to the capture using the given BPF program string. 172 | --precision Set the time stamp precision returned in captures (Micro/Nano). 173 | --savefile Save the captured packets to file. 174 | -s, --snaplen Set the snaplen size (the maximum length of a packet captured into the buffer). 175 | Useful if you only want certain headers, but not the entire packet.The default is 176 | 65535. 177 | -t, --timeout Set the read timeout for the Capture. By default, this is 0, so it will block 178 | indefinitely. 179 | --tstamp_type Set the time stamp type to be used by a capture device (Host / HostLowPrec / 180 | HostHighPrec / Adapter / AdapterUnsynced). 181 | 182 | ``` 183 | ```zsh 184 | USAGE: 185 | snoopy parse [OPTIONS] 186 | 187 | FLAGS: 188 | -h, --help Prints help information 189 | -V, --version Prints version information 190 | 191 | OPTIONS: 192 | -f, --filter Set filter to the capture using the given BPF program string. 193 | -s, --savefile Parse the packets into JSON and save them to memory. 194 | 195 | ARGS: 196 | 197 | ``` 198 | 199 | > Note: The filters can be defined according to the syntax specified [here](http://biot.com/capstats/bpf.html). 200 | 201 | ## Docker 202 | 203 | Run the following commands in the command line inside the folder : 204 | 205 | ```zsh 206 | docker build -t snoopy . 207 | docker container run -it snoopy 208 | ``` 209 | 210 | ## Build 211 | 212 | Run the following command in the command line inside the folder : 213 | 214 | ```zsh 215 | cargo build 216 | ``` 217 | 218 | ## Todo 219 | 220 | * Benchmarking 221 | * Support for other protocols 222 | 223 | ## License 224 | 225 | This project is under the MIT license. -------------------------------------------------------------------------------- /Sample/sample.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kanishkarj/snoopy/1223d3f8847ed2123a926c68e5947319eb0e0119/Sample/sample.pcap -------------------------------------------------------------------------------- /src/args/capture.rs: -------------------------------------------------------------------------------- 1 | use crate::lib::packet_capture::PacketCapture; 2 | use clap::{App, Arg, ArgMatches, SubCommand}; 3 | use pcap::{Capture, Inactive, Precision, TimestampType}; 4 | use std::cell::RefCell; 5 | 6 | fn is_tstamp_type(val: String) -> Result<(), String> { 7 | let domain_set = vec![ 8 | "Host", 9 | "HostLowPrec", 10 | "HostHighPrec", 11 | "Adapter", 12 | "AdapterUnsynced", 13 | ]; 14 | if domain_set.contains(&&val[..]) { 15 | Ok(()) 16 | } else { 17 | Err(format!("The value must be one of {:?}", domain_set)) 18 | } 19 | } 20 | 21 | fn is_i32(val: String) -> Result<(), String> { 22 | match val.parse::() { 23 | Ok(_) => Ok(()), 24 | Err(err) => Err(err.to_string()), 25 | } 26 | } 27 | 28 | fn is_precision_type(val: String) -> Result<(), String> { 29 | let domain_set = vec!["Micro", "Nano"]; 30 | if domain_set.contains(&&val[..]) { 31 | Ok(()) 32 | } else { 33 | Err(format!("The value must be one of {:?}", domain_set)) 34 | } 35 | } 36 | 37 | pub struct CaptureSubcommand {} 38 | 39 | impl<'a, 'b> CaptureSubcommand { 40 | pub fn new() -> CaptureSubcommand { 41 | CaptureSubcommand {} 42 | } 43 | 44 | pub fn get_subcommand(&self) -> App<'a, 'b> { 45 | let run_args = vec![ 46 | Arg::with_name("device_handle") 47 | .help("Specify the device interface") 48 | .takes_value(true) 49 | .long("handle"), 50 | Arg::with_name("timeout") 51 | .help("Set the read timeout for the Capture. By default, this is 0, so it will block indefinitely.") 52 | .takes_value(true) 53 | .short("t") 54 | .long("timeout") 55 | .validator(is_i32), 56 | Arg::with_name("promisc") 57 | .help("Set promiscuous mode on or off. By default, this is off.") 58 | .short("p") 59 | .long("promisc"), 60 | Arg::with_name("rfmon") 61 | .help("Set rfmon mode on or off. The default is maintained by pcap.") 62 | .short("r") 63 | .long("rfmon"), 64 | Arg::with_name("buffer_size") 65 | .help("Set the buffer size for incoming packet data. The default is 1000000. This should always be larger than the snaplen.") 66 | .takes_value(true) 67 | .short("b") 68 | .long("buffer_size") 69 | .validator(is_i32), 70 | Arg::with_name("snaplen") 71 | .help("Set the snaplen size (the maximum length of a packet captured into the buffer). \ 72 | Useful if you only want certain headers, but not the entire packet.The default is 65535.") 73 | .takes_value(true) 74 | .short("s") 75 | .long("snaplen") 76 | .validator(is_i32), 77 | Arg::with_name("precision") 78 | .help("Set the time stamp precision returned in captures (Micro/Nano).") 79 | .takes_value(true) 80 | .long("precision") 81 | .validator(is_precision_type), 82 | Arg::with_name("tstamp_type") 83 | .help("Set the time stamp type to be used by a capture device \ 84 | (Host / HostLowPrec / HostHighPrec / Adapter / AdapterUnsynced).") 85 | .takes_value(true) 86 | .long("tstamp_type") 87 | .validator(is_tstamp_type), 88 | Arg::with_name("filter") 89 | .help("Set filter to the capture using the given BPF program string.") 90 | .takes_value(true) 91 | .long("filter") 92 | .short("f"), 93 | Arg::with_name("savefile") 94 | .help("Save the captured packets to file.") 95 | .takes_value(true) 96 | .long("savefile") 97 | ]; 98 | 99 | SubCommand::with_name("capture") 100 | .about("Capture packets from interfaces.") 101 | .subcommand(SubCommand::with_name("list").about("List all interfaces.")) 102 | .subcommand( 103 | SubCommand::with_name("run") 104 | .about("Start capturing packets.") 105 | .args(&run_args), 106 | ) 107 | } 108 | 109 | pub fn run_args( 110 | &self, 111 | device: RefCell>, 112 | args: &ArgMatches, 113 | ) -> RefCell> { 114 | let mut device = device.into_inner(); 115 | // the validators will ensure we are passing the proper type, hence using unwrap is not a problem. 116 | if let Some(val) = args.value_of("timeout") { 117 | device = device.timeout(val.parse().unwrap()); 118 | } 119 | if let Some(val) = args.value_of("promisc") { 120 | device = device.promisc(val.parse().unwrap()); 121 | } 122 | if let Some(val) = args.value_of("rfmon") { 123 | device = device.rfmon(val.parse().unwrap()); 124 | } 125 | if let Some(val) = args.value_of("buffer_size") { 126 | device = device.buffer_size(val.parse().unwrap()); 127 | } 128 | if let Some(val) = args.value_of("snaplen") { 129 | device = device.snaplen(val.parse().unwrap()); 130 | } 131 | if let Some(val) = args.value_of("precision") { 132 | device = device.precision(self.get_precision_type(val).unwrap()); 133 | } 134 | if let Some(val) = args.value_of("tstamp_type") { 135 | device = device.tstamp_type(self.get_tstamp_type(val).unwrap()); 136 | } 137 | if let Some(val) = args.value_of("tstamp_type") { 138 | device = device.tstamp_type(self.get_tstamp_type(val).unwrap()); 139 | } 140 | RefCell::new(device) 141 | } 142 | 143 | pub fn start(&self, device: RefCell>, args: &ArgMatches) { 144 | let device = device.into_inner(); 145 | let mut packet_capture = PacketCapture::new(); 146 | 147 | match device.open() { 148 | Ok(mut cap_handle) => { 149 | // Set pacp capture filters 150 | if let Some(val) = args.value_of("filter") { 151 | cap_handle 152 | .filter(val) 153 | .expect("Filters invalid, please check the documentation."); 154 | } 155 | 156 | // To select between saving to file and printing to console. 157 | if let Some(val) = args.value_of("savefile") { 158 | packet_capture.save_to_file(cap_handle, val); 159 | } else { 160 | packet_capture.print_to_console(cap_handle); 161 | } 162 | } 163 | Err(err) => { 164 | eprintln!("{:?}", err); 165 | } 166 | } 167 | } 168 | 169 | fn get_precision_type(&self, val: &str) -> Result { 170 | match val { 171 | "Micro" => Ok(Precision::Nano), 172 | "Nano" => Ok(Precision::Nano), 173 | _ => Err(()), 174 | } 175 | } 176 | 177 | fn get_tstamp_type(&self, val: &str) -> Result { 178 | match val { 179 | "Host" => Ok(TimestampType::Host), 180 | "HostLowPrec" => Ok(TimestampType::HostLowPrec), 181 | "HostHighPrec" => Ok(TimestampType::HostHighPrec), 182 | "Adapter" => Ok(TimestampType::Adapter), 183 | "AdapterUnsynced" => Ok(TimestampType::AdapterUnsynced), 184 | _ => Err(()), 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/args/mod.rs: -------------------------------------------------------------------------------- 1 | mod capture; 2 | mod parse; 3 | 4 | use clap::{crate_authors, crate_description, crate_name, crate_version, App}; 5 | use pcap::{Capture, Device}; 6 | 7 | use crate::args::capture::CaptureSubcommand; 8 | use crate::args::parse::ParseSubcommand; 9 | use crate::lib::packet_capture::PacketCapture; 10 | use std::cell::RefCell; 11 | 12 | fn print_default_device(name: String) { 13 | println!("{:-^1$}", "-", 20,); 14 | println!("Sniffing {}", name); 15 | println!("{:-^1$} \n\n", "-", 20,); 16 | } 17 | 18 | pub fn parse_cli_args() { 19 | let capture_subcommand = CaptureSubcommand::new(); 20 | let parse_subcommand = ParseSubcommand::new(); 21 | 22 | let matches = App::new(crate_name!()) 23 | .version(crate_version!()) 24 | .author(crate_authors!()) 25 | .about(crate_description!()) 26 | .subcommand(capture_subcommand.get_subcommand()) 27 | .subcommand(parse_subcommand.get_subcommand()) 28 | .get_matches(); 29 | 30 | if let Some(sub) = matches.subcommand_matches("capture") { 31 | if sub.subcommand_matches("list").is_some() { 32 | if let Err(err) = PacketCapture::list_devices() { 33 | eprintln!("{}", err.to_string()) 34 | } 35 | } else if let Some(run_args) = sub.subcommand_matches("run") { 36 | let device; 37 | 38 | match run_args.value_of("device_handle") { 39 | Some(handle) => { 40 | device = Capture::from_device(handle); 41 | } 42 | None => { 43 | let capture_device = Device::lookup().unwrap(); 44 | print_default_device(capture_device.name.clone()); 45 | device = Capture::from_device(capture_device); 46 | } 47 | } 48 | 49 | match device { 50 | Ok(device) => { 51 | let device = RefCell::new(device); 52 | let device = capture_subcommand.run_args(device, run_args); 53 | capture_subcommand.start(device, run_args); 54 | } 55 | Err(err) => { 56 | eprintln!("{}", err.to_string()); 57 | } 58 | } 59 | } 60 | }; 61 | 62 | if let Some(args) = matches.subcommand_matches("parse") { 63 | parse_subcommand.start(args); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/args/parse.rs: -------------------------------------------------------------------------------- 1 | use crate::lib::packet_capture::PacketCapture; 2 | use clap::{App, Arg, ArgMatches, SubCommand}; 3 | 4 | pub struct ParseSubcommand {} 5 | 6 | impl<'a, 'b> ParseSubcommand { 7 | pub fn new() -> ParseSubcommand { 8 | ParseSubcommand {} 9 | } 10 | 11 | pub fn get_subcommand(&self) -> App<'a, 'b> { 12 | let parse_args = vec![ 13 | Arg::with_name("file_name").required(true), 14 | Arg::with_name("savefile") 15 | .help("Parse the packets into JSON and save them to memory.") 16 | .takes_value(true) 17 | .short("s") 18 | .long("savefile"), 19 | Arg::with_name("filter") 20 | .help("Set filter to the capture using the given BPF program string.") 21 | .takes_value(true) 22 | .long("filter") 23 | .short("f"), 24 | ]; 25 | 26 | SubCommand::with_name("parse") 27 | .about("Parse pcap files.") 28 | .args(&parse_args) 29 | } 30 | 31 | pub fn start(&self, args: &ArgMatches) { 32 | let mut save_file_path = None; 33 | let mut packet_capture = PacketCapture::new(); 34 | let mut filter = None; 35 | 36 | if let Some(val) = args.value_of("filter") { 37 | filter = Some(val.to_string()); 38 | } 39 | if let Some(val) = args.value_of("savefile") { 40 | save_file_path = Some(val); 41 | } 42 | if let Some(val) = args.value_of("file_name") { 43 | packet_capture.parse_from_file(val, save_file_path, filter); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod packet_capture; 2 | pub mod packet_parse; 3 | -------------------------------------------------------------------------------- /src/lib/packet_capture.rs: -------------------------------------------------------------------------------- 1 | use crate::lib::packet_parse::{PacketHeader, PacketParse, ParsedPacket}; 2 | use pcap::{Active, Capture, Device}; 3 | use std::fs; 4 | use std::net::IpAddr; 5 | use std::sync::{Arc, Mutex}; 6 | use threadpool::ThreadPool; 7 | 8 | pub struct PacketCapture { 9 | err_count: u64, 10 | } 11 | 12 | impl PacketCapture { 13 | pub fn new() -> PacketCapture { 14 | PacketCapture { err_count: 0 } 15 | } 16 | 17 | pub fn list_devices() -> Result<(), pcap::Error> { 18 | let devices: Vec = Device::list()?.iter().map(|val| val.name.clone()).collect(); 19 | println!("All Interfaces : "); 20 | devices.iter().for_each(|val| println!("* {}", val)); 21 | Ok(()) 22 | } 23 | 24 | fn print_err(&mut self, err: String) { 25 | self.err_count += 1; 26 | eprintln!("ERROR {} : {}", self.err_count, err); 27 | } 28 | 29 | pub fn save_to_file(&mut self, mut cap_handle: Capture, file_name: &str) { 30 | match cap_handle.savefile(&file_name) { 31 | Ok(mut file) => { 32 | while let Ok(packet) = cap_handle.next() { 33 | file.write(&packet); 34 | } 35 | } 36 | Err(err) => { 37 | self.print_err(err.to_string()); 38 | } 39 | } 40 | } 41 | 42 | pub fn print_to_console(&mut self, mut cap_handle: Capture) { 43 | self.print_headers(); 44 | 45 | while let Ok(packet) = cap_handle.next() { 46 | let data = packet.data.to_owned(); 47 | let len = packet.header.len; 48 | let ts: String = format!( 49 | "{}.{:06}", 50 | &packet.header.ts.tv_sec, &packet.header.ts.tv_usec 51 | ); 52 | 53 | let packet_parse = PacketParse::new(); 54 | let parsed_packet = packet_parse.parse_packet(data, len, ts); 55 | match parsed_packet { 56 | Ok(parsed_packet) => { 57 | self.print_packet(&parsed_packet); 58 | } 59 | Err(err) => { 60 | self.print_err(err.to_string()); 61 | } 62 | } 63 | } 64 | } 65 | 66 | fn print_headers(&self) { 67 | println!( 68 | "{0: <25} | {1: <15} | {2: <25} | {3: <15} | {4: <15} | {5: <15} | {6: <35} |", 69 | "Source IP", "Source Port", "Dest IP", "Dest Port", "Protocol", "Length", "Timestamp" 70 | ); 71 | println!("{:-^1$}", "-", 165,); 72 | } 73 | 74 | fn get_packet_meta(&self, parsed_packet: &ParsedPacket) -> (String, String, String, String) { 75 | let mut src_addr = "".to_string(); 76 | let mut dst_addr = "".to_string(); 77 | let mut src_port = "".to_string(); 78 | let mut dst_port = "".to_string(); 79 | 80 | parsed_packet.headers.iter().for_each(|pack| { 81 | match pack { 82 | PacketHeader::Tcp(packet) => { 83 | src_port = packet.source_port.to_string(); 84 | dst_port = packet.dest_port.to_string(); 85 | } 86 | PacketHeader::Udp(packet) => { 87 | src_port = packet.source_port.to_string(); 88 | dst_port = packet.dest_port.to_string(); 89 | } 90 | PacketHeader::Ipv4(packet) => { 91 | src_addr = IpAddr::V4(packet.source_addr).to_string(); 92 | dst_addr = IpAddr::V4(packet.dest_addr).to_string(); 93 | } 94 | PacketHeader::Ipv6(packet) => { 95 | src_addr = IpAddr::V6(packet.source_addr).to_string(); 96 | dst_addr = IpAddr::V6(packet.dest_addr).to_string(); 97 | } 98 | PacketHeader::Arp(packet) => { 99 | src_addr = packet.src_addr.to_string(); 100 | dst_addr = packet.dest_addr.to_string(); 101 | } 102 | _ => {} 103 | }; 104 | }); 105 | 106 | (src_addr, src_port, dst_addr, dst_port) 107 | } 108 | 109 | fn print_packet(&self, parsed_packet: &ParsedPacket) { 110 | let (src_addr, src_port, dst_addr, dst_port) = self.get_packet_meta(&parsed_packet); 111 | let protocol = &parsed_packet.headers[0].to_string(); 112 | let length = &parsed_packet.len; 113 | let ts = &parsed_packet.timestamp; 114 | println!( 115 | "{0: <25} | {1: <15} | {2: <25} | {3: <15} | {4: <15} | {5: <15} | {6: <35}", 116 | src_addr, src_port, dst_addr, dst_port, protocol, length, ts 117 | ); 118 | } 119 | 120 | pub fn parse_from_file( 121 | &mut self, 122 | file_name: &str, 123 | save_file_path: Option<&str>, 124 | filter: Option, 125 | ) { 126 | let pool = ThreadPool::new(num_cpus::get() * 2); 127 | match Capture::from_file(file_name) { 128 | Ok(mut cap_handle) => { 129 | let packets = Arc::new(Mutex::new(Vec::new())); 130 | 131 | if let Some(filter) = filter { 132 | cap_handle 133 | .filter(&filter) 134 | .expect("Filters invalid, please check the documentation."); 135 | } 136 | 137 | while let Ok(packet) = cap_handle.next() { 138 | let data = packet.data.to_owned(); 139 | let len = packet.header.len; 140 | let ts: String = format!( 141 | "{}.{:06}", 142 | &packet.header.ts.tv_sec, &packet.header.ts.tv_usec 143 | ); 144 | 145 | let packets = packets.clone(); 146 | 147 | pool.execute(move || { 148 | let packet_parse = PacketParse::new(); 149 | let parsed_packet = packet_parse.parse_packet(data, len, ts); 150 | 151 | packets.lock().unwrap().push(parsed_packet); 152 | }); 153 | } 154 | 155 | pool.join(); 156 | 157 | if let Some(path) = save_file_path { 158 | let mut parsed_packets = vec![]; 159 | 160 | let packets = packets.lock().unwrap(); 161 | let packets = &*packets; 162 | packets.iter().for_each(|pkt| match pkt { 163 | Ok(pkt) => { 164 | parsed_packets.push(pkt); 165 | } 166 | Err(err) => { 167 | self.print_err(err.to_string()); 168 | } 169 | }); 170 | 171 | let packets = serde_json::to_string(&packets).unwrap(); 172 | fs::write(path, packets).unwrap(); 173 | } else { 174 | let packets = packets.lock().unwrap(); 175 | 176 | self.print_headers(); 177 | 178 | packets.iter().for_each(|pack| match pack { 179 | Ok(pack) => { 180 | self.print_packet(pack); 181 | } 182 | Err(err) => { 183 | self.print_err(err.to_string()); 184 | } 185 | }) 186 | } 187 | } 188 | Err(err) => { 189 | self.print_err(err.to_string()); 190 | } 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/lib/packet_parse.rs: -------------------------------------------------------------------------------- 1 | use pktparse::arp::ArpPacket; 2 | use pktparse::ethernet::{EtherType, EthernetFrame}; 3 | use pktparse::ip::IPProtocol; 4 | use pktparse::ipv4::IPv4Header; 5 | use pktparse::ipv6::IPv6Header; 6 | use pktparse::tcp::TcpHeader; 7 | use pktparse::udp::UdpHeader; 8 | use pktparse::*; 9 | use std::string::ToString; 10 | use tls_parser::TlsMessage; 11 | 12 | use serde::{Deserialize, Serialize}; 13 | 14 | pub struct PacketParse {} 15 | 16 | #[derive(Debug, Serialize, Deserialize, PartialEq)] 17 | pub enum PacketHeader { 18 | Tls(TlsType), 19 | Dns(DnsPacket), 20 | Tcp(TcpHeader), 21 | Udp(UdpHeader), 22 | Ipv4(IPv4Header), 23 | Ipv6(IPv6Header), 24 | Ether(EthernetFrame), 25 | Arp(ArpPacket), 26 | } 27 | 28 | #[derive(Debug, Serialize, Deserialize)] 29 | pub struct ParsedPacket { 30 | pub len: u32, 31 | pub timestamp: String, 32 | pub headers: Vec, 33 | pub remaining: Vec, 34 | } 35 | 36 | impl ParsedPacket { 37 | pub fn new() -> ParsedPacket { 38 | ParsedPacket { 39 | len: 0, 40 | timestamp: "".to_string(), 41 | headers: vec![], 42 | remaining: vec![], 43 | } 44 | } 45 | } 46 | 47 | #[derive(Debug, Serialize, Deserialize, PartialEq)] 48 | pub enum TlsType { 49 | Handshake, 50 | ChangeCipherSpec, 51 | Alert, 52 | ApplicationData, 53 | Heartbeat, 54 | EncryptedData, 55 | } 56 | 57 | #[derive(Debug, Serialize, Deserialize, PartialEq)] 58 | pub struct DnsPacket { 59 | questions: Vec, 60 | answers: Vec, 61 | } 62 | 63 | impl From> for DnsPacket { 64 | fn from(dns_packet: dns_parser::Packet) -> Self { 65 | let questions: Vec = dns_packet 66 | .questions 67 | .iter() 68 | .map(|q| q.qname.to_string()) 69 | .collect(); 70 | let answers: Vec = dns_packet 71 | .answers 72 | .iter() 73 | .map(|a| a.name.to_string()) 74 | .collect(); 75 | Self { questions, answers } 76 | } 77 | } 78 | 79 | impl ToString for PacketHeader { 80 | fn to_string(&self) -> String { 81 | match self { 82 | PacketHeader::Ipv4(_) => String::from("Ipv4"), 83 | PacketHeader::Ipv6(_) => String::from("Ipv6"), 84 | PacketHeader::Dns(_) => String::from("Dns"), 85 | PacketHeader::Tls(_) => String::from("Tls"), 86 | PacketHeader::Tcp(_) => String::from("Tcp"), 87 | PacketHeader::Udp(_) => String::from("Udp"), 88 | PacketHeader::Ether(_) => String::from("Ether"), 89 | PacketHeader::Arp(_) => String::from("Arp"), 90 | } 91 | } 92 | } 93 | 94 | impl PacketParse { 95 | pub fn new() -> PacketParse { 96 | PacketParse {} 97 | } 98 | 99 | pub fn parse_packet( 100 | &self, 101 | data: Vec, 102 | len: u32, 103 | ts: String, 104 | ) -> Result { 105 | let mut parsed_packet = self.parse_link_layer(&data)?; 106 | parsed_packet.len = len; 107 | parsed_packet.timestamp = ts; 108 | Ok(parsed_packet) 109 | } 110 | 111 | pub fn parse_link_layer(&self, content: &[u8]) -> Result { 112 | let mut pack = ParsedPacket::new(); 113 | match ethernet::parse_ethernet_frame(content) { 114 | Ok((content, headers)) => { 115 | match headers.ethertype { 116 | EtherType::IPv4 => { 117 | self.parse_ipv4(content, &mut pack)?; 118 | } 119 | EtherType::IPv6 => { 120 | self.parse_ipv6(content, &mut pack)?; 121 | } 122 | EtherType::ARP => { 123 | self.parse_arp(content, &mut pack)?; 124 | } 125 | _ => { 126 | pack.remaining = content.to_owned(); 127 | } 128 | } 129 | pack.headers.push(PacketHeader::Ether(headers)); 130 | } 131 | Err(_) => { 132 | pack.remaining = content.to_owned(); 133 | } 134 | } 135 | Ok(pack) 136 | } 137 | 138 | pub fn parse_ipv4( 139 | &self, 140 | content: &[u8], 141 | parsed_packet: &mut ParsedPacket, 142 | ) -> Result<(), String> { 143 | match ipv4::parse_ipv4_header(content) { 144 | Ok((content, headers)) => { 145 | self.parse_transport_layer(&headers.protocol, content, parsed_packet)?; 146 | parsed_packet.headers.push(PacketHeader::Ipv4(headers)); 147 | Ok(()) 148 | } 149 | Err(err) => { 150 | parsed_packet.remaining = content.to_owned(); 151 | Err(err.to_string()) 152 | } 153 | } 154 | } 155 | 156 | pub fn parse_ipv6( 157 | &self, 158 | content: &[u8], 159 | parsed_packet: &mut ParsedPacket, 160 | ) -> Result<(), String> { 161 | match ipv6::parse_ipv6_header(content) { 162 | Ok((content, headers)) => { 163 | self.parse_transport_layer(&headers.next_header, content, parsed_packet)?; 164 | parsed_packet.headers.push(PacketHeader::Ipv6(headers)); 165 | Ok(()) 166 | } 167 | Err(err) => { 168 | parsed_packet.remaining = content.to_owned(); 169 | Err(err.to_string()) 170 | } 171 | } 172 | } 173 | 174 | fn parse_transport_layer( 175 | &self, 176 | protocol: &ip::IPProtocol, 177 | content: &[u8], 178 | parsed_packet: &mut ParsedPacket, 179 | ) -> Result<(), String> { 180 | match protocol { 181 | IPProtocol::UDP => { 182 | self.parse_udp(content, parsed_packet)?; 183 | Ok(()) 184 | } 185 | IPProtocol::TCP => { 186 | self.parse_tcp(content, parsed_packet)?; 187 | Ok(()) 188 | } 189 | _ => { 190 | parsed_packet.remaining = content.to_owned(); 191 | Err("Neither TCP nor UDP".to_string()) 192 | } 193 | } 194 | } 195 | 196 | fn parse_tcp(&self, content: &[u8], parsed_packet: &mut ParsedPacket) -> Result<(), String> { 197 | match tcp::parse_tcp_header(content) { 198 | Ok((content, headers)) => { 199 | self.parse_tls(content, parsed_packet); 200 | parsed_packet.headers.push(PacketHeader::Tcp(headers)); 201 | Ok(()) 202 | } 203 | Err(err) => { 204 | parsed_packet.remaining = content.to_owned(); 205 | Err(err.to_string()) 206 | } 207 | } 208 | } 209 | 210 | fn parse_udp(&self, content: &[u8], parsed_packet: &mut ParsedPacket) -> Result<(), String> { 211 | match udp::parse_udp_header(content) { 212 | Ok((content, headers)) => { 213 | self.parse_dns(content, parsed_packet); 214 | parsed_packet.headers.push(PacketHeader::Udp(headers)); 215 | Ok(()) 216 | } 217 | Err(err) => { 218 | parsed_packet.remaining = content.to_owned(); 219 | Err(err.to_string()) 220 | } 221 | } 222 | } 223 | 224 | fn parse_arp(&self, content: &[u8], parsed_packet: &mut ParsedPacket) -> Result<(), String> { 225 | match arp::parse_arp_pkt(content) { 226 | Ok((_content, headers)) => { 227 | parsed_packet.headers.push(PacketHeader::Arp(headers)); 228 | Ok(()) 229 | } 230 | Err(err) => { 231 | parsed_packet.remaining = content.to_owned(); 232 | Err(err.to_string()) 233 | } 234 | } 235 | } 236 | 237 | fn parse_dns(&self, content: &[u8], parsed_packet: &mut ParsedPacket) { 238 | match dns_parser::Packet::parse(content) { 239 | Ok(packet) => { 240 | parsed_packet 241 | .headers 242 | .push(PacketHeader::Dns(DnsPacket::from(packet))); 243 | } 244 | Err(_) => { 245 | parsed_packet.remaining = content.to_owned(); 246 | } 247 | } 248 | } 249 | 250 | fn parse_tls(&self, content: &[u8], parsed_packet: &mut ParsedPacket) { 251 | if let Ok((_content, headers)) = tls_parser::parse_tls_plaintext(content) { 252 | if let Some(msg) = headers.msg.get(0) { 253 | match msg { 254 | TlsMessage::Handshake(_) => { 255 | parsed_packet 256 | .headers 257 | .push(PacketHeader::Tls(TlsType::Handshake)); 258 | } 259 | TlsMessage::ApplicationData(app_data) => { 260 | parsed_packet 261 | .headers 262 | .push(PacketHeader::Tls(TlsType::ApplicationData)); 263 | parsed_packet.remaining = app_data.blob.to_owned(); 264 | } 265 | TlsMessage::Heartbeat(_) => { 266 | parsed_packet 267 | .headers 268 | .push(PacketHeader::Tls(TlsType::Heartbeat)); 269 | } 270 | TlsMessage::ChangeCipherSpec => { 271 | parsed_packet 272 | .headers 273 | .push(PacketHeader::Tls(TlsType::ChangeCipherSpec)); 274 | } 275 | TlsMessage::Alert(_) => { 276 | parsed_packet 277 | .headers 278 | .push(PacketHeader::Tls(TlsType::Alert)); 279 | } 280 | } 281 | } 282 | } else if let Ok((_content, headers)) = tls_parser::parse_tls_encrypted(content) { 283 | parsed_packet 284 | .headers 285 | .push(PacketHeader::Tls(TlsType::EncryptedData)); 286 | parsed_packet.remaining = headers.msg.blob.to_owned(); 287 | } else { 288 | parsed_packet.remaining = content.to_owned(); 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod args; 2 | mod lib; 3 | 4 | use args::parse_cli_args; 5 | 6 | fn main() { 7 | parse_cli_args(); 8 | } 9 | --------------------------------------------------------------------------------